Index: ext/rtree/rtree.c ================================================================== --- ext/rtree/rtree.c +++ ext/rtree/rtree.c @@ -1051,13 +1051,13 @@ */ static int rtreeDestroy(sqlite3_vtab *pVtab){ Rtree *pRtree = (Rtree *)pVtab; int rc; char *zCreate = sqlite3_mprintf( - "DROP TABLE '%q'.'%q_node';" - "DROP TABLE '%q'.'%q_rowid';" - "DROP TABLE '%q'.'%q_parent';", + "DROP TABLE '%q'.'%q_node'," + "'%q'.'%q_rowid'," + "'%q'.'%q_parent';", pRtree->zDb, pRtree->zName, pRtree->zDb, pRtree->zName, pRtree->zDb, pRtree->zName ); if( !zCreate ){ Index: src/build.c ================================================================== --- src/build.c +++ src/build.c @@ -3258,88 +3258,133 @@ } } #endif /* -** Write code to erase the table with root-page iTable from database iDb. -** Also write code to modify the sqlite_schema table and internal schema -** if a root-page of another table is moved by the btree-layer whilst -** erasing iTable (this can happen with an auto-vacuum database). -*/ -static void destroyRootPage(Parse *pParse, int iTable, int iDb){ - Vdbe *v = sqlite3GetVdbe(pParse); - int r1 = sqlite3GetTempReg(pParse); - if( iTable<2 ) sqlite3ErrorMsg(pParse, "corrupt schema"); - sqlite3VdbeAddOp3(v, OP_Destroy, iTable, r1, iDb); - sqlite3MayAbort(pParse); -#ifndef SQLITE_OMIT_AUTOVACUUM - /* OP_Destroy stores an in integer r1. If this integer - ** is non-zero, then it is the root page number of a table moved to - ** location iTable. The following code modifies the sqlite_schema table to - ** reflect this. - ** - ** The "#NNN" in the SQL is a special constant that means whatever value - ** is in register NNN. See grammar rules associated with the TK_REGISTER - ** token for additional information. - */ - sqlite3NestedParse(pParse, - "UPDATE %Q." LEGACY_SCHEMA_TABLE - " SET rootpage=%d WHERE #%d AND rootpage=#%d", - pParse->db->aDb[iDb].zDbSName, iTable, r1, r1); -#endif - sqlite3ReleaseTempReg(pParse, r1); -} +** The RootStack object holds a list (really a Heap) of btree root pages +** and schema numbers that need to be deleted using OP_Destroy. +** +** OP_Destroy opcodes must be issued in order of decreasing root page +** numbers in order to avoid having auto-vacuum disrupt subsequent +** OP_Destroy opcodes. For that reason, all pending OP_Destroy calls +** are accumulated in an instance of this object. Then at the end of +** code generation, this object is used to generate the OP_Destroy opcodes +** in decreasing order. +** +** The data structure is a max-heap. Each rootpage/schema-number combo +** is stored as a 64-bit integer, with the schema-number in the upper 32 +** bits and the page number in the lower 32-bits. The root of the heap +** (RootStack.a[0]) is the largest entry in the heap. The children of +** heap entry i are i*2+1 and i*2+2. The heap always stays balanced by +** ensuring that a parent entry is larger than both children. +** +*/ +typedef struct RootStack RootStack; +struct RootStack { + u32 nAlloc; /* Slots allocated for a[] */ + u32 nUsed; /* Slots used for in a[] */ + u64 *a; /* Sorting heap. Each entry has iDb in the upper 32 bits and + ** a page number in the lower 32 bits */ +}; /* -** Write VDBE code to erase table pTab and all associated indices on disk. -** Code to update the sqlite_schema tables and internal schema definitions -** in case a root-page belonging to another table is moved by the btree layer -** is also added (this can happen with an auto-vacuum database). -*/ -static void destroyTable(Parse *pParse, Table *pTab){ - /* If the database may be auto-vacuum capable (if SQLITE_OMIT_AUTOVACUUM - ** is not defined), then it is important to call OP_Destroy on the - ** table and index root-pages in order, starting with the numerically - ** largest root-page number. This guarantees that none of the root-pages - ** to be destroyed is relocated by an earlier OP_Destroy. i.e. if the - ** following were coded: - ** - ** OP_Destroy 4 0 - ** ... - ** OP_Destroy 5 0 - ** - ** and root page 5 happened to be the largest root-page number in the - ** database, then root page 5 would be moved to page 4 by the - ** "OP_Destroy 4 0" opcode. The subsequent "OP_Destroy 5 0" would hit - ** a free-list page. - */ - Pgno iTab = pTab->tnum; - Pgno iDestroyed = 0; - - while( 1 ){ - Index *pIdx; - Pgno iLargest = 0; - - if( iDestroyed==0 || iTabpIndex; pIdx; pIdx=pIdx->pNext){ - Pgno iIdx = pIdx->tnum; - assert( pIdx->pSchema==pTab->pSchema ); - if( (iDestroyed==0 || (iIdxiLargest ){ - iLargest = iIdx; - } - } - if( iLargest==0 ){ +** Add a new Pgno and iDb to the RootStack in question. +** +** The new entry is inserted at the of the heap (most distant child) +** and then the heap is rebalanced. +*/ +static void rootStackPush(Parse *pParse, RootStack *p, Pgno pgno, int iDb){ + u64 iNew; + int i, j; + if( p->nAllocnUsed+1 ){ + p->nAlloc = p->nAlloc*2 + 12; + p->a = sqlite3DbRealloc(pParse->db, p->a, sizeof(Pgno)*p->nAlloc); + if( p->a==0 ){ + p->nAlloc = p->nUsed = 0; return; - }else{ - int iDb = sqlite3SchemaToIndex(pParse->db, pTab->pSchema); - assert( iDb>=0 && iDbdb->nDb ); - destroyRootPage(pParse, iLargest, iDb); - iDestroyed = iLargest; + } + } + assert( pgno>0 || CORRUPT_DB ); + assert( iDb>=0 && iDbdb->nDb ); + iNew = (((u64)iDb)<<32) | pgno; + i = p->nUsed++; + p->a[i] = iNew; + while( i>=1 && p->a[j = (i-1)/2]a[i] ){ + u64 tmp = p->a[i]; + p->a[i] = p->a[j]; + p->a[j] = tmp; + i = j; + } +} + +/* +** Remove the largest entry from the RootStack heap. Rebalance the +** heap and then return that largest entry. +*/ +static u64 rootStackPop(RootStack *p){ + u64 iMax; + int i, j; + assert( p->nUsed>0 ); + assert( p->a!=0 ); + iMax = p->a[0]; + p->a[0] = p->a[--p->nUsed]; + i = 0; + while( (j = i*2+1)nUsed ){ + u64 tmp; + if( j+1nUsed && p->a[j+1]>p->a[j] ) j++; + if( p->a[i]>p->a[j] ) break; + tmp = p->a[i]; + p->a[i] = p->a[j]; + p->a[j] = tmp; + i = j; + } + return iMax; +} + +/* +** Generate OP_Destroy opcodes for every btree named in the given +** RootStack object. Issue these OP_Destroy opcodes in order of decreasing +** root page number. Then clean up any memory used by the RootStack. +*/ +static void rootStackCode(Parse *pParse, RootStack *p){ + int iLastDb = -1; + int r1 = sqlite3GetTempReg(pParse); + int r2 = sqlite3GetTempReg(pParse); + int regReturn = sqlite3GetTempReg(pParse); + Vdbe *v = pParse->pVdbe; + int addrSub = 0; + while( pParse->nErr==0 && p->nUsed>0 ){ + u64 iNext = rootStackPop(p); + Pgno pgno = iNext & 0xffffffff; + int iDb = (iNext>>32)&0xffff; + if( pgno<2 ) sqlite3ErrorMsg(pParse, "corrupt schema"); + sqlite3MayAbort(pParse); + if( iDb!=iLastDb ){ + /* Code a subroutine to that will update the schema table when + ** a root page number changes. The old root page is in register r1. + ** Root page is moved to the value in register r2. */ + iLastDb = iDb; + sqlite3VdbeAddOp0(v, OP_Goto); + addrSub = sqlite3VdbeCurrentAddr(v); + sqlite3NestedParse(pParse, + "UPDATE %Q." LEGACY_SCHEMA_TABLE + " SET rootpage=#%d WHERE rootpage=#%d", + pParse->db->aDb[iDb].zDbSName, r2, r1); + sqlite3VdbeAddOp1(v, OP_Return, regReturn); + sqlite3VdbeJumpHere(v, addrSub-1); } + sqlite3VdbeAddOp3(v, OP_Destroy, pgno, r1, iDb); + sqlite3VdbeAddOp2(v, OP_IfNot, r1, sqlite3VdbeCurrentAddr(v)+3); + VdbeCoverage(v); + sqlite3VdbeAddOp2(v, OP_Integer, pgno, r2); + sqlite3VdbeAddOp2(v, OP_Gosub, regReturn, addrSub); } + if( pParse->nErr==0 ){ + sqlite3ReleaseTempReg(pParse, r1); + sqlite3ReleaseTempReg(pParse, regReturn); + } + sqlite3DbFree(pParse->db, p->a); } /* ** Remove entries from the sqlite_statN tables (for N in (1,2,3)) ** after a DROP INDEX or DROP TABLE command. @@ -3365,11 +3410,16 @@ } /* ** Generate code to drop a table. */ -void sqlite3CodeDropTable(Parse *pParse, Table *pTab, int iDb, int isView){ +static void sqlite3CodeDropTable( + Parse *pParse, /* Parsing context */ + RootStack *pStack, /* List of pending OP_Destroys */ + Table *pTab, /* Table to be dropped */ + int iDb /* Schema holding pTab */ +){ Vdbe *v; sqlite3 *db = pParse->db; Trigger *pTrigger; Db *pDb = &db->aDb[iDb]; @@ -3418,12 +3468,17 @@ */ sqlite3NestedParse(pParse, "DELETE FROM %Q." LEGACY_SCHEMA_TABLE " WHERE tbl_name=%Q and type!='trigger'", pDb->zDbSName, pTab->zName); - if( !isView && !IsVirtual(pTab) ){ - destroyTable(pParse, pTab); + if( IsOrdinaryTable(pTab) ){ + Index *pIdx; + rootStackPush(pParse, pStack, pTab->tnum, iDb); + for(pIdx=pTab->pIndex; pIdx; pIdx=pIdx->pNext){ + if( pIdx->tnum==pTab->tnum ) continue; + rootStackPush(pParse, pStack, pIdx->tnum, iDb); + } } /* Remove the table entry from SQLite's internal schema and modify ** the schema cookie. */ @@ -3475,110 +3530,138 @@ ** This routine is called to do the work of a DROP TABLE statement. ** pName is the name of the table to be dropped. */ void sqlite3DropTable(Parse *pParse, SrcList *pName, int isView, int noErr){ Table *pTab; - Vdbe *v; sqlite3 *db = pParse->db; int iDb; - - if( db->mallocFailed ){ - goto exit_drop_table; - } - assert( pParse->nErr==0 ); - assert( pName->nSrc==1 ); - if( sqlite3ReadSchema(pParse) ) goto exit_drop_table; - if( noErr ) db->suppressErr++; - assert( isView==0 || isView==LOCATE_VIEW ); - pTab = sqlite3LocateTableItem(pParse, isView, &pName->a[0]); - if( noErr ) db->suppressErr--; - - if( pTab==0 ){ - if( noErr ){ - sqlite3CodeVerifyNamedSchema(pParse, pName->a[0].zDatabase); - sqlite3ForceNotReadOnly(pParse); - } - goto exit_drop_table; - } - iDb = sqlite3SchemaToIndex(db, pTab->pSchema); - assert( iDb>=0 && iDbnDb ); - - /* If pTab is a virtual table, call ViewGetColumnNames() to ensure - ** it is initialized. - */ - if( IsVirtual(pTab) && sqlite3ViewGetColumnNames(pParse, pTab) ){ - goto exit_drop_table; - } + int ii, jj; + RootStack rootStack; + + memset(&rootStack, 0, sizeof(rootStack)); + (void)sqlite3GetVdbe(pParse); + sqlite3ReadSchema(pParse); + assert( pName!=0 || pParse->nErr!=0 ); + for(ii=0; pParse->nErr==0 && iinSrc; ii++){ + if( noErr ) db->suppressErr++; + assert( isView==0 || isView==LOCATE_VIEW ); + pTab = sqlite3LocateTableItem(pParse, isView, &pName->a[ii]); + if( noErr ) db->suppressErr--; + + if( pTab==0 ){ + if( noErr ){ + sqlite3CodeVerifyNamedSchema(pParse, pName->a[ii].zDatabase); + sqlite3ForceNotReadOnly(pParse); + } + testcase( ii+1nSrc ); + continue; + } + iDb = sqlite3SchemaToIndex(db, pTab->pSchema); + assert( iDb>=0 && iDbnDb ); + + /* If pTab is a virtual table, call ViewGetColumnNames() to ensure + ** it is initialized. + */ + if( IsVirtual(pTab) && sqlite3ViewGetColumnNames(pParse, pTab) ){ + break; + } #ifndef SQLITE_OMIT_AUTHORIZATION - { - int code; - const char *zTab = SCHEMA_TABLE(iDb); - const char *zDb = db->aDb[iDb].zDbSName; - const char *zArg2 = 0; - if( sqlite3AuthCheck(pParse, SQLITE_DELETE, zTab, 0, zDb)){ - goto exit_drop_table; - } - if( isView ){ - if( !OMIT_TEMPDB && iDb==1 ){ - code = SQLITE_DROP_TEMP_VIEW; - }else{ - code = SQLITE_DROP_VIEW; - } -#ifndef SQLITE_OMIT_VIRTUALTABLE - }else if( IsVirtual(pTab) ){ - code = SQLITE_DROP_VTABLE; - zArg2 = sqlite3GetVTable(db, pTab)->pMod->zName; -#endif - }else{ - if( !OMIT_TEMPDB && iDb==1 ){ - code = SQLITE_DROP_TEMP_TABLE; - }else{ - code = SQLITE_DROP_TABLE; - } - } - if( sqlite3AuthCheck(pParse, code, pTab->zName, zArg2, zDb) ){ - goto exit_drop_table; - } - if( sqlite3AuthCheck(pParse, SQLITE_DELETE, pTab->zName, 0, zDb) ){ - goto exit_drop_table; - } - } -#endif - if( tableMayNotBeDropped(db, pTab) ){ - sqlite3ErrorMsg(pParse, "table %s may not be dropped", pTab->zName); - goto exit_drop_table; - } + { + int code; + const char *zTab = SCHEMA_TABLE(iDb); + const char *zDb = db->aDb[iDb].zDbSName; + const char *zArg2 = 0; + if( sqlite3AuthCheck(pParse, SQLITE_DELETE, zTab, 0, zDb)){ + testcase( ii>0 ); + testcase( ii+1nSrc ); + break; + } + if( isView ){ + if( !OMIT_TEMPDB && iDb==1 ){ + code = SQLITE_DROP_TEMP_VIEW; + }else{ + code = SQLITE_DROP_VIEW; + } +#ifndef SQLITE_OMIT_VIRTUALTABLE + }else if( IsVirtual(pTab) ){ + code = SQLITE_DROP_VTABLE; + zArg2 = sqlite3GetVTable(db, pTab)->pMod->zName; +#endif + }else{ + if( !OMIT_TEMPDB && iDb==1 ){ + code = SQLITE_DROP_TEMP_TABLE; + }else{ + code = SQLITE_DROP_TABLE; + } + } + if( sqlite3AuthCheck(pParse, code, pTab->zName, zArg2, zDb) ){ + testcase( ii>0 ); + testcase( ii+1nSrc ); + break; + } + if( sqlite3AuthCheck(pParse, SQLITE_DELETE, pTab->zName, 0, zDb) ){ + testcase( ii>0 ); + testcase( ii+1nSrc ); + break; + } + } +#endif + if( tableMayNotBeDropped(db, pTab) ){ + testcase( ii>0 ); + testcase( ii+1nSrc ); + sqlite3ErrorMsg(pParse, "table %s may not be dropped", pTab->zName); + break; + } #ifndef SQLITE_OMIT_VIEW - /* Ensure DROP TABLE is not used on a view, and DROP VIEW is not used - ** on a table. - */ - if( isView && !IsView(pTab) ){ - sqlite3ErrorMsg(pParse, "use DROP TABLE to delete table %s", pTab->zName); - goto exit_drop_table; - } - if( !isView && IsView(pTab) ){ - sqlite3ErrorMsg(pParse, "use DROP VIEW to delete view %s", pTab->zName); - goto exit_drop_table; - } + /* Ensure DROP TABLE is not used on a view, and DROP VIEW is not used + ** on a table. + */ + if( isView && !IsView(pTab) ){ + testcase( ii>0 ); + testcase( ii+1nSrc ); + sqlite3ErrorMsg(pParse, "use DROP TABLE to delete table %s", pTab->zName); + break; + } + if( !isView && IsView(pTab) ){ + testcase( ii>0 ); + testcase( ii+1nSrc ); + sqlite3ErrorMsg(pParse, "use DROP VIEW to delete view %s", pTab->zName); + break; + } #endif + /* If this table or view has appeared previously in the list of tables + ** or views to be dropped, then the prior appearance is sufficient so + ** skip this one. */ + for(jj=ii-1; jj>=0 && pName->a[jj].pTab!=pTab; jj--){} + if( jj>=0 ) continue; - /* Generate code to remove the table from the schema table - ** on disk. - */ - v = sqlite3GetVdbe(pParse); - if( v ){ + /* Remember the table for use in the second pass */ + pName->a[ii].pTab = pTab; + pTab->nTabRef++; + + /* Generate code to clear this table from sqlite_statN and to + ** cascade foreign key constraints. + */ sqlite3BeginWriteOperation(pParse, 1, iDb); - if( !isView ){ + if( IsOrdinaryTable(pTab) ){ sqlite3ClearStatTables(pParse, iDb, "tbl", pTab->zName); sqlite3FkDropTable(pParse, pName, pTab); } - sqlite3CodeDropTable(pParse, pTab, iDb, isView); } -exit_drop_table: + /* Generate code to actually delete the tables/views in a second pass. + ** Btrees must be deleted largest root page first, to avoid problems + ** caused by autovacuum page reordering. */ + for(ii=0; pParse->nErr==0 && iinSrc; ii++){ + pTab = pName->a[ii].pTab; + if( pTab==0 ) continue; + iDb = sqlite3SchemaToIndex(db, pTab->pSchema); + sqlite3CodeDropTable(pParse, &rootStack, pTab, iDb); + } sqlite3SrcListDelete(db, pName); + rootStackCode(pParse, &rootStack); } /* ** This routine is called to create a new foreign key on the table ** currently under construction. pFromCol determines which columns @@ -4574,71 +4657,86 @@ ** This routine will drop an existing named index. This routine ** implements the DROP INDEX statement. */ void sqlite3DropIndex(Parse *pParse, SrcList *pName, int ifExists){ Index *pIndex; - Vdbe *v; sqlite3 *db = pParse->db; + Vdbe *v; int iDb; - - if( db->mallocFailed ){ - goto exit_drop_index; - } - assert( pParse->nErr==0 ); /* Never called with prior non-OOM errors */ - assert( pName->nSrc==1 ); - if( SQLITE_OK!=sqlite3ReadSchema(pParse) ){ - goto exit_drop_index; - } - pIndex = sqlite3FindIndex(db, pName->a[0].zName, pName->a[0].zDatabase); - if( pIndex==0 ){ - if( !ifExists ){ - sqlite3ErrorMsg(pParse, "no such index: %S", pName->a); - }else{ - sqlite3CodeVerifyNamedSchema(pParse, pName->a[0].zDatabase); - sqlite3ForceNotReadOnly(pParse); - } - pParse->checkSchema = 1; - goto exit_drop_index; - } - if( pIndex->idxType!=SQLITE_IDXTYPE_APPDEF ){ - sqlite3ErrorMsg(pParse, "index associated with UNIQUE " - "or PRIMARY KEY constraint cannot be dropped", 0); - goto exit_drop_index; - } - iDb = sqlite3SchemaToIndex(db, pIndex->pSchema); + int ii, jj; + RootStack rootStack; + + memset(&rootStack, 0, sizeof(rootStack)); + v = sqlite3GetVdbe(pParse); + sqlite3ReadSchema(pParse); + assert( pName!=0 || pParse->nErr!=0 ); + for(ii=0; pParse->nErr==0 && iinSrc; ii++){ + pName->a[ii].regReturn = 0; + pIndex = sqlite3FindIndex(db, pName->a[ii].zName, pName->a[ii].zDatabase); + if( pIndex==0 ){ + if( !ifExists ){ + sqlite3ErrorMsg(pParse, "no such index: %S", pName->a+ii); + }else{ + sqlite3CodeVerifyNamedSchema(pParse, pName->a[ii].zDatabase); + sqlite3ForceNotReadOnly(pParse); + testcase( ii>0 ); + testcase( ii+1nSrc ); + } + pParse->checkSchema = 1; + continue; + } + if( pIndex->idxType!=SQLITE_IDXTYPE_APPDEF ){ + sqlite3ErrorMsg(pParse, "index associated with UNIQUE " + "or PRIMARY KEY constraint cannot be dropped", 0); + testcase( ii>0 ); + testcase( ii+1nSrc ); + break; + } + iDb = sqlite3SchemaToIndex(db, pIndex->pSchema); #ifndef SQLITE_OMIT_AUTHORIZATION - { - int code = SQLITE_DROP_INDEX; - Table *pTab = pIndex->pTable; - const char *zDb = db->aDb[iDb].zDbSName; - const char *zTab = SCHEMA_TABLE(iDb); - if( sqlite3AuthCheck(pParse, SQLITE_DELETE, zTab, 0, zDb) ){ - goto exit_drop_index; - } - if( !OMIT_TEMPDB && iDb==1 ) code = SQLITE_DROP_TEMP_INDEX; - if( sqlite3AuthCheck(pParse, code, pIndex->zName, pTab->zName, zDb) ){ - goto exit_drop_index; - } - } + { + int code = SQLITE_DROP_INDEX; + Table *pTab = pIndex->pTable; + const char *zDb = db->aDb[iDb].zDbSName; + const char *zTab = SCHEMA_TABLE(iDb); + if( sqlite3AuthCheck(pParse, SQLITE_DELETE, zTab, 0, zDb) ){ + testcase( ii>0 ); + testcase( ii+1nSrc ); + break; + } + if( !OMIT_TEMPDB && iDb==1 ) code = SQLITE_DROP_TEMP_INDEX; + if( sqlite3AuthCheck(pParse, code, pIndex->zName, pTab->zName, zDb) ){ + testcase( ii>0 ); + testcase( ii+1nSrc ); + break; + } + } #endif - /* Generate code to remove the index and from the schema table */ - v = sqlite3GetVdbe(pParse); - if( v ){ + /* Skip over redundant DROP INDEXes */ + for(jj=ii-1; jj>=0 && pName->a[jj].u2.pIdx!=pIndex; jj--){} + if( jj>=0 ) continue; + + /* Record that this index needs to be dropped. */ + pName->a[ii].u2.pIdx = pIndex; + + /* Generate code to remove the index and from the schema table and + ** from sqlite_statN tables */ sqlite3BeginWriteOperation(pParse, 1, iDb); sqlite3NestedParse(pParse, - "DELETE FROM %Q." LEGACY_SCHEMA_TABLE " WHERE name=%Q AND type='index'", + "DELETE FROM %Q." LEGACY_SCHEMA_TABLE + " WHERE name=%Q AND type='index'", db->aDb[iDb].zDbSName, pIndex->zName ); sqlite3ClearStatTables(pParse, iDb, "idx", pIndex->zName); + + rootStackPush(pParse, &rootStack, pIndex->tnum, iDb); sqlite3ChangeCookie(pParse, iDb); - destroyRootPage(pParse, pIndex->tnum, iDb); sqlite3VdbeAddOp4(v, OP_DropIndex, iDb, 0, 0, pIndex->zName, 0); } - -exit_drop_index: sqlite3SrcListDelete(db, pName); + rootStackCode(pParse, &rootStack); } /* ** pArray is a pointer to an array of objects. Each object in the ** array is szEntry bytes in size. This routine uses sqlite3DbRealloc() Index: src/fkey.c ================================================================== --- src/fkey.c +++ src/fkey.c @@ -716,29 +716,38 @@ } /* ** This function is called to generate code that runs when table pTab is ** being dropped from the database. The SrcList passed as the second argument -** to this function contains a single entry guaranteed to resolve to -** table pTab. +** to this function contains entries for every table that is being dropped, +** with the SrcItem.pTab line being set to the table being dropped. pTab +** will be one of those tables. ** ** Normally, no code is required. However, if either ** ** (a) The table is the parent table of a FK constraint, or ** (b) The table is the child table of a deferred FK constraint and it is ** determined at runtime that there are outstanding deferred FK ** constraint violations in the database, ** -** then the equivalent of "DELETE FROM " is executed before dropping -** the table from the database. Triggers are disabled while running this -** DELETE, but foreign key actions are not. +** then the equivalent of "DELETE FROM " is executed for every table +** being dropped, in the order specified in the DROP TABLE statement, which +** is the same as the order in which these tables appear in pName. +** Triggers are disabled while running these DELETEs, but foreign key +** actions are not. +** +** This routine sets the SrcList.addrFillSub value for all pName entries +** for which DELETE FROM has been run. This prevents the DELETE FROM +** from being run multiple times. */ void sqlite3FkDropTable(Parse *pParse, SrcList *pName, Table *pTab){ sqlite3 *db = pParse->db; if( (db->flags&SQLITE_ForeignKeys) && IsOrdinaryTable(pTab) ){ int iSkip = 0; Vdbe *v = sqlite3GetVdbe(pParse); + SrcList *pSrc; + int ii; assert( v ); /* VDBE has already been allocated */ assert( IsOrdinaryTable(pTab) ); if( sqlite3FkReferences(pTab)==0 ){ /* Search for a deferred foreign key constraint for which this table @@ -754,11 +763,21 @@ iSkip = sqlite3VdbeMakeLabel(pParse); sqlite3VdbeAddOp2(v, OP_FkIfZero, 1, iSkip); VdbeCoverage(v); } pParse->disableTriggers = 1; - sqlite3DeleteFrom(pParse, sqlite3SrcListDup(db, pName, 0), 0, 0, 0); + for(ii=0; iinSrc; ii++){ + if( pName->a[ii].pTab==0 ) continue; + if( pName->a[ii].addrFillSub ) continue; + pName->a[ii].addrFillSub = 1; + pSrc = sqlite3SrcListAppend(pParse, 0, 0, 0); + if( pSrc ){ + pSrc->a[0].zDatabase = sqlite3DbStrDup(db, pName->a[ii].zDatabase); + pSrc->a[0].zName = sqlite3DbStrDup(db, pName->a[ii].zName); + sqlite3DeleteFrom(pParse, pSrc, 0, 0, 0); + } + } pParse->disableTriggers = 0; /* If the DELETE has generated immediate foreign key constraint ** violations, halt the VDBE and return an error at this point, before ** any modifications to the schema are made. This is because statement Index: src/parse.y ================================================================== --- src/parse.y +++ src/parse.y @@ -469,11 +469,11 @@ resolvetype(A) ::= IGNORE. {A = OE_Ignore;} resolvetype(A) ::= REPLACE. {A = OE_Replace;} ////////////////////////// The DROP TABLE ///////////////////////////////////// // -cmd ::= DROP TABLE ifexists(E) fullname(X). { +cmd ::= DROP TABLE ifexists(E) fullnamelist(X). { sqlite3DropTable(pParse, X, 0, E); } %type ifexists {int} ifexists(A) ::= IF EXISTS. {A = 1;} ifexists(A) ::= . {A = 0;} @@ -483,11 +483,11 @@ %ifndef SQLITE_OMIT_VIEW cmd ::= createkw(X) temp(T) VIEW ifnotexists(E) nm(Y) dbnm(Z) eidlist_opt(C) AS select(S). { sqlite3CreateView(pParse, &X, &Y, &Z, C, S, T, E); } -cmd ::= DROP VIEW ifexists(E) fullname(X). { +cmd ::= DROP VIEW ifexists(E) fullnamelist(X). { sqlite3DropTable(pParse, X, 1, E); } %endif SQLITE_OMIT_VIEW //////////////////////// The SELECT statement ///////////////////////////////// @@ -771,10 +771,22 @@ } fullname(A) ::= nm(X) DOT nm(Y). { A = sqlite3SrcListAppend(pParse,0,&X,&Y); if( IN_RENAME_OBJECT && A ) sqlite3RenameTokenMap(pParse, A->a[0].zName, &Y); } + +%type fullnamelist {SrcList*} +%destructor fullnamelist {sqlite3SrcListDelete(pParse->db, $$);} +fullnamelist(A) ::= fullname(A). +fullnamelist(A) ::= fullnamelist(L) COMMA nm(X). { + A = sqlite3SrcListAppend(pParse,L,&X,0); + assert( !IN_RENAME_OBJECT ); /* Used only by DROP, which cannot be part of schema */ +} +fullnamelist(A) ::= fullnamelist(L) COMMA nm(X) DOT nm(Y). { + A = sqlite3SrcListAppend(pParse,L,&X,&Y); + assert( !IN_RENAME_OBJECT ); /* Used only by DROP, which cannot be part of schema */ +} %type xfullname {SrcList*} %destructor xfullname {sqlite3SrcListDelete(pParse->db, $$);} xfullname(A) ::= nm(X). {A = sqlite3SrcListAppend(pParse,0,&X,0); /*A-overwrites-X*/} @@ -1502,11 +1514,11 @@ collate(C) ::= COLLATE ids. {C = 1;} ///////////////////////////// The DROP INDEX command ///////////////////////// // -cmd ::= DROP INDEX ifexists(E) fullname(X). {sqlite3DropIndex(pParse, X, E);} +cmd ::= DROP INDEX ifexists(E) fullnamelist(X). {sqlite3DropIndex(pParse, X, E);} ///////////////////////////// The VACUUM command ///////////////////////////// // %if !SQLITE_OMIT_VACUUM && !SQLITE_OMIT_ATTACH %type vinto {Expr*} @@ -1659,11 +1671,11 @@ raisetype(A) ::= FAIL. {A = OE_Fail;} //////////////////////// DROP TRIGGER statement ////////////////////////////// %ifndef SQLITE_OMIT_TRIGGER -cmd ::= DROP TRIGGER ifexists(NOERR) fullname(X). { +cmd ::= DROP TRIGGER ifexists(NOERR) fullnamelist(X). { sqlite3DropTrigger(pParse,X,NOERR); } %endif !SQLITE_OMIT_TRIGGER //////////////////////// ATTACH DATABASE file AS name ///////////////////////// Index: src/sqliteInt.h ================================================================== --- src/sqliteInt.h +++ src/sqliteInt.h @@ -3312,10 +3312,12 @@ ExprList *pFuncArg; /* Arguments to table-valued-function */ } u1; union { Index *pIBIndex; /* Index structure corresponding to u1.zIndexedBy */ CteUse *pCteUse; /* CTE Usage info when fg.isCte is true */ + Trigger *pTrig; /* Trigger in argument list of DROP TRIGGER */ + Index *pIdx; /* Index in argument list to DROP INDEX */ } u2; }; /* ** The OnOrUsing object represents either an ON clause or a USING clause. @@ -4924,11 +4926,10 @@ #if SQLITE_MAX_ATTACHED>30 int sqlite3DbMaskAllZero(yDbMask); #endif void sqlite3DropTable(Parse*, SrcList*, int, int); -void sqlite3CodeDropTable(Parse*, Table*, int, int); void sqlite3DeleteTable(sqlite3*, Table*); void sqlite3DeleteTableGeneric(sqlite3*, void*); void sqlite3FreeIndex(sqlite3*, Index*); #ifndef SQLITE_OMIT_AUTOINCREMENT void sqlite3AutoincrementBegin(Parse *pParse); @@ -5572,11 +5573,11 @@ ** this case foreign keys are parsed, but no other functionality is ** provided (enforcement of FK constraints requires the triggers sub-system). */ #if !defined(SQLITE_OMIT_FOREIGN_KEY) && !defined(SQLITE_OMIT_TRIGGER) void sqlite3FkCheck(Parse*, Table*, int, int, int*, int); - void sqlite3FkDropTable(Parse*, SrcList *, Table*); + void sqlite3FkDropTable(Parse*, SrcList*, Table*); void sqlite3FkActions(Parse*, Table*, ExprList*, int, int*, int); int sqlite3FkRequired(Parse*, Table*, int*, int); u32 sqlite3FkOldmask(Parse*, Table*); FKey *sqlite3FkReferences(Table *); void sqlite3FkClearTriggerCache(sqlite3*,int); Index: src/trigger.c ================================================================== --- src/trigger.c +++ src/trigger.c @@ -622,39 +622,43 @@ Trigger *pTrigger = 0; int i; const char *zDb; const char *zName; sqlite3 *db = pParse->db; - - if( db->mallocFailed ) goto drop_trigger_cleanup; - if( SQLITE_OK!=sqlite3ReadSchema(pParse) ){ - goto drop_trigger_cleanup; - } - - assert( pName->nSrc==1 ); - zDb = pName->a[0].zDatabase; - zName = pName->a[0].zName; - assert( zDb!=0 || sqlite3BtreeHoldsAllMutexes(db) ); - for(i=OMIT_TEMPDB; inDb; i++){ - int j = (i<2) ? i^1 : i; /* Search TEMP before MAIN */ - if( zDb && sqlite3DbIsNamed(db, j, zDb)==0 ) continue; - assert( sqlite3SchemaMutexHeld(db, j, 0) ); - pTrigger = sqlite3HashFind(&(db->aDb[j].pSchema->trigHash), zName); - if( pTrigger ) break; - } - if( !pTrigger ){ - if( !noErr ){ - sqlite3ErrorMsg(pParse, "no such trigger: %S", pName->a); - }else{ - sqlite3CodeVerifyNamedSchema(pParse, zDb); - } - pParse->checkSchema = 1; - goto drop_trigger_cleanup; - } - sqlite3DropTriggerPtr(pParse, pTrigger); - -drop_trigger_cleanup: + int ii, jj; + + sqlite3ReadSchema(pParse); + assert( pName!=0 || pParse->nErr!=0 ); + for(ii=0; pParse->nErr==0 && iinSrc; ii++){ + zDb = pName->a[ii].zDatabase; + zName = pName->a[ii].zName; + assert( zDb!=0 || sqlite3BtreeHoldsAllMutexes(db) ); + for(i=OMIT_TEMPDB; inDb; i++){ + int j = (i<2) ? i^1 : i; /* Search TEMP before MAIN */ + if( zDb && sqlite3DbIsNamed(db, j, zDb)==0 ) continue; + assert( sqlite3SchemaMutexHeld(db, j, 0) ); + pTrigger = sqlite3HashFind(&(db->aDb[j].pSchema->trigHash), zName); + if( pTrigger ) break; + } + pName->a[ii].u2.pTrig = pTrigger; + if( !pTrigger ){ + if( !noErr ){ + sqlite3ErrorMsg(pParse, "no such trigger: %S", pName->a+ii); + }else{ + sqlite3CodeVerifyNamedSchema(pParse, zDb); + } + testcase( ii>0 ); + testcase( ii+1nSrc ); + pParse->checkSchema = 1; + continue; + } + for(jj=ii-1; jj>=0; jj--){ + if( pName->a[jj].u2.pTrig==pTrigger ) break; + } + if( jj>=0 ) continue; + sqlite3DropTriggerPtr(pParse, pTrigger); + } sqlite3SrcListDelete(db, pName); } /* ** Return a pointer to the Table structure for the table that a trigger Index: test/aggnested.test ================================================================== --- test/aggnested.test +++ test/aggnested.test @@ -115,12 +115,11 @@ # Further variants of the test case, as found in the ticket # do_test aggnested-3.1 { db eval { - DROP TABLE IF EXISTS t1; - DROP TABLE IF EXISTS t2; + DROP TABLE IF EXISTS t1, t2; CREATE TABLE t1 ( id1 INTEGER PRIMARY KEY AUTOINCREMENT, value1 INTEGER ); INSERT INTO t1 VALUES(4469,2),(4476,1); @@ -148,12 +147,11 @@ } } {1 1} do_test aggnested-3.2 { db eval { - DROP TABLE IF EXISTS t1; - DROP TABLE IF EXISTS t2; + DROP TABLE IF EXISTS t1, t2; CREATE TABLE t1 ( id1 INTEGER, value1 INTEGER, x1 INTEGER ); @@ -176,12 +174,11 @@ GROUP BY id1); } } {1 0} do_test aggnested-3.3 { db eval { - DROP TABLE IF EXISTS t1; - DROP TABLE IF EXISTS t2; + DROP TABLE IF EXISTS t1, t2; CREATE TABLE t1(id1, value1); INSERT INTO t1 VALUES(4469,2),(4469,1); CREATE TABLE t2 (value2); INSERT INTO t2 VALUES(1); SELECT (SELECT sum(value2=value1) FROM t2), max(value1) @@ -247,22 +244,20 @@ # 2019-08-31 # Problem found by dbsqlfuzz # do_execsql_test aggnested-4.1 { - DROP TABLE IF EXISTS aa; - DROP TABLE IF EXISTS bb; + DROP TABLE IF EXISTS aa, bb; CREATE TABLE aa(x INT); INSERT INTO aa(x) VALUES(123); CREATE TABLE bb(y INT); INSERT INTO bb(y) VALUES(456); SELECT (SELECT sum(x+(SELECT y)) FROM bb) FROM aa; } {579} do_execsql_test aggnested-4.2 { SELECT (SELECT sum(x+y) FROM bb) FROM aa; } {579} do_execsql_test aggnested-4.3 { - DROP TABLE IF EXISTS tx; - DROP TABLE IF EXISTS ty; + DROP TABLE IF EXISTS tx, ty; CREATE TABLE tx(x INT); INSERT INTO tx VALUES(1),(2),(3),(4),(5); CREATE TABLE ty(y INT); INSERT INTO ty VALUES(91),(92),(93); SELECT min((SELECT count(y) FROM ty)) FROM tx; @@ -475,13 +470,12 @@ # 2023-12-16 # New test case for check-in [4470f657d2069972] from 2023-11-02 # https://bugs.chromium.org/p/chromium/issues/detail?id=1511689 # do_execsql_test 10.1 { - DROP TABLE IF EXISTS t0; - DROP TABLE IF EXISTS t1; + DROP TABLE IF EXISTS t0, t1; CREATE TABLE t0(c1, c2); INSERT INTO t0 VALUES(1,2); CREATE TABLE t1(c3, c4); INSERT INTO t1 VALUES(3,4); SELECT * FROM t0 WHERE EXISTS (SELECT 1 FROM t1 GROUP BY c3 HAVING ( SELECT count(*) FROM (SELECT 1 UNION ALL SELECT sum(DISTINCT c1) ) ) ) BETWEEN 1 AND 1; } {1 2} finish_test Index: test/aggorderby.test ================================================================== --- test/aggorderby.test +++ test/aggorderby.test @@ -58,12 +58,11 @@ SELECT c, max(a ORDER BY a) FROM t1; } {7 9} do_execsql_test aggorderby-5.0 { - DROP TABLE IF EXISTS t1; - DROP TABLE IF EXISTS t3; + DROP TABLE IF EXISTS t1, t3; CREATE TABLE t1(a TEXT); INSERT INTO t1 VALUES('aaa'),('bbb'); CREATE TABLE t3(d TEXT); INSERT INTO t3 VALUES('/'),('-'); SELECT (SELECT string_agg(a,d) FROM t3) FROM t1; } {aaa-aaa bbb-bbb} do_execsql_test aggorderby-5.1 { Index: test/alter3.test ================================================================== --- test/alter3.test +++ test/alter3.test @@ -103,13 +103,11 @@ catchsql { # May not exist if foriegn-keys are omitted at compile time. DROP TABLE t2; } execsql { - DROP TABLE abc; - DROP TABLE t1; - DROP TABLE t3; + DROP TABLE abc, t1, t3; } } {} do_test alter3-2.1 { execsql { @@ -294,12 +292,11 @@ SELECT * FROM t1; } } {1 one 2 two} do_test alter3-5.99 { execsql { - DROP TABLE aux.t1; - DROP TABLE t1; + DROP TABLE aux.t1, main.t1; } } {} } #---------------------------------------------------------------- Index: test/alter4.test ================================================================== --- test/alter4.test +++ test/alter4.test @@ -112,13 +112,11 @@ catchsql { # May not exist if foriegn-keys are omitted at compile time. DROP TABLE t2; } execsql { - DROP TABLE abc; - DROP TABLE t1; - DROP TABLE t3; + DROP TABLE abc, t1, t3; } } {} do_test alter4-2.1 { execsql { Index: test/analyze.test ================================================================== --- test/analyze.test +++ test/analyze.test @@ -193,12 +193,11 @@ execsql { CREATE TABLE t3 AS SELECT a, b, rowid AS c, 'hi' AS d FROM t1; CREATE INDEX t3i1 ON t3(a); CREATE INDEX t3i2 ON t3(a,b,c,d); CREATE INDEX t3i3 ON t3(d,b,c,a); - DROP TABLE t1; - DROP TABLE t2; + DROP TABLE t1, t2; SELECT idx, stat FROM sqlite_stat1 ORDER BY idx; } } {} do_test analyze-3.9 { execsql { Index: test/auth.test ================================================================== --- test/auth.test +++ test/auth.test @@ -214,11 +214,11 @@ set ::authargs [list $arg1 $arg2 $arg3 $arg4] return SQLITE_DENY } return SQLITE_OK } - catchsql {DROP TABLE t2} + catchsql {DROP TABLE IF EXISTS none1, none2, t2, none3} } {1 {not authorized}} do_test auth-1.21.2 { set ::authargs } {t2 {} main {}} do_test auth-1.22 { @@ -481,11 +481,11 @@ if {$code=="SQLITE_DELETE" && $arg1=="sqlite_master"} { return SQLITE_DENY } return SQLITE_OK } - catchsql {DROP TABLE t2} + catchsql {DROP TABLE IF EXISTS none1, t2, none2} } {1 {not authorized}} do_test auth-1.64 { execsql {SELECT name FROM sqlite_master} } {t2} do_test auth-1.65 { @@ -724,11 +724,11 @@ set ::authargs [list $arg1 $arg2 $arg3 $arg4] return SQLITE_DENY } return SQLITE_OK } - catchsql {DROP VIEW v2} + catchsql {DROP VIEW IF EXISTS none1, v2, none2} } {1 {not authorized}} do_test auth-1.102 { set ::authargs } {v2 {} main {}} do_test auth-1.103 { @@ -1088,11 +1088,11 @@ set ::authargs [list $arg1 $arg2 $arg3 $arg4] return SQLITE_DENY } return SQLITE_OK } - catchsql {DROP TRIGGER r2} + catchsql {DROP TRIGGER IF EXISTS none1, r2, none2} } {1 {not authorized}} do_test auth-1.154 { set ::authargs } {r2 t2 main {}} do_test auth-1.155 { @@ -1387,11 +1387,11 @@ set ::authargs [list $arg1 $arg2 $arg3 $arg4] return SQLITE_DENY } return SQLITE_OK } - catchsql {DROP INDEX i2} + catchsql {DROP INDEX IF EXISTS none1, i2, none2} } {1 {not authorized}} do_test auth-1.205a { set ::authargs } {i2 t2 main {}} db eval { ADDED test/drop-many.test Index: test/drop-many.test ================================================================== --- /dev/null +++ test/drop-many.test @@ -0,0 +1,161 @@ +# 2024-02-29 +# +# 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. +# +#*********************************************************************** +# +# Test cases for DROP TABLE, DROP INDEX, DROP TRIGGER, and DROP VIEW that +# list multiple objects to be dropped. + + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +set testprefix drop-many + +do_execsql_test 1.1 { + CREATE TABLE t1(a); + CREATE TABLE t2(b, c UNIQUE); + CREATE TABLE t3(d TEXT PRIMARY KEY, e); + CREATE INDEX t3e ON t3(e); + ATTACH ':memory:' AS aux1; + CREATE TABLE aux1.t4(f INT, g INT, h INT PRIMARY KEY) WITHOUT ROWID; + CREATE INDEX aux1.t4g ON t4(g); + CREATE INDEX t2b ON t2(b); + CREATE VIEW v5 AS SELECT 1,2,3; + CREATE VIEW v6 AS SELECT * FROM t3; + CREATE VIEW aux1.v7 AS SELECT 'hello'; + CREATE TRIGGER r1 AFTER INSERT ON t1 BEGIN SELECT 'this is trigger r1'; END; + CREATE TRIGGER r2 BEFORE DELETE ON t2 BEGIN INSERT INTO t1 VALUES(old.b); END; + CREATE TRIGGER aux1.r3 AFTER UPDATE ON t4 BEGIN SELECT 'trigger r3'; END; + CREATE TRIGGER aux1.r4 INSTEAD OF UPDATE ON v7 BEGIN SELECT NULL; END; +} {} +do_execsql_test 1.2 { + BEGIN; + DROP TABLE t2, t4, t3, t1; + SELECT name FROM sqlite_schema WHERE type='table' + UNION ALL + SELECT name from aux1.sqlite_schema WHERE type='table' + ORDER BY name; + ROLLBACK; +} {} +do_catchsql_test 1.3.1 { + DROP TABLE t2, t4, t3, t05, t1; +} {1 {no such table: t05}} +do_execsql_test 1.3.2 { + SELECT name FROM sqlite_schema WHERE type='table' + UNION ALL + SELECT name from aux1.sqlite_schema WHERE type='table' + ORDER BY name; +} {t1 t2 t3 t4} +do_execsql_test 1.4 { + BEGIN; + DROP TABLE IF EXISTS t01, t2, t02, t4, t03, t3, t04, t1, t05; + SELECT name FROM sqlite_schema WHERE type='table' + UNION ALL + SELECT name from aux1.sqlite_schema WHERE type='table' + ORDER BY name; + ROLLBACK; +} {} +do_catchsql_test 1.5.1 { + DROP TABLE IF EXISTS t2, t4, t3, v7, t1; +} {1 {use DROP VIEW to delete view v7}} +do_execsql_test 1.5.2 { + SELECT name FROM sqlite_schema WHERE type='table' + UNION ALL + SELECT name from aux1.sqlite_schema WHERE type='table' + ORDER BY name; +} {t1 t2 t3 t4} + + +do_execsql_test 2.1 { + BEGIN; + DROP VIEW v5, v6, v7; + SELECT name FROM sqlite_schema WHERE type='view' + UNION ALL + SELECT name from aux1.sqlite_schema WHERE type='view' + ORDER BY name; + ROLLBACK; +} {} +do_catchsql_test 2.2.1 { + DROP VIEW v5, v6, v8, v7; +} {1 {no such view: v8}} +do_execsql_test 2.2.2 { + SELECT name FROM sqlite_schema WHERE type='view' + UNION ALL + SELECT name from aux1.sqlite_schema WHERE type='view' + ORDER BY name; +} {v5 v6 v7} +do_catchsql_test 2.3.1 { + DROP VIEW v5, v6, t1, v7; +} {1 {use DROP TABLE to delete table t1}} +do_execsql_test 2.3.2 { + SELECT name FROM sqlite_schema WHERE type='view' + UNION ALL + SELECT name from aux1.sqlite_schema WHERE type='view' + ORDER BY name; +} {v5 v6 v7} + + +do_execsql_test 3.1 { + BEGIN; + DROP INDEX t2b, aux1.t4g, main.t3e; + SELECT name FROM sqlite_schema WHERE type='index' AND name NOT LIKE 'sqlite%' + UNION ALL + SELECT name from aux1.sqlite_schema WHERE type='index' + ORDER BY name; + ROLLBACK; +} {} +do_catchsql_test 3.2.1 { + DROP INDEX t2b, aux1.t4g, t1, main.t3e; +} {1 {no such index: t1}} +do_execsql_test 3.2.2 { + SELECT name FROM sqlite_schema WHERE type='index' AND name NOT LIKE 'sqlite%' + UNION ALL + SELECT name from aux1.sqlite_schema WHERE type='index' + ORDER BY name; +} {t2b t3e t4g} +do_execsql_test 3.3 { + BEGIN; + DROP INDEX IF EXISTS aux1.none, t2b, none2, aux1.t4g, main.t3e, none3; + SELECT name FROM sqlite_schema WHERE type='index' AND name NOT LIKE 'sqlite%' + UNION ALL + SELECT name from aux1.sqlite_schema WHERE type='index' + ORDER BY name; + ROLLBACK; +} {} + + +do_execsql_test 4.1 { + BEGIN; + DROP TRIGGER main.r1, r2, r3, aux1.r4; + SELECT name FROM sqlite_schema WHERE type='trigger' + UNION ALL + SELECT name from aux1.sqlite_schema WHERE type='trigger' + ORDER BY name; + ROLLBACK; +} {} +do_catchsql_test 4.2.1 { + DROP TRIGGER main.r1, r2, r3, main.t1, aux1.r4; +} {1 {no such trigger: main.t1}} +do_execsql_test 4.2.2 { + SELECT name FROM sqlite_schema WHERE type='trigger' + UNION ALL + SELECT name from aux1.sqlite_schema WHERE type='trigger' + ORDER BY name; +} {r1 r2 r3 r4} +do_execsql_test 4.3 { + BEGIN; + DROP TRIGGER IF EXISTS none1, main.r1, r2, aux1.none2, r3, aux1.r4; + SELECT name FROM sqlite_schema WHERE type='trigger' + UNION ALL + SELECT name from aux1.sqlite_schema WHERE type='trigger' + ORDER BY name; + ROLLBACK; +} {} + +finish_test Index: test/fkey1.test ================================================================== --- test/fkey1.test +++ test/fkey1.test @@ -68,10 +68,21 @@ DROP TABLE t5; DROP TABLE t8; DROP TABLE t6; DROP TABLE t10; } +} {} +do_test fkey1-2.2 { + execsql { + CREATE TABLE t5(x references t4); + CREATE TABLE t6(x references t4); + CREATE TABLE t7(x references t4); + CREATE TABLE t8(x references t4); + CREATE TABLE t9(x references t4); + CREATE TABLE t10(x references t4); + DROP TABLE t7, t9, t5, t8, t6, t10; + } } {} do_test fkey1-3.1 { execsql { CREATE TABLE t5(a PRIMARY KEY, b, c);