Index: ext/session/session1.test ================================================================== --- ext/session/session1.test +++ ext/session/session1.test @@ -86,35 +86,35 @@ execsql { INSERT INTO t1 VALUES(1, 'Sukhothai') } execsql { INSERT INTO t1 VALUES(2, 'Ayutthaya') } execsql { INSERT INTO t1 VALUES(3, 'Thonburi') } } {} do_changeset_test 2.1.2 S { - {INSERT t1 {} {i 1 t Sukhothai}} - {INSERT t1 {} {i 2 t Ayutthaya}} - {INSERT t1 {} {i 3 t Thonburi}} + {INSERT t1 0 {} {i 1 t Sukhothai}} + {INSERT t1 0 {} {i 2 t Ayutthaya}} + {INSERT t1 0 {} {i 3 t Thonburi}} } do_changeset_invert_test 2.1.3 S { - {DELETE t1 {i 1 t Sukhothai} {}} - {DELETE t1 {i 2 t Ayutthaya} {}} - {DELETE t1 {i 3 t Thonburi} {}} + {DELETE t1 0 {i 1 t Sukhothai} {}} + {DELETE t1 0 {i 2 t Ayutthaya} {}} + {DELETE t1 0 {i 3 t Thonburi} {}} } do_test 2.1.4 { S delete } {} do_test 2.2.1 { sqlite3session S db main S attach t1 execsql { DELETE FROM t1 WHERE 1 } } {} do_changeset_test 2.2.2 S { - {DELETE t1 {i 1 t Sukhothai} {}} - {DELETE t1 {i 2 t Ayutthaya} {}} - {DELETE t1 {i 3 t Thonburi} {}} + {DELETE t1 0 {i 1 t Sukhothai} {}} + {DELETE t1 0 {i 2 t Ayutthaya} {}} + {DELETE t1 0 {i 3 t Thonburi} {}} } do_changeset_invert_test 2.2.3 S { - {INSERT t1 {} {i 1 t Sukhothai}} - {INSERT t1 {} {i 2 t Ayutthaya}} - {INSERT t1 {} {i 3 t Thonburi}} + {INSERT t1 0 {} {i 1 t Sukhothai}} + {INSERT t1 0 {} {i 2 t Ayutthaya}} + {INSERT t1 0 {} {i 3 t Thonburi}} } do_test 2.2.4 { S delete } {} do_test 2.3.1 { execsql { DELETE FROM t1 } @@ -129,23 +129,23 @@ UPDATE t1 SET x = 20, y = 'Thapae' WHERE x = 3; } } {} do_changeset_test 2.3.2 S { - {INSERT t1 {} {i 10 t Sukhothai}} - {DELETE t1 {i 1 t Sukhothai} {}} - {UPDATE t1 {i 2 t Ayutthaya} {{} {} t Surin}} - {DELETE t1 {i 3 t Thonburi} {}} - {INSERT t1 {} {i 20 t Thapae}} + {INSERT t1 0 {} {i 10 t Sukhothai}} + {DELETE t1 0 {i 1 t Sukhothai} {}} + {UPDATE t1 0 {i 2 t Ayutthaya} {{} {} t Surin}} + {DELETE t1 0 {i 3 t Thonburi} {}} + {INSERT t1 0 {} {i 20 t Thapae}} } do_changeset_invert_test 2.3.3 S { - {DELETE t1 {i 10 t Sukhothai} {}} - {INSERT t1 {} {i 1 t Sukhothai}} - {UPDATE t1 {{} {} t Surin} {i 2 t Ayutthaya}} - {INSERT t1 {} {i 3 t Thonburi}} - {DELETE t1 {i 20 t Thapae} {}} + {DELETE t1 0 {i 10 t Sukhothai} {}} + {INSERT t1 0 {} {i 1 t Sukhothai}} + {UPDATE t1 0 {{} {} t Surin} {i 2 t Ayutthaya}} + {INSERT t1 0 {} {i 3 t Thonburi}} + {DELETE t1 0 {i 20 t Thapae} {}} } do_test 2.3.4 { S delete } {} do_test 2.4.1 { sqlite3session S db main Index: ext/session/session2.test ================================================================== --- ext/session/session2.test +++ ext/session/session2.test @@ -39,17 +39,17 @@ } do_iterator_test 1.1 t1 { DELETE FROM t1 WHERE a = 'i'; INSERT INTO t1 VALUES('ii', 'two'); } { - {DELETE t1 {t i t one} {}} - {INSERT t1 {} {t ii t two}} + {DELETE t1 0 {t i t one} {}} + {INSERT t1 0 {} {t ii t two}} } do_iterator_test 1.2 t1 { INSERT INTO t1 VALUES(1.5, 99.9) } { - {INSERT t1 {} {f 1.5 f 99.9}} + {INSERT t1 0 {} {f 1.5 f 99.9}} } # Execute each of the following blocks of SQL on database [db1]. Collect # changes using a session object. Apply the resulting changeset to @@ -226,34 +226,34 @@ 1 { INSERT INTO t1 VALUES(123); INSERT INTO t1 VALUES(NULL); INSERT INTO t1 VALUES(456); } { - {INSERT t1 {} {i 456}} - {INSERT t1 {} {i 123}} + {INSERT t1 0 {} {i 456}} + {INSERT t1 0 {} {i 123}} } 2 { UPDATE t1 SET a = NULL; } { - {DELETE t1 {i 456} {}} - {DELETE t1 {i 123} {}} + {DELETE t1 0 {i 456} {}} + {DELETE t1 0 {i 123} {}} } 3 { DELETE FROM t1 } { } 4 { INSERT INTO t3 VALUES(NULL, NULL) } { - {INSERT t3 {} {n {} i 1}} + {INSERT t3 0 {} {n {} i 1}} } 5 { INSERT INTO t2 VALUES(1, 2, NULL) } { } 6 { INSERT INTO t2 VALUES(1, NULL, 3) } { } 7 { INSERT INTO t2 VALUES(1, NULL, NULL) } { } - 8 { INSERT INTO t2 VALUES(1, 2, 3) } { {INSERT t2 {} {i 1 i 2 i 3}} } - 9 { DELETE FROM t2 WHERE 1 } { {DELETE t2 {i 1 i 2 i 3} {}} } + 8 { INSERT INTO t2 VALUES(1, 2, 3) } { {INSERT t2 0 {} {i 1 i 2 i 3}} } + 9 { DELETE FROM t2 WHERE 1 } { {DELETE t2 0 {i 1 i 2 i 3} {}} } } { do_iterator_test 4.$tn {t1 t2 t3} $sql $changeset } @@ -267,20 +267,114 @@ CREATE TABLE t1(a PRIMARY KEY); CREATE TABLE t2(x, y PRIMARY KEY); } foreach {tn sql changeset} { - 1 { INSERT INTO t1 VALUES(35) } { {INSERT t1 {} {i 35}} } - 2 { INSERT INTO t2 VALUES(36, 37) } { {INSERT t2 {} {i 36 i 37}} } + 1 { INSERT INTO t1 VALUES(35) } { {INSERT t1 0 {} {i 35}} } + 2 { INSERT INTO t2 VALUES(36, 37) } { {INSERT t2 0 {} {i 36 i 37}} } 3 { DELETE FROM t1 WHERE 1; UPDATE t2 SET x = 34; } { - {UPDATE t2 {i 36 i 37} {i 34 {} {}}} - {DELETE t1 {i 35} {}} + {UPDATE t2 0 {i 36 i 37} {i 34 {} {}}} + {DELETE t1 0 {i 35} {}} } } { do_iterator_test 5.$tn * $sql $changeset } -finish_test +#------------------------------------------------------------------------- +# The next block of tests verify that the "indirect" flag is set +# correctly within changesets. The indirect flag is set for a change +# if either of the following are true: +# +# * The sqlite3session_indirect() API has been used to set the session +# indirect flag to true, or +# * The change was made by a trigger. +# +# If the same row is updated more than once during a session, then the +# change is considered indirect only if all changes meet the criteria +# above. +# +test_reset +db function indirect [list S indirect] + +do_execsql_test 6.0 { + CREATE TABLE t1(a PRIMARY KEY, b, c); + + CREATE TABLE t2(x PRIMARY KEY, y); + CREATE TRIGGER AFTER INSERT ON t2 WHEN new.x%2 BEGIN + INSERT INTO t2 VALUES(new.x+1, NULL); + END; +} + +do_iterator_test 6.1.1 * { + INSERT INTO t1 VALUES(1, 'one', 'i'); + SELECT indirect(1); + INSERT INTO t1 VALUES(2, 'two', 'ii'); + SELECT indirect(0); + INSERT INTO t1 VALUES(3, 'three', 'iii'); +} { + {INSERT t1 0 {} {i 1 t one t i}} + {INSERT t1 1 {} {i 2 t two t ii}} + {INSERT t1 0 {} {i 3 t three t iii}} +} + +do_iterator_test 6.1.2 * { + SELECT indirect(1); + UPDATE t1 SET c = 'I' WHERE a = 1; + SELECT indirect(0); +} { + {UPDATE t1 1 {i 1 {} {} t i} {{} {} {} {} t I}} +} +do_iterator_test 6.1.3 * { + SELECT indirect(1); + UPDATE t1 SET c = '.' WHERE a = 1; + SELECT indirect(0); + UPDATE t1 SET c = 'o' WHERE a = 1; +} { + {UPDATE t1 0 {i 1 {} {} t I} {{} {} {} {} t o}} +} +do_iterator_test 6.1.4 * { + SELECT indirect(0); + UPDATE t1 SET c = 'x' WHERE a = 1; + SELECT indirect(1); + UPDATE t1 SET c = 'i' WHERE a = 1; +} { + {UPDATE t1 0 {i 1 {} {} t o} {{} {} {} {} t i}} +} +do_iterator_test 6.1.4 * { + SELECT indirect(1); + UPDATE t1 SET c = 'y' WHERE a = 1; + SELECT indirect(1); + UPDATE t1 SET c = 'I' WHERE a = 1; +} { + {UPDATE t1 1 {i 1 {} {} t i} {{} {} {} {} t I}} +} + +do_iterator_test 6.1.5 * { + INSERT INTO t2 VALUES(1, 'x'); +} { + {INSERT t2 0 {} {i 1 t x}} + {INSERT t2 1 {} {i 2 n {}}} +} + +do_iterator_test 6.1.6 * { + SELECT indirect(1); + INSERT INTO t2 VALUES(3, 'x'); + SELECT indirect(0); + UPDATE t2 SET y = 'y' WHERE x>2; +} { + {INSERT t2 0 {} {i 3 t y}} + {INSERT t2 0 {} {i 4 t y}} +} + +do_iterator_test 6.1.7 * { + SELECT indirect(1); + DELETE FROM t2 WHERE x = 4; + SELECT indirect(0); + INSERT INTO t2 VALUES(4, 'new'); +} { + {UPDATE t2 0 {i 4 t y} {{} {} t new}} +} +finish_test Index: ext/session/sessionfault.test ================================================================== --- ext/session/sessionfault.test +++ ext/session/sessionfault.test @@ -36,11 +36,11 @@ # # Test 1.1 attaches tables individually by name to the session object. # Whereas test 1.2 passes NULL to sqlite3session_attach() to attach all # tables. # -do_faultsim_test pagerfault-1.1 -faults oom-* -prep { +do_faultsim_test 1.1 -faults oom-* -prep { catch {db2 close} catch {db close} faultsim_restore_and_reopen sqlite3 db2 test.db2 } -body { @@ -53,11 +53,11 @@ faultsim_test_result {0 {}} {1 SQLITE_NOMEM} faultsim_integrity_check if {$testrc==0} { compare_db db db2 } } -do_faultsim_test pagerfault-1.2 -faults oom-* -prep { +do_faultsim_test 1.2 -faults oom-* -prep { catch {db2 close} catch {db close} faultsim_restore_and_reopen } -body { sqlite3session S db main @@ -80,11 +80,11 @@ compare_db db db2 } } #------------------------------------------------------------------------- -# The following block of tests - pagerfault-2.* - are designed to check +# The following block of tests - 2.* - are designed to check # the handling of faults in the sqlite3changeset_apply() function. # catch {db close} catch {db2 close} forcedelete test.db2 test.db @@ -108,11 +108,11 @@ INSERT INTO t2 VALUES('keyvalue', 'value 2'); } } { proc xConflict args [list return $conflict_policy] - do_faultsim_test pagerfault-2.$tn -faults oom-transient -prep { + do_faultsim_test 2.$tn -faults oom-transient -prep { catch {db2 close} catch {db close} faultsim_restore_and_reopen set ::changeset [changeset_from_sql $::sql] sqlite3 db2 test.db2 @@ -130,11 +130,11 @@ #------------------------------------------------------------------------- # This test case is designed so that a malloc() failure occurs while # resizing the session object hash-table from 256 to 512 buckets. This # is not an error, just a sub-optimal condition. # -do_faultsim_test pagerfault-3 -faults oom-* -prep { +do_faultsim_test 3 -faults oom-* -prep { catch {db2 close} catch {db close} faultsim_restore_and_reopen sqlite3 db2 test.db2 @@ -189,11 +189,11 @@ } {} faultsim_save_and_close db2 close -do_faultsim_test pagerfault-4 -faults oom-* -prep { +do_faultsim_test 4 -faults oom-* -prep { catch {db2 close} catch {db close} faultsim_restore_and_reopen sqlite3 db2 test.db2 sqlite3session S db main @@ -232,23 +232,23 @@ DELETE FROM t1 WHERE a = 'string'; UPDATE t1 SET a = 20 WHERE b = 2; }] db close -do_faultsim_test pagerfault-5 -faults oom* -body { +do_faultsim_test 5 -faults oom* -body { set ::inverse [sqlite3changeset_invert $::changeset] set {} {} } -test { faultsim_test_result {0 {}} {1 SQLITE_NOMEM} if {$testrc==0} { set x [list] sqlite3session_foreach c $::inverse { lappend x $c } foreach c { - {DELETE t1 {t xxx t yyy} {}} - {INSERT t1 {} {t string i 1}} - {UPDATE t1 {i 20 {} {}} {i 4 i 2}} + {DELETE t1 0 {t xxx t yyy} {}} + {INSERT t1 0 {} {t string i 1}} + {UPDATE t1 0 {i 20 {} {}} {i 4 i 2}} } { lappend y $c } if {$x != $y} { error "changeset no good" } } } finish_test Index: ext/session/sqlite3session.c ================================================================== --- ext/session/sqlite3session.c +++ ext/session/sqlite3session.c @@ -17,10 +17,11 @@ */ struct sqlite3_session { sqlite3 *db; /* Database handle session is attached to */ char *zDb; /* Name of database session is attached to */ int bEnable; /* True if currently recording */ + int bIndirect; /* True if all changes are indirect */ int bAutoAttach; /* True to auto-attach tables */ int rc; /* Non-zero if an error has occurred */ sqlite3_session *pNext; /* Next session object on same db. */ SessionTable *pTable; /* List of attached tables */ }; @@ -35,10 +36,11 @@ int rc; /* Iterator error code */ sqlite3_stmt *pConflict; /* Points to conflicting row, if any */ char *zTab; /* Current table */ int nCol; /* Number of columns in zTab */ int op; /* Current operation */ + int bIndirect; /* True if current change was indirect */ sqlite3_value **apValue; /* old.* and new.* values */ }; /* ** Each session object maintains a set of the following structures, one @@ -129,10 +131,11 @@ ** For each row modified during a session, there exists a single instance of ** this structure stored in a SessionTable.aChange[] hash table. */ struct SessionChange { int bInsert; /* True if row was inserted this session */ + int bIndirect; /* True if this change is "indirect" */ int nRecord; /* Number of bytes in buffer aRecord[] */ u8 *aRecord; /* Buffer containing old.* record */ SessionChange *pNext; /* For hash-table collisions */ }; @@ -658,12 +661,10 @@ int op, sqlite3_session *pSession, SessionTable *pTab ){ sqlite3 *db = pSession->db; - SessionChange *pChange; - SessionChange *pC; int iHash; int bNullPk = 0; int rc = SQLITE_OK; if( pSession->rc ) return; @@ -677,23 +678,26 @@ /* Search the hash table for an existing entry for rowid=iKey2. If ** one is found, store a pointer to it in pChange and unlink it from ** the hash table. Otherwise, set pChange to NULL. */ rc = sessionPreupdateHash(db, pTab, op==SQLITE_INSERT, &iHash, &bNullPk); - if( bNullPk==0 ){ + if( rc==SQLITE_OK && bNullPk==0 ){ + SessionChange *pC; for(pC=pTab->apChange[iHash]; rc==SQLITE_OK && pC; pC=pC->pNext){ int bEqual; rc = sessionPreupdateEqual(db, pTab, pC, op==SQLITE_INSERT, &bEqual); if( bEqual ) 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 ** values (if this is an INSERT). */ + SessionChange *pChange; /* New change object */ int nByte; /* Number of bytes to allocate */ int i; /* Used to iterate through columns */ + assert( rc==SQLITE_OK ); pTab->nEntry++; /* Figure out how large an allocation is required */ nByte = sizeof(SessionChange); for(i=0; inCol && rc==SQLITE_OK; i++){ @@ -730,16 +734,25 @@ rc = sessionSerializeValue(&pChange->aRecord[nByte], p, &nByte); } } if( rc==SQLITE_OK ){ /* Add the change back to the hash-table */ + if( pSession->bIndirect || sqlite3_preupdate_depth(pSession->db) ){ + pChange->bIndirect = 1; + } pChange->nRecord = nByte; pChange->bInsert = (op==SQLITE_INSERT); pChange->pNext = pTab->apChange[iHash]; pTab->apChange[iHash] = pChange; }else{ sqlite3_free(pChange); + } + }else if( rc==SQLITE_OK && 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 ){ + pC->bIndirect = 0; } } } /* If an error has occurred, mark the session object as failed. */ @@ -1134,10 +1147,11 @@ int nRewind = pBuf->nBuf; /* Set to zero if any values are modified */ int i; /* Used to iterate through columns */ u8 *pCsr = p->aRecord; /* Used to iterate through old.* values */ sessionAppendByte(pBuf, SQLITE_UPDATE, pRc); + sessionAppendByte(pBuf, p->bIndirect, pRc); for(i=0; ibInsert ){ sessionAppendByte(&buf, SQLITE_INSERT, &rc); + sessionAppendByte(&buf, p->bIndirect, &rc); for(iCol=0; iColbInsert ){ /* A DELETE change */ sessionAppendByte(&buf, SQLITE_DELETE, &rc); + sessionAppendByte(&buf, p->bIndirect, &rc); sessionAppendBlob(&buf, p->aRecord, p->nRecord, &rc); } if( rc==SQLITE_OK ){ rc = sqlite3_reset(pSel); } @@ -1413,10 +1429,24 @@ } ret = pSession->bEnable; sqlite3_mutex_leave(sqlite3_db_mutex(pSession->db)); return ret; } + +/* +** Enable or disable the session object passed as the first argument. +*/ +int sqlite3session_indirect(sqlite3_session *pSession, int bIndirect){ + int ret; + sqlite3_mutex_enter(sqlite3_db_mutex(pSession->db)); + if( bIndirect>=0 ){ + pSession->bIndirect = bIndirect; + } + ret = pSession->bIndirect; + sqlite3_mutex_leave(sqlite3_db_mutex(pSession->db)); + return ret; +} /* ** Create an iterator used to iterate through the contents of a changeset. */ int sqlite3changeset_start( @@ -1546,19 +1576,21 @@ int nByte; /* Bytes to allocate for apValue */ aChange += sessionVarintGet(aChange, &p->nCol); p->zTab = (char *)aChange; aChange += (strlen((char *)aChange) + 1); p->op = *(aChange++); + p->bIndirect = *(aChange++); sqlite3_free(p->apValue); nByte = sizeof(sqlite3_value *) * p->nCol * 2; p->apValue = (sqlite3_value **)sqlite3_malloc(nByte); if( !p->apValue ){ return (p->rc = SQLITE_NOMEM); } memset(p->apValue, 0, sizeof(sqlite3_value*)*p->nCol*2); }else{ p->op = c; + p->bIndirect = *(aChange++); } if( p->op!=SQLITE_UPDATE && p->op!=SQLITE_DELETE && p->op!=SQLITE_INSERT ){ return (p->rc = SQLITE_CORRUPT); } @@ -1585,15 +1617,17 @@ */ int sqlite3changeset_op( sqlite3_changeset_iter *pIter, /* Iterator handle */ const char **pzTab, /* OUT: Pointer to table name */ int *pnCol, /* OUT: Number of columns in table */ - int *pOp /* OUT: SQLITE_INSERT, DELETE or UPDATE */ + int *pOp, /* OUT: SQLITE_INSERT, DELETE or UPDATE */ + int *pbIndirect /* OUT: True if change is indirect */ ){ *pOp = pIter->op; *pnCol = pIter->nCol; *pzTab = pIter->zTab; + if( pbIndirect ) *pbIndirect = pIter->bIndirect; return SQLITE_OK; } /* ** This function may only be called while the iterator is pointing to an @@ -1738,35 +1772,37 @@ } case SQLITE_INSERT: case SQLITE_DELETE: { int nByte; - u8 *aEnd = &aIn[i+1]; + u8 *aEnd = &aIn[i+2]; sessionReadRecord(&aEnd, nCol, 0); aOut[i] = (eType==SQLITE_DELETE ? SQLITE_INSERT : SQLITE_DELETE); - nByte = aEnd - &aIn[i+1]; - memcpy(&aOut[i+1], &aIn[i+1], nByte); - i += 1 + nByte; + aOut[i+1] = aIn[i+1]; + nByte = aEnd - &aIn[i+2]; + memcpy(&aOut[i+2], &aIn[i+2], nByte); + i += 2 + nByte; break; } case SQLITE_UPDATE: { int nByte1; /* Size of old.* record in bytes */ int nByte2; /* Size of new.* record in bytes */ - u8 *aEnd = &aIn[i+1]; + u8 *aEnd = &aIn[i+2]; sessionReadRecord(&aEnd, nCol, 0); - nByte1 = aEnd - &aIn[i+1]; + nByte1 = aEnd - &aIn[i+2]; sessionReadRecord(&aEnd, nCol, 0); - nByte2 = aEnd - &aIn[i+1] - nByte1; + nByte2 = aEnd - &aIn[i+2] - nByte1; aOut[i] = SQLITE_UPDATE; - memcpy(&aOut[i+1], &aIn[i+1+nByte1], nByte2); - memcpy(&aOut[i+1+nByte2], &aIn[i+1], nByte1); + aOut[i+1] = aIn[i+1]; + memcpy(&aOut[i+2], &aIn[i+2+nByte1], nByte2); + memcpy(&aOut[i+2+nByte2], &aIn[i+2], nByte1); - i += 1 + nByte1 + nByte2; + i += 2 + nByte1 + nByte2; break; } default: sqlite3_free(aOut); @@ -2095,11 +2131,11 @@ int rc; /* Return code */ int nCol; /* Number of columns in table */ int op; /* Changset operation (SQLITE_UPDATE etc.) */ const char *zDummy; /* Unused */ - sqlite3changeset_op(pIter, &zDummy, &nCol, &op); + sqlite3changeset_op(pIter, &zDummy, &nCol, &op, 0); rc = sessionBindRow(pIter, op==SQLITE_INSERT ? sqlite3changeset_new : sqlite3changeset_old, nCol, abPK, pSelect ); @@ -2158,11 +2194,11 @@ int rc; int nCol; int op; const char *zDummy; - sqlite3changeset_op(pIter, &zDummy, &nCol, &op); + sqlite3changeset_op(pIter, &zDummy, &nCol, &op, 0); assert( eType==SQLITE_CHANGESET_CONFLICT || eType==SQLITE_CHANGESET_DATA ); assert( SQLITE_CHANGESET_CONFLICT+1==SQLITE_CHANGESET_CONSTRAINT ); assert( SQLITE_CHANGESET_DATA+1==SQLITE_CHANGESET_NOTFOUND ); @@ -2246,11 +2282,11 @@ assert( p->pDelete && p->pUpdate && p->pInsert && p->pSelect ); assert( p->azCol && p->abPK ); assert( !pbReplace || *pbReplace==0 ); - sqlite3changeset_op(pIter, &zDummy, &nCol, &op); + sqlite3changeset_op(pIter, &zDummy, &nCol, &op, 0); if( op==SQLITE_DELETE ){ /* Bind values to the DELETE statement. */ rc = sessionBindRow(pIter, sqlite3changeset_old, nCol, 0, p->pDelete); @@ -2360,11 +2396,11 @@ int nCol; int op; int bReplace = 0; int bRetry = 0; const char *zNew; - sqlite3changeset_op(pIter, &zNew, &nCol, &op); + sqlite3changeset_op(pIter, &zNew, &nCol, &op, 0); if( zTab==0 || sqlite3_strnicmp(zNew, zTab, nTab+1) ){ sqlite3_free(sApply.azCol); sqlite3_finalize(sApply.pDelete); sqlite3_finalize(sApply.pUpdate); Index: ext/session/sqlite3session.h ================================================================== --- ext/session/sqlite3session.h +++ ext/session/sqlite3session.h @@ -88,10 +88,39 @@ ** The return value indicates the final state of the session object: 0 if ** the session is disabled, or 1 if it is enabled. */ int sqlite3session_enable(sqlite3_session *pSession, int bEnable); +/* +** CAPI3REF: Set Or Clear the Indirect Change Flag +** +** Each change recorded by a session object is marked as either direct or +** indirect. A change is marked as indirect if either: +** +**
    +**
  • The session object "indirect" flag is set when the change is +** made, or +**
  • The change is made by an SQL trigger or foreign key action +** instead of directly as a result of a users SQL statement. +**
+** +** If a single row is affected by more than one operation within a session, +** then the change is considered indirect if all operations meet the criteria +** for an indirect change above, or direct otherwise. +** +** This function is used to set, clear or query the session object indirect +** flag. If the second argument passed to this function is zero, then the +** indirect flag is cleared. If it is greater than zero, the indirect flag +** is set. Passing a value less than zero does not modify the current value +** of the indirect flag, and may be used to query the current state of the +** indirect flag for the specified session object. +** +** The return value indicates the final state of the indirect flag: 0 if +** it is clear, or 1 if it is set. +*/ +int sqlite3session_indirect(sqlite3_session *pSession, int bIndirect); + /* ** CAPI3REF: Attach A Table To A Session Object ** ** If argument zTab is not NULL, then it is the name of a table to attach ** to the session object passed as the first argument. All subsequent changes @@ -290,24 +319,28 @@ ** If argument pzTab is not NULL, then *pzTab is set to point to a ** nul-terminated utf-8 encoded string containing the name of the table ** affected by the current change. The buffer remains valid until either ** sqlite3changeset_next() is called on the iterator or until the ** conflict-handler function returns. If pnCol is not NULL, then *pnCol is -** set to the number of columns in the table affected by the change. Finally, -** if pOp is not NULL, then *pOp is set to one of [SQLITE_INSERT], -** [SQLITE_DELETE] or [SQLITE_UPDATE], depending on the type of change that -** the iterator currently points to. +** set to the number of columns in the table affected by the change. If +** pbIncorrect is not NULL, then *pbIndirect is set to true (1) if the change +** is an indirect change, or false (0) otherwise. See the documentation for +** [sqlite3session_indirect()] for a description of direct and indirect +** changes. Finally, if pOp is not NULL, then *pOp is set to one of +** [SQLITE_INSERT], [SQLITE_DELETE] or [SQLITE_UPDATE], depending on the +** type of change that the iterator currently points to. ** ** If no error occurs, SQLITE_OK is returned. If an error does occur, an ** SQLite error code is returned. The values of the output variables may not ** be trusted in this case. */ int sqlite3changeset_op( sqlite3_changeset_iter *pIter, /* Iterator object */ const char **pzTab, /* OUT: Pointer to table name */ int *pnCol, /* OUT: Number of columns in table */ - int *pOp /* OUT: SQLITE_INSERT, DELETE or UPDATE */ + int *pOp, /* OUT: SQLITE_INSERT, DELETE or UPDATE */ + int *pbIndirect /* OUT: True for an 'indirect' change */ ); /* ** CAPI3REF: Obtain old.* Values From A Changeset Iterator ** Index: ext/session/test_session.c ================================================================== --- ext/session/test_session.c +++ ext/session/test_session.c @@ -15,10 +15,11 @@ /* ** Tclcmd: $session attach TABLE ** $session changeset ** $session delete ** $session enable BOOL +** $session indirect BOOL */ static int test_session_cmd( void *clientData, Tcl_Interp *interp, int objc, @@ -32,11 +33,12 @@ int iSub; } aSub[] = { { "attach", 1, "TABLE", }, /* 0 */ { "changeset", 0, "", }, /* 1 */ { "delete", 0, "", }, /* 2 */ - { "enable", 1, "", }, /* 3 */ + { "enable", 1, "BOOL", }, /* 3 */ + { "indirect", 1, "BOOL", }, /* 4 */ { 0 } }; int iSub; int rc; @@ -86,10 +88,18 @@ if( Tcl_GetBooleanFromObj(interp, objv[2], &val) ) return TCL_ERROR; val = sqlite3session_enable(pSession, val); Tcl_SetObjResult(interp, Tcl_NewBooleanObj(val)); break; } + + case 4: { /* indirect */ + int val; + if( Tcl_GetBooleanFromObj(interp, objv[2], &val) ) return TCL_ERROR; + val = sqlite3session_indirect(pSession, val); + Tcl_SetObjResult(interp, Tcl_NewBooleanObj(val)); + break; + } } return TCL_OK; } @@ -203,11 +213,11 @@ int nCol; /* Number of columns in table zTab */ pEval = Tcl_DuplicateObj(p->pScript); Tcl_IncrRefCount(pEval); - sqlite3changeset_op(pIter, &zTab, &nCol, &op); + sqlite3changeset_op(pIter, &zTab, &nCol, &op, 0); /* Append the operation type. */ Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj( op==SQLITE_INSERT ? "INSERT" : op==SQLITE_UPDATE ? "UPDATE" : @@ -394,19 +404,21 @@ int op; /* SQLITE_INSERT, UPDATE or DELETE */ const char *zTab; /* Name of table change applies to */ Tcl_Obj *pVar; /* Tcl value to set $VARNAME to */ Tcl_Obj *pOld; /* Vector of old.* values */ Tcl_Obj *pNew; /* Vector of new.* values */ + int bIndirect; - sqlite3changeset_op(pIter, &zTab, &nCol, &op); + sqlite3changeset_op(pIter, &zTab, &nCol, &op, &bIndirect); pVar = Tcl_NewObj(); Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj( op==SQLITE_INSERT ? "INSERT" : op==SQLITE_UPDATE ? "UPDATE" : "DELETE", -1 )); Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj(zTab, -1)); + Tcl_ListObjAppendElement(0, pVar, Tcl_NewBooleanObj(bIndirect)); pOld = Tcl_NewObj(); if( op!=SQLITE_INSERT ){ int i; for(i=0; i