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