Index: ext/session/session2.test ================================================================== --- ext/session/session2.test +++ ext/session/session2.test @@ -167,11 +167,21 @@ 16 { INSERT INTO %T4% VALUES('abc', 'def'); 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 do_common_sql { CREATE TABLE t1(a PRIMARY KEY, b); ADDED ext/session/sessionD.test Index: ext/session/sessionD.test ================================================================== --- /dev/null +++ ext/session/sessionD.test @@ -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: ext/session/sqlite3session.c ================================================================== --- ext/session/sqlite3session.c +++ ext/session/sqlite3session.c @@ -23,10 +23,19 @@ # else # define SESSIONS_STRM_CHUNK_SIZE 1024 # 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. */ struct sqlite3_session { sqlite3 *db; /* Database handle session is attached to */ @@ -37,10 +46,11 @@ int rc; /* Non-zero if an error has occurred */ void *pFilterCtx; /* First argument to pass to xTableFilter */ 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 */ }; /* ** Instances of this structure are used to build strings or binary records. */ @@ -314,12 +324,12 @@ if( eType==SQLITE_TEXT ){ z = (u8 *)sqlite3_value_text(pValue); }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 ){ sessionVarintPut(&aBuf[1], n); memcpy(&aBuf[nVarint + 1], eType==SQLITE_TEXT ? @@ -395,31 +405,31 @@ ** If an error occurs, an SQLite error code is returned and the final values ** of *piHash asn *pbNullPK are undefined. Otherwise, SQLITE_OK is returned ** 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 */ int *pbNullPK /* OUT: True if there are NULL values in PK */ ){ unsigned int h = 0; /* Hash value to return */ 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; inCol; i++){ if( pTab->abPK[i] ){ int rc; int eType; 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; eType = sqlite3_value_type(pVal); h = sessionHashAppendType(h, eType); @@ -433,17 +443,19 @@ memcpy(&iVal, &rVal, 8); } 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; } } @@ -719,15 +731,16 @@ ** as the change stored in argument pChange. If so, it returns true. Otherwise ** if the pre-update-hook does not affect the same row as pChange, it returns ** 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 */ assert( op==SQLITE_INSERT || op==SQLITE_UPDATE || op==SQLITE_DELETE ); for(iCol=0; iColnCol; iCol++){ @@ -742,15 +755,15 @@ ** fail. This is because they cache their return values, and by the ** time control flows to here they have already been called once from ** 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; /* A SessionChange object never has a NULL value in a PK column */ @@ -974,11 +987,11 @@ pSession->rc = sessionTableInfo(pSession->db, pSession->zDb, pTab->zName, &pTab->nCol, 0, &pTab->azCol, &pTab->abPK ); } if( pSession->rc==SQLITE_OK - && pTab->nCol!=sqlite3_preupdate_count(pSession->db) + && pTab->nCol!=pSession->hook.xCount(pSession->hook.pCtx) ){ pSession->rc = SQLITE_SCHEMA; } return pSession->rc; } @@ -994,13 +1007,12 @@ static void sessionPreupdateOneChange( int op, /* One of SQLITE_UPDATE, INSERT, DELETE */ 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; /* Load table details if required */ @@ -1013,18 +1025,18 @@ } /* 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 ){ /* Create a new change object containing all the old values (if ** this is an SQLITE_UPDATE or SQLITE_DELETE), or just the PK @@ -1039,14 +1051,14 @@ /* Figure out how large an allocation is required */ nByte = sizeof(SessionChange); for(i=0; inCol; 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 ); } /* This may fail if SQLite value p contains a utf-16 string that must ** be converted to utf-8 and an OOM error occurs while doing so. */ @@ -1070,19 +1082,19 @@ ** It is not possible for an OOM to occur in this block. */ nByte = 0; for(i=0; inCol; 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; pChange->op = op; pChange->pNext = pTab->apChange[iHash]; @@ -1089,11 +1101,13 @@ pTab->apChange[iHash] = pChange; }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; } } } @@ -1101,10 +1115,43 @@ error_out: if( rc!=SQLITE_OK ){ pSession->rc = rc; } } + +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. */ static void xPreUpdate( @@ -1116,11 +1163,10 @@ sqlite3_int64 iKey1, /* Rowid of row about to be deleted/updated */ sqlite3_int64 iKey2 /* New rowid value (for a rowid UPDATE) */ ){ sqlite3_session *pSession; int nDb = sqlite3Strlen30(zDb); - int nName = sqlite3Strlen30(zName); assert( sqlite3_mutex_held(db->mutex) ); for(pSession=(sqlite3_session *)pCtx; pSession; pSession=pSession->pNext){ SessionTable *pTab; @@ -1130,39 +1176,314 @@ ** to the next session object attached to this database. */ if( pSession->bEnable==0 ) continue; 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) ); - } - - if( 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; inCol, 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); + } + 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; iazCol[i]) ) bMismatch = 1; + } } - break; + + 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; } /* ** Create a session object. This session object will record changes to ** database zDb attached to connection db. @@ -1185,10 +1506,11 @@ memset(pNew, 0, sizeof(sqlite3_session)); pNew->db = db; 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 ** handle mutex. */ sqlite3_mutex_enter(sqlite3_db_mutex(db)); @@ -1498,17 +1820,18 @@ sessionPutI64(aBuf, i); sessionAppendBlob(p, aBuf, 8, pRc); } 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{ *pRc = SQLITE_NOMEM; } @@ -2177,11 +2500,11 @@ ){ /* In theory this code could just pass SQLITE_TRANSIENT as the final ** 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); return SQLITE_OK; } Index: ext/session/sqlite3session.h ================================================================== --- ext/session/sqlite3session.h +++ ext/session/sqlite3session.h @@ -270,10 +270,67 @@ int sqlite3session_changeset( sqlite3_session *pSession, /* Session object */ int *pnChangeset, /* OUT: Size of buffer at *ppChangeset */ 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: +** +**
    +**
  • Has the same name, +**
  • Has the same set of columns declared in the same order, and +**
  • Has the same PRIMARY KEY definition. +**
+** +** 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: +** +**
    +**
  • 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. +** +**
  • 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. +** +**
  • For each row (primary key) that exists in both tables, but features +** different in each, an UPDATE record is added to the session. +**
+** +** 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: ext/session/test_session.c ================================================================== --- ext/session/test_session.c +++ ext/session/test_session.c @@ -115,18 +115,19 @@ const char *zSub; int nArg; 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; int rc; @@ -214,10 +215,28 @@ p->pFilterScript = Tcl_DuplicateObj(objv[2]); Tcl_IncrRefCount(p->pFilterScript); 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; }