Index: src/main.c ================================================================== --- src/main.c +++ src/main.c @@ -762,10 +762,30 @@ } sqlite3_free(db); return SQLITE_OK; } + +/* +** Invoke the transaction-hook. +*/ +void sqlite3TransactionHook(sqlite3 *db, int op, int iLevel){ + assert( op==SQLITE_BEGIN || op==SQLITE_COMMIT || op==SQLITE_ROLLBACK ); + assert( op==SQLITE_BEGIN || iLeveliOpenTrans || iLevel==0 ); + assert( op==SQLITE_BEGIN || iLeveliOpenTrans || db->iOpenTrans==0 ); + assert( op!=SQLITE_BEGIN || iLevel==db->iOpenTrans || iLevel==0 ); + assert( op!=SQLITE_BEGIN || iLevel==db->iOpenTrans || db->iOpenTrans==1 ); + + if( op==SQLITE_BEGIN && iLevel!=db->iOpenTrans ) return; + if( op!=SQLITE_BEGIN && iLevel>=db->iOpenTrans ) return; + + if( db->xTransCallback ){ + db->xTransCallback(db->pTransArg, op, iLevel); + } + db->iOpenTrans = iLevel + (op==SQLITE_BEGIN); +} + /* ** Rollback all database files. */ void sqlite3RollbackAll(sqlite3 *db){ int i; @@ -794,10 +814,14 @@ /* If one has been configured, invoke the rollback-hook callback */ if( db->xRollbackCallback && (inTrans || !db->autoCommit) ){ db->xRollbackCallback(db->pRollbackArg); } + + /* If a transaction-hook is configured, invoke it now to report on + ** the rollback operation. */ + sqlite3TransactionHook(db, SQLITE_ROLLBACK, 0); } /* ** Return a static string that describes the kind of error specified in the ** argument. @@ -1287,10 +1311,28 @@ db->xPreUpdateCallback = xCallback; db->pPreUpdateArg = pArg; sqlite3_mutex_leave(db->mutex); return pRet; } + +/* +** Register a callback to be invoked each time a transaction or savepoint +** is opened, committed or rolled back. +*/ +void *sqlite3_transaction_hook( + sqlite3 *db, /* Database handle */ + void(*xCallback)(void *, int, int), /* Callback function */ + void *pArg /* First callback argument */ +){ + void *pRet; + sqlite3_mutex_enter(db->mutex); + pRet = db->pTransArg; + db->xTransCallback = xCallback; + db->pTransArg = pArg; + sqlite3_mutex_leave(db->mutex); + return pRet; +} #ifndef SQLITE_OMIT_WAL /* ** The sqlite3_wal_hook() callback registered by sqlite3_wal_autocheckpoint(). ** Invoke sqlite3_wal_checkpoint if the number of frames in the log file Index: src/sqlite.h.in ================================================================== --- src/sqlite.h.in +++ src/sqlite.h.in @@ -6342,11 +6342,19 @@ */ #define SQLITE_CHECKPOINT_PASSIVE 0 #define SQLITE_CHECKPOINT_FULL 1 #define SQLITE_CHECKPOINT_RESTART 2 -void *sqlite3_preupdate_hook( + +/* +** CAPI3REF: The pre-update hook. +** +** The preupdate_old() API may only be used from within an SQLITE_UPDATE or +** SQLITE_DELETE pre-update callback. The preupdate_modified() API may only +** be used from within an SQLITE_UPDATE pre-update callback. +*/ +SQLITE_EXPERIMENTAL void *sqlite3_preupdate_hook( sqlite3 *db, void(*xPreUpdate)( void *pCtx, /* Copy of third arg to preupdate_hook() */ sqlite3 *db, /* Database handle */ int op, /* SQLITE_UPDATE, DELETE or INSERT */ @@ -6355,20 +6363,23 @@ sqlite3_int64 iKey1, /* Rowid of row about to be deleted/updated */ sqlite3_int64 iKey2 /* New rowid value (for a rowid UPDATE) */ ), void* ); +SQLITE_EXPERIMENTAL int sqlite3_preupdate_old(sqlite3 *, int, sqlite3_value **); +SQLITE_EXPERIMENTAL int sqlite3_preupdate_modified(sqlite3 *, int, int *); +SQLITE_EXPERIMENTAL int sqlite3_preupdate_count(sqlite3 *); /* -** The following APIs may only be used from within a pre-update callback. More -** specifically, the preupdate_old() API may only be used from within an -** SQLITE_UPDATE or SQLITE_DELETE pre-update callback. The preupdate_modified() -** API may only be used from within an SQLITE_UPDATE pre-update callback. +** CAPI3REF: The transaction hook. */ -int sqlite3_preupdate_old(sqlite3 *, int, sqlite3_value **); -int sqlite3_preupdate_modified(sqlite3 *, int, int *); -int sqlite3_preupdate_count(sqlite3 *); +SQLITE_EXPERIMENTAL +void *sqlite3_transaction_hook(sqlite3 *, void(*)(void *, int, int), void *); + +#define SQLITE_BEGIN 1 +#define SQLITE_COMMIT 2 +#define SQLITE_ROLLBACK 3 /* ** Undo the hack that converts floating point types to integer for ** builds on processors without floating point support. Index: src/sqliteInt.h ================================================================== --- src/sqliteInt.h +++ src/sqliteInt.h @@ -831,10 +831,13 @@ void *pPreUpdateArg; /* First argument to xPreUpdateCallback */ void (*xPreUpdateCallback)( /* Registered using sqlite3_preupdate_hook() */ void*,sqlite3*,int,char const*,char const*,sqlite3_int64,sqlite3_int64 ); PreUpdate *pPreUpdate; /* Context for active pre-update callback */ + void *pTransArg; /* First argument to xTransCallback */ + void (*xTransCallback)(void*,int,int); + int iOpenTrans; /* Open transaction (xTransCallback) plus 1 */ #ifndef SQLITE_OMIT_WAL int (*xWalCallback)(void *, sqlite3 *, const char *, int); void *pWalArg; #endif void(*xCollNeeded)(void*,sqlite3*,int eTextRep,const char*); @@ -2746,10 +2749,11 @@ Vdbe *sqlite3GetVdbe(Parse*); void sqlite3PrngSaveState(void); void sqlite3PrngRestoreState(void); void sqlite3PrngResetState(void); void sqlite3RollbackAll(sqlite3*); +void sqlite3TransactionHook(sqlite3 *, int, int); void sqlite3CodeVerifySchema(Parse*, int); void sqlite3BeginTransaction(Parse*, int); void sqlite3CommitTransaction(Parse*); void sqlite3RollbackTransaction(Parse*); void sqlite3Savepoint(Parse*, int, Token*); Index: src/tclsqlite.c ================================================================== --- src/tclsqlite.c +++ src/tclsqlite.c @@ -122,10 +122,11 @@ char *zNull; /* Text to substitute for an SQL NULL value */ SqlFunc *pFunc; /* List of SQL functions */ Tcl_Obj *pUpdateHook; /* Update hook script (if any) */ Tcl_Obj *pPreUpdateHook; /* Pre-update hook script (if any) */ Tcl_Obj *pRollbackHook; /* Rollback hook script (if any) */ + Tcl_Obj *pTransHook; /* Transaction hook script (if any) */ Tcl_Obj *pWalHook; /* WAL hook script (if any) */ Tcl_Obj *pUnlockNotify; /* Unlock notify script (if any) */ SqlCollate *pCollate; /* List of SQL collation functions */ int rc; /* Return code of most recent sqlite3_exec() */ Tcl_Obj *pCollateNeeded; /* Collation needed script */ @@ -594,10 +595,34 @@ assert(pDb->pRollbackHook); if( TCL_OK!=Tcl_EvalObjEx(pDb->interp, pDb->pRollbackHook, 0) ){ Tcl_BackgroundError(pDb->interp); } } + +/* +** sqlite3_transaction_hook() callback. +*/ +static void DbTransHandler(void *clientData, int op, int iLevel){ + static const char *azStr[] = { "BEGIN", "COMMIT", "ROLLBACK" }; + SqliteDb *pDb = (SqliteDb*)clientData; + Tcl_Interp *interp = pDb->interp; + Tcl_Obj *pScript; + + assert(pDb->pTransHook); + assert( SQLITE_BEGIN==1 ); + assert( SQLITE_COMMIT==2 ); + assert( SQLITE_ROLLBACK==3 ); + + pScript = Tcl_DuplicateObj(pDb->pTransHook); + Tcl_IncrRefCount(pScript); + Tcl_ListObjAppendElement(interp, pScript, Tcl_NewStringObj(azStr[op-1], -1)); + Tcl_ListObjAppendElement(interp, pScript, Tcl_NewIntObj(iLevel)); + if( TCL_OK!=Tcl_EvalObjEx(interp, pScript, 0) ){ + Tcl_BackgroundError(interp); + } + Tcl_DecrRefCount(pScript); +} /* ** This procedure handles wal_hook callbacks. */ static int DbWalHandler( @@ -1626,10 +1651,11 @@ sqlite3_preupdate_hook(db, (pDb->pPreUpdateHook?DbPreUpdateHandler:0), pDb); sqlite3_update_hook(db, (pDb->pUpdateHook?DbUpdateHandler:0), pDb); sqlite3_rollback_hook(db, (pDb->pRollbackHook?DbRollbackHandler:0), pDb); sqlite3_wal_hook(db, (pDb->pWalHook?DbWalHandler:0), pDb); + sqlite3_transaction_hook(db, (pDb->pTransHook?DbTransHandler:0), pDb); } /* ** The "sqlite" command below creates a new Tcl command for each ** connection it opens to an SQLite database. This routine is invoked @@ -1657,11 +1683,12 @@ "last_insert_rowid", "nullvalue", "onecolumn", "preupdate", "profile", "progress", "rekey", "restore", "rollback_hook", "status", "timeout", "total_changes", "trace", - "transaction", "unlock_notify", "update_hook", + "transaction", "transaction_hook", + "unlock_notify", "update_hook", "version", "wal_hook", 0 }; enum DB_enum { DB_AUTHORIZER, DB_BACKUP, DB_BUSY, DB_CACHE, DB_CHANGES, DB_CLOSE, @@ -1672,11 +1699,12 @@ DB_LAST_INSERT_ROWID, DB_NULLVALUE, DB_ONECOLUMN, DB_PREUPDATE, DB_PROFILE, DB_PROGRESS, DB_REKEY, DB_RESTORE, DB_ROLLBACK_HOOK, DB_STATUS, DB_TIMEOUT, DB_TOTAL_CHANGES, DB_TRACE, - DB_TRANSACTION, DB_UNLOCK_NOTIFY, DB_UPDATE_HOOK, + DB_TRANSACTION, DB_TRANSACTION_HOOK, + DB_UNLOCK_NOTIFY, DB_UPDATE_HOOK, DB_VERSION, DB_WAL_HOOK }; /* don't leave trailing commas on DB_enum, it confuses the AIX xlc compiler */ if( objc<2 ){ @@ -2912,23 +2940,26 @@ /* ** $db wal_hook ?script? ** $db update_hook ?script? ** $db rollback_hook ?script? + ** $db transaction_hook ?script? */ case DB_WAL_HOOK: case DB_UPDATE_HOOK: - case DB_ROLLBACK_HOOK: { + case DB_ROLLBACK_HOOK: + case DB_TRANSACTION_HOOK: { sqlite3 *db = pDb->db; /* set ppHook to point at pUpdateHook or pRollbackHook, depending on ** whether [$db update_hook] or [$db rollback_hook] was invoked. */ Tcl_Obj **ppHook; if( choice==DB_WAL_HOOK ) ppHook = &pDb->pWalHook; if( choice==DB_UPDATE_HOOK ) ppHook = &pDb->pUpdateHook; if( choice==DB_ROLLBACK_HOOK ) ppHook = &pDb->pRollbackHook; + if( choice==DB_TRANSACTION_HOOK ) ppHook = &pDb->pTransHook; if( objc>3 ){ Tcl_WrongNumArgs(interp, 2, objv, "?SCRIPT?"); return TCL_ERROR; } Index: src/vdbe.c ================================================================== --- src/vdbe.c +++ src/vdbe.c @@ -2566,10 +2566,12 @@ db->autoCommit = 0; db->isTransactionSavepoint = 1; }else{ db->nSavepoint++; } + + sqlite3TransactionHook(db, SQLITE_BEGIN, db->nSavepoint); /* Link the new savepoint into the database handle's list. */ pNew->pNext = db->pSavepoint; db->pSavepoint = pNew; pNew->nDeferredCons = db->nDeferredCons; @@ -2633,10 +2635,17 @@ if( p1==SAVEPOINT_ROLLBACK && (db->flags&SQLITE_InternChanges)!=0 ){ sqlite3ExpirePreparedStatements(db); sqlite3ResetInternalSchema(db, 0); db->flags = (db->flags | SQLITE_InternChanges); } + + assert( SAVEPOINT_ROLLBACK+1==SQLITE_ROLLBACK ); + assert( SAVEPOINT_RELEASE+1==SQLITE_COMMIT ); + sqlite3TransactionHook(db, p1+1, iSavepoint+1); + if( p1==SAVEPOINT_ROLLBACK ){ + sqlite3TransactionHook(db, SQLITE_BEGIN, iSavepoint+1); + } } /* Regardless of whether this is a RELEASE or ROLLBACK, destroy all ** savepoints nested inside of the savepoint being operated on. */ while( db->pSavepoint!=pSavepoint ){ @@ -2708,10 +2717,13 @@ sqlite3RollbackAll(db); db->autoCommit = 1; }else if( (rc = sqlite3VdbeCheckFk(p, 1))!=SQLITE_OK ){ goto vdbe_return; }else{ + if( desiredAutoCommit==0 ){ + sqlite3TransactionHook(db, SQLITE_BEGIN, 0); + } db->autoCommit = (u8)desiredAutoCommit; if( sqlite3VdbeHalt(p)==SQLITE_BUSY ){ p->pc = pc; db->autoCommit = (u8)(1-desiredAutoCommit); p->rc = rc = SQLITE_BUSY; @@ -2794,10 +2806,11 @@ assert( db->nStatement>=0 && db->nSavepoint>=0 ); db->nStatement++; p->iStatement = db->nSavepoint + db->nStatement; } rc = sqlite3BtreeBeginStmt(pBt, p->iStatement); + sqlite3TransactionHook(db, SQLITE_BEGIN, p->iStatement); /* Store the current value of the database handles deferred constraint ** counter. If the statement transaction needs to be rolled back, ** the value of this counter needs to be restored too. */ p->nStmtDefCons = db->nDeferredCons; Index: src/vdbeapi.c ================================================================== --- src/vdbeapi.c +++ src/vdbeapi.c @@ -399,11 +399,19 @@ sqlite3OsCurrentTimeInt64(db->pVfs, &p->startTime); } #endif db->activeVdbeCnt++; - if( p->readOnly==0 ) db->writeVdbeCnt++; + if( p->readOnly==0 ){ + /* If this statement will open an implicit transaction, invoke the + ** transaction-hook here. */ + if( db->autoCommit && db->writeVdbeCnt==0 ){ + assert( db->nSavepoint==0 ); + sqlite3TransactionHook(db, SQLITE_BEGIN, 0); + } + db->writeVdbeCnt++; + } p->pc = 0; } #ifndef SQLITE_OMIT_EXPLAIN if( p->explain ){ rc = sqlite3VdbeList(p); Index: src/vdbeaux.c ================================================================== --- src/vdbeaux.c +++ src/vdbeaux.c @@ -1842,10 +1842,16 @@ enable_simulated_io_errors(); sqlite3VtabCommit(db); } #endif + + /* If a transaction-hook is configured, invoke it now to report on the + ** successful commit operation. */ + if( rc==SQLITE_OK ){ + sqlite3TransactionHook(db, SQLITE_COMMIT, 0); + } return rc; } /* @@ -1944,10 +1950,13 @@ rc = rc2; } } } db->nStatement--; + assert( SAVEPOINT_ROLLBACK+1==SQLITE_ROLLBACK ); + assert( SAVEPOINT_RELEASE+1==SQLITE_COMMIT ); + sqlite3TransactionHook(db, eOp+1, p->iStatement); p->iStatement = 0; /* If the statement transaction is being rolled back, also restore the ** database handles deferred constraint counter to the value it had when ** the statement transaction was opened. */ Index: test/hook.test ================================================================== --- test/hook.test +++ test/hook.test @@ -614,18 +614,153 @@ INSERT INTO t8 VALUES('one', 'two'); INSERT INTO t8 VALUES('three', 'four'); ALTER TABLE t8 ADD COLUMN c DEFAULT 'xxx'; } +# At time of writing, these two are broken. They demonstraight that the +# sqlite3_preupdate_old() method does not handle the case where ALTER TABLE +# has been used to add a column with a default value other than NULL. +# do_preupdate_test 7.5.2.1 { DELETE FROM t8 WHERE a = 'one' } { DELETE main t8 1 1 one two xxx } - do_preupdate_test 7.5.2.2 { UPDATE t8 SET b = 'five' } { UPDATE main t8 2 2 three four xxx } + +#---------------------------------------------------------------------------- +# The following tests - hook-8.* - test the transaction hook. +# +db close +forcedelete test.db +sqlite3 db test.db + +proc transaction_hook {op iLevel} { + lappend ::transaction_hook $op $iLevel +} +db transaction_hook transaction_hook + +proc do_transaction_test {tn sql x} { + set X [list] + foreach elem $x {lappend X $elem} + + uplevel do_test $tn [list " + set ::transaction_hook \[list\] + catchsql { $sql } + set ::transaction_hook + "] [list $X] +} + +do_transaction_test 8.1.1 "CREATE TABLE t1(x)" {BEGIN 0 COMMIT 0} +do_transaction_test 8.1.2 "BEGIN" {BEGIN 0} +do_transaction_test 8.1.3 "COMMIT" {COMMIT 0} +do_transaction_test 8.1.4 "BEGIN ; ROLLBACK" {BEGIN 0 ROLLBACK 0} + +do_execsql_test 8.2.0 { + CREATE TABLE t2(a PRIMARY KEY, b); + INSERT INTO t2 VALUES(1, 'one'); + INSERT INTO t2 VALUES(2, 'two'); + INSERT INTO t2 VALUES(3, 'three'); +} +do_transaction_test 8.2.1 { + INSERT INTO t2 VALUES(2, 'xxx') +} {BEGIN 0 ROLLBACK 0} + +do_transaction_test 8.2.2 { + BEGIN; INSERT INTO t2 SELECT a-2, b FROM t2; +} {BEGIN 0 BEGIN 1 ROLLBACK 1} + +do_transaction_test 8.2.3 { + INSERT OR ROLLBACK INTO t2 SELECT a-2, b FROM t2; +} {ROLLBACK 0} + +do_transaction_test 8.2.4 { + BEGIN; INSERT INTO t2 SELECT a+3, b FROM t2; +} {BEGIN 0 BEGIN 1 COMMIT 1} + +do_transaction_test 8.2.5 "COMMIT" {COMMIT 0} + +do_transaction_test 8.3.1 {SELECT * FROM t2} {} + +do_transaction_test 8.4.1 { + SAVEPOINT top; + RELEASE top; +} {BEGIN 0 COMMIT 0} + +do_transaction_test 8.4.2 { + SAVEPOINT top; + ROLLBACK TO top; + RELEASE top; +} {BEGIN 0 ROLLBACK 0 BEGIN 0 COMMIT 0} + +do_transaction_test 8.4.3 { + SAVEPOINT zero; + SAVEPOINT one; + SAVEPOINT two; + SAVEPOINT three; + ROLLBACK TO zero; + SAVEPOINT one; + SAVEPOINT two; + SAVEPOINT three; + ROLLBACK TO one; + SAVEPOINT two; + RELEASE zero; + + SAVEPOINT zero; + SAVEPOINT one; + SAVEPOINT two; + RELEASE two; + SAVEPOINT two; + SAVEPOINT three; + RELEASE one; + ROLLBACK TO zero; + RELEASE zero; +} { + BEGIN 0 BEGIN 1 BEGIN 2 BEGIN 3 ROLLBACK 0 BEGIN 0 + BEGIN 1 BEGIN 2 BEGIN 3 ROLLBACK 1 BEGIN 1 BEGIN 2 + COMMIT 0 + + BEGIN 0 BEGIN 1 BEGIN 2 COMMIT 2 BEGIN 2 BEGIN 3 COMMIT 1 + ROLLBACK 0 BEGIN 0 COMMIT 0 +} + +do_transaction_test 8.4.4 { + BEGIN; + SAVEPOINT zero; + SAVEPOINT one; + SAVEPOINT two; + SAVEPOINT three; + ROLLBACK TO zero; + SAVEPOINT one; + SAVEPOINT two; + SAVEPOINT three; + ROLLBACK TO one; + SAVEPOINT two; + RELEASE zero; + + SAVEPOINT zero; + SAVEPOINT one; + SAVEPOINT two; + RELEASE two; + SAVEPOINT two; + SAVEPOINT three; + RELEASE one; + ROLLBACK TO zero; + RELEASE zero; + COMMIT; +} { + BEGIN 0 + BEGIN 1 BEGIN 2 BEGIN 3 BEGIN 4 ROLLBACK 1 BEGIN 1 + BEGIN 2 BEGIN 3 BEGIN 4 ROLLBACK 2 BEGIN 2 BEGIN 3 + COMMIT 1 + + BEGIN 1 BEGIN 2 BEGIN 3 COMMIT 3 BEGIN 3 BEGIN 4 COMMIT 2 + ROLLBACK 1 BEGIN 1 COMMIT 1 + COMMIT 0 +} finish_test + Index: test/tclsqlite.test ================================================================== --- test/tclsqlite.test +++ test/tclsqlite.test @@ -33,11 +33,11 @@ lappend v $msg } [list 1 "wrong # args: should be \"$r\""] do_test tcl-1.2 { set v [catch {db bogus} msg] lappend v $msg -} {1 {bad option "bogus": must be authorizer, backup, busy, cache, changes, close, collate, collation_needed, commit_hook, complete, copy, enable_load_extension, errorcode, eval, exists, function, incrblob, interrupt, last_insert_rowid, nullvalue, onecolumn, preupdate, profile, progress, rekey, restore, rollback_hook, status, timeout, total_changes, trace, transaction, unlock_notify, update_hook, version, or wal_hook}} +} {1 {bad option "bogus": must be authorizer, backup, busy, cache, changes, close, collate, collation_needed, commit_hook, complete, copy, enable_load_extension, errorcode, eval, exists, function, incrblob, interrupt, last_insert_rowid, nullvalue, onecolumn, preupdate, profile, progress, rekey, restore, rollback_hook, status, timeout, total_changes, trace, transaction, transaction_hook, unlock_notify, update_hook, version, or wal_hook}} do_test tcl-1.2.1 { set v [catch {db cache bogus} msg] lappend v $msg } {1 {bad option "bogus": must be flush or size}} do_test tcl-1.2.2 {