From: dan Date: Wed, 8 Apr 2015 16:01:12 +0000 (+0000) Subject: Add experimental API sqlite3session_diff(). X-Git-Tag: version-3.13.0~148^2~71^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=refs%2Fheads%2Fsessions-diff;p=thirdparty%2Fsqlite.git Add experimental API sqlite3session_diff(). FossilOrigin-Name: c4340b2ea2115ad97dcac036f9034e132ab789e5 --- diff --git a/ext/session/session2.test b/ext/session/session2.test index 04be5f0917..0035e39dbf 100644 --- a/ext/session/session2.test +++ b/ext/session/session2.test @@ -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 index 0000000000..c26728742a --- /dev/null +++ b/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 + diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c index f861bd1ec6..5966947820 100644 --- a/ext/session/sqlite3session.c +++ b/ext/session/sqlite3session.c @@ -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; inCol; 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; 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 ); } @@ -1072,15 +1084,15 @@ static void sessionPreupdateOneChange( 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; @@ -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; izName, zName, nName+1) ){ + 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); } - 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; iazCol[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); diff --git a/ext/session/sqlite3session.h b/ext/session/sqlite3session.h index e768d66324..ed1c5d917c 100644 --- a/ext/session/sqlite3session.h +++ b/ext/session/sqlite3session.h @@ -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: +** +**
    +**
  • 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 diff --git a/ext/session/test_session.c b/ext/session/test_session.c index 6c538bcec1..3dc24fc69e 100644 --- a/ext/session/test_session.c +++ b/ext/session/test_session.c @@ -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; diff --git a/manifest b/manifest index 42a45e41f3..b4d0667b70 100644 --- 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 diff --git a/manifest.uuid b/manifest.uuid index e451de8f37..8b03fb6fb8 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -aeca95ac77f6f320a916f7e3c5a7a588ef4a20c8 \ No newline at end of file +c4340b2ea2115ad97dcac036f9034e132ab789e5 \ No newline at end of file