diff --git a/manifest b/manifest index 2ac6ff316f..04d3534b4d 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Fix\sa\smemory\sallocation\sbug\sintroduced\slast\sweek\sby\scheck-in\n[a9e819082ba].\s\sThe\sbug\sonly\sappears\son\ssystems\swhere\sthe\ssize\sof\sa\sstructure\nis\snot\salways\sa\smultiple\sof\s8\s-\swhich\sin\spractice\smeans\sonly\son\s32-bit\s\nwindows\ssystems. -D 2015-12-11T04:11:17.453 +C Add\sthe\sexperimental\ssnapshot\sinterface.\s\sBecause\sit\sis\sexperimental,\sit\sis\nsubject\sto\schange\sor\sremoval\sat\sa\slater\sdate. +D 2015-12-11T12:44:52.505 F Makefile.in 28bcd6149e050dff35d4dcfd97e890cd387a499d F Makefile.linux-gcc 7bc79876b875010e8c8f9502eb935ca92aa3c434 F Makefile.msc e8fdca1cb89a1b58b5f4d3a130ea9a3d28cb314d @@ -304,7 +304,7 @@ F src/insert.c e1d20ae8979e25519c2670233718676bedcfedc9 F src/journal.c b4124532212b6952f42eb2c12fa3c25701d8ba8d F src/legacy.c ba1863ea58c4c840335a84ec276fc2b25e22bc4e F src/loadext.c 84996d7d70a605597d79c1f1d7b2012a5fd34f2b -F src/main.c a950e48920e8c0f0ff82b2b2ccfe11aa89ca11d4 +F src/main.c 2f33510a6a392c606fe64964e695e55ad124242f F src/malloc.c 8f787669e79de26efc42272b5797bc00fff527c6 F src/mem0.c 6a55ebe57c46ca1a7d98da93aaa07f99f1059645 F src/mem1.c 6919bcf12f221868ea066eec27e579fed95ce98b @@ -326,8 +326,8 @@ F src/os_setup.h c9d4553b5aaa6f73391448b265b89bed0b890faa F src/os_unix.c 0ca6d8710366fbb01a275160f018334cd347cbda F src/os_win.c 386fba30419e8458b13209781c2af5590eab2811 F src/os_win.h eb7a47aa17b26b77eb97e4823f20a00b8bda12ca -F src/pager.c f92aacd5216d8815136c9e0190041783c602641a -F src/pager.h 9153c71a89dc82a5a77e485f3929792116c70aae +F src/pager.c 58d2593612acb6b542de6715b4af397ea1fa0a35 +F src/pager.h bf25005b4656cd805af43487c3139fca9678d0cc F src/parse.y 23737e649c26ce327603799e57f5c2ff50e5e6ba F src/pcache.c 73895411fa6b7bd6f0091212feabbe833b358d23 F src/pcache.h 1ff11adce609ba7de139b6abfabaf9a2bac947b5 @@ -341,7 +341,7 @@ F src/resolve.c a83b41104e6ff69855d03cd0aaa09e93927ec39f F src/rowset.c eccf6af6d620aaa4579bd3b72c1b6395d9e9fa1e F src/select.c f8fded11fc443a9f5a73cc5db069d06b34460e2f F src/shell.c abbc74ea43dbf2f306ea18282d666683fb5efab2 -F src/sqlite.h.in 1248a78548024bdc8ef5893faa0ff9552b4cceb4 +F src/sqlite.h.in 7d87d71b9a4689c51fa092f48f16590ff71558e3 F src/sqlite3.rc 992c9f5fb8285ae285d6be28240a7e8d3a7f2bad F src/sqlite3ext.h dfbe62ffd95b99afe2140d8c35b180d11924072d F src/sqliteInt.h beb4a63b94428f52a3d7c7af2ba8bdc7d4682a03 @@ -349,7 +349,7 @@ F src/sqliteLimit.h 216557999cb45f2e3578ed53ebefe228d779cb46 F src/status.c 70912d7be68e9e2dbc4010c93d344af61d4c59ba F src/table.c 51b46b2a62d1b3a959633d593b89bab5e2c9155e F src/tclsqlite.c d9439b6a910985b7fff43ba6756bcef00de22649 -F src/test1.c 90c4e80c7f4b0ad82c6dbe699c8f430b641769f5 +F src/test1.c 4f1b42699068b7806af3111786f5ad760c2c1ff7 F src/test2.c 5586f43fcd9a1be0830793cf9d354082c261b25b F src/test3.c a8887dabbbee3059af338f20d290084a63ed1b0f F src/test4.c d168f83cc78d02e8d35567bb5630e40dcd85ac1e @@ -363,7 +363,7 @@ F src/test_autoext.c dea8a01a7153b9adc97bd26161e4226329546e12 F src/test_backup.c 2e6e6a081870150f20c526a2e9d0d29cda47d803 F src/test_blob.c e5a7a81d61a780da79101aeb1e60d300af169e07 F src/test_btree.c 2e9978eca99a9a4bfa8cae949efb00886860a64f -F src/test_config.c 48850687dd5abc8260e23835632511054ccae172 +F src/test_config.c 0dee90328e3dedf8ba002ee94b6a7e7ea7726fe4 F src/test_demovfs.c 0de72c2c89551629f58486fde5734b7d90758852 F src/test_devsym.c e7498904e72ba7491d142d5c83b476c4e76993bc F src/test_fs.c 993c7eab65bed6add4bb48cca29775e963f710cf @@ -415,8 +415,8 @@ F src/vdbesort.c a7ec02da4494c59dfd071126dd3726be5a11459d F src/vdbetrace.c 8befe829faff6d9e6f6e4dee5a7d3f85cc85f1a0 F src/vtab.c 2a8b44aa372c33f6154208e7a7f6c44254549806 F src/vxworks.h c18586c8edc1bddbc15c004fa16aeb1e1342b4fb -F src/wal.c 1569802364cd192bbd5c4a8ea3fd6de593edecbd -F src/wal.h df01efe09c5cb8c8e391ff1715cca294f89668a4 +F src/wal.c cb709aa42fc11b1ea92a00c8c7e6214de0995fa3 +F src/wal.h 907943dfdef10b583e81906679a347e0ec6f1b1b F src/walker.c 2e14d17f592d176b6dc879c33fbdec4fbccaa2ba F src/where.c b18edbb9e5afabb77f4f27550c471c5c824e0fe7 F src/whereInt.h e20801d89e34de1912bb6a3babb30c390da27add @@ -1020,6 +1020,8 @@ F test/skipscan2.test d1d1450952b7275f0b0a3a981f0230532743951a F test/skipscan3.test ec5bab3f81c7038b43450e7b3062e04a198bdbb5 F test/skipscan5.test 67817a4b6857c47e0e33ba3e506da6f23ef68de2 F test/skipscan6.test 5866039d03a56f5bd0b3d172a012074a1d90a15b +F test/snapshot.test efc6b4edc5d571161835f9dd8552e181ad1f0ac2 +F test/snapshot_fault.test 25973aeb1b86a280800e0bcf1eb5ce70e9ef57ab F test/soak.test 0b5b6375c9f4110c828070b826b3b4b0bb65cd5f F test/softheap1.test 843cd84db9891b2d01b9ab64cef3e9020f98d087 F test/sort.test 3f492e5b7be1d3f756728d2ff6edf4f6091e84cb @@ -1408,7 +1410,8 @@ F tool/vdbe_profile.tcl 246d0da094856d72d2c12efec03250d71639d19f F tool/warnings-clang.sh f6aa929dc20ef1f856af04a730772f59283631d4 F tool/warnings.sh 48bd54594752d5be3337f12c72f28d2080cb630b F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f -P 05bc4f920ce23da48d1da6cd36a956fd6fd7c862 -R bb8ff31fbee90cb95998ef6668cbcbeb +P 96d3e99ffe59b5280b8b7fc269c94a72e15c345b 843c15a552657ca43ff200edb7da7566752d7941 +R 1e8a48e65f87836435758a04b37e3926 +T +closed 843c15a552657ca43ff200edb7da7566752d7941 U drh -Z dbc67560cb8111f117bbc951b94851d2 +Z f796eddbea3599c0fdbed9d3e73688d6 diff --git a/manifest.uuid b/manifest.uuid index 1ada9da49b..bcf7d5193b 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -96d3e99ffe59b5280b8b7fc269c94a72e15c345b \ No newline at end of file +9b124a5a310aec0145495c9980bc0c1308fcca02 \ No newline at end of file diff --git a/src/main.c b/src/main.c index d552f7fbc8..5b2130511e 100644 --- a/src/main.c +++ b/src/main.c @@ -3866,3 +3866,86 @@ int sqlite3_db_readonly(sqlite3 *db, const char *zDbName){ pBt = sqlite3DbNameToBtree(db, zDbName); return pBt ? sqlite3BtreeIsReadonly(pBt) : -1; } + +#ifdef SQLITE_ENABLE_SNAPSHOT +/* +** Obtain a snapshot handle for the snapshot of database zDb currently +** being read by handle db. +*/ +int sqlite3_snapshot_get( + sqlite3 *db, + const char *zDb, + sqlite3_snapshot **ppSnapshot +){ + int rc = SQLITE_ERROR; +#ifndef SQLITE_OMIT_WAL + int iDb; + +#ifdef SQLITE_ENABLE_API_ARMOR + if( !sqlite3SafetyCheckOk(db) ){ + return SQLITE_MISUSE_BKPT; + } +#endif + sqlite3_mutex_enter(db->mutex); + + iDb = sqlite3FindDbName(db, zDb); + if( iDb==0 || iDb>1 ){ + Btree *pBt = db->aDb[iDb].pBt; + if( 0==sqlite3BtreeIsInTrans(pBt) ){ + rc = sqlite3BtreeBeginTrans(pBt, 0); + if( rc==SQLITE_OK ){ + rc = sqlite3PagerSnapshotGet(sqlite3BtreePager(pBt), ppSnapshot); + } + } + } + + sqlite3_mutex_leave(db->mutex); +#endif /* SQLITE_OMIT_WAL */ + return rc; +} + +/* +** Open a read-transaction on the snapshot idendified by pSnapshot. +*/ +int sqlite3_snapshot_open( + sqlite3 *db, + const char *zDb, + sqlite3_snapshot *pSnapshot +){ + int rc = SQLITE_ERROR; +#ifndef SQLITE_OMIT_WAL + +#ifdef SQLITE_ENABLE_API_ARMOR + if( !sqlite3SafetyCheckOk(db) ){ + return SQLITE_MISUSE_BKPT; + } +#endif + sqlite3_mutex_enter(db->mutex); + if( db->autoCommit==0 ){ + int iDb; + iDb = sqlite3FindDbName(db, zDb); + if( iDb==0 || iDb>1 ){ + Btree *pBt = db->aDb[iDb].pBt; + if( 0==sqlite3BtreeIsInReadTrans(pBt) ){ + rc = sqlite3PagerSnapshotOpen(sqlite3BtreePager(pBt), pSnapshot); + if( rc==SQLITE_OK ){ + rc = sqlite3BtreeBeginTrans(pBt, 0); + sqlite3PagerSnapshotOpen(sqlite3BtreePager(pBt), 0); + } + } + } + } + + sqlite3_mutex_leave(db->mutex); +#endif /* SQLITE_OMIT_WAL */ + return rc; +} + +/* +** Free a snapshot handle obtained from sqlite3_snapshot_get(). +*/ +void sqlite3_snapshot_free(sqlite3_snapshot *pSnapshot){ + sqlite3_free(pSnapshot); +} +#endif /* SQLITE_ENABLE_SNAPSHOT */ + diff --git a/src/pager.c b/src/pager.c index bf74eac549..2c8dceb750 100644 --- a/src/pager.c +++ b/src/pager.c @@ -7301,6 +7301,34 @@ int sqlite3PagerCloseWal(Pager *pPager){ return rc; } +#ifdef SQLITE_ENABLE_SNAPSHOT +/* +** If this is a WAL database, obtain a snapshot handle for the snapshot +** currently open. Otherwise, return an error. +*/ +int sqlite3PagerSnapshotGet(Pager *pPager, sqlite3_snapshot **ppSnapshot){ + int rc = SQLITE_ERROR; + if( pPager->pWal ){ + rc = sqlite3WalSnapshotGet(pPager->pWal, ppSnapshot); + } + return rc; +} + +/* +** If this is a WAL database, store a pointer to pSnapshot. Next time a +** read transaction is opened, attempt to read from the snapshot it +** identifies. If this is not a WAL database, return an error. +*/ +int sqlite3PagerSnapshotOpen(Pager *pPager, sqlite3_snapshot *pSnapshot){ + int rc = SQLITE_OK; + if( pPager->pWal ){ + sqlite3WalSnapshotOpen(pPager->pWal, pSnapshot); + }else{ + rc = SQLITE_ERROR; + } + return rc; +} +#endif /* SQLITE_ENABLE_SNAPSHOT */ #endif /* !SQLITE_OMIT_WAL */ #ifdef SQLITE_ENABLE_ZIPVFS diff --git a/src/pager.h b/src/pager.h index cf9cda625d..ba4eec438d 100644 --- a/src/pager.h +++ b/src/pager.h @@ -168,6 +168,10 @@ int sqlite3PagerSharedLock(Pager *pPager); int sqlite3PagerWalCallback(Pager *pPager); int sqlite3PagerOpenWal(Pager *pPager, int *pisOpen); int sqlite3PagerCloseWal(Pager *pPager); +# ifdef SQLITE_ENABLE_SNAPSHOT + int sqlite3PagerSnapshotGet(Pager *pPager, sqlite3_snapshot **ppSnapshot); + int sqlite3PagerSnapshotOpen(Pager *pPager, sqlite3_snapshot *pSnapshot); +# endif #endif #ifdef SQLITE_ENABLE_ZIPVFS diff --git a/src/sqlite.h.in b/src/sqlite.h.in index e797571062..be58c7cd1f 100644 --- a/src/sqlite.h.in +++ b/src/sqlite.h.in @@ -4405,8 +4405,8 @@ unsigned int sqlite3_value_subtype(sqlite3_value*); ** previously obtained from [sqlite3_value_dup()]. ^If V is a NULL pointer ** then sqlite3_value_free(V) is a harmless no-op. */ -SQLITE_EXPERIMENTAL sqlite3_value *sqlite3_value_dup(const sqlite3_value*); -SQLITE_EXPERIMENTAL void sqlite3_value_free(sqlite3_value*); +sqlite3_value *sqlite3_value_dup(const sqlite3_value*); +void sqlite3_value_free(sqlite3_value*); /* ** CAPI3REF: Obtain Aggregate Function Context @@ -7851,33 +7851,127 @@ void sqlite3_stmt_scanstatus_reset(sqlite3_stmt*); /* ** CAPI3REF: Flush caches to disk mid-transaction ** -** If a write-transaction is open when this function is called, any dirty +** ^If a write-transaction is open on [database connection] D when the +** [sqlite3_db_cacheflush(D)] interface invoked, any dirty ** pages in the pager-cache that are not currently in use are written out ** to disk. A dirty page may be in use if a database cursor created by an ** active SQL statement is reading from it, or if it is page 1 of a database -** file (page 1 is always "in use"). Dirty pages are flushed for all -** databases - "main", "temp" and any attached databases. +** file (page 1 is always "in use"). ^The [sqlite3_db_cacheflush(D)] +** interface flushes caches for all schemas - "main", "temp", and +** any [attached] databases. ** -** If this function needs to obtain extra database locks before dirty pages -** can be flushed to disk, it does so. If said locks cannot be obtained +** ^If this function needs to obtain extra database locks before dirty pages +** can be flushed to disk, it does so. ^If those locks cannot be obtained ** immediately and there is a busy-handler callback configured, it is invoked -** in the usual manner. If the required lock still cannot be obtained, then +** in the usual manner. ^If the required lock still cannot be obtained, then ** the database is skipped and an attempt made to flush any dirty pages -** belonging to the next (if any) database. If any databases are skipped +** belonging to the next (if any) database. ^If any databases are skipped ** because locks cannot be obtained, but no other error occurs, this ** function returns SQLITE_BUSY. ** -** If any other error occurs while flushing dirty pages to disk (for +** ^If any other error occurs while flushing dirty pages to disk (for ** example an IO error or out-of-memory condition), then processing is -** abandoned and an SQLite error code returned to the caller immediately. +** abandoned and an SQLite [error code] is returned to the caller immediately. ** -** Otherwise, if no error occurs, SQLITE_OK is returned. +** ^Otherwise, if no error occurs, [sqlite3_db_cacheflush()] returns SQLITE_OK. ** -** This function does not set the database handle error code or message -** returned by the sqlite3_errcode() and sqlite3_errmsg() functions. +** ^This function does not set the database handle error code or message +** returned by the [sqlite3_errcode()] and [sqlite3_errmsg()] functions. */ int sqlite3_db_cacheflush(sqlite3*); +/* +** CAPI3REF: Database Snapshot +** KEYWORDS: {snapshot} +** EXPERIMENTAL +** +** An instance of the snapshot object records the state of a [WAL mode] +** database for some specific point in history. +** +** In [WAL mode], multiple [database connections] that are open on the +** same database file can each be reading a different historical version +** of the database file. When a [database connection] begins a read +** transaction, that connection sees an unchanging copy of the database +** as it existed for the point in time when the transaction first started. +** Subsequent changes to the database from other connections are not seen +** by the reader until a new read transaction is started. +** +** The sqlite3_snapshot object records state information about an historical +** version of the database file so that it is possible to later open a new read +** transaction that sees that historical version of the database rather than +** the most recent version. +** +** The constructor for this object is [sqlite3_snapshot_get()]. The +** [sqlite3_snapshot_open()] method causes a fresh read transaction to refer +** to an historical snapshot (if possible). The destructor for +** sqlite3_snapshot objects is [sqlite3_snapshot_free()]. +*/ +typedef struct sqlite3_snapshot sqlite3_snapshot; + +/* +** CAPI3REF: Record A Database Snapshot +** EXPERIMENTAL +** +** ^The [sqlite3_snapshot_get(D,S,P)] interface attempts to make a +** new [sqlite3_snapshot] object that records the current state of +** schema S in database connection D. ^On success, the +** [sqlite3_snapshot_get(D,S,P)] interface writes a pointer to the newly +** created [sqlite3_snapshot] object into *P and returns SQLITE_OK. +** ^If schema S of [database connection] D is not a [WAL mode] database +** that is in a read transaction, then [sqlite3_snapshot_get(D,S,P)] +** leaves the *P value unchanged and returns an appropriate [error code]. +** +** The [sqlite3_snapshot] object returned from a successful call to +** [sqlite3_snapshot_get()] must be freed using [sqlite3_snapshot_free()] +** to avoid a memory leak. +** +** The [sqlite3_snapshot_get()] interface is only available when the +** SQLITE_ENABLE_SNAPSHOT compile-time option is used. +*/ +SQLITE_EXPERIMENTAL int sqlite3_snapshot_get( + sqlite3 *db, + const char *zSchema, + sqlite3_snapshot **ppSnapshot +); + +/* +** CAPI3REF: Start a read transaction on an historical snapshot +** EXPERIMENTAL +** +** ^The [sqlite3_snapshot_open(D,S,P)] interface attempts to move the +** read transaction that is currently open on schema S of +** [database connection] D so that it refers to historical [snapshot] P. +** ^The [sqlite3_snapshot_open()] interface returns SQLITE_OK on success +** or an appropriate [error code] if it fails. +** +** ^In order to succeed, a call to [sqlite3_snapshot_open(D,S,P)] must be +** the first operation, apart from other sqlite3_snapshot_open() calls, +** following the [BEGIN] that starts a new read transaction. +** ^A [snapshot] will fail to open if it has been overwritten by a +** [checkpoint]. +** +** The [sqlite3_snapshot_open()] interface is only available when the +** SQLITE_ENABLE_SNAPSHOT compile-time option is used. +*/ +SQLITE_EXPERIMENTAL int sqlite3_snapshot_open( + sqlite3 *db, + const char *zSchema, + sqlite3_snapshot *pSnapshot +); + +/* +** CAPI3REF: Destroy a snapshot +** EXPERIMENTAL +** +** ^The [sqlite3_snapshot_free(P)] interface destroys [sqlite3_snapshot] P. +** The application must eventually free every [sqlite3_snapshot] object +** using this routine to avoid a memory leak. +** +** The [sqlite3_snapshot_free()] interface is only available when the +** SQLITE_ENABLE_SNAPSHOT compile-time option is used. +*/ +SQLITE_EXPERIMENTAL void sqlite3_snapshot_free(sqlite3_snapshot*); + /* ** Undo the hack that converts floating point types to integer for ** builds on processors without floating point support. diff --git a/src/test1.c b/src/test1.c index b8154a64fe..c5e71582bc 100644 --- a/src/test1.c +++ b/src/test1.c @@ -2269,6 +2269,94 @@ static int vfsCurrentTimeInt64( return TCL_OK; } +#ifdef SQLITE_ENABLE_SNAPSHOT +/* +** Usage: sqlite3_snapshot_get DB DBNAME +*/ +static int test_snapshot_get( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + int rc; + sqlite3 *db; + char *zName; + sqlite3_snapshot *pSnapshot = 0; + + if( objc!=3 ){ + Tcl_WrongNumArgs(interp, 1, objv, "DB DBNAME"); + return TCL_ERROR; + } + if( getDbPointer(interp, Tcl_GetString(objv[1]), &db) ) return TCL_ERROR; + zName = Tcl_GetString(objv[2]); + + rc = sqlite3_snapshot_get(db, zName, &pSnapshot); + if( rc!=SQLITE_OK ){ + Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1)); + return TCL_ERROR; + }else{ + char zBuf[100]; + if( sqlite3TestMakePointerStr(interp, zBuf, pSnapshot) ) return TCL_ERROR; + Tcl_SetObjResult(interp, Tcl_NewStringObj(zBuf, -1)); + } + return TCL_OK; +} +#endif /* SQLITE_ENABLE_SNAPSHOT */ + +#ifdef SQLITE_ENABLE_SNAPSHOT +/* +** Usage: sqlite3_snapshot_open DB DBNAME SNAPSHOT +*/ +static int test_snapshot_open( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + int rc; + sqlite3 *db; + char *zName; + sqlite3_snapshot *pSnapshot; + + if( objc!=4 ){ + Tcl_WrongNumArgs(interp, 1, objv, "DB DBNAME SNAPSHOT"); + return TCL_ERROR; + } + if( getDbPointer(interp, Tcl_GetString(objv[1]), &db) ) return TCL_ERROR; + zName = Tcl_GetString(objv[2]); + pSnapshot = (sqlite3_snapshot*)sqlite3TestTextToPtr(Tcl_GetString(objv[3])); + + rc = sqlite3_snapshot_open(db, zName, pSnapshot); + if( rc!=SQLITE_OK ){ + Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1)); + return TCL_ERROR; + } + return TCL_OK; +} +#endif /* SQLITE_ENABLE_SNAPSHOT */ + +#ifdef SQLITE_ENABLE_SNAPSHOT +/* +** Usage: sqlite3_snapshot_free SNAPSHOT +*/ +static int test_snapshot_free( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + sqlite3_snapshot *pSnapshot; + if( objc!=2 ){ + Tcl_WrongNumArgs(interp, 1, objv, "SNAPSHOT"); + return TCL_ERROR; + } + pSnapshot = (sqlite3_snapshot*)sqlite3TestTextToPtr(Tcl_GetString(objv[1])); + sqlite3_snapshot_free(pSnapshot); + return TCL_OK; +} +#endif /* SQLITE_ENABLE_SNAPSHOT */ + /* ** Usage: sqlite3_next_stmt DB STMT ** @@ -7083,6 +7171,11 @@ int Sqlitetest1_Init(Tcl_Interp *interp){ { "sqlite3_config_sqllog", test_config_sqllog, 0 }, #endif { "vfs_current_time_int64", vfsCurrentTimeInt64, 0 }, +#ifdef SQLITE_ENABLE_SNAPSHOT + { "sqlite3_snapshot_get", test_snapshot_get, 0 }, + { "sqlite3_snapshot_open", test_snapshot_open, 0 }, + { "sqlite3_snapshot_free", test_snapshot_free, 0 }, +#endif }; static int bitmask_size = sizeof(Bitmask)*8; static int longdouble_size = sizeof(LONGDOUBLE_TYPE); diff --git a/src/test_config.c b/src/test_config.c index a9ef182e02..30b421e00b 100644 --- a/src/test_config.c +++ b/src/test_config.c @@ -143,6 +143,12 @@ static void set_options(Tcl_Interp *interp){ Tcl_SetVar2(interp, "sqlite_options", "mem5", "0", TCL_GLOBAL_ONLY); #endif +#ifdef SQLITE_ENABLE_SNAPSHOT + Tcl_SetVar2(interp, "sqlite_options", "snapshot", "1", TCL_GLOBAL_ONLY); +#else + Tcl_SetVar2(interp, "sqlite_options", "snapshot", "0", TCL_GLOBAL_ONLY); +#endif + #ifdef SQLITE_MUTEX_OMIT Tcl_SetVar2(interp, "sqlite_options", "mutex", "0", TCL_GLOBAL_ONLY); #else diff --git a/src/wal.c b/src/wal.c index 144db27a30..e4d2cf544c 100644 --- a/src/wal.c +++ b/src/wal.c @@ -272,7 +272,8 @@ int sqlite3WalTrace = 0; /* ** Indices of various locking bytes. WAL_NREADER is the number -** of available reader locks and should be at least 3. +** of available reader locks and should be at least 3. The default +** is SQLITE_SHM_NLOCK==8 and WAL_NREADER==5. */ #define WAL_WRITE_LOCK 0 #define WAL_ALL_BUT_WRITE 1 @@ -292,7 +293,10 @@ typedef struct WalCkptInfo WalCkptInfo; ** The following object holds a copy of the wal-index header content. ** ** The actual header in the wal-index consists of two copies of this -** object. +** object followed by one instance of the WalCkptInfo object. +** For all versions of SQLite through 3.10.0 and probably beyond, +** the locking bytes (WalCkptInfo.aLock) start at offset 120 and +** the total header size is 136 bytes. ** ** The szPage value can be any power of 2 between 512 and 32768, inclusive. ** Or it can be 1 to represent a 65536-byte page. The latter case was @@ -325,6 +329,16 @@ struct WalIndexHdr { ** However, a WAL_WRITE_LOCK thread can move the value of nBackfill from ** mxFrame back to zero when the WAL is reset. ** +** nBackfillAttempted is the largest value of nBackfill that a checkpoint +** has attempted to achieve. Normally nBackfill==nBackfillAtempted, however +** the nBackfillAttempted is set before any backfilling is done and the +** nBackfill is only set after all backfilling completes. So if a checkpoint +** crashes, nBackfillAttempted might be larger than nBackfill. The +** WalIndexHdr.mxFrame must never be less than nBackfillAttempted. +** +** The aLock[] field is a set of bytes used for locking. These bytes should +** never be read or written. +** ** There is one entry in aReadMark[] for each reader lock. If a reader ** holds read-lock K, then the value in aReadMark[K] is no greater than ** the mxFrame for that reader. The value READMARK_NOT_USED (0xffffffff) @@ -364,6 +378,9 @@ struct WalIndexHdr { struct WalCkptInfo { u32 nBackfill; /* Number of WAL frames backfilled into DB */ u32 aReadMark[WAL_NREADER]; /* Reader marks */ + u8 aLock[SQLITE_SHM_NLOCK]; /* Reserved space for locks */ + u32 nBackfillAttempted; /* WAL frames perhaps written, or maybe not */ + u32 notUsed0; /* Available for future enhancements */ }; #define READMARK_NOT_USED 0xffffffff @@ -373,9 +390,8 @@ struct WalCkptInfo { ** only support mandatory file-locks, we do not read or write data ** from the region of the file on which locks are applied. */ -#define WALINDEX_LOCK_OFFSET (sizeof(WalIndexHdr)*2 + sizeof(WalCkptInfo)) -#define WALINDEX_LOCK_RESERVED 16 -#define WALINDEX_HDR_SIZE (WALINDEX_LOCK_OFFSET+WALINDEX_LOCK_RESERVED) +#define WALINDEX_LOCK_OFFSET (sizeof(WalIndexHdr)*2+offsetof(WalCkptInfo,aLock)) +#define WALINDEX_HDR_SIZE (sizeof(WalIndexHdr)*2+sizeof(WalCkptInfo)) /* Size of header before each frame in wal */ #define WAL_FRAME_HDRSIZE 24 @@ -434,6 +450,9 @@ struct Wal { #ifdef SQLITE_DEBUG u8 lockError; /* True if a locking error has occurred */ #endif +#ifdef SQLITE_ENABLE_SNAPSHOT + WalIndexHdr *pSnapshot; /* Start transaction here if not NULL */ +#endif }; /* @@ -1198,6 +1217,7 @@ finished: */ pInfo = walCkptInfo(pWal); pInfo->nBackfill = 0; + pInfo->nBackfillAttempted = pWal->hdr.mxFrame; pInfo->aReadMark[0] = 0; for(i=1; iaReadMark[i] = READMARK_NOT_USED; if( pWal->hdr.mxFrame ) pInfo->aReadMark[1] = pWal->hdr.mxFrame; @@ -1269,7 +1289,11 @@ int sqlite3WalOpen( /* In the amalgamation, the os_unix.c and os_win.c source files come before ** this source file. Verify that the #defines of the locking byte offsets ** in os_unix.c and os_win.c agree with the WALINDEX_LOCK_OFFSET value. + ** For that matter, if the lock offset ever changes from its initial design + ** value of 120, we need to know that so there is an assert() to check it. */ + assert( 120==WALINDEX_LOCK_OFFSET ); + assert( 136==WALINDEX_HDR_SIZE ); #ifdef WIN_SHM_BASE assert( WIN_SHM_BASE==WALINDEX_LOCK_OFFSET ); #endif @@ -1655,6 +1679,7 @@ static void walRestartHdr(Wal *pWal, u32 salt1){ memcpy(&pWal->hdr.aSalt[1], &salt1, 4); walIndexWriteHdr(pWal); pInfo->nBackfill = 0; + pInfo->nBackfillAttempted = 0; pInfo->aReadMark[1] = 0; for(i=2; iaReadMark[i] = READMARK_NOT_USED; assert( pInfo->aReadMark[0]==0 ); @@ -1764,6 +1789,8 @@ static int walCheckpoint( i64 nSize; /* Current size of database file */ u32 nBackfill = pInfo->nBackfill; + pInfo->nBackfillAttempted = mxSafeFrame; + /* Sync the WAL to disk */ if( sync_flags ){ rc = sqlite3OsSync(pWal->pWalFd, sync_flags); @@ -2147,6 +2174,7 @@ static int walTryBeginRead(Wal *pWal, int *pChanged, int useWal, int cnt){ int mxI; /* Index of largest aReadMark[] value */ int i; /* Loop counter */ int rc = SQLITE_OK; /* Return code */ + u32 mxFrame; /* Wal frame to lock to */ assert( pWal->readLock<0 ); /* Not currently locked */ @@ -2210,7 +2238,12 @@ static int walTryBeginRead(Wal *pWal, int *pChanged, int useWal, int cnt){ } pInfo = walCkptInfo(pWal); - if( !useWal && pInfo->nBackfill==pWal->hdr.mxFrame ){ + if( !useWal && pInfo->nBackfill==pWal->hdr.mxFrame +#ifdef SQLITE_ENABLE_SNAPSHOT + && (pWal->pSnapshot==0 || pWal->hdr.mxFrame==0 + || 0==memcmp(&pWal->hdr, pWal->pSnapshot, sizeof(WalIndexHdr))) +#endif + ){ /* The WAL has been completely backfilled (or it is empty). ** and can be safely ignored. */ @@ -2248,85 +2281,88 @@ static int walTryBeginRead(Wal *pWal, int *pChanged, int useWal, int cnt){ */ mxReadMark = 0; mxI = 0; + mxFrame = pWal->hdr.mxFrame; +#ifdef SQLITE_ENABLE_SNAPSHOT + if( pWal->pSnapshot && pWal->pSnapshot->mxFramepSnapshot->mxFrame; + } +#endif for(i=1; iaReadMark[i]; - if( mxReadMark<=thisMark && thisMark<=pWal->hdr.mxFrame ){ + if( mxReadMark<=thisMark && thisMark<=mxFrame ){ assert( thisMark!=READMARK_NOT_USED ); mxReadMark = thisMark; mxI = i; } } - /* There was once an "if" here. The extra "{" is to preserve indentation. */ - { - if( (pWal->readOnly & WAL_SHM_RDONLY)==0 - && (mxReadMarkhdr.mxFrame || mxI==0) - ){ - for(i=1; iaReadMark[i] = pWal->hdr.mxFrame; - mxI = i; - walUnlockExclusive(pWal, WAL_READ_LOCK(i), 1); - break; - }else if( rc!=SQLITE_BUSY ){ - return rc; - } + if( (pWal->readOnly & WAL_SHM_RDONLY)==0 + && (mxReadMarkaReadMark[i] = mxFrame; + mxI = i; + walUnlockExclusive(pWal, WAL_READ_LOCK(i), 1); + break; + }else if( rc!=SQLITE_BUSY ){ + return rc; } } - if( mxI==0 ){ - assert( rc==SQLITE_BUSY || (pWal->readOnly & WAL_SHM_RDONLY)!=0 ); - return rc==SQLITE_BUSY ? WAL_RETRY : SQLITE_READONLY_CANTLOCK; - } + } + if( mxI==0 ){ + assert( rc==SQLITE_BUSY || (pWal->readOnly & WAL_SHM_RDONLY)!=0 ); + return rc==SQLITE_BUSY ? WAL_RETRY : SQLITE_READONLY_CANTLOCK; + } - rc = walLockShared(pWal, WAL_READ_LOCK(mxI)); - if( rc ){ - return rc==SQLITE_BUSY ? WAL_RETRY : rc; - } - /* Now that the read-lock has been obtained, check that neither the - ** value in the aReadMark[] array or the contents of the wal-index - ** header have changed. - ** - ** It is necessary to check that the wal-index header did not change - ** between the time it was read and when the shared-lock was obtained - ** on WAL_READ_LOCK(mxI) was obtained to account for the possibility - ** that the log file may have been wrapped by a writer, or that frames - ** that occur later in the log than pWal->hdr.mxFrame may have been - ** copied into the database by a checkpointer. If either of these things - ** happened, then reading the database with the current value of - ** pWal->hdr.mxFrame risks reading a corrupted snapshot. So, retry - ** instead. - ** - ** Before checking that the live wal-index header has not changed - ** since it was read, set Wal.minFrame to the first frame in the wal - ** file that has not yet been checkpointed. This client will not need - ** to read any frames earlier than minFrame from the wal file - they - ** can be safely read directly from the database file. - ** - ** Because a ShmBarrier() call is made between taking the copy of - ** nBackfill and checking that the wal-header in shared-memory still - ** matches the one cached in pWal->hdr, it is guaranteed that the - ** checkpointer that set nBackfill was not working with a wal-index - ** header newer than that cached in pWal->hdr. If it were, that could - ** cause a problem. The checkpointer could omit to checkpoint - ** a version of page X that lies before pWal->minFrame (call that version - ** A) on the basis that there is a newer version (version B) of the same - ** page later in the wal file. But if version B happens to like past - ** frame pWal->hdr.mxFrame - then the client would incorrectly assume - ** that it can read version A from the database file. However, since - ** we can guarantee that the checkpointer that set nBackfill could not - ** see any pages past pWal->hdr.mxFrame, this problem does not come up. - */ - pWal->minFrame = pInfo->nBackfill+1; - walShmBarrier(pWal); - if( pInfo->aReadMark[mxI]!=mxReadMark - || memcmp((void *)walIndexHdr(pWal), &pWal->hdr, sizeof(WalIndexHdr)) - ){ - walUnlockShared(pWal, WAL_READ_LOCK(mxI)); - return WAL_RETRY; - }else{ - assert( mxReadMark<=pWal->hdr.mxFrame ); - pWal->readLock = (i16)mxI; - } + rc = walLockShared(pWal, WAL_READ_LOCK(mxI)); + if( rc ){ + return rc==SQLITE_BUSY ? WAL_RETRY : rc; + } + /* Now that the read-lock has been obtained, check that neither the + ** value in the aReadMark[] array or the contents of the wal-index + ** header have changed. + ** + ** It is necessary to check that the wal-index header did not change + ** between the time it was read and when the shared-lock was obtained + ** on WAL_READ_LOCK(mxI) was obtained to account for the possibility + ** that the log file may have been wrapped by a writer, or that frames + ** that occur later in the log than pWal->hdr.mxFrame may have been + ** copied into the database by a checkpointer. If either of these things + ** happened, then reading the database with the current value of + ** pWal->hdr.mxFrame risks reading a corrupted snapshot. So, retry + ** instead. + ** + ** Before checking that the live wal-index header has not changed + ** since it was read, set Wal.minFrame to the first frame in the wal + ** file that has not yet been checkpointed. This client will not need + ** to read any frames earlier than minFrame from the wal file - they + ** can be safely read directly from the database file. + ** + ** Because a ShmBarrier() call is made between taking the copy of + ** nBackfill and checking that the wal-header in shared-memory still + ** matches the one cached in pWal->hdr, it is guaranteed that the + ** checkpointer that set nBackfill was not working with a wal-index + ** header newer than that cached in pWal->hdr. If it were, that could + ** cause a problem. The checkpointer could omit to checkpoint + ** a version of page X that lies before pWal->minFrame (call that version + ** A) on the basis that there is a newer version (version B) of the same + ** page later in the wal file. But if version B happens to like past + ** frame pWal->hdr.mxFrame - then the client would incorrectly assume + ** that it can read version A from the database file. However, since + ** we can guarantee that the checkpointer that set nBackfill could not + ** see any pages past pWal->hdr.mxFrame, this problem does not come up. + */ + pWal->minFrame = pInfo->nBackfill+1; + walShmBarrier(pWal); + if( pInfo->aReadMark[mxI]!=mxReadMark + || memcmp((void *)walIndexHdr(pWal), &pWal->hdr, sizeof(WalIndexHdr)) + ){ + walUnlockShared(pWal, WAL_READ_LOCK(mxI)); + return WAL_RETRY; + }else{ + assert( mxReadMark<=pWal->hdr.mxFrame ); + pWal->readLock = (i16)mxI; } return rc; } @@ -2349,6 +2385,14 @@ int sqlite3WalBeginReadTransaction(Wal *pWal, int *pChanged){ int rc; /* Return code */ int cnt = 0; /* Number of TryBeginRead attempts */ +#ifdef SQLITE_ENABLE_SNAPSHOT + int bChanged = 0; + WalIndexHdr *pSnapshot = pWal->pSnapshot; + if( pSnapshot && memcmp(pSnapshot, &pWal->hdr, sizeof(WalIndexHdr))!=0 ){ + bChanged = 1; + } +#endif + do{ rc = walTryBeginRead(pWal, pChanged, 0, ++cnt); }while( rc==WAL_RETRY ); @@ -2356,6 +2400,65 @@ int sqlite3WalBeginReadTransaction(Wal *pWal, int *pChanged){ testcase( (rc&0xff)==SQLITE_IOERR ); testcase( rc==SQLITE_PROTOCOL ); testcase( rc==SQLITE_OK ); + +#ifdef SQLITE_ENABLE_SNAPSHOT + if( rc==SQLITE_OK ){ + if( pSnapshot && memcmp(pSnapshot, &pWal->hdr, sizeof(WalIndexHdr))!=0 ){ + /* At this point the client has a lock on an aReadMark[] slot holding + ** a value equal to or smaller than pSnapshot->mxFrame, but pWal->hdr + ** is populated with the wal-index header corresponding to the head + ** of the wal file. Verify that pSnapshot is still valid before + ** continuing. Reasons why pSnapshot might no longer be valid: + ** + ** (1) The WAL file has been reset since the snapshot was taken. + ** In this case, the salt will have changed. + ** + ** (2) A checkpoint as been attempted that wrote frames past + ** pSnapshot->mxFrame into the database file. Note that the + ** checkpoint need not have completed for this to cause problems. + */ + volatile WalCkptInfo *pInfo = walCkptInfo(pWal); + + assert( pWal->readLock>0 || pWal->hdr.mxFrame==0 ); + assert( pInfo->aReadMark[pWal->readLock]<=pSnapshot->mxFrame ); + + /* It is possible that there is a checkpointer thread running + ** concurrent with this code. If this is the case, it may be that the + ** checkpointer has already determined that it will checkpoint + ** snapshot X, where X is later in the wal file than pSnapshot, but + ** has not yet set the pInfo->nBackfillAttempted variable to indicate + ** its intent. To avoid the race condition this leads to, ensure that + ** there is no checkpointer process by taking a shared CKPT lock + ** before checking pInfo->nBackfillAttempted. */ + rc = walLockShared(pWal, WAL_CKPT_LOCK); + + if( rc==SQLITE_OK ){ + /* Check that the wal file has not been wrapped. Assuming that it has + ** not, also check that no checkpointer has attempted to checkpoint any + ** frames beyond pSnapshot->mxFrame. If either of these conditions are + ** true, return SQLITE_BUSY_SNAPSHOT. Otherwise, overwrite pWal->hdr + ** with *pSnapshot and set *pChanged as appropriate for opening the + ** snapshot. */ + if( !memcmp(pSnapshot->aSalt, pWal->hdr.aSalt, sizeof(pWal->hdr.aSalt)) + && pSnapshot->mxFrame>=pInfo->nBackfillAttempted + ){ + memcpy(&pWal->hdr, pSnapshot, sizeof(WalIndexHdr)); + *pChanged = bChanged; + }else{ + rc = SQLITE_BUSY_SNAPSHOT; + } + + /* Release the shared CKPT lock obtained above. */ + walUnlockShared(pWal, WAL_CKPT_LOCK); + } + + + if( rc!=SQLITE_OK ){ + sqlite3WalEndReadTransaction(pWal); + } + } + } +#endif return rc; } @@ -3165,6 +3268,35 @@ int sqlite3WalHeapMemory(Wal *pWal){ return (pWal && pWal->exclusiveMode==WAL_HEAPMEMORY_MODE ); } +#ifdef SQLITE_ENABLE_SNAPSHOT +/* Create a snapshot object. The content of a snapshot is opaque to +** every other subsystem, so the WAL module can put whatever it needs +** in the object. +*/ +int sqlite3WalSnapshotGet(Wal *pWal, sqlite3_snapshot **ppSnapshot){ + int rc = SQLITE_OK; + WalIndexHdr *pRet; + + assert( pWal->readLock>=0 && pWal->writeLock==0 ); + + pRet = (WalIndexHdr*)sqlite3_malloc(sizeof(WalIndexHdr)); + if( pRet==0 ){ + rc = SQLITE_NOMEM; + }else{ + memcpy(pRet, &pWal->hdr, sizeof(WalIndexHdr)); + *ppSnapshot = (sqlite3_snapshot*)pRet; + } + + return rc; +} + +/* Try to open on pSnapshot when the next read-transaction starts +*/ +void sqlite3WalSnapshotOpen(Wal *pWal, sqlite3_snapshot *pSnapshot){ + pWal->pSnapshot = (WalIndexHdr*)pSnapshot; +} +#endif /* SQLITE_ENABLE_SNAPSHOT */ + #ifdef SQLITE_ENABLE_ZIPVFS /* ** If the argument is not NULL, it points to a Wal object that holds a diff --git a/src/wal.h b/src/wal.h index 092546354b..94a049493e 100644 --- a/src/wal.h +++ b/src/wal.h @@ -126,6 +126,11 @@ int sqlite3WalExclusiveMode(Wal *pWal, int op); */ int sqlite3WalHeapMemory(Wal *pWal); +#ifdef SQLITE_ENABLE_SNAPSHOT +int sqlite3WalSnapshotGet(Wal *pWal, sqlite3_snapshot **ppSnapshot); +void sqlite3WalSnapshotOpen(Wal *pWal, sqlite3_snapshot *pSnapshot); +#endif + #ifdef SQLITE_ENABLE_ZIPVFS /* If the WAL file is not empty, return the number of bytes of content ** stored in each frame (i.e. the db page-size when the WAL was created). diff --git a/test/snapshot.test b/test/snapshot.test new file mode 100644 index 0000000000..6e160166c3 --- /dev/null +++ b/test/snapshot.test @@ -0,0 +1,340 @@ +# 2015 December 7 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# This file implements regression tests for SQLite library. The focus +# of this file is the sqlite3_snapshot_xxx() APIs. +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +ifcapable !snapshot {finish_test; return} +set testprefix snapshot + +#------------------------------------------------------------------------- +# Check some error conditions in snapshot_get(). It is an error if: +# +# 1) snapshot_get() is called on a non-WAL database, or +# 2) there is an open write transaction on the database. +# +do_execsql_test 1.0 { + CREATE TABLE t1(a, b); + INSERT INTO t1 VALUES(1, 2); + INSERT INTO t1 VALUES(3, 4); +} + +do_test 1.1.1 { + execsql { BEGIN; SELECT * FROM t1; } + list [catch { sqlite3_snapshot_get db main } msg] $msg +} {1 SQLITE_ERROR} +do_execsql_test 1.1.2 COMMIT + +do_test 1.2.1 { + execsql { + PRAGMA journal_mode = WAL; + BEGIN; + INSERT INTO t1 VALUES(5, 6); + INSERT INTO t1 VALUES(7, 8); + } + list [catch { sqlite3_snapshot_get db main } msg] $msg +} {1 SQLITE_ERROR} +do_execsql_test 1.3.2 COMMIT + +#------------------------------------------------------------------------- +# Check that a simple case works. Reuse the database created by the +# block of tests above. +# +do_execsql_test 2.1.0 { + BEGIN; + SELECT * FROM t1; +} {1 2 3 4 5 6 7 8} + +breakpoint +do_test 2.1.1 { + set snapshot [sqlite3_snapshot_get db main] + execsql { + COMMIT; + INSERT INTO t1 VALUES(9, 10); + SELECT * FROM t1; + } +} {1 2 3 4 5 6 7 8 9 10} + +do_test 2.1.2 { + execsql BEGIN + sqlite3_snapshot_open db main $snapshot + execsql { + SELECT * FROM t1; + } +} {1 2 3 4 5 6 7 8} + +do_test 2.1.3 { + sqlite3_snapshot_free $snapshot + execsql COMMIT +} {} + +do_test 2.2.0 { + sqlite3 db2 test.db + execsql { + BEGIN; + SELECT * FROM t1; + } db2 +} {1 2 3 4 5 6 7 8 9 10} + +do_test 2.2.1 { + set snapshot [sqlite3_snapshot_get db2 main] + execsql { + INSERT INTO t1 VALUES(11, 12); + SELECT * FROM t1; + } +} {1 2 3 4 5 6 7 8 9 10 11 12} + +do_test 2.2.2 { + execsql BEGIN + sqlite3_snapshot_open db main $snapshot + execsql { + SELECT * FROM t1; + } +} {1 2 3 4 5 6 7 8 9 10} + +do_test 2.2.3 { + sqlite3_snapshot_free $snapshot + execsql COMMIT + execsql COMMIT db2 + db2 close +} {} + +do_test 2.3.1 { + execsql { DELETE FROM t1 WHERE a>6 } + set snapshot [sqlite3_snapshot_get db main] + execsql { + INSERT INTO t1 VALUES('a', 'b'); + INSERT INTO t1 VALUES('c', 'd'); + SELECT * FROM t1; + } +} {1 2 3 4 5 6 a b c d} +do_test 2.3.2 { + execsql BEGIN + sqlite3_snapshot_open db main $snapshot + execsql { SELECT * FROM t1 } +} {1 2 3 4 5 6} + +do_test 2.3.3 { + catchsql { + INSERT INTO t1 VALUES('x','y') + } +} {1 {database is locked}} +do_test 2.3.4 { + execsql COMMIT + sqlite3_snapshot_free $snapshot +} {} + +#------------------------------------------------------------------------- +# Check some errors in sqlite3_snapshot_open(). It is an error if: +# +# 1) the db is in auto-commit mode, +# 2) the db has an open (read or write) transaction, +# 3) the db is not a wal database, +# +# Reuse the database created by earlier tests. +# +do_execsql_test 3.0.0 { + CREATE TABLE t2(x, y); + INSERT INTO t2 VALUES('a', 'b'); + INSERT INTO t2 VALUES('c', 'd'); + BEGIN; + SELECT * FROM t2; +} {a b c d} +do_test 3.0.1 { + set snapshot [sqlite3_snapshot_get db main] + execsql { COMMIT } + execsql { INSERT INTO t2 VALUES('e', 'f'); } +} {} + +do_test 3.1 { + list [catch {sqlite3_snapshot_open db main $snapshot } msg] $msg +} {1 SQLITE_ERROR} + +do_test 3.2.1 { + execsql { + BEGIN; + SELECT * FROM t2; + } +} {a b c d e f} +do_test 3.2.2 { + list [catch {sqlite3_snapshot_open db main $snapshot } msg] $msg +} {1 SQLITE_ERROR} + +do_test 3.2.3 { + execsql { + COMMIT; + BEGIN; + INSERT INTO t2 VALUES('g', 'h'); + } + list [catch {sqlite3_snapshot_open db main $snapshot } msg] $msg +} {1 SQLITE_ERROR} +do_execsql_test 3.2.4 COMMIT + +do_test 3.3.1 { + execsql { PRAGMA journal_mode = DELETE } + execsql { BEGIN } + list [catch {sqlite3_snapshot_open db main $snapshot } msg] $msg +} {1 SQLITE_ERROR} + +do_test 3.3.2 { + sqlite3_snapshot_free $snapshot + execsql COMMIT +} {} + +#------------------------------------------------------------------------- +# Check that SQLITE_BUSY_SNAPSHOT is returned if the specified snapshot +# no longer exists because the wal file has been checkpointed. +# +# 1. Reading a snapshot from the middle of a wal file is not possible +# after the wal file has been checkpointed. +# +# 2. That a snapshot from the end of a wal file can not be read once +# the wal file has been wrapped. +# +do_execsql_test 4.1.0 { + PRAGMA journal_mode = wal; + CREATE TABLE t3(i, j); + INSERT INTO t3 VALUES('o', 't'); + INSERT INTO t3 VALUES('t', 'f'); + BEGIN; + SELECT * FROM t3; +} {wal o t t f} + +do_test 4.1.1 { + set snapshot [sqlite3_snapshot_get db main] + execsql COMMIT +} {} +do_test 4.1.2 { + execsql { + INSERT INTO t3 VALUES('f', 's'); + BEGIN; + } + sqlite3_snapshot_open db main $snapshot + execsql { SELECT * FROM t3 } +} {o t t f} + +do_test 4.1.3 { + execsql { + COMMIT; + PRAGMA wal_checkpoint; + BEGIN; + } + list [catch {sqlite3_snapshot_open db main $snapshot} msg] $msg +} {1 SQLITE_BUSY_SNAPSHOT} +do_test 4.1.4 { + sqlite3_snapshot_free $snapshot + execsql COMMIT +} {} + +do_test 4.2.1 { + execsql { + INSERT INTO t3 VALUES('s', 'e'); + INSERT INTO t3 VALUES('n', 't'); + BEGIN; + SELECT * FROM t3; + } +} {o t t f f s s e n t} +do_test 4.2.2 { + set snapshot [sqlite3_snapshot_get db main] + execsql { + COMMIT; + PRAGMA wal_checkpoint; + BEGIN; + } + sqlite3_snapshot_open db main $snapshot + execsql { SELECT * FROM t3 } +} {o t t f f s s e n t} +do_test 4.2.3 { + execsql { + COMMIT; + INSERT INTO t3 VALUES('e', 't'); + BEGIN; + } + list [catch {sqlite3_snapshot_open db main $snapshot} msg] $msg +} {1 SQLITE_BUSY_SNAPSHOT} +do_test 4.2.4 { + sqlite3_snapshot_free $snapshot +} {} + +#------------------------------------------------------------------------- +# Check that SQLITE_BUSY is returned if a checkpoint is running when +# sqlite3_snapshot_open() is called. +# +reset_db +db close +testvfs tvfs +sqlite3 db test.db -vfs tvfs + +do_execsql_test 5.1 { + PRAGMA journal_mode = wal; + CREATE TABLE x1(x, xx, xxx); + INSERT INTO x1 VALUES('z', 'zz', 'zzz'); + BEGIN; + SELECT * FROM x1; +} {wal z zz zzz} + +do_test 5.2 { + set ::snapshot [sqlite3_snapshot_get db main] + sqlite3 db2 test.db -vfs tvfs + execsql { + INSERT INTO x1 VALUES('a', 'aa', 'aaa'); + COMMIT; + } +} {} + +set t53 0 +proc write_callback {args} { + do_test 5.3.[incr ::t53] { + execsql BEGIN + list [catch { sqlite3_snapshot_open db main $::snapshot } msg] $msg + } {1 SQLITE_BUSY} + catchsql COMMIT +} + +tvfs filter xWrite +tvfs script write_callback +db2 eval { PRAGMA wal_checkpoint } +db close +db2 close +tvfs delete +sqlite3_snapshot_free $snapshot + +#------------------------------------------------------------------------- +# Test that sqlite3_snapshot_get() may be called immediately after +# "BEGIN; PRAGMA user_version;". And that sqlite3_snapshot_open() may +# be called after opening the db handle and running the script +# "PRAGMA user_version; BEGIN". +reset_db +do_execsql_test 6.1 { + PRAGMA journal_mode = wal; + CREATE TABLE x1(x, xx, xxx); + INSERT INTO x1 VALUES('z', 'zz', 'zzz'); + BEGIN; + PRAGMA user_version; +} {wal 0} +do_test 6.2 { + set ::snapshot [sqlite3_snapshot_get db main] + execsql { + INSERT INTO x1 VALUES('a', 'aa', 'aaa'); + COMMIT; + } +} {} +do_test 6.3 { + sqlite3 db2 test.db + db2 eval "PRAGMA user_version ; BEGIN" + sqlite3_snapshot_open db2 main $::snapshot + db2 eval { SELECT * FROM x1 } +} {z zz zzz} +sqlite3_snapshot_free $snapshot + +finish_test diff --git a/test/snapshot_fault.test b/test/snapshot_fault.test new file mode 100644 index 0000000000..3ac13daefd --- /dev/null +++ b/test/snapshot_fault.test @@ -0,0 +1,164 @@ +# 2015 December 10 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# This file implements regression tests for SQLite library. The focus +# of this file is the sqlite3_snapshot_xxx() APIs. +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +ifcapable !snapshot {finish_test; return} +set testprefix snapshot_fault + +#------------------------------------------------------------------------- +# Check that an sqlite3_snapshot_open() client cannot be tricked into +# reading a corrupt snapshot even if a second client fails while +# checkpointing the db. +# +do_faultsim_test 1.0 -prep { + faultsim_delete_and_reopen + sqlite3 db2 test.db + db2 eval { + CREATE TABLE t1(a, b UNIQUE, c UNIQUE); + INSERT INTO t1 VALUES(1, randomblob(500), randomblob(500)); + INSERT INTO t1 VALUES(2, randomblob(500), randomblob(500)); + PRAGMA journal_mode = wal; + INSERT INTO t1 VALUES(3, randomblob(500), randomblob(500)); + BEGIN; + SELECT a FROM t1; + } + set ::snapshot [sqlite3_snapshot_get db2 main] + db2 eval COMMIT + db2 eval { + UPDATE t1 SET b=randomblob(501), c=randomblob(501) WHERE a=1; + INSERT INTO t1 VALUES(4, randomblob(500), randomblob(500)); + INSERT INTO t1 VALUES(5, randomblob(500), randomblob(500)); + INSERT INTO t1 VALUES(6, randomblob(500), randomblob(500)); + } +} -body { + db eval { PRAGMA wal_checkpoint } +} -test { + db2 eval BEGIN + if {[catch { sqlite3_snapshot_open db2 main $::snapshot } msg]} { + if {$msg != "SQLITE_BUSY_SNAPSHOT" && $msg != "SQLITE_BUSY"} { + error "error is $msg" + } + } else { + set res [db2 eval { + SELECT a FROM t1; + PRAGMA integrity_check; + }] + if {$res != "1 2 3 ok"} { error "res is $res" } + } + + sqlite3_snapshot_free $::snapshot +} + +#------------------------------------------------------------------------- +# This test is similar to the previous one. Except, after the +# "PRAGMA wal_checkpoint" command fails the db is closed and reopened +# so as to require wal file recovery. It should not be possible to open +# a snapshot that is part of the body of a recovered wal file. +# +do_faultsim_test 2.0 -prep { + faultsim_delete_and_reopen + db eval { + CREATE TABLE t1(a, b UNIQUE, c UNIQUE); + INSERT INTO t1 VALUES(1, randomblob(500), randomblob(500)); + INSERT INTO t1 VALUES(2, randomblob(500), randomblob(500)); + PRAGMA journal_mode = wal; + INSERT INTO t1 VALUES(3, randomblob(500), randomblob(500)); + BEGIN; + SELECT a FROM t1; + } + set ::snapshot [sqlite3_snapshot_get db main] + db eval COMMIT + + db eval { + UPDATE t1 SET b=randomblob(501), c=randomblob(501) WHERE a=1; + INSERT INTO t1 VALUES(4, randomblob(500), randomblob(500)); + INSERT INTO t1 VALUES(5, randomblob(500), randomblob(500)); + INSERT INTO t1 VALUES(6, randomblob(500), randomblob(500)); + } +} -body { + db eval { PRAGMA wal_checkpoint } +} -test { + + db_save + db close + db_restore_and_reopen + db eval { SELECT * FROM t1 } + + db eval BEGIN + if {[catch { sqlite3_snapshot_open db main $::snapshot } msg]} { + if {$msg != "SQLITE_BUSY_SNAPSHOT" && $msg != "SQLITE_BUSY"} { + error "error is $msg" + } + } else { + # This branch should actually never be taken. But it was useful in + # determining whether or not this test was actually working (by + # running a modified version of SQLite that allowed snapshots to be + # opened following a recovery). + error "TEST HAS FAILED" + + set res [db eval { + SELECT a FROM t1; + PRAGMA integrity_check; + }] + if {$res != "1 2 3 ok"} { error "res is $res" } + } + + sqlite3_snapshot_free $::snapshot +} + +#------------------------------------------------------------------------- +# Test the handling of faults that occur within sqlite3_snapshot_open(). +# +do_faultsim_test 3.0 -prep { + faultsim_delete_and_reopen + db eval { + CREATE TABLE t1(a, b UNIQUE, c UNIQUE); + INSERT INTO t1 VALUES(1, randomblob(500), randomblob(500)); + INSERT INTO t1 VALUES(2, randomblob(500), randomblob(500)); + PRAGMA journal_mode = wal; + INSERT INTO t1 VALUES(3, randomblob(500), randomblob(500)); + BEGIN; + SELECT a FROM t1; + } + set ::snapshot [sqlite3_snapshot_get db main] + db eval COMMIT + db eval { + UPDATE t1 SET b=randomblob(501), c=randomblob(501) WHERE a=1; + INSERT INTO t1 VALUES(4, randomblob(500), randomblob(500)); + INSERT INTO t1 VALUES(5, randomblob(500), randomblob(500)); + INSERT INTO t1 VALUES(6, randomblob(500), randomblob(500)); + BEGIN; + } +} -body { + if { [catch { sqlite3_snapshot_open db main $::snapshot } msg] } { + error $msg + } +} -test { + faultsim_test_result {0 {}} {1 SQLITE_IOERR} \ + {1 SQLITE_IOERR_NOMEM} {1 SQLITE_IOERR_READ} + if {$testrc==0} { + set res [db eval { + SELECT a FROM t1; + PRAGMA integrity_check; + }] + if {$res != "1 2 3 ok"} { error "res is $res" } + } + + sqlite3_snapshot_free $::snapshot +} + + + +finish_test