Index: src/main.c ================================================================== --- src/main.c +++ src/main.c @@ -3864,5 +3864,88 @@ } #endif 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 */ + Index: src/pager.c ================================================================== --- src/pager.c +++ src/pager.c @@ -7299,10 +7299,38 @@ } } 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 /* ** A read-lock must be held on the pager when this function is called. If Index: src/pager.h ================================================================== --- src/pager.h +++ src/pager.h @@ -166,10 +166,14 @@ int sqlite3PagerCheckpoint(Pager *pPager, int, int*, int*); int sqlite3PagerWalSupported(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 int sqlite3PagerWalFramesize(Pager *pPager); #endif Index: src/sqlite.h.in ================================================================== --- src/sqlite.h.in +++ src/sqlite.h.in @@ -4403,12 +4403,12 @@ ** ** ^The sqlite3_value_free(V) interface frees an [sqlite3_value] object ** 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 ** METHOD: sqlite3_context ** @@ -7849,37 +7849,131 @@ 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. */ #ifdef SQLITE_OMIT_FLOATING_POINT Index: src/test1.c ================================================================== --- src/test1.c +++ src/test1.c @@ -2267,10 +2267,98 @@ pVfs->xCurrentTimeInt64(pVfs, &t); Tcl_SetObjResult(interp, Tcl_NewWideIntObj(t)); 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 ** ** Return the next statment in sequence after STMT. */ @@ -7081,10 +7169,15 @@ #endif #ifdef SQLITE_ENABLE_SQLLOG { "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); int i; extern int sqlite3_sync_count, sqlite3_fullsync_count; Index: src/test_config.c ================================================================== --- src/test_config.c +++ src/test_config.c @@ -140,10 +140,16 @@ #ifdef SQLITE_ENABLE_MEMSYS5 Tcl_SetVar2(interp, "sqlite_options", "mem5", "1", TCL_GLOBAL_ONLY); #else 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 Tcl_SetVar2(interp, "sqlite_options", "mutex", "1", TCL_GLOBAL_ONLY); Index: src/wal.c ================================================================== --- src/wal.c +++ src/wal.c @@ -270,11 +270,12 @@ #define WAL_MAX_VERSION 3007000 #define WALINDEX_MAX_VERSION 3007000 /* ** 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 #define WAL_CKPT_LOCK 1 #define WAL_RECOVER_LOCK 2 @@ -290,11 +291,14 @@ /* ** 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 ** added in 3.7.1 when support for 64K pages was added. */ @@ -322,10 +326,20 @@ ** database "backfilling".) The nBackfill number is never greater than ** WalIndexHdr.mxFrame. nBackfill can only be increased by threads ** holding the WAL_CKPT_LOCK lock (which includes a recovery thread). ** 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) ** for any aReadMark[] means that entry is unused. aReadMark[0] is @@ -362,22 +376,24 @@ ** order to read from any aReadMark[] entries. */ 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 /* A block of WALINDEX_LOCK_RESERVED bytes beginning at ** WALINDEX_LOCK_OFFSET is reserved for locks. Since some systems ** 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 /* Size of write ahead log header, including checksum. */ @@ -432,10 +448,13 @@ const char *zWalName; /* Name of WAL file */ u32 nCkpt; /* Checkpoint sequence counter in the wal-header */ #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 }; /* ** Candidate values for Wal.exclusiveMode. */ @@ -1196,10 +1215,11 @@ ** currently holding locks that exclude all other readers, writers and ** checkpointers. */ 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; /* If more than one frame was recovered from the log file, report an @@ -1267,11 +1287,15 @@ assert( pDbFd ); /* 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 #ifdef UNIX_SHM_BASE assert( UNIX_SHM_BASE==WALINDEX_LOCK_OFFSET ); @@ -1653,10 +1677,11 @@ pWal->hdr.mxFrame = 0; sqlite3Put4byte((u8*)&aSalt[0], 1 + sqlite3Get4byte((u8*)&aSalt[0])); 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 ); } @@ -1761,10 +1786,12 @@ if( pInfo->nBackfillnBackfill; + + pInfo->nBackfillAttempted = mxSafeFrame; /* Sync the WAL to disk */ if( sync_flags ){ rc = sqlite3OsSync(pWal->pWalFd, sync_flags); } @@ -2145,10 +2172,11 @@ volatile WalCkptInfo *pInfo; /* Checkpoint information in wal-index */ u32 mxReadMark; /* Largest aReadMark[] value */ 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 */ /* Take steps to avoid spinning forever if there is a protocol error. ** @@ -2208,11 +2236,16 @@ return rc; } } 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. */ rc = walLockShared(pWal, WAL_READ_LOCK(0)); walShmBarrier(pWal); @@ -2246,89 +2279,92 @@ ** to select one of the aReadMark[] entries that is closest to ** but not exceeding pWal->hdr.mxFrame and lock that entry. */ 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( 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; - } + 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; + } + + 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; } /* @@ -2347,17 +2383,84 @@ */ 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 ); testcase( (rc&0xff)==SQLITE_BUSY ); 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; } /* ** Finish with a read transaction. All this does is release the @@ -3163,10 +3266,39 @@ */ 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 ** read-lock. This function returns the database page-size if it is known, ** or zero if it is not (or if pWal is NULL). Index: src/wal.h ================================================================== --- src/wal.h +++ src/wal.h @@ -123,10 +123,15 @@ /* Return true if the argument is non-NULL and the WAL module is using ** heap-memory for the wal-index. Otherwise, if the argument is NULL or the ** WAL module is using shared-memory, return false. */ 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). */ ADDED test/snapshot.test Index: test/snapshot.test ================================================================== --- /dev/null +++ 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 ADDED test/snapshot_fault.test Index: test/snapshot_fault.test ================================================================== --- /dev/null +++ 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