diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/delete.c | 16 | ||||
-rw-r--r-- | src/fkey.c | 3 | ||||
-rw-r--r-- | src/insert.c | 17 | ||||
-rw-r--r-- | src/main.c | 21 | ||||
-rw-r--r-- | src/shell.c | 284 | ||||
-rw-r--r-- | src/sqlite.h.in | 101 | ||||
-rw-r--r-- | src/sqliteInt.h | 12 | ||||
-rw-r--r-- | src/tclsqlite.c | 232 | ||||
-rw-r--r-- | src/test_config.c | 12 | ||||
-rw-r--r-- | src/update.c | 21 | ||||
-rw-r--r-- | src/vdbe.c | 130 | ||||
-rw-r--r-- | src/vdbe.h | 4 | ||||
-rw-r--r-- | src/vdbeInt.h | 21 | ||||
-rw-r--r-- | src/vdbeapi.c | 181 | ||||
-rw-r--r-- | src/vdbeaux.c | 84 | ||||
-rw-r--r-- | src/vdbeblob.c | 28 | ||||
-rw-r--r-- | src/vdbemem.c | 4 |
17 files changed, 1075 insertions, 96 deletions
diff --git a/src/delete.c b/src/delete.c index 0fe064bc4..6eae462ba 100644 --- a/src/delete.c +++ b/src/delete.c @@ -366,6 +366,9 @@ void sqlite3DeleteFrom( && pWhere==0 && !bComplex && !IsVirtual(pTab) +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + && db->xPreUpdateCallback==0 +#endif ){ assert( !isView ); sqlite3TableLock(pParse, iDb, pTab->tnum, 1, pTab->zName); @@ -715,14 +718,19 @@ void sqlite3GenerateRowDelete( /* Delete the index and table entries. Skip this step if pTab is really ** a view (in which case the only effect of the DELETE statement is to - ** fire the INSTEAD OF triggers). */ + ** fire the INSTEAD OF triggers). + ** + ** If variable 'count' is non-zero, then this OP_Delete instruction should + ** invoke the update-hook. The pre-update-hook, on the other hand should + ** be invoked unless table pTab is a system table. The difference is that + ** the update-hook is not invoked for rows removed by REPLACE, but the + ** pre-update-hook is. + */ if( pTab->pSelect==0 ){ u8 p5 = 0; sqlite3GenerateRowIndexDelete(pParse, pTab, iDataCur, iIdxCur,0,iIdxNoSeek); sqlite3VdbeAddOp2(v, OP_Delete, iDataCur, (count?OPFLAG_NCHANGE:0)); - if( count ){ - sqlite3VdbeChangeP4(v, -1, pTab->zName, P4_TRANSIENT); - } + sqlite3VdbeChangeP4(v, -1, (char*)pTab, P4_TABLE); if( eMode!=ONEPASS_OFF ){ sqlite3VdbeChangeP5(v, OPFLAG_AUXDELETE); } diff --git a/src/fkey.c b/src/fkey.c index 9a1a85fec..4f2740ec3 100644 --- a/src/fkey.c +++ b/src/fkey.c @@ -1167,6 +1167,9 @@ static Trigger *fkActionTrigger( } pTrigger = pFKey->apTrigger[iAction]; + if( (db->flags & SQLITE_DeferFKs) && action==OE_Restrict ){ + return 0; + } if( action!=OE_None && !pTrigger ){ char const *zFrom; /* Name of child table */ diff --git a/src/insert.c b/src/insert.c index 03fdf09ac..5dc045ab6 100644 --- a/src/insert.c +++ b/src/insert.c @@ -1431,9 +1431,18 @@ void sqlite3GenerateConstraintChecks( if( pTrigger || sqlite3FkRequired(pParse, pTab, 0, 0) ){ sqlite3MultiWrite(pParse); sqlite3GenerateRowDelete(pParse, pTab, pTrigger, iDataCur, iIdxCur, - regNewData, 1, 0, OE_Replace, - ONEPASS_SINGLE, -1); + regNewData, 1, 0, OE_Replace, 1, -1); }else{ +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + if( HasRowid(pTab) ){ + /* This OP_Delete opcode fires the pre-update-hook only. It does + ** not modify the b-tree. It is more efficient to let the coming + ** OP_Insert replace the existing entry than it is to delete the + ** existing entry and then insert a new one. */ + sqlite3VdbeAddOp2(v, OP_Delete, iDataCur, OPFLAG_ISNOOP); + sqlite3VdbeChangeP4(v, -1, (char *)pTab, P4_TABLE); + } +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ if( pTab->pIndex ){ sqlite3MultiWrite(pParse); sqlite3GenerateRowIndexDelete(pParse, pTab, iDataCur, iIdxCur,0,-1); @@ -1703,7 +1712,7 @@ void sqlite3CompleteInsertion( } sqlite3VdbeAddOp3(v, OP_Insert, iDataCur, regRec, regNewData); if( !pParse->nested ){ - sqlite3VdbeChangeP4(v, -1, pTab->zName, P4_TRANSIENT); + sqlite3VdbeChangeP4(v, -1, (char *)pTab, P4_TABLE); } sqlite3VdbeChangeP5(v, pik_flags); } @@ -2103,7 +2112,7 @@ static int xferOptimization( } sqlite3VdbeAddOp2(v, OP_RowData, iSrc, regData); sqlite3VdbeAddOp4(v, OP_Insert, iDest, regData, regRowid, - pDest->zName, 0); + (char*)pDest, P4_TABLE); sqlite3VdbeChangeP5(v, OPFLAG_NCHANGE|OPFLAG_LASTROWID|OPFLAG_APPEND); sqlite3VdbeAddOp2(v, OP_Next, iSrc, addr1); VdbeCoverage(v); sqlite3VdbeAddOp2(v, OP_Close, iSrc, 0); diff --git a/src/main.c b/src/main.c index 70e46a4ee..40a192e16 100644 --- a/src/main.c +++ b/src/main.c @@ -1922,6 +1922,27 @@ void *sqlite3_rollback_hook( return pRet; } +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** Register a callback to be invoked each time a row is updated, +** inserted or deleted using this database connection. +*/ +void *sqlite3_preupdate_hook( + sqlite3 *db, /* Attach the hook to this database */ + void(*xCallback)( /* Callback function */ + void*,sqlite3*,int,char const*,char const*,sqlite3_int64,sqlite3_int64), + void *pArg /* First callback argument */ +){ + void *pRet; + sqlite3_mutex_enter(db->mutex); + pRet = db->pPreUpdateArg; + db->xPreUpdateCallback = xCallback; + db->pPreUpdateArg = pArg; + sqlite3_mutex_leave(db->mutex); + return pRet; +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + #ifndef SQLITE_OMIT_WAL /* ** The sqlite3_wal_hook() callback registered by sqlite3_wal_autocheckpoint(). diff --git a/src/shell.c b/src/shell.c index 789a464d0..2ce2f292d 100644 --- a/src/shell.c +++ b/src/shell.c @@ -539,6 +539,19 @@ static char *one_input_line(FILE *in, char *zPrior, int isContinuation){ return zResult; } +#if defined(SQLITE_ENABLE_SESSION) +/* +** State information for a single open session +*/ +typedef struct OpenSession OpenSession; +struct OpenSession { + char *zName; /* Symbolic name for this session */ + int nFilter; /* Number of xFilter rejection GLOB patterns */ + char **azFilter; /* Array of xFilter rejection GLOB patterns */ + sqlite3_session *p; /* The open session */ +}; +#endif + /* ** Render output like fprintf(). Except, if the output is going to the ** console and if this is running on a Windows machine, translate the @@ -625,6 +638,10 @@ struct ShellState { int *aiIndent; /* Array of indents used in MODE_Explain */ int nIndent; /* Size of array aiIndent[] */ int iIndent; /* Index of current op in aiIndent[] */ +#if defined(SQLITE_ENABLE_SESSION) + int nSession; /* Number of active sessions */ + OpenSession aSession[4]; /* Array of sessions. [0] is in focus. */ +#endif }; /* @@ -1996,6 +2013,9 @@ static char zHelp[] = " LIKE pattern TABLE.\n" ".separator COL ?ROW? Change the column separator and optionally the row\n" " separator for both the output mode and .import\n" +#if defined(SQLITE_ENABLE_SESSION) + ".session CMD ... Create or control sessions\n" +#endif ".shell CMD ARGS... Run CMD ARGS... in a system shell\n" ".show Show the current values for various settings\n" ".stats ?on|off? Show stats or turn stats on or off\n" @@ -2013,6 +2033,30 @@ static char zHelp[] = " Negative values right-justify\n" ; +#if defined(SQLITE_ENABLE_SESSION) +/* +** Print help information for the ".sessions" command +*/ +void session_help(ShellState *p){ + fprintf(p->out, + ".session ?NAME? SUBCOMMAND ?ARGS...?\n" + "If ?NAME? is omitted, the first defined session is used.\n" + "Subcommands:\n" + " attach TABLE Attach TABLE\n" + " changeset FILE Write a changeset into FILE\n" + " close Close one session\n" + " enable ?BOOLEAN? Set or query the enable bit\n" + " filter GLOB... Reject tables matching GLOBs\n" + " indirect ?BOOLEAN? Mark or query the indirect status\n" + " isempty Query whether the session is empty\n" + " list List currently open session names\n" + " open DB NAME Open a new session on DB\n" + " patchset FILE Write a patchset into FILE\n" + ); +} +#endif + + /* Forward reference */ static int process_input(ShellState *p, FILE *in); /* @@ -2078,6 +2122,51 @@ static void writefileFunc( sqlite3_result_int64(context, rc); } +#if defined(SQLITE_ENABLE_SESSION) +/* +** Close a single OpenSession object and release all of its associated +** resources. +*/ +static void session_close(OpenSession *pSession){ + int i; + sqlite3session_delete(pSession->p); + sqlite3_free(pSession->zName); + for(i=0; i<pSession->nFilter; i++){ + sqlite3_free(pSession->azFilter[i]); + } + sqlite3_free(pSession->azFilter); + memset(pSession, 0, sizeof(OpenSession)); +} +#endif + +/* +** Close all OpenSession objects and release all assocaited resources. +*/ +static void session_close_all(ShellState *p){ +#if defined(SQLITE_ENABLE_SESSION) + int i; + for(i=0; i<p->nSession; i++){ + session_close(&p->aSession[i]); + } + p->nSession = 0; +#endif +} + +/* +** Implementation of the xFilter function for an open session. Omit +** any tables named by ".session filter" but let all other table through. +*/ +#if defined(SQLITE_ENABLE_SESSION) +static int session_filter(void *pCtx, const char *zTab){ + OpenSession *pSession = (OpenSession*)pCtx; + int i; + for(i=0; i<pSession->nFilter; i++){ + if( sqlite3_strglob(pSession->azFilter[i], zTab)==0 ) return 0; + } + return 1; +} +#endif + /* ** Make sure the database is open. If it is not, then open it. If ** the database fails to open, print an error message and exit. @@ -3534,6 +3623,7 @@ static int do_meta_command(char *zLine, ShellState *p){ p->zDbFilename = zNewFilename; open_db(p, 1); if( p->db!=0 ){ + session_close_all(p); sqlite3_close(savedDb); sqlite3_free(p->zFreeOnClose); p->zFreeOnClose = zNewFilename; @@ -3774,7 +3864,6 @@ static int do_meta_command(char *zLine, ShellState *p){ } }else - #if defined(SQLITE_DEBUG) && defined(SQLITE_ENABLE_SELECTTRACE) if( c=='s' && n==11 && strncmp(azArg[0], "selecttrace", n)==0 ){ extern int sqlite3SelectTrace; @@ -3782,6 +3871,198 @@ static int do_meta_command(char *zLine, ShellState *p){ }else #endif +#if defined(SQLITE_ENABLE_SESSION) + if( c=='s' && strncmp(azArg[0],"session",n)==0 && n>=3 ){ + OpenSession *pSession = &p->aSession[0]; + char **azCmd = &azArg[1]; + int iSes = 0; + int nCmd = nArg - 1; + int i; + if( nArg<=1 ) goto session_syntax_error; + open_db(p, 0); + if( nArg>=3 ){ + for(iSes=0; iSes<p->nSession; iSes++){ + if( strcmp(p->aSession[iSes].zName, azArg[1])==0 ) break; + } + if( iSes<p->nSession ){ + pSession = &p->aSession[iSes]; + azCmd++; + nCmd--; + }else{ + pSession = &p->aSession[0]; + iSes = 0; + } + } + + /* .session attach TABLE + ** Invoke the sqlite3session_attach() interface to attach a particular + ** table so that it is never filtered. + */ + if( strcmp(azCmd[0],"attach")==0 ){ + if( nCmd!=2 ) goto session_syntax_error; + if( pSession->p==0 ){ + session_not_open: + fprintf(stderr, "ERROR: No sessions are open\n"); + }else{ + rc = sqlite3session_attach(pSession->p, azCmd[1]); + if( rc ){ + fprintf(stderr, "ERROR: sqlite3session_attach() returns %d\n", rc); + rc = 0; + } + } + }else + + /* .session changeset FILE + ** .session patchset FILE + ** Write a changeset or patchset into a file. The file is overwritten. + */ + if( strcmp(azCmd[0],"changeset")==0 || strcmp(azCmd[0],"patchset")==0 ){ + FILE *out = 0; + if( nCmd!=2 ) goto session_syntax_error; + if( pSession->p==0 ) goto session_not_open; + out = fopen(azCmd[1], "wb"); + if( out==0 ){ + fprintf(stderr, "ERROR: cannot open \"%s\" for writing\n", azCmd[1]); + }else{ + int szChng; + void *pChng; + if( azCmd[0][0]=='c' ){ + rc = sqlite3session_changeset(pSession->p, &szChng, &pChng); + }else{ + rc = sqlite3session_patchset(pSession->p, &szChng, &pChng); + } + if( rc ){ + printf("Error: error code %d\n", rc); + rc = 0; + } + if( pChng + && fwrite(pChng, szChng, 1, out)!=1 ){ + fprintf(stderr, "ERROR: Failed to write entire %d-byte output\n", + szChng); + } + sqlite3_free(pChng); + fclose(out); + } + }else + + /* .session close + ** Close the identified session + */ + if( strcmp(azCmd[0], "close")==0 ){ + if( nCmd!=1 ) goto session_syntax_error; + if( p->nSession ){ + session_close(pSession); + p->aSession[iSes] = p->aSession[--p->nSession]; + } + }else + + /* .session enable ?BOOLEAN? + ** Query or set the enable flag + */ + if( strcmp(azCmd[0], "enable")==0 ){ + int ii; + if( nCmd>2 ) goto session_syntax_error; + ii = nCmd==1 ? -1 : booleanValue(azCmd[1]); + if( p->nSession ){ + ii = sqlite3session_enable(pSession->p, ii); + fprintf(p->out, "session %s enable flag = %d\n", pSession->zName, ii); + } + }else + + /* .session filter GLOB .... + ** Set a list of GLOB patterns of table names to be excluded. + */ + if( strcmp(azCmd[0], "filter")==0 ){ + int ii, nByte; + if( nCmd<2 ) goto session_syntax_error; + if( p->nSession ){ + for(ii=0; ii<pSession->nFilter; ii++){ + sqlite3_free(pSession->azFilter[ii]); + } + sqlite3_free(pSession->azFilter); + nByte = sizeof(pSession->azFilter[0])*(nCmd-1); + pSession->azFilter = sqlite3_malloc( nByte ); + if( pSession->azFilter==0 ){ + fprintf(stderr, "Error: out or memory\n"); + exit(1); + } + for(ii=1; ii<nCmd; ii++){ + pSession->azFilter[ii-1] = sqlite3_mprintf("%s", azCmd[ii]); + } + pSession->nFilter = ii-1; + } + }else + + /* .session indirect ?BOOLEAN? + ** Query or set the indirect flag + */ + if( strcmp(azCmd[0], "indirect")==0 ){ + int ii; + if( nCmd>2 ) goto session_syntax_error; + ii = nCmd==1 ? -1 : booleanValue(azCmd[1]); + if( p->nSession ){ + ii = sqlite3session_indirect(pSession->p, ii); + fprintf(p->out, "session %s indirect flag = %d\n", pSession->zName,ii); + } + }else + + /* .session isempty + ** Determine if the session is empty + */ + if( strcmp(azCmd[0], "isempty")==0 ){ + int ii; + if( nCmd!=1 ) goto session_syntax_error; + if( p->nSession ){ + ii = sqlite3session_isempty(pSession->p); + fprintf(p->out, "session %s isempty flag = %d\n", pSession->zName, ii); + } + }else + + /* .session list + ** List all currently open sessions + */ + if( strcmp(azCmd[0],"list")==0 ){ + for(i=0; i<p->nSession; i++){ + fprintf(p->out, "%d %s\n", i, p->aSession[i].zName); + } + }else + + /* .session open DB NAME + ** Open a new session called NAME on the attached database DB. + ** DB is normally "main". + */ + if( strcmp(azCmd[0],"open")==0 ){ + char *zName; + if( nCmd!=3 ) goto session_syntax_error; + zName = azCmd[2]; + if( zName[0]==0 ) goto session_syntax_error; + for(i=0; i<p->nSession; i++){ + if( strcmp(p->aSession[i].zName,zName)==0 ){ + fprintf(stderr, "Session \"%s\" already exists\n", zName); + goto meta_command_exit; + } + } + if( p->nSession>=ArraySize(p->aSession) ){ + fprintf(stderr, "Maximum of %d sessions\n", ArraySize(p->aSession)); + goto meta_command_exit; + } + pSession = &p->aSession[p->nSession]; + rc = sqlite3session_create(p->db, azCmd[1], &pSession->p); + if( rc ){ + fprintf(stderr, "Cannot open session: error code=%d\n", rc); + rc = 0; + goto meta_command_exit; + } + pSession->nFilter = 0; + sqlite3session_table_filter(pSession->p, session_filter, pSession); + p->nSession++; + pSession->zName = sqlite3_mprintf("%s", zName); + }else + /* If no command name matches, show a syntax error */ + session_syntax_error: + session_help(p); + }else +#endif #ifdef SQLITE_DEBUG /* Undocumented commands for internal testing. Subject to change @@ -5049,6 +5330,7 @@ int SQLITE_CDECL main(int argc, char **argv){ } set_table_name(&data, 0); if( data.db ){ + session_close_all(&data); sqlite3_close(data.db); } sqlite3_free(data.zFreeOnClose); diff --git a/src/sqlite.h.in b/src/sqlite.h.in index 3b267974b..cce23aa22 100644 --- a/src/sqlite.h.in +++ b/src/sqlite.h.in @@ -5226,8 +5226,8 @@ void *sqlite3_rollback_hook(sqlite3*, void(*)(void *), void*); ** on the same [database connection] D, or NULL for ** the first call on D. ** -** See also the [sqlite3_commit_hook()] and [sqlite3_rollback_hook()] -** interfaces. +** See also the [sqlite3_commit_hook()], [sqlite3_rollback_hook()], +** and [sqlite3_preupdate_hook()] interfaces. */ void *sqlite3_update_hook( sqlite3*, @@ -6761,7 +6761,7 @@ int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int resetFlg); ** database file in rollback mode databases. Any pages written as part of ** transaction rollback or database recovery operations are not included. ** If an IO or other error occurs while writing a page to disk, the effect -** on subsequent SQLITE_DBSTATUS_CACHE_WRITE requests is undefined.)^ ^The +** on subsequent SQLITE_DBSTATUS_CACHE_WRITE requests is undefined). ^The ** highwater mark associated with SQLITE_DBSTATUS_CACHE_WRITE is always 0. ** </dd> ** @@ -7916,6 +7916,101 @@ void sqlite3_stmt_scanstatus_reset(sqlite3_stmt*); int sqlite3_db_cacheflush(sqlite3*); /* +** CAPI3REF: The pre-update hook. +** EXPERIMENTAL +** +** ^These interfaces are only available if SQLite is compiled using the +** [SQLITE_ENABLE_UPDATE_HOOK] compile-time option. +** +** ^The [sqlite3_preupdate_hook()] interface registers a callback function +** that is invoked prior to each [INSERT], [UPDATE], and [DELETE] operation. +** ^At most one preupdate hook may be registered at a time on a single +** [database connection]; each call to [sqlite3_preupdate_hook()] overrides +** the previous setting. +** ^The preupdate hook is disabled by invoking [sqlite3_preupdate_hook()] +** with a NULL pointer as the second parameter. +** ^The third parameter to [sqlite3_preupdate_hook()] is passed through as +** the first parameter to callbacks. +** +** ^The preupdate hook only fires for changes to real tables; the preupdate +** hook is not invoked for changes to virtual tables. +** +** ^The second parameter to the preupdate callback is a pointer to +** the [database connection] that registered the preupdate hook. +** ^The third parameter to the preupdate callback is one of the constants +** [SQLITE_INSERT], [SQLITE_DELETE], or [SQLITE_UPDATE] to indentify the +** kind of update operation that is about to occur. +** ^(The fourth parameter to the preupdate callback is the name of the +** database within the database connection that is being modified. This +** will be "main" for the main database or "temp" for TEMP tables or +** the name given after the AS keyword in the [ATTACH] statement for attached +** databases.)^ +** ^The fifth parameter to the preupdate callback is the name of the +** table that is being modified. +** ^The sixth parameter to the preupdate callback is the initial [rowid] of the +** row being changes for SQLITE_UPDATE and SQLITE_DELETE changes and is +** undefined for SQLITE_INSERT changes. +** ^The seventh parameter to the preupdate callback is the final [rowid] of +** the row being changed for SQLITE_UPDATE and SQLITE_INSERT changes and is +** undefined for SQLITE_DELETE changes. +** +** The [sqlite3_preupdate_old()], [sqlite3_preupdate_new()], +** [sqlite3_preupdate_count()], and [sqlite3_preupdate_depth()] interfaces +** provide additional information about a preupdate event. These routines +** may only be called from within a preupdate callback. Invoking any of +** these routines from outside of a preupdate callback or with a +** [database connection] pointer that is different from the one supplied +** to the preupdate callback results in undefined and probably undesirable +** behavior. +** +** ^The [sqlite3_preupdate_count(D)] interface returns the number of columns +** in the row that is being inserted, updated, or deleted. +** +** ^The [sqlite3_preupdate_old(D,N,P)] interface writes into P a pointer to +** a [protected sqlite3_value] that contains the value of the Nth column of +** the table row before it is updated. The N parameter must be between 0 +** and one less than the number of columns or the behavior will be +** undefined. This must only be used within SQLITE_UPDATE and SQLITE_DELETE +** preupdate callbacks; if it is used by an SQLITE_INSERT callback then the +** behavior is undefined. The [sqlite3_value] that P points to +** will be destroyed when the preupdate callback returns. +** +** ^The [sqlite3_preupdate_new(D,N,P)] interface writes into P a pointer to +** a [protected sqlite3_value] that contains the value of the Nth column of +** the table row after it is updated. The N parameter must be between 0 +** and one less than the number of columns or the behavior will be +** undefined. This must only be used within SQLITE_INSERT and SQLITE_UPDATE +** preupdate callbacks; if it is used by an SQLITE_DELETE callback then the +** behavior is undefined. The [sqlite3_value] that P points to +** will be destroyed when the preupdate callback returns. +** +** ^The [sqlite3_preupdate_depth(D)] interface returns 0 if the preupdate +** callback was invoked as a result of a direct insert, update, or delete +** operation; or 1 for inserts, updates, or deletes invoked by top-level +** triggers; or 2 for changes resulting from triggers called by top-level +** triggers; and so forth. +** +** See also: [sqlite3_update_hook()] +*/ +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 */ + char const *zDb, /* Database name */ + char const *zName, /* Table name */ + 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_count(sqlite3 *); +SQLITE_EXPERIMENTAL int sqlite3_preupdate_depth(sqlite3 *); +SQLITE_EXPERIMENTAL int sqlite3_preupdate_new(sqlite3 *, int, sqlite3_value **); + +/* ** CAPI3REF: Database Snapshot ** KEYWORDS: {snapshot} ** EXPERIMENTAL diff --git a/src/sqliteInt.h b/src/sqliteInt.h index f2f485778..de357537e 100644 --- a/src/sqliteInt.h +++ b/src/sqliteInt.h @@ -978,6 +978,7 @@ typedef struct LookasideSlot LookasideSlot; typedef struct Module Module; typedef struct NameContext NameContext; typedef struct Parse Parse; +typedef struct PreUpdate PreUpdate; typedef struct PrintfArguments PrintfArguments; typedef struct RowSet RowSet; typedef struct Savepoint Savepoint; @@ -1261,6 +1262,13 @@ struct sqlite3 { void (*xRollbackCallback)(void*); /* Invoked at every commit. */ void *pUpdateArg; void (*xUpdateCallback)(void*,int, const char*,const char*,sqlite_int64); +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + 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 */ +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ #ifndef SQLITE_OMIT_WAL int (*xWalCallback)(void *, sqlite3 *, const char *, int); void *pWalArg; @@ -2907,6 +2915,9 @@ struct AuthContext { /* ** Bitfield flags for P5 value in various opcodes. +** +** Note that the values for ISNOOP and LENGTHARG are the same. But as +** those bits are never used on the same opcode, the overlap is harmless. */ #define OPFLAG_NCHANGE 0x01 /* OP_Insert: Set to update db->nChange */ /* Also used in P2 (not P5) of OP_Delete */ @@ -2915,6 +2926,7 @@ struct AuthContext { #define OPFLAG_ISUPDATE 0x04 /* This OP_Insert is an sql UPDATE */ #define OPFLAG_APPEND 0x08 /* This is likely to be an append */ #define OPFLAG_USESEEKRESULT 0x10 /* Try to avoid a seek in BtreeInsert() */ +#define OPFLAG_ISNOOP 0x40 /* OP_Delete does pre-update-hook only */ #define OPFLAG_LENGTHARG 0x40 /* OP_Column only used for length() */ #define OPFLAG_TYPEOFARG 0x80 /* OP_Column only used for typeof() */ #define OPFLAG_BULKCSR 0x01 /* OP_Open** used to open bulk cursor */ diff --git a/src/tclsqlite.c b/src/tclsqlite.c index 42dd6b0f4..96b5691f1 100644 --- a/src/tclsqlite.c +++ b/src/tclsqlite.c @@ -140,6 +140,7 @@ struct SqliteDb { 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 *pWalHook; /* WAL hook script (if any) */ Tcl_Obj *pUnlockNotify; /* Unlock notify script (if any) */ @@ -519,6 +520,9 @@ static void DbDeleteCmd(void *db){ if( pDb->pUpdateHook ){ Tcl_DecrRefCount(pDb->pUpdateHook); } + if( pDb->pPreUpdateHook ){ + Tcl_DecrRefCount(pDb->pPreUpdateHook); + } if( pDb->pRollbackHook ){ Tcl_DecrRefCount(pDb->pRollbackHook); } @@ -686,6 +690,42 @@ static void DbUnlockNotify(void **apArg, int nArg){ } #endif +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** Pre-update hook callback. +*/ +static void DbPreUpdateHandler( + void *p, + sqlite3 *db, + int op, + const char *zDb, + const char *zTbl, + sqlite_int64 iKey1, + sqlite_int64 iKey2 +){ + SqliteDb *pDb = (SqliteDb *)p; + Tcl_Obj *pCmd; + static const char *azStr[] = {"DELETE", "INSERT", "UPDATE"}; + + assert( (SQLITE_DELETE-1)/9 == 0 ); + assert( (SQLITE_INSERT-1)/9 == 1 ); + assert( (SQLITE_UPDATE-1)/9 == 2 ); + assert( pDb->pPreUpdateHook ); + assert( db==pDb->db ); + assert( op==SQLITE_INSERT || op==SQLITE_UPDATE || op==SQLITE_DELETE ); + + pCmd = Tcl_DuplicateObj(pDb->pPreUpdateHook); + Tcl_IncrRefCount(pCmd); + Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(azStr[(op-1)/9], -1)); + Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(zDb, -1)); + Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(zTbl, -1)); + Tcl_ListObjAppendElement(0, pCmd, Tcl_NewWideIntObj(iKey1)); + Tcl_ListObjAppendElement(0, pCmd, Tcl_NewWideIntObj(iKey2)); + Tcl_EvalObjEx(pDb->interp, pCmd, TCL_EVAL_DIRECT); + Tcl_DecrRefCount(pCmd); +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + static void DbUpdateHandler( void *p, int op, @@ -695,14 +735,18 @@ static void DbUpdateHandler( ){ SqliteDb *pDb = (SqliteDb *)p; Tcl_Obj *pCmd; + static const char *azStr[] = {"DELETE", "INSERT", "UPDATE"}; + + assert( (SQLITE_DELETE-1)/9 == 0 ); + assert( (SQLITE_INSERT-1)/9 == 1 ); + assert( (SQLITE_UPDATE-1)/9 == 2 ); assert( pDb->pUpdateHook ); assert( op==SQLITE_INSERT || op==SQLITE_UPDATE || op==SQLITE_DELETE ); pCmd = Tcl_DuplicateObj(pDb->pUpdateHook); Tcl_IncrRefCount(pCmd); - Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj( - ( (op==SQLITE_INSERT)?"INSERT":(op==SQLITE_UPDATE)?"UPDATE":"DELETE"), -1)); + Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(azStr[(op-1)/9], -1)); Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(zDb, -1)); Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(zTbl, -1)); Tcl_ListObjAppendElement(0, pCmd, Tcl_NewWideIntObj(rowid)); @@ -1615,6 +1659,46 @@ static int DbEvalNextCmd( } /* +** This function is used by the implementations of the following database +** handle sub-commands: +** +** $db update_hook ?SCRIPT? +** $db wal_hook ?SCRIPT? +** $db commit_hook ?SCRIPT? +** $db preupdate hook ?SCRIPT? +*/ +static void DbHookCmd( + Tcl_Interp *interp, /* Tcl interpreter */ + SqliteDb *pDb, /* Database handle */ + Tcl_Obj *pArg, /* SCRIPT argument (or NULL) */ + Tcl_Obj **ppHook /* Pointer to member of SqliteDb */ +){ + sqlite3 *db = pDb->db; + + if( *ppHook ){ + Tcl_SetObjResult(interp, *ppHook); + if( pArg ){ + Tcl_DecrRefCount(*ppHook); + *ppHook = 0; + } + } + if( pArg ){ + assert( !(*ppHook) ); + if( Tcl_GetCharLength(pArg)>0 ){ + *ppHook = pArg; + Tcl_IncrRefCount(*ppHook); + } + } + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + sqlite3_preupdate_hook(db, (pDb->pPreUpdateHook?DbPreUpdateHandler:0), pDb); +#endif + 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); +} + +/* ** The "sqlite" command below creates a new Tcl command for each ** connection it opens to an SQLite database. This routine is invoked ** whenever one of those connection-specific commands is executed @@ -1639,11 +1723,12 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){ "errorcode", "eval", "exists", "function", "incrblob", "interrupt", "last_insert_rowid", "nullvalue", "onecolumn", - "profile", "progress", "rekey", - "restore", "rollback_hook", "status", - "timeout", "total_changes", "trace", - "transaction", "unlock_notify", "update_hook", - "version", "wal_hook", 0 + "preupdate", "profile", "progress", + "rekey", "restore", "rollback_hook", + "status", "timeout", "total_changes", + "trace", "transaction", "unlock_notify", + "update_hook", "version", "wal_hook", + 0 }; enum DB_enum { DB_AUTHORIZER, DB_BACKUP, DB_BUSY, @@ -1653,11 +1738,11 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){ DB_ERRORCODE, DB_EVAL, DB_EXISTS, DB_FUNCTION, DB_INCRBLOB, DB_INTERRUPT, DB_LAST_INSERT_ROWID, DB_NULLVALUE, DB_ONECOLUMN, - 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_VERSION, DB_WAL_HOOK + 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_VERSION, DB_WAL_HOOK, }; /* don't leave trailing commas on DB_enum, it confuses the AIX xlc compiler */ @@ -2856,6 +2941,90 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){ } /* + ** $db preupdate_hook count + ** $db preupdate_hook hook ?SCRIPT? + ** $db preupdate_hook new INDEX + ** $db preupdate_hook old INDEX + */ + case DB_PREUPDATE: { +#ifndef SQLITE_ENABLE_PREUPDATE_HOOK + Tcl_AppendResult(interp, "preupdate_hook was omitted at compile-time"); + rc = TCL_ERROR; +#else + static const char *azSub[] = {"count", "depth", "hook", "new", "old", 0}; + enum DbPreupdateSubCmd { + PRE_COUNT, PRE_DEPTH, PRE_HOOK, PRE_NEW, PRE_OLD + }; + int iSub; + + if( objc<3 ){ + Tcl_WrongNumArgs(interp, 2, objv, "SUB-COMMAND ?ARGS?"); + } + if( Tcl_GetIndexFromObj(interp, objv[2], azSub, "sub-command", 0, &iSub) ){ + return TCL_ERROR; + } + + switch( (enum DbPreupdateSubCmd)iSub ){ + case PRE_COUNT: { + int nCol = sqlite3_preupdate_count(pDb->db); + Tcl_SetObjResult(interp, Tcl_NewIntObj(nCol)); + break; + } + + case PRE_HOOK: { + if( objc>4 ){ + Tcl_WrongNumArgs(interp, 2, objv, "hook ?SCRIPT?"); + return TCL_ERROR; + } + DbHookCmd(interp, pDb, (objc==4 ? objv[3] : 0), &pDb->pPreUpdateHook); + break; + } + + case PRE_DEPTH: { + Tcl_Obj *pRet; + if( objc!=3 ){ + Tcl_WrongNumArgs(interp, 3, objv, ""); + return TCL_ERROR; + } + pRet = Tcl_NewIntObj(sqlite3_preupdate_depth(pDb->db)); + Tcl_SetObjResult(interp, pRet); + break; + } + + case PRE_NEW: + case PRE_OLD: { + int iIdx; + sqlite3_value *pValue; + if( objc!=4 ){ + Tcl_WrongNumArgs(interp, 3, objv, "INDEX"); + return TCL_ERROR; + } + if( Tcl_GetIntFromObj(interp, objv[3], &iIdx) ){ + return TCL_ERROR; + } + + if( iSub==PRE_OLD ){ + rc = sqlite3_preupdate_old(pDb->db, iIdx, &pValue); + }else{ + assert( iSub==PRE_NEW ); + rc = sqlite3_preupdate_new(pDb->db, iIdx, &pValue); + } + + if( rc==SQLITE_OK ){ + Tcl_Obj *pObj; + pObj = Tcl_NewStringObj((char*)sqlite3_value_text(pValue), -1); + Tcl_SetObjResult(interp, pObj); + }else{ + Tcl_AppendResult(interp, sqlite3_errmsg(pDb->db), 0); + return TCL_ERROR; + } + } + } +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + break; + } + + /* ** $db wal_hook ?script? ** $db update_hook ?script? ** $db rollback_hook ?script? @@ -2863,42 +3032,19 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){ case DB_WAL_HOOK: case DB_UPDATE_HOOK: case DB_ROLLBACK_HOOK: { - /* 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_UPDATE_HOOK ){ - ppHook = &pDb->pUpdateHook; - }else if( choice==DB_WAL_HOOK ){ - ppHook = &pDb->pWalHook; - }else{ - ppHook = &pDb->pRollbackHook; - } - - if( objc!=2 && objc!=3 ){ + 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( objc>3 ){ Tcl_WrongNumArgs(interp, 2, objv, "?SCRIPT?"); return TCL_ERROR; } - if( *ppHook ){ - Tcl_SetObjResult(interp, *ppHook); - if( objc==3 ){ - Tcl_DecrRefCount(*ppHook); - *ppHook = 0; - } - } - if( objc==3 ){ - assert( !(*ppHook) ); - if( Tcl_GetCharLength(objv[2])>0 ){ - *ppHook = objv[2]; - Tcl_IncrRefCount(*ppHook); - } - } - - sqlite3_update_hook(pDb->db, (pDb->pUpdateHook?DbUpdateHandler:0), pDb); - sqlite3_rollback_hook(pDb->db,(pDb->pRollbackHook?DbRollbackHandler:0),pDb); - sqlite3_wal_hook(pDb->db,(pDb->pWalHook?DbWalHandler:0),pDb); + DbHookCmd(interp, pDb, (objc==3 ? objv[2] : 0), ppHook); break; } @@ -3772,6 +3918,9 @@ static void init_all(Tcl_Interp *interp){ extern int Sqlitemultiplex_Init(Tcl_Interp*); extern int SqliteSuperlock_Init(Tcl_Interp*); extern int SqlitetestSyscall_Init(Tcl_Interp*); +#if defined(SQLITE_ENABLE_SESSION) && defined(SQLITE_ENABLE_PREUPDATE_HOOK) + extern int TestSession_Init(Tcl_Interp*); +#endif extern int Fts5tcl_Init(Tcl_Interp *); extern int SqliteRbu_Init(Tcl_Interp*); extern int Sqlitetesttcl_Init(Tcl_Interp*); @@ -3817,6 +3966,9 @@ static void init_all(Tcl_Interp *interp){ Sqlitemultiplex_Init(interp); SqliteSuperlock_Init(interp); SqlitetestSyscall_Init(interp); +#if defined(SQLITE_ENABLE_SESSION) && defined(SQLITE_ENABLE_PREUPDATE_HOOK) + TestSession_Init(interp); +#endif Fts5tcl_Init(interp); SqliteRbu_Init(interp); Sqlitetesttcl_Init(interp); diff --git a/src/test_config.c b/src/test_config.c index 30b421e00..f712ebac6 100644 --- a/src/test_config.c +++ b/src/test_config.c @@ -143,6 +143,12 @@ static void set_options(Tcl_Interp *interp){ Tcl_SetVar2(interp, "sqlite_options", "mem5", "0", TCL_GLOBAL_ONLY); #endif +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + Tcl_SetVar2(interp, "sqlite_options", "preupdate", "1", TCL_GLOBAL_ONLY); +#else + Tcl_SetVar2(interp, "sqlite_options", "preupdate", "0", TCL_GLOBAL_ONLY); +#endif + #ifdef SQLITE_ENABLE_SNAPSHOT Tcl_SetVar2(interp, "sqlite_options", "snapshot", "1", TCL_GLOBAL_ONLY); #else @@ -521,6 +527,12 @@ Tcl_SetVar2(interp, "sqlite_options", "mergesort", "1", TCL_GLOBAL_ONLY); Tcl_SetVar2(interp, "sqlite_options", "schema_version", "1", TCL_GLOBAL_ONLY); #endif +#ifdef SQLITE_ENABLE_SESSION + Tcl_SetVar2(interp, "sqlite_options", "session", "1", TCL_GLOBAL_ONLY); +#else + Tcl_SetVar2(interp, "sqlite_options", "session", "0", TCL_GLOBAL_ONLY); +#endif + #ifdef SQLITE_ENABLE_STAT4 Tcl_SetVar2(interp, "sqlite_options", "stat4", "1", TCL_GLOBAL_ONLY); #else diff --git a/src/update.c b/src/update.c index 9b351662e..c147eaaad 100644 --- a/src/update.c +++ b/src/update.c @@ -590,10 +590,23 @@ void sqlite3Update( VdbeCoverageNeverTaken(v); } sqlite3GenerateRowIndexDelete(pParse, pTab, iDataCur, iIdxCur, aRegIdx, -1); - - /* If changing the record number, delete the old record. */ - if( hasFK || chngKey || pPk!=0 ){ - sqlite3VdbeAddOp2(v, OP_Delete, iDataCur, 0); + + /* If changing the rowid value, or if there are foreign key constraints + ** to process, delete the old record. Otherwise, add a noop OP_Delete + ** to invoke the pre-update hook. + ** + ** That (regNew==regnewRowid+1) is true is also important for the + ** pre-update hook. If the caller invokes preupdate_new(), the returned + ** value is copied from memory cell (regNewRowid+1+iCol), where iCol + ** is the column index supplied by the user. + */ + assert( regNew==regNewRowid+1 ); + sqlite3VdbeAddOp3(v, OP_Delete, iDataCur, + OPFLAG_ISUPDATE | ((hasFK || chngKey || pPk!=0) ? 0 : OPFLAG_ISNOOP), + regNewRowid + ); + if( !pParse->nested ){ + sqlite3VdbeChangeP4(v, -1, (char*)pTab, P4_TABLE); } if( bReplace || chngKey ){ sqlite3VdbeJumpHere(v, addr1); diff --git a/src/vdbe.c b/src/vdbe.c index 6f4ac5713..2757a624e 100644 --- a/src/vdbe.c +++ b/src/vdbe.c @@ -87,6 +87,16 @@ static void updateMaxBlobsize(Mem *p){ #endif /* +** This macro evaluates to true if either the update hook or the preupdate +** hook are enabled for database connect DB. +*/ +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +# define HAS_UPDATE_HOOK(DB) ((DB)->xPreUpdateCallback||(DB)->xUpdateCallback) +#else +# define HAS_UPDATE_HOOK(DB) ((DB)->xUpdateCallback) +#endif + +/* ** The next global variable is incremented each time the OP_Found opcode ** is executed. This is used to test whether or not the foreign key ** operation implemented using OP_FkIsZero is working. This variable @@ -2889,7 +2899,7 @@ case OP_Savepoint: { }else{ db->nSavepoint++; } - + /* Link the new savepoint into the database handle's list. */ pNew->pNext = db->pSavepoint; db->pSavepoint = pNew; @@ -4246,9 +4256,9 @@ case OP_NewRowid: { /* out2 */ ** is part of an INSERT operation. The difference is only important to ** the update hook. ** -** Parameter P4 may point to a string containing the table-name, or -** may be NULL. If it is not NULL, then the update-hook -** (sqlite3.xUpdateCallback) is invoked following a successful insert. +** Parameter P4 may point to a Table structure, or may be NULL. If it is +** not NULL, then the update-hook (sqlite3.xUpdateCallback) is invoked +** following a successful insert. ** ** (WARNING/TODO: If P1 is a pseudo-cursor and P2 is dynamically ** allocated, then ownership of P2 is transferred to the pseudo-cursor @@ -4274,8 +4284,8 @@ case OP_InsertInt: { int nZero; /* Number of zero-bytes to append */ int seekResult; /* Result of prior seek or 0 if no USESEEKRESULT flag */ const char *zDb; /* database name - used by the update hook */ - const char *zTbl; /* Table name - used by the opdate hook */ - int op; /* Opcode for update hook: SQLITE_UPDATE or SQLITE_INSERT */ + Table *pTab; /* Table structure - used by update and pre-update hooks */ + int op = 0; /* Opcode for update hook: SQLITE_UPDATE or SQLITE_INSERT */ pData = &aMem[pOp->p2]; assert( pOp->p1>=0 && pOp->p1<p->nCursor ); @@ -4285,6 +4295,7 @@ case OP_InsertInt: { assert( pC->eCurType==CURTYPE_BTREE ); assert( pC->uc.pCursor!=0 ); assert( pC->isTable ); + assert( pOp->p4type==P4_TABLE || pOp->p4type>=P4_STATIC ); REGISTER_TRACE(pOp->p2, pData); if( pOp->opcode==OP_Insert ){ @@ -4298,6 +4309,25 @@ case OP_InsertInt: { iKey = pOp->p3; } + if( pOp->p4type==P4_TABLE && HAS_UPDATE_HOOK(db) ){ + assert( pC->isTable ); + assert( pC->iDb>=0 ); + zDb = db->aDb[pC->iDb].zName; + pTab = pOp->p4.pTab; + op = ((pOp->p5 & OPFLAG_ISUPDATE) ? SQLITE_UPDATE : SQLITE_INSERT); + } + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + /* Invoke the pre-update hook, if any */ + if( db->xPreUpdateCallback + && pOp->p4type==P4_TABLE + && !(pOp->p5 & OPFLAG_ISUPDATE) + && HasRowid(pTab) + ){ + sqlite3VdbePreUpdateHook(p, pC, SQLITE_INSERT, zDb, pTab, iKey, pOp->p2); + } +#endif + if( pOp->p5 & OPFLAG_NCHANGE ) p->nChange++; if( pOp->p5 & OPFLAG_LASTROWID ) db->lastRowid = lastRowid = iKey; if( pData->flags & MEM_Null ){ @@ -4321,18 +4351,13 @@ case OP_InsertInt: { /* Invoke the update-hook if required. */ if( rc ) goto abort_due_to_error; - if( db->xUpdateCallback && pOp->p4.z ){ - zDb = db->aDb[pC->iDb].zName; - zTbl = pOp->p4.z; - op = ((pOp->p5 & OPFLAG_ISUPDATE) ? SQLITE_UPDATE : SQLITE_INSERT); - assert( pC->isTable ); - db->xUpdateCallback(db->pUpdateArg, op, zDb, zTbl, iKey); - assert( pC->iDb>=0 ); + if( db->xUpdateCallback && op && HasRowid(pTab) ){ + db->xUpdateCallback(db->pUpdateArg, op, zDb, pTab->zName, iKey); } break; } -/* Opcode: Delete P1 P2 * P4 P5 +/* Opcode: Delete P1 P2 P3 P4 P5 ** ** Delete the record at which the P1 cursor is currently pointing. ** @@ -4356,15 +4381,24 @@ case OP_InsertInt: { ** P1 must not be pseudo-table. It has to be a real table with ** multiple rows. ** -** If P4 is not NULL, then it is the name of the table that P1 is -** pointing to. The update hook will be invoked, if it exists. -** If P4 is not NULL then the P1 cursor must have been positioned -** using OP_NotFound prior to invoking this opcode. +** If P4 is not NULL then it points to a Table struture. In this case either +** the update or pre-update hook, or both, may be invoked. The P1 cursor must +** have been positioned using OP_NotFound prior to invoking this opcode in +** this case. Specifically, if one is configured, the pre-update hook is +** invoked if P4 is not NULL. The update-hook is invoked if one is configured, +** P4 is not NULL, and the OPFLAG_NCHANGE flag is set in P2. +** +** If the OPFLAG_ISUPDATE flag is set in P2, then P3 contains the address +** of the memory cell that contains the value that the rowid of the row will +** be set to by the update. */ case OP_Delete: { VdbeCursor *pC; - u8 hasUpdateCallback; + const char *zDb; + Table *pTab; + int opflags; + opflags = pOp->p2; assert( pOp->p1>=0 && pOp->p1<p->nCursor ); pC = p->apCsr[pOp->p1]; assert( pC!=0 ); @@ -4372,22 +4406,45 @@ case OP_Delete: { assert( pC->uc.pCursor!=0 ); assert( pC->deferredMoveto==0 ); - hasUpdateCallback = db->xUpdateCallback && pOp->p4.z && pC->isTable; - if( pOp->p5 && hasUpdateCallback ){ - sqlite3BtreeKeySize(pC->uc.pCursor, &pC->movetoTarget); - } - #ifdef SQLITE_DEBUG - /* The seek operation that positioned the cursor prior to OP_Delete will - ** have also set the pC->movetoTarget field to the rowid of the row that - ** is being deleted */ - if( pOp->p4.z && pC->isTable && pOp->p5==0 ){ + if( pOp->p4type==P4_TABLE && HasRowid(pOp->p4.pTab) && pOp->p5==0 ){ + /* If p5 is zero, the seek operation that positioned the cursor prior to + ** OP_Delete will have also set the pC->movetoTarget field to the rowid of + ** the row that is being deleted */ i64 iKey = 0; sqlite3BtreeKeySize(pC->uc.pCursor, &iKey); - assert( pC->movetoTarget==iKey ); + assert( pC->movetoTarget==iKey ); } #endif + /* If the update-hook or pre-update-hook will be invoked, set zDb to + ** the name of the db to pass as to it. Also set local pTab to a copy + ** of p4.pTab. Finally, if p5 is true, indicating that this cursor was + ** last moved with OP_Next or OP_Prev, not Seek or NotFound, set + ** VdbeCursor.movetoTarget to the current rowid. */ + if( pOp->p4.pTab && HAS_UPDATE_HOOK(db) ){ + assert( pC->iDb>=0 ); + zDb = db->aDb[pC->iDb].zName; + pTab = pOp->p4.pTab; + if( pOp->p5 && pC->isTable ){ + sqlite3BtreeKeySize(pC->uc.pCursor, &pC->movetoTarget); + } + } + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + /* Invoke the pre-update-hook if required. */ + if( db->xPreUpdateCallback && pOp->p4.pTab && HasRowid(pTab) ){ + assert( !(opflags & OPFLAG_ISUPDATE) || (aMem[pOp->p3].flags & MEM_Int) ); + sqlite3VdbePreUpdateHook(p, pC, + (opflags & OPFLAG_ISUPDATE) ? SQLITE_UPDATE : SQLITE_DELETE, + zDb, pTab, pC->movetoTarget, + pOp->p3 + ); + } +#endif + + if( opflags & OPFLAG_ISNOOP ) break; + /* Only flags that can be set are SAVEPOISTION and AUXDELETE */ assert( (pOp->p5 & ~(OPFLAG_SAVEPOSITION|OPFLAG_AUXDELETE))==0 ); assert( OPFLAG_SAVEPOSITION==BTREE_SAVEPOSITION ); @@ -4409,15 +4466,18 @@ case OP_Delete: { rc = sqlite3BtreeDelete(pC->uc.pCursor, pOp->p5); pC->cacheStatus = CACHE_STALE; + if( rc ) goto abort_due_to_error; /* Invoke the update-hook if required. */ - if( rc ) goto abort_due_to_error; - if( hasUpdateCallback ){ - db->xUpdateCallback(db->pUpdateArg, SQLITE_DELETE, - db->aDb[pC->iDb].zName, pOp->p4.z, pC->movetoTarget); - assert( pC->iDb>=0 ); + if( opflags & OPFLAG_NCHANGE ){ + p->nChange++; + if( rc==SQLITE_OK && db->xUpdateCallback && HasRowid(pTab) ){ + db->xUpdateCallback(db->pUpdateArg, SQLITE_DELETE, zDb, pTab->zName, + pC->movetoTarget); + assert( pC->iDb>=0 ); + } } - if( pOp->p2 & OPFLAG_NCHANGE ) p->nChange++; + break; } /* Opcode: ResetCount * * * * * diff --git a/src/vdbe.h b/src/vdbe.h index 4c02f5844..f6acb7d51 100644 --- a/src/vdbe.h +++ b/src/vdbe.h @@ -60,6 +60,7 @@ struct VdbeOp { KeyInfo *pKeyInfo; /* Used when p4type is P4_KEYINFO */ int *ai; /* Used when p4type is P4_INTARRAY */ SubProgram *pProgram; /* Used when p4type is P4_SUBPROGRAM */ + Table *pTab; /* Used when p4type is P4_TABLE */ #ifdef SQLITE_ENABLE_CURSOR_HINTS Expr *pExpr; /* Used when p4type is P4_EXPR */ #endif @@ -124,7 +125,8 @@ typedef struct VdbeOpList VdbeOpList; #define P4_INTARRAY (-15) /* P4 is a vector of 32-bit integers */ #define P4_SUBPROGRAM (-18) /* P4 is a pointer to a SubProgram structure */ #define P4_ADVANCE (-19) /* P4 is a pointer to BtreeNext() or BtreePrev() */ -#define P4_FUNCCTX (-20) /* P4 is a pointer to an sqlite3_context object */ +#define P4_TABLE (-20) /* P4 is a pointer to a Table structure */ +#define P4_FUNCCTX (-21) /* P4 is a pointer to an sqlite3_context object */ /* Error message codes for OP_Halt */ #define P5_ConstraintNotNull 1 diff --git a/src/vdbeInt.h b/src/vdbeInt.h index a78795f61..1218009d0 100644 --- a/src/vdbeInt.h +++ b/src/vdbeInt.h @@ -429,6 +429,25 @@ struct Vdbe { #define VDBE_MAGIC_DEAD 0xb606c3c8 /* The VDBE has been deallocated */ /* +** Structure used to store the context required by the +** sqlite3_preupdate_*() API functions. +*/ +struct PreUpdate { + Vdbe *v; + VdbeCursor *pCsr; /* Cursor to read old values from */ + int op; /* One of SQLITE_INSERT, UPDATE, DELETE */ + u8 *aRecord; /* old.* database record */ + KeyInfo keyinfo; + UnpackedRecord *pUnpacked; /* Unpacked version of aRecord[] */ + UnpackedRecord *pNewUnpacked; /* Unpacked version of new.* record */ + int iNewReg; /* Register for new.* values */ + i64 iKey1; /* First key value passed to hook */ + i64 iKey2; /* Second key value passed to hook */ + int iPKey; /* If not negative index of IPK column */ + Mem *aNew; /* Array of new.* values */ +}; + +/* ** Function prototypes */ void sqlite3VdbeError(Vdbe*, const char *, ...); @@ -487,6 +506,8 @@ int sqlite3VdbeMemClearAndResize(Mem *pMem, int n); int sqlite3VdbeCloseStatement(Vdbe *, int); void sqlite3VdbeFrameDelete(VdbeFrame*); int sqlite3VdbeFrameRestore(VdbeFrame *); +void sqlite3VdbePreUpdateHook( + Vdbe *, VdbeCursor *, int, const char*, Table *, i64, int); int sqlite3VdbeTransferError(Vdbe *p); int sqlite3VdbeSorterInit(sqlite3 *, int, VdbeCursor *); diff --git a/src/vdbeapi.c b/src/vdbeapi.c index c2038137d..1feecb3e1 100644 --- a/src/vdbeapi.c +++ b/src/vdbeapi.c @@ -1604,6 +1604,187 @@ int sqlite3_stmt_status(sqlite3_stmt *pStmt, int op, int resetFlag){ return (int)v; } +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** Allocate and populate an UnpackedRecord structure based on the serialized +** record in nKey/pKey. Return a pointer to the new UnpackedRecord structure +** if successful, or a NULL pointer if an OOM error is encountered. +*/ +static UnpackedRecord *vdbeUnpackRecord( + KeyInfo *pKeyInfo, + int nKey, + const void *pKey +){ + char *dummy; /* Dummy argument for AllocUnpackedRecord() */ + UnpackedRecord *pRet; /* Return value */ + + pRet = sqlite3VdbeAllocUnpackedRecord(pKeyInfo, 0, 0, &dummy); + if( pRet ){ + memset(pRet->aMem, 0, sizeof(Mem)*(pKeyInfo->nField+1)); + sqlite3VdbeRecordUnpack(pKeyInfo, nKey, pKey, pRet); + } + return pRet; +} + +/* +** This function is called from within a pre-update callback to retrieve +** a field of the row currently being updated or deleted. +*/ +int sqlite3_preupdate_old(sqlite3 *db, int iIdx, sqlite3_value **ppValue){ + PreUpdate *p = db->pPreUpdate; + int rc = SQLITE_OK; + + /* Test that this call is being made from within an SQLITE_DELETE or + ** SQLITE_UPDATE pre-update callback, and that iIdx is within range. */ + if( !p || p->op==SQLITE_INSERT ){ + rc = SQLITE_MISUSE_BKPT; + goto preupdate_old_out; + } + if( iIdx>=p->pCsr->nField || iIdx<0 ){ + rc = SQLITE_RANGE; + goto preupdate_old_out; + } + + /* If the old.* record has not yet been loaded into memory, do so now. */ + if( p->pUnpacked==0 ){ + u32 nRec; + u8 *aRec; + + rc = sqlite3BtreeDataSize(p->pCsr->uc.pCursor, &nRec); + if( rc!=SQLITE_OK ) goto preupdate_old_out; + aRec = sqlite3DbMallocRaw(db, nRec); + if( !aRec ) goto preupdate_old_out; + rc = sqlite3BtreeData(p->pCsr->uc.pCursor, 0, nRec, aRec); + if( rc==SQLITE_OK ){ + p->pUnpacked = vdbeUnpackRecord(&p->keyinfo, nRec, aRec); + if( !p->pUnpacked ) rc = SQLITE_NOMEM; + } + if( rc!=SQLITE_OK ){ + sqlite3DbFree(db, aRec); + goto preupdate_old_out; + } + p->aRecord = aRec; + } + + if( iIdx>=p->pUnpacked->nField ){ + *ppValue = (sqlite3_value *)columnNullValue(); + }else{ + *ppValue = &p->pUnpacked->aMem[iIdx]; + if( iIdx==p->iPKey ){ + sqlite3VdbeMemSetInt64(*ppValue, p->iKey1); + } + } + + preupdate_old_out: + sqlite3Error(db, rc); + return sqlite3ApiExit(db, rc); +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** This function is called from within a pre-update callback to retrieve +** the number of columns in the row being updated, deleted or inserted. +*/ +int sqlite3_preupdate_count(sqlite3 *db){ + PreUpdate *p = db->pPreUpdate; + return (p ? p->keyinfo.nField : 0); +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** This function is designed to be called from within a pre-update callback +** only. It returns zero if the change that caused the callback was made +** immediately by a user SQL statement. Or, if the change was made by a +** trigger program, it returns the number of trigger programs currently +** on the stack (1 for a top-level trigger, 2 for a trigger fired by a +** top-level trigger etc.). +** +** For the purposes of the previous paragraph, a foreign key CASCADE, SET NULL +** or SET DEFAULT action is considered a trigger. +*/ +int sqlite3_preupdate_depth(sqlite3 *db){ + PreUpdate *p = db->pPreUpdate; + return (p ? p->v->nFrame : 0); +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** This function is called from within a pre-update callback to retrieve +** a field of the row currently being updated or inserted. +*/ +int sqlite3_preupdate_new(sqlite3 *db, int iIdx, sqlite3_value **ppValue){ + PreUpdate *p = db->pPreUpdate; + int rc = SQLITE_OK; + Mem *pMem; + + if( !p || p->op==SQLITE_DELETE ){ + rc = SQLITE_MISUSE_BKPT; + goto preupdate_new_out; + } + if( iIdx>=p->pCsr->nField || iIdx<0 ){ + rc = SQLITE_RANGE; + goto preupdate_new_out; + } + + if( p->op==SQLITE_INSERT ){ + /* For an INSERT, memory cell p->iNewReg contains the serialized record + ** that is being inserted. Deserialize it. */ + UnpackedRecord *pUnpack = p->pNewUnpacked; + if( !pUnpack ){ + Mem *pData = &p->v->aMem[p->iNewReg]; + rc = sqlite3VdbeMemExpandBlob(pData); + if( rc!=SQLITE_OK ) goto preupdate_new_out; + pUnpack = vdbeUnpackRecord(&p->keyinfo, pData->n, pData->z); + if( !pUnpack ){ + rc = SQLITE_NOMEM; + goto preupdate_new_out; + } + p->pNewUnpacked = pUnpack; + } + if( iIdx>=pUnpack->nField ){ + pMem = (sqlite3_value *)columnNullValue(); + }else{ + pMem = &pUnpack->aMem[iIdx]; + if( iIdx==p->iPKey ){ + sqlite3VdbeMemSetInt64(pMem, p->iKey2); + } + } + }else{ + /* For an UPDATE, memory cell (p->iNewReg+1+iIdx) contains the required + ** value. Make a copy of the cell contents and return a pointer to it. + ** It is not safe to return a pointer to the memory cell itself as the + ** caller may modify the value text encoding. + */ + assert( p->op==SQLITE_UPDATE ); + if( !p->aNew ){ + p->aNew = (Mem *)sqlite3DbMallocZero(db, sizeof(Mem) * p->pCsr->nField); + if( !p->aNew ){ + rc = SQLITE_NOMEM; + goto preupdate_new_out; + } + } + assert( iIdx>=0 && iIdx<p->pCsr->nField ); + pMem = &p->aNew[iIdx]; + if( pMem->flags==0 ){ + if( iIdx==p->iPKey ){ + sqlite3VdbeMemSetInt64(pMem, p->iKey2); + }else{ + rc = sqlite3VdbeMemCopy(pMem, &p->v->aMem[p->iNewReg+1+iIdx]); + if( rc!=SQLITE_OK ) goto preupdate_new_out; + } + } + } + *ppValue = pMem; + + preupdate_new_out: + sqlite3Error(db, rc); + return sqlite3ApiExit(db, rc); +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + #ifdef SQLITE_ENABLE_STMT_SCANSTATUS /* ** Return status data for a single loop within query pStmt. diff --git a/src/vdbeaux.c b/src/vdbeaux.c index 9c75d1e07..50f5e99c5 100644 --- a/src/vdbeaux.c +++ b/src/vdbeaux.c @@ -78,6 +78,7 @@ const char *sqlite3_sql(sqlite3_stmt *pStmt){ void sqlite3VdbeSwap(Vdbe *pA, Vdbe *pB){ Vdbe tmp, *pTmp; char *zTmp; + assert( pA->db==pB->db ); tmp = *pA; *pA = *pB; *pB = tmp; @@ -3504,6 +3505,7 @@ void sqlite3VdbeRecordUnpack( pMem->db = pKeyInfo->db; /* pMem->flags = 0; // sqlite3VdbeSerialGet() will set this for us */ pMem->szMalloc = 0; + pMem->z = 0; d += sqlite3VdbeSerialGet(&aKey[d], serial_type, pMem); pMem++; if( (++u)>=p->nField ) break; @@ -4483,3 +4485,85 @@ void sqlite3VtabImportErrmsg(Vdbe *p, sqlite3_vtab *pVtab){ } } #endif /* SQLITE_OMIT_VIRTUALTABLE */ + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + +/* +** If the second argument is not NULL, release any allocations associated +** with the memory cells in the p->aMem[] array. Also free the UnpackedRecord +** structure itself, using sqlite3DbFree(). +** +** This function is used to free UnpackedRecord structures allocated by +** the vdbeUnpackRecord() function found in vdbeapi.c. +*/ +static void vdbeFreeUnpacked(sqlite3 *db, UnpackedRecord *p){ + if( p ){ + int i; + for(i=0; i<p->nField; i++){ + Mem *pMem = &p->aMem[i]; + if( pMem->zMalloc ) sqlite3VdbeMemRelease(pMem); + } + sqlite3DbFree(db, p); + } +} + +/* +** Invoke the pre-update hook. If this is an UPDATE or DELETE pre-update call, +** then cursor passed as the second argument should point to the row about +** to be update or deleted. If the application calls sqlite3_preupdate_old(), +** the required value will be read from the row the cursor points to. +*/ +void sqlite3VdbePreUpdateHook( + Vdbe *v, /* Vdbe pre-update hook is invoked by */ + VdbeCursor *pCsr, /* Cursor to grab old.* values from */ + int op, /* SQLITE_INSERT, UPDATE or DELETE */ + const char *zDb, /* Database name */ + Table *pTab, /* Modified table */ + i64 iKey1, /* Initial key value */ + int iReg /* Register for new.* record */ +){ + sqlite3 *db = v->db; + i64 iKey2; + PreUpdate preupdate; + const char *zTbl = pTab->zName; + static const u8 fakeSortOrder = 0; + + assert( db->pPreUpdate==0 ); + memset(&preupdate, 0, sizeof(PreUpdate)); + if( op==SQLITE_UPDATE ){ + iKey2 = v->aMem[iReg].u.i; + }else{ + iKey2 = iKey1; + } + + assert( pCsr->nField==pTab->nCol + || (pCsr->nField==pTab->nCol+1 && op==SQLITE_DELETE && iReg==-1) + ); + + preupdate.v = v; + preupdate.pCsr = pCsr; + preupdate.op = op; + preupdate.iNewReg = iReg; + preupdate.keyinfo.db = db; + preupdate.keyinfo.enc = ENC(db); + preupdate.keyinfo.nField = pTab->nCol; + preupdate.keyinfo.aSortOrder = (u8*)&fakeSortOrder; + preupdate.iKey1 = iKey1; + preupdate.iKey2 = iKey2; + preupdate.iPKey = pTab->iPKey; + + db->pPreUpdate = &preupdate; + db->xPreUpdateCallback(db->pPreUpdateArg, db, op, zDb, zTbl, iKey1, iKey2); + db->pPreUpdate = 0; + sqlite3DbFree(db, preupdate.aRecord); + vdbeFreeUnpacked(db, preupdate.pUnpacked); + vdbeFreeUnpacked(db, preupdate.pNewUnpacked); + if( preupdate.aNew ){ + int i; + for(i=0; i<pCsr->nField; i++){ + sqlite3VdbeMemRelease(&preupdate.aNew[i]); + } + sqlite3DbFree(db, preupdate.aNew); + } +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ diff --git a/src/vdbeblob.c b/src/vdbeblob.c index 2ba5301e7..f2b3ffef2 100644 --- a/src/vdbeblob.c +++ b/src/vdbeblob.c @@ -30,6 +30,8 @@ struct Incrblob { BtCursor *pCsr; /* Cursor pointing at blob row */ sqlite3_stmt *pStmt; /* Statement holding cursor open */ sqlite3 *db; /* The associated database */ + char *zDb; /* Database name */ + Table *pTab; /* Table object */ }; @@ -173,6 +175,8 @@ int sqlite3_blob_open( sqlite3BtreeLeaveAll(db); goto blob_open_out; } + pBlob->pTab = pTab; + pBlob->zDb = db->aDb[sqlite3SchemaToIndex(db, pTab->pSchema)].zName; /* Now search pTab for the exact column. */ for(iCol=0; iCol<pTab->nCol; iCol++) { @@ -394,6 +398,30 @@ static int blobReadWrite( */ assert( db == v->db ); sqlite3BtreeEnterCursor(p->pCsr); + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + if( xCall==sqlite3BtreePutData && db->xPreUpdateCallback ){ + /* If a pre-update hook is registered and this is a write cursor, + ** invoke it here. + ** + ** TODO: The preupdate-hook is passed SQLITE_DELETE, even though this + ** operation should really be an SQLITE_UPDATE. This is probably + ** incorrect, but is convenient because at this point the new.* values + ** are not easily obtainable. And for the sessions module, an + ** SQLITE_UPDATE where the PK columns do not change is handled in the + ** same way as an SQLITE_DELETE (the SQLITE_DELETE code is actually + ** slightly more efficient). Since you cannot write to a PK column + ** using the incremental-blob API, this works. For the sessions module + ** anyhow. + */ + sqlite3_int64 iKey; + sqlite3BtreeKeySize(p->pCsr, &iKey); + sqlite3VdbePreUpdateHook( + v, v->apCsr[0], SQLITE_DELETE, p->zDb, p->pTab, iKey, -1 + ); + } +#endif + rc = xCall(p->pCsr, iOffset+p->iOffset, n, z); sqlite3BtreeLeaveCursor(p->pCsr); if( rc==SQLITE_ABORT ){ diff --git a/src/vdbemem.c b/src/vdbemem.c index b5139fe32..abe2dd251 100644 --- a/src/vdbemem.c +++ b/src/vdbemem.c @@ -802,10 +802,6 @@ void sqlite3VdbeMemShallowCopy(Mem *pTo, const Mem *pFrom, int srcType){ int sqlite3VdbeMemCopy(Mem *pTo, const Mem *pFrom){ int rc = SQLITE_OK; - /* The pFrom==0 case in the following assert() is when an sqlite3_value - ** from sqlite3_value_dup() is used as the argument - ** to sqlite3_result_value(). */ - assert( pTo->db==pFrom->db || pFrom->db==0 ); assert( (pFrom->flags & MEM_RowSet)==0 ); if( VdbeMemDynamic(pTo) ) vdbeMemClearExternAndSetNull(pTo); memcpy(pTo, pFrom, MEMCELLSIZE); |