]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Added FOR EACH ROW triggers functionality (CVS 562)
authordanielk1977 <danielk1977@noemail.net>
Wed, 15 May 2002 08:30:12 +0000 (08:30 +0000)
committerdanielk1977 <danielk1977@noemail.net>
Wed, 15 May 2002 08:30:12 +0000 (08:30 +0000)
FossilOrigin-Name: 794bf67b6b36fce8854d5daff12f21dbb943240c

18 files changed:
manifest
manifest.uuid
src/build.c
src/delete.c
src/expr.c
src/insert.c
src/main.c
src/parse.y
src/sqliteInt.h
src/tokenize.c
src/trigger.c [new file with mode: 0644]
src/update.c
src/vdbe.c
src/vdbe.h
src/where.c
test/trigger1.test [new file with mode: 0644]
test/trigger2.test [new file with mode: 0644]
www/lang.tcl

index ac49a383887a446af1503316e17522dd80f41676..b4995384864a34bae7be20779db0350ee6af4e8b 100644 (file)
--- 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
index 7110bdab46ef199cd0cbe343f0a986d7e09947cf..21f977034f913a87c16944437aec2156809cd732 100644 (file)
@@ -1 +1 @@
-06cdaf1c80f7bc25fc555c7c8a35258faed2d2e9
\ No newline at end of file
+794bf67b6b36fce8854d5daff12f21dbb943240c
\ No newline at end of file
index a32f21e1602dd94ac32159e464c688760956bae1..aff5d51bf44848e9a662df39ae3581059c7b5c40 100644 (file)
@@ -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 <ctype.h>
@@ -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;
index 3f7e0f7edf6f4ad60ca14e1cbb5d451acc1aed7e..2d04100ebffb8b9f29d6efeb0b23b59d59ab9a07 100644 (file)
@@ -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);
 
index 9184758b66f8bb33ef7337e88f6f284db689396d..b41e0ad8c2a2bdc3dea84a43fc11587f688d7f7b 100644 (file)
@@ -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; j<tt->pTab->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;
index a848ee12bb456927ee4a6af1b2b1a34d8ae07f71..0174071304becc6e00a97dbdc71cf4392e845257 100644 (file)
@@ -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; i<pTab->nCol; i++){
+      if( pColumn==0 ){
+       j = i;
+      }else{
+       for(j=0; j<pColumn->nId; 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);
+  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);
+       sqliteVdbeAddOp(v, OP_NewRecno, base, 0);
+      }
+      sqliteVdbeAddOp(v, OP_MustBeInt, 0, 0);
     }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);
       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; i<pTab->nCol; 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; j<pColumn->nId; 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; i<pTab->nCol; 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; j<pColumn->nId; 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);
   }
index 56865f13c821a89372d9a30a6ae9cd53bab34dd1..1a85ae3c50053d4e5721959207a07295fcf178cc 100644 (file)
@@ -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;
       }
index 744a0b91468a3cbad2bc0dd48cc5526eb798653b..b7907467717bd7599aa7664410b8665ae8f20af4 100644 (file)
@@ -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}
 ** 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);
+}
+
index f8251bbd149edea1827870df6d2081de302f1495..7354050a1438d012abde1f82816dd1ba593200a9 100644 (file)
@@ -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,7 +558,63 @@ 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 <column-list> 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 *);
index 84eaab7f7d7641e190a760c1185518a91ae8da25..00f4e07418fdd172c46df3aaa7dad52ad08ccce2 100644 (file)
@@ -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 (file)
index 0000000..8ad7027
--- /dev/null
@@ -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; ii<pChanges->nExpr; 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; jj<pTab->nCol; 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;
+}
+
+
index 8bf5e7c371060a032b1da04e822c1310475e9e57..f729f950fd276939c60e603e0708dcfbf22bda54 100644 (file)
@@ -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; i<pTab->nCol; 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);
index 6d577f844aecad114c07eefd586ae40c2b56d30f..a7a12af12cba23bc4d69e8c55fb6198bd92f241b 100644 (file)
@@ -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 <ctype.h>
@@ -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
index f75b67551f2cc444ef442342cf5f3485d4f78bac..1260ce755e505f656163998b3436080b1c335806 100644 (file)
@@ -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
index 42b6bac4d03b7a237bcc43a9fc09358e7cb6ea3d..1bf6f25ee158c2729a8ced774b607a08bc799147 100644 (file)
@@ -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; i<nExpr; i++){
     exprAnalyze(base, &aExpr[i]);
+    if (pParse->trigStack && 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 (file)
index 0000000..45b4d6e
--- /dev/null
@@ -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 (file)
index 0000000..474d70c
--- /dev/null
@@ -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
+
index 59f8052c05e65e8325609673559c07a8b2383b59..83b4a56dcf93f8900b0c4c312a117aa54264d5a6 100644 (file)
@@ -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 {<html>
 <head>
@@ -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 "<li><a href=\"#[lindex $section 1]\">[lindex $section 0]</a></li>"
 }
@@ -1089,6 +1091,129 @@ the database backend and VACUUM has become a no-op.
 </p>
 }
 
+Section {CREATE TRIGGER} createtrigger
+
+Syntax {sql-statement} {
+CREATE TRIGGER <trigger-name> [ BEFORE | AFTER ]
+<database-event>
+<trigger-action>
+}
+
+Syntax {database-event} {
+DELETE | 
+INSERT | 
+UPDATE | 
+UPDATE OF <column-list>
+ON <table-name> 
+}
+
+Syntax {trigger-action} {
+[ FOR EACH ROW ] [ WHEN <expression> ] 
+BEGIN 
+  <trigger-step> ; [ <trigger-step> ; ]*
+END
+}
+
+Syntax {trigger-step} {
+<update-statement> | <insert-statement> | 
+<delete-statement> | <select-statement> 
+}
+
+puts {
+<p>The CREATE TRIGGER statement is used to add triggers to the 
+database schema. Triggers are database operations (the <i>trigger-action</i>) 
+that are automatically performed when a specified database event (the
+<i>database-event</i>) occurs.  </p>
+
+<p>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.</p>
+
+<p>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 <i>trigger-steps</i> 
+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.</p>
+
+<p>Both the WHEN clause and the <i>trigger-steps</i> may access elements of 
+the row being inserted, deleted or updated using references of the form 
+"NEW.<i>column-name</i>" and "OLD.<i>column-name</i>", where
+<i>column-name</i> 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
+<i>trigger-event</i>s for which they are relevant, as follows:</p>
+
+<table border=0 cellpadding=10>
+<tr>
+<td valign="top" align="right" width=120><i>INSERT</i></td>
+<td valign="top">NEW references are valid</td>
+</tr>
+<tr>
+<td valign="top" align="right" width=120><i>UPDATE</i></td>
+<td valign="top">NEW and OLD references are valid</td>
+</tr>
+<tr>
+<td valign="top" align="right" width=120><i>DELETE</i></td>
+<td valign="top">OLD references are valid</td>
+</tr>
+</table>
+</p>
+
+<p>If a WHEN clause is supplied, the SQL statements specified as <i>trigger-steps</i> 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.</p>
+
+<p>The specified <i>trigger-time</i> determines when the <i>trigger-steps</i>
+will be executed relative to the insertion, modification or removal of the
+associated row.</p>
+
+<p>An ON CONFLICT clause may be specified as part of an UPDATE or INSERT
+<i>trigger-step</i>. 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.</p>
+
+<p>Triggers are automatically dropped when the table that they are 
+associated with is dropped.</p>
+
+<p>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).</p>
+
+<p><b>Example:</b></p>
+
+<p>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:</p>
+}
+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 {
+<p>With this trigger installed, executing the statement:</p>
+}
+Example {
+UPDATE customers SET address = '1 Main St.' WHERE name = 'Jack Jones';
+}
+puts {
+<p>causes the following to be automatically executed:</p>
+}
+Example {
+UPDATE orders SET address = '1 Main St.' WHERE customer_name = 'Jack Jones';
+}
+
+Section {DROP TRIGGER} droptrigger
+Syntax {sql-statement} {
+DROP TRIGGER <trigger-name>
+}
+puts { 
+  <p>Used to drop a trigger from the database schema. Note that triggers
+  are automatically dropped when the associated table is dropped.</p>
+}
+
 
 puts {
 <p><hr /></p>