diff --git a/manifest b/manifest index 35d84c28a0..d026825f19 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Fix\ssome\sstrict-aliasing\sproblems\sin\sfts3_expr.c.\s(CVS\s6035) -D 2008-12-17T15:49:52 +C Add\sthe\ssavepoint\sfeature.\sThis\sfeature\sis\slargely\suntested\sat\sthis\spoint.\s(CVS\s6036) +D 2008-12-17T17:30:26 F Makefile.arm-wince-mingw32ce-gcc fcd5e9cd67fe88836360bb4f9ef4cb7f8e2fb5a0 F Makefile.in f7e4c81c347b04f7b0f1c1b081a168645d7b8af7 F Makefile.linux-gcc d53183f4aa6a9192d249731c90dbdffbd2c68654 @@ -102,10 +102,10 @@ F src/attach.c 1c35f95da3c62d19de75b44cfefd12c81c1791b3 F src/auth.c c8b2ab5c8bad4bd90ed7c294694f48269162c627 F src/bitvec.c 4300d311b17fb3c1476623fd895a8feac02a0b08 F src/btmutex.c 63c5cc4ad5715690767ffcb741e185d7bc35ec1a -F src/btree.c fb64a2afba6d417b78d6727c4de34a821495ed1e -F src/btree.h 179c3ea813780df78a289a8f5130db18e6d4616e +F src/btree.c c402a9a15fe62508b332517b162f6fdbcf1bfb47 +F src/btree.h 4f141cf748d2ee7c6d7fc175f64f87a45cd44113 F src/btreeInt.h 7ef2c872371d7508657f8d7a4efe651c741d6ee6 -F src/build.c ae4359475f82acbd947db6d957e2ff39d66de26e +F src/build.c f3e8377cbc0d007b01aab1e7d4fc1d5b296c422e F src/callback.c bee8949d619b1b7b1e4dfac8a19c5116ae1dd12a F src/complete.c cb14e06dbe79dee031031f0d9e686ff306afe07c F src/date.c 3dbac3eac2848af416786b6e1e3150f7c740dac6 @@ -121,7 +121,7 @@ F src/insert.c f6db1e6f43aae337e64a755208abb6ff124edc19 F src/journal.c cffd2cd214e58c0e99c3ff632b3bee6c7cbb260e F src/legacy.c 4f7410b29598d991628ca40b150aa089649f17d8 F src/loadext.c 2f53996c693a347edc2d773e9217dde49d96ae64 -F src/main.c 64857582ae00cc638973cbc47997d25fdbf26cf6 +F src/main.c 1d2b56821327321af1d6275603c22f86d55b3438 F src/malloc.c e2b4e6d7033372bd43adb0192bf5f64c0aa03c91 F src/mem0.c f2f84062d1f35814d6535c9f9e33de3bfb3b132c F src/mem1.c 3bfb39e4f60b0179713a7c087b2d4f0dc205735f @@ -141,9 +141,9 @@ F src/os_common.h 24525d8b7bce66c374dfc1810a6c9043f3359b60 F src/os_os2.c bed77dc26e3a95ce4a204936b9a1ca6fe612fcc5 F src/os_unix.c 96b4a6e87335ba943455740f311b4dfb63f26756 F src/os_win.c 496e3ceb499aedc63622a89ef76f7af2dd902709 -F src/pager.c 7e8c2b7b7131031cfa88ab0fdbb2de047f5be934 -F src/pager.h 37f5173612b7803f44656c16e80df3280234bb18 -F src/parse.y 3dfd941533cdc6ce0b09b905b25c4eb73858400b +F src/pager.c dd1aba4a1dc246b72d15fa9ffcf59902cea51d54 +F src/pager.h 7191294438881eb4d13eedade97891e8dc993905 +F src/parse.y 4d0e33a702dc3ea7b69d8ae1914b3fbd32e46057 F src/pcache.c 16dc8da6e6ba6250f8dfd9ee46036db1cbceedc6 F src/pcache.h f20c3e82dd6da622c3fe296170cb1801f9a2d75a F src/pcache1.c 533b18aa2456b0f135e376289443e0a342e0c456 @@ -157,13 +157,13 @@ F src/select.c a4316c5e8a417687e159b3d3ae689363d1dec5df F src/shell.c 60638e2fdfe97f1eb9c18caf87d3744d8269d012 F src/sqlite.h.in 065a828e299960316aa34f05b9f0f10f33afe4c8 F src/sqlite3ext.h 1db7d63ab5de4b3e6b83dd03d1a4e64fef6d2a17 -F src/sqliteInt.h a1478d1ec40bca0b511c06e2b4c93dc932aa3426 +F src/sqliteInt.h e26694bae99940ab603f30d15bf493d301d4e249 F src/sqliteLimit.h f435e728c6b620ef7312814d660a81f9356eb5c8 F src/status.c 237b193efae0cf6ac3f0817a208de6c6c6ef6d76 F src/table.c 23db1e5f27c03160987c122a078b4bb51ef0b2f8 F src/tclsqlite.c 23afb60549af943e135ded441a631f4745be6040 F src/test1.c b193b8b80617bdb8297b25a87d00ee8d5a125d0d -F src/test2.c 897528183edf2839c2a3c991d415905db56f1240 +F src/test2.c 4e0ea288e1cf237f8ff26c8817f177f45486f4a6 F src/test3.c 88a246b56b824275300e6c899634fbac1dc94b14 F src/test4.c f79ab52d27ff49b784b631a42e2ccd52cfd5c84c F src/test5.c 162a1cea2105a2c460a3f39fa6919617b562a288 @@ -196,7 +196,7 @@ F src/update.c 080889d241e4dcd1c545c8051eb6de86f4939295 F src/utf.c 1da9c832dba0fa8f865b5b902d93f420a1ee4245 F src/util.c ea62608f66f33a7e8322de83024ae37c415c0c7f F src/vacuum.c 383d6297bddc011ab04a9eed110db6eaf523e8e9 -F src/vdbe.c 3fd1fe6408598121213e19e1808e5de882c7b636 +F src/vdbe.c 79d3ec97b28e2a95ad2c43ecf2d757de312cb989 F src/vdbe.h 03516f28bf5aca00a53c4dccd6c313f96adb94f6 F src/vdbeInt.h e6e80a99ce634983b7cc2498843b4d2e5540900a F src/vdbeapi.c 85c33cfbfa56249cbe627831610afafba754477d @@ -490,6 +490,7 @@ F test/rollback.test 1f70ab4301d8d105d41438a436cad1fc8897f5e5 F test/rowid.test 1c8fc43c60d273e6ea44dfb992db587f3164312c F test/rtree.test b85fd4f0861a40ca366ac195e363be2528dcfadf F test/safety.test b69e2b2dd5d52a3f78e216967086884bbc1a09c6 +F test/savepoint.test fdad3b61f4a00a96cd773ca0c758cf2f53918ae3 F test/schema.test a8b000723375fd42c68d310091bdbd744fde647c F test/schema2.test 35e1c9696443d6694c8980c411497c2b5190d32e F test/select1.test d0a4cad954fd41c030ec16ffbd2d08a4c0548742 @@ -660,7 +661,7 @@ F tool/lempar.c c9151d2a4adf418fd9c3970dbb923b7a188b37c2 F tool/memleak.awk 4e7690a51bf3ed757e611273d43fe3f65b510133 F tool/memleak2.awk 9cc20c8e8f3c675efac71ea0721ee6874a1566e8 F tool/memleak3.tcl 7707006ee908cffff210c98158788d85bb3fcdbf -F tool/mkkeywordhash.c b7f85b700627becf998304a0a98aa4f0dfe30269 +F tool/mkkeywordhash.c 698ea044ca0b49bde8a9f3ce6429e8dc5a259d25 F tool/mkopts.tcl 66ac10d240cc6e86abd37dc908d50382f84ff46e x F tool/mkspeedsql.tcl a1a334d288f7adfe6e996f2e712becf076745c97 F tool/mksqlite3c.tcl c259bcf64ae8fce346e3ae302c3fd6db977f89a8 @@ -678,7 +679,7 @@ F tool/speedtest16.c c8a9c793df96db7e4933f0852abb7a03d48f2e81 F tool/speedtest2.tcl ee2149167303ba8e95af97873c575c3e0fab58ff F tool/speedtest8.c 2902c46588c40b55661e471d7a86e4dd71a18224 F tool/speedtest8inst1.c 293327bc76823f473684d589a8160bde1f52c14e -P 7389b9ecb80294569845c40a23e0c832d07f7a45 -R b5a5267b6551ee3859c69dff94406f49 +P 20a4ca5d361ecbb982129171f10cccac4f5ad093 +R 149ae663dd4ddd00dcd03e3c7806764e U danielk1977 -Z 6c8b8d45848acab7be1cd4bfcb7320ce +Z d50bab95338b938b6f59c39d60f1fcf0 diff --git a/manifest.uuid b/manifest.uuid index 3c1c5c577d..3a1e45d72e 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -20a4ca5d361ecbb982129171f10cccac4f5ad093 \ No newline at end of file +34b56600ec0c5cd7b5faab265750252bc9850e3e \ No newline at end of file diff --git a/src/btree.c b/src/btree.c index 2cf890e61c..34b4ffa442 100644 --- a/src/btree.c +++ b/src/btree.c @@ -9,7 +9,7 @@ ** May you share freely, never taking more than you give. ** ************************************************************************* -** $Id: btree.c,v 1.548 2008/12/16 13:46:30 drh Exp $ +** $Id: btree.c,v 1.549 2008/12/17 17:30:26 danielk1977 Exp $ ** ** This file implements a external (disk-based) database using BTrees. ** See the header comment on "btreeInt.h" for additional information. @@ -2059,6 +2059,9 @@ int sqlite3BtreeBeginTrans(Btree *p, int wrflag){ trans_begun: + if( rc==SQLITE_OK && wrflag ){ + rc = sqlite3PagerOpenSavepoint(pBt->pPager, p->db->nSavepoint); + } btreeIntegrity(p); sqlite3BtreeLeave(p); return rc; @@ -2729,14 +2732,23 @@ int sqlite3BtreeBeginStmt(Btree *p){ rc = pBt->readOnly ? SQLITE_READONLY : SQLITE_ERROR; }else{ assert( pBt->inTransaction==TRANS_WRITE ); - rc = pBt->readOnly ? SQLITE_OK : sqlite3PagerStmtBegin(pBt->pPager); + if( pBt->readOnly ){ + rc = SQLITE_OK; + }else{ + /* At the pager level, a statement transaction is a savepoint with + ** an index greater than all savepoints created explicitly using + ** SQL statements. It is illegal to open, release or rollback any + ** such savepoints while the statement transaction savepoint is active. + */ + int iStmtpoint = p->db->nSavepoint + 1; + rc = sqlite3PagerOpenSavepoint(pBt->pPager, iStmtpoint); + } pBt->inStmt = 1; } sqlite3BtreeLeave(p); return rc; } - /* ** Commit the statment subtransaction currently in progress. If no ** subtransaction is active, this is a no-op. @@ -2747,7 +2759,8 @@ int sqlite3BtreeCommitStmt(Btree *p){ sqlite3BtreeEnter(p); pBt->db = p->db; if( pBt->inStmt && !pBt->readOnly ){ - rc = sqlite3PagerStmtCommit(pBt->pPager); + int iStmtpoint = p->db->nSavepoint; + rc = sqlite3PagerSavepoint(pBt->pPager, SAVEPOINT_RELEASE, iStmtpoint); }else{ rc = SQLITE_OK; } @@ -2770,13 +2783,38 @@ int sqlite3BtreeRollbackStmt(Btree *p){ sqlite3BtreeEnter(p); pBt->db = p->db; if( pBt->inStmt && !pBt->readOnly ){ - rc = sqlite3PagerStmtRollback(pBt->pPager); + int iStmtpoint = p->db->nSavepoint; + rc = sqlite3PagerSavepoint(pBt->pPager, SAVEPOINT_ROLLBACK, iStmtpoint); + if( rc==SQLITE_OK ){ + rc = sqlite3PagerSavepoint(pBt->pPager, SAVEPOINT_RELEASE, iStmtpoint); + } pBt->inStmt = 0; } sqlite3BtreeLeave(p); return rc; } +/* +** The second argument to this function, op, is always SAVEPOINT_ROLLBACK +** or SAVEPOINT_RELEASE. This function either releases or rolls back the +** savepoint identified by parameter iSavepoint, depending on the value of +** op. +*/ +int sqlite3BtreeSavepoint(Btree *p, int op, int iSavepoint){ + int rc = SQLITE_OK; + if( p && p->inTrans==TRANS_WRITE ){ + BtShared *pBt = p->pBt; + assert( pBt->inStmt==0 ); + assert( op==SAVEPOINT_RELEASE || op==SAVEPOINT_ROLLBACK ); + assert( iSavepoint>=0 || (iSavepoint==-1 && op==SAVEPOINT_ROLLBACK) ); + sqlite3BtreeEnter(p); + pBt->db = p->db; + rc = sqlite3PagerSavepoint(pBt->pPager, op, iSavepoint); + sqlite3BtreeLeave(p); + } + return rc; +} + /* ** Create a new cursor for the BTree whose root is on the page ** iTable. The act of acquiring a cursor gets a read lock on diff --git a/src/btree.h b/src/btree.h index ba16730863..070059c154 100644 --- a/src/btree.h +++ b/src/btree.h @@ -13,7 +13,7 @@ ** subsystem. See comments in the source code for a detailed description ** of what each interface routine does. ** -** @(#) $Id: btree.h,v 1.105 2008/10/27 13:59:34 danielk1977 Exp $ +** @(#) $Id: btree.h,v 1.106 2008/12/17 17:30:26 danielk1977 Exp $ */ #ifndef _BTREE_H_ #define _BTREE_H_ @@ -101,6 +101,7 @@ int sqlite3BtreeIsInReadTrans(Btree*); void *sqlite3BtreeSchema(Btree *, int, void(*)(void *)); int sqlite3BtreeSchemaLocked(Btree *); int sqlite3BtreeLockTable(Btree *, int, u8); +int sqlite3BtreeSavepoint(Btree *, int, int); const char *sqlite3BtreeGetFilename(Btree *); const char *sqlite3BtreeGetDirname(Btree *); diff --git a/src/build.c b/src/build.c index c6f82e77aa..d172777b71 100644 --- a/src/build.c +++ b/src/build.c @@ -22,7 +22,7 @@ ** COMMIT ** ROLLBACK ** -** $Id: build.c,v 1.508 2008/12/10 22:30:25 shane Exp $ +** $Id: build.c,v 1.509 2008/12/17 17:30:26 danielk1977 Exp $ */ #include "sqliteInt.h" #include @@ -3311,6 +3311,23 @@ void sqlite3RollbackTransaction(Parse *pParse){ } } +/* +** This function is called by the parser when it parses a command to create, +** release or rollback an SQL savepoint. +*/ +void sqlite3Savepoint(Parse *pParse, int op, Token *pName){ + Vdbe *v; + if( pParse->nErr || pParse->db->mallocFailed ) return; + + /* TODO: Invoke the authorization callback */ + + v = sqlite3GetVdbe(pParse); + if( v ){ + const char *zName = (const char *)pName->z; + sqlite3VdbeAddOp4(v, OP_Savepoint, op, 0, 0, zName, pName->n); + } +} + /* ** Make sure the TEMP database is open and available for use. Return ** the number of errors. Leave any error messages in the pParse structure. diff --git a/src/main.c b/src/main.c index fd6abb67f2..5cecd7b252 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.519 2008/12/10 23:04:13 drh Exp $ +** $Id: main.c,v 1.520 2008/12/17 17:30:26 danielk1977 Exp $ */ #include "sqliteInt.h" #include @@ -545,6 +545,21 @@ int sqlite3_total_changes(sqlite3 *db){ return db->nTotalChange; } +/* +** Close all open savepoints. This function only manipulates fields of the +** database handle object, it does not close any savepoints that may be open +** at the b-tree/pager level. +*/ +void sqlite3CloseSavepoints(sqlite3 *db){ + while( db->pSavepoint ){ + Savepoint *pTmp = db->pSavepoint; + db->pSavepoint = pTmp->pNext; + sqlite3DbFree(db, pTmp); + } + db->nSavepoint = 0; + db->isTransactionSavepoint = 0; +} + /* ** Close an existing SQLite database */ @@ -587,6 +602,9 @@ int sqlite3_close(sqlite3 *db){ } assert( sqlite3SafetyCheckSickOrOk(db) ); + /* Free any outstanding Savepoint structures. */ + sqlite3CloseSavepoints(db); + for(j=0; jnDb; j++){ struct Db *pDb = &db->aDb[j]; if( pDb->pBt ){ diff --git a/src/pager.c b/src/pager.c index 63d0d1cd55..7743a3f929 100644 --- a/src/pager.c +++ b/src/pager.c @@ -18,7 +18,7 @@ ** file simultaneously, or one process from reading the database while ** another is writing. ** -** @(#) $Id: pager.c,v 1.514 2008/12/10 22:15:00 drh Exp $ +** @(#) $Id: pager.c,v 1.515 2008/12/17 17:30:26 danielk1977 Exp $ */ #ifndef SQLITE_OMIT_DISKIO #include "sqliteInt.h" @@ -140,6 +140,28 @@ # define CODEC2(P,D,N,X) ((char*)D) #endif +/* +** An instance of the following structure is allocated for each active +** savepoint and statement transaction in the system. All such structures +** are stored in the Pager.aSavepoint[] array, which is allocated and +** resized using sqlite3Realloc(). +** +** When a savepoint is created, the PagerSavepoint.iHdrOffset field is +** set to 0. If a journal-header is written into the main journal while +** the savepoint is active, then iHdrOffset is set to the byte offset +** immediately following the last journal record written into the main +** journal before the journal-header. This is required during savepoint +** rollback (see pagerPlaybackSavepoint()). +*/ +typedef struct PagerSavepoint PagerSavepoint; +struct PagerSavepoint { + i64 iOffset; /* Starting offset in main journal */ + i64 iHdrOffset; /* See above */ + Bitvec *pInSavepoint; /* Set of pages in this savepoint */ + Pgno nOrig; /* Original number of pages in file */ + Pgno iSubRec; /* Index of first record in sub-journal */ +}; + /* ** A open page cache is an instance of the following structure. ** @@ -157,9 +179,6 @@ struct Pager { u8 journalStarted; /* True if header of journal is synced */ u8 useJournal; /* Use a rollback journal on this file */ u8 noReadlock; /* Do not bother to obtain readlocks */ - u8 stmtOpen; /* True if the statement subjournal is open */ - u8 stmtInUse; /* True we are in a statement subtransaction */ - u8 stmtAutoopen; /* Open stmt journal when main journal is opened*/ u8 noSync; /* Do not sync the journal if true */ u8 fullSync; /* Do extra syncs of the journal for robustness */ u8 sync_flags; /* One of SYNC_NORMAL or SYNC_FULL */ @@ -181,7 +200,6 @@ struct Pager { int errCode; /* One of several kinds of errors */ Pgno dbSize; /* Number of pages in the file */ Pgno origDbSize; /* dbSize before the current change */ - Pgno stmtSize; /* Size of database (in pages) at stmt_begin() */ int nRec; /* Number of pages written to the journal */ u32 cksumInit; /* Quasi-random value added to every checksum */ int stmtNRec; /* Number of records in stmt subjournal */ @@ -191,20 +209,16 @@ struct Pager { int mxPage; /* Maximum number of pages to hold in cache */ Pgno mxPgno; /* Maximum allowed size of the database */ Bitvec *pInJournal; /* One bit for each page in the database file */ - Bitvec *pInStmt; /* One bit for each page in the database */ Bitvec *pAlwaysRollback; /* One bit for each page marked always-rollback */ char *zFilename; /* Name of the database file */ char *zJournal; /* Name of the journal file */ char *zDirectory; /* Directory hold database and journal files */ sqlite3_file *fd, *jfd; /* File descriptors for database and journal */ - sqlite3_file *stfd; /* File descriptor for the statement subjournal*/ + sqlite3_file *sjfd; /* File descriptor for the sub-journal*/ int (*xBusyHandler)(void*); /* Function to call when busy */ void *pBusyHandlerArg; /* Context argument for xBusyHandler */ i64 journalOff; /* Current byte offset in the journal file */ i64 journalHdr; /* Byte offset to previous journal header */ - i64 stmtHdrOff; /* First journal header written this statement */ - i64 stmtCksum; /* cksumInit when statement was started */ - i64 stmtJSize; /* Size of journal at stmt_begin() */ u32 sectorSize; /* Assumed sector size during rollback */ #ifdef SQLITE_TEST int nHit, nMiss; /* Cache hits and missing */ @@ -219,6 +233,9 @@ struct Pager { char dbFileVers[16]; /* Changes whenever database file changes */ i64 journalSizeLimit; /* Size limit for persistent journal files */ PCache *pPCache; /* Pointer to page cache object */ + + PagerSavepoint *aSavepoint; + int nSavepoint; }; /* @@ -305,13 +322,26 @@ static const unsigned char aJournalMagic[] = { #define PAGER_MAX_PGNO 2147483647 /* -** Return true if page *pPg has already been written to the statement -** journal (or statement snapshot has been created, if *pPg is part -** of an in-memory database). +** Return false if it is necessary to write page *pPg into the sub-journal. +** More accurately, true is returned if either: +** +** * No savepoints are open, or +** * The page has been saved to the sub-journal since the most recent +** savepoint was opened. +** +** TODO: There's a bug here. To do with PagerSavepoint.nOrig. Also consider +** the idea that the page may not be required by the outermost savepoint +** but may be required by some earlier savepoint, due to an incremental +** vacuum operation. */ -static int pageInStatement(PgHdr *pPg){ +static int pageInSavepoint(PgHdr *pPg){ Pager *pPager = pPg->pPager; - return sqlite3BitvecTest(pPager->pInStmt, pPg->pgno); + if( pPager->nSavepoint==0 ){ + return 1; + } + return sqlite3BitvecTest( + pPager->aSavepoint[pPager->nSavepoint-1].pInSavepoint, pPg->pgno + ); } static int pageInJournal(PgHdr *pPg){ @@ -631,13 +661,20 @@ static int writeJournalHdr(Pager *pPager){ char *zHeader = pPager->pTmpSpace; u32 nHeader = pPager->pageSize; u32 nWrite; + int ii; if( nHeader>JOURNAL_HDR_SZ(pPager) ){ nHeader = JOURNAL_HDR_SZ(pPager); } - if( pPager->stmtHdrOff==0 ){ - pPager->stmtHdrOff = pPager->journalOff; + /* If there are active savepoints and any of them were created since the + ** most recent journal header was written, update the PagerSavepoint.iHdrOff + ** fields now. + */ + for(ii=0; iinSavepoint; ii++){ + if( pPager->aSavepoint[ii].iHdrOffset==0 ){ + pPager->aSavepoint[ii].iHdrOffset = pPager->journalOff; + } } seekJournalHdr(pPager); @@ -885,6 +922,31 @@ static void pager_reset(Pager *pPager){ sqlite3PcacheClear(pPager->pPCache); } +static void releaseAllSavepoint(Pager *pPager){ + int ii; + for(ii=0; iinSavepoint; ii++){ + sqlite3BitvecDestroy(pPager->aSavepoint[ii].pInSavepoint); + } + if( !pPager->exclusiveMode ){ + sqlite3OsClose(pPager->sjfd); + } + sqlite3_free(pPager->aSavepoint); + pPager->aSavepoint = 0; + pPager->nSavepoint = 0; +} + +static int addToSavepointBitvecs(Pager *pPager, Pgno pgno){ + int ii; + for(ii=0; iinSavepoint; ii++){ + PagerSavepoint *p = &pPager->aSavepoint[ii]; + if( pgno<=p->nOrig ){ + /* TODO: malloc() failure handling */ + sqlite3BitvecSet(p->pInSavepoint, pgno); + } + } + return SQLITE_OK; +} + /* ** Unlock the database file. ** @@ -921,16 +983,9 @@ static void pager_unlock(Pager *pPager){ if( pPager->errCode ){ if( rc==SQLITE_OK ) pPager->errCode = SQLITE_OK; pager_reset(pPager); - if( pPager->stmtOpen ){ - sqlite3OsClose(pPager->stfd); - sqlite3BitvecDestroy(pPager->pInStmt); - pPager->pInStmt = 0; - } - pPager->stmtOpen = 0; - pPager->stmtInUse = 0; + releaseAllSavepoint(pPager); pPager->journalOff = 0; pPager->journalStarted = 0; - pPager->stmtAutoopen = 0; pPager->origDbSize = 0; } @@ -976,11 +1031,7 @@ static int pager_end_transaction(Pager *pPager, int hasMaster){ if( pPager->statestmtOpen && !pPager->exclusiveMode ){ - sqlite3OsClose(pPager->stfd); - pPager->stmtOpen = 0; - } + releaseAllSavepoint(pPager); if( pPager->journalOpen ){ if( pPager->journalMode==PAGER_JOURNALMODE_MEMORY ){ int isMemoryJournal = sqlite3IsMemJournal(pPager->jfd); @@ -1079,21 +1130,19 @@ static u32 pager_cksum(Pager *pPager, const u8 *aData){ ** checksums - the statement journal does not. */ static int pager_playback_one_page( - Pager *pPager, /* The pager being played back */ - sqlite3_file *jfd, /* The file that is the journal being rolled back */ - i64 offset, /* Offset of the page within the journal */ - int isMainJrnl /* True for main rollback journal. False for Stmt jrnl */ + Pager *pPager, /* The pager being played back */ + int isMainJrnl, /* 1 -> main journal. 0 -> sub-journal. */ + i64 offset, /* Offset of record to playback */ + Bitvec *pDone /* Bitvec of pages already played back */ ){ int rc; PgHdr *pPg; /* An existing page in the cache */ Pgno pgno; /* The page number of a page in journal */ u32 cksum; /* Checksum used for sanity checking */ u8 *aData = (u8 *)pPager->pTmpSpace; /* Temp storage for a page */ + sqlite3_file *jfd = (isMainJrnl ? pPager->jfd : pPager->sjfd); - /* isMainJrnl should be true for the main journal and false for - ** statement journals. Verify that this is always the case - */ - assert( jfd == (isMainJrnl ? pPager->jfd : pPager->stfd) ); + /* The temp storage must be allocated at this point */ assert( aData ); rc = read32bits(jfd, offset, &pgno); @@ -1110,17 +1159,20 @@ static int pager_playback_one_page( if( pgno==0 || pgno==PAGER_MJ_PGNO(pPager) ){ return SQLITE_DONE; } - if( pgno>(unsigned)pPager->dbSize ){ + if( pgno>(Pgno)pPager->dbSize || sqlite3BitvecTest(pDone, pgno) ){ return SQLITE_OK; } if( isMainJrnl ){ rc = read32bits(jfd, offset+pPager->pageSize+4, &cksum); if( rc ) return rc; pPager->journalOff += 4; - if( pager_cksum(pPager, aData)!=cksum ){ + if( !pDone && pager_cksum(pPager, aData)!=cksum ){ return SQLITE_DONE; } } + if( pDone && (rc = sqlite3BitvecSet(pDone, pgno)) ){ + return rc; + } assert( pPager->state==PAGER_RESERVED || pPager->state>=PAGER_EXCLUSIVE ); @@ -1505,7 +1557,7 @@ static int pager_playback(Pager *pPager, int isHot){ /* Copy original pages out of the journal and back into the database file. */ for(u=0; ujfd, pPager->journalOff, 1); + rc = pager_playback_one_page(pPager, 1, pPager->journalOff, 0); if( rc!=SQLITE_OK ){ if( rc==SQLITE_DONE ){ rc = SQLITE_OK; @@ -1549,101 +1601,73 @@ end_playback: } /* -** Playback the statement journal. -** -** This is similar to playing back the transaction journal but with -** a few extra twists. -** -** (1) The number of pages in the database file at the start of -** the statement is stored in pPager->stmtSize, not in the -** journal file itself. -** -** (2) In addition to playing back the statement journal, also -** playback all pages of the transaction journal beginning -** at offset pPager->stmtJSize. +** Playback a savepoint. */ -static int pager_stmt_playback(Pager *pPager){ +static int pagerPlaybackSavepoint(Pager *pPager, PagerSavepoint *pSavepoint){ i64 szJ; /* Size of the full journal */ - i64 hdrOff; - int nRec; /* Number of Records */ - int i; /* Loop counter */ - int rc; + i64 iHdrOff; /* End of first segment of main-journal records */ + Pgno ii; /* Loop counter */ + int rc; /* Return code */ + Bitvec *pDone = 0; /* Bitvec to ensure pages played back only once */ - szJ = pPager->journalOff; - - /* Set hdrOff to be the offset just after the end of the last journal - ** page written before the first journal-header for this statement - ** transaction was written, or the end of the file if no journal - ** header was written. - */ - hdrOff = pPager->stmtHdrOff; - assert( pPager->fullSync || !hdrOff ); - if( !hdrOff ){ - hdrOff = szJ; + /* Allocate a bitvec to use to store the set of pages rolled back */ + if( pSavepoint ){ + pDone = sqlite3BitvecCreate(pSavepoint->nOrig); + if( !pDone ){ + return SQLITE_NOMEM; + } } - - /* Truncate the database back to its original size. + + /* Truncate the database back to the size it was before the + ** savepoint being reverted was opened. */ - rc = pager_truncate(pPager, pPager->stmtSize); + rc = pager_truncate(pPager, pSavepoint?pSavepoint->nOrig:pPager->origDbSize); assert( pPager->state>=PAGER_SHARED ); - /* Figure out how many records are in the statement journal. + /* Now roll back all main journal file records that occur after byte + ** byte offset PagerSavepoint.iOffset that have a page number less than + ** or equal to PagerSavepoint.nOrig. As each record is played back, + ** the corresponding bit in bitvec PagerSavepoint.pInSavepoint is + ** cleared. */ - assert( pPager->stmtInUse && pPager->journalOpen ); - nRec = pPager->stmtNRec; - - /* Copy original pages out of the statement journal and back into the - ** database file. Note that the statement journal omits checksums from - ** each record since power-failure recovery is not important to statement - ** journals. - */ - for(i=0; ipageSize); - rc = pager_playback_one_page(pPager, pPager->stfd, offset, 0); - assert( rc!=SQLITE_DONE ); - if( rc!=SQLITE_OK ) goto end_stmt_playback; + szJ = pPager->journalOff; + if( pSavepoint ){ + iHdrOff = pSavepoint->iHdrOffset ? pSavepoint->iHdrOffset : szJ; + pPager->journalOff = pSavepoint->iOffset; + while( rc==SQLITE_OK && pPager->journalOffjournalOff, pDone); + assert( rc!=SQLITE_DONE ); + } + }else{ + pPager->journalOff = 0; } - - /* Now roll some pages back from the transaction journal. Pager.stmtJSize - ** was the size of the journal file when this statement was started, so - ** everything after that needs to be rolled back, either into the - ** database, the memory cache, or both. - ** - ** If it is not zero, then Pager.stmtHdrOff is the offset to the start - ** of the first journal header written during this statement transaction. - */ - pPager->journalOff = pPager->stmtJSize; - pPager->cksumInit = (int)(pPager->stmtCksum & 0xffffffff); - while( pPager->journalOff < hdrOff ){ - rc = pager_playback_one_page(pPager, pPager->jfd, pPager->journalOff, 1); - assert( rc!=SQLITE_DONE ); - if( rc!=SQLITE_OK ) goto end_stmt_playback; - } - - while( pPager->journalOff < szJ ){ + while( rc==SQLITE_OK && pPager->journalOffjournalOff) / (pPager->pageSize+8)); + nJRec = (szJ - pPager->journalOff) / (pPager->pageSize+8); } - for(i=nJRec-1; i>=0 && pPager->journalOff < szJ; i--){ - rc = pager_playback_one_page(pPager, pPager->jfd, pPager->journalOff, 1); + for(ii=0; rc==SQLITE_OK && iijournalOff, pDone); + assert( rc!=SQLITE_DONE ); + } + } + assert( rc!=SQLITE_OK || pPager->journalOff==szJ ); + + /* Now roll back pages from the sub-journal. */ + if( pSavepoint ){ + for(ii=pSavepoint->iSubRec; rc==SQLITE_OK && iistmtNRec; ii++){ + i64 offset = ii*(4+pPager->pageSize); + rc = pager_playback_one_page(pPager, 0, offset, pDone); assert( rc!=SQLITE_DONE ); - if( rc!=SQLITE_OK ) goto end_stmt_playback; } } - pPager->journalOff = szJ; - -end_stmt_playback: - if( rc==SQLITE_OK) { + sqlite3BitvecDestroy(pDone); + if( rc==SQLITE_OK ){ pPager->journalOff = szJ; - /* pager_reload_cache(pPager); */ } return rc; } @@ -1815,7 +1839,7 @@ int sqlite3PagerOpen( pPtr = ((u8 *)&pPager[1]) + pcacheSize; pPager->vfsFlags = vfsFlags; pPager->fd = (sqlite3_file*)&pPtr[pVfs->szOsFile*0]; - pPager->stfd = (sqlite3_file*)&pPtr[pVfs->szOsFile]; + pPager->sjfd = (sqlite3_file*)&pPtr[pVfs->szOsFile]; pPager->jfd = (sqlite3_file*)&pPtr[pVfs->szOsFile+journalFileSize]; pPager->zFilename = (char*)&pPtr[pVfs->szOsFile+2*journalFileSize]; pPager->zDirectory = &pPager->zFilename[nPathname+1]; @@ -2247,9 +2271,7 @@ int sqlite3PagerClose(Pager *pPager){ } sqlite3BitvecDestroy(pPager->pInJournal); sqlite3BitvecDestroy(pPager->pAlwaysRollback); - if( pPager->stmtOpen ){ - sqlite3OsClose(pPager->stfd); - } + releaseAllSavepoint(pPager); sqlite3OsClose(pPager->fd); /* Temp files are automatically deleted by the OS ** if( pPager->tempFile ){ @@ -2949,6 +2971,18 @@ int sqlite3PagerUnref(DbPage *pPg){ return SQLITE_OK; } +static int openSubJournal(Pager *pPager){ + int rc = SQLITE_OK; + if( pPager->journalOpen && !pPager->sjfd->pMethods ){ + if( pPager->journalMode==PAGER_JOURNALMODE_MEMORY ){ + sqlite3MemJournalOpen(pPager->sjfd); + }else{ + rc = sqlite3PagerOpentemp(pPager, pPager->sjfd, SQLITE_OPEN_SUBJOURNAL); + } + } + return rc; +} + /* ** Create a journal file for pPager. There should already be a RESERVED ** or EXCLUSIVE lock on the database file when this routine is called. @@ -3012,8 +3046,8 @@ static int pager_open_journal(Pager *pPager){ rc = writeJournalHdr(pPager); - if( pPager->stmtAutoopen && rc==SQLITE_OK ){ - rc = sqlite3PagerStmtBegin(pPager); + if( pPager->nSavepoint && rc==SQLITE_OK ){ + rc = openSubJournal(pPager); } if( rc!=SQLITE_OK && rc!=SQLITE_NOMEM && rc!=SQLITE_IOERR_NOMEM ){ rc = pager_end_transaction(pPager, 0); @@ -3155,7 +3189,7 @@ static int pager_write(PgHdr *pPg){ ** to the journal then we can return right away. */ sqlite3PcacheMakeDirty(pPg); - if( pageInJournal(pPg) && (pageInStatement(pPg) || pPager->stmtInUse==0) ){ + if( pageInJournal(pPg) && pageInSavepoint(pPg) ){ pPager->dirtyCache = 1; pPager->dbModified = 1; }else{ @@ -3226,9 +3260,7 @@ static int pager_write(PgHdr *pPg){ if( !pPager->noSync ){ pPg->flags |= PGHDR_NEED_SYNC; } - if( pPager->stmtInUse ){ - sqlite3BitvecSet(pPager->pInStmt, pPg->pgno); - } + addToSavepointBitvecs(pPager, pPg->pgno); }else{ if( !pPager->journalStarted && !pPager->noSync ){ pPg->flags |= PGHDR_NEED_SYNC; @@ -3247,24 +3279,21 @@ static int pager_write(PgHdr *pPg){ ** the statement journal format differs from the standard journal format ** in that it omits the checksums and the header. */ - if( pPager->stmtInUse - && !pageInStatement(pPg) - && pPg->pgno<=pPager->stmtSize - ){ + if( !pageInSavepoint(pPg) ){ i64 offset = pPager->stmtNRec*(4+pPager->pageSize); char *pData2 = CODEC2(pPager, pData, pPg->pgno, 7); assert( pageInJournal(pPg) || pPg->pgno>pPager->origDbSize ); - rc = write32bits(pPager->stfd, offset, pPg->pgno); + rc = write32bits(pPager->sjfd, offset, pPg->pgno); if( rc==SQLITE_OK ){ - rc = sqlite3OsWrite(pPager->stfd, pData2, pPager->pageSize, offset+4); + rc = sqlite3OsWrite(pPager->sjfd, pData2, pPager->pageSize, offset+4); } PAGERTRACE3("STMT-JOURNAL %d page %d\n", PAGERID(pPager), pPg->pgno); if( rc!=SQLITE_OK ){ return rc; } pPager->stmtNRec++; - assert( pPager->pInStmt!=0 ); - sqlite3BitvecSet(pPager->pInStmt, pPg->pgno); + assert( pPager->nSavepoint>0 ); + addToSavepointBitvecs(pPager, pPg->pgno); } } @@ -3429,7 +3458,7 @@ int sqlite3PagerDontWrite(DbPage *pDbPage){ } rc = sqlite3BitvecSet(pPager->pAlwaysRollback, pPg->pgno); - if( rc==SQLITE_OK && (pPg->flags&PGHDR_DIRTY) && !pPager->stmtInUse ){ + if( rc==SQLITE_OK && (pPg->flags&PGHDR_DIRTY) && pPager->nSavepoint==0 ){ assert( pPager->state>=PAGER_SHARED ); if( pPager->dbSize==pPg->pgno && pPager->origDbSizedbSize ){ /* If this pages is the last page in the file and the file has grown @@ -3501,10 +3530,7 @@ void sqlite3PagerDontRollback(DbPage *pPg){ assert( pPager->pInJournal!=0 ); sqlite3BitvecSet(pPager->pInJournal, pPg->pgno); pPg->flags &= ~PGHDR_NEED_READ; - if( pPager->stmtInUse ){ - assert( pPager->stmtSize >= pPager->origDbSize ); - sqlite3BitvecSet(pPager->pInStmt, pPg->pgno); - } + addToSavepointBitvecs(pPager, pPg->pgno); PAGERTRACE3("DONT_ROLLBACK page %d of %d\n", pPg->pgno, PAGERID(pPager)); IOTRACE(("GARBAGE %p %d\n", pPager, pPg->pgno)) } @@ -3869,92 +3895,92 @@ int sqlite3PagerIsMemdb(Pager *pPager){ #endif /* -** Set the statement rollback point. -** -** This routine should be called with the transaction journal already -** open. A new statement journal is created that can be used to rollback -** changes of a single SQL command within a larger transaction. +** Ensure that there are at least nSavepoint savepoints open. */ -static int pagerStmtBegin(Pager *pPager){ - int rc; - assert( !pPager->stmtInUse ); - assert( pPager->state>=PAGER_SHARED ); - assert( pPager->dbSizeValid ); - PAGERTRACE2("STMT-BEGIN %d\n", PAGERID(pPager)); - if( !pPager->journalOpen ){ - pPager->stmtAutoopen = 1; - return SQLITE_OK; - } - assert( pPager->journalOpen ); - assert( pPager->pInStmt==0 ); - pPager->pInStmt = sqlite3BitvecCreate(pPager->dbSize); - if( pPager->pInStmt==0 ){ - /* sqlite3OsLock(pPager->fd, SHARED_LOCK); */ - return SQLITE_NOMEM; - } - pPager->stmtJSize = pPager->journalOff; - pPager->stmtSize = pPager->dbSize; - pPager->stmtHdrOff = 0; - pPager->stmtCksum = pPager->cksumInit; - if( !pPager->stmtOpen ){ - if( pPager->journalMode==PAGER_JOURNALMODE_MEMORY ){ - sqlite3MemJournalOpen(pPager->stfd); - }else{ - rc = sqlite3PagerOpentemp(pPager, pPager->stfd, SQLITE_OPEN_SUBJOURNAL); - if( rc ){ - goto stmt_begin_failed; +int sqlite3PagerOpenSavepoint(Pager *pPager, int nSavepoint){ + int rc = SQLITE_OK; + + if( nSavepoint>pPager->nSavepoint ){ + int ii; + + /* Either the sub-journal is open or there are no active savepoints. */ + assert( pPager->nSavepoint==0 || pPager->sjfd->pMethods ); + + /* Grow the Pager.aSavepoint array using realloc(). Return SQLITE_NOMEM + ** if the allocation fails. Otherwise, zero the new portion in case a + ** malloc failure occurs while populating it in the for(...) loop below. + */ + PagerSavepoint *aNew = (PagerSavepoint *)sqlite3Realloc( + pPager->aSavepoint, sizeof(PagerSavepoint)*nSavepoint + ); + if( !aNew ){ + return SQLITE_NOMEM; + } + memset(&aNew[pPager->nSavepoint], 0, + (nSavepoint - pPager->nSavepoint) * sizeof(PagerSavepoint) + ); + pPager->aSavepoint = aNew; + ii = pPager->nSavepoint; + pPager->nSavepoint = nSavepoint; + + /* Populate the PagerSavepoint structures just allocated. */ + for(/* no-op */; iidbSize>=0 ); + aNew[ii].nOrig = pPager->dbSize; + aNew[ii].iOffset = (pPager->journalOpen ? pPager->journalOff : 0); + aNew[ii].iSubRec = pPager->stmtNRec; + aNew[ii].pInSavepoint = sqlite3BitvecCreate(pPager->dbSize); + if( !aNew[ii].pInSavepoint ){ + return SQLITE_NOMEM; } } - pPager->stmtOpen = 1; - pPager->stmtNRec = 0; + + /* Open the sub-journal, if it is not already opened. */ + rc = openSubJournal(pPager); } - pPager->stmtInUse = 1; - return SQLITE_OK; - -stmt_begin_failed: - if( pPager->pInStmt ){ - sqlite3BitvecDestroy(pPager->pInStmt); - pPager->pInStmt = 0; - } - return rc; -} -int sqlite3PagerStmtBegin(Pager *pPager){ - int rc; - rc = pagerStmtBegin(pPager); + return rc; } /* -** Commit a statement. -*/ -int sqlite3PagerStmtCommit(Pager *pPager){ - if( pPager->stmtInUse ){ - PAGERTRACE2("STMT-COMMIT %d\n", PAGERID(pPager)); - sqlite3BitvecDestroy(pPager->pInStmt); - pPager->pInStmt = 0; - pPager->stmtNRec = 0; - pPager->stmtInUse = 0; - if( sqlite3IsMemJournal(pPager->stfd) ){ - sqlite3OsTruncate(pPager->stfd, 0); +** Parameter op is always either SAVEPOINT_ROLLBACK or SAVEPOINT_RELEASE. +** If it is SAVEPOINT_RELEASE, then release and destroy the savepoint with +** index iSavepoint. If it is SAVEPOINT_ROLLBACK, then rollback all changes +** that have occured since savepoint iSavepoint was created. +** +** In either case, all savepoints with an index greater than iSavepoint +** are destroyed. +** +** If there are less than (iSavepoint+1) active savepoints when this +** function is called it is a no-op. +*/ +int sqlite3PagerSavepoint(Pager *pPager, int op, int iSavepoint){ + int rc = SQLITE_OK; + + assert( op==SAVEPOINT_RELEASE || op==SAVEPOINT_ROLLBACK ); + + if( iSavepointnSavepoint ){ + int ii; + int nNew = iSavepoint + (op==SAVEPOINT_ROLLBACK); + for(ii=nNew; iinSavepoint; ii++){ + sqlite3BitvecDestroy(pPager->aSavepoint[ii].pInSavepoint); + } + pPager->nSavepoint = nNew; + + if( op==SAVEPOINT_ROLLBACK ){ + PagerSavepoint *pSavepoint = (nNew==0) ? 0 : &pPager->aSavepoint[nNew-1]; + rc = pagerPlaybackSavepoint(pPager, pSavepoint); + assert(rc!=SQLITE_DONE); + } + + /* If this is a release of the outermost savepoint, truncate + ** the sub-journal. */ + if( nNew==0 && op==SAVEPOINT_RELEASE && pPager->sjfd->pMethods ){ + assert( rc==SQLITE_OK ); + rc = sqlite3OsTruncate(pPager->sjfd, 0); + pPager->stmtNRec = 0; } } - pPager->stmtAutoopen = 0; - return SQLITE_OK; -} - -/* -** Rollback a statement. -*/ -int sqlite3PagerStmtRollback(Pager *pPager){ - int rc; - if( pPager->stmtInUse ){ - PAGERTRACE2("STMT-ROLLBACK %d\n", PAGERID(pPager)); - rc = pager_stmt_playback(pPager); - sqlite3PagerStmtCommit(pPager); - }else{ - rc = SQLITE_OK; - } - pPager->stmtAutoopen = 0; return rc; } diff --git a/src/pager.h b/src/pager.h index 76d7067565..e69e7df1fc 100644 --- a/src/pager.h +++ b/src/pager.h @@ -13,7 +13,7 @@ ** subsystem. The page cache subsystem reads and writes a file a page ** at a time and provides a journal for rollback. ** -** @(#) $Id: pager.h,v 1.88 2008/12/10 16:45:51 drh Exp $ +** @(#) $Id: pager.h,v 1.89 2008/12/17 17:30:26 danielk1977 Exp $ */ #ifndef _PAGER_H_ @@ -116,6 +116,9 @@ i64 sqlite3PagerJournalSizeLimit(Pager *, i64); void *sqlite3PagerTempSpace(Pager*); int sqlite3PagerSync(Pager *pPager); +int sqlite3PagerOpenSavepoint(Pager *pPager, int n); +int sqlite3PagerSavepoint(Pager *pPager, int op, int iSavepoint); + #ifdef SQLITE_HAS_CODEC void sqlite3PagerSetCodec(Pager*,void*(*)(void*,void*,Pgno,int),void*); #endif diff --git a/src/parse.y b/src/parse.y index e6788b3b78..149281df96 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.265 2008/12/10 18:03:46 drh Exp $ +** @(#) $Id: parse.y,v 1.266 2008/12/17 17:30:26 danielk1977 Exp $ */ // All token codes are small integers with #defines that begin with "TK_" @@ -118,6 +118,18 @@ cmd ::= COMMIT trans_opt. {sqlite3CommitTransaction(pParse);} cmd ::= END trans_opt. {sqlite3CommitTransaction(pParse);} cmd ::= ROLLBACK trans_opt. {sqlite3RollbackTransaction(pParse);} +savepoint_opt ::= SAVEPOINT. +savepoint_opt ::= . +cmd ::= SAVEPOINT nm(X). { + sqlite3Savepoint(pParse, SAVEPOINT_BEGIN, &X); +} +cmd ::= RELEASE savepoint_opt nm(X). { + sqlite3Savepoint(pParse, SAVEPOINT_RELEASE, &X); +} +cmd ::= ROLLBACK trans_opt TO savepoint_opt nm(X). { + sqlite3Savepoint(pParse, SAVEPOINT_ROLLBACK, &X); +} + ///////////////////// The CREATE TABLE statement //////////////////////////// // cmd ::= create_table create_table_args. diff --git a/src/sqliteInt.h b/src/sqliteInt.h index ae4b8d1487..c3177afa22 100644 --- a/src/sqliteInt.h +++ b/src/sqliteInt.h @@ -11,7 +11,7 @@ ************************************************************************* ** Internal interface definitions for SQLite. ** -** @(#) $Id: sqliteInt.h,v 1.809 2008/12/10 21:19:57 drh Exp $ +** @(#) $Id: sqliteInt.h,v 1.810 2008/12/17 17:30:26 danielk1977 Exp $ */ #ifndef _SQLITEINT_H_ #define _SQLITEINT_H_ @@ -522,6 +522,7 @@ typedef struct LookasideSlot LookasideSlot; typedef struct Module Module; typedef struct NameContext NameContext; typedef struct Parse Parse; +typedef struct Savepoint Savepoint; typedef struct Select Select; typedef struct SrcList SrcList; typedef struct StrAccum StrAccum; @@ -765,6 +766,9 @@ struct sqlite3 { #ifdef SQLITE_SSE sqlite3_stmt *pFetch; /* Used by SSE to fetch stored statements */ #endif + Savepoint *pSavepoint; /* List of active savepoints */ + int nSavepoint; /* Number of non-transaction savepoints */ + u8 isTransactionSavepoint; /* True if the outermost savepoint is a TS */ }; /* @@ -876,6 +880,25 @@ struct FuncDef { #define AGGREGATE(zName, nArg, arg, nc, xStep, xFinal) \ {nArg, SQLITE_UTF8, nc*8, SQLITE_INT_TO_PTR(arg), 0, 0, xStep,xFinal,#zName,0} +/* +** All current savepoints are stored in a linked list starting at +** sqlite3.pSavepoint. The first element in the list is the most recently +** opened savepoint. Savepoints are added to the list by the vdbe +** OP_Savepoint instruction. +*/ +struct Savepoint { + char *zName; /* Savepoint name (nul-terminated) */ + Savepoint *pNext; /* Parent savepoint (if any) */ +}; + +/* +** The following are used as the second parameter to sqlite3Savepoint(), +** and as the P1 argument to the OP_Savepoint instruction. +*/ +#define SAVEPOINT_BEGIN 0 +#define SAVEPOINT_RELEASE 1 +#define SAVEPOINT_ROLLBACK 2 + /* ** Each SQLite module (virtual table definition) is defined by an @@ -2249,6 +2272,8 @@ void sqlite3CodeVerifySchema(Parse*, int); void sqlite3BeginTransaction(Parse*, int); void sqlite3CommitTransaction(Parse*); void sqlite3RollbackTransaction(Parse*); +void sqlite3Savepoint(Parse*, int, Token*); +void sqlite3CloseSavepoints(sqlite3 *); int sqlite3ExprIsConstant(Expr*); int sqlite3ExprIsConstantNotJoin(Expr*); int sqlite3ExprIsConstantOrFunction(Expr*); diff --git a/src/test2.c b/src/test2.c index 083cc13a47..1111ebba50 100644 --- a/src/test2.c +++ b/src/test2.c @@ -13,7 +13,7 @@ ** is not included in the SQLite library. It is used for automated ** testing of the SQLite library. ** -** $Id: test2.c,v 1.62 2008/09/29 11:49:48 danielk1977 Exp $ +** $Id: test2.c,v 1.63 2008/12/17 17:30:26 danielk1977 Exp $ */ #include "sqliteInt.h" #include "tcl.h" @@ -197,7 +197,7 @@ static int pager_stmt_begin( return TCL_ERROR; } pPager = sqlite3TestTextToPtr(argv[1]); - rc = sqlite3PagerStmtBegin(pPager); + rc = sqlite3PagerOpenSavepoint(pPager, 1); if( rc!=SQLITE_OK ){ Tcl_AppendResult(interp, errorName(rc), 0); return TCL_ERROR; @@ -224,7 +224,8 @@ static int pager_stmt_rollback( return TCL_ERROR; } pPager = sqlite3TestTextToPtr(argv[1]); - rc = sqlite3PagerStmtRollback(pPager); + rc = sqlite3PagerSavepoint(pPager, SAVEPOINT_ROLLBACK, 0); + sqlite3PagerSavepoint(pPager, SAVEPOINT_RELEASE, 0); if( rc!=SQLITE_OK ){ Tcl_AppendResult(interp, errorName(rc), 0); return TCL_ERROR; @@ -251,7 +252,7 @@ static int pager_stmt_commit( return TCL_ERROR; } pPager = sqlite3TestTextToPtr(argv[1]); - rc = sqlite3PagerStmtCommit(pPager); + rc = sqlite3PagerSavepoint(pPager, SAVEPOINT_RELEASE, 0); if( rc!=SQLITE_OK ){ Tcl_AppendResult(interp, errorName(rc), 0); return TCL_ERROR; diff --git a/src/vdbe.c b/src/vdbe.c index 2bc26d3b38..22cc31fd1f 100644 --- a/src/vdbe.c +++ b/src/vdbe.c @@ -43,7 +43,7 @@ ** in this file for details. If in doubt, do not deviate from existing ** commenting and indentation practices when changing or adding code. ** -** $Id: vdbe.c,v 1.803 2008/12/15 15:27:52 drh Exp $ +** $Id: vdbe.c,v 1.804 2008/12/17 17:30:26 danielk1977 Exp $ */ #include "sqliteInt.h" #include @@ -499,6 +499,26 @@ static int fileExists(sqlite3 *db, const char *zFile){ } #endif +#ifndef NDEBUG +/* +** This function is only called from within an assert() expression. It +** checks that the sqlite3.nTransaction variable is correctly set to +** the number of non-transaction savepoints currently in the +** linked list starting at sqlite3.pSavepoint. +** +** Usage: +** +** assert( checkSavepointCount(db) ); +*/ +static int checkSavepointCount(sqlite3 *db){ + int n = 0; + Savepoint *p; + for(p=db->pSavepoint; p; p=p->pNext) n++; + assert( n==(db->nSavepoint + db->isTransactionSavepoint) ); + return 1; +} +#endif + /* ** Execute as much of a VDBE program as we can then return. ** @@ -2356,6 +2376,141 @@ case OP_Statement: { break; } +/* Opcode: Savepoint P1 * * P4 * +** +** Open, release or rollback the savepoint named by parameter P4, depending +** on the value of P1. To open a new savepoint, P1==0. To release (commit) an +** existing savepoint, P1==1, or to rollback an existing savepoint P1==2. +*/ +case OP_Savepoint: { + int p1 = pOp->p1; + char *zName = pOp->p4.z; /* Name of savepoint */ + + /* Assert that the p1 parameter is valid. Also that if there is no open + ** transaction, then there cannot be any savepoints. + */ + assert( db->pSavepoint==0 || db->autoCommit==0 ); + assert( p1==SAVEPOINT_BEGIN||p1==SAVEPOINT_RELEASE||p1==SAVEPOINT_ROLLBACK ); + assert( db->pSavepoint || db->isTransactionSavepoint==0 ); + assert( checkSavepointCount(db) ); + + if( p1==SAVEPOINT_BEGIN ){ + if( db->writeVdbeCnt>1 ){ + /* A new savepoint cannot be created if there are active write + ** statements (i.e. open read/write incremental blob handles). + */ + sqlite3SetString(&p->zErrMsg, db, "cannot open savepoint - " + "SQL statements in progress"); + rc = SQLITE_BUSY; + }else{ + int nName = sqlite3Strlen30(zName); + Savepoint *pNew; + + /* Create a new savepoint structure. */ + pNew = sqlite3DbMallocRaw(db, sizeof(Savepoint)+nName+1); + if( pNew ){ + pNew->zName = (char *)&pNew[1]; + memcpy(pNew->zName, zName, nName+1); + + /* If there is no open transaction, then mark this as a special + ** "transaction savepoint". */ + if( db->autoCommit ){ + db->autoCommit = 0; + db->isTransactionSavepoint = 1; + }else{ + db->nSavepoint++; + } + + /* Link the new savepoint into the database handle's list. */ + pNew->pNext = db->pSavepoint; + db->pSavepoint = pNew; + } + } + }else{ + Savepoint *pSavepoint; + int iSavepoint = 0; + + /* Find the named savepoint. If there is no such savepoint, then an + ** an error is returned to the user. */ + for( + pSavepoint=db->pSavepoint; + pSavepoint && sqlite3StrICmp(pSavepoint->zName, zName); + pSavepoint=pSavepoint->pNext + ){ + iSavepoint++; + } + if( !pSavepoint ){ + sqlite3SetString(&p->zErrMsg, db, "no such savepoint: %s", zName); + rc = SQLITE_ERROR; + }else if( + db->writeVdbeCnt>0 || (p1==SAVEPOINT_ROLLBACK && db->activeVdbeCnt>1) + ){ + /* It is not possible to release (commit) a savepoint if there are + ** active write statements. It is not possible to rollback a savepoint + ** if there are any active statements at all. + */ + sqlite3SetString(&p->zErrMsg, db, + "cannot %s savepoint - SQL statements in progress", + (p1==SAVEPOINT_ROLLBACK ? "rollback": "release") + ); + rc = SQLITE_BUSY; + }else{ + + /* Determine whether or not this is a transaction savepoint. If so, + ** operate on the currently open transaction. If this is a RELEASE + ** command, then the transaction is committed. If it is a ROLLBACK + ** command, then all changes made by the current transaction are + ** reverted, but the transaction is not actually closed. + */ + int isTransaction = pSavepoint->pNext==0 && db->isTransactionSavepoint; + if( isTransaction && p1==SAVEPOINT_RELEASE ){ + db->isTransactionSavepoint = 0; + db->autoCommit = 1; + if( sqlite3VdbeHalt(p)==SQLITE_BUSY ){ + p->pc = pc; + db->autoCommit = 0; + p->rc = rc = SQLITE_BUSY; + goto vdbe_return; + } + }else{ + int ii; + iSavepoint = db->nSavepoint - iSavepoint - 1; + for(ii=0; iinDb; ii++){ + rc = sqlite3BtreeSavepoint(db->aDb[ii].pBt, p1, iSavepoint); + if( rc!=SQLITE_OK ){ + goto abort_due_to_error; + } + } + if( p1==SAVEPOINT_ROLLBACK && db->flags&SQLITE_InternChanges ){ + sqlite3ExpirePreparedStatements(db); + sqlite3ResetInternalSchema(db, 0); + } + } + + /* Regardless of whether this is a RELEASE or ROLLBACK, destroy all + ** savepoints nested inside of the savepoint being operated on. */ + while( db->pSavepoint!=pSavepoint ){ + Savepoint *pTmp = db->pSavepoint; + db->pSavepoint = pTmp->pNext; + sqlite3DbFree(db, pTmp); + db->nSavepoint--; + } + + /* If it is a RELEASE, then destroy the savepoint being operated on too */ + if( p1==SAVEPOINT_RELEASE ){ + assert( pSavepoint==db->pSavepoint ); + db->pSavepoint = pSavepoint->pNext; + sqlite3DbFree(db, pSavepoint); + if( !isTransaction ){ + db->nSavepoint--; + } + } + } + } + + break; +} + /* Opcode: AutoCommit P1 P2 * * * ** ** Set the database auto-commit flag to P1 (1 or 0). If P2 is true, roll @@ -2390,7 +2545,7 @@ case OP_AutoCommit: { "SQL statements in progress"); rc = SQLITE_BUSY; }else if( desiredAutoCommit!=db->autoCommit ){ - if( pOp->p2 ){ + if( rollback ){ assert( desiredAutoCommit==1 ); sqlite3RollbackAll(db); db->autoCommit = 1; @@ -2403,6 +2558,7 @@ case OP_AutoCommit: { goto vdbe_return; } } + sqlite3CloseSavepoints(db); if( p->rc==SQLITE_OK ){ rc = SQLITE_DONE; }else{ diff --git a/test/savepoint.test b/test/savepoint.test new file mode 100644 index 0000000000..289709dbf8 --- /dev/null +++ b/test/savepoint.test @@ -0,0 +1,269 @@ +# 2008 December 15 +# +# 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. +# +#*********************************************************************** +# +# $Id: savepoint.test,v 1.1 2008/12/17 17:30:26 danielk1977 Exp $ + +set testdir [file dirname $argv0] +source $testdir/tester.tcl + + +#---------------------------------------------------------------------- +# The following tests - savepoint-1.* - test that the SAVEPOINT, RELEASE +# and ROLLBACK TO comands are correctly parsed, and that the auto-commit +# flag is correctly set and unset as a result. +# +do_test savepoint-1.1 { + execsql { + SAVEPOINT sp1; + RELEASE sp1; + } +} {} +do_test savepoint-1.2 { + execsql { + SAVEPOINT sp1; + ROLLBACK TO sp1; + } +} {} +do_test savepoint-1.3 { + execsql { SAVEPOINT sp1 } + db close +} {} +sqlite3 db test.db +do_test savepoint-1.4.1 { + execsql { + SAVEPOINT sp1; + SAVEPOINT sp2; + RELEASE sp1; + } + sqlite3_get_autocommit db +} {1} +do_test savepoint-1.4.2 { + execsql { + SAVEPOINT sp1; + SAVEPOINT sp2; + RELEASE sp2; + } + sqlite3_get_autocommit db +} {0} +do_test savepoint-1.4.3 { + execsql { RELEASE sp1 } + sqlite3_get_autocommit db +} {1} +do_test savepoint-1.4.4 { + execsql { + SAVEPOINT sp1; + SAVEPOINT sp2; + ROLLBACK TO sp1; + } + sqlite3_get_autocommit db +} {0} +do_test savepoint-1.4.5 { + execsql { RELEASE SAVEPOINT sp1 } + sqlite3_get_autocommit db +} {1} +do_test savepoint-1.4.6 { + execsql { + SAVEPOINT sp1; + SAVEPOINT sp2; + SAVEPOINT sp3; + ROLLBACK TO SAVEPOINT sp3; + ROLLBACK TRANSACTION TO sp2; + ROLLBACK TRANSACTION TO SAVEPOINT sp1; + } + sqlite3_get_autocommit db +} {0} +do_test savepoint-1.4.7 { + execsql { RELEASE SAVEPOINT SP1 } + sqlite3_get_autocommit db +} {1} +do_test savepoint-1.5 { + execsql { + SAVEPOINT sp1; + ROLLBACK TO sp1; + } +} {} +do_test savepoint-1.6 { + execsql COMMIT +} {} + +#------------------------------------------------------------------------ +# These tests - savepoint-2.* - test rollbacks and releases of savepoints +# with a very simple data set. +# + +do_test savepoint-2.1 { + execsql { + CREATE TABLE t1(a, b, c); + BEGIN; + INSERT INTO t1 VALUES(1, 2, 3); + SAVEPOINT one; + UPDATE t1 SET a = 2, b = 3, c = 4; + } + execsql { SELECT * FROM t1 } +} {2 3 4} +do_test savepoint-2.2 { + execsql { + ROLLBACK TO one; + } + execsql { SELECT * FROM t1 } +} {1 2 3} +do_test savepoint-2.3 { + execsql { + INSERT INTO t1 VALUES(4, 5, 6); + } + execsql { SELECT * FROM t1 } +} {1 2 3 4 5 6} +do_test savepoint-2.4 { + execsql { + ROLLBACK TO one; + } + execsql { SELECT * FROM t1 } +} {1 2 3} + + +do_test savepoint-2.5 { + execsql { + INSERT INTO t1 VALUES(7, 8, 9); + SAVEPOINT two; + INSERT INTO t1 VALUES(10, 11, 12); + } + execsql { SELECT * FROM t1 } +} {1 2 3 7 8 9 10 11 12} +do_test savepoint-2.6 { + execsql { + ROLLBACK TO two; + } + execsql { SELECT * FROM t1 } +} {1 2 3 7 8 9} +do_test savepoint-2.7 { + execsql { + INSERT INTO t1 VALUES(10, 11, 12); + } + execsql { SELECT * FROM t1 } +} {1 2 3 7 8 9 10 11 12} +do_test savepoint-2.8 { + execsql { + ROLLBACK TO one; + } + execsql { SELECT * FROM t1 } +} {1 2 3} +do_test savepoint-2.9 { + execsql { + INSERT INTO t1 VALUES('a', 'b', 'c'); + SAVEPOINT two; + INSERT INTO t1 VALUES('d', 'e', 'f'); + } + execsql { SELECT * FROM t1 } +} {1 2 3 a b c d e f} +do_test savepoint-2.10 { + execsql { + RELEASE two; + } + execsql { SELECT * FROM t1 } +} {1 2 3 a b c d e f} +do_test savepoint-2.11 { + execsql { + ROLLBACK; + } + execsql { SELECT * FROM t1 } +} {} + +#------------------------------------------------------------------------ +# This block of tests - savepoint-3.* - test that when a transaction +# savepoint is rolled back, locks are not released from database files. +# And that when a transaction savepoint is released, they are released. +# +do_test savepoint-3.1 { + execsql { SAVEPOINT "transaction" } + execsql { PRAGMA lock_status } +} {main unlocked temp closed} + +do_test savepoint-3.2 { + execsql { INSERT INTO t1 VALUES(1, 2, 3) } + execsql { PRAGMA lock_status } +} {main reserved temp closed} + +do_test savepoint-3.3 { + execsql { ROLLBACK TO "transaction" } + execsql { PRAGMA lock_status } +} {main reserved temp closed} + +do_test savepoint-3.4 { + execsql { INSERT INTO t1 VALUES(1, 2, 3) } + execsql { PRAGMA lock_status } +} {main reserved temp closed} + +do_test savepoint-3.5 { + execsql { RELEASE "transaction" } + execsql { PRAGMA lock_status } +} {main unlocked temp closed} + +#------------------------------------------------------------------------ +# Test that savepoints that include schema modifications are handled +# correctly. Test cases savepoint-4.*. +# +do_test savepoint-4.1 { + execsql { + CREATE TABLE t2(d, e, f); + SELECT sql FROM sqlite_master; + } +} {{CREATE TABLE t1(a, b, c)} {CREATE TABLE t2(d, e, f)}} +do_test savepoint-4.2 { + execsql { + BEGIN; + CREATE TABLE t3(g,h); + INSERT INTO t3 VALUES('I', 'II'); + SAVEPOINT one; + DROP TABLE t3; + } +} {} +do_test savepoint-4.3 { + execsql { + CREATE TABLE t3(g, h, i); + INSERT INTO t3 VALUES('III', 'IV', 'V'); + } + execsql {SELECT * FROM t3} +} {III IV V} +do_test savepoint-4.4 { + execsql { ROLLBACK TO one; } + execsql {SELECT * FROM t3} +} {I II} +do_test savepoint-4.5 { + execsql { + ROLLBACK; + SELECT sql FROM sqlite_master; + } +} {{CREATE TABLE t1(a, b, c)} {CREATE TABLE t2(d, e, f)}} + +do_test savepoint-4.6 { + execsql { + BEGIN; + INSERT INTO t1 VALUES('o', 't', 't'); + SAVEPOINT sp1; + CREATE TABLE t3(a, b, c); + INSERT INTO t3 VALUES('z', 'y', 'x'); + } + execsql {SELECT * FROM t3} +} {z y x} +do_test savepoint-4.7 { + execsql { + ROLLBACK TO sp1; + CREATE TABLE t3(a); + INSERT INTO t3 VALUES('value'); + } + execsql {SELECT * FROM t3} +} {value} +do_test savepoint-4.8 { + execsql COMMIT +} {} + +finish_test + diff --git a/tool/mkkeywordhash.c b/tool/mkkeywordhash.c index d397cb9f24..3ef12e201c 100644 --- a/tool/mkkeywordhash.c +++ b/tool/mkkeywordhash.c @@ -15,7 +15,7 @@ static const char zHdr[] = "**\n" "** The code in this file has been automatically generated by\n" "**\n" - "** $Header: /home/drh/sqlite/trans/cvs/sqlite/sqlite/tool/mkkeywordhash.c,v 1.34 2008/12/10 20:11:01 shane Exp $\n" + "** $Header: /home/drh/sqlite/trans/cvs/sqlite/sqlite/tool/mkkeywordhash.c,v 1.35 2008/12/17 17:30:26 danielk1977 Exp $\n" "**\n" "** The code in this file implements a function that determines whether\n" "** or not a given identifier is really an SQL keyword. The same thing\n" @@ -233,12 +233,14 @@ static Keyword aKeywordTable[] = { { "REFERENCES", "TK_REFERENCES", FKEY }, { "REGEXP", "TK_LIKE_KW", ALWAYS }, { "REINDEX", "TK_REINDEX", REINDEX }, + { "RELEASE", "TK_RELEASE", ALWAYS }, { "RENAME", "TK_RENAME", ALTER }, { "REPLACE", "TK_REPLACE", CONFLICT }, { "RESTRICT", "TK_RESTRICT", FKEY }, { "RIGHT", "TK_JOIN_KW", ALWAYS }, { "ROLLBACK", "TK_ROLLBACK", ALWAYS }, { "ROW", "TK_ROW", TRIGGER }, + { "SAVEPOINT", "TK_SAVEPOINT", ALWAYS }, { "SELECT", "TK_SELECT", ALWAYS }, { "SET", "TK_SET", ALWAYS }, { "TABLE", "TK_TABLE", ALWAYS },