diff --git a/manifest b/manifest index ac49a38388..b499538486 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Version\s2.4.12\s(CVS\s561) -D 2002-05-10T14:41:54 +C Added\sFOR\sEACH\sROW\striggers\sfunctionality\s(CVS\s562) +D 2002-05-15T08:30:13 F Makefile.in 50f1b3351df109b5774771350d8c1b8d3640130d F Makefile.template 89e373b2dad0321df00400fa968dc14b61a03296 F README a4c0ba11354ef6ba0776b400d057c59da47a4cc0 @@ -20,40 +20,41 @@ F sqlite.1 83f4a9d37bdf2b7ef079a82d54eaf2e3509ee6ea F src/TODO af7f3cab0228e34149cf98e073aa83d45878e7e6 F src/btree.c 7dd7ddc66459982dd0cb9800958c1f8d65a32d9f F src/btree.h 8abeabfe6e0b1a990b64fa457592a6482f6674f3 -F src/build.c 6a5064503250e7b8cdd9285970d01522df8468f7 -F src/delete.c 6a6b8192cdff5e4b083da3bc63de099f3790d01f +F src/build.c cbf1b552d381c3f94baad9be2defbc60a158ac64 +F src/delete.c 392159781f9dff5f07ce2cb7d3a3a184eb38c0ab F src/encode.c 346b12b46148506c32038524b95c4631ab46d760 -F src/expr.c cf8d2ea17e419fc83b23e080195b2952e0be4164 +F src/expr.c 6888e37e4eecdc20567aedd442328df752465723 F src/func.c a31dcba85bc2ecb9b752980289cf7e6cd0cafbce F src/hash.c cc259475e358baaf299b00a2c7370f2b03dda892 F src/hash.h dca065dda89d4575f3176e75e9a3dc0f4b4fb8b9 -F src/insert.c 31233f44fc79edbb43523a830e54736a8e222ff4 -F src/main.c 0a4643660b2a3dee3427a10fcea336756526c27c +F src/insert.c 9f89b395e25f2a9eaea841fa736a4036d33d2b24 +F src/main.c 6bc0b3dd014f6af13007472581593e87b2797139 F src/md5.c b2b1a34fce66ceca97f4e0dabc20be8be7933c92 F src/os.c 5ab8b6b4590d0c1ab8e96c67996c170e4462e0fc F src/os.h 4a361fccfbc4e7609b3e1557f604f94c1e96ad10 F src/pager.c ba5740104cc27b342cd43eebfdc44d60f64a3ded F src/pager.h 6fddfddd3b73aa8abc081b973886320e3c614f0e -F src/parse.y 83850a81fe9170d32eb683e77d7602736c663e34 +F src/parse.y 164789531d0c6a2c28fb4baded14afc1be4bd4aa F src/printf.c 300a90554345751f26e1fc0c0333b90a66110a1d F src/random.c 19e8e00fe0df32a742f115773f57651be327cabe F src/select.c 1b623a7d826ec7c245bc542b665d61724da2a62d F src/shell.c 5acbe59e137d60d8efd975c683dbea74ab626530 F src/shell.tcl 27ecbd63dd88396ad16d81ab44f73e6c0ea9d20e F src/sqlite.h.in 0038faa6d642de06b91143ee65a131bd831d020b -F src/sqliteInt.h b37d2d28e4ca3dcf67772bf937aa12b171d9610b +F src/sqliteInt.h a96603825503c5bbd095f1ac34ce1023f89a908e F src/table.c eed2098c9b577aa17f8abe89313a9c4413f57d63 F src/tclsqlite.c 9300c9606a38bc0c75d6c0bc8a6197ab979353d1 F src/test1.c 09d95048b66ce6dcd2bae90f443589043d7d631e F src/test2.c 669cc22781c6461a273416ec1a7414d25c081730 F src/test3.c 4e52fff8b01f08bd202f7633feda5639b7ba2b5e F src/threadtest.c 81f0598e0f031c1bd506af337fdc1b7e8dff263f -F src/tokenize.c 5624d342601f616157ba266abccc1368a5afee70 -F src/update.c 7dd714a6a7fa47f849ebb36b6d915974d6c6accb +F src/tokenize.c f12f78c58b2a79ea4eee880efad63a328e103c62 +F src/trigger.c b8df3e8f0952979bbbcbd0cb05b7d564924a3282 +F src/update.c 2e8becd1cd3a597f74f8879e2c246cca5d20a119 F src/util.c 707c30f8c13cddace7c08556ac450c0b786660b3 -F src/vdbe.c c957417fa83b5fb717dcd81204c253125b3e7e0c -F src/vdbe.h 67840a462e1daedb958cca0ccc97db140d3d9152 -F src/where.c 5e3e97adfa5800378f2ed45bb9312dd3a70e239c +F src/vdbe.c 428d7dba1fb84a3da6170c3cb387d177c315a72a +F src/vdbe.h 126a651ba26f05de075dcc6da5466244a31af6b8 +F src/where.c 3138c1b44193ab5f432919ab25e49f3d97bd6108 F test/all.test e4d3821eeba751829b419cd47814bd20af4286d1 F test/bigrow.test 8ab252dba108f12ad64e337b0f2ff31a807ac578 F test/btree.test bf326f546a666617367a7033fa2c07451bd4f8e1 @@ -98,6 +99,8 @@ F test/tclsqlite.test 79deeffd7cd637ca0f06c5dbbf2f44d272079533 F test/temptable.test daa83489eea2e9aaeeece09675c28be84c72cb67 F test/tester.tcl dc1b56bd628b487e4d75bfd1e7480b5ed8810ac6 F test/trans.test ae0b9a82d5d34122c3a3108781eb8d078091ccee +F test/trigger1.test 06dd47935cf38ce5de0b232e7b61aad57685bae1 +F test/trigger2.test 662818d5cc3313c14819df1c9084c119057a0bde F test/unique.test 07776624b82221a80c8b4138ce0dd8b0853bb3ea F test/update.test 3cf1ca0565f678063c2dfa9a7948d2d66ae1a778 F test/vacuum.test 059871b312eb910bbe49dafde1d01490cc2c6bbe @@ -124,14 +127,14 @@ F www/dynload.tcl 02eb8273aa78cfa9070dd4501dca937fb22b466c F www/faq.tcl 45bdb18b75ac3aa1befec42985fb892413aac0bb F www/formatchng.tcl 2ce21ff30663fad6618198fe747ce675df577590 F www/index.tcl d0c52fbf031d0a3ee6d9d77aa669d5a4b24b6130 -F www/lang.tcl d47800eb1da14a2ea501c6088beccc4001fb0486 +F www/lang.tcl a22cf9eff51e65ec5aa39b1efb5b7952d800ac06 F www/mingw.tcl f1c7c0a7f53387dd9bb4f8c7e8571b7561510ebc F www/opcode.tcl bdec8ef9f100dbd87bbef8976c54b88e43fd8ccc F www/speed.tcl da8afcc1d3ccc5696cfb388a68982bc3d9f7f00f F www/sqlite.tcl 8b5884354cb615049aed83039f8dfe1552a44279 F www/tclsqlite.tcl 1db15abeb446aad0caf0b95b8b9579720e4ea331 F www/vdbe.tcl 2013852c27a02a091d39a766bc87cff329f21218 -P 232b7ef2c8207eb6d2564a641446267d3dec97af -R 3b5832b01c3219f88712622031d79de7 -U drh -Z 49360959ab78dbfb5d6ce76a90c8f01e +P 06cdaf1c80f7bc25fc555c7c8a35258faed2d2e9 +R f9f4dba69771590feeaaa99270d2ac88 +U danielk1977 +Z 1a13ba7db9cc1bf25e7e68999e113789 diff --git a/manifest.uuid b/manifest.uuid index 7110bdab46..21f977034f 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -06cdaf1c80f7bc25fc555c7c8a35258faed2d2e9 \ No newline at end of file +794bf67b6b36fce8854d5daff12f21dbb943240c \ No newline at end of file diff --git a/src/build.c b/src/build.c index a32f21e160..aff5d51bf4 100644 --- a/src/build.c +++ b/src/build.c @@ -25,7 +25,7 @@ ** ROLLBACK ** PRAGMA ** -** $Id: build.c,v 1.87 2002/05/08 21:30:15 drh Exp $ +** $Id: build.c,v 1.88 2002/05/15 08:30:13 danielk1977 Exp $ */ #include "sqliteInt.h" #include @@ -250,6 +250,22 @@ void sqliteCommitInternalChanges(sqlite *db){ sqliteUnlinkAndDeleteIndex(db, pIndex); } sqliteHashClear(&db->idxDrop); + + /* Set the commit flag on all triggers added this transaction */ + for(pElem=sqliteHashFirst(&db->trigHash); pElem; pElem=sqliteHashNext(pElem)){ + Trigger *pTrigger = sqliteHashData(pElem); + pTrigger->isCommit = 1; + } + + /* Delete the structures for triggers removed this transaction */ + pElem = sqliteHashFirst(&db->trigDrop); + while (pElem) { + Trigger *pTrigger = sqliteHashData(pElem); + sqliteDeleteTrigger(pTrigger); + pElem = sqliteHashNext(pElem); + } + sqliteHashClear(&db->trigDrop); + db->flags &= ~SQLITE_InternChanges; } @@ -304,6 +320,48 @@ void sqliteRollbackInternalChanges(sqlite *db){ assert( pOld==0 || pOld==p ); } sqliteHashClear(&db->idxDrop); + + /* Remove any triggers that haven't been commited yet */ + for(pElem = sqliteHashFirst(&db->trigHash); pElem; + pElem = (pElem?sqliteHashNext(pElem):0)) { + Trigger * pTrigger = sqliteHashData(pElem); + if (!pTrigger->isCommit) { + Table * tbl = sqliteFindTable(db, pTrigger->table); + if (tbl) { + if (tbl->pTrigger == pTrigger) + tbl->pTrigger = pTrigger->pNext; + else { + Trigger * cc = tbl->pTrigger; + while (cc) { + if (cc->pNext == pTrigger) { + cc->pNext = cc->pNext->pNext; + break; + } + cc = cc->pNext; + } + assert(cc); + } + } + sqliteHashInsert(&db->trigHash, pTrigger->name, + 1 + strlen(pTrigger->name), 0); + sqliteDeleteTrigger(pTrigger); + pElem = sqliteHashFirst(&db->trigHash); + } + } + + /* Any triggers that were dropped - put 'em back in place */ + for(pElem = sqliteHashFirst(&db->trigDrop); pElem; + pElem = sqliteHashNext(pElem)) { + Trigger * pTrigger = sqliteHashData(pElem); + Table * tab = sqliteFindTable(db, pTrigger->table); + sqliteHashInsert(&db->trigHash, pTrigger->name, + strlen(pTrigger->name) + 1, pTrigger); + + pTrigger->pNext = tab->pTrigger; + tab->pTrigger = pTrigger; + } + + sqliteHashClear(&db->trigDrop); db->flags &= ~SQLITE_InternChanges; } @@ -595,7 +653,7 @@ void sqliteAddPrimaryKey(Parse *pParse, IdList *pList, int onError){ ** and the probability of hitting the same cookie value is only ** 1 chance in 2^32. So we're safe enough. */ -static void changeCookie(sqlite *db){ +void changeCookie(sqlite *db){ if( db->next_cookie==db->schema_cookie ){ db->next_cookie = db->schema_cookie + sqliteRandomByte() + 1; db->flags |= SQLITE_InternChanges; @@ -1036,6 +1094,13 @@ void sqliteDropTable(Parse *pParse, Token *pName, int isView){ }; Index *pIdx; sqliteBeginWriteOperation(pParse); + /* Drop all triggers associated with the table being dropped */ + while (pTable->pTrigger) { + Token tt; + tt.z = pTable->pTrigger->name; + tt.n = strlen(pTable->pTrigger->name); + sqliteDropTrigger(pParse, &tt, 1); + } if( !pTable->isTemp ){ base = sqliteVdbeAddOpList(v, ArraySize(dropTable), dropTable); sqliteVdbeChangeP3(v, base+2, pTable->zName, 0); @@ -1653,6 +1718,7 @@ void sqliteBeginWriteOperation(Parse *pParse){ Vdbe *v; v = sqliteGetVdbe(pParse); if( v==0 ) return; + if (pParse->trigStack) return; /* if this is in a trigger */ if( (pParse->db->flags & SQLITE_InTrans)==0 ){ sqliteVdbeAddOp(v, OP_Transaction, 0, 0); sqliteVdbeAddOp(v, OP_VerifyCookie, pParse->db->schema_cookie, 0); @@ -1672,6 +1738,7 @@ void sqliteBeginMultiWriteOperation(Parse *pParse){ Vdbe *v; v = sqliteGetVdbe(pParse); if( v==0 ) return; + if (pParse->trigStack) return; /* if this is in a trigger */ if( (pParse->db->flags & SQLITE_InTrans)==0 ){ sqliteVdbeAddOp(v, OP_Transaction, 0, 0); sqliteVdbeAddOp(v, OP_VerifyCookie, pParse->db->schema_cookie, 0); @@ -1689,6 +1756,7 @@ void sqliteBeginMultiWriteOperation(Parse *pParse){ */ void sqliteEndWriteOperation(Parse *pParse){ Vdbe *v; + if (pParse->trigStack) return; /* if this is in a trigger */ v = sqliteGetVdbe(pParse); if( v==0 ) return; if( pParse->db->flags & SQLITE_InTrans ){ @@ -1915,6 +1983,14 @@ void sqlitePragma(Parse *pParse, Token *pLeft, Token *pRight, int minusFlag){ } }else + if( sqliteStrICmp(zLeft, "trigger_overhead_test")==0 ){ + if( getBoolean(zRight) ){ + always_code_trigger_setup = 1; + }else{ + always_code_trigger_setup = 0; + } + }else + if( sqliteStrICmp(zLeft, "vdbe_trace")==0 ){ if( getBoolean(zRight) ){ db->flags |= SQLITE_VdbeTrace; diff --git a/src/delete.c b/src/delete.c index 3f7e0f7edf..2d04100ebf 100644 --- a/src/delete.c +++ b/src/delete.c @@ -12,7 +12,7 @@ ** This file contains C code routines that are called by the parser ** to handle DELETE FROM statements. ** -** $Id: delete.c,v 1.30 2002/04/12 10:08:59 drh Exp $ +** $Id: delete.c,v 1.31 2002/05/15 08:30:13 danielk1977 Exp $ */ #include "sqliteInt.h" @@ -84,6 +84,8 @@ void sqliteDeleteFrom( sqlite *db; /* Main database structure */ int openOp; /* Opcode used to open a cursor to the table */ + int row_triggers_exist = 0; + int oldIdx = -1; if( pParse->nErr || sqlite_malloc_failed ){ pTabList = 0; @@ -91,6 +93,31 @@ void sqliteDeleteFrom( } db = pParse->db; + /* Check for the special case of a VIEW with one or more ON DELETE triggers + * defined + */ + { + Table * pTab; + char * zTab = sqliteTableNameFromToken(pTableName); + + if(zTab != 0) { + pTab = sqliteFindTable(pParse->db, zTab); + if (pTab) { + row_triggers_exist = + sqliteTriggersExist(pParse, pTab->pTrigger, + TK_DELETE, TK_BEFORE, TK_ROW, 0) || + sqliteTriggersExist(pParse, pTab->pTrigger, + TK_DELETE, TK_AFTER, TK_ROW, 0); + } + sqliteFree(zTab); + if (row_triggers_exist && pTab->pSelect ) { + /* Just fire VIEW triggers */ + sqliteViewTriggers(pParse, pTab, pWhere, OE_Replace, 0); + return; + } + } + } + /* Locate the table which we want to delete. This table has to be ** put in an IdList structure because some of the subroutines we ** will be calling are designed to work with multiple tables and expect @@ -102,6 +129,9 @@ void sqliteDeleteFrom( pTab = pTabList->a[0].pTab; assert( pTab->pSelect==0 ); /* This table is not a view */ + if (row_triggers_exist) + oldIdx = pParse->nTab++; + /* Resolve the column names in all the expressions. */ base = pParse->nTab++; @@ -118,7 +148,10 @@ void sqliteDeleteFrom( */ v = sqliteGetVdbe(pParse); if( v==0 ) goto delete_from_cleanup; - sqliteBeginWriteOperation(pParse); + if (row_triggers_exist) + sqliteBeginMultiWriteOperation(pParse); + else + sqliteBeginWriteOperation(pParse); /* Initialize the counter of the number of rows deleted, if ** we are counting rows. @@ -130,7 +163,7 @@ void sqliteDeleteFrom( /* Special case: A DELETE without a WHERE clause deletes everything. ** It is easier just to erase the whole table. */ - if( pWhere==0 ){ + if( pWhere==0 && !row_triggers_exist){ if( db->flags & SQLITE_CountRows ){ /* If counting rows deleted, just count the total number of ** entries in the table. */ @@ -176,17 +209,66 @@ void sqliteDeleteFrom( ** because deleting an item can change the scan order. */ sqliteVdbeAddOp(v, OP_ListRewind, 0, 0); + end = sqliteVdbeMakeLabel(v); + + if (row_triggers_exist) { + int ii; + addr = sqliteVdbeAddOp(v, OP_ListRead, 0, end); + sqliteVdbeAddOp(v, OP_Dup, 0, 0); + + openOp = pTab->isTemp ? OP_OpenAux : OP_Open; + sqliteVdbeAddOp(v, openOp, base, pTab->tnum); + sqliteVdbeAddOp(v, OP_MoveTo, base, 0); + sqliteVdbeAddOp(v, OP_OpenTemp, oldIdx, 0); + + sqliteVdbeAddOp(v, OP_Integer, 13, 0); + for (ii = 0; ii < pTab->nCol; ii++) { + if (ii == pTab->iPKey) + sqliteVdbeAddOp(v, OP_Recno, base, 0); + else + sqliteVdbeAddOp(v, OP_Column, base, ii); + } + sqliteVdbeAddOp(v, OP_MakeRecord, pTab->nCol, 0); + sqliteVdbeAddOp(v, OP_PutIntKey, oldIdx, 0); + sqliteVdbeAddOp(v, OP_Close, base, 0); + sqliteVdbeAddOp(v, OP_Rewind, oldIdx, 0); + + sqliteCodeRowTrigger(pParse, TK_DELETE, 0, TK_BEFORE, pTab, -1, + oldIdx, (pParse->trigStack)?pParse->trigStack->orconf:OE_Default); + } + + pParse->nTab = base + 1; openOp = pTab->isTemp ? OP_OpenWrAux : OP_OpenWrite; sqliteVdbeAddOp(v, openOp, base, pTab->tnum); for(i=1, pIdx=pTab->pIndex; pIdx; i++, pIdx=pIdx->pNext){ - sqliteVdbeAddOp(v, openOp, base+i, pIdx->tnum); + sqliteVdbeAddOp(v, openOp, pParse->nTab++, pIdx->tnum); } - end = sqliteVdbeMakeLabel(v); - addr = sqliteVdbeAddOp(v, OP_ListRead, 0, end); - sqliteGenerateRowDelete(v, pTab, base, 1); + + if (!row_triggers_exist) + addr = sqliteVdbeAddOp(v, OP_ListRead, 0, end); + + sqliteGenerateRowDelete(v, pTab, base, pParse->trigStack?0:1); + + if (row_triggers_exist) { + for(i=1, pIdx=pTab->pIndex; pIdx; i++, pIdx=pIdx->pNext){ + sqliteVdbeAddOp(v, OP_Close, base + i, pIdx->tnum); + } + sqliteVdbeAddOp(v, OP_Close, base, 0); + sqliteCodeRowTrigger(pParse, TK_DELETE, 0, TK_AFTER, pTab, -1, + oldIdx, (pParse->trigStack)?pParse->trigStack->orconf:OE_Default); + } + sqliteVdbeAddOp(v, OP_Goto, 0, addr); sqliteVdbeResolveLabel(v, end); sqliteVdbeAddOp(v, OP_ListReset, 0, 0); + + if (!row_triggers_exist) { + for(i=1, pIdx=pTab->pIndex; pIdx; i++, pIdx=pIdx->pNext){ + sqliteVdbeAddOp(v, OP_Close, base + i, pIdx->tnum); + } + sqliteVdbeAddOp(v, OP_Close, base, 0); + pParse->nTab = base; + } } sqliteEndWriteOperation(pParse); diff --git a/src/expr.c b/src/expr.c index 9184758b66..b41e0ad8c2 100644 --- a/src/expr.c +++ b/src/expr.c @@ -12,7 +12,7 @@ ** This file contains routines used for analyzing expressions and ** for generating VDBE code that evaluates expressions in SQLite. ** -** $Id: expr.c,v 1.58 2002/04/20 14:24:42 drh Exp $ +** $Id: expr.c,v 1.59 2002/05/15 08:30:13 danielk1977 Exp $ */ #include "sqliteInt.h" @@ -481,6 +481,33 @@ int sqliteExprResolveIds( } } } + + /* If we have not already resolved this *.* expression, then maybe + * it is a new.* or old.* trigger argument reference */ + if (cnt == 0 && pParse->trigStack != 0) { + TriggerStack * tt = pParse->trigStack; + int j; + int t = 0; + if (tt->newIdx != -1 && sqliteStrICmp("new", zLeft) == 0) { + pExpr->iTable = tt->newIdx; + cntTab++; + t = 1; + } + if (tt->oldIdx != -1 && sqliteStrICmp("old", zLeft) == 0) { + pExpr->iTable = tt->oldIdx; + cntTab++; + t = 1; + } + + if (t) + for(j=0; jpTab->nCol; j++) { + if( sqliteStrICmp(tt->pTab->aCol[j].zName, zRight)==0 ){ + cnt++; + pExpr->iColumn = j; + } + } + } + if( cnt==0 && cntTab==1 && sqliteIsRowid(zRight) ){ cnt = 1; pExpr->iColumn = -1; diff --git a/src/insert.c b/src/insert.c index a848ee12bb..0174071304 100644 --- a/src/insert.c +++ b/src/insert.c @@ -12,7 +12,7 @@ ** This file contains C code routines that are called by the parser ** to handle INSERT statements in SQLite. ** -** $Id: insert.c,v 1.52 2002/04/12 10:08:59 drh Exp $ +** $Id: insert.c,v 1.53 2002/05/15 08:30:13 danielk1977 Exp $ */ #include "sqliteInt.h" @@ -40,7 +40,7 @@ void sqliteInsert( int onError /* How to handle constraint errors */ ){ Table *pTab; /* The table to insert into */ - char *zTab; /* Name of the table into which we are inserting */ + char *zTab = 0; /* Name of the table into which we are inserting */ int i, j, idx; /* Loop counters */ Vdbe *v; /* Generate code into this virtual machine */ Index *pIdx; /* For looping over indices of the table */ @@ -53,6 +53,9 @@ void sqliteInsert( int keyColumn = -1; /* Column that is the INTEGER PRIMARY KEY */ int endOfLoop; /* Label for the end of the insertion loop */ + int row_triggers_exist = 0; /* True if there are FOR EACH ROW triggers */ + int newIdx = -1; + if( pParse->nErr || sqlite_malloc_failed ) goto insert_cleanup; db = pParse->db; @@ -60,21 +63,48 @@ void sqliteInsert( */ zTab = sqliteTableNameFromToken(pTableName); if( zTab==0 ) goto insert_cleanup; - pTab = sqliteTableNameToTable(pParse, zTab); + pTab = sqliteFindTable(pParse->db, zTab); + if( pTab==0 ){ + sqliteSetString(&pParse->zErrMsg, "no such table: ", zTab, 0); + pParse->nErr++; + goto insert_cleanup; + } + + /* Ensure that: + * (a) the table is not read-only, + * (b) that if it is a view then ON INSERT triggers exist + */ + row_triggers_exist = + sqliteTriggersExist(pParse, pTab->pTrigger, TK_INSERT, + TK_BEFORE, TK_ROW, 0) || + sqliteTriggersExist(pParse, pTab->pTrigger, TK_INSERT, TK_AFTER, TK_ROW, 0); + if( pTab->readOnly || (pTab->pSelect && !row_triggers_exist) ){ + sqliteSetString(&pParse->zErrMsg, + pTab->pSelect ? "view " : "table ", + zTab, + " may not be modified", 0); + pParse->nErr++; + goto insert_cleanup; + } sqliteFree(zTab); + zTab = 0; + if( pTab==0 ) goto insert_cleanup; - assert( pTab->pSelect==0 ); /* This table is not a VIEW */ /* Allocate a VDBE */ v = sqliteGetVdbe(pParse); if( v==0 ) goto insert_cleanup; - if( pSelect ){ + if( pSelect || row_triggers_exist ){ sqliteBeginMultiWriteOperation(pParse); }else{ sqliteBeginWriteOperation(pParse); } + /* if there are row triggers, allocate a temp table for new.* references. */ + if (row_triggers_exist) + newIdx = pParse->nTab++; + /* Figure out how many columns of data are supplied. If the data ** is coming from a SELECT statement, then this step has to generate ** all the code to implement the SELECT statement and leave the data @@ -173,25 +203,29 @@ void sqliteInsert( keyColumn = pTab->iPKey; } - /* Open cursors into the table that is received the new data and - ** all indices of that table. - */ - base = pParse->nTab; - openOp = pTab->isTemp ? OP_OpenWrAux : OP_OpenWrite; - sqliteVdbeAddOp(v, openOp, base, pTab->tnum); - sqliteVdbeChangeP3(v, -1, pTab->zName, P3_STATIC); - for(idx=1, pIdx=pTab->pIndex; pIdx; pIdx=pIdx->pNext, idx++){ - sqliteVdbeAddOp(v, openOp, idx+base, pIdx->tnum); - sqliteVdbeChangeP3(v, -1, pIdx->zName, P3_STATIC); - } - pParse->nTab += idx; - + /* Open the temp table for FOR EACH ROW triggers */ + if (row_triggers_exist) + sqliteVdbeAddOp(v, OP_OpenTemp, newIdx, 0); + /* Initialize the count of rows to be inserted */ - if( db->flags & SQLITE_CountRows ){ + if( db->flags & SQLITE_CountRows && !pParse->trigStack){ sqliteVdbeAddOp(v, OP_Integer, 0, 0); /* Initialize the row count */ } + /* Open tables and indices if there are no row triggers */ + if (!row_triggers_exist) { + base = pParse->nTab; + openOp = pTab->isTemp ? OP_OpenWrAux : OP_OpenWrite; + sqliteVdbeAddOp(v, openOp, base, pTab->tnum); + sqliteVdbeChangeP3(v, -1, pTab->zName, P3_STATIC); + for(idx=1, pIdx=pTab->pIndex; pIdx; pIdx=pIdx->pNext, idx++){ + sqliteVdbeAddOp(v, openOp, idx+base, pIdx->tnum); + sqliteVdbeChangeP3(v, -1, pIdx->zName, P3_STATIC); + } + pParse->nTab += idx; + } + /* If the data source is a SELECT statement, then we have to create ** a loop because there might be multiple rows of data. If the data ** source is an expression list, then exactly one row will be inserted @@ -203,91 +237,159 @@ void sqliteInsert( iCont = sqliteVdbeCurrentAddr(v); } + if (row_triggers_exist) { + + /* build the new.* reference row */ + sqliteVdbeAddOp(v, OP_Integer, 13, 0); + for(i=0; inCol; i++){ + if( pColumn==0 ){ + j = i; + }else{ + for(j=0; jnId; j++){ + if( pColumn->a[j].idx==i ) break; + } + } + if( pColumn && j>=pColumn->nId ){ + sqliteVdbeAddOp(v, OP_String, 0, 0); + sqliteVdbeChangeP3(v, -1, pTab->aCol[i].zDflt, P3_STATIC); + }else if( srcTab>=0 ){ + sqliteVdbeAddOp(v, OP_Column, srcTab, j); + }else{ + sqliteExprCode(pParse, pList->a[j].pExpr); + } + } + sqliteVdbeAddOp(v, OP_MakeRecord, pTab->nCol, 0); + sqliteVdbeAddOp(v, OP_PutIntKey, newIdx, 0); + sqliteVdbeAddOp(v, OP_Rewind, newIdx, 0); + + /* Fire BEFORE triggers */ + if ( + sqliteCodeRowTrigger(pParse, TK_INSERT, 0, TK_BEFORE, pTab, newIdx, -1, + onError) + ) goto insert_cleanup; + + /* Open the tables and indices for the INSERT */ + if (!pTab->pSelect) { + base = pParse->nTab; + openOp = pTab->isTemp ? OP_OpenWrAux : OP_OpenWrite; + sqliteVdbeAddOp(v, openOp, base, pTab->tnum); + sqliteVdbeChangeP3(v, -1, pTab->zName, P3_STATIC); + for(idx=1, pIdx=pTab->pIndex; pIdx; pIdx=pIdx->pNext, idx++){ + sqliteVdbeAddOp(v, openOp, idx+base, pIdx->tnum); + sqliteVdbeChangeP3(v, -1, pIdx->zName, P3_STATIC); + } + pParse->nTab += idx; + } + } + /* Push the record number for the new entry onto the stack. The ** record number is a randomly generate integer created by NewRecno ** except when the table has an INTEGER PRIMARY KEY column, in which ** case the record number is the same as that column. */ - if( keyColumn>=0 ){ - if( srcTab>=0 ){ - sqliteVdbeAddOp(v, OP_Column, srcTab, keyColumn); - }else{ - int addr; - sqliteExprCode(pParse, pList->a[keyColumn].pExpr); + if (!pTab->pSelect) { + if( keyColumn>=0 ){ + if( srcTab>=0 ){ + sqliteVdbeAddOp(v, OP_Column, srcTab, keyColumn); + }else{ + int addr; + sqliteExprCode(pParse, pList->a[keyColumn].pExpr); - /* If the PRIMARY KEY expression is NULL, then use OP_NewRecno - ** to generate a unique primary key value. - */ - addr = sqliteVdbeAddOp(v, OP_Dup, 0, 1); - sqliteVdbeAddOp(v, OP_NotNull, 0, addr+4); - sqliteVdbeAddOp(v, OP_Pop, 1, 0); + /* If the PRIMARY KEY expression is NULL, then use OP_NewRecno + ** to generate a unique primary key value. + */ + addr = sqliteVdbeAddOp(v, OP_Dup, 0, 1); + sqliteVdbeAddOp(v, OP_NotNull, 0, addr+4); + sqliteVdbeAddOp(v, OP_Pop, 1, 0); + sqliteVdbeAddOp(v, OP_NewRecno, base, 0); + } + sqliteVdbeAddOp(v, OP_MustBeInt, 0, 0); + }else{ sqliteVdbeAddOp(v, OP_NewRecno, base, 0); } - sqliteVdbeAddOp(v, OP_MustBeInt, 0, 0); - }else{ - sqliteVdbeAddOp(v, OP_NewRecno, base, 0); - } - /* Push onto the stack, data for all columns of the new entry, beginning - ** with the first column. - */ - for(i=0; inCol; i++){ - if( i==pTab->iPKey ){ - /* The value of the INTEGER PRIMARY KEY column is always a NULL. - ** Whenever this column is read, the record number will be substituted - ** in its place. So will fill this column with a NULL to avoid - ** taking up data space with information that will never be used. */ - sqliteVdbeAddOp(v, OP_String, 0, 0); - continue; - } - if( pColumn==0 ){ - j = i; - }else{ - for(j=0; jnId; j++){ - if( pColumn->a[j].idx==i ) break; + /* Push onto the stack, data for all columns of the new entry, beginning + ** with the first column. + */ + for(i=0; inCol; i++){ + if( i==pTab->iPKey ){ + /* The value of the INTEGER PRIMARY KEY column is always a NULL. + ** Whenever this column is read, the record number will be substituted + ** in its place. So will fill this column with a NULL to avoid + ** taking up data space with information that will never be used. */ + sqliteVdbeAddOp(v, OP_String, 0, 0); + continue; + } + if( pColumn==0 ){ + j = i; + }else{ + for(j=0; jnId; j++){ + if( pColumn->a[j].idx==i ) break; + } + } + if( pColumn && j>=pColumn->nId ){ + sqliteVdbeAddOp(v, OP_String, 0, 0); + sqliteVdbeChangeP3(v, -1, pTab->aCol[i].zDflt, P3_STATIC); + }else if( srcTab>=0 ){ + sqliteVdbeAddOp(v, OP_Column, srcTab, j); + }else{ + sqliteExprCode(pParse, pList->a[j].pExpr); } } - if( pColumn && j>=pColumn->nId ){ - sqliteVdbeAddOp(v, OP_String, 0, 0); - sqliteVdbeChangeP3(v, -1, pTab->aCol[i].zDflt, P3_STATIC); - }else if( srcTab>=0 ){ - sqliteVdbeAddOp(v, OP_Column, srcTab, j); - }else{ - sqliteExprCode(pParse, pList->a[j].pExpr); + + /* Generate code to check constraints and generate index keys and + ** do the insertion. + */ + endOfLoop = sqliteVdbeMakeLabel(v); + sqliteGenerateConstraintChecks(pParse, pTab, base, 0,0,0,onError,endOfLoop); + sqliteCompleteInsertion(pParse, pTab, base, 0,0,0); + + /* Update the count of rows that are inserted + */ + if( (db->flags & SQLITE_CountRows)!=0 && !pParse->trigStack){ + sqliteVdbeAddOp(v, OP_AddImm, 1, 0); } } - /* Generate code to check constraints and generate index keys and - ** do the insertion. - */ - endOfLoop = sqliteVdbeMakeLabel(v); - sqliteGenerateConstraintChecks(pParse, pTab, base, 0,0,0, onError, endOfLoop); - sqliteCompleteInsertion(pParse, pTab, base, 0,0,0); + if (row_triggers_exist) { + /* Close all tables opened */ + if (!pTab->pSelect) { + sqliteVdbeAddOp(v, OP_Close, base, 0); + for(idx=1, pIdx=pTab->pIndex; pIdx; pIdx=pIdx->pNext, idx++){ + sqliteVdbeAddOp(v, OP_Close, idx+base, 0); + } + } - /* Update the count of rows that are inserted - */ - if( (db->flags & SQLITE_CountRows)!=0 ){ - sqliteVdbeAddOp(v, OP_AddImm, 1, 0); + /* Code AFTER triggers */ + if ( + sqliteCodeRowTrigger(pParse, TK_INSERT, 0, TK_AFTER, pTab, newIdx, -1, + onError) + ) goto insert_cleanup; } /* The bottom of the loop, if the data source is a SELECT statement - */ + */ sqliteVdbeResolveLabel(v, endOfLoop); if( srcTab>=0 ){ sqliteVdbeAddOp(v, OP_Next, srcTab, iCont); sqliteVdbeResolveLabel(v, iBreak); sqliteVdbeAddOp(v, OP_Close, srcTab, 0); } - sqliteVdbeAddOp(v, OP_Close, base, 0); - for(idx=1, pIdx=pTab->pIndex; pIdx; pIdx=pIdx->pNext, idx++){ - sqliteVdbeAddOp(v, OP_Close, idx+base, 0); + + if (!row_triggers_exist) { + /* Close all tables opened */ + sqliteVdbeAddOp(v, OP_Close, base, 0); + for(idx=1, pIdx=pTab->pIndex; pIdx; pIdx=pIdx->pNext, idx++){ + sqliteVdbeAddOp(v, OP_Close, idx+base, 0); + } } + sqliteEndWriteOperation(pParse); /* - ** Return the number of rows inserted. + ** Return the number of rows inserted. */ - if( db->flags & SQLITE_CountRows ){ + if( db->flags & SQLITE_CountRows && !pParse->trigStack ){ sqliteVdbeAddOp(v, OP_ColumnCount, 1, 0); sqliteVdbeAddOp(v, OP_ColumnName, 0, 0); sqliteVdbeChangeP3(v, -1, "rows inserted", P3_STATIC); @@ -297,6 +399,7 @@ void sqliteInsert( insert_cleanup: if( pList ) sqliteExprListDelete(pList); if( pSelect ) sqliteSelectDelete(pSelect); + if ( zTab ) sqliteFree(zTab); sqliteIdListDelete(pColumn); } @@ -573,7 +676,7 @@ void sqliteCompleteInsertion( sqliteVdbeAddOp(v, OP_IdxPut, base+i+1, 0); } sqliteVdbeAddOp(v, OP_MakeRecord, pTab->nCol, 0); - sqliteVdbeAddOp(v, OP_PutIntKey, base, 1); + sqliteVdbeAddOp(v, OP_PutIntKey, base, pParse->trigStack?0:1); if( isUpdate && recnoChng ){ sqliteVdbeAddOp(v, OP_Pop, 1, 0); } diff --git a/src/main.c b/src/main.c index 56865f13c8..1a85ae3c50 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.71 2002/05/10 13:14:07 drh Exp $ +** $Id: main.c,v 1.72 2002/05/15 08:30:13 danielk1977 Exp $ */ #include "sqliteInt.h" #include "os.h" @@ -326,6 +326,8 @@ sqlite *sqlite_open(const char *zFilename, int mode, char **pzErrMsg){ if( db==0 ) goto no_mem_on_open; sqliteHashInit(&db->tblHash, SQLITE_HASH_STRING, 0); sqliteHashInit(&db->idxHash, SQLITE_HASH_STRING, 0); + sqliteHashInit(&db->trigHash, SQLITE_HASH_STRING, 0); + sqliteHashInit(&db->trigDrop, SQLITE_HASH_STRING, 0); sqliteHashInit(&db->tblDrop, SQLITE_HASH_POINTER, 0); sqliteHashInit(&db->idxDrop, SQLITE_HASH_POINTER, 0); sqliteHashInit(&db->aFunc, SQLITE_HASH_STRING, 1); @@ -383,11 +385,29 @@ no_mem_on_open: static void clearHashTable(sqlite *db, int preserveTemps){ HashElem *pElem; Hash temp1; + Hash temp2; assert( sqliteHashFirst(&db->tblDrop)==0 ); /* There can not be uncommitted */ assert( sqliteHashFirst(&db->idxDrop)==0 ); /* DROP TABLEs or DROP INDEXs */ temp1 = db->tblHash; - sqliteHashInit(&db->tblHash, SQLITE_HASH_STRING, 0); + temp2 = db->trigHash; + sqliteHashInit(&db->trigHash, SQLITE_HASH_STRING, 0); sqliteHashClear(&db->idxHash); + + for (pElem=sqliteHashFirst(&temp2); pElem; pElem=sqliteHashNext(pElem)){ + Trigger * pTrigger = sqliteHashData(pElem); + Table *pTab = sqliteFindTable(db, pTrigger->table); + assert(pTab); + if (pTab->isTemp) { + sqliteHashInsert(&db->trigHash, pTrigger->name, strlen(pTrigger->name), + pTrigger); + } else { + sqliteDeleteTrigger(pTrigger); + } + } + sqliteHashClear(&temp2); + + sqliteHashInit(&db->tblHash, SQLITE_HASH_STRING, 0); + for(pElem=sqliteHashFirst(&temp1); pElem; pElem=sqliteHashNext(pElem)){ Table *pTab = sqliteHashData(pElem); if( preserveTemps && pTab->isTemp ){ @@ -413,6 +433,7 @@ static void clearHashTable(sqlite *db, int preserveTemps){ } } sqliteHashClear(&temp1); + db->flags &= ~SQLITE_Initialized; } @@ -458,6 +479,7 @@ void sqlite_close(sqlite *db){ */ int sqlite_complete(const char *zSql){ int isComplete = 0; + int seenCreate = 0; while( *zSql ){ switch( *zSql ){ case ';': { @@ -501,6 +523,16 @@ int sqlite_complete(const char *zSql){ break; } default: { + if (seenCreate && !sqliteStrNICmp(zSql, "trigger", 7)) + while (sqliteStrNICmp(zSql, "end", 3)) + if (!*++zSql) return 0; + + if (!sqliteStrNICmp(zSql, "create", 6)) { + zSql = zSql + 5; + seenCreate = 1; + } else + seenCreate = 0; + isComplete = 0; break; } diff --git a/src/parse.y b/src/parse.y index 744a0b9146..b790746771 100644 --- a/src/parse.y +++ b/src/parse.y @@ -14,7 +14,7 @@ ** the parser. Lemon will also generate a header file containing ** numeric codes for all of the tokens. ** -** @(#) $Id: parse.y,v 1.63 2002/05/08 21:46:15 drh Exp $ +** @(#) $Id: parse.y,v 1.64 2002/05/15 08:30:14 danielk1977 Exp $ */ %token_prefix TK_ %token_type {Token} @@ -33,6 +33,11 @@ ** A structure for holding two integers */ struct twoint { int a,b; }; + +/* +** A structure for holding an integer and an IdList +*/ +struct int_idlist { int a; IdList * b; }; } // These are extra tokens used by the lexer but never seen by the @@ -628,3 +633,63 @@ number(A) ::= INTEGER(X). {A = X;} number(A) ::= FLOAT(X). {A = X;} plus_opt ::= PLUS. plus_opt ::= . + +//////////////////////////// The CREATE TRIGGER command ///////////////////// +cmd ::= CREATE(A) TRIGGER ids(B) trigger_time(C) trigger_event(D) ON ids(E) + foreach_clause(F) when_clause(G) + BEGIN trigger_cmd_list(S) END(Z). { + sqliteCreateTrigger(pParse, &B, C, D.a, D.b, &E, F, G, S, + A.z, (int)(Z.z - A.z) + Z.n ); +} + +%type trigger_time {int} +trigger_time(A) ::= BEFORE. { A = TK_BEFORE; } +trigger_time(A) ::= AFTER. { A = TK_AFTER; } +trigger_time(A) ::= INSTEAD OF. { A = TK_INSTEAD;} +trigger_time(A) ::= . { A = TK_BEFORE; } + +%type trigger_event {struct int_idlist} +trigger_event(A) ::= DELETE. { A.a = TK_DELETE; A.b = 0; } +trigger_event(A) ::= INSERT. { A.a = TK_INSERT; A.b = 0; } +trigger_event(A) ::= UPDATE. { A.a = TK_UPDATE; A.b = 0;} +trigger_event(A) ::= UPDATE OF inscollist(X). {A.a = TK_UPDATE; A.b = X; } + +%type foreach_clause {int} +foreach_clause(A) ::= . { A = TK_ROW; } +foreach_clause(A) ::= FOR EACH ROW. { A = TK_ROW; } +foreach_clause(A) ::= FOR EACH STATEMENT. { A = TK_STATEMENT; } + +%type when_clause {Expr *} +when_clause(A) ::= . { A = 0; } +when_clause(A) ::= WHEN expr(X). { A = X; } + +%type trigger_cmd_list {TriggerStep *} +trigger_cmd_list(A) ::= trigger_cmd(X) SEMI trigger_cmd_list(Y). { + X->pNext = Y ; A = X; } +trigger_cmd_list(A) ::= . { A = 0; } + +%type trigger_cmd {TriggerStep *} +// UPDATE +trigger_cmd(A) ::= UPDATE orconf(R) ids(X) SET setlist(Y) where_opt(Z). + { A = sqliteTriggerUpdateStep(&X, Y, Z, R); } + +// INSERT +trigger_cmd(A) ::= INSERT orconf(R) INTO ids(X) inscollist_opt(F) + VALUES LP itemlist(Y) RP. +{A = sqliteTriggerInsertStep(&X, F, Y, 0, R);} + +trigger_cmd(A) ::= INSERT orconf(R) INTO ids(X) inscollist_opt(F) select(S). + {A = sqliteTriggerInsertStep(&X, F, 0, S, R);} + +// DELETE +trigger_cmd(A) ::= DELETE FROM ids(X) where_opt(Y). + {A = sqliteTriggerDeleteStep(&X, Y);} + +// SELECT +trigger_cmd(A) ::= select(X). {A = sqliteTriggerSelectStep(X); } + +//////////////////////// DROP TRIGGER statement ////////////////////////////// +cmd ::= DROP TRIGGER ids(X). { + sqliteDropTrigger(pParse,&X,0); +} + diff --git a/src/sqliteInt.h b/src/sqliteInt.h index f8251bbd14..7354050a14 100644 --- a/src/sqliteInt.h +++ b/src/sqliteInt.h @@ -11,7 +11,7 @@ ************************************************************************* ** Internal interface definitions for SQLite. ** -** @(#) $Id: sqliteInt.h,v 1.107 2002/05/10 13:14:07 drh Exp $ +** @(#) $Id: sqliteInt.h,v 1.108 2002/05/15 08:30:14 danielk1977 Exp $ */ #include "sqlite.h" #include "hash.h" @@ -144,6 +144,9 @@ typedef struct WhereLevel WhereLevel; typedef struct Select Select; typedef struct AggExpr AggExpr; typedef struct FuncDef FuncDef; +typedef struct Trigger Trigger; +typedef struct TriggerStep TriggerStep; +typedef struct TriggerStack TriggerStack; /* ** Each database is an instance of the following structure @@ -170,6 +173,9 @@ struct sqlite { int magic; /* Magic number for detect library misuse */ int nChange; /* Number of rows changed */ int recursionDepth; /* Number of nested calls to sqlite_exec() */ + + Hash trigHash; /* All triggers indexed by name */ + Hash trigDrop; /* Uncommited dropped triggers */ }; /* @@ -270,6 +276,8 @@ struct Table { u8 isTransient; /* True if automatically deleted when VDBE finishes */ u8 hasPrimKey; /* True if there exists a primary key */ u8 keyConf; /* What to do in case of uniqueness conflict on iPKey */ + + Trigger *pTrigger; /* List of SQL triggers on this table */ }; /* @@ -550,8 +558,64 @@ struct Parse { ** while generating expressions. Normally false */ int schemaVerified; /* True if an OP_VerifySchema has been coded someplace ** other than after an OP_Transaction */ + + TriggerStack * trigStack; }; +struct TriggerStack { + Trigger * pTrigger; + Table * pTab; /* Table that triggers are currently being coded as */ + int newIdx; /* Index of "new" temp table */ + int oldIdx; /* Index of "old" temp table */ + int orconf; /* Current orconf policy */ + struct TriggerStack * pNext; +}; +struct TriggerStep { + int op; /* One of TK_DELETE, TK_UPDATE, TK_INSERT, TK_SELECT */ + int orconf; + + Select * pSelect; /* Valid for SELECT and sometimes + INSERT steps (when pExprList == 0) */ + Token target; /* Valid for DELETE, UPDATE, INSERT steps */ + Expr * pWhere; /* Valid for DELETE, UPDATE steps */ + ExprList * pExprList; /* Valid for UPDATE statements and sometimes + INSERT steps (when pSelect == 0) */ + IdList *pIdList; /* Valid for INSERT statements only */ + + TriggerStep * pNext; /* Next in the link-list */ +}; +struct Trigger { + char * name; /* The name of the trigger */ + char * table; /* The table or view to which the trigger applies */ + int op; /* One of TK_DELETE, TK_UPDATE, TK_INSERT */ + int tr_tm; /* One of TK_BEFORE, TK_AFTER, TK_INSTEAD */ + Expr * pWhen; /* The WHEN clause of the expresion (may be NULL) */ + IdList * pColumns; /* If this is an UPDATE OF trigger, + the column names are stored in this list */ + int foreach; /* One of TK_ROW or TK_STATEMENT */ + + TriggerStep * step_list; /* Link list of trigger program steps */ + + char * strings; /* pointer to the allocation of Token strings */ + Trigger * pNext; /* Next trigger associated with the table */ + int isCommit; +}; + +TriggerStep * sqliteTriggerSelectStep(Select *); +TriggerStep * sqliteTriggerInsertStep(Token *, IdList *, ExprList *, + Select *, int); +TriggerStep * sqliteTriggerUpdateStep(Token *, ExprList *, Expr *, int); +TriggerStep * sqliteTriggerDeleteStep(Token *, Expr *); + +extern int always_code_trigger_setup; + +void sqliteCreateTrigger(Parse * ,Token *, int, int, IdList *, Token *, int, Expr *, TriggerStep *, char const *,int); +void sqliteDropTrigger(Parse *, Token *, int); +int sqliteTriggersExist( Parse * , Trigger * , int , int , int, ExprList * ); +int sqliteCodeRowTrigger( Parse * pParse, int op, ExprList *, int tr_tm, Table * tbl, int newTable, int oldTable, int onError); + +void sqliteViewTriggers(Parse *, Table *, Expr *, int, ExprList *); + /* ** Internal function prototypes */ @@ -662,3 +726,5 @@ void sqliteRegisterBuildinFunctions(sqlite*); int sqliteSafetyOn(sqlite*); int sqliteSafetyOff(sqlite*); int sqliteSafetyCheck(sqlite*); + +void changeCookie(sqlite *); diff --git a/src/tokenize.c b/src/tokenize.c index 84eaab7f7d..00f4e07418 100644 --- a/src/tokenize.c +++ b/src/tokenize.c @@ -15,7 +15,7 @@ ** individual tokens and sends those tokens one-by-one over to the ** parser for analysis. ** -** $Id: tokenize.c,v 1.40 2002/03/24 13:13:29 drh Exp $ +** $Id: tokenize.c,v 1.41 2002/05/15 08:30:14 danielk1977 Exp $ */ #include "sqliteInt.h" #include "os.h" @@ -39,10 +39,12 @@ struct Keyword { */ static Keyword aKeywordTable[] = { { "ABORT", 0, TK_ABORT, 0 }, + { "AFTER", 0, TK_AFTER, 0 }, { "ALL", 0, TK_ALL, 0 }, { "AND", 0, TK_AND, 0 }, { "AS", 0, TK_AS, 0 }, { "ASC", 0, TK_ASC, 0 }, + { "BEFORE", 0, TK_BEFORE, 0 }, { "BEGIN", 0, TK_BEGIN, 0 }, { "BETWEEN", 0, TK_BETWEEN, 0 }, { "BY", 0, TK_BY, 0 }, @@ -61,10 +63,12 @@ static Keyword aKeywordTable[] = { { "DISTINCT", 0, TK_DISTINCT, 0 }, { "DROP", 0, TK_DROP, 0 }, { "END", 0, TK_END, 0 }, + { "EACH", 0, TK_EACH, 0 }, { "ELSE", 0, TK_ELSE, 0 }, { "EXCEPT", 0, TK_EXCEPT, 0 }, { "EXPLAIN", 0, TK_EXPLAIN, 0 }, { "FAIL", 0, TK_FAIL, 0 }, + { "FOR", 0, TK_FOR, 0 }, { "FROM", 0, TK_FROM, 0 }, { "GLOB", 0, TK_GLOB, 0 }, { "GROUP", 0, TK_GROUP, 0 }, @@ -73,6 +77,7 @@ static Keyword aKeywordTable[] = { { "IN", 0, TK_IN, 0 }, { "INDEX", 0, TK_INDEX, 0 }, { "INSERT", 0, TK_INSERT, 0 }, + { "INSTEAD", 0, TK_INSTEAD, 0 }, { "INTERSECT", 0, TK_INTERSECT, 0 }, { "INTO", 0, TK_INTO, 0 }, { "IS", 0, TK_IS, 0 }, @@ -83,6 +88,7 @@ static Keyword aKeywordTable[] = { { "NOT", 0, TK_NOT, 0 }, { "NOTNULL", 0, TK_NOTNULL, 0 }, { "NULL", 0, TK_NULL, 0 }, + { "OF", 0, TK_OF, 0 }, { "OFFSET", 0, TK_OFFSET, 0 }, { "ON", 0, TK_ON, 0 }, { "OR", 0, TK_OR, 0 }, @@ -91,6 +97,7 @@ static Keyword aKeywordTable[] = { { "PRIMARY", 0, TK_PRIMARY, 0 }, { "REPLACE", 0, TK_REPLACE, 0 }, { "ROLLBACK", 0, TK_ROLLBACK, 0 }, + { "ROW", 0, TK_ROW, 0 }, { "SELECT", 0, TK_SELECT, 0 }, { "SET", 0, TK_SET, 0 }, { "TABLE", 0, TK_TABLE, 0 }, @@ -98,6 +105,7 @@ static Keyword aKeywordTable[] = { { "TEMPORARY", 0, TK_TEMP, 0 }, { "THEN", 0, TK_THEN, 0 }, { "TRANSACTION", 0, TK_TRANSACTION, 0 }, + { "TRIGGER", 0, TK_TRIGGER, 0 }, { "UNION", 0, TK_UNION, 0 }, { "UNIQUE", 0, TK_UNIQUE, 0 }, { "UPDATE", 0, TK_UPDATE, 0 }, diff --git a/src/trigger.c b/src/trigger.c new file mode 100644 index 0000000000..8ad7027a13 --- /dev/null +++ b/src/trigger.c @@ -0,0 +1,643 @@ +/* + * All copyright on this work is disclaimed by the author. + * + */ + +#include "sqliteInt.h" +/* + * This is called by the parser when it sees a CREATE TRIGGER statement + */ +void +sqliteCreateTrigger( + Parse * pParse, /* The parse context of the CREATE TRIGGER statement */ + Token * nm, /* The name of the trigger */ + int tr_tm, /* One of TK_BEFORE, TK_AFTER */ + int op, /* One of TK_INSERT, TK_UPDATE, TK_DELETE */ + IdList * cols, /* column list if this is an UPDATE OF trigger */ + Token * tbl, /* The name of the table/view the trigger applies to */ + int foreach, /* One of TK_ROW or TK_STATEMENT */ + Expr * pWhen, /* WHEN clause */ + TriggerStep * steps, /* The triggered program */ + char const * cc, int len) /* The string data to make persistent */ +{ + Trigger * nt; + Table * tab; + int offset; + TriggerStep * ss; + + /* Check that: + 1. the trigger name does not already exist. + 2. the table (or view) does exist. + */ + { + char * tmp_str = sqliteStrNDup(nm->z, nm->n); + if (sqliteHashFind(&(pParse->db->trigHash), tmp_str, nm->n + 1)) { + sqliteSetNString(&pParse->zErrMsg, "trigger ", -1, + nm->z, nm->n, " already exists", -1, 0); + sqliteFree(tmp_str); + pParse->nErr++; + goto trigger_cleanup; + } + sqliteFree(tmp_str); + } + { + char * tmp_str = sqliteStrNDup(tbl->z, tbl->n); + tab = sqliteFindTable(pParse->db, tmp_str); + sqliteFree(tmp_str); + if (!tab) { + sqliteSetNString(&pParse->zErrMsg, "no such table: ", -1, + tbl->z, tbl->n, 0); + pParse->nErr++; + goto trigger_cleanup; + } + } + + /* Build the Trigger object */ + nt = (Trigger *)sqliteMalloc(sizeof(Trigger)); + + nt->name = sqliteStrNDup(nm->z, nm->n); + nt->table = sqliteStrNDup(tbl->z, tbl->n); + nt->op = op; + nt->tr_tm = tr_tm; + nt->pWhen = pWhen; + nt->pColumns = cols; + nt->foreach = foreach; + nt->step_list = steps; + nt->isCommit = 0; + + nt->strings = sqliteStrNDup(cc, len); + offset = (int)(nt->strings - cc); + + sqliteExprMoveStrings(nt->pWhen, offset); + + ss = nt->step_list; + while (ss) { + sqliteSelectMoveStrings(ss->pSelect, offset); + if (ss->target.z) ss->target.z += offset; + sqliteExprMoveStrings(ss->pWhere, offset); + sqliteExprListMoveStrings(ss->pExprList, offset); + + ss = ss->pNext; + } + + /* if we are not initializing, and this trigger is not on a TEMP table, + build the sqlite_master entry */ + if (!pParse->initFlag && !tab->isTemp) { + + /* Make an entry in the sqlite_master table */ + sqliteBeginWriteOperation(pParse); + + sqliteVdbeAddOp(pParse->pVdbe, OP_OpenWrite, 0, 2); + sqliteVdbeChangeP3(pParse->pVdbe, -1, MASTER_NAME, P3_STATIC); + sqliteVdbeAddOp(pParse->pVdbe, OP_NewRecno, 0, 0); + sqliteVdbeAddOp(pParse->pVdbe, OP_String, 0, 0); + sqliteVdbeChangeP3(pParse->pVdbe, -1, "trigger", P3_STATIC); + sqliteVdbeAddOp(pParse->pVdbe, OP_String, 0, 0); + sqliteVdbeChangeP3(pParse->pVdbe, -1, nt->name, 0); + sqliteVdbeAddOp(pParse->pVdbe, OP_String, 0, 0); + sqliteVdbeChangeP3(pParse->pVdbe, -1, nt->table, 0); + sqliteVdbeAddOp(pParse->pVdbe, OP_Integer, 0, 0); + sqliteVdbeAddOp(pParse->pVdbe, OP_String, 0, 0); + sqliteVdbeChangeP3(pParse->pVdbe, -1, nt->strings, 0); + sqliteVdbeAddOp(pParse->pVdbe, OP_MakeRecord, 5, 0); + sqliteVdbeAddOp(pParse->pVdbe, OP_PutIntKey, 0, 1); + + /* Change the cookie, since the schema is changed */ + changeCookie(pParse->db); + sqliteVdbeAddOp(pParse->pVdbe, OP_Integer, pParse->db->next_cookie, 0); + sqliteVdbeAddOp(pParse->pVdbe, OP_SetCookie, 0, 0); + + sqliteVdbeAddOp(pParse->pVdbe, OP_Close, 0, 0); + + sqliteEndWriteOperation(pParse); + } + + if (!pParse->explain) { + /* Stick it in the hash-table */ + sqliteHashInsert(&(pParse->db->trigHash), nt->name, nm->n + 1, nt); + + /* Attach it to the table object */ + nt->pNext = tab->pTrigger; + tab->pTrigger = nt; + return; + } else { + sqliteFree(nt->strings); + sqliteFree(nt->name); + sqliteFree(nt->table); + sqliteFree(nt); + } + +trigger_cleanup: + + sqliteIdListDelete(cols); + sqliteExprDelete(pWhen); + { + TriggerStep * pp; + TriggerStep * nn; + + pp = steps; + while (pp) { + nn = pp->pNext; + sqliteExprDelete(pp->pWhere); + sqliteExprListDelete(pp->pExprList); + sqliteSelectDelete(pp->pSelect); + sqliteIdListDelete(pp->pIdList); + sqliteFree(pp); + pp = nn; + } + } +} + + TriggerStep * +sqliteTriggerSelectStep(Select * s) +{ + TriggerStep * tt = sqliteMalloc(sizeof(TriggerStep)); + + tt->op = TK_SELECT; + tt->pSelect = s; + tt->orconf = OE_Default; + + return tt; +} + +TriggerStep * +sqliteTriggerInsertStep(Token * tbl, IdList * col, ExprList * val, Select * s, int orconf) +{ + TriggerStep * tt = sqliteMalloc(sizeof(TriggerStep)); + + assert(val == 0 || s == 0); + assert(val != 0 || s != 0); + + tt->op = TK_INSERT; + tt->pSelect = s; + tt->target = *tbl; + tt->pIdList = col; + tt->pExprList = val; + tt->orconf = orconf; + + return tt; +} + +TriggerStep * +sqliteTriggerUpdateStep(Token * tbl, ExprList * val, Expr * w, int orconf) +{ + TriggerStep * tt = sqliteMalloc(sizeof(TriggerStep)); + + tt->op = TK_UPDATE; + tt->target = *tbl; + tt->pExprList = val; + tt->pWhere = w; + tt->orconf = orconf; + + return tt; +} + +TriggerStep * +sqliteTriggerDeleteStep(Token * tbl, Expr * w) +{ + TriggerStep * tt = sqliteMalloc(sizeof(TriggerStep)); + + tt->op = TK_DELETE; + tt->target = *tbl; + tt->pWhere = w; + tt->orconf = OE_Default; + + return tt; +} + + +/* This does a recursive delete of the trigger structure */ +void sqliteDeleteTrigger(Trigger * tt) +{ + TriggerStep * ts, * tc; + ts = tt->step_list; + + while (ts) { + tc = ts; + ts = ts->pNext; + + sqliteExprDelete(tc->pWhere); + sqliteExprListDelete(tc->pExprList); + sqliteSelectDelete(tc->pSelect); + sqliteIdListDelete(tc->pIdList); + + sqliteFree(tc); + } + + sqliteFree(tt->name); + sqliteFree(tt->table); + sqliteExprDelete(tt->pWhen); + sqliteIdListDelete(tt->pColumns); + sqliteFree(tt->strings); + sqliteFree(tt); +} + +/* + * "nested" is true if this is begin called as the result of a DROP TABLE + */ +void sqliteDropTrigger(Parse *pParse, Token * trigname, int nested) +{ + char * tmp_name; + Trigger * trig; + Table * tbl; + + tmp_name = sqliteStrNDup(trigname->z, trigname->n); + + /* ensure that the trigger being dropped exists */ + trig = sqliteHashFind(&(pParse->db->trigHash), tmp_name, trigname->n + 1); + if (!trig) { + sqliteSetNString(&pParse->zErrMsg, "no such trigger: ", -1, + tmp_name, -1, 0); + sqliteFree(tmp_name); + return; + } + + /* + * If this is not an "explain", do the following: + * 1. Remove the trigger from its associated table structure + * 2. Move the trigger from the trigHash hash to trigDrop + */ + if (!pParse->explain) { + /* 1 */ + tbl = sqliteFindTable(pParse->db, trig->table); + assert(tbl); + if (tbl->pTrigger == trig) + tbl->pTrigger = trig->pNext; + else { + Trigger * cc = tbl->pTrigger; + while (cc) { + if (cc->pNext == trig) { + cc->pNext = cc->pNext->pNext; + break; + } + cc = cc->pNext; + } + assert(cc); + } + + /* 2 */ + sqliteHashInsert(&(pParse->db->trigHash), tmp_name, + trigname->n + 1, NULL); + sqliteHashInsert(&(pParse->db->trigDrop), trig->name, + trigname->n + 1, trig); + } + + /* Unless this is a trigger on a TEMP TABLE, generate code to destroy the + * database record of the trigger */ + if (!tbl->isTemp) { + int base; + static VdbeOp dropTrigger[] = { + { OP_OpenWrite, 0, 2, MASTER_NAME}, + { OP_Rewind, 0, ADDR(9), 0}, + { OP_String, 0, 0, 0}, /* 2 */ + { OP_MemStore, 1, 1, 0}, + { OP_MemLoad, 1, 0, 0}, /* 4 */ + { OP_Column, 0, 1, 0}, + { OP_Ne, 0, ADDR(8), 0}, + { OP_Delete, 0, 0, 0}, + { OP_Next, 0, ADDR(4), 0}, /* 8 */ + { OP_Integer, 0, 0, 0}, /* 9 */ + { OP_SetCookie, 0, 0, 0}, + { OP_Close, 0, 0, 0}, + }; + + if (!nested) + sqliteBeginWriteOperation(pParse); + + base = sqliteVdbeAddOpList(pParse->pVdbe, + ArraySize(dropTrigger), dropTrigger); + sqliteVdbeChangeP3(pParse->pVdbe, base+2, tmp_name, 0); + + if (!nested) + changeCookie(pParse->db); + + sqliteVdbeChangeP1(pParse->pVdbe, base+9, pParse->db->next_cookie); + + if (!nested) + sqliteEndWriteOperation(pParse); + } + + sqliteFree(tmp_name); +} + +static int checkColumnOverLap(IdList * ii, ExprList * ee) +{ + int i, e; + if (!ii) return 1; + if (!ee) return 1; + + for (i = 0; i < ii->nId; i++) + for (e = 0; e < ee->nExpr; e++) + if (!sqliteStrICmp(ii->a[i].zName, ee->a[e].zName)) + return 1; + + return 0; +} + +/* A global variable that is TRUE if we should always set up temp tables for + * for triggers, even if there are no triggers to code. This is used to test + * how much overhead the triggers algorithm is causing. + * + * This flag can be set or cleared using the "trigger_overhead_test" pragma. + * The pragma is not documented since it is not really part of the interface + * to SQLite, just the test procedure. +*/ +int always_code_trigger_setup = 0; + +/* + * Returns true if a trigger matching op, tr_tm and foreach that is NOT already + * on the Parse objects trigger-stack (to prevent recursive trigger firing) is + * found in the list specified as pTrigger. + */ +int sqliteTriggersExist( + Parse * pParse, + Trigger * pTrigger, + int op, /* one of TK_DELETE, TK_INSERT, TK_UPDATE */ + int tr_tm, /* one of TK_BEFORE, TK_AFTER */ + int foreach, /* one of TK_ROW or TK_STATEMENT */ + ExprList * pChanges) +{ + Trigger * tt; + + if (always_code_trigger_setup) return 1; + + tt = pTrigger; + while (tt) { + if (tt->op == op && tt->tr_tm == tr_tm && tt->foreach == foreach && + checkColumnOverLap(tt->pColumns, pChanges)) { + TriggerStack * ss; + ss = pParse->trigStack; + while (ss && ss->pTrigger != pTrigger) ss = ss->pNext; + if (!ss) return 1; + } + tt = tt->pNext; + } + + return 0; +} + +static int codeTriggerProgram( + Parse *pParse, + TriggerStep * program, + int onError) +{ + TriggerStep * step = program; + int orconf; + + while (step) { + int saveNTab = pParse->nTab; + orconf = (onError == OE_Default)?step->orconf:onError; + pParse->trigStack->orconf = orconf; + switch(step->op) { + case TK_SELECT: { + int tmp_tbl = pParse->nTab++; + sqliteVdbeAddOp(pParse->pVdbe, OP_OpenTemp, tmp_tbl, 0); + sqliteVdbeAddOp(pParse->pVdbe, OP_KeyAsData, tmp_tbl, 1); + sqliteSelect(pParse, step->pSelect, + SRT_Union, tmp_tbl, 0, 0, 0); + sqliteVdbeAddOp(pParse->pVdbe, OP_Close, tmp_tbl, 0); + pParse->nTab--; + break; + } + case TK_UPDATE: { + sqliteVdbeAddOp(pParse->pVdbe, OP_PushList, 0, 0); + sqliteUpdate(pParse, &step->target, + sqliteExprListDup(step->pExprList), + sqliteExprDup(step->pWhere), orconf); + sqliteVdbeAddOp(pParse->pVdbe, OP_PopList, 0, 0); + break; + } + case TK_INSERT: { + sqliteInsert(pParse, &step->target, + sqliteExprListDup(step->pExprList), + sqliteSelectDup(step->pSelect), + sqliteIdListDup(step->pIdList), orconf); + break; + } + case TK_DELETE: { + sqliteVdbeAddOp(pParse->pVdbe, OP_PushList, 0, 0); + sqliteDeleteFrom(pParse, &step->target, + sqliteExprDup(step->pWhere) + ); + sqliteVdbeAddOp(pParse->pVdbe, OP_PopList, 0, 0); + break; + } + default: + assert(0); + } + pParse->nTab = saveNTab; + step = step->pNext; + } + + return 0; +} + +int sqliteCodeRowTrigger( + Parse * pParse, /* Parse context */ + int op, /* One of TK_UPDATE, TK_INSERT, TK_DELETE */ + ExprList * changes, /* Changes list for any UPDATE OF triggers */ + int tr_tm, /* One of TK_BEFORE, TK_AFTER */ + Table * tbl, /* The table to code triggers from */ + int newTable, /* The indice of the "new" row to access */ + int oldTable, /* The indice of the "old" row to access */ + int onError) /* ON CONFLICT policy */ +{ + Trigger * pTrigger; + TriggerStack * pTriggerStack; + + + assert(op == TK_UPDATE || op == TK_INSERT || op == TK_DELETE); + assert(tr_tm == TK_BEFORE || tr_tm == TK_AFTER); + + assert(newTable != -1 || oldTable != -1); + + pTrigger = tbl->pTrigger; + while (pTrigger) { + int fire_this = 0; + + /* determine whether we should code this trigger */ + if (pTrigger->op == op && pTrigger->tr_tm == tr_tm && + pTrigger->foreach == TK_ROW) { + fire_this = 1; + pTriggerStack = pParse->trigStack; + while (pTriggerStack) { + if (pTriggerStack->pTrigger == pTrigger) fire_this = 0; + pTriggerStack = pTriggerStack->pNext; + } + if (op == TK_UPDATE && pTrigger->pColumns && + !checkColumnOverLap(pTrigger->pColumns, changes)) + fire_this = 0; + } + + if (fire_this) { + int endTrigger; + IdList dummyTablist; + Expr * whenExpr; + + dummyTablist.nId = 0; + dummyTablist.a = 0; + + /* Push an entry on to the trigger stack */ + pTriggerStack = sqliteMalloc(sizeof(TriggerStack)); + pTriggerStack->pTrigger = pTrigger; + pTriggerStack->newIdx = newTable; + pTriggerStack->oldIdx = oldTable; + pTriggerStack->pTab = tbl; + pTriggerStack->pNext = pParse->trigStack; + pParse->trigStack = pTriggerStack; + + /* code the WHEN clause */ + endTrigger = sqliteVdbeMakeLabel(pParse->pVdbe); + whenExpr = sqliteExprDup(pTrigger->pWhen); + if (sqliteExprResolveIds(pParse, 0, &dummyTablist, 0, whenExpr)) { + pParse->trigStack = pParse->trigStack->pNext; + sqliteFree(pTriggerStack); + sqliteExprDelete(whenExpr); + return 1; + } + sqliteExprIfFalse(pParse, whenExpr, endTrigger); + sqliteExprDelete(whenExpr); + + codeTriggerProgram(pParse, pTrigger->step_list, onError); + + /* Pop the entry off the trigger stack */ + pParse->trigStack = pParse->trigStack->pNext; + sqliteFree(pTriggerStack); + + sqliteVdbeResolveLabel(pParse->pVdbe, endTrigger); + } + pTrigger = pTrigger->pNext; + } + + return 0; +} + +/* + * Handle UPDATE and DELETE triggers on views + */ +void sqliteViewTriggers(Parse *pParse, Table *pTab, + Expr * pWhere, int onError, ExprList * pChanges) +{ + int oldIdx = -1; + int newIdx = -1; + int *aXRef = 0; + Vdbe *v; + int endOfLoop; + int startOfLoop; + Select theSelect; + Token tblNameToken; + + assert(pTab->pSelect); + + tblNameToken.z = pTab->zName; + tblNameToken.n = strlen(pTab->zName); + + theSelect.isDistinct = 0; + theSelect.pEList = sqliteExprListAppend(0, sqliteExpr(TK_ALL, 0, 0, 0), 0); + theSelect.pSrc = sqliteIdListAppend(0, &tblNameToken); + theSelect.pWhere = pWhere; pWhere = 0; + theSelect.pGroupBy = 0; + theSelect.pHaving = 0; + theSelect.pOrderBy = 0; + theSelect.op = TK_SELECT; /* ?? */ + theSelect.pPrior = 0; + theSelect.nLimit = -1; + theSelect.nOffset = -1; + theSelect.zSelect = 0; + theSelect.base = 0; + + v = sqliteGetVdbe(pParse); + assert(v); + sqliteBeginMultiWriteOperation(pParse); + + /* Allocate temp tables */ + oldIdx = pParse->nTab++; + sqliteVdbeAddOp(v, OP_OpenTemp, oldIdx, 0); + if (pChanges) { + newIdx = pParse->nTab++; + sqliteVdbeAddOp(v, OP_OpenTemp, newIdx, 0); + } + + /* Snapshot the view */ + if (sqliteSelect(pParse, &theSelect, SRT_Table, oldIdx, 0, 0, 0)) { + goto trigger_cleanup; + } + + /* loop thru the view snapshot, executing triggers for each row */ + endOfLoop = sqliteVdbeMakeLabel(v); + sqliteVdbeAddOp(v, OP_Rewind, oldIdx, endOfLoop); + + /* Loop thru the view snapshot, executing triggers for each row */ + startOfLoop = sqliteVdbeCurrentAddr(v); + + /* Build the updated row if required */ + if (pChanges) { + int ii, jj; + + aXRef = sqliteMalloc( sizeof(int) * pTab->nCol ); + if( aXRef==0 ) goto trigger_cleanup; + for (ii = 0; ii < pTab->nCol; ii++) + aXRef[ii] = -1; + + for(ii=0; iinExpr; ii++){ + int jj; + if( sqliteExprResolveIds(pParse, oldIdx, theSelect.pSrc , 0, + pChanges->a[ii].pExpr) ) + goto trigger_cleanup; + + if( sqliteExprCheck(pParse, pChanges->a[ii].pExpr, 0, 0) ) + goto trigger_cleanup; + + for(jj=0; jjnCol; jj++){ + if( sqliteStrICmp(pTab->aCol[jj].zName, pChanges->a[ii].zName)==0 ){ + aXRef[jj] = ii; + break; + } + } + if( jj>=pTab->nCol ){ + sqliteSetString(&pParse->zErrMsg, "no such column: ", + pChanges->a[ii].zName, 0); + pParse->nErr++; + goto trigger_cleanup; + } + } + + sqliteVdbeAddOp(v, OP_Integer, 13, 0); + + for (ii = 0; ii < pTab->nCol; ii++) + if( aXRef[ii] < 0 ) + sqliteVdbeAddOp(v, OP_Column, oldIdx, ii); + else + sqliteExprCode(pParse, pChanges->a[aXRef[ii]].pExpr); + + sqliteVdbeAddOp(v, OP_MakeRecord, pTab->nCol, 0); + sqliteVdbeAddOp(v, OP_PutIntKey, newIdx, 0); + sqliteVdbeAddOp(v, OP_Rewind, newIdx, 0); + + sqliteCodeRowTrigger(pParse, TK_UPDATE, pChanges, TK_BEFORE, + pTab, newIdx, oldIdx, onError); + sqliteCodeRowTrigger(pParse, TK_UPDATE, pChanges, TK_AFTER, + pTab, newIdx, oldIdx, onError); + } else { + sqliteCodeRowTrigger(pParse, TK_DELETE, 0, TK_BEFORE, pTab, -1, oldIdx, + onError); + sqliteCodeRowTrigger(pParse, TK_DELETE, 0, TK_AFTER, pTab, -1, oldIdx, + onError); + } + + sqliteVdbeAddOp(v, OP_Next, oldIdx, startOfLoop); + + sqliteVdbeResolveLabel(v, endOfLoop); + sqliteEndWriteOperation(pParse); + +trigger_cleanup: + sqliteFree(aXRef); + sqliteExprListDelete(pChanges); + sqliteExprDelete(pWhere); + sqliteExprListDelete(theSelect.pEList); + sqliteIdListDelete(theSelect.pSrc); + sqliteExprDelete(theSelect.pWhere); + return; +} + + diff --git a/src/update.c b/src/update.c index 8bf5e7c371..f729f950fd 100644 --- a/src/update.c +++ b/src/update.c @@ -12,7 +12,7 @@ ** This file contains C code routines that are called by the parser ** to handle UPDATE statements. ** -** $Id: update.c,v 1.36 2002/03/03 18:59:41 drh Exp $ +** $Id: update.c,v 1.37 2002/05/15 08:30:14 danielk1977 Exp $ */ #include "sqliteInt.h" @@ -47,9 +47,38 @@ void sqliteUpdate( Expr *pRecnoExpr; /* Expression defining the new record number */ int openAll; /* True if all indices need to be opened */ + int row_triggers_exist = 0; + + int newIdx = -1; /* index of trigger "new" temp table */ + int oldIdx = -1; /* index of trigger "old" temp table */ + if( pParse->nErr || sqlite_malloc_failed ) goto update_cleanup; db = pParse->db; + /* Check for the special case of a VIEW with one or more ON UPDATE triggers + * defined + */ + { + char * zTab = sqliteTableNameFromToken(pTableName); + + if(zTab != 0) { + pTab = sqliteFindTable(pParse->db, zTab); + if (pTab) { + row_triggers_exist = + sqliteTriggersExist(pParse, pTab->pTrigger, + TK_UPDATE, TK_BEFORE, TK_ROW, pChanges) || + sqliteTriggersExist(pParse, pTab->pTrigger, + TK_UPDATE, TK_AFTER, TK_ROW, pChanges); + } + sqliteFree(zTab); + if (row_triggers_exist && pTab->pSelect ) { + /* Just fire VIEW triggers */ + sqliteViewTriggers(pParse, pTab, pWhere, onError, pChanges); + return; + } + } + } + /* Locate the table which we want to update. This table has to be ** put in an IdList structure because some of the subroutines we ** will be calling are designed to work with multiple tables and expect @@ -63,6 +92,11 @@ void sqliteUpdate( if( aXRef==0 ) goto update_cleanup; for(i=0; inCol; i++) aXRef[i] = -1; + if (row_triggers_exist) { + newIdx = pParse->nTab++; + oldIdx = pParse->nTab++; + } + /* Resolve the column names in all the expressions in both the ** WHERE clause and in the new values. Also find the column index ** for each column to be updated in the pChanges array. @@ -159,17 +193,62 @@ void sqliteUpdate( /* Initialize the count of updated rows */ - if( db->flags & SQLITE_CountRows ){ + if( db->flags & SQLITE_CountRows && !pParse->trigStack ){ sqliteVdbeAddOp(v, OP_Integer, 0, 0); } + if (row_triggers_exist) { + int ii; + + sqliteVdbeAddOp(v, OP_OpenTemp, oldIdx, 0); + sqliteVdbeAddOp(v, OP_OpenTemp, newIdx, 0); + + sqliteVdbeAddOp(v, OP_ListRewind, 0, 0); + addr = sqliteVdbeAddOp(v, OP_ListRead, 0, 0); + sqliteVdbeAddOp(v, OP_Dup, 0, 0); + + sqliteVdbeAddOp(v, OP_Dup, 0, 0); + sqliteVdbeAddOp(v, OP_Open, base, pTab->tnum); + sqliteVdbeAddOp(v, OP_MoveTo, base, 0); + + sqliteVdbeAddOp(v, OP_Integer, 13, 0); + for (ii = 0; ii < pTab->nCol; ii++) { + if (ii == pTab->iPKey) + sqliteVdbeAddOp(v, OP_Recno, base, 0); + else + sqliteVdbeAddOp(v, OP_Column, base, ii); + } + sqliteVdbeAddOp(v, OP_MakeRecord, pTab->nCol, 0); + sqliteVdbeAddOp(v, OP_PutIntKey, oldIdx, 0); + + sqliteVdbeAddOp(v, OP_Integer, 13, 0); + for (ii = 0; ii < pTab->nCol; ii++){ + if( aXRef[ii] < 0 ){ + if (ii == pTab->iPKey) + sqliteVdbeAddOp(v, OP_Recno, base, 0); + else + sqliteVdbeAddOp(v, OP_Column, base, ii); + }else{ + sqliteExprCode(pParse, pChanges->a[aXRef[ii]].pExpr); + } + } + sqliteVdbeAddOp(v, OP_MakeRecord, pTab->nCol, 0); + sqliteVdbeAddOp(v, OP_PutIntKey, newIdx, 0); + sqliteVdbeAddOp(v, OP_Close, base, 0); + + sqliteVdbeAddOp(v, OP_Rewind, oldIdx, 0); + sqliteVdbeAddOp(v, OP_Rewind, newIdx, 0); + + if (sqliteCodeRowTrigger(pParse, TK_UPDATE, pChanges, TK_BEFORE, pTab, + newIdx, oldIdx, onError)) goto update_cleanup; + } + /* Rewind the list of records that need to be updated and ** open every index that needs updating. Note that if any ** index could potentially invoke a REPLACE conflict resolution ** action, then we need to open all indices because we might need ** to be deleting some records. */ - sqliteVdbeAddOp(v, OP_ListRewind, 0, 0); openOp = pTab->isTemp ? OP_OpenWrAux : OP_OpenWrite; sqliteVdbeAddOp(v, openOp, base, pTab->tnum); if( onError==OE_Replace ){ @@ -197,8 +276,12 @@ void sqliteUpdate( ** Also, the old data is needed to delete the old index entires. ** So make the cursor point at the old record. */ - addr = sqliteVdbeAddOp(v, OP_ListRead, 0, 0); - sqliteVdbeAddOp(v, OP_Dup, 0, 0); + if (!row_triggers_exist) { + int ii; + sqliteVdbeAddOp(v, OP_ListRewind, 0, 0); + addr = sqliteVdbeAddOp(v, OP_ListRead, 0, 0); + sqliteVdbeAddOp(v, OP_Dup, 0, 0); + } sqliteVdbeAddOp(v, OP_MoveTo, base, 0); /* If the record number will change, push the record number as it @@ -241,7 +324,7 @@ void sqliteUpdate( /* If changing the record number, delete the old record. */ if( chngRecno ){ - sqliteVdbeAddOp(v, OP_Delete, 0, 0); + sqliteVdbeAddOp(v, OP_Delete, base, 0); } /* Create the new index entries and the new record. @@ -250,22 +333,49 @@ void sqliteUpdate( /* Increment the row counter */ - if( db->flags & SQLITE_CountRows ){ + if( db->flags & SQLITE_CountRows && !pParse->trigStack){ sqliteVdbeAddOp(v, OP_AddImm, 1, 0); } + if (row_triggers_exist) { + for(i=0, pIdx=pTab->pIndex; pIdx; pIdx=pIdx->pNext, i++){ + if( openAll || aIdxUsed[i] ) + sqliteVdbeAddOp(v, OP_Close, base+i+1, 0); + } + sqliteVdbeAddOp(v, OP_Close, base, 0); + pParse->nTab = base; + + if (sqliteCodeRowTrigger(pParse, TK_UPDATE, pChanges, TK_AFTER, pTab, + newIdx, oldIdx, onError)) goto update_cleanup; + } + /* Repeat the above with the next record to be updated, until ** all record selected by the WHERE clause have been updated. */ sqliteVdbeAddOp(v, OP_Goto, 0, addr); sqliteVdbeChangeP2(v, addr, sqliteVdbeCurrentAddr(v)); sqliteVdbeAddOp(v, OP_ListReset, 0, 0); + + /* Close all tables if there were no FOR EACH ROW triggers */ + if (!row_triggers_exist) { + for(i=0, pIdx=pTab->pIndex; pIdx; pIdx=pIdx->pNext, i++){ + if( openAll || aIdxUsed[i] ){ + sqliteVdbeAddOp(v, OP_Close, base+i+1, 0); + } + } + sqliteVdbeAddOp(v, OP_Close, base, 0); + pParse->nTab = base; + } else { + sqliteVdbeAddOp(v, OP_Close, newIdx, 0); + sqliteVdbeAddOp(v, OP_Close, oldIdx, 0); + } + sqliteEndWriteOperation(pParse); /* ** Return the number of rows that were changed. */ - if( db->flags & SQLITE_CountRows ){ + if( db->flags & SQLITE_CountRows && !pParse->trigStack ){ sqliteVdbeAddOp(v, OP_ColumnCount, 1, 0); sqliteVdbeAddOp(v, OP_ColumnName, 0, 0); sqliteVdbeChangeP3(v, -1, "rows updated", P3_STATIC); diff --git a/src/vdbe.c b/src/vdbe.c index 6d577f844a..a7a12af12c 100644 --- a/src/vdbe.c +++ b/src/vdbe.c @@ -30,7 +30,7 @@ ** But other routines are also provided to help in building up ** a program instruction by instruction. ** -** $Id: vdbe.c,v 1.141 2002/05/10 13:14:07 drh Exp $ +** $Id: vdbe.c,v 1.142 2002/05/15 08:30:14 danielk1977 Exp $ */ #include "sqliteInt.h" #include @@ -249,6 +249,9 @@ struct Vdbe { int nCallback; /* Number of callbacks invoked so far */ int iLimit; /* Limit on the number of callbacks remaining */ int iOffset; /* Offset before beginning to do callbacks */ + + int keylistStackDepth; + Keylist ** keylistStack; }; /* @@ -999,6 +1002,15 @@ static void Cleanup(Vdbe *p){ sqliteFree(p->aSet); p->aSet = 0; p->nSet = 0; + if (p->keylistStackDepth > 0) { + int ii; + for (ii = 0; ii < p->keylistStackDepth; ii++) { + KeylistFree(p->keylistStack[ii]); + } + sqliteFree(p->keylistStack); + p->keylistStackDepth = 0; + p->keylistStack = 0; + } } /* @@ -1062,7 +1074,7 @@ static char *zOpName[] = { 0, "Le", "Gt", "Ge", "IsNull", "NotNull", "Negative", "And", "Or", "Not", "Concat", "Noop", "Function", - "Limit", + "Limit", "PushList", "PopList", }; /* @@ -4539,6 +4551,39 @@ case OP_SetFound: { break; } +/* Opcode: PushList * * * +** +** Save the current Vdbe list such that it can be restored by a PopList +** opcode. The list is empty after this is executed. +*/ +case OP_PushList: { + p->keylistStackDepth++; + assert(p->keylistStackDepth > 0); + p->keylistStack = sqliteRealloc(p->keylistStack, + sizeof(Keylist *) * p->keylistStackDepth); + p->keylistStack[p->keylistStackDepth - 1] = p->pList; + p->pList = 0; + break; +} + +/* Opcode: PopList * * * +** +** Restore the Vdbe list to the state it was in when PushList was last +** executed. +*/ +case OP_PopList: { + assert(p->keylistStackDepth > 0); + p->keylistStackDepth--; + KeylistFree(p->pList); + p->pList = p->keylistStack[p->keylistStackDepth]; + p->keylistStack[p->keylistStackDepth] = 0; + if (p->keylistStackDepth == 0) { + sqliteFree(p->keylistStack); + p->keylistStack = 0; + } + break; +} + /* Opcode: SetNotFound P1 P2 * ** ** Pop the stack once and compare the value popped off with the diff --git a/src/vdbe.h b/src/vdbe.h index f75b67551f..1260ce755e 100644 --- a/src/vdbe.h +++ b/src/vdbe.h @@ -15,7 +15,7 @@ ** or VDBE. The VDBE implements an abstract machine that runs a ** simple program to access and modify the underlying database. ** -** $Id: vdbe.h,v 1.50 2002/04/20 14:24:43 drh Exp $ +** $Id: vdbe.h,v 1.51 2002/05/15 08:30:14 danielk1977 Exp $ */ #ifndef _SQLITE_VDBE_H_ #define _SQLITE_VDBE_H_ @@ -198,7 +198,10 @@ typedef struct VdbeOp VdbeOp; #define OP_Limit 113 -#define OP_MAX 113 +#define OP_PushList 114 +#define OP_PopList 115 + +#define OP_MAX 115 /* ** Prototypes for the VDBE interface. See comments on the implementation diff --git a/src/where.c b/src/where.c index 42b6bac4d0..1bf6f25ee1 100644 --- a/src/where.c +++ b/src/where.c @@ -13,7 +13,7 @@ ** the WHERE clause of SQL statements. Also found here are subroutines ** to generate VDBE code to evaluate expressions. ** -** $Id: where.c,v 1.41 2002/04/30 19:20:29 drh Exp $ +** $Id: where.c,v 1.42 2002/05/15 08:30:14 danielk1977 Exp $ */ #include "sqliteInt.h" @@ -216,6 +216,22 @@ WhereInfo *sqliteWhereBegin( */ for(i=0; itrigStack && pParse->trigStack->newIdx >= 0) { + aExpr[i].prereqRight = + aExpr[i].prereqRight & ~(1 << pParse->trigStack->newIdx - base); + aExpr[i].prereqLeft = + aExpr[i].prereqLeft & ~(1 << pParse->trigStack->newIdx - base); + aExpr[i].prereqAll = + aExpr[i].prereqAll & ~(1 << pParse->trigStack->newIdx - base); + } + if (pParse->trigStack && pParse->trigStack->oldIdx >= 0) { + aExpr[i].prereqRight = + aExpr[i].prereqRight & ~(1 << pParse->trigStack->oldIdx - base); + aExpr[i].prereqLeft = + aExpr[i].prereqLeft & ~(1 << pParse->trigStack->oldIdx - base); + aExpr[i].prereqAll = + aExpr[i].prereqAll & ~(1 << pParse->trigStack->oldIdx - base); + } } /* Figure out a good nesting order for the tables. aOrder[0] will diff --git a/test/trigger1.test b/test/trigger1.test new file mode 100644 index 0000000000..45b4d6e2fd --- /dev/null +++ b/test/trigger1.test @@ -0,0 +1,113 @@ +# 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 tests creating and dropping triggers, and interaction thereof +# with the database COMMIT/ROLLBACK logic. +# +# 1. CREATE and DROP TRIGGER tests +# trig-1.1: Error if table does not exist +# trig-1.2: Error if trigger already exists +# trig-1.3: Created triggers are deleted if the transaction is rolled back +# trig-1.4: DROP TRIGGER removes trigger +# trig-1.5: Dropped triggers are restored if the transaction is rolled back +# trig-1.6: Error if dropped trigger doesn't exist +# trig-1.7: Dropping the table automatically drops all triggers +# trig-1.8: A trigger created on a TEMP table is not inserted into sqlite_master +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl + +do_test trig_cd-1.1 { + catchsql { + CREATE TRIGGER trig UPDATE ON no_such_table BEGIN + SELECT * from sqlite_master; + END; + } +} {1 {no such table: no_such_table}} + +execsql { + CREATE TABLE t1(a); +} +execsql { + CREATE TRIGGER tr1 INSERT ON t1 BEGIN + INSERT INTO t1 values(1); + END; +} +do_test trig_cd-1.2 { + catchsql { + CREATE TRIGGER tr1 DELETE ON t1 BEGIN + SELECT * FROM sqlite_master; + END + } +} {1 {trigger tr1 already exists}} + +do_test trig_cd-1.3 { + catchsql { + BEGIN; + CREATE TRIGGER tr2 INSERT ON t1 BEGIN + SELECT * from sqlite_master; END; + ROLLBACK; + CREATE TRIGGER tr2 INSERT ON t1 BEGIN + SELECT * from sqlite_master; END; + } +} {0 {}} + +do_test trig_cd-1.4 { + catchsql { + DROP TRIGGER tr1; + CREATE TRIGGER tr1 DELETE ON t1 BEGIN + SELECT * FROM sqlite_master; + END + } +} {0 {}} + +do_test trig_cd-1.5 { + execsql { + BEGIN; + DROP TRIGGER tr2; + ROLLBACK; + DROP TRIGGER tr2; + } +} {} + +do_test trig_cd-1.6 { + catchsql { + DROP TRIGGER biggles; + } +} {1 {no such trigger: biggles}} + +do_test trig_cd-1.7 { + catchsql { + DROP TABLE t1; + DROP TRIGGER tr1; + } +} {1 {no such trigger: tr1}} + +execsql { + CREATE TEMP TABLE temp_table(a); +} +do_test trig_cd-1.8 { + execsql { + CREATE TRIGGER temp_trig UPDATE ON temp_table BEGIN + SELECT * from sqlite_master; + END; + SELECT count(*) FROM sqlite_master WHERE name = 'temp_trig'; + } +} {0} + +catchsql { + DROP TABLE temp_table; +} +catchsql { + DROP TABLE t1; +} + +finish_test + diff --git a/test/trigger2.test b/test/trigger2.test new file mode 100644 index 0000000000..474d70cd3b --- /dev/null +++ b/test/trigger2.test @@ -0,0 +1,597 @@ +# 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. +# +#*********************************************************************** +# +# Regression testing of FOR EACH ROW table triggers +# +# 1. Trigger execution order tests. +# These tests ensure that BEFORE and AFTER triggers are fired at the correct +# times relative to each other and the triggering statement. +# +# trig-1.1.*: ON UPDATE trigger execution model. +# trig-1.2.*: DELETE trigger execution model. +# trig-1.3.*: INSERT trigger execution model. +# +# 2. Trigger program execution tests. +# These tests ensure that trigger programs execute correctly (ie. that a +# trigger program can correctly execute INSERT, UPDATE, DELETE * SELECT +# statements, and combinations thereof). +# +# 3. Selective trigger execution +# This tests that conditional triggers (ie. UPDATE OF triggers and triggers +# with WHEN clauses) are fired only fired when they are supposed to be. +# +# trig-3.1: UPDATE OF triggers +# trig-3.2: WHEN clause +# +# 4. Cascaded trigger execution +# Tests that trigger-programs may cause other triggers to fire. Also that a +# trigger-program is never executed recursively. +# +# trig-4.1: Trivial cascading trigger +# trig-4.2: Trivial recursive trigger handling +# +# 5. Count changes behaviour. +# Verify that rows altered by triggers are not included in the return value +# of the "count changes" interface. +# +# 6. ON CONFLICT clause handling +# trig-6.1[a-f]: INSERT statements +# trig-6.2[a-f]: UPDATE statements +# +# 7. Triggers on views fire correctly. +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl + +# 1. +set ii 0 +foreach tbl_defn [ list \ + {CREATE TABLE tbl (a, b);} \ + {CREATE TABLE tbl (a INTEGER PRIMARY KEY, b);} \ + {CREATE TABLE tbl (a, b PRIMARY KEY);} \ + {CREATE TABLE tbl (a, b); CREATE INDEX tbl_idx ON tbl(b);} ] { + incr ii + catchsql { DROP INDEX tbl_idx; } + catchsql { + DROP TABLE rlog; + DROP TABLE clog; + DROP TABLE tbl; + DROP TABLE other_tbl; + } + + execsql $tbl_defn + + execsql { + INSERT INTO tbl VALUES(1, 2); + INSERT INTO tbl VALUES(3, 4); + + CREATE TABLE rlog (idx, old_a, old_b, db_sum_a, db_sum_b, new_a, new_b); + CREATE TABLE clog (idx, old_a, old_b, db_sum_a, db_sum_b, new_a, new_b); + + CREATE TRIGGER before_update_row BEFORE UPDATE ON tbl FOR EACH ROW + BEGIN + INSERT INTO rlog VALUES ( (SELECT max(idx) + 1 FROM rlog), + old.a, old.b, + (SELECT sum(a) FROM tbl), (SELECT sum(b) FROM tbl), + new.a, new.b); + END; + + CREATE TRIGGER after_update_row AFTER UPDATE ON tbl FOR EACH ROW + BEGIN + INSERT INTO rlog VALUES ( (SELECT max(idx) + 1 FROM rlog), + old.a, old.b, + (SELECT sum(a) FROM tbl), (SELECT sum(b) FROM tbl), + new.a, new.b); + END; + + CREATE TRIGGER conditional_update_row AFTER UPDATE ON tbl FOR EACH ROW + WHEN old.a = 1 + BEGIN + INSERT INTO clog VALUES ( (SELECT max(idx) + 1 FROM clog), + old.a, old.b, + (SELECT sum(a) FROM tbl), (SELECT sum(b) FROM tbl), + new.a, new.b); + END; + } + + do_test trig-1.1.$ii { + execsql { + UPDATE tbl SET a = a * 10, b = b * 10; + SELECT * FROM rlog ORDER BY idx; + SELECT * FROM clog ORDER BY idx; + } + } [list 1 1 2 4 6 10 20 \ + 2 1 2 13 24 10 20 \ + 3 3 4 13 24 30 40 \ + 4 3 4 40 60 30 40 \ + 1 1 2 13 24 10 20 ] + + execsql { + DELETE FROM rlog; + DELETE FROM tbl; + INSERT INTO tbl VALUES (100, 100); + INSERT INTO tbl VALUES (300, 200); + CREATE TRIGGER delete_before_row BEFORE DELETE ON tbl FOR EACH ROW + BEGIN + INSERT INTO rlog VALUES ( (SELECT max(idx) + 1 FROM rlog), + old.a, old.b, + (SELECT sum(a) FROM tbl), (SELECT sum(b) FROM tbl), + 0, 0); + END; + + CREATE TRIGGER delete_after_row AFTER DELETE ON tbl FOR EACH ROW + BEGIN + INSERT INTO rlog VALUES ( (SELECT max(idx) + 1 FROM rlog), + old.a, old.b, + (SELECT sum(a) FROM tbl), (SELECT sum(b) FROM tbl), + 0, 0); + END; + } + do_test trig-1.2.$ii { + execsql { + DELETE FROM tbl; + SELECT * FROM rlog; + } + } [list 1 100 100 400 300 0 0 \ + 2 100 100 300 200 0 0 \ + 3 300 200 300 200 0 0 \ + 4 300 200 0 0 0 0 ] + + execsql { + DELETE FROM rlog; + CREATE TRIGGER insert_before_row BEFORE INSERT ON tbl FOR EACH ROW + BEGIN + INSERT INTO rlog VALUES ( (SELECT max(idx) + 1 FROM rlog), + 0, 0, + (SELECT sum(a) FROM tbl), (SELECT sum(b) FROM tbl), + new.a, new.b); + END; + + CREATE TRIGGER insert_after_row AFTER INSERT ON tbl FOR EACH ROW + BEGIN + INSERT INTO rlog VALUES ( (SELECT max(idx) + 1 FROM rlog), + 0, 0, + (SELECT sum(a) FROM tbl), (SELECT sum(b) FROM tbl), + new.a, new.b); + END; + } + do_test trig-1.3.$ii { + execsql { + + CREATE TABLE other_tbl(a, b); + INSERT INTO other_tbl VALUES(1, 2); + INSERT INTO other_tbl VALUES(3, 4); + -- INSERT INTO tbl SELECT * FROM other_tbl; + INSERT INTO tbl VALUES(5, 6); + DROP TABLE other_tbl; + + SELECT * FROM rlog; + } + } [list 1 0 0 0 0 5 6 \ + 2 0 0 5 6 5 6 ] +} +catchsql { + DROP TABLE rlog; + DROP TABLE clog; + DROP TABLE tbl; + DROP TABLE other_tbl; +} + +# 2. +set ii 0 +foreach tr_program [ list \ + {UPDATE tbl SET b = old.b;} \ + {INSERT INTO log VALUES(new.c, 2, 3);} \ + {DELETE FROM log WHERE a = 1;} \ + {INSERT INTO tbl VALUES(500, new.b * 10, 700); + UPDATE tbl SET c = old.c; + DELETE FROM log;} \ + {INSERT INTO log select * from tbl;} + ] \ +{ + foreach test_varset [ list \ + { + set statement {UPDATE tbl SET c = 10 WHERE a = 1;} + set prep {INSERT INTO tbl VALUES(1, 2, 3);} + set newC 10 + set newB 2 + set newA 1 + set oldA 1 + set oldB 2 + set oldC 3 + } \ + { + set statement {DELETE FROM tbl WHERE a = 1;} + set prep {INSERT INTO tbl VALUES(1, 2, 3);} + set oldA 1 + set oldB 2 + set oldC 3 + } \ + { + set statement {INSERT INTO tbl VALUES(1, 2, 3);} + set newA 1 + set newB 2 + set newC 3 + } + ] \ + { + set statement {} + set prep {} + set newA {''} + set newB {''} + set newC {''} + set oldA {''} + set oldB {''} + set oldC {''} + + incr ii + + eval $test_varset + + set statement_type [string range $statement 0 5] + set tr_program_fixed $tr_program + if {$statement_type == "DELETE"} { + regsub -all new\.a $tr_program_fixed {''} tr_program_fixed + regsub -all new\.b $tr_program_fixed {''} tr_program_fixed + regsub -all new\.c $tr_program_fixed {''} tr_program_fixed + } + if {$statement_type == "INSERT"} { + regsub -all old\.a $tr_program_fixed {''} tr_program_fixed + regsub -all old\.b $tr_program_fixed {''} tr_program_fixed + regsub -all old\.c $tr_program_fixed {''} tr_program_fixed + } + + + set tr_program_cooked $tr_program + regsub -all new\.a $tr_program_cooked $newA tr_program_cooked + regsub -all new\.b $tr_program_cooked $newB tr_program_cooked + regsub -all new\.c $tr_program_cooked $newC tr_program_cooked + regsub -all old\.a $tr_program_cooked $oldA tr_program_cooked + regsub -all old\.b $tr_program_cooked $oldB tr_program_cooked + regsub -all old\.c $tr_program_cooked $oldC tr_program_cooked + + catchsql { + DROP TABLE tbl; + DROP TABLE log; + } + execsql { + CREATE TABLE tbl(a PRIMARY KEY, b, c); + CREATE TABLE log(a, b, c); + } + + set query {SELECT * FROM tbl; SELECT * FROM log;} + set prep "$prep; INSERT INTO log VALUES(1, 2, 3); INSERT INTO log VALUES(10, 20, 30);" + +# Check execution of BEFORE programs: + + set before_data [ execsql "$prep $tr_program_cooked $statement $query" ] + + execsql "DELETE FROM tbl; DELETE FROM log; $prep"; + execsql "CREATE TRIGGER the_trigger BEFORE [string range $statement 0 6] ON tbl BEGIN $tr_program_fixed END;" + + do_test trig-2-$ii-before "execsql {$statement $query}" $before_data + + execsql "DROP TRIGGER the_trigger;" + execsql "DELETE FROM tbl; DELETE FROM log;" + +# Check execution of AFTER programs + set after_data [ execsql "$prep $statement $tr_program_cooked $query" ] + + execsql "DELETE FROM tbl; DELETE FROM log; $prep"; + + execsql "CREATE TRIGGER the_trigger AFTER [string range $statement 0 6] ON tbl BEGIN $tr_program_fixed END;" + + do_test trig-2-$ii-after "execsql {$statement $query}" $after_data + execsql "DROP TRIGGER the_trigger;" + } +} +catchsql { + DROP TABLE tbl; + DROP TABLE log; +} + +# 3. + +# trig-3.1: UPDATE OF triggers +execsql { + CREATE TABLE tbl (a, b, c, d); + CREATE TABLE log (a); + INSERT INTO log VALUES (0); + INSERT INTO tbl VALUES (0, 0, 0, 0); + INSERT INTO tbl VALUES (1, 0, 0, 0); + CREATE TRIGGER tbl_after_update_cd BEFORE UPDATE OF c, d ON tbl + BEGIN + UPDATE log SET a = a + 1; + END; +} +do_test trig-3.1 { + execsql { + UPDATE tbl SET b = 1, c = 10; -- 2 + UPDATE tbl SET b = 10; -- 0 + UPDATE tbl SET d = 4 WHERE a = 0; --1 + UPDATE tbl SET a = 4, b = 10; --0 + SELECT * FROM log; + } +} {3} +execsql { + DROP TABLE tbl; + DROP TABLE log; +} + +# trig-3.2: WHEN clause +set when_triggers [ list \ + {t1 BEFORE INSERT ON tbl WHEN new.a > 20} \ + {t2 BEFORE INSERT ON tbl WHEN (SELECT count(*) FROM tbl) = 0} ] + +execsql { + CREATE TABLE tbl (a, b, c, d); + CREATE TABLE log (a); + INSERT INTO log VALUES (0); +} + +foreach trig $when_triggers { + execsql "CREATE TRIGGER $trig BEGIN UPDATE log set a = a + 1; END;" +} + +do_test trig-3.2 { + execsql { + + INSERT INTO tbl VALUES(0, 0, 0, 0); -- 1 + SELECT * FROM log; + UPDATE log SET a = 0; + + INSERT INTO tbl VALUES(0, 0, 0, 0); -- 0 + SELECT * FROM log; + UPDATE log SET a = 0; + + INSERT INTO tbl VALUES(200, 0, 0, 0); -- 1 + SELECT * FROM log; + UPDATE log SET a = 0; + } +} {1 0 1} +execsql { + DROP TABLE tbl; + DROP TABLE log; +} + +# Simple cascaded trigger +execsql { + CREATE TABLE tblA(a, b); + CREATE TABLE tblB(a, b); + CREATE TABLE tblC(a, b); + + CREATE TRIGGER tr1 BEFORE INSERT ON tblA BEGIN + INSERT INTO tblB values(new.a, new.b); + END; + + CREATE TRIGGER tr2 BEFORE INSERT ON tblB BEGIN + INSERT INTO tblC values(new.a, new.b); + END; +} +do_test trig-4.1 { + execsql { + INSERT INTO tblA values(1, 2); + SELECT * FROM tblA; + SELECT * FROM tblB; + SELECT * FROM tblC; + } +} {1 2 1 2 1 2} +execsql { + DROP TABLE tblA; + DROP TABLE tblB; + DROP TABLE tblC; +} + +# Simple recursive trigger +execsql { + CREATE TABLE tbl(a, b, c); + CREATE TRIGGER tbl_trig BEFORE INSERT ON tbl + BEGIN + INSERT INTO tbl VALUES (new.a, new.b, new.c); + END; +} +do_test trig-4.2 { + execsql { + INSERT INTO tbl VALUES (1, 2, 3); + select * from tbl; + } +} {1 2 3 1 2 3} +execsql { + DROP TABLE tbl; +} + +# 5. +execsql { + CREATE TABLE tbl(a, b, c); + CREATE TRIGGER tbl_trig BEFORE INSERT ON tbl + BEGIN + INSERT INTO tbl VALUES (1, 2, 3); + INSERT INTO tbl VALUES (2, 2, 3); + UPDATE tbl set b = 10 WHERE a = 1; + DELETE FROM tbl WHERE a = 1; + DELETE FROM tbl; + END; +} +do_test trig-5 { + execsql { + INSERT INTO tbl VALUES(100, 200, 300); + } + db changes +} {1} +execsql { + DROP TABLE tbl; +} + + +# Handling of ON CONFLICT by INSERT statements inside triggers +execsql { + CREATE TABLE tbl (a primary key, b, c); + CREATE TRIGGER ai_tbl AFTER INSERT ON tbl BEGIN + INSERT OR IGNORE INTO tbl values (new.a, 0, 0); + END; +} +do_test trig-6.1a { + execsql { + BEGIN; + INSERT INTO tbl values (1, 2, 3); + SELECT * from tbl; + } +} {1 2 3} +do_test trig-6.1b { + catchsql { + INSERT OR ABORT INTO tbl values (2, 2, 3); + } +} {1 {constraint failed}} +do_test trig-6.1c { + execsql { + SELECT * from tbl; + } +} {1 2 3} +do_test trig-6.1d { + catchsql { + INSERT OR FAIL INTO tbl values (2, 2, 3); + } +} {1 {constraint failed}} +do_test trig-6.1e { + execsql { + SELECT * from tbl; + } +} {1 2 3 2 2 3} +do_test trig-6.1f { + execsql { + INSERT OR REPLACE INTO tbl values (2, 2, 3); + SELECT * from tbl; + } +} {1 2 3 2 0 0} +do_test trig-6.1g { + catchsql { + INSERT OR ROLLBACK INTO tbl values (3, 2, 3); + } +} {1 {constraint failed}} +do_test trig-6.1h { + execsql { + SELECT * from tbl; + } +} {} + + +# Handling of ON CONFLICT by UPDATE statements inside triggers +execsql { + INSERT INTO tbl values (4, 2, 3); + INSERT INTO tbl values (6, 3, 4); + CREATE TRIGGER au_tbl AFTER UPDATE ON tbl BEGIN + UPDATE OR IGNORE tbl SET a = new.a, c = 10; + END; +} +do_test trig-6.2a { + execsql { + BEGIN; + UPDATE tbl SET a = 1 WHERE a = 4; + SELECT * from tbl; + } +} {1 2 10 6 3 4} +do_test trig-6.2b { + catchsql { + UPDATE OR ABORT tbl SET a = 4 WHERE a = 1; + } +} {1 {constraint failed}} +do_test trig-6.2c { + execsql { + SELECT * from tbl; + } +} {1 2 10 6 3 4} +do_test trig-6.2d { + catchsql { + UPDATE OR FAIL tbl SET a = 4 WHERE a = 1; + } +} {1 {constraint failed}} +do_test trig-6.2e { + execsql { + SELECT * from tbl; + } +} {4 2 10 6 3 4} +do_test trig-6.2f { + execsql { + UPDATE OR REPLACE tbl SET a = 1 WHERE a = 4; + SELECT * from tbl; + } +} {1 3 10} +execsql { + INSERT INTO tbl VALUES (2, 3, 4); +} +do_test trig-6.2g { + catchsql { + UPDATE OR ROLLBACK tbl SET a = 4 WHERE a = 1; + } +} {1 {constraint failed}} +do_test trig-6.2h { + execsql { + SELECT * from tbl; + } +} {4 2 3 6 3 4} +execsql { + DROP TABLE tbl; +} + +# 7. Triggers on views +execsql { + CREATE TABLE ab(a, b); + CREATE TABLE cd(c, d); + INSERT INTO ab VALUES (1, 2); + INSERT INTO ab VALUES (0, 0); + INSERT INTO cd VALUES (3, 4); + + CREATE TABLE tlog(ii INTEGER PRIMARY KEY, + olda, oldb, oldc, oldd, newa, newb, newc, newd); + + CREATE VIEW abcd AS SELECT a, b, c, d FROM ab, cd; + + CREATE TRIGGER before_update BEFORE UPDATE ON abcd BEGIN + INSERT INTO tlog VALUES(NULL, + old.a, old.b, old.c, old.d, new.a, new.b, new.c, new.d); + END; + CREATE TRIGGER after_update AFTER UPDATE ON abcd BEGIN + INSERT INTO tlog VALUES(NULL, + old.a, old.b, old.c, old.d, new.a, new.b, new.c, new.d); + END; + + CREATE TRIGGER before_delete BEFORE DELETE ON abcd BEGIN + INSERT INTO tlog VALUES(NULL, + old.a, old.b, old.c, old.d, 0, 0, 0, 0); + END; + CREATE TRIGGER after_delete AFTER DELETE ON abcd BEGIN + INSERT INTO tlog VALUES(NULL, + old.a, old.b, old.c, old.d, 0, 0, 0, 0); + END; + + CREATE TRIGGER before_insert BEFORE INSERT ON abcd BEGIN + INSERT INTO tlog VALUES(NULL, + 0, 0, 0, 0, new.a, new.b, new.c, new.d); + END; + CREATE TRIGGER after_insert AFTER INSERT ON abcd BEGIN + INSERT INTO tlog VALUES(NULL, + 0, 0, 0, 0, new.a, new.b, new.c, new.d); + END; +} + +do_test trig-7 { + execsql { + UPDATE abcd SET a = 100, b = 5*5 WHERE a = 1; + DELETE FROM abcd WHERE a = 1; + INSERT INTO abcd VALUES(10, 20, 30, 40); + SELECT * FROM tlog; + } +} [ list 1 1 2 3 4 100 25 3 4 \ + 2 1 2 3 4 100 25 3 4 \ + 3 1 2 3 4 0 0 0 0 4 1 2 3 4 0 0 0 0 \ + 5 0 0 0 0 10 20 30 40 6 0 0 0 0 10 20 30 40 ] + +finish_test + diff --git a/www/lang.tcl b/www/lang.tcl index 59f8052c05..83b4a56dcf 100644 --- a/www/lang.tcl +++ b/www/lang.tcl @@ -1,7 +1,7 @@ # # Run this Tcl script to generate the sqlite.html file. # -set rcsid {$Id: lang.tcl,v 1.33 2002/05/06 11:47:33 drh Exp $} +set rcsid {$Id: lang.tcl,v 1.34 2002/05/15 08:30:15 danielk1977 Exp $} puts { @@ -54,6 +54,8 @@ foreach {section} [lsort -index 0 -dictionary { {{ON CONFLICT clause} conflict} {{CREATE VIEW} createview} {{DROP VIEW} dropview} + {{CREATE TRIGGER} createtrigger} + {{DROP TRIGGER} droptrigger} }] { puts "
  • [lindex $section 0]
  • " } @@ -1089,6 +1091,129 @@ the database backend and VACUUM has become a no-op.

    } +Section {CREATE TRIGGER} createtrigger + +Syntax {sql-statement} { +CREATE TRIGGER [ BEFORE | AFTER ] + + +} + +Syntax {database-event} { +DELETE | +INSERT | +UPDATE | +UPDATE OF +ON +} + +Syntax {trigger-action} { +[ FOR EACH ROW ] [ WHEN ] +BEGIN + ; [ ; ]* +END +} + +Syntax {trigger-step} { + | | + | +} + +puts { +

    The CREATE TRIGGER statement is used to add triggers to the +database schema. Triggers are database operations (the trigger-action) +that are automatically performed when a specified database event (the +database-event) occurs.

    + +

    A trigger may be specified to fire whenever a DELETE, INSERT or UPDATE of a +particular database table occurs, or whenever an UPDATE of one or more +specified columns of a table are updated.

    + +

    At this time SQLite supports only FOR EACH ROW triggers, not FOR EACH +STATEMENT triggers. Hence explicitly specifying FOR EACH ROW is optional. FOR +EACH ROW implies that the SQL statements specified as trigger-steps +may be executed (depending on the WHEN clause) for each database row being +inserted, updated or deleted by the statement causing the trigger to fire.

    + +

    Both the WHEN clause and the trigger-steps may access elements of +the row being inserted, deleted or updated using references of the form +"NEW.column-name" and "OLD.column-name", where +column-name is the name of a column from the table that the trigger +is associated with. OLD and NEW references may only be used in triggers on +trigger-events for which they are relevant, as follows:

    + + + + + + + + + + + + + + +
    INSERTNEW references are valid
    UPDATENEW and OLD references are valid
    DELETEOLD references are valid
    +

    + +

    If a WHEN clause is supplied, the SQL statements specified as trigger-steps are only executed for rows for which the WHEN clause is true. If no WHEN clause is supplied, the SQL statements are executed for all rows.

    + +

    The specified trigger-time determines when the trigger-steps +will be executed relative to the insertion, modification or removal of the +associated row.

    + +

    An ON CONFLICT clause may be specified as part of an UPDATE or INSERT +trigger-step. However if an ON CONFLICT clause is specified as part of +the statement causing the trigger to fire, then this conflict handling +policy is used instead.

    + +

    Triggers are automatically dropped when the table that they are +associated with is dropped.

    + +

    Triggers may be created on views, as well as ordinary tables. If one or +more INSERT, DELETE or UPDATE triggers are defined on a view, then it is not +an error to execute an INSERT, DELETE or UPDATE statement on the view, +respectively. Thereafter, executing an INSERT, DELETE or UPDATE on the view +causes the associated triggers to fire. The real tables underlying the view +are not modified (except possibly explicitly, by a trigger program).

    + +

    Example:

    + +

    Assuming that customer records are stored in the "customers" table, and +that order records are stored in the "orders" table, the following trigger +ensures that all associated orders are redirected when a customer changes +his or her address:

    +} +Example { +CREATE TRIGGER update_customer_address UPDATE OF address ON customers + BEGIN + UPDATE orders SET address = new.address WHERE customer_name = old.name; + END; +} +puts { +

    With this trigger installed, executing the statement:

    +} +Example { +UPDATE customers SET address = '1 Main St.' WHERE name = 'Jack Jones'; +} +puts { +

    causes the following to be automatically executed:

    +} +Example { +UPDATE orders SET address = '1 Main St.' WHERE customer_name = 'Jack Jones'; +} + +Section {DROP TRIGGER} droptrigger +Syntax {sql-statement} { +DROP TRIGGER +} +puts { +

    Used to drop a trigger from the database schema. Note that triggers + are automatically dropped when the associated table is dropped.

    +} + puts {