aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/delete.c13
-rw-r--r--src/insert.c14
-rw-r--r--src/main.c19
-rw-r--r--src/sqlite.h.in24
-rw-r--r--src/sqliteInt.h7
-rw-r--r--src/tclsqlite.c184
-rw-r--r--src/update.c13
-rw-r--r--src/vdbe.c90
-rw-r--r--src/vdbeInt.h13
-rw-r--r--src/vdbeapi.c115
-rw-r--r--src/vdbeaux.c34
11 files changed, 443 insertions, 83 deletions
diff --git a/src/delete.c b/src/delete.c
index bd7ac3d1f..c7a686a8b 100644
--- a/src/delete.c
+++ b/src/delete.c
@@ -531,13 +531,18 @@ 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 ){
sqlite3GenerateRowIndexDelete(pParse, pTab, iCur, 0);
sqlite3VdbeAddOp2(v, OP_Delete, iCur, (count?OPFLAG_NCHANGE:0));
- if( count ){
- sqlite3VdbeChangeP4(v, -1, pTab->zName, P4_STATIC);
- }
+ sqlite3VdbeChangeP4(v, -1, pTab->zName, P4_STATIC);
}
/* Do any ON CASCADE, SET NULL or SET DEFAULT operations required to
diff --git a/src/insert.c b/src/insert.c
index adf6ef2ed..98f00bfe3 100644
--- a/src/insert.c
+++ b/src/insert.c
@@ -1287,9 +1287,17 @@ void sqlite3GenerateConstraintChecks(
sqlite3GenerateRowDelete(
pParse, pTab, baseCur, regRowid, 0, pTrigger, OE_Replace
);
- }else if( pTab->pIndex ){
- sqlite3MultiWrite(pParse);
- sqlite3GenerateRowIndexDelete(pParse, pTab, baseCur, 0);
+ }else{
+ /* 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, baseCur, OPFLAG_ISNOOP);
+ sqlite3VdbeChangeP4(v, -1, pTab->zName, P4_STATIC);
+ if( pTab->pIndex ){
+ sqlite3MultiWrite(pParse);
+ sqlite3GenerateRowIndexDelete(pParse, pTab, baseCur, 0);
+ }
}
seenReplace = 1;
break;
diff --git a/src/main.c b/src/main.c
index 833b9812b..3976f7ebc 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1271,6 +1271,25 @@ void *sqlite3_rollback_hook(
return pRet;
}
+/*
+** 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;
+}
+
#ifndef SQLITE_OMIT_WAL
/*
** The sqlite3_wal_hook() callback registered by sqlite3_wal_autocheckpoint().
diff --git a/src/sqlite.h.in b/src/sqlite.h.in
index 3400c6c91..413031ecf 100644
--- a/src/sqlite.h.in
+++ b/src/sqlite.h.in
@@ -6344,6 +6344,30 @@ int sqlite3_wal_checkpoint_v2(
#define SQLITE_CHECKPOINT_FULL 1
#define SQLITE_CHECKPOINT_RESTART 2
+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*
+);
+
+/*
+** 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.
+*/
+int sqlite3_preupdate_old(sqlite3 *, int, sqlite3_value **);
+int sqlite3_preupdate_modified(sqlite3 *, int, int *);
+int sqlite3_preupdate_count(sqlite3 *);
+
/*
** Undo the hack that converts floating point types to integer for
diff --git a/src/sqliteInt.h b/src/sqliteInt.h
index 2987dcd48..254193684 100644
--- a/src/sqliteInt.h
+++ b/src/sqliteInt.h
@@ -619,6 +619,7 @@ typedef struct LookasideSlot LookasideSlot;
typedef struct Module Module;
typedef struct NameContext NameContext;
typedef struct Parse Parse;
+typedef struct PreUpdate PreUpdate;
typedef struct RowSet RowSet;
typedef struct Savepoint Savepoint;
typedef struct Select Select;
@@ -827,6 +828,11 @@ struct sqlite3 {
void (*xRollbackCallback)(void*); /* Invoked at every commit. */
void *pUpdateArg;
void (*xUpdateCallback)(void*,int, const char*,const char*,sqlite_int64);
+ 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 */
#ifndef SQLITE_OMIT_WAL
int (*xWalCallback)(void *, sqlite3 *, const char *, int);
void *pWalArg;
@@ -2254,6 +2260,7 @@ struct AuthContext {
#define OPFLAG_APPEND 0x08 /* This is likely to be an append */
#define OPFLAG_USESEEKRESULT 0x10 /* Try to avoid a seek in BtreeInsert() */
#define OPFLAG_CLEARCACHE 0x20 /* Clear pseudo-table cache in OP_Column */
+#define OPFLAG_ISNOOP 0x40 /* OP_Delete does pre-update-hook only */
/*
* Each trigger present in the database schema is stored as an instance of
diff --git a/src/tclsqlite.c b/src/tclsqlite.c
index 57f38d78d..91c6094fa 100644
--- a/src/tclsqlite.c
+++ b/src/tclsqlite.c
@@ -122,6 +122,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) */
@@ -483,6 +484,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);
}
@@ -649,6 +653,40 @@ static void DbUnlockNotify(void **apArg, int nArg){
}
#endif
+/*
+** 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);
+}
+
static void DbUpdateHandler(
void *p,
int op,
@@ -658,14 +696,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));
@@ -1551,6 +1593,44 @@ 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);
+ }
+ }
+
+ 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);
+}
+
+/*
** 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
@@ -1575,6 +1655,7 @@ 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",
+ "preupdate",
"profile", "progress", "rekey",
"restore", "rollback_hook", "status",
"timeout", "total_changes", "trace",
@@ -1589,6 +1670,7 @@ 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_PREUPDATE,
DB_PROFILE, DB_PROGRESS, DB_REKEY,
DB_RESTORE, DB_ROLLBACK_HOOK, DB_STATUS,
DB_TIMEOUT, DB_TOTAL_CHANGES, DB_TRACE,
@@ -2763,6 +2845,71 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){
break;
}
+ case DB_PREUPDATE: {
+ static const char *azSub[] = {"count", "hook", "modified", "old", 0};
+ enum DbPreupdateSubCmd {
+ PRE_COUNT, PRE_HOOK, PRE_MODIFIED, 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_MODIFIED:
+ case PRE_OLD: {
+ int iIdx;
+ 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_MODIFIED ){
+ int iRes;
+ rc = sqlite3_preupdate_modified(pDb->db, iIdx, &iRes);
+ if( rc==SQLITE_OK ) Tcl_SetObjResult(interp, Tcl_NewIntObj(iRes));
+ }else{
+ sqlite3_value *pValue;
+ assert( iSub==PRE_OLD );
+ rc = sqlite3_preupdate_old(pDb->db, iIdx, &pValue);
+ if( rc==SQLITE_OK ){
+ Tcl_Obj *pObj = Tcl_NewStringObj(sqlite3_value_text(pValue), -1);
+ Tcl_SetObjResult(interp, pObj);
+ }
+ }
+
+ if( rc!=SQLITE_OK ){
+ Tcl_AppendResult(interp, sqlite3_errmsg(pDb->db), 0);
+ return TCL_ERROR;
+ }
+ }
+ }
+
+ break;
+ }
+
/*
** $db wal_hook ?script?
** $db update_hook ?script?
@@ -2771,42 +2918,21 @@ 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: {
+ 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_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;
}
diff --git a/src/update.c b/src/update.c
index 8bf58d766..ea8f95ad5 100644
--- a/src/update.c
+++ b/src/update.c
@@ -490,9 +490,16 @@ void sqlite3Update(
j1 = sqlite3VdbeAddOp3(v, OP_NotExists, iCur, 0, regOldRowid);
sqlite3GenerateRowIndexDelete(pParse, pTab, iCur, aRegIdx);
- /* If changing the record number, delete the old record. */
- if( hasFK || chngRowid ){
- sqlite3VdbeAddOp2(v, OP_Delete, iCur, 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.
+ */
+ sqlite3VdbeAddOp3(v, OP_Delete, iCur,
+ OPFLAG_ISUPDATE | ((hasFK || chngRowid) ? 0 : OPFLAG_ISNOOP),
+ regNewRowid
+ );
+ if( !pParse->nested ){
+ sqlite3VdbeChangeP4(v, -1, pTab->zName, P4_STATIC);
}
sqlite3VdbeJumpHere(v, j1);
diff --git a/src/vdbe.c b/src/vdbe.c
index 00ed1438b..c5492eedc 100644
--- a/src/vdbe.c
+++ b/src/vdbe.c
@@ -3868,6 +3868,24 @@ case OP_InsertInt: {
iKey = pOp->p3;
}
+ if( pOp->p4.z && (db->xPreUpdateCallback || db->xUpdateCallback) ){
+ assert( pC->isTable );
+ assert( pC->iDb>=0 );
+ zDb = db->aDb[pC->iDb].zName;
+ zTbl = pOp->p4.z;
+ op = ((pOp->p5 & OPFLAG_ISUPDATE) ? SQLITE_UPDATE : SQLITE_INSERT);
+ }
+
+ /* Invoke the pre-update hook, if any */
+ if( db->xPreUpdateCallback
+ && pOp->p4.z
+ && (!(pOp->p5 & OPFLAG_ISUPDATE) || pC->rowidIsValid==0)
+ ){
+ sqlite3VdbePreUpdateHook(p, pC,
+ pC->rowidIsValid ? op : SQLITE_INSERT, zDb, zTbl, iKey, iKey
+ );
+ }
+
if( pOp->p5 & OPFLAG_NCHANGE ) p->nChange++;
if( pOp->p5 & OPFLAG_LASTROWID ) db->lastRowid = iKey;
if( pData->flags & MEM_Null ){
@@ -3893,17 +3911,12 @@ case OP_InsertInt: {
/* Invoke the update-hook if required. */
if( rc==SQLITE_OK && 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 );
}
break;
}
-/* Opcode: Delete P1 P2 * P4 *
+/* Opcode: Delete P1 P2 P3 P4 *
**
** Delete the record at which the P1 cursor is currently pointing.
**
@@ -3918,30 +3931,31 @@ 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, 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: {
i64 iKey;
VdbeCursor *pC;
+ const char *zDb;
+ const char *zTbl;
+ int opflags;
+ opflags = pOp->p2;
iKey = 0;
assert( pOp->p1>=0 && pOp->p1<p->nCursor );
pC = p->apCsr[pOp->p1];
assert( pC!=0 );
assert( pC->pCursor!=0 ); /* Only valid for real tables, no pseudotables */
- /* If the update-hook will be invoked, set iKey to the rowid of the
- ** row being deleted.
- */
- if( db->xUpdateCallback && pOp->p4.z ){
- assert( pC->isTable );
- assert( pC->rowidIsValid ); /* lastRowid set by previous OP_NotFound */
- iKey = pC->lastRowid;
- }
-
/* The OP_Delete opcode always follows an OP_NotExists or OP_Last or
** OP_Column on the same table without any intervening operations that
** might move or invalidate the cursor. Hence cursor pC is always pointing
@@ -3953,18 +3967,42 @@ case OP_Delete: {
rc = sqlite3VdbeCursorMoveto(pC);
if( NEVER(rc!=SQLITE_OK) ) goto abort_due_to_error;
+ /* If the update-hook or pre-update-hook will be invoked, set iKey to
+ ** the rowid of the row being deleted. Set zDb and zTab as well.
+ */
+ if( pOp->p4.z && (db->xPreUpdateCallback || db->xUpdateCallback) ){
+ assert( pC->iDb>=0 );
+ assert( pC->isTable );
+ assert( pC->rowidIsValid ); /* lastRowid set by previous OP_NotFound */
+ iKey = pC->lastRowid;
+ zDb = db->aDb[pC->iDb].zName;
+ zTbl = pOp->p4.z;
+ }
+
+ /* Invoke the pre-update-hook if required. */
+ if( db->xPreUpdateCallback && pOp->p4.z ){
+ assert( !(opflags & OPFLAG_ISUPDATE) || (aMem[pOp->p3].flags & MEM_Int) );
+ sqlite3VdbePreUpdateHook(p, pC,
+ (opflags & OPFLAG_ISUPDATE) ? SQLITE_UPDATE : SQLITE_DELETE,
+ zDb, zTbl, iKey,
+ (opflags & OPFLAG_ISUPDATE) ? aMem[pOp->p3].u.i : iKey
+ );
+ }
+
+ if( opflags & OPFLAG_ISNOOP ) break;
+
sqlite3BtreeSetCachedRowid(pC->pCursor, 0);
rc = sqlite3BtreeDelete(pC->pCursor);
pC->cacheStatus = CACHE_STALE;
- /* Invoke the update-hook if required. */
- if( rc==SQLITE_OK && db->xUpdateCallback && pOp->p4.z ){
- const char *zDb = db->aDb[pC->iDb].zName;
- const char *zTbl = pOp->p4.z;
- db->xUpdateCallback(db->pUpdateArg, SQLITE_DELETE, zDb, zTbl, iKey);
- assert( pC->iDb>=0 );
+ /* Update the change-counter and invoke the update-hook if required. */
+ if( opflags & OPFLAG_NCHANGE ){
+ p->nChange++;
+ assert( pOp->p4.z );
+ if( rc==SQLITE_OK && db->xUpdateCallback ){
+ db->xUpdateCallback(db->pUpdateArg, SQLITE_DELETE, zDb, zTbl, iKey);
+ }
}
- if( pOp->p2 & OPFLAG_NCHANGE ) p->nChange++;
break;
}
/* Opcode: ResetCount * * * * *
diff --git a/src/vdbeInt.h b/src/vdbeInt.h
index b42729d23..a764d2366 100644
--- a/src/vdbeInt.h
+++ b/src/vdbeInt.h
@@ -332,6 +332,17 @@ 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 {
+ VdbeCursor *pCsr; /* Cursor to read old values from */
+ int op; /* One of SQLITE_INSERT, UPDATE, DELETE */
+ u8 *aRecord; /* old.* database record */
+ UnpackedRecord *pUnpacked; /* Unpacked version of aRecord[] */
+};
+
+/*
** Function prototypes
*/
void sqlite3VdbeFreeCursor(Vdbe *, VdbeCursor*);
@@ -387,6 +398,8 @@ int sqlite3VdbeCloseStatement(Vdbe *, int);
void sqlite3VdbeFrameDelete(VdbeFrame*);
int sqlite3VdbeFrameRestore(VdbeFrame *);
void sqlite3VdbeMemStoreType(Mem *pMem);
+void sqlite3VdbePreUpdateHook(
+ Vdbe *, VdbeCursor *, int, const char*, const char*, i64, i64);
#ifdef SQLITE_DEBUG
void sqlite3VdbeMemPrepareToChange(Vdbe*,Mem*);
diff --git a/src/vdbeapi.c b/src/vdbeapi.c
index 5578b868e..8b1f2e21b 100644
--- a/src/vdbeapi.c
+++ b/src/vdbeapi.c
@@ -673,6 +673,26 @@ int sqlite3_data_count(sqlite3_stmt *pStmt){
return pVm->nResColumn;
}
+/*
+** Return a pointer to static memory containing an SQL NULL value.
+*/
+static const Mem *columnNullValue(void){
+ /* Even though the Mem structure contains an element
+ ** of type i64, on certain architecture (x86) with certain compiler
+ ** switches (-Os), gcc may align this Mem object on a 4-byte boundary
+ ** instead of an 8-byte one. This all works fine, except that when
+ ** running with SQLITE_DEBUG defined the SQLite code sometimes assert()s
+ ** that a Mem structure is located on an 8-byte boundary. To prevent
+ ** this assert() from failing, when building with SQLITE_DEBUG defined
+ ** using gcc, force nullMem to be 8-byte aligned using the magical
+ ** __attribute__((aligned(8))) macro. */
+ static const Mem nullMem
+#if defined(SQLITE_DEBUG) && defined(__GNUC__)
+ __attribute__((aligned(8)))
+#endif
+ = {0, "", (double)0, {0}, 0, MEM_Null, SQLITE_NULL, 0, 0, 0 };
+ return &nullMem;
+}
/*
** Check to see if column iCol of the given statement is valid. If
@@ -692,27 +712,13 @@ static Mem *columnMem(sqlite3_stmt *pStmt, int i){
pOut = &pVm->pResultSet[i];
}else{
/* If the value passed as the second argument is out of range, return
- ** a pointer to the following static Mem object which contains the
- ** value SQL NULL. Even though the Mem structure contains an element
- ** of type i64, on certain architecture (x86) with certain compiler
- ** switches (-Os), gcc may align this Mem object on a 4-byte boundary
- ** instead of an 8-byte one. This all works fine, except that when
- ** running with SQLITE_DEBUG defined the SQLite code sometimes assert()s
- ** that a Mem structure is located on an 8-byte boundary. To prevent
- ** this assert() from failing, when building with SQLITE_DEBUG defined
- ** using gcc, force nullMem to be 8-byte aligned using the magical
- ** __attribute__((aligned(8))) macro. */
- static const Mem nullMem
-#if defined(SQLITE_DEBUG) && defined(__GNUC__)
- __attribute__((aligned(8)))
-#endif
- = {0, "", (double)0, {0}, 0, MEM_Null, SQLITE_NULL, 0, 0, 0 };
-
+ ** a pointer to a static Mem object that contains the value SQL NULL.
+ */
if( pVm && ALWAYS(pVm->db) ){
sqlite3_mutex_enter(pVm->db->mutex);
sqlite3Error(pVm->db, SQLITE_RANGE, 0);
}
- pOut = (Mem*)&nullMem;
+ pOut = (Mem*)columnNullValue();
}
return pOut;
}
@@ -1322,3 +1328,78 @@ int sqlite3_stmt_status(sqlite3_stmt *pStmt, int op, int resetFlag){
if( resetFlag ) pVdbe->aCounter[op-1] = 0;
return v;
}
+
+int sqlite3_preupdate_old(sqlite3 *db, int iIdx, sqlite3_value **ppValue){
+ PreUpdate *p = db->pPreUpdate;
+ int rc = SQLITE_OK;
+
+ 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( p->pUnpacked==0 ){
+ KeyInfo keyinfo;
+ u32 nRecord;
+ u8 *aRecord;
+
+ memset(&keyinfo, 0, sizeof(KeyInfo));
+ keyinfo.db = db;
+ keyinfo.enc = ENC(db);
+ keyinfo.nField = p->pCsr->nField;
+
+ rc = sqlite3BtreeDataSize(p->pCsr->pCursor, &nRecord);
+ if( rc!=SQLITE_OK ) goto preupdate_old_out;
+ aRecord = sqlite3DbMallocRaw(db, nRecord);
+ if( !aRecord ) goto preupdate_old_out;
+ rc = sqlite3BtreeData(p->pCsr->pCursor, 0, nRecord, aRecord);
+ if( rc!=SQLITE_OK ){
+ sqlite3DbFree(db, aRecord);
+ goto preupdate_old_out;
+ }
+
+ p->pUnpacked = sqlite3VdbeRecordUnpack(&keyinfo, nRecord, aRecord, 0, 0);
+ p->aRecord = aRecord;
+ }
+
+ if( iIdx>=p->pUnpacked->nField ){
+ *ppValue = (sqlite3_value *)columnNullValue();
+ }else{
+ *ppValue = &p->pUnpacked->aMem[iIdx];
+ sqlite3VdbeMemStoreType(*ppValue);
+ }
+
+ preupdate_old_out:
+ sqlite3Error(db, rc, 0);
+ return sqlite3ApiExit(db, rc);
+}
+
+int sqlite3_preupdate_count(sqlite3 *db){
+ PreUpdate *p = db->pPreUpdate;
+ return (p ? p->pCsr->nField : 0);
+}
+
+int sqlite3_preupdate_modified(sqlite3 *db, int iIdx, int *pbMod){
+ PreUpdate *p = db->pPreUpdate;
+ int rc = SQLITE_OK;
+
+ if( !p || p->op!=SQLITE_UPDATE ){
+ rc = SQLITE_MISUSE_BKPT;
+ goto preupdate_mod_out;
+ }
+ if( iIdx>=p->pCsr->nField || iIdx<0 ){
+ rc = SQLITE_RANGE;
+ goto preupdate_mod_out;
+ }
+ *pbMod = 1;
+
+ preupdate_mod_out:
+ sqlite3Error(db, rc, 0);
+ return sqlite3ApiExit(db, rc);
+}
+
+
diff --git a/src/vdbeaux.c b/src/vdbeaux.c
index 64ff48991..31cf66914 100644
--- a/src/vdbeaux.c
+++ b/src/vdbeaux.c
@@ -2830,7 +2830,7 @@ void sqlite3VdbeDeleteUnpackedRecord(UnpackedRecord *p){
** strings and blobs static. And none of the elements are
** ever transformed, so there is never anything to delete.
*/
- if( NEVER(pMem->zMalloc) ) sqlite3VdbeMemRelease(pMem);
+ if( pMem->zMalloc ) sqlite3VdbeMemRelease(pMem);
}
if( p->flags & UNPACKED_NEED_FREE ){
sqlite3DbFree(p->pKeyInfo->db, p);
@@ -3162,3 +3162,35 @@ void sqlite3VdbeSetVarmask(Vdbe *v, int iVar){
v->expmask |= ((u32)1 << (iVar-1));
}
}
+
+/*
+** 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 */
+ const char *zTbl, /* Table name */
+ i64 iKey1, /* Initial key value */
+ i64 iKey2 /* Final key value */
+){
+ sqlite3 *db = v->db;
+
+ PreUpdate preupdate;
+ memset(&preupdate, 0, sizeof(PreUpdate));
+
+ preupdate.pCsr = pCsr;
+ preupdate.op = op;
+ db->pPreUpdate = &preupdate;
+ db->xPreUpdateCallback(db->pPreUpdateArg, db, op, zDb, zTbl, iKey1, iKey2);
+ db->pPreUpdate = 0;
+ sqlite3DbFree(db, preupdate.aRecord);
+ if( preupdate.pUnpacked ){
+ sqlite3VdbeDeleteUnpackedRecord(preupdate.pUnpacked);
+ }
+}
+