]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Add the sqlite3changeset_apply() function. Does not yet handle all cases.
authordan <dan@noemail.net>
Fri, 11 Mar 2011 19:05:52 +0000 (19:05 +0000)
committerdan <dan@noemail.net>
Fri, 11 Mar 2011 19:05:52 +0000 (19:05 +0000)
FossilOrigin-Name: 2b19be7bf753c7dd12e1c3b384981a3ea1bc8145

ext/session/sqlite3session.c
ext/session/sqlite3session.h
ext/session/test_session.c
manifest
manifest.uuid
test/session1.test

index 0b6a985279378dd4af6d1d4d3efd91adc112341f..86778d65f0bb23e9553b2a5d903fce0031f44bf1 100644 (file)
@@ -573,6 +573,47 @@ static void sessionAppendBlob(
   }
 }
 
+static void sessionAppendStr(
+  SessionBuffer *p, 
+  const char *zStr, 
+  int *pRc
+){
+  int nStr = strlen(zStr);
+  if( *pRc==SQLITE_OK && 0==sessionBufferGrow(p, nStr, pRc) ){
+    memcpy(&p->aBuf[p->nBuf], zStr, nStr);
+    p->nBuf += nStr;
+  }
+}
+
+static void sessionAppendInteger(
+  SessionBuffer *p, 
+  int iVal,
+  int *pRc
+){
+  char aBuf[24];
+  sqlite3_snprintf(sizeof(aBuf)-1, aBuf, "%d", iVal);
+  sessionAppendStr(p, aBuf, pRc);
+}
+
+static void sessionAppendIdent(
+  SessionBuffer *p, 
+  const char *zStr, 
+  int *pRc
+){
+  int nStr = strlen(zStr)*2 + 2 + 1;
+  if( *pRc==SQLITE_OK && 0==sessionBufferGrow(p, nStr, pRc) ){
+    char *zOut = (char *)&p->aBuf[p->nBuf];
+    const char *zIn = zStr;
+    *zOut++ = '"';
+    while( *zIn ){
+      if( *zIn=='"' ) *zOut++ = '"';
+      *zOut++ = *(zIn++);
+    }
+    *zOut++ = '"';
+    p->nBuf = ((u8 *)zOut - p->aBuf);
+  }
+}
+
 static void sessionAppendCol(
   SessionBuffer *p, 
   sqlite3_stmt *pStmt, 
@@ -616,6 +657,7 @@ static void sessionAppendUpdate(
   sqlite3_stmt *pStmt, 
   SessionBuffer *pBuf,
   SessionChange *p,
+  u8 *abPK,
   int *pRc
 ){
   if( *pRc==SQLITE_OK ){
@@ -667,6 +709,9 @@ static void sessionAppendUpdate(
           nCopy = nAdvance;
         }
       }
+      if( abPK[i] ){
+        nCopy = nAdvance;
+      }
 
       if( nCopy==0 ){
         sessionAppendByte(pBuf, 0, pRc);
@@ -686,8 +731,98 @@ static void sessionAppendUpdate(
       sqlite3_free(buf2.aBuf);
     }
   }
+}
 
+static int sessionTableInfo(
+  sqlite3 *db,                    /* Database connection */
+  const char *zThis,              /* Table name */
+  int nCol,                       /* Expected number of columns */
+  const char **pzTab,             /* OUT: Copy of zThis */
+  const char ***pazCol,           /* OUT: Array of column names for table */
+  u8 **pabPK                      /* OUT: Array of booleans - true for PK col */
+){
+  char *zPragma;
+  sqlite3_stmt *pStmt;
+  int rc;
+  int nByte;
+  int nDbCol = 0;
+  int nThis;
+  int i;
+  u8 *pAlloc;
+  u8 *pFree = 0;
+
+  char **azCol;
+  u8 *abPK;
+
+  nThis = strlen(zThis);
+  zPragma = sqlite3_mprintf("PRAGMA main.table_info('%q')", zThis);
+  if( !zPragma ) return SQLITE_NOMEM;
+
+  rc = sqlite3_prepare_v2(db, zPragma, -1, &pStmt, 0);
+  sqlite3_free(zPragma);
+  if( rc!=SQLITE_OK ) return rc;
+
+  nByte = nThis + 1;
+  while( SQLITE_ROW==sqlite3_step(pStmt) ){
+    nByte += sqlite3_column_bytes(pStmt, 1);
+    nDbCol++;
+  }
+  rc = sqlite3_reset(pStmt);
+
+  if( nDbCol!=nCol ){
+    rc = SQLITE_SCHEMA;
+  }
+  if( rc==SQLITE_OK ){
+    nByte += nDbCol * (sizeof(const char *) + sizeof(u8) + 1);
+    pAlloc = sqlite3_malloc(nByte);
+    if( pAlloc==0 ){
+      rc = SQLITE_NOMEM;
+    }
+  }
+  if( rc==SQLITE_OK ){
+    pFree = pAlloc;
+    if( pazCol ){
+      azCol = (char **)pAlloc;
+      pAlloc = (u8 *)&azCol[nCol];
+    }
+    if( pabPK ){
+      abPK = (u8 *)pAlloc;
+      pAlloc = &abPK[nCol];
+    }
+    if( pzTab ){
+      memcpy(pAlloc, zThis, nThis+1);
+      *pzTab = (char *)pAlloc;
+      pAlloc += nThis+1;
+    }
+  
+    i = 0;
+    while( SQLITE_ROW==sqlite3_step(pStmt) ){
+      int nName = sqlite3_column_bytes(pStmt, 1);
+      const unsigned char *zName = sqlite3_column_text(pStmt, 1);
+      if( zName==0 ) break;
+      if( pazCol ){
+        memcpy(pAlloc, zName, nName+1);
+        azCol[i] = (char *)pAlloc;
+        pAlloc += nName+1;
+      }
+      if( pabPK ) abPK[i] = sqlite3_column_int(pStmt, 5);
+      i++;
+    }
+    rc = sqlite3_reset(pStmt);
+  
+  }
+  if( rc==SQLITE_OK ){
+    if( pazCol ) *pazCol = (const char **)azCol;
+    if( pabPK ) *pabPK = abPK;
+  }else{
+    if( pazCol ) *pazCol = 0;
+    if( pabPK ) *pabPK = 0;
+    if( pzTab ) *pzTab = 0;
+    sqlite3_free(pFree);
+  }
 
+  sqlite3_finalize(pStmt);
+  return rc;
 }
 
 /*
@@ -717,6 +852,7 @@ int sqlite3session_changeset(
       sqlite3_stmt *pStmt = 0;
       int bNoop = 1;
       int nRewind = buf.nBuf;
+      u8 *abPK = 0;
 
       /* Write a table header */
       sessionAppendByte(&buf, 'T', &rc);
@@ -740,6 +876,10 @@ int sqlite3session_changeset(
         rc = SQLITE_SCHEMA;
       }
 
+      if( rc==SQLITE_OK ){
+        rc = sessionTableInfo(db, pTab->zName, pTab->nCol, 0, 0, &abPK);
+      }
+
       for(i=0; i<pTab->nChange; i++){
         SessionChange *p;
         for(p=pTab->apChange[i]; rc==SQLITE_OK && p; p=p->pNext){
@@ -747,7 +887,7 @@ int sqlite3session_changeset(
           if( sqlite3_step(pStmt)==SQLITE_ROW ){
             int iCol;
             if( p->aRecord ){
-              sessionAppendUpdate(pStmt, &buf, p, &rc);
+              sessionAppendUpdate(pStmt, &buf, p, abPK, &rc);
             }else{
               sessionAppendByte(&buf, SQLITE_INSERT, &rc);
               for(iCol=0; iCol<pTab->nCol; iCol++){
@@ -766,6 +906,7 @@ int sqlite3session_changeset(
       }
 
       sqlite3_finalize(pStmt);
+      sqlite3_free(abPK);
 
       if( bNoop ){
         buf.nBuf = nRewind;
@@ -797,6 +938,7 @@ struct sqlite3_changeset_iter {
   u8 *pNext;                      /* Pointer to next change within aChangeset */
   int rc;
 
+  sqlite3_stmt *pConflict;        /* Conflicting row, if any */
   char *zTab;                     /* Current table */
   int nCol;                       /* Number of columns in zTab */
   int op;                         /* Current operation */
@@ -947,9 +1089,9 @@ int sqlite3changeset_next(sqlite3_changeset_iter *p){
 */
 int sqlite3changeset_op(
   sqlite3_changeset_iter *pIter,
-  const char **pzTab,                 /* OUT: Pointer to table name */
-  int *pnCol,                         /* OUT: Number of columns in table */
-  int *pOp                            /* OUT: SQLITE_INSERT, DELETE or UPDATE */
+  const char **pzTab,             /* OUT: Pointer to table name */
+  int *pnCol,                     /* OUT: Number of columns in table */
+  int *pOp                        /* OUT: SQLITE_INSERT, DELETE or UPDATE */
 ){
   *pOp = pIter->op;
   *pnCol = pIter->nCol;
@@ -960,8 +1102,11 @@ int sqlite3changeset_op(
 int sqlite3changeset_old(
   sqlite3_changeset_iter *pIter,
   int iVal,
-  sqlite3_value **ppValue             /* OUT: Old value (or NULL pointer) */
+  sqlite3_value **ppValue         /* OUT: Old value (or NULL pointer) */
 ){
+  if( pIter->op!=SQLITE_UPDATE && pIter->op!=SQLITE_DELETE ){
+    return SQLITE_MISUSE;
+  }
   if( iVal<0 || iVal>=pIter->nCol ){
     return SQLITE_RANGE;
   }
@@ -972,8 +1117,11 @@ int sqlite3changeset_old(
 int sqlite3changeset_new(
   sqlite3_changeset_iter *pIter,
   int iVal,
-  sqlite3_value **ppValue             /* OUT: New value (or NULL pointer) */
+  sqlite3_value **ppValue         /* OUT: New value (or NULL pointer) */
 ){
+  if( pIter->op!=SQLITE_UPDATE && pIter->op!=SQLITE_INSERT ){
+    return SQLITE_MISUSE;
+  }
   if( iVal<0 || iVal>=pIter->nCol ){
     return SQLITE_RANGE;
   }
@@ -981,6 +1129,21 @@ int sqlite3changeset_new(
   return SQLITE_OK;
 }
 
+int sqlite3changeset_conflict(
+  sqlite3_changeset_iter *pIter,
+  int iVal,
+  sqlite3_value **ppValue         /* OUT: Value from conflicting row */
+){
+  if( !pIter->pConflict ){
+    return SQLITE_MISUSE;
+  }
+  if( iVal<0 || iVal>=sqlite3_column_count(pIter->pConflict) ){
+    return SQLITE_RANGE;
+  }
+  *ppValue = sqlite3_column_value(pIter->pConflict, iVal);
+  return SQLITE_OK;
+}
+
 /*
 ** Finalize an iterator allocated with sqlite3changeset_start().
 **
@@ -1073,5 +1236,436 @@ int sqlite3changeset_invert(
   return SQLITE_OK;
 }
 
+static void sessionUpdateDeleteWhere(
+  SessionBuffer *pBuf,            /* Buffer to append to */
+  int nCol,                       /* Number of entries in azCol and abPK */
+  const char **azCol,             /* Column names */
+  u8 *abPK,                       /* True for PK columns */ 
+  int *pRc                        /* IN/OUT: Error code */
+){
+  if( *pRc==SQLITE_OK ){
+    int i;
+    const char *zSep = "";
+
+    sessionAppendStr(pBuf, " WHERE ", pRc);
+
+    for(i=0; i<nCol; i++){
+      if( abPK[i] ){
+        sessionAppendStr(pBuf, zSep, pRc);
+        sessionAppendIdent(pBuf, azCol[i], pRc);
+        sessionAppendStr(pBuf, " = ?", pRc);
+        sessionAppendInteger(pBuf, i+1, pRc);
+        zSep = "AND ";
+      }
+    }
+
+    sessionAppendStr(pBuf, " AND (?", pRc);
+    sessionAppendInteger(pBuf, nCol+1, pRc);
+    sessionAppendStr(pBuf, " OR ", pRc);
+
+    zSep = "";
+    for(i=0; i<nCol; i++){
+      if( !abPK[i] ){
+        sessionAppendStr(pBuf, zSep, pRc);
+        sessionAppendIdent(pBuf, azCol[i], pRc);
+        sessionAppendStr(pBuf, " IS ?", pRc);
+        sessionAppendInteger(pBuf, i+1, pRc);
+        zSep = "AND ";
+      }
+    }
+    sessionAppendStr(pBuf, ")", pRc);
+  }
+}
+
+/*
+** Formulate a statement to DELETE a row from database db. Assuming a table
+** structure like this:
+**
+**     CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c));
+**
+** The DELETE statement looks like this:
+**
+**     DELETE FROM x WHERE a = :1 AND c = :3 AND :5 OR (b IS :2 AND d IS :4)
+**
+** Variable :5 (nCol+1) is a boolean. It should be set to 0 if we require
+** matching b and d values, or 1 otherwise. The second case comes up if the
+** conflict handler is invoked with NOTFOUND and returns CHANGESET_REPLACE.
+*/
+static int sessionDeleteRow(
+  sqlite3 *db,                    /* Database handle */
+  const char *zTab,               /* Table name */
+  int nCol,                       /* Number of entries in azCol and abPK */
+  const char **azCol,             /* Column names */
+  u8 *abPK,                       /* True for PK columns */ 
+  sqlite3_stmt **ppStmt           /* OUT: Compiled SELECT statement. */
+){
+  int rc = SQLITE_OK;
+  if( *ppStmt==0 ){
+    SessionBuffer buf = {0, 0, 0};
+
+    sessionAppendStr(&buf, "DELETE FROM ", &rc);
+    sessionAppendIdent(&buf, zTab, &rc);
+
+    sessionUpdateDeleteWhere(&buf, nCol, azCol, abPK, &rc);
+
+    if( rc==SQLITE_OK ){
+      rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, ppStmt, 0);
+    }
+    sqlite3_free(buf.aBuf);
+  }
+
+  return rc;
+}
+
+/*
+** Formulate and prepare a statement to UPDATE a row from database db. 
+** Assuming a table structure like this:
+**
+**     CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c));
+**
+** The UPDATE statement looks like this:
+**
+**     UPDATE x SET
+**     a = CASE WHEN ?2  THEN ?3  ELSE a END,
+**     b = CASE WHEN ?5  THEN ?6  ELSE a END,
+**     c = CASE WHEN ?8  THEN ?9  ELSE a END,
+**     d = CASE WHEN ?11 THEN ?12 ELSE a END
+**     WHERE a = ?1 AND c = ?7 AND (?13 OR 
+**       (?5==0 OR b IS ?4) AND (?11==0 OR b IS ?10) AND
+**     )
+**
+** For each column in the table, there are three variables to bind:
+**
+**     ?(i*3+1)    The old.* value of the column, if any.
+**     ?(i*3+2)    A boolean flag indicating that the value is being modified.
+**     ?(i*3+3)    The new.* value of the column, if any.
+**
+** Also, a boolean flag that, if set to true, causes the statement to update
+** a row even if the non-PK values do not match. This is required if the
+** conflict-handler is invoked with CHANGESET_DATA and returns
+** CHANGESET_REPLACE. This is variable "?(nCol*3+1)".
+**
+*/
+static int sessionUpdateRow(
+  sqlite3 *db,                    /* Database handle */
+  const char *zTab,               /* Table name */
+  int nCol,                       /* Number of entries in azCol and abPK */
+  const char **azCol,             /* Column names */
+  u8 *abPK,                       /* True for PK columns */ 
+  sqlite3_stmt **ppStmt           /* OUT: Compiled SELECT statement. */
+){
+  int rc = SQLITE_OK;
+  if( *ppStmt==0 ){
+    int i;
+    const char *zSep = "";
+    SessionBuffer buf = {0, 0, 0};
+
+    /* Append "UPDATE tbl SET " */
+    sessionAppendStr(&buf, "UPDATE ", &rc);
+    sessionAppendIdent(&buf, zTab, &rc);
+    sessionAppendStr(&buf, " SET ", &rc);
+
+    /* Append the assignments */
+    for(i=0; i<nCol; i++){
+      sessionAppendStr(&buf, zSep, &rc);
+      sessionAppendIdent(&buf, azCol[i], &rc);
+      sessionAppendStr(&buf, " = CASE WHEN ?", &rc);
+      sessionAppendInteger(&buf, i*3+2, &rc);
+      sessionAppendStr(&buf, " THEN ?", &rc);
+      sessionAppendInteger(&buf, i*3+3, &rc);
+      sessionAppendStr(&buf, " ELSE ", &rc);
+      sessionAppendIdent(&buf, azCol[i], &rc);
+      sessionAppendStr(&buf, " END", &rc);
+      zSep = ", ";
+    }
+
+    /* Append the PK part of the WHERE clause */
+    sessionAppendStr(&buf, " WHERE ", &rc);
+    for(i=0; i<nCol; i++){
+      if( abPK[i] ){
+        sessionAppendIdent(&buf, azCol[i], &rc);
+        sessionAppendStr(&buf, " = ?", &rc);
+        sessionAppendInteger(&buf, i*3+1, &rc);
+        sessionAppendStr(&buf, " AND ", &rc);
+      }
+    }
+
+    /* Append the non-PK part of the WHERE clause */
+    sessionAppendStr(&buf, " (?", &rc);
+    sessionAppendInteger(&buf, nCol*3+1, &rc);
+    sessionAppendStr(&buf, " OR 1", &rc);
+    for(i=0; i<nCol; i++){
+      if( !abPK[i] ){
+        sessionAppendStr(&buf, " AND (?", &rc);
+        sessionAppendInteger(&buf, i*3+2, &rc);
+        sessionAppendStr(&buf, "=0 OR ", &rc);
+        sessionAppendIdent(&buf, azCol[i], &rc);
+        sessionAppendStr(&buf, " IS ?", &rc);
+        sessionAppendInteger(&buf, i*3+1, &rc);
+        sessionAppendStr(&buf, ")", &rc);
+      }
+    }
+    sessionAppendStr(&buf, ")", &rc);
+
+    if( rc==SQLITE_OK ){
+      rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, ppStmt, 0);
+    }
+    sqlite3_free(buf.aBuf);
+  }
+
+  return rc;
+}
+
+static int sessionSelectRow(
+  sqlite3 *db,                    /* Database handle */
+  const char *zTab,               /* Table name */
+  int nCol,                       /* Number of entries in azCol and abPK */
+  const char **azCol,             /* Column names */
+  u8 *abPK,                       /* True for PK columns */ 
+  sqlite3_stmt **ppStmt           /* OUT: Compiled SELECT statement. */
+){
+  int rc = SQLITE_OK;
+  if( *ppStmt==0 ){
+    int i;
+    const char *zSep = "";
+    SessionBuffer buf = {0, 0, 0};
+  
+    sessionAppendStr(&buf, "SELECT * FROM ", &rc);
+    sessionAppendIdent(&buf, zTab, &rc);
+    sessionAppendStr(&buf, " WHERE ", &rc);
+  
+    for(i=0; i<nCol; i++){
+      if( abPK[i] ){
+        sessionAppendStr(&buf, zSep, &rc);
+        sessionAppendIdent(&buf, azCol[i], &rc);
+        sessionAppendStr(&buf, " = ?", &rc);
+        sessionAppendInteger(&buf, i+1, &rc);
+        zSep = " AND ";
+      }
+    }
+  
+    if( rc==SQLITE_OK ){
+      rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, ppStmt, 0);
+    }
+    sqlite3_free(buf.aBuf);
+  }
+  return rc;
+}
+
+int sqlite3changeset_apply(
+  sqlite3 *db,
+  int nChangeset,
+  void *pChangeset,
+  int(*xConflict)(
+    void *pCtx,                   /* Copy of fifth arg to _apply() */
+    int eConflict,                /* DATA, MISSING, CONFLICT, CONSTRAINT */
+    sqlite3_changeset_iter *p     /* Handle describing change and conflict */
+  ),
+  void *pCtx
+){
+  sqlite3_changeset_iter *pIter;
+  int rc;
+  int rc2;
+
+  const char *zTab = 0;           /* Name of current table */
+  int nTab = 0;                   /* Result of strlen(zTab) */
+  int nCol = 0;                   /* Number of columns in table zTab */
+  const char **azCol = 0;         /* Array of column names */
+  u8 *abPK = 0;                   /* Boolean array - true if column is in PK */
+
+  sqlite3_stmt *pDelete = 0;      /* DELETE statement */
+  sqlite3_stmt *pUpdate = 0;      /* DELETE statement */
+  sqlite3_stmt *pInsert = 0;      /* INSERT statement */
+  sqlite3_stmt *pSelect = 0;      /* SELECT statement */
+
+  rc = sqlite3_exec(db, "SAVEPOINT changeset_apply", 0, 0, 0);
+  sqlite3changeset_start(&pIter, nChangeset, pChangeset);
+  while( SQLITE_ROW==sqlite3changeset_next(pIter) ){
+    int op;
+    const char *zThis;
+    sqlite3changeset_op(pIter, &zThis, &nCol, &op);
+    if( zTab==0 || sqlite3_strnicmp(zThis, zTab, nTab+1) ){
+      sqlite3_free(azCol);
+      rc = sessionTableInfo(db, zThis, nCol, &zTab, &azCol, &abPK);
+      nTab = strlen(zTab);
+
+      sqlite3_finalize(pDelete);
+      sqlite3_finalize(pUpdate);
+      sqlite3_finalize(pInsert);
+      sqlite3_finalize(pSelect);
+      pSelect = pUpdate = pInsert = pDelete = 0;
+    }
+
+    if( op==SQLITE_DELETE ){
+      int res;
+      int i;
+      rc = sessionDeleteRow(db, zTab, nCol, azCol, abPK, &pDelete);
+      for(i=0; rc==SQLITE_OK && i<nCol; i++){
+        sqlite3_value *pVal;
+        rc = sqlite3changeset_old(pIter, i, &pVal);
+        if( rc==SQLITE_OK ){
+          rc = sqlite3_bind_value(pDelete, i+1, pVal);
+        }
+      }
+      if( rc==SQLITE_OK ) rc = sqlite3_bind_int(pDelete, nCol+1, 0);
+      if( rc!=SQLITE_OK ) break;
+
+      sqlite3_step(pDelete);
+      rc = sqlite3_reset(pDelete);
+      if( rc==SQLITE_OK && sqlite3_changes(db)==0 ){
+
+        /* A NOTFOUND or DATA error. Search the table to see if it contains
+        ** a row with a matching primary key. If so, this is a DATA conflict.
+        ** Otherwise, if there is no primary key match, it is a NOTFOUND. */
+        rc = sessionSelectRow(db, zTab, nCol, azCol, abPK, &pSelect);
+        for(i=0; rc==SQLITE_OK && i<nCol; i++){
+          if( abPK[i] ){
+            sqlite3_value *pVal;
+            rc = sqlite3changeset_old(pIter, i, &pVal);
+            if( rc==SQLITE_OK ) sqlite3_bind_value(pSelect, i+1, pVal);
+          }
+        }
+        if( rc!=SQLITE_OK ) break;
+        if( SQLITE_ROW==sqlite3_step(pSelect) ){
+          pIter->pConflict = pSelect;
+          res = xConflict(pCtx, SQLITE_CHANGESET_DATA, pIter);
+          pIter->pConflict = 0;
+          sqlite3_reset(pSelect);
+        }else{
+          rc = sqlite3_reset(pSelect);
+          if( rc==SQLITE_OK ){
+            res = xConflict(pCtx, SQLITE_CHANGESET_NOTFOUND, pIter);
+          }
+        }
+
+      }else if( rc==SQLITE_CONSTRAINT ){
+        res = xConflict(pCtx, SQLITE_CHANGESET_CONSTRAINT, pIter);
+        rc = SQLITE_OK;
+      }
+
+      if( rc!=SQLITE_OK ) break;
+
+    }else if( op==SQLITE_UPDATE ){
+      int i;
+      int res;
+      rc = sessionUpdateRow(db, zTab, nCol, azCol, abPK, &pUpdate);
+      for(i=0; rc==SQLITE_OK && i<nCol; i++){
+        sqlite3_value *pOld = 0;
+        sqlite3_value *pNew = 0;
+        rc = sqlite3changeset_old(pIter, i, &pOld);
+        if( rc==SQLITE_OK ){
+          rc = sqlite3changeset_new(pIter, i, &pNew);
+        }
+        if( rc==SQLITE_OK ){
+          if( pOld ) sqlite3_bind_value(pUpdate, i*3+1, pOld);
+          sqlite3_bind_int(pUpdate, i*3+2, !!pNew);
+          if( pNew ) sqlite3_bind_value(pUpdate, i*3+3, pOld);
+        }
+      }
+      if( rc==SQLITE_OK ) rc = sqlite3_bind_int(pUpdate, nCol*3+1, 0);
+      if( rc!=SQLITE_OK ) break;
+
+      sqlite3_step(pUpdate);
+      rc = sqlite3_reset(pUpdate);
+      if( rc==SQLITE_OK && sqlite3_changes(db)==0 ){
+        /* A NOTFOUND or DATA error. Search the table to see if it contains
+        ** a row with a matching primary key. If so, this is a DATA conflict.
+        ** Otherwise, if there is no primary key match, it is a NOTFOUND. */
+        rc = sessionSelectRow(db, zTab, nCol, azCol, abPK, &pSelect);
+        for(i=0; rc==SQLITE_OK && i<nCol; i++){
+          if( abPK[i] ){
+            sqlite3_value *pVal;
+            rc = sqlite3changeset_old(pIter, i, &pVal);
+            if( rc==SQLITE_OK ) sqlite3_bind_value(pSelect, i+1, pVal);
+          }
+        }
+        if( rc!=SQLITE_OK ) break;
+        if( SQLITE_ROW==sqlite3_step(pSelect) ){
+          pIter->pConflict = pSelect;
+          res = xConflict(pCtx, SQLITE_CHANGESET_DATA, pIter);
+          pIter->pConflict = 0;
+          sqlite3_reset(pSelect);
+        }else{
+          rc = sqlite3_reset(pSelect);
+          if( rc==SQLITE_OK ){
+            res = xConflict(pCtx, SQLITE_CHANGESET_NOTFOUND, pIter);
+          }
+        }
+      }else if( rc==SQLITE_CONSTRAINT ){
+        assert(0);
+      }
+
+    }else{
+      int i;
+      assert( op==SQLITE_INSERT );
+      if( pInsert==0 ){
+        SessionBuffer buf = {0, 0, 0};
+        sessionAppendStr(&buf, "INSERT INTO main.", &rc);
+        sessionAppendIdent(&buf, zTab, &rc);
+        sessionAppendStr(&buf, " VALUES(?", &rc);
+        for(i=1; i<nCol; i++) sessionAppendStr(&buf, ", ?", &rc);
+        sessionAppendStr(&buf, ")", &rc);
+
+        if( rc==SQLITE_OK ){
+          rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, &pInsert, 0);
+        }
+        sqlite3_free(buf.aBuf);
+      }
+
+      for(i=0; rc==SQLITE_OK && i<nCol; i++){
+        sqlite3_value *pVal;
+        rc = sqlite3changeset_new(pIter, i, &pVal);
+        if( rc==SQLITE_OK ){
+          rc = sqlite3_bind_value(pInsert, i+1, pVal);
+        }
+      }
+      if( rc!=SQLITE_OK ) break;
+
+      sqlite3_step(pInsert);
+      rc = sqlite3_reset(pInsert);
+      if( rc==SQLITE_CONSTRAINT && xConflict ){
+        int res;
+
+        /* Figure out if this is a primary key or other constraint. */
+        rc = sessionSelectRow(db, zTab, nCol, azCol, abPK, &pSelect);
+        for(i=0; rc==SQLITE_OK && i<nCol; i++){
+          if( abPK[i] ){
+            sqlite3_value *pVal;
+            rc = sqlite3changeset_new(pIter, i, &pVal);
+            if( rc==SQLITE_OK ) sqlite3_bind_value(pSelect, i+1, pVal);
+          }
+        }
+        if( rc!=SQLITE_OK ) break;
+        if( SQLITE_ROW==sqlite3_step(pSelect) ){
+          pIter->pConflict = pSelect;
+          res = xConflict(pCtx, SQLITE_CHANGESET_CONFLICT, pIter);
+          pIter->pConflict = 0;
+          sqlite3_reset(pSelect);
+        }else{
+          rc = sqlite3_reset(pSelect);
+          if( rc==SQLITE_OK ){
+            res = xConflict(pCtx, SQLITE_CHANGESET_CONSTRAINT, pIter);
+          }
+        }
+      }
+    }
+  }
+  rc2 = sqlite3changeset_finalize(pIter);
+  if( rc==SQLITE_DONE ) rc = rc2;
+
+  if( rc==SQLITE_OK ){
+    rc = sqlite3_exec(db, "RELEASE changeset_apply", 0, 0, 0);
+  }else{
+    sqlite3_exec(db, "ROLLBACK TO changeset_apply", 0, 0, 0);
+    sqlite3_exec(db, "RELEASE changeset_apply", 0, 0, 0);
+  }
+
+  sqlite3_finalize(pInsert);
+  sqlite3_finalize(pDelete);
+  sqlite3_finalize(pUpdate);
+  sqlite3_finalize(pSelect);
+  sqlite3_free(azCol);
+  return rc;
+}
 
 #endif        /* #ifdef SQLITE_ENABLE_SESSION */
index fe6ca2075b7036c73def18e79f0884d0e58d2ada..8c3a708ce8d4f1a000bc1da90db6be160aa0d278 100644 (file)
@@ -112,6 +112,21 @@ int sqlite3changeset_new(
   int iVal,
   sqlite3_value **ppValue             /* OUT: New value (or NULL pointer) */
 );
+/*
+** This function is only usable with sqlite3_changeset_iter objects passed
+** to the xConflict callback by sqlite3changeset_apply(). It cannot be used
+** with iterators created using sqlite3changeset_start().
+**
+** It is used to access the "conflicting row" information available to the
+** conflict handler if the second argument is either SQLITE_CHANGESET_DATA
+** or SQLITE_CHANGESET_CONFLICT.
+*/
+int sqlite3changeset_conflict(
+  sqlite3_changeset_iter *pIter,
+  int iVal,
+  sqlite3_value **ppValue         /* OUT: Value from conflicting row */
+);
+
 
 /*
 ** Finalize an iterator allocated with sqlite3changeset_start().
@@ -129,6 +144,36 @@ int sqlite3changeset_invert(
   int *pnOut, void **ppOut        /* OUT: Inverse of input */
 );
 
+/*
+** Apply a changeset to a database.
+**
+** It is safe to execute SQL statements, including those that write to the
+** table that the callback related to, from within the xConflict callback.
+** This can be used to further customize the applications conflict
+** resolution strategy.
+*/
+int sqlite3changeset_apply(
+  sqlite3 *db,
+  int nChangeset,
+  void *pChangeset,
+  int(*xConflict)(
+    void *pCtx,                   /* Copy of fifth arg to _apply() */
+    int eConflict,                /* DATA, MISSING, CONFLICT, CONSTRAINT */
+    sqlite3_changeset_iter *p     /* Handle describing change and conflict */
+  ),
+  void *pCtx
+);
+
+/* Values passed as the second argument to a conflict-handler */
+#define SQLITE_CHANGESET_DATA       1
+#define SQLITE_CHANGESET_NOTFOUND   2
+#define SQLITE_CHANGESET_CONFLICT   3
+#define SQLITE_CHANGESET_CONSTRAINT 4
+
+/* Valid return values from a conflict-handler */
+#define SQLITE_CHANGESET_OMIT       0
+#define SQLITE_CHANGESET_REPLACE    1
+#define SQLITE_CHANGESET_ABORT      2
 
 #endif
 
index 1e8a9eccf6390bc234ca5d527e738ef2cee986c5..a805280787bf3e92a62c0e791fec5de825be3674 100644 (file)
@@ -168,6 +168,139 @@ static void test_append_value(Tcl_Obj *pList, sqlite3_value *pVal){
   }
 }
 
+typedef struct TestConflictHandler TestConflictHandler;
+struct TestConflictHandler {
+  Tcl_Interp *interp;
+  Tcl_Obj *pScript;
+};
+
+static int test_conflict_handler(
+  void *pCtx,                     /* Pointer to TestConflictHandler structure */
+  int eConf,                      /* DATA, MISSING, CONFLICT, CONSTRAINT */
+  sqlite3_changeset_iter *pIter   /* Handle describing change and conflict */
+){
+  TestConflictHandler *p = (TestConflictHandler *)pCtx;
+  Tcl_Obj *pEval;
+  Tcl_Interp *interp = p->interp;
+
+  int op;                         /* SQLITE_UPDATE, DELETE or INSERT */
+  const char *zTab;               /* Name of table conflict is on */
+  int nCol;                       /* Number of columns in table zTab */
+
+  pEval = Tcl_DuplicateObj(p->pScript);
+  Tcl_IncrRefCount(pEval);
+
+  sqlite3changeset_op(pIter, &zTab, &nCol, &op);
+
+  /* Append the operation type. */
+  Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj(
+      op==SQLITE_INSERT ? "INSERT" :
+      op==SQLITE_UPDATE ? "UPDATE" : 
+      "DELETE", -1
+  ));
+
+  /* Append the table name. */
+  Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj(zTab, -1));
+
+  /* Append the conflict type. */
+  switch( eConf ){
+    case SQLITE_CHANGESET_DATA:
+      Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("DATA",-1));
+      break;
+    case SQLITE_CHANGESET_NOTFOUND:
+      Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("NOTFOUND",-1));
+      break;
+    case SQLITE_CHANGESET_CONFLICT:
+      Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("CONFLICT",-1));
+      break;
+    case SQLITE_CHANGESET_CONSTRAINT:
+      Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("CONSTRAINT",-1));
+      break;
+  }
+
+  /* If this is not an INSERT, append the old row */
+  if( op!=SQLITE_INSERT ){
+    int i;
+    Tcl_Obj *pOld = Tcl_NewObj();
+    for(i=0; i<nCol; i++){
+      sqlite3_value *pVal;
+      sqlite3changeset_old(pIter, i, &pVal);
+      test_append_value(pOld, pVal);
+    }
+    Tcl_ListObjAppendElement(0, pEval, pOld);
+  }
+
+  /* If this is not a DELETE, append the new row */
+  if( op!=SQLITE_DELETE ){
+    int i;
+    Tcl_Obj *pNew = Tcl_NewObj();
+    for(i=0; i<nCol; i++){
+      sqlite3_value *pVal;
+      sqlite3changeset_new(pIter, i, &pVal);
+      test_append_value(pNew, pVal);
+    }
+    Tcl_ListObjAppendElement(0, pEval, pNew);
+  }
+
+  /* If this is a CHANGESET_DATA or CHANGESET_CONFLICT conflict, append
+  ** the conflicting row. */
+  if( eConf==SQLITE_CHANGESET_DATA || eConf==SQLITE_CHANGESET_CONFLICT ){
+    int i;
+    Tcl_Obj *pConflict = Tcl_NewObj();
+    for(i=0; i<nCol; i++){
+      sqlite3_value *pVal;
+      sqlite3changeset_conflict(pIter, i, &pVal);
+      test_append_value(pConflict, pVal);
+    }
+    Tcl_ListObjAppendElement(0, pEval, pConflict);
+  }
+
+  if( TCL_OK!=Tcl_EvalObjEx(interp, pEval, TCL_EVAL_GLOBAL) ){
+    Tcl_BackgroundError(interp);
+  }
+  Tcl_DecrRefCount(pEval);
+  return SQLITE_CHANGESET_OMIT;
+}
+
+/*
+** sqlite3changeset_apply DB CHANGESET SCRIPT
+*/
+static int test_sqlite3changeset_apply(
+  void * clientData,
+  Tcl_Interp *interp,
+  int objc,
+  Tcl_Obj *CONST objv[]
+){
+  sqlite3 *db;                    /* Database handle */
+  Tcl_CmdInfo info;               /* Database Tcl command (objv[1]) info */
+  int rc;                         /* Return code from changeset_invert() */
+  void *pChangeset;               /* Buffer containing changeset */
+  int nChangeset;                 /* Size of buffer aChangeset in bytes */
+  TestConflictHandler ctx;
+
+  if( objc!=4 ){
+    Tcl_WrongNumArgs(interp, 1, objv, "DB CHANGESET SCRIPT");
+    return TCL_ERROR;
+  }
+  if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(objv[1]), &info) ){
+    Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(objv[2]), 0);
+    return TCL_ERROR;
+  }
+  db = *(sqlite3 **)info.objClientData;
+  pChangeset = (void *)Tcl_GetByteArrayFromObj(objv[2], &nChangeset);
+  ctx.pScript = objv[3];
+  ctx.interp = interp;
+
+  rc = sqlite3changeset_apply(
+      db, nChangeset, pChangeset, test_conflict_handler, (void *)&ctx
+  );
+  if( rc!=SQLITE_OK ){
+    return test_session_error(interp, rc);
+  }
+  Tcl_ResetResult(interp);
+  return TCL_OK;
+}
+
 /*
 ** sqlite3changeset_invert CHANGESET
 */
@@ -185,6 +318,7 @@ static int test_sqlite3changeset_invert(
 
   if( objc!=2 ){
     Tcl_WrongNumArgs(interp, 1, objv, "CHANGESET");
+    return TCL_ERROR;
   }
   aChangeset = (void *)Tcl_GetByteArrayFromObj(objv[1], &nChangeSet);
 
@@ -283,6 +417,9 @@ int TestSession_Init(Tcl_Interp *interp){
   Tcl_CreateObjCommand(
       interp, "sqlite3changeset_invert", test_sqlite3changeset_invert, 0, 0
   );
+  Tcl_CreateObjCommand(
+      interp, "sqlite3changeset_apply", test_sqlite3changeset_apply, 0, 0
+  );
   return TCL_OK;
 }
 
index 2042e258c88f0e8539f997a60a773cad22c61015..b3938bacd9c7c0202ab96c421a33943032621754 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Add\sa\sfunction\sto\sthe\ssession\sextension\sinvert\sa\schangeset.
-D 2011-03-09T11:17:05
+C Add\sthe\ssqlite3changeset_apply()\sfunction.\sDoes\snot\syet\shandle\sall\scases.
+D 2011-03-11T19:05:52
 F Makefile.arm-wince-mingw32ce-gcc d6df77f1f48d690bd73162294bbba7f59507c72f
 F Makefile.in 27701a1653595a1f2187dc61c8117e00a6c1d50f
 F Makefile.linux-gcc 91d710bdc4998cb015f39edf3cb314ec4f4d7e23
@@ -98,9 +98,9 @@ F ext/rtree/rtree_util.tcl 06aab2ed5b826545bf215fff90ecb9255a8647ea
 F ext/rtree/sqlite3rtree.h 1af0899c63a688e272d69d8e746f24e76f10a3f0
 F ext/rtree/tkt3363.test 142ab96eded44a3615ec79fba98c7bde7d0f96de
 F ext/rtree/viewrtree.tcl eea6224b3553599ae665b239bd827e182b466024
-F ext/session/sqlite3session.c 82d86bbdfb0e0b731c4e7f9ab9a418340205abe1
-F ext/session/sqlite3session.h d7fe09ca2e12fa6007f7df8344249111c9c78e5f
-F ext/session/test_session.c 7bc88e75a78248dbde3a666f7c25cb228542a60c
+F ext/session/sqlite3session.c 724a064f0d9a909c12dfb668b42ce007378f352e
+F ext/session/sqlite3session.h 3246613b20857e58f7419e4e26dbe9161677aff0
+F ext/session/test_session.c 1b4f278d0ae164e2d02c11f5e1f2df3a2567ba41
 F install-sh 9d4de14ab9fb0facae2f48780b874848cbf2f895
 F ltmain.sh 3ff0879076df340d2e23ae905484d8c15d5fdea8
 F main.mk ae0868e05c76eaa8a0ae3d6927a949b1c8e810d7
@@ -639,7 +639,7 @@ F test/selectA.test 06d1032fa9009314c95394f2ca2e60d9f7ae8532
 F test/selectB.test f305cc6660804cb239aab4e2f26b0e288b59958b
 F test/selectC.test f9bf1bc4581b5b8158caa6e4e4f682acb379fb25
 F test/server1.test f5b790d4c0498179151ca8a7715a65a7802c859c
-F test/session1.test f0b04d2840b6a2fc9893096a1e1dadcd4f241993
+F test/session1.test 6a2e0db809e44879cec5314a6fd238ad768d9723
 F test/shared.test b9114eaea7e748a3a4c8ff7b9ca806c8f95cef3e
 F test/shared2.test 7f6ad2d857d0f4e5d6a0b9a897b5e56a6b6ea18c
 F test/shared3.test d69bdd5f156580876c5345652d21dc2092e85962
@@ -913,7 +913,7 @@ F tool/speedtest2.tcl ee2149167303ba8e95af97873c575c3e0fab58ff
 F tool/speedtest8.c 2902c46588c40b55661e471d7a86e4dd71a18224
 F tool/speedtest8inst1.c 293327bc76823f473684d589a8160bde1f52c14e
 F tool/vdbe-compress.tcl d70ea6d8a19e3571d7ab8c9b75cba86d1173ff0f
-P 269a81a37d7dbdcdec3c2322074916af0fbac91c
-R 06c861473767f41ba98f1b1628042bc2
+P 75d5dff725dbb66d67d56ad042926f1daae56dbe
+R 7203b6a6763bad83d70ecbe2db2167c5
 U dan
-Z 8e42230c84fa0976b233cb2ac7764b8c
+Z 77d8992b93ad5a2a0231fb47a635b3cb
index 07c3df47780df4f491c5c9b0da62581f32ca3537..f741e56cfffa5164aef9f5060c3c00975894bb5c 100644 (file)
@@ -1 +1 @@
-75d5dff725dbb66d67d56ad042926f1daae56dbe
\ No newline at end of file
+2b19be7bf753c7dd12e1c3b384981a3ea1bc8145
\ No newline at end of file
index 8a8a51cec64f6c78e1f1cdd67eb043226694e92a..737d796ba3fb0dfe6bf634e334e99b4760d80323 100644 (file)
@@ -147,4 +147,136 @@ do_changeset_test 2.4.2 S {}
 do_changeset_invert_test 2.4.3 S {}
 do_test 2.4.4 { S delete } {}
 
+#-------------------------------------------------------------------------
+# Test the application of simple changesets.
+#
+db close
+forcedelete test.db test.db2
+sqlite3 db test.db
+sqlite3 db2 test.db2
+
+proc xConflict {args} { 
+  lappend ::xConflict $args
+  return "" 
+}
+
+proc do_conflict_test {tn args} {
+  set O(-tables)    [list]
+  set O(-sql)       [list]
+  set O(-conflicts) [list]
+
+  array set V $args
+  foreach key [array names V] {
+    if {![info exists O($key)]} {error "no such option: $key"}
+  }
+  array set O $args
+
+  sqlite3session S db main
+  foreach t $O(-tables) { S attach $t }
+  execsql $O(-sql)
+
+  set ::xConflict [list]
+  sqlite3changeset_apply db2 [S changeset] xConflict
+
+  set conflicts [list]
+  foreach c $O(-conflicts) {
+    lappend conflicts $c
+  }
+
+  uplevel do_test $tn [list { set ::xConflict }] [list $conflicts]
+  S delete
+}
+
+proc do_db2_test {testname sql {result {}}} {
+  uplevel do_test $testname [list "execsql {$sql} db2"] [list [list {*}$result]]
+}
+
+# Test INSERT changesets.
+#
+do_test 3.1.0 {
+  execsql { CREATE TABLE t1(a PRIMARY KEY, b NOT NULL) } db2
+  execsql { 
+    CREATE TABLE t1(a PRIMARY KEY, b);
+    INSERT INTO t1 VALUES(1, 'one');
+    INSERT INTO t1 VALUES(2, 'two');
+  } db 
+} {}
+do_db2_test 3.1.1 "INSERT INTO t1 VALUES(6, 'VI')"
+do_conflict_test 3.2.2 -tables t1 -sql {
+  INSERT INTO t1 VALUES(3, 'three');
+  INSERT INTO t1 VALUES(4, 'four');
+  INSERT INTO t1 VALUES(5, 'five');
+  INSERT INTO t1 VALUES(6, 'six');
+  INSERT INTO t1 VALUES(7, 'seven');
+  INSERT INTO t1 VALUES(8, NULL);
+} -conflicts {
+  {INSERT t1 CONFLICT {i 6 t six} {i 6 t VI}}
+  {INSERT t1 CONSTRAINT {i 8 n {}}}
+}
+do_db2_test 3.1.2 "SELECT * FROM t1" {
+  6 VI 3 three 4 four 5 five 7 seven
+}
+do_execsql_test 3.1.3 "SELECT * FROM t1" {
+  1 one 2 two 3 three 4 four 5 five 6 six 7 seven 8 {}
+}
+
+# Test DELETE changesets.
+#
+do_execsql_test 3.2.1 {
+  PRAGMA foreign_keys = on;
+  CREATE TABLE t2(a PRIMARY KEY, b);
+  CREATE TABLE t3(c, d REFERENCES t2);
+  INSERT INTO t2 VALUES(1, 'one');
+  INSERT INTO t2 VALUES(2, 'two');
+  INSERT INTO t2 VALUES(3, 'three');
+  INSERT INTO t2 VALUES(4, 'four');
+}
+do_db2_test 3.2.2 {
+  PRAGMA foreign_keys = on;
+  CREATE TABLE t2(a PRIMARY KEY, b);
+  CREATE TABLE t3(c, d REFERENCES t2);
+  INSERT INTO t2 VALUES(1, 'one');
+  INSERT INTO t2 VALUES(2, 'two');
+  INSERT INTO t2 VALUES(4, 'five');
+  INSERT INTO t3 VALUES('i', 1);
+}
+do_conflict_test 3.2.3 -tables t2 -sql {
+  DELETE FROM t2 WHERE a = 1;
+  DELETE FROM t2 WHERE a = 2;
+  DELETE FROM t2 WHERE a = 3;
+  DELETE FROM t2 WHERE a = 4;
+} -conflicts {
+  {DELETE t2 CONSTRAINT {i 1 t one}}
+  {DELETE t2 NOTFOUND {i 3 t three}}
+  {DELETE t2 DATA {i 4 t four} {i 4 t five}}
+}
+do_execsql_test 3.2.4 "SELECT * FROM t2" {}
+do_db2_test     3.2.5 "SELECT * FROM t2" {1 one 4 five}
+
+# Test UPDATE changesets.
+#
+do_execsql_test 3.3.1 {
+  CREATE TABLE t4(a, b, c, PRIMARY KEY(b, c));
+  INSERT INTO t4 VALUES(1, 2, 3);
+  INSERT INTO t4 VALUES(4, 5, 6);
+}
+do_db2_test 3.3.2 {
+  CREATE TABLE t4(a, b, c, PRIMARY KEY(b, c));
+  INSERT INTO t4 VALUES(0, 2, 3);
+  INSERT INTO t4 VALUES(4, 5, 7);
+}
+
+do_conflict_test 3.2.3 -tables t4 -sql {
+  UPDATE t4 SET a = -1 WHERE b = 2;
+} -conflicts {
+  {UPDATE t4 DATA {i 1 i 2 i 3} {i -1 i 2 i 3} {i 0 i 2 i 3}}
+}
+
+do_db2_test 3.3.4 {
+  SELECT * FROM t4
+} {0 2 3 4 5 7}
+
+
+catch { db2 close }
 finish_test
+