diff --git a/manifest b/manifest index 30dd63c5fe..1ebff78c20 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Use\sa\sintermediate\stable\swhen\sinserting\sa\sTEMP\stable\sfrom\sa\sSELECT\sthat\nreads\sfrom\sthat\ssame\sTEMP\stable.\s\sTicket\s#275.\s(CVS\s895) -D 2003-04-03T01:50:44 +C Added\sexperimental\sAPIs:\ssqlite_begin_hook()\sand\ssqlite_commit_hook().\s(CVS\s896) +D 2003-04-03T15:46:04 F Makefile.in 3c4ba24253e61c954d67adbbb4245e7117c5357e F Makefile.linux-gcc b86a99c493a5bfb402d1d9178dcdc4bd4b32f906 F README f1de682fbbd94899d50aca13d387d1b3fd3be2dd @@ -30,7 +30,7 @@ F src/func.c 882c3ed5a02be18cd904715c7ec62947a34a3605 F src/hash.c 4fc39feb7b7711f6495ee9f2159559bedb043e1f F src/hash.h cd0433998bc1a3759d244e1637fe5a3c13b53bf8 F src/insert.c e2f5e7feecb507d904a7da48874595f440b715aa -F src/main.c 6d9a38491fdc40c041df64a7399244c364481a09 +F src/main.c 60a64f792a9bcd2e270ef143d8cc75cd74bde8e9 F src/md5.c fe4f9c9c6f71dfc26af8da63e4d04489b1430565 F src/os.c dfed46091f69cd2d1e601f8a214d41344f2b00b6 F src/os.h aa52f0c9da321ff6134d19f2ca959e18e33615d0 @@ -42,10 +42,10 @@ F src/random.c 19e8e00fe0df32a742f115773f57651be327cabe F src/select.c 14e2e2a512f4edfc75fb310ebcb502ff3ee87402 F src/shell.c c13ff46e905a59eb1c7dbea7c1850f8f115e6395 F src/shell.tcl 27ecbd63dd88396ad16d81ab44f73e6c0ea9d20e -F src/sqlite.h.in be3e56214fecc73d72195ca62d8a3d6663602ff4 -F src/sqliteInt.h 18eb5a7f2ba010ad7d7ba81625f4b35fe3438239 +F src/sqlite.h.in f49c2cdec7d24cb03e496a1ca519e16306495ee1 +F src/sqliteInt.h 5c2371b20f69f2607ee988ba0aee85cdf9367370 F src/table.c eed2098c9b577aa17f8abe89313a9c4413f57d63 -F src/tclsqlite.c 4cb0ffa863123ae037db359849a231ff5cebfed4 +F src/tclsqlite.c 7a072c3c8ba9796edc25e5ffa62b68558134e192 F src/test1.c 7ad4e6308dde0bf5a0f0775ce20cb2ec37a328f8 F src/test2.c 5014337d8576b731cce5b5a14bec4f0daf432700 F src/test3.c c12ea7f1c3fbbd58904e81e6cb10ad424e6fc728 @@ -54,7 +54,7 @@ F src/tokenize.c 675b4718d17c69fe7609dc8e85e426ef002be811 F src/trigger.c bd5a5b234b47f28f9f21a46243dcaf1c5b2383a3 F src/update.c b368369f1fbe6d7f56a53e5ffad3b75dae9e3e1a F src/util.c 8953c612a036e30f24c1c1f5a1498176173daa37 -F src/vdbe.c b3b840b555b5238926b7ebb01cd47526a29bb853 +F src/vdbe.c 540c480912e11e23f5c08a31b8b3594df016f4d1 F src/vdbe.h 985c24f312d10f9ef8f9a8b8ea62fcdf68e82f21 F src/where.c e5733f7d5e9cc4ed3590dc3401f779e7b7bb8127 F test/all.test 569a92a8ee88f5300c057cc4a8f50fbbc69a3242 @@ -73,6 +73,7 @@ F test/expr.test 290e2617b89b5c5e3bf71f19367d285102128be4 F test/fkey1.test d65c824459916249bee501532d6154ddab0b5db7 F test/format3.test 64ab6c4db132b28a645996d413530f7b2a462cc2 F test/func.test 000515779001ac6899eec4b54e65c6e2501279d4 +F test/hook.test 7a4c97b886801d265c981dc4ec123c77af642a9d F test/in.test 3171a2b3170a8223665c1a4f26be5f3eda36cc4b F test/index.test faeb1bcf776e3ff9ba1b4be1eadd1fece708aa7b F test/insert.test 5697ba098e4d8a6f0151f281b7e39dec9c439e05 @@ -106,7 +107,7 @@ F test/sort.test 61a729023ae4ac3be9b225dc0be026fb43fec4e6 F test/subselect.test f0fea8cf9f386d416d64d152e3c65f9116d0f50f F test/table.test 371a1fc1c470982b2f68f9732f903a5d96f949c4 F test/tableapi.test 3c80421a889e1d106df16e5800fa787f0d2914a6 -F test/tclsqlite.test 4a69bd223eb18f8cdc58a9ca193881121fa7a8d2 +F test/tclsqlite.test 62773bcb94f7d7b69f1ab05c0ae07a22c737440f F test/temptable.test 6feff1960c707e924d5462356c5303943dac4a8e F test/tester.tcl d7a5835edaf118539241145d8188f0822b673488 F test/trans.test 75e7a171b5d2d94ee56766459113e2ad0e5f809d @@ -155,7 +156,7 @@ F www/speed.tcl cb4c10a722614aea76d2c51f32ee43400d5951be F www/sqlite.tcl ae3dcfb077e53833b59d4fcc94d8a12c50a44098 F www/tclsqlite.tcl 1db15abeb446aad0caf0b95b8b9579720e4ea331 F www/vdbe.tcl 2013852c27a02a091d39a766bc87cff329f21218 -P 79b3aed2a74a67cbad631c4e2e4a43469d80c162 -R 6e7a48442426732645524eccf6cea407 +P 087d1e83af12b3a9aedd4945f02774a1043b1eb4 +R 367f6fd088c73003bdf4587260d5c5b6 U drh -Z b83fa935d7c88982a4c85635851f4582 +Z 9020be80da9a15f4bf8643579ba2aa56 diff --git a/manifest.uuid b/manifest.uuid index 534960f4e9..12b051336e 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -087d1e83af12b3a9aedd4945f02774a1043b1eb4 \ No newline at end of file +5efbf62313519d0a6e8b8f5dbb29d3ef428d73e8 \ No newline at end of file diff --git a/src/main.c b/src/main.c index aa4e4b3cf1..dacf11235f 100644 --- a/src/main.c +++ b/src/main.c @@ -14,7 +14,7 @@ ** other files are for internal use by SQLite and should not be ** accessed by users of the library. ** -** $Id: main.c,v 1.119 2003/03/31 00:30:48 drh Exp $ +** $Id: main.c,v 1.120 2003/04/03 15:46:04 drh Exp $ */ #include "sqliteInt.h" #include "os.h" @@ -1023,3 +1023,29 @@ void *sqlite_trace(sqlite *db, void (*xTrace)(void*,const char*), void *pArg){ return 0; #endif } + +/* +** Register functions to be invoked when a transaction is started or when +** a transaction commits. If either function returns non-zero, then the +** corresponding operation aborts with a constraint error. +*/ +void *sqlite_begin_hook( + sqlite *db, + int (*xCallback)(void*), + void *pArg +){ + void *pOld = db->pBeginArg; + db->xBeginCallback = xCallback; + db->pBeginArg = pArg; + return pOld; +} +void *sqlite_commit_hook( + sqlite *db, + int (*xCallback)(void*), + void *pArg +){ + void *pOld = db->pCommitArg; + db->xCommitCallback = xCallback; + db->pCommitArg = pArg; + return pOld; +} diff --git a/src/sqlite.h.in b/src/sqlite.h.in index 7b224ea526..7d9e12b8c6 100644 --- a/src/sqlite.h.in +++ b/src/sqlite.h.in @@ -12,7 +12,7 @@ ** This header file defines the interface that the SQLite library ** presents to client programs. ** -** @(#) $Id: sqlite.h.in,v 1.43 2003/03/31 00:30:49 drh Exp $ +** @(#) $Id: sqlite.h.in,v 1.44 2003/04/03 15:46:04 drh Exp $ */ #ifndef _SQLITE_H_ #define _SQLITE_H_ @@ -676,6 +676,21 @@ int sqlite_step( */ int sqlite_finalize(sqlite_vm*, char **pzErrMsg); +/*** EXPERIMENTAL *** +** +** Register a callback function to be invoked whenever a new transaction +** is started or committed. The pArg argument is passed through to the +** callback. If the callback function returns non-zero, then the operation +** is aborted with a constraint error. +** +** If another function was previously registered, its pArg value is returned. +** Otherwise NULL is returned. +** +** Registering a NULL function disables the callback. +*/ +void *sqlite_begin_hook(sqlite*, int(*)(void*), void*); +void *sqlite_commit_hook(sqlite*, int(*)(void*), void*); + #ifdef __cplusplus } /* End of the 'extern "C"' block */ #endif diff --git a/src/sqliteInt.h b/src/sqliteInt.h index 93bf922621..fa1d4fd30f 100644 --- a/src/sqliteInt.h +++ b/src/sqliteInt.h @@ -11,7 +11,7 @@ ************************************************************************* ** Internal interface definitions for SQLite. ** -** @(#) $Id: sqliteInt.h,v 1.169 2003/03/31 13:36:09 drh Exp $ +** @(#) $Id: sqliteInt.h,v 1.170 2003/04/03 15:46:04 drh Exp $ */ #include "config.h" #include "sqlite.h" @@ -244,6 +244,10 @@ struct sqlite { int nTable; /* Number of tables in the database */ void *pBusyArg; /* 1st Argument to the busy callback */ int (*xBusyCallback)(void *,const char*,int); /* The busy callback */ + void *pBeginArg; /* Argument to the xBeginCallback() */ + int (*xBeginCallback)(void*); /* Invoked at every transaction start */ + void *pCommitArg; /* Argument to xCommitCallback() */ + int (*xCommitCallback)(void*);/* Invoked at every commit. */ Hash aFunc; /* All functions that can be in SQL exprs */ int lastRowid; /* ROWID of most recent insert */ int priorNewRowid; /* Last randomly generated ROWID */ diff --git a/src/tclsqlite.c b/src/tclsqlite.c index 69f70bd416..dd21cad4e3 100644 --- a/src/tclsqlite.c +++ b/src/tclsqlite.c @@ -11,7 +11,7 @@ ************************************************************************* ** A TCL Interface to SQLite ** -** $Id: tclsqlite.c,v 1.45 2003/03/30 19:17:03 drh Exp $ +** $Id: tclsqlite.c,v 1.46 2003/04/03 15:46:04 drh Exp $ */ #ifndef NO_TCL /* Omit this whole file if TCL is unavailable */ @@ -51,6 +51,8 @@ struct SqliteDb { sqlite *db; /* The "real" database structure */ Tcl_Interp *interp; /* The interpreter used for this database */ char *zBusy; /* The busy callback routine */ + char *zBegin; /* The begin-transaction callback routine */ + char *zCommit; /* The commit-transaction callback routine */ SqlFunc *pFunc; /* List of SQL functions */ int rc; /* Return code of most recent sqlite_exec() */ }; @@ -260,6 +262,12 @@ static void DbDeleteCmd(void *db){ if( pDb->zBusy ){ Tcl_Free(pDb->zBusy); } + if( pDb->zBegin ){ + Tcl_Free(pDb->zBegin); + } + if( pDb->zCommit ){ + Tcl_Free(pDb->zCommit); + } Tcl_Free((char*)pDb); } @@ -288,6 +296,39 @@ static int DbBusyHandler(void *cd, const char *zTable, int nTries){ return 1; } +/* +** This routine is called when a new transaction is started. The +** TCL script in pDb->zBegin is executed. If it returns non-zero or +** if it throws an exception, the transaction is aborted. +*/ +static int DbBeginHandler(void *cd){ + SqliteDb *pDb = (SqliteDb*)cd; + int rc; + + rc = Tcl_Eval(pDb->interp, pDb->zBegin); + if( rc!=TCL_OK || atoi(Tcl_GetStringResult(pDb->interp)) ){ + return 1; + } + return 0; +} + +/* +** This routine is called when a transaction is committed. The +** TCL script in pDb->zCommit is executed. If it returns non-zero or +** if it throws an exception, the transaction is rolled back instead +** of being committed. +*/ +static int DbCommitHandler(void *cd){ + SqliteDb *pDb = (SqliteDb*)cd; + int rc; + + rc = Tcl_Eval(pDb->interp, pDb->zCommit); + if( rc!=TCL_OK || atoi(Tcl_GetStringResult(pDb->interp)) ){ + return 1; + } + return 0; +} + /* ** This routine is called to evaluate an SQL function implemented ** using TCL script. @@ -328,15 +369,16 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){ SqliteDb *pDb = (SqliteDb*)cd; int choice; static const char *DB_strs[] = { - "busy", "changes", "close", - "complete", "errorcode", "eval", - "function", "last_insert_rowid", "timeout", - 0 + "begin_hook", "busy", "changes", + "close", "commit_hook", "complete", + "errorcode", "eval", "function", + "last_insert_rowid", "timeout", 0 }; enum DB_enum { - DB_BUSY, DB_CHANGES, DB_CLOSE, - DB_COMPLETE, DB_ERRORCODE, DB_EVAL, - DB_FUNCTION, DB_LAST_INSERT_ROWID,DB_TIMEOUT, + DB_BEGIN_HOOK, DB_BUSY, DB_CHANGES, + DB_CLOSE, DB_COMMIT_HOOK, DB_COMPLETE, + DB_ERRORCODE, DB_EVAL, DB_FUNCTION, + DB_LAST_INSERT_ROWID, DB_TIMEOUT, }; if( objc<2 ){ @@ -349,6 +391,43 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){ switch( (enum DB_enum)choice ){ + /* $db begin_callback ?CALLBACK? + ** + ** Invoke the given callback at the beginning of every SQL transaction. + ** If the callback throws an exception or returns non-zero, then the + ** transaction is aborted. If CALLBACK is an empty string, the callback + ** is disabled. + */ + case DB_BEGIN_HOOK: { + if( objc>3 ){ + Tcl_WrongNumArgs(interp, 2, objv, "?CALLBACK?"); + }else if( objc==2 ){ + if( pDb->zBegin ){ + Tcl_AppendResult(interp, pDb->zBegin, 0); + } + }else{ + char *zBegin; + int len; + if( pDb->zBegin ){ + Tcl_Free(pDb->zBegin); + } + zBegin = Tcl_GetStringFromObj(objv[2], &len); + if( zBegin && len>0 ){ + pDb->zBegin = Tcl_Alloc( len + 1 ); + strcpy(pDb->zBegin, zBegin); + }else{ + pDb->zBegin = 0; + } + if( pDb->zBegin ){ + pDb->interp = interp; + sqlite_begin_hook(pDb->db, DbBeginHandler, pDb); + }else{ + sqlite_begin_hook(pDb->db, 0, 0); + } + } + break; + } + /* $db busy ?CALLBACK? ** ** Invoke the given callback if an SQL statement attempts to open @@ -413,6 +492,43 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){ break; } + /* $db commit_hook ?CALLBACK? + ** + ** Invoke the given callback just before committing every SQL transaction. + ** If the callback throws an exception or returns non-zero, then the + ** transaction is aborted. If CALLBACK is an empty string, the callback + ** is disabled. + */ + case DB_COMMIT_HOOK: { + if( objc>3 ){ + Tcl_WrongNumArgs(interp, 2, objv, "?CALLBACK?"); + }else if( objc==2 ){ + if( pDb->zCommit ){ + Tcl_AppendResult(interp, pDb->zCommit, 0); + } + }else{ + char *zCommit; + int len; + if( pDb->zCommit ){ + Tcl_Free(pDb->zCommit); + } + zCommit = Tcl_GetStringFromObj(objv[2], &len); + if( zCommit && len>0 ){ + pDb->zCommit = Tcl_Alloc( len + 1 ); + strcpy(pDb->zCommit, zCommit); + }else{ + pDb->zCommit = 0; + } + if( pDb->zCommit ){ + pDb->interp = interp; + sqlite_commit_hook(pDb->db, DbCommitHandler, pDb); + }else{ + sqlite_commit_hook(pDb->db, 0, 0); + } + } + break; + } + /* $db complete SQL ** ** Return TRUE if SQL is a complete SQL statement. Return FALSE if diff --git a/src/vdbe.c b/src/vdbe.c index f1c0aa6f8e..5c59d8b791 100644 --- a/src/vdbe.c +++ b/src/vdbe.c @@ -36,7 +36,7 @@ ** in this file for details. If in doubt, do not deviate from existing ** commenting and indentation practices when changing or adding code. ** -** $Id: vdbe.c,v 1.210 2003/04/03 01:50:45 drh Exp $ +** $Id: vdbe.c,v 1.211 2003/04/03 15:46:04 drh Exp $ */ #include "sqliteInt.h" #include @@ -3233,6 +3233,13 @@ case OP_Transaction: { } db->aDb[i].inTrans = 1; p->undoTransOnError = 1; + if( db->xBeginCallback!=0 && i==1 && rc==SQLITE_OK ){ + if( sqliteSafetyOff(db) ) goto abort_due_to_misuse; + if( db->xBeginCallback(db->pBeginArg)!=0 ){ + rc = SQLITE_CONSTRAINT; + } + if( sqliteSafetyOn(db) ) goto abort_due_to_misuse; + } break; } @@ -3246,6 +3253,14 @@ case OP_Transaction: { */ case OP_Commit: { int i; + if( db->xCommitCallback!=0 ){ + if( sqliteSafetyOff(db) ) goto abort_due_to_misuse; + if( db->xCommitCallback(db->pCommitArg)!=0 ){ + rc = SQLITE_CONSTRAINT; + } + if( sqliteSafetyOn(db) ) goto abort_due_to_misuse; + if( rc ) break; + } assert( rc==SQLITE_OK ); for(i=0; rc==SQLITE_OK && inDb; i++){ if( db->aDb[i].inTrans ){ diff --git a/test/hook.test b/test/hook.test new file mode 100644 index 0000000000..78682b75bb --- /dev/null +++ b/test/hook.test @@ -0,0 +1,148 @@ +# 2003 April 3 +# +# 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 implements regression tests for TCL interface to the +# SQLite library. +# +# The focus of the tests in this file is the following interface: +# +# sqlite_begin_hook +# sqlite_commit_hook +# +# $Id: hook.test,v 1.1 2003/04/03 15:46:05 drh Exp $ + +set testdir [file dirname $argv0] +source $testdir/tester.tcl + +do_test hook-1.1 { + db begin_hook +} {} +do_test hook-1.2 { + db commit_hook +} {} + +do_test hook-2.1 { + set begin_cnt 0 + proc begin_hook {} { + incr ::begin_cnt + return 0 + } + db begin_hook begin_hook + db begin_hook +} {begin_hook} +do_test hook-2.2 { + set begin_cnt +} {0} +do_test hook-2.3 { + execsql { + CREATE TABLE t1(a,b); + } + set begin_cnt +} {1} +do_test hook-2.4 { + execsql { + INSERT INTO t1 VALUES(1,2); + INSERT INTO t1 SELECT a+1, b+1 FROM t1; + INSERT INTO t1 SELECT a+2, b+2 FROM t1; + } + set begin_cnt +} {4} +do_test hook-2.5 { + execsql { + SELECT * FROM t1 + } +} {1 2 2 3 3 4 4 5} +do_test hook-2.6 { + set begin_cnt +} {4} +do_test hook-2.7 { + proc begin_hook {} { + incr ::begin_cnt + return 1 + } + catchsql { + INSERT INTO t1 VALUES(9,10); + } +} {1 {constraint failed}} +do_test hook-2.8 { + set begin_cnt +} {5} +do_test hook-2.9 { + execsql { + SELECT * FROM t1; + } +} {1 2 2 3 3 4 4 5} +do_test hook-2.10 { + db begin_hook {} + db begin_hook +} {} +do_test hook-2.11 { + execsql { + INSERT INTO t1 VALUES(9,10); + SELECT * FROM t1 + } +} {1 2 2 3 3 4 4 5 9 10} + +do_test hook-3.1 { + set commit_cnt 0 + proc commit_hook {} { + incr ::commit_cnt + return 0 + } + db commit_hook ::commit_hook + db commit_hook +} {::commit_hook} +do_test hook-3.2 { + set commit_cnt +} {0} +do_test hook-3.3 { + execsql { + CREATE TABLE t2(a,b); + } + set commit_cnt +} {1} +do_test hook-3.4 { + execsql { + INSERT INTO t2 VALUES(1,2); + INSERT INTO t2 SELECT a+1, b+1 FROM t2; + INSERT INTO t2 SELECT a+2, b+2 FROM t2; + } + set commit_cnt +} {4} +do_test hook-3.5 { + set commit_cnt {} + proc commit_hook {} { + set ::commit_cnt [execsql {SELECT * FROM t2}] + return 0 + } + execsql { + INSERT INTO t2 VALUES(5,6); + } + set commit_cnt +} {1 2 2 3 3 4 4 5 5 6} +do_test hook-3.6 { + set commit_cnt {} + proc commit_hook {} { + set ::commit_cnt [execsql {SELECT * FROM t2}] + return 1 + } + catchsql { + INSERT INTO t2 VALUES(6,7); + } +} {1 {constraint failed}} +do_test hook-3.7 { + set commit_cnt +} {1 2 2 3 3 4 4 5 5 6 6 7} +do_test hook-3.8 { + execsql {SELECT * FROM t2} +} {1 2 2 3 3 4 4 5 5 6} + + +finish_test diff --git a/test/tclsqlite.test b/test/tclsqlite.test index cca0abb15c..14bf1f347a 100644 --- a/test/tclsqlite.test +++ b/test/tclsqlite.test @@ -15,7 +15,7 @@ # interface is pretty well tested. This file contains some addition # tests for fringe issues that the main test suite does not cover. # -# $Id: tclsqlite.test,v 1.10 2003/03/30 19:17:03 drh Exp $ +# $Id: tclsqlite.test,v 1.11 2003/04/03 15:46:05 drh Exp $ set testdir [file dirname $argv0] source $testdir/tester.tcl @@ -29,7 +29,7 @@ do_test tcl-1.1 { do_test tcl-1.2 { set v [catch {db bogus} msg] lappend v $msg -} {1 {bad option "bogus": must be busy, changes, close, complete, errorcode, eval, function, last_insert_rowid, or timeout}} +} {1 {bad option "bogus": must be begin_hook, busy, changes, close, commit_hook, complete, errorcode, eval, function, last_insert_rowid, or timeout}} do_test tcl-1.3 { execsql {CREATE TABLE t1(a int, b int)} execsql {INSERT INTO t1 VALUES(10,20)}