]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Add experimental API sqlite3session_diff(). sessions-diff
authordan <dan@noemail.net>
Wed, 8 Apr 2015 16:01:12 +0000 (16:01 +0000)
committerdan <dan@noemail.net>
Wed, 8 Apr 2015 16:01:12 +0000 (16:01 +0000)
FossilOrigin-Name: c4340b2ea2115ad97dcac036f9034e132ab789e5

ext/session/session2.test
ext/session/sessionD.test [new file with mode: 0644]
ext/session/sqlite3session.c
ext/session/sqlite3session.h
ext/session/test_session.c
manifest
manifest.uuid

index 04be5f0917fa7de42a0ce3006c3577f8867ad8ba..0035e39dbf60b36aa1ce92e4bffd81b37757f1cd 100644 (file)
@@ -169,7 +169,17 @@ set set_of_tests {
     INSERT INTO %T4% VALUES('def', 'abc');
   }
   17 { UPDATE %T4% SET b = 1 }
+
   18 { DELETE FROM %T4% WHERE 1 }
+
+  19 { 
+    INSERT INTO t1 VALUES('', '');
+    INSERT INTO t1 VALUES(X'', X'');
+  }
+  20 { 
+    DELETE FROM t1;
+    INSERT INTO t1 VALUES('', NULL);
+  }
 }
 
 test_reset
diff --git a/ext/session/sessionD.test b/ext/session/sessionD.test
new file mode 100644 (file)
index 0000000..c267287
--- /dev/null
@@ -0,0 +1,150 @@
+# 2014 August 16
+#
+# The author disclaims copyright to this source code.  In place of
+# a legal notice, here is a blessing:
+#
+#    May you do good and not evil.
+#    May you find forgiveness for yourself and forgive others.
+#    May you share freely, never taking more than you give.
+#
+#***********************************************************************
+# This file focuses on the sqlite3session_diff() function.
+#
+
+if {![info exists testdir]} {
+  set testdir [file join [file dirname [info script]] .. .. test]
+} 
+source [file join [file dirname [info script]] session_common.tcl]
+source $testdir/tester.tcl
+ifcapable !session {finish_test; return}
+
+set testprefix sessionD
+
+proc scksum {db dbname} {
+
+  if {$dbname=="temp"} {
+    set master sqlite_temp_master
+  } else {
+    set master $dbname.sqlite_master
+  }
+
+  set alltab [$db eval "SELECT name FROM $master WHERE type='table'"]
+  set txt [$db eval "SELECT * FROM $master ORDER BY type,name,sql"]
+  foreach tab $alltab {
+    set cols [list]
+    db eval "PRAGMA $dbname.table_info = $tab" x { 
+      lappend cols "quote($x(name))" 
+    }
+    set cols [join $cols ,]
+    append txt [db eval "SELECT $cols FROM $tab ORDER BY $cols"]
+  }
+  return [md5 $txt]
+}
+
+proc do_diff_test {tn setup} {
+  reset_db
+  forcedelete test.db2
+  execsql { ATTACH 'test.db2' AS aux }
+  execsql $setup
+
+  sqlite3session S db main
+  foreach tbl [db eval {SELECT name FROM sqlite_master WHERE type='table'}] {
+    S attach $tbl
+    S diff aux $tbl
+  }
+
+  set C [S changeset]
+  S delete
+
+  sqlite3 db2 test.db2
+  sqlite3changeset_apply db2 $C ""
+  uplevel do_test $tn.1 [list {execsql { PRAGMA integrity_check } db2}] ok
+  db2 close
+
+  set cksum [scksum db main]
+  uplevel do_test $tn.2 [list {scksum db aux}] [list $cksum]
+}
+
+
+forcedelete test.db2
+do_execsql_test 1.0 {
+  CREATE TABLE t2(a PRIMARY KEY, b);
+  INSERT INTO t2 VALUES(1, 'one');
+  INSERT INTO t2 VALUES(2, 'two');
+
+  ATTACH 'test.db2' AS aux;
+  CREATE TABLE aux.t2(a PRIMARY KEY, b);
+}
+
+do_test 1.1 {
+  sqlite3session S db main
+  S attach t2
+  S diff aux t2
+  set C [S changeset]
+  S delete
+} {}
+
+do_test 1.2 {
+  sqlite3 db2 test.db2
+  sqlite3changeset_apply db2 $C ""
+  db2 close
+  db eval { SELECT * FROM aux.t2 }
+} {1 one 2 two}
+
+do_diff_test 2.1 {
+  CREATE TABLE aux.t1(x, y, PRIMARY KEY(y));
+  CREATE TABLE t1(x, y, PRIMARY KEY(y));
+
+  INSERT INTO t1 VALUES(1, 2);
+  INSERT INTO t1 VALUES(NULL, 'xyz');
+  INSERT INTO t1 VALUES(4.5, 5.5);
+}
+
+do_diff_test 2.2 {
+  CREATE TABLE aux.t1(x, y, PRIMARY KEY(y));
+  CREATE TABLE t1(x, y, PRIMARY KEY(y));
+
+  INSERT INTO aux.t1 VALUES(1, 2);
+  INSERT INTO aux.t1 VALUES(NULL, 'xyz');
+  INSERT INTO aux.t1 VALUES(4.5, 5.5);
+}
+
+do_diff_test 2.3 {
+  CREATE TABLE aux.t1(a PRIMARY KEY, b TEXT);
+  CREATE TABLE t1(a PRIMARY KEY, b TEXT);
+
+  INSERT INTO aux.t1 VALUES(1, 'one');
+  INSERT INTO aux.t1 VALUES(2, 'two');
+  INSERT INTO aux.t1 VALUES(3, 'three');
+
+  INSERT INTO t1 VALUES(1, 'I');
+  INSERT INTO t1 VALUES(2, 'two');
+  INSERT INTO t1 VALUES(3, 'III');
+}
+
+do_diff_test 2.4 {
+  CREATE TABLE aux.t1(a, b, c, d, PRIMARY KEY(c, b, a));
+  CREATE TABLE t1(a, b, c, d, PRIMARY KEY(c, b, a));
+
+  INSERT INTO t1 VALUES('hvkzyipambwdqlvwv','',-458331.50,X'DA51ED5E84');
+  INSERT INTO t1 VALUES(X'C5C6B5DD','jjxrath',40917,830244);
+  INSERT INTO t1 VALUES(-204877.54,X'1704C253D5F3AFA8',155120.88,NULL);
+  INSERT INTO t1 
+  VALUES('ckmqmzoeuvxisxqy',X'EB5A5D3A1DD22FD1','tidhjcbvbppdt',-642987.37);
+  INSERT INTO t1 VALUES(-851726,-161992,-469943,-159541);
+  INSERT INTO t1 VALUES(X'4A6A667F858938',185083,X'7A',NULL);
+
+  INSERT INTO aux.t1 VALUES(415075.74,'auawczkb',X'',X'57B4FAAF2595');
+  INSERT INTO aux.t1 VALUES(727637,711560,-181340,'hphuo');
+  INSERT INTO aux.t1 
+  VALUES(-921322.81,662959,'lvlgwdgxaurr','ajjrzrbhqflsutnymgc');
+  INSERT INTO aux.t1 VALUES(-146061,-377892,X'4E','gepvpvvuhszpxabbb');
+  INSERT INTO aux.t1 VALUES(-851726,-161992,-469943,-159541);
+  INSERT INTO aux.t1 VALUES(X'4A6A667F858938',185083,X'7A',NULL);
+  INSERT INTO aux.t1 VALUES(-204877.54,X'1704C253D5F3AFA8',155120.88, 4);
+  INSERT INTO aux.t1 
+  VALUES('ckmqmzoeuvxisxqy',X'EB5A5D3A1DD22FD1','tidgtsplhjcbvbppdt',-642987.3);
+}
+
+finish_test
+
index f861bd1ec65c20fd845e72bd77cf0e9acd6e2d89..59669478200cc3324ff7236f038a6dc3affb6216 100644 (file)
@@ -25,6 +25,15 @@ typedef struct SessionInput SessionInput;
 # endif
 #endif
 
+typedef struct SessionHook SessionHook;
+struct SessionHook {
+  void *pCtx;
+  int (*xOld)(void*,int,sqlite3_value**);
+  int (*xNew)(void*,int,sqlite3_value**);
+  int (*xCount)(void*);
+  int (*xDepth)(void*);
+};
+
 /*
 ** Session handle structure.
 */
@@ -39,6 +48,7 @@ struct sqlite3_session {
   int (*xTableFilter)(void *pCtx, const char *zTab);
   sqlite3_session *pNext;         /* Next session object on same db. */
   SessionTable *pTable;           /* List of attached tables */
+  SessionHook hook;               /* APIs to grab new and old data with */
 };
 
 /*
@@ -316,8 +326,8 @@ static int sessionSerializeValue(
         }else{
           z = (u8 *)sqlite3_value_blob(pValue);
         }
-        if( z==0 ) return SQLITE_NOMEM;
         n = sqlite3_value_bytes(pValue);
+        if( z==0 && (eType!=SQLITE_BLOB || n>0) ) return SQLITE_NOMEM;
         nVarint = sessionVarintLen(n);
   
         if( aBuf ){
@@ -397,7 +407,7 @@ static unsigned int sessionHashAppendType(unsigned int h, int eType){
 ** and the output variables are set as described above.
 */
 static int sessionPreupdateHash(
-  sqlite3 *db,                    /* Database handle */
+  sqlite3_session *pSession,      /* Session object that owns pTab */
   SessionTable *pTab,             /* Session table handle */
   int bNew,                       /* True to hash the new.* PK */
   int *piHash,                    /* OUT: Hash value */
@@ -407,7 +417,7 @@ static int sessionPreupdateHash(
   int i;                          /* Used to iterate through columns */
 
   assert( *pbNullPK==0 );
-  assert( pTab->nCol==sqlite3_preupdate_count(db) );
+  assert( pTab->nCol==pSession->hook.xCount(pSession->hook.pCtx) );
   for(i=0; i<pTab->nCol; i++){
     if( pTab->abPK[i] ){
       int rc;
@@ -415,9 +425,9 @@ static int sessionPreupdateHash(
       sqlite3_value *pVal;
 
       if( bNew ){
-        rc = sqlite3_preupdate_new(db, i, &pVal);
+        rc = pSession->hook.xNew(pSession->hook.pCtx, i, &pVal);
       }else{
-        rc = sqlite3_preupdate_old(db, i, &pVal);
+        rc = pSession->hook.xOld(pSession->hook.pCtx, i, &pVal);
       }
       if( rc!=SQLITE_OK ) return rc;
 
@@ -435,13 +445,15 @@ static int sessionPreupdateHash(
         h = sessionHashAppendI64(h, iVal);
       }else if( eType==SQLITE_TEXT || eType==SQLITE_BLOB ){
         const u8 *z;
+        int n;
         if( eType==SQLITE_TEXT ){
           z = (const u8 *)sqlite3_value_text(pVal);
         }else{
           z = (const u8 *)sqlite3_value_blob(pVal);
         }
-        if( !z ) return SQLITE_NOMEM;
-        h = sessionHashAppendBlob(h, sqlite3_value_bytes(pVal), z);
+        n = sqlite3_value_bytes(pVal);
+        if( !z && (eType!=SQLITE_BLOB || n>0) ) return SQLITE_NOMEM;
+        h = sessionHashAppendBlob(h, n, z);
       }else{
         assert( eType==SQLITE_NULL );
         *pbNullPK = 1;
@@ -721,11 +733,12 @@ static int sessionMergeUpdate(
 ** false.
 */
 static int sessionPreupdateEqual(
-  sqlite3 *db,                    /* Database handle */
+  sqlite3_session *pSession,      /* Session object that owns SessionTable */
   SessionTable *pTab,             /* Table associated with change */
   SessionChange *pChange,         /* Change to compare to */
   int op                          /* Current pre-update operation */
 ){
+  sqlite3 *db = pSession->db;
   int iCol;                       /* Used to iterate through columns */
   u8 *a = pChange->aRecord;       /* Cursor used to scan change record */
 
@@ -744,11 +757,11 @@ static int sessionPreupdateEqual(
       ** within sessionPreupdateHash(). The first two asserts below verify
       ** this (that the method has already been called). */
       if( op==SQLITE_INSERT ){
-        assert( db->pPreUpdate->pNewUnpacked || db->pPreUpdate->aNew );
-        rc = sqlite3_preupdate_new(db, iCol, &pVal);
+        /* assert( db->pPreUpdate->pNewUnpacked || db->pPreUpdate->aNew ); */
+        rc = pSession->hook.xNew(pSession->hook.pCtx, iCol, &pVal);
       }else{
-        assert( db->pPreUpdate->pUnpacked );
-        rc = sqlite3_preupdate_old(db, iCol, &pVal);
+        /* assert( db->pPreUpdate->pUnpacked ); */
+        rc = pSession->hook.xOld(pSession->hook.pCtx, iCol, &pVal);
       }
       assert( rc==SQLITE_OK );
       if( sqlite3_value_type(pVal)!=eType ) return 0;
@@ -976,7 +989,7 @@ static int sessionInitTable(sqlite3_session *pSession, SessionTable *pTab){
     );
   }
   if( pSession->rc==SQLITE_OK 
-   && pTab->nCol!=sqlite3_preupdate_count(pSession->db) 
+   && pTab->nCol!=pSession->hook.xCount(pSession->hook.pCtx)
   ){
     pSession->rc = SQLITE_SCHEMA;
   }
@@ -996,9 +1009,8 @@ static void sessionPreupdateOneChange(
   sqlite3_session *pSession,      /* Session object pTab is attached to */
   SessionTable *pTab              /* Table that change applies to */
 ){
-  sqlite3 *db = pSession->db;
   int iHash; 
-  int bNullPk = 0; 
+  int bNull = 0; 
   int rc = SQLITE_OK;
 
   if( pSession->rc ) return;
@@ -1015,14 +1027,14 @@ static void sessionPreupdateOneChange(
   /* Calculate the hash-key for this change. If the primary key of the row
   ** includes a NULL value, exit early. Such changes are ignored by the
   ** session module. */
-  rc = sessionPreupdateHash(db, pTab, op==SQLITE_INSERT, &iHash, &bNullPk);
+  rc = sessionPreupdateHash(pSession, pTab, op==SQLITE_INSERT, &iHash, &bNull);
   if( rc!=SQLITE_OK ) goto error_out;
 
-  if( bNullPk==0 ){
+  if( bNull==0 ){
     /* Search the hash table for an existing record for this row. */
     SessionChange *pC;
     for(pC=pTab->apChange[iHash]; pC; pC=pC->pNext){
-      if( sessionPreupdateEqual(db, pTab, pC, op) ) break;
+      if( sessionPreupdateEqual(pSession, pTab, pC, op) ) break;
     }
 
     if( pC==0 ){
@@ -1041,10 +1053,10 @@ static void sessionPreupdateOneChange(
       for(i=0; i<pTab->nCol; i++){
         sqlite3_value *p = 0;
         if( op!=SQLITE_INSERT ){
-          TESTONLY(int trc = ) sqlite3_preupdate_old(pSession->db, i, &p);
+          TESTONLY(int trc = ) pSession->hook.xOld(pSession->hook.pCtx, i, &p);
           assert( trc==SQLITE_OK );
         }else if( pTab->abPK[i] ){
-          TESTONLY(int trc = ) sqlite3_preupdate_new(pSession->db, i, &p);
+          TESTONLY(int trc = ) pSession->hook.xNew(pSession->hook.pCtx, i, &p);
           assert( trc==SQLITE_OK );
         }
 
@@ -1072,15 +1084,15 @@ static void sessionPreupdateOneChange(
       for(i=0; i<pTab->nCol; i++){
         sqlite3_value *p = 0;
         if( op!=SQLITE_INSERT ){
-          sqlite3_preupdate_old(pSession->db, i, &p);
+          pSession->hook.xOld(pSession->hook.pCtx, i, &p);
         }else if( pTab->abPK[i] ){
-          sqlite3_preupdate_new(pSession->db, i, &p);
+          pSession->hook.xNew(pSession->hook.pCtx, i, &p);
         }
         sessionSerializeValue(&pChange->aRecord[nByte], p, &nByte);
       }
 
       /* Add the change to the hash-table */
-      if( pSession->bIndirect || sqlite3_preupdate_depth(pSession->db) ){
+      if( pSession->bIndirect || pSession->hook.xDepth(pSession->hook.pCtx) ){
         pChange->bIndirect = 1;
       }
       pChange->nRecord = nByte;
@@ -1091,7 +1103,9 @@ static void sessionPreupdateOneChange(
     }else if( pC->bIndirect ){
       /* If the existing change is considered "indirect", but this current
       ** change is "direct", mark the change object as direct. */
-      if( sqlite3_preupdate_depth(pSession->db)==0 && pSession->bIndirect==0 ){
+      if( pSession->hook.xDepth(pSession->hook.pCtx)==0 
+       && pSession->bIndirect==0 
+      ){
         pC->bIndirect = 0;
       }
     }
@@ -1104,6 +1118,39 @@ static void sessionPreupdateOneChange(
   }
 }
 
+static int sessionFindTable(
+  sqlite3_session *pSession, 
+  const char *zName,
+  SessionTable **ppTab
+){
+  int rc = SQLITE_OK;
+  int nName = sqlite3Strlen30(zName);
+  SessionTable *pRet;
+
+  /* Search for an existing table */
+  for(pRet=pSession->pTable; pRet; pRet=pRet->pNext){
+    if( 0==sqlite3_strnicmp(pRet->zName, zName, nName+1) ) break;
+  }
+
+  if( pRet==0 && pSession->bAutoAttach ){
+    /* If there is a table-filter configured, invoke it. If it returns 0,
+    ** do not automatically add the new table. */
+    if( pSession->xTableFilter==0
+     || pSession->xTableFilter(pSession->pFilterCtx, zName) 
+    ){
+      rc = sqlite3session_attach(pSession, zName);
+      if( rc==SQLITE_OK ){
+        pRet = pSession->pTable;
+        assert( 0==sqlite3_strnicmp(pRet->zName, zName, nName+1) );
+      }
+    }
+  }
+
+  assert( rc==SQLITE_OK || pRet==0 );
+  *ppTab = pRet;
+  return rc;
+}
+
 /*
 ** The 'pre-update' hook registered by this module with SQLite databases.
 */
@@ -1118,7 +1165,6 @@ static void xPreUpdate(
 ){
   sqlite3_session *pSession;
   int nDb = sqlite3Strlen30(zDb);
-  int nName = sqlite3Strlen30(zName);
 
   assert( sqlite3_mutex_held(db->mutex) );
 
@@ -1132,35 +1178,310 @@ static void xPreUpdate(
     if( pSession->rc ) continue;
     if( sqlite3_strnicmp(zDb, pSession->zDb, nDb+1) ) continue;
 
-    for(pTab=pSession->pTable; pTab || pSession->bAutoAttach; pTab=pTab->pNext){
-      if( !pTab ){
-        /* This branch is taken if table zName has not yet been attached to
-        ** this session and the auto-attach flag is set.  */
-
-        /* If there is a table-filter configured, invoke it. If it returns 0,
-        ** this change will not be recorded. Break out of the loop early in
-        ** this case.  */
-        if( pSession->xTableFilter 
-         && pSession->xTableFilter(pSession->pFilterCtx, zName)==0
-        ){
-          break;
-        }
-
-        pSession->rc = sqlite3session_attach(pSession,zName);
-        if( pSession->rc ) break;
-        pTab = pSession->pTable;
-        assert( 0==sqlite3_strnicmp(pTab->zName, zName, nName+1) );
+    pSession->rc = sessionFindTable(pSession, zName, &pTab);
+    if( pTab ){
+      assert( pSession->rc==SQLITE_OK );
+      sessionPreupdateOneChange(op, pSession, pTab);
+      if( op==SQLITE_UPDATE ){
+        sessionPreupdateOneChange(SQLITE_INSERT, pSession, pTab);
       }
+    }
+  }
+}
+
+/*
+** The pre-update hook implementations.
+*/
+static int sessionPreupdateOld(void *pCtx, int iVal, sqlite3_value **ppVal){
+  return sqlite3_preupdate_old((sqlite3*)pCtx, iVal, ppVal);
+}
+static int sessionPreupdateNew(void *pCtx, int iVal, sqlite3_value **ppVal){
+  return sqlite3_preupdate_new((sqlite3*)pCtx, iVal, ppVal);
+}
+static int sessionPreupdateCount(void *pCtx){
+  return sqlite3_preupdate_count((sqlite3*)pCtx);
+}
+static int sessionPreupdateDepth(void *pCtx){
+  return sqlite3_preupdate_depth((sqlite3*)pCtx);
+}
+
+/*
+** Install the pre-update hooks on the session object passed as the only
+** argument.
+*/
+static void sessionPreupdateHooks(
+  sqlite3_session *pSession
+){
+  pSession->hook.pCtx = (void*)pSession->db;
+  pSession->hook.xOld = sessionPreupdateOld;
+  pSession->hook.xNew = sessionPreupdateNew;
+  pSession->hook.xCount = sessionPreupdateCount;
+  pSession->hook.xDepth = sessionPreupdateDepth;
+}
+
+typedef struct SessionDiffCtx SessionDiffCtx;
+struct SessionDiffCtx {
+  sqlite3_stmt *pStmt;
+  int nOldOff;
+};
+
+/*
+** The diff hook implementations.
+*/
+static int sessionDiffOld(void *pCtx, int iVal, sqlite3_value **ppVal){
+  SessionDiffCtx *p = (SessionDiffCtx*)pCtx;
+  *ppVal = sqlite3_column_value(p->pStmt, iVal+p->nOldOff);
+  return SQLITE_OK;
+}
+static int sessionDiffNew(void *pCtx, int iVal, sqlite3_value **ppVal){
+  SessionDiffCtx *p = (SessionDiffCtx*)pCtx;
+  *ppVal = sqlite3_column_value(p->pStmt, iVal);
+   return SQLITE_OK;
+}
+static int sessionDiffCount(void *pCtx){
+  SessionDiffCtx *p = (SessionDiffCtx*)pCtx;
+  return p->nOldOff ? p->nOldOff : sqlite3_column_count(p->pStmt);
+}
+static int sessionDiffDepth(void *pCtx){
+  return 0;
+}
+
+/*
+** Install the diff hooks on the session object passed as the only
+** argument.
+*/
+static void sessionDiffHooks(
+  sqlite3_session *pSession,
+  SessionDiffCtx *pDiffCtx
+){
+  pSession->hook.pCtx = (void*)pDiffCtx;
+  pSession->hook.xOld = sessionDiffOld;
+  pSession->hook.xNew = sessionDiffNew;
+  pSession->hook.xCount = sessionDiffCount;
+  pSession->hook.xDepth = sessionDiffDepth;
+}
+
+static char *sessionExprComparePK(
+  int nCol,
+  const char *zDb1, const char *zDb2, 
+  const char *zTab,
+  const char **azCol, u8 *abPK
+){
+  int i;
+  const char *zSep = "";
+  char *zRet = 0;
+
+  for(i=0; i<nCol; i++){
+    if( abPK[i] ){
+      zRet = sqlite3_mprintf("%z%s\"%w\".\"%w\".\"%w\"=\"%w\".\"%w\".\"%w\"",
+          zRet, zSep, zDb1, zTab, azCol[i], zDb2, zTab, azCol[i]
+      );
+      zSep = " AND ";
+      if( zRet==0 ) break;
+    }
+  }
+
+  return zRet;
+}
+
+static char *sessionExprCompareOther(
+  int nCol,
+  const char *zDb1, const char *zDb2, 
+  const char *zTab,
+  const char **azCol, u8 *abPK
+){
+  int i;
+  const char *zSep = "";
+  char *zRet = 0;
+  int bHave = 0;
 
-      if( 0==sqlite3_strnicmp(pTab->zName, zName, nName+1) ){
+  for(i=0; i<nCol; i++){
+    if( abPK[i]==0 ){
+      bHave = 1;
+      zRet = sqlite3_mprintf(
+          "%z%s\"%w\".\"%w\".\"%w\" IS NOT \"%w\".\"%w\".\"%w\"",
+          zRet, zSep, zDb1, zTab, azCol[i], zDb2, zTab, azCol[i]
+      );
+      zSep = " OR ";
+      if( zRet==0 ) break;
+    }
+  }
+
+  if( bHave==0 ){
+    assert( zRet==0 );
+    zRet = sqlite3_mprintf("0");
+  }
+
+  return zRet;
+}
+
+static char *sessionSelectFindNew(
+  int nCol,
+  const char *zDb1,      /* Pick rows in this db only */
+  const char *zDb2,      /* But not in this one */
+  const char *zTbl,      /* Table name */
+  const char *zExpr
+){
+  char *zRet = sqlite3_mprintf(
+      "SELECT * FROM \"%w\".\"%w\" WHERE NOT EXISTS ("
+      "  SELECT 1 FROM \"%w\".\"%w\" WHERE %s"
+      ")",
+      zDb1, zTbl, zDb2, zTbl, zExpr
+  );
+  return zRet;
+}
+
+static int sessionDiffFindNew(
+  int op,
+  sqlite3_session *pSession,
+  SessionTable *pTab,
+  const char *zDb1,
+  const char *zDb2,
+  char *zExpr
+){
+  int rc = SQLITE_OK;
+  char *zStmt = sessionSelectFindNew(pTab->nCol, zDb1, zDb2, pTab->zName,zExpr);
+
+  if( zStmt==0 ){
+    rc = SQLITE_NOMEM;
+  }else{
+    sqlite3_stmt *pStmt;
+    rc = sqlite3_prepare(pSession->db, zStmt, -1, &pStmt, 0);
+    if( rc==SQLITE_OK ){
+      SessionDiffCtx *pDiffCtx = (SessionDiffCtx*)pSession->hook.pCtx;
+      pDiffCtx->pStmt = pStmt;
+      pDiffCtx->nOldOff = 0;
+      while( SQLITE_ROW==sqlite3_step(pStmt) ){
         sessionPreupdateOneChange(op, pSession, pTab);
-        if( op==SQLITE_UPDATE ){
-          sessionPreupdateOneChange(SQLITE_INSERT, pSession, pTab);
+      }
+      rc = sqlite3_finalize(pStmt);
+    }
+    sqlite3_free(zStmt);
+  }
+
+  return rc;
+}
+
+static int sessionDiffFindModified(
+  sqlite3_session *pSession, 
+  SessionTable *pTab, 
+  const char *zFrom, 
+  const char *zExpr
+){
+  int rc = SQLITE_OK;
+
+  char *zExpr2 = sessionExprCompareOther(pTab->nCol,
+      pSession->zDb, zFrom, pTab->zName, pTab->azCol, pTab->abPK
+  );
+  if( zExpr2==0 ){
+    rc = SQLITE_NOMEM;
+  }else{
+    char *zStmt = sqlite3_mprintf(
+        "SELECT * FROM \"%w\".\"%w\", \"%w\".\"\%w\" WHERE %s AND %z",
+        pSession->zDb, pTab->zName, zFrom, pTab->zName, zExpr, zExpr2
+    );
+    if( zStmt==0 ){
+      rc = SQLITE_NOMEM;
+    }else{
+      sqlite3_stmt *pStmt;
+      rc = sqlite3_prepare(pSession->db, zStmt, -1, &pStmt, 0);
+
+      if( rc==SQLITE_OK ){
+        SessionDiffCtx *pDiffCtx = (SessionDiffCtx*)pSession->hook.pCtx;
+        pDiffCtx->pStmt = pStmt;
+        pDiffCtx->nOldOff = pTab->nCol;
+        while( SQLITE_ROW==sqlite3_step(pStmt) ){
+          sessionPreupdateOneChange(SQLITE_UPDATE, pSession, pTab);
         }
-        break;
+        rc = sqlite3_finalize(pStmt);
       }
+      sqlite3_free(zStmt);
     }
   }
+
+  return rc;
+}
+
+int sqlite3session_diff(
+  sqlite3_session *pSession,
+  const char *zFrom,
+  const char *zTbl,
+  char **pzErrMsg
+){
+  const char *zDb = pSession->zDb;
+  int rc = pSession->rc;
+  SessionDiffCtx d;
+
+  memset(&d, 0, sizeof(d));
+  sessionDiffHooks(pSession, &d);
+
+  if( pzErrMsg ) *pzErrMsg = 0;
+  if( rc==SQLITE_OK ){
+    char *zExpr = 0;
+    sqlite3 *db = pSession->db;
+    SessionTable *pTo;            /* Table zTbl */
+
+    /* Locate and if necessary initialize the target table object */
+    rc = sessionFindTable(pSession, zTbl, &pTo);
+    if( pTo==0 ) goto diff_out;
+    if( pTo->nCol==0 ){
+      rc = pSession->rc = sessionTableInfo(db, zDb, 
+          pTo->zName, &pTo->nCol, 0, &pTo->azCol, &pTo->abPK
+      );
+    }
+
+    /* Check the table schemas match */
+    if( rc==SQLITE_OK ){
+      int nCol;                   /* Columns in zFrom.zTbl */
+      u8 *abPK;
+      const char **azCol = 0;
+      rc = sessionTableInfo(db, zFrom, zTbl, &nCol, 0, &azCol, &abPK);
+      if( rc==SQLITE_OK ){
+        int bMismatch = 0;
+        if( pTo->nCol!=nCol || memcmp(pTo->abPK, abPK, nCol) ){
+          bMismatch = 1;
+        }else{
+          int i;
+          for(i=0; i<nCol; i++){
+            if( sqlite3_stricmp(azCol[i], pTo->azCol[i]) ) bMismatch = 1;
+          }
+        }
+
+        if( bMismatch ){
+          *pzErrMsg = sqlite3_mprintf("table schemas do not match");
+          rc = SQLITE_ERROR;
+        }
+      }
+      sqlite3_free(azCol);
+    }
+
+    if( rc==SQLITE_OK ){
+      zExpr = sessionExprComparePK(pTo->nCol, 
+          zDb, zFrom, pTo->zName, pTo->azCol, pTo->abPK
+      );
+    }
+
+    /* Find new rows */
+    if( rc==SQLITE_OK ){
+      rc = sessionDiffFindNew(SQLITE_INSERT, pSession, pTo, zDb, zFrom, zExpr);
+    }
+
+    /* Find old rows */
+    if( rc==SQLITE_OK ){
+      rc = sessionDiffFindNew(SQLITE_DELETE, pSession, pTo, zFrom, zDb, zExpr);
+    }
+
+    /* Find modified rows */
+    if( rc==SQLITE_OK ){
+      rc = sessionDiffFindModified(pSession, pTo, zFrom, zExpr);
+    }
+
+    sqlite3_free(zExpr);
+  }
+
+ diff_out:
+  sessionPreupdateHooks(pSession);
+  return rc;
 }
 
 /*
@@ -1187,6 +1508,7 @@ int sqlite3session_create(
   pNew->zDb = (char *)&pNew[1];
   pNew->bEnable = 1;
   memcpy(pNew->zDb, zDb, nDb+1);
+  sessionPreupdateHooks(pNew);
 
   /* Add the new session object to the linked list of session objects 
   ** attached to database handle $db. Do this under the cover of the db
@@ -1500,13 +1822,14 @@ static void sessionAppendCol(
     }
     if( eType==SQLITE_BLOB || eType==SQLITE_TEXT ){
       u8 *z;
+      int nByte;
       if( eType==SQLITE_BLOB ){
         z = (u8 *)sqlite3_column_blob(pStmt, iCol);
       }else{
         z = (u8 *)sqlite3_column_text(pStmt, iCol);
       }
-      if( z ){
-        int nByte = sqlite3_column_bytes(pStmt, iCol);
+      nByte = sqlite3_column_bytes(pStmt, iCol);
+      if( z || (eType==SQLITE_BLOB && nByte==0) ){
         sessionAppendVarint(p, nByte, pRc);
         sessionAppendBlob(p, z, nByte, pRc);
       }else{
@@ -2179,7 +2502,7 @@ static int sessionValueSetStr(
   ** argument to sqlite3ValueSetStr() and have the copy created 
   ** automatically. But doing so makes it difficult to detect any OOM
   ** error. Hence the code to create the copy externally. */
-  u8 *aCopy = sqlite3_malloc(nData);
+  u8 *aCopy = sqlite3_malloc(nData+1);
   if( aCopy==0 ) return SQLITE_NOMEM;
   memcpy(aCopy, aData, nData);
   sqlite3ValueSetStr(pVal, nData, (char*)aCopy, enc, sqlite3_free);
index e768d663244be806690a7ee277b772c188367afe..ed1c5d917cf6477b0392c6bef76c9a77bd2d7abf 100644 (file)
@@ -273,6 +273,63 @@ int sqlite3session_changeset(
   void **ppChangeset              /* OUT: Buffer containing changeset */
 );
 
+/*
+** CAPI3REF: Load The Difference Between Tables Into A Session 
+**
+** If it is not already attached to the session object passed as the first
+** argument, this function attaches table zTbl in the same manner as the
+** [sqlite3session_attach()] function. If zTbl does not exist, or if it
+** does not have a primary key, this function is a no-op (but does not return
+** an error).
+**
+** Argument zFromDb must be the name of a database ("main", "temp" etc.)
+** attached to the same database handle as the session object that contains 
+** a table compatible with the table attached to the session by this function.
+** A table is considered compatible if it:
+**
+** <ul>
+**   <li> Has the same name,
+**   <li> Has the same set of columns declared in the same order, and
+**   <li> Has the same PRIMARY KEY definition.
+** </ul>
+**
+** This function adds a set of changes to the session object that could be
+** used to update the table in database zFrom (call this the "from-table") 
+** so that its content is the same as the table attached to the session 
+** object (call this the "to-table"). Specifically:
+**
+** <ul>
+**   <li> For each row (primary key) that exists in the to-table but not in 
+**     the from-table, an INSERT record is added to the session object.
+**
+**   <li> For each row (primary key) that exists in the to-table but not in 
+**     the from-table, a DELETE record is added to the session object.
+**
+**   <li> For each row (primary key) that exists in both tables, but features 
+**     different in each, an UPDATE record is added to the session.
+** </ul>
+**
+** To clarify, if this function is called and then a changeset constructed
+** using [sqlite3session_changeset()], then after applying that changeset to 
+** database zFrom the contents of the two compatible tables would be 
+** identical.
+**
+** It an error if database zFrom does not exist or does not contain the
+** required compatible table.
+**
+** If the operation successful, SQLITE_OK is returned. Otherwise, an SQLite
+** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg
+** may be set to point to a buffer containing an English language error 
+** message. It is the responsibility of the caller to free this buffer using
+** sqlite3_free().
+*/
+int sqlite3session_diff(
+  sqlite3_session *pSession,
+  const char *zFromDb,
+  const char *zTbl,
+  char **pzErrMsg
+);
+
 
 /*
 ** CAPI3REF: Generate A Patchset From A Session Object
index 6c538bcec12b1515dc1802366b236193b2bb1d0d..3dc24fc69e167bf8d6060a7b0467e5841aa0a611 100644 (file)
@@ -117,14 +117,15 @@ static int test_session_cmd(
     const char *zMsg;
     int iSub;
   } aSub[] = {
-    { "attach",       1, "TABLE",  }, /* 0 */
-    { "changeset",    0, "",       }, /* 1 */
-    { "delete",       0, "",       }, /* 2 */
-    { "enable",       1, "BOOL",   }, /* 3 */
-    { "indirect",     1, "BOOL",   }, /* 4 */
-    { "isempty",      0, "",       }, /* 5 */
-    { "table_filter", 1, "SCRIPT", }, /* 6 */
-    { "patchset",     0, "",       }, /* 7 */
+    { "attach",       1, "TABLE",      }, /* 0 */
+    { "changeset",    0, "",           }, /* 1 */
+    { "delete",       0, "",           }, /* 2 */
+    { "enable",       1, "BOOL",       }, /* 3 */
+    { "indirect",     1, "BOOL",       }, /* 4 */
+    { "isempty",      0, "",           }, /* 5 */
+    { "table_filter", 1, "SCRIPT",     }, /* 6 */
+    { "patchset",     0, "",           }, /* 7 */
+    { "diff",         2, "FROMDB TBL", }, /* 8 */
     { 0 }
   };
   int iSub;
@@ -216,6 +217,24 @@ static int test_session_cmd(
       sqlite3session_table_filter(pSession, test_table_filter, clientData);
       break;
     }
+
+    case 8: {      /* diff */
+      char *zErr = 0;
+      rc = sqlite3session_diff(pSession, 
+          Tcl_GetString(objv[2]),
+          Tcl_GetString(objv[3]),
+          &zErr
+      );
+      assert( rc!=SQLITE_OK || zErr==0 );
+      if( zErr ){
+        Tcl_AppendResult(interp, zErr, 0);
+        return TCL_ERROR;
+      }
+      if( rc ){
+        return test_session_error(interp, rc);
+      }
+      break;
+    }
   }
 
   return TCL_OK;
index 42a45e41f3f4844c51e632057730faafc01112d4..b4d0667b70287ae77d699c8c13275d9d9fe5e9ab 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Merge\sprintf()\swidth\sand\sprecision\soverflow\sfixes\sfrom\strunk.
-D 2015-04-07T23:10:44.364
+C Add\sexperimental\sAPI\ssqlite3session_diff().
+D 2015-04-08T16:01:12.482
 F Makefile.arm-wince-mingw32ce-gcc d6df77f1f48d690bd73162294bbba7f59507c72f
 F Makefile.in 3083cf0c2bc6618e532b9478ce735bb512322985
 F Makefile.linux-gcc 91d710bdc4998cb015f39edf3cb314ec4f4d7e23
@@ -148,7 +148,7 @@ F ext/rtree/tkt3363.test 142ab96eded44a3615ec79fba98c7bde7d0f96de
 F ext/rtree/viewrtree.tcl eea6224b3553599ae665b239bd827e182b466024
 F ext/session/changeset.c 4ccbaa4531944c24584bf6a61ba3a39c62b6267a
 F ext/session/session1.test 4653867f32a98ce4bbb4a181aac6debe51ca4dfb
-F ext/session/session2.test 99ca0da7ddb617d42bafd83adccf99f18ae0384b
+F ext/session/session2.test a95a2d270b32638c1acba7cb9c81856712d469ac
 F ext/session/session3.test a7a9ce59b8d1e49e2cc23d81421ac485be0eea01
 F ext/session/session4.test a6ed685da7a5293c5d6f99855bcf41dbc352ca84
 F ext/session/session5.test 716bc6fafd625ce60dfa62ae128971628c1a1169
@@ -158,11 +158,12 @@ F ext/session/session9.test 5409d90d8141881d08285ed1c2c0d8d10fb92069
 F ext/session/sessionA.test 1feeab0b8e03527f08f2f1defb442da25480138f
 F ext/session/sessionB.test 06961b7c3641151f5d23088250ecad132501113c
 F ext/session/sessionC.test 3982f8577b0744c5ce3aaef7cfeb5bd903f17fe4
+F ext/session/sessionD.test 95903bd6a23dadbda7e192f730dd7dedd2d369ef
 F ext/session/session_common.tcl 9de0451b6a47218fc16b9ed8876b6238a0a3d88d
 F ext/session/sessionfault.test bef044d0952c0d62c31c8d2400be72c8684545cc
-F ext/session/sqlite3session.c 838050c4c217d2843e4705b14be25d8f0457f155
-F ext/session/sqlite3session.h 16608d29879a0ed3c6be6b7fb18dcdb5c707aaef
-F ext/session/test_session.c a28352e99bc6a83b94e4cce99a7bf25c73d6d489
+F ext/session/sqlite3session.c 3fcf2efe81c90602db76b1da33538895034eef66
+F ext/session/sqlite3session.h d9ebd8d4c5791aafdf18165575c7f2223c09279b
+F ext/session/test_session.c 037fc25340a918eb2195972fed439d7adf7b5db9
 F ext/userauth/sqlite3userauth.h 19cb6f0e31316d0ee4afdfb7a85ef9da3333a220
 F ext/userauth/user-auth.txt e6641021a9210364665fe625d067617d03f27b04
 F ext/userauth/userauth.c 5fa3bdb492f481bbc1709fc83c91ebd13460c69e
@@ -1267,7 +1268,10 @@ F tool/vdbe_profile.tcl 67746953071a9f8f2f668b73fe899074e2c6d8c1
 F tool/warnings-clang.sh f6aa929dc20ef1f856af04a730772f59283631d4
 F tool/warnings.sh 0abfd78ceb09b7f7c27c688c8e3fe93268a13b32
 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f
-P 271c110bcf5bf2ea7e113dd01dec876a08e3c047 8e4ac2ce24415926247961b00a62425ae85d6ffb
-R e52a83cd9a76eb22e0a9f49afec82395
-U drh
-Z 65d91c87ac07196da2d7921b7b9389b5
+P aeca95ac77f6f320a916f7e3c5a7a588ef4a20c8
+R 5c3cbffcff665bc16c7e835a4d990399
+T *branch * sessions-diff
+T *sym-sessions-diff *
+T -sym-sessions *
+U dan
+Z b513e16008d3799dcac5a3d5f415d7d8
index e451de8f37e021a89e17ab58e7036f605d2b39db..8b03fb6fb81b13ead3385e8cf8d373d6b20f8728 100644 (file)
@@ -1 +1 @@
-aeca95ac77f6f320a916f7e3c5a7a588ef4a20c8
\ No newline at end of file
+c4340b2ea2115ad97dcac036f9034e132ab789e5
\ No newline at end of file