Index: src/main.c ================================================================== --- src/main.c +++ src/main.c @@ -3979,26 +3979,27 @@ 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); + if( db->autoCommit==0 ){ + int 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); @@ -4036,10 +4037,42 @@ } } } } + sqlite3_mutex_leave(db->mutex); +#endif /* SQLITE_OMIT_WAL */ + return rc; +} + +/* +** Recover as many snapshots as possible from the wal file associated with +** schema zDb of database db. +*/ +int sqlite3_snapshot_recover(sqlite3 *db, const char *zDb){ + int rc = SQLITE_ERROR; + int iDb; +#ifndef SQLITE_OMIT_WAL + +#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==sqlite3BtreeIsInReadTrans(pBt) ){ + rc = sqlite3BtreeBeginTrans(pBt, 0); + if( rc==SQLITE_OK ){ + rc = sqlite3PagerSnapshotRecover(sqlite3BtreePager(pBt)); + sqlite3BtreeCommit(pBt); + } + } + } sqlite3_mutex_leave(db->mutex); #endif /* SQLITE_OMIT_WAL */ return rc; } Index: src/pager.c ================================================================== --- src/pager.c +++ src/pager.c @@ -7403,10 +7403,24 @@ }else{ rc = SQLITE_ERROR; } return rc; } + +/* +** If this is a WAL database, call sqlite3WalSnapshotRecover(). If this +** is not a WAL database, return an error. +*/ +int sqlite3PagerSnapshotRecover(Pager *pPager){ + int rc; + if( pPager->pWal ){ + rc = sqlite3WalSnapshotRecover(pPager->pWal); + }else{ + rc = SQLITE_ERROR; + } + return rc; +} #endif /* SQLITE_ENABLE_SNAPSHOT */ #endif /* !SQLITE_OMIT_WAL */ #ifdef SQLITE_ENABLE_ZIPVFS /* Index: src/pager.h ================================================================== --- src/pager.h +++ src/pager.h @@ -180,10 +180,11 @@ int sqlite3PagerCloseWal(Pager *pPager, sqlite3*); int sqlite3PagerUseWal(Pager *pPager); # ifdef SQLITE_ENABLE_SNAPSHOT int sqlite3PagerSnapshotGet(Pager *pPager, sqlite3_snapshot **ppSnapshot); int sqlite3PagerSnapshotOpen(Pager *pPager, sqlite3_snapshot *pSnapshot); + int sqlite3PagerSnapshotRecover(Pager *pPager); # endif #else # define sqlite3PagerUseWal(x) 0 #endif Index: src/sqlite.h.in ================================================================== --- src/sqlite.h.in +++ src/sqlite.h.in @@ -8281,11 +8281,13 @@ ** 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; +typedef struct sqlite3_snapshot { + unsigned char hidden[48]; +} sqlite3_snapshot; /* ** CAPI3REF: Record A Database Snapshot ** EXPERIMENTAL ** @@ -8292,13 +8294,36 @@ ** ^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]. +** If there is not already a read-transaction open on schema S when +** this function is called, one is opened automatically. +** +** The following must be true for this function to succeed. If any of +** the following statements are false when sqlite3_snapshot_get() is +** called, SQLITE_ERROR is returned. The final value of *P is undefined +** in this case. +** +** +** +** This function may also return SQLITE_NOMEM. If it is called with the +** database handle in autocommit mode but fails for some other reason, +** whether or not a read transaction is opened on schema S is undefined. ** ** 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. ** @@ -8387,10 +8412,32 @@ SQLITE_EXPERIMENTAL int sqlite3_snapshot_cmp( sqlite3_snapshot *p1, sqlite3_snapshot *p2 ); +/* +** CAPI3REF: Recover snapshots from a wal file +** EXPERIMENTAL +** +** If all connections disconnect from a database file but do not perform +** a checkpoint, the existing wal file is opened along with the database +** file the next time the database is opened. At this point it is only +** possible to successfully call sqlite3_snapshot_open() to open the most +** recent snapshot of the database (the one at the head of the wal file), +** even though the wal file may contain other valid snapshots for which +** clients have sqlite3_snapshot handles. +** +** This function attempts to scan the wal file associated with database zDb +** of database handle db and make all valid snapshots available to +** sqlite3_snapshot_open(). It is an error if there is already a read +** transaction open on the database, or if the database is not a wal mode +** database. +** +** SQLITE_OK is returned if successful, or an SQLite error code otherwise. +*/ +SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const char *zDb); + /* ** 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 @@ -2308,10 +2308,42 @@ } return TCL_OK; } #endif /* SQLITE_ENABLE_SNAPSHOT */ +#ifdef SQLITE_ENABLE_SNAPSHOT +/* +** Usage: sqlite3_snapshot_recover DB DBNAME +*/ +static int SQLITE_TCLAPI test_snapshot_recover( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + int rc; + sqlite3 *db; + char *zName; + + 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_recover(db, zName); + if( rc!=SQLITE_OK ){ + Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1)); + return TCL_ERROR; + }else{ + Tcl_ResetResult(interp); + } + return TCL_OK; +} +#endif /* SQLITE_ENABLE_SNAPSHOT */ + #ifdef SQLITE_ENABLE_SNAPSHOT /* ** Usage: sqlite3_snapshot_open DB DBNAME SNAPSHOT */ static int SQLITE_TCLAPI test_snapshot_open( @@ -2383,10 +2415,117 @@ p1 = (sqlite3_snapshot*)sqlite3TestTextToPtr(Tcl_GetString(objv[1])); p2 = (sqlite3_snapshot*)sqlite3TestTextToPtr(Tcl_GetString(objv[2])); res = sqlite3_snapshot_cmp(p1, p2); Tcl_SetObjResult(interp, Tcl_NewIntObj(res)); return TCL_OK; +} +#endif /* SQLITE_ENABLE_SNAPSHOT */ + +#ifdef SQLITE_ENABLE_SNAPSHOT +/* +** Usage: sqlite3_snapshot_get_blob DB DBNAME +*/ +static int SQLITE_TCLAPI test_snapshot_get_blob( + 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{ + Tcl_SetObjResult(interp, + Tcl_NewByteArrayObj((unsigned char*)pSnapshot, sizeof(sqlite3_snapshot)) + ); + sqlite3_snapshot_free(pSnapshot); + } + return TCL_OK; +} +#endif /* SQLITE_ENABLE_SNAPSHOT */ + +#ifdef SQLITE_ENABLE_SNAPSHOT + /* + ** Usage: sqlite3_snapshot_open_blob DB DBNAME SNAPSHOT +*/ +static int SQLITE_TCLAPI test_snapshot_open_blob( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + int rc; + sqlite3 *db; + char *zName; + unsigned char *pBlob; + int nBlob; + + 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]); + pBlob = Tcl_GetByteArrayFromObj(objv[3], &nBlob); + if( nBlob!=sizeof(sqlite3_snapshot) ){ + Tcl_AppendResult(interp, "bad SNAPSHOT", 0); + return TCL_ERROR; + } + rc = sqlite3_snapshot_open(db, zName, (sqlite3_snapshot*)pBlob); + 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_cmp_blob SNAPSHOT1 SNAPSHOT2 +*/ +static int SQLITE_TCLAPI test_snapshot_cmp_blob( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + int res; + unsigned char *p1; + unsigned char *p2; + int n1; + int n2; + + if( objc!=3 ){ + Tcl_WrongNumArgs(interp, 1, objv, "SNAPSHOT1 SNAPSHOT2"); + return TCL_ERROR; + } + + p1 = Tcl_GetByteArrayFromObj(objv[1], &n1); + p2 = Tcl_GetByteArrayFromObj(objv[2], &n2); + + if( n1!=sizeof(sqlite3_snapshot) || n1!=n2 ){ + Tcl_AppendResult(interp, "bad SNAPSHOT", 0); + return TCL_ERROR; + } + + res = sqlite3_snapshot_cmp((sqlite3_snapshot*)p1, (sqlite3_snapshot*)p2); + Tcl_SetObjResult(interp, Tcl_NewIntObj(res)); + return TCL_OK; } #endif /* SQLITE_ENABLE_SNAPSHOT */ /* ** Usage: sqlite3_delete_database FILENAME @@ -7537,10 +7676,14 @@ #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 }, { "sqlite3_snapshot_cmp", test_snapshot_cmp, 0 }, + { "sqlite3_snapshot_recover", test_snapshot_recover, 0 }, + { "sqlite3_snapshot_get_blob", test_snapshot_get_blob, 0 }, + { "sqlite3_snapshot_open_blob", test_snapshot_open_blob, 0 }, + { "sqlite3_snapshot_cmp_blob", test_snapshot_cmp_blob, 0 }, #endif { "sqlite3_delete_database", test_delete_database, 0 }, }; static int bitmask_size = sizeof(Bitmask)*8; static int longdouble_size = sizeof(LONGDOUBLE_TYPE); Index: src/wal.c ================================================================== --- src/wal.c +++ src/wal.c @@ -2377,10 +2377,88 @@ pWal->readLock = (i16)mxI; } return rc; } +#ifdef SQLITE_ENABLE_SNAPSHOT +/* +** Attempt to reduce the value of the WalCkptInfo.nBackfillAttempted +** variable so that older snapshots can be accessed. To do this, loop +** through all wal frames from nBackfillAttempted to (nBackfill+1), +** comparing their content to the corresponding page with the database +** file, if any. Set nBackfillAttempted to the frame number of the +** first frame for which the wal file content matches the db file. +** +** This is only really safe if the file-system is such that any page +** writes made by earlier checkpointers were atomic operations, which +** is not always true. It is also possible that nBackfillAttempted +** may be left set to a value larger than expected, if a wal frame +** contains content that duplicate of an earlier version of the same +** page. +** +** SQLITE_OK is returned if successful, or an SQLite error code if an +** error occurs. It is not an error if nBackfillAttempted cannot be +** decreased at all. +*/ +int sqlite3WalSnapshotRecover(Wal *pWal){ + int rc; + + assert( pWal->readLock>=0 ); + rc = walLockExclusive(pWal, WAL_CKPT_LOCK, 1); + if( rc==SQLITE_OK ){ + volatile WalCkptInfo *pInfo = walCkptInfo(pWal); + int szPage = (int)pWal->szPage; + i64 szDb; /* Size of db file in bytes */ + + rc = sqlite3OsFileSize(pWal->pDbFd, &szDb); + if( rc==SQLITE_OK ){ + void *pBuf1 = sqlite3_malloc(szPage); + void *pBuf2 = sqlite3_malloc(szPage); + if( pBuf1==0 || pBuf2==0 ){ + rc = SQLITE_NOMEM; + }else{ + u32 i = pInfo->nBackfillAttempted; + for(i=pInfo->nBackfillAttempted; i>pInfo->nBackfill; i--){ + volatile ht_slot *dummy; + volatile u32 *aPgno; /* Array of page numbers */ + u32 iZero; /* Frame corresponding to aPgno[0] */ + u32 pgno; /* Page number in db file */ + i64 iDbOff; /* Offset of db file entry */ + i64 iWalOff; /* Offset of wal file entry */ + + rc = walHashGet(pWal, walFramePage(i), &dummy, &aPgno, &iZero); + if( rc!=SQLITE_OK ) break; + pgno = aPgno[i-iZero]; + iDbOff = (i64)(pgno-1) * szPage; + + if( iDbOff+szPage<=szDb ){ + iWalOff = walFrameOffset(i, szPage) + WAL_FRAME_HDRSIZE; + rc = sqlite3OsRead(pWal->pWalFd, pBuf1, szPage, iWalOff); + + if( rc==SQLITE_OK ){ + rc = sqlite3OsRead(pWal->pDbFd, pBuf2, szPage, iDbOff); + } + + if( rc!=SQLITE_OK || 0==memcmp(pBuf1, pBuf2, szPage) ){ + break; + } + } + + pInfo->nBackfillAttempted = i-1; + } + } + + sqlite3_free(pBuf1); + sqlite3_free(pBuf2); + } + walUnlockExclusive(pWal, WAL_CKPT_LOCK, 1); + } + + return rc; +} +#endif /* SQLITE_ENABLE_SNAPSHOT */ + /* ** Begin a read transaction on the database. ** ** This routine used to be called sqlite3OpenSnapshot() and with good reason: ** it takes a snapshot of the state of the WAL and wal-index for the current @@ -2439,11 +2517,15 @@ ** 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. */ + ** before checking pInfo->nBackfillAttempted. + ** + ** TODO: Does the aReadMark[] lock prevent a checkpointer from doing + ** this already? + */ 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 @@ -3391,13 +3473,18 @@ ** in the object. */ int sqlite3WalSnapshotGet(Wal *pWal, sqlite3_snapshot **ppSnapshot){ int rc = SQLITE_OK; WalIndexHdr *pRet; + static const u32 aZero[4] = { 0, 0, 0, 0 }; assert( pWal->readLock>=0 && pWal->writeLock==0 ); + if( memcmp(&pWal->hdr.aFrameCksum[0],aZero,16)==0 ){ + *ppSnapshot = 0; + return SQLITE_ERROR; + } pRet = (WalIndexHdr*)sqlite3_malloc(sizeof(WalIndexHdr)); if( pRet==0 ){ rc = SQLITE_NOMEM_BKPT; }else{ memcpy(pRet, &pWal->hdr, sizeof(WalIndexHdr)); Index: src/wal.h ================================================================== --- src/wal.h +++ src/wal.h @@ -129,10 +129,11 @@ int sqlite3WalHeapMemory(Wal *pWal); #ifdef SQLITE_ENABLE_SNAPSHOT int sqlite3WalSnapshotGet(Wal *pWal, sqlite3_snapshot **ppSnapshot); void sqlite3WalSnapshotOpen(Wal *pWal, sqlite3_snapshot *pSnapshot); +int sqlite3WalSnapshotRecover(Wal *pWal); #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). Index: test/snapshot.test ================================================================== --- test/snapshot.test +++ test/snapshot.test @@ -24,415 +24,459 @@ if {[permutation]=="inmemory_journal"} { finish_test return } -#------------------------------------------------------------------------- -# 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} - -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} -do_test 6.4 { - db2 close - sqlite3 db2 test.db - db2 eval "PRAGMA application_id" - db2 eval "BEGIN" - sqlite3_snapshot_open db2 main $::snapshot - db2 eval { SELECT * FROM x1 } -} {z zz zzz} - -do_test 6.5 { - db2 close - sqlite3 db2 test.db - db2 eval "BEGIN" - list [catch {sqlite3_snapshot_open db2 main $::snapshot} msg] $msg -} {1 SQLITE_ERROR} - -sqlite3_snapshot_free $snapshot - -#------------------------------------------------------------------------- -# The following tests investigate the sqlite3_snapshot_cmp() API. -# - -# Compare snapshots $p1 and $p2, checking that the result is $r. -# -proc do_snapshot_cmp_test {tn p1 p2 r} { - uplevel [list do_test $tn.1 [list sqlite3_snapshot_cmp $p1 $p2] $r] - uplevel [list do_test $tn.2 [list sqlite3_snapshot_cmp $p2 $p1] [expr $r*-1]] - uplevel [list do_test $tn.3 [list sqlite3_snapshot_cmp $p1 $p1] 0] - uplevel [list do_test $tn.4 [list sqlite3_snapshot_cmp $p2 $p2] 0] -} - -catch { db2 close } -reset_db - -do_execsql_test 7.1 { - PRAGMA journal_mode = wal; - CREATE TABLE t1(x); -} wal - -do_test 7.1.2 { - execsql { BEGIN ; PRAGMA application_id } - set p1 [sqlite3_snapshot_get db main] - execsql { - INSERT INTO t1 VALUES(10); - COMMIT; - } - execsql { BEGIN ; PRAGMA application_id } - set p2 [sqlite3_snapshot_get db main] - execsql COMMIT -} {} - -do_snapshot_cmp_test 7.1.3 $p1 $p2 -1 -sqlite3_snapshot_free $p1 -sqlite3_snapshot_free $p2 - -do_execsql_test 7.2.1 { - INSERT INTO t1 VALUES(11); - INSERT INTO t1 VALUES(12); - INSERT INTO t1 VALUES(13); - BEGIN; - PRAGMA application_id; -} {0} -do_test 7.2.2 { - set p1 [sqlite3_snapshot_get db main] - execsql { - COMMIT; - INSERT INTO t1 VALUES(14); - PRAGMA wal_checkpoint; - BEGIN; - PRAGMA application_id; - } - set p2 [sqlite3_snapshot_get db main] - execsql COMMIT -} {} - -do_snapshot_cmp_test 7.2.3 $p1 $p2 -1 -sqlite3_snapshot_free $p2 - -do_test 7.3.1 { - execsql { - INSERT INTO t1 VALUES(14); - BEGIN; - PRAGMA application_id; - } - set p2 [sqlite3_snapshot_get db main] - execsql COMMIT -} {} - -do_snapshot_cmp_test 7.3.2 $p1 $p2 -1 -sqlite3_snapshot_free $p1 -sqlite3_snapshot_free $p2 +foreach {tn tcl} { + 1 { + proc snapshot_get {DB DBNAME} { + uplevel [list sqlite3_snapshot_get $DB $DBNAME] + } + proc snapshot_open {DB DBNAME SNAPSHOT} { + uplevel [list sqlite3_snapshot_open $DB $DBNAME $SNAPSHOT] + } + proc snapshot_free {SNAPSHOT} { + uplevel [list sqlite3_snapshot_free $SNAPSHOT] + } + proc snapshot_cmp {SNAPSHOT1 SNAPSHOT2} { + uplevel [list sqlite3_snapshot_cmp $SNAPSHOT1 $SNAPSHOT2] + } + } + + 2 { + proc snapshot_get {DB DBNAME} { + uplevel [list sqlite3_snapshot_get_blob $DB $DBNAME] + } + proc snapshot_open {DB DBNAME SNAPSHOT} { + uplevel [list sqlite3_snapshot_open_blob $DB $DBNAME $SNAPSHOT] + } + proc snapshot_free {SNAPSHOT} { + } + proc snapshot_cmp {SNAPSHOT1 SNAPSHOT2} { + uplevel [list sqlite3_snapshot_cmp_blob $SNAPSHOT1 $SNAPSHOT2] + } + } +} { + + reset_db + eval $tcl + + #------------------------------------------------------------------------- + # 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. + # 3) the database handle is in auto-commit mode + # + do_execsql_test $tn.1.0 { + CREATE TABLE t1(a, b); + INSERT INTO t1 VALUES(1, 2); + INSERT INTO t1 VALUES(3, 4); + } + + do_test $tn.1.1.1 { + execsql { BEGIN; SELECT * FROM t1; } + list [catch { snapshot_get db main } msg] $msg + } {1 SQLITE_ERROR} + do_execsql_test $tn.1.1.2 COMMIT + + do_test $tn.1.2.1 { + execsql { + PRAGMA journal_mode = WAL; + BEGIN; + INSERT INTO t1 VALUES(5, 6); + INSERT INTO t1 VALUES(7, 8); + } + list [catch { snapshot_get db main } msg] $msg + } {1 SQLITE_ERROR} + do_execsql_test $tn.1.2.2 COMMIT + + do_test $tn.1.3.1 { + list [catch { snapshot_get db main } msg] $msg + } {1 SQLITE_ERROR} + do_test $tn.1.3.2 { + db trans { set snap [snapshot_get db main] } + snapshot_free $snap + } {} + + #------------------------------------------------------------------------- + # Check that a simple case works. Reuse the database created by the + # block of tests above. + # + do_execsql_test $tn.2.1.0 { + BEGIN; + SELECT * FROM t1; + } {1 2 3 4 5 6 7 8} + + do_test $tn.2.1.1 { + set snapshot [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 $tn.2.1.2 { + execsql BEGIN + snapshot_open db main $snapshot + execsql { + SELECT * FROM t1; + } + } {1 2 3 4 5 6 7 8} + + do_test $tn.2.1.3 { + snapshot_free $snapshot + execsql COMMIT + } {} + + do_test $tn.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 $tn.2.2.1 { + set snapshot [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 $tn.2.2.2 { + execsql BEGIN + snapshot_open db main $snapshot + execsql { + SELECT * FROM t1; + } + } {1 2 3 4 5 6 7 8 9 10} + + do_test $tn.2.2.3 { + snapshot_free $snapshot + execsql COMMIT + execsql COMMIT db2 + db2 close + } {} + + do_test $tn.2.3.1 { + execsql { DELETE FROM t1 WHERE a>6 } + db trans { set snapshot [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 $tn.2.3.2 { + execsql BEGIN + snapshot_open db main $snapshot + execsql { SELECT * FROM t1 } + } {1 2 3 4 5 6} + + do_test $tn.2.3.3 { + catchsql { + INSERT INTO t1 VALUES('x','y') + } + } {1 {database is locked}} + do_test $tn.2.3.4 { + execsql COMMIT + snapshot_free $snapshot + } {} + + #------------------------------------------------------------------------- + # Check some errors in 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 $tn.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 $tn.3.0.1 { + set snapshot [snapshot_get db main] + execsql { COMMIT } + execsql { INSERT INTO t2 VALUES('e', 'f'); } + } {} + + do_test $tn.3.1 { + list [catch {snapshot_open db main $snapshot } msg] $msg + } {1 SQLITE_ERROR} + + do_test $tn.3.2.1 { + execsql { + BEGIN; + SELECT * FROM t2; + } + } {a b c d e f} + do_test $tn.3.2.2 { + list [catch {snapshot_open db main $snapshot } msg] $msg + } {1 SQLITE_ERROR} + + do_test $tn.3.2.3 { + execsql { + COMMIT; + BEGIN; + INSERT INTO t2 VALUES('g', 'h'); + } + list [catch {snapshot_open db main $snapshot } msg] $msg + } {1 SQLITE_ERROR} + do_execsql_test $tn.3.2.4 COMMIT + + do_test $tn.3.3.1 { + execsql { PRAGMA journal_mode = DELETE } + execsql { BEGIN } + list [catch {snapshot_open db main $snapshot } msg] $msg + } {1 SQLITE_ERROR} + + do_test $tn.$tn.3.3.2 { + 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 $tn.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 $tn.4.1.1 { + set snapshot [snapshot_get db main] + execsql COMMIT + } {} + do_test $tn.4.1.2 { + execsql { + INSERT INTO t3 VALUES('f', 's'); + BEGIN; + } + snapshot_open db main $snapshot + execsql { SELECT * FROM t3 } + } {o t t f} + + do_test $tn.4.1.3 { + execsql { + COMMIT; + PRAGMA wal_checkpoint; + BEGIN; + } + list [catch {snapshot_open db main $snapshot} msg] $msg + } {1 SQLITE_BUSY_SNAPSHOT} + do_test $tn.4.1.4 { + snapshot_free $snapshot + execsql COMMIT + } {} + + do_test $tn.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 $tn.4.2.2 { + set snapshot [snapshot_get db main] + execsql { + COMMIT; + PRAGMA wal_checkpoint; + BEGIN; + } + snapshot_open db main $snapshot + execsql { SELECT * FROM t3 } + } {o t t f f s s e n t} + do_test $tn.4.2.3 { + execsql { + COMMIT; + INSERT INTO t3 VALUES('e', 't'); + BEGIN; + } + list [catch {snapshot_open db main $snapshot} msg] $msg + } {1 SQLITE_BUSY_SNAPSHOT} + do_test $tn.4.2.4 { + 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 $tn.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 $tn.5.2 { + set ::snapshot [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 $tn.5.3.[incr ::t53] { + execsql BEGIN + list [catch { 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 + 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 $tn.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 $tn.6.2 { + set ::snapshot [snapshot_get db main] + execsql { + INSERT INTO x1 VALUES('a', 'aa', 'aaa'); + COMMIT; + } + } {} + do_test $tn.6.3 { + sqlite3 db2 test.db + db2 eval "PRAGMA user_version ; BEGIN" + snapshot_open db2 main $::snapshot + db2 eval { SELECT * FROM x1 } + } {z zz zzz} + do_test $tn.6.4 { + db2 close + sqlite3 db2 test.db + db2 eval "PRAGMA application_id" + db2 eval "BEGIN" + snapshot_open db2 main $::snapshot + db2 eval { SELECT * FROM x1 } + } {z zz zzz} + + do_test $tn.6.5 { + db2 close + sqlite3 db2 test.db + db2 eval "BEGIN" + list [catch {snapshot_open db2 main $::snapshot} msg] $msg + } {1 SQLITE_ERROR} + + snapshot_free $snapshot + + #------------------------------------------------------------------------- + # The following tests investigate the sqlite3_snapshot_cmp() API. + # + + # Compare snapshots $p1 and $p2, checking that the result is $r. + # + proc do_snapshot_cmp_test {tn p1 p2 r} { + uplevel [list do_test $tn.1 [list snapshot_cmp $p1 $p2] $r] + uplevel [list do_test $tn.2 [list snapshot_cmp $p2 $p1] [expr $r*-1]] + uplevel [list do_test $tn.3 [list snapshot_cmp $p1 $p1] 0] + uplevel [list do_test $tn.4 [list snapshot_cmp $p2 $p2] 0] + } + + catch { db2 close } + reset_db + + do_execsql_test $tn.7.1 { + PRAGMA journal_mode = wal; + CREATE TABLE t1(x); + } wal + + do_test $tn.7.1.2 { + execsql { BEGIN ; PRAGMA application_id } + set p1 [snapshot_get db main] + execsql { + INSERT INTO t1 VALUES(10); + COMMIT; + } + execsql { BEGIN ; PRAGMA application_id } + set p2 [snapshot_get db main] + execsql COMMIT + } {} + + do_snapshot_cmp_test $tn.7.1.3 $p1 $p2 -1 + snapshot_free $p1 + snapshot_free $p2 + + do_execsql_test $tn.7.2.1 { + INSERT INTO t1 VALUES(11); + INSERT INTO t1 VALUES(12); + INSERT INTO t1 VALUES(13); + BEGIN; + PRAGMA application_id; + } {0} + do_test $tn.7.2.2 { + set p1 [snapshot_get db main] + execsql { + COMMIT; + INSERT INTO t1 VALUES(14); + PRAGMA wal_checkpoint; + BEGIN; + PRAGMA application_id; + } + set p2 [snapshot_get db main] + execsql COMMIT + } {} + + do_snapshot_cmp_test $tn.7.2.3 $p1 $p2 -1 + snapshot_free $p2 + + do_test $tn.7.3.1 { + execsql { + INSERT INTO t1 VALUES(14); + BEGIN; + PRAGMA application_id; + } + set p2 [snapshot_get db main] + execsql COMMIT + } {} + + do_snapshot_cmp_test $tn.7.3.2 $p1 $p2 -1 + snapshot_free $p1 + snapshot_free $p2 +} finish_test ADDED test/snapshot2.test Index: test/snapshot2.test ================================================================== --- /dev/null +++ test/snapshot2.test @@ -0,0 +1,202 @@ +# 2016 November 18 +# +# 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 snapshot2 + +# This test does not work with the inmemory_journal permutation. The reason +# is that each connection opened as part of this permutation executes +# "PRAGMA journal_mode=memory", which fails if the database is in wal mode +# and there are one or more existing connections. +if {[permutation]=="inmemory_journal"} { + finish_test + return +} + +#------------------------------------------------------------------------- +# Check that it is not possible to obtain a snapshot immediately after +# a wal mode database with an empty wal file is opened. But it is after +# the file has been written, even by some other connection. +# +do_execsql_test 1.0 { + PRAGMA journal_mode = wal; + CREATE TABLE t1(a, b, c); + INSERT INTO t1 VALUES(1, 2, 3); + INSERT INTO t1 VALUES(4, 5, 6); +} {wal} + +db close +do_test 1.1.1 { list [file exists test.db] [file exists test.db-wal] } {1 0} + +sqlite3 db test.db +do_execsql_test 1.1.2 { SELECT * FROM t1 } {1 2 3 4 5 6} + +do_test 1.1.3 { + execsql BEGIN + list [catch { sqlite3_snapshot_get_blob db main } msg] $msg +} {1 SQLITE_ERROR} +execsql COMMIT + +do_test 1.1.4 { + execsql { INSERT INTO t1 VALUES(7, 8, 9) } + execsql BEGIN + string length [sqlite3_snapshot_get_blob db main] +} 48 +execsql COMMIT + +db close +do_test 1.2.1 { list [file exists test.db] [file exists test.db-wal] } {1 0} + +sqlite3 db test.db +do_execsql_test 1.2.2 { SELECT * FROM t1 } {1 2 3 4 5 6 7 8 9} + +do_test 1.2.3 { + execsql BEGIN + list [catch { sqlite3_snapshot_get_blob db main } msg] $msg +} {1 SQLITE_ERROR} +execsql COMMIT + +do_test 1.2.4 { + sqlite3 db2 test.db + execsql { INSERT INTO t1 VALUES(10, 11, 12) } db2 + execsql BEGIN + string length [sqlite3_snapshot_get_blob db main] +} 48 +execsql COMMIT +db2 close + +#------------------------------------------------------------------------- +# Simple tests for sqlite3_snapshot_recover(). +# +reset_db +do_execsql_test 2.0 { + CREATE TABLE t1(x); + PRAGMA journal_mode = wal; + INSERT INTO t1 VALUES(1); + INSERT INTO t1 VALUES(2); +} {wal} + +do_test 2.1 { + db trans { set snap [sqlite3_snapshot_get_blob db main] } + sqlite3_db_config db NO_CKPT_ON_CLOSE 1 + db close + sqlite3 db test.db + + execsql {SELECT * FROM sqlite_master} + execsql BEGIN + sqlite3_snapshot_open_blob db main $snap + execsql COMMIT; + execsql { INSERT INTO t1 VALUES(3); } +} {} + +do_test 2.2 { + sqlite3_db_config db NO_CKPT_ON_CLOSE 1 + db close + sqlite3 db test.db + + execsql {SELECT * FROM sqlite_master} + execsql BEGIN + list [catch { sqlite3_snapshot_open_blob db main $snap } msg] $msg +} {1 SQLITE_BUSY_SNAPSHOT} + +do_test 2.3 { + execsql COMMIT + sqlite3_snapshot_recover db main + execsql BEGIN + sqlite3_snapshot_open_blob db main $snap + execsql { SELECT * FROM t1 } +} {1 2} + +do_test 2.4 { + execsql COMMIT + execsql { SELECT * FROM t1 } +} {1 2 3} + +do_test 2.5 { + execsql { PRAGMA wal_checkpoint } + sqlite3_db_config db NO_CKPT_ON_CLOSE 1 + db close + sqlite3 db test.db + + sqlite3_snapshot_recover db main + execsql BEGIN + list [catch { sqlite3_snapshot_open_blob db main $snap } msg] $msg +} {1 SQLITE_BUSY_SNAPSHOT} + +#------------------------------------------------------------------------- +# Check that calling sqlite3_snapshot_recover() does not confuse the +# pager cache. +reset_db +do_execsql_test 3.0 { + PRAGMA journal_mode = wal; + CREATE TABLE t1(x, y); + INSERT INTO t1 VALUES('a', 'b'); + INSERT INTO t1 VALUES('c', 'd'); +} {wal} +do_test 3.1 { + sqlite3 db2 test.db + execsql { INSERT INTO t1 VALUES('e', 'f') } db2 + db2 close + sqlite3_snapshot_recover db main +} {} +do_execsql_test 3.2 { + SELECT * FROM t1; +} {a b c d e f} + +#------------------------------------------------------------------------- +# Check that sqlite3_snapshot_recover() returns an error if it is called +# with an open read-transaction. Or on a database that does not exist. Or +# on the temp database. Or on a db that is not in wal mode. +# +do_test 4.1 { + sqlite3_snapshot_recover db main +} {} +do_test 4.2 { + execsql { + BEGIN; + SELECT * FROM sqlite_master; + } + list [catch { sqlite3_snapshot_recover db main } msg] $msg +} {1 SQLITE_ERROR} +do_test 4.3 { + execsql COMMIT + sqlite3_snapshot_recover db main +} {} +do_test 4.4 { + list [catch { sqlite3_snapshot_recover db aux } msg] $msg +} {1 SQLITE_ERROR} +do_test 4.5 { + forcedelete test.db2 + execsql { + ATTACH 'test.db2' AS aux; + PRAGMA aux.journal_mode = wal; + CREATE TABLE aux.t2(x, y); + } + list [catch { sqlite3_snapshot_recover db aux } msg] $msg +} {0 {}} +do_test 4.6 { + list [catch { sqlite3_snapshot_recover db temp } msg] $msg +} {1 SQLITE_ERROR} +do_test 4.7 { + execsql { + PRAGMA aux.journal_mode = delete; + } + list [catch { sqlite3_snapshot_recover db aux } msg] $msg +} {1 SQLITE_ERROR} + +finish_test + + Index: test/snapshot_fault.test ================================================================== --- test/snapshot_fault.test +++ test/snapshot_fault.test @@ -156,9 +156,71 @@ if {$res != "1 2 3 ok"} { error "res is $res" } } sqlite3_snapshot_free $::snapshot } + +#------------------------------------------------------------------------- +# Test the handling of faults that occur within sqlite3_snapshot_recover(). +# +reset_db +do_execsql_test 4.0 { + PRAGMA journal_mode = wal; + CREATE TABLE t1(zzz); + INSERT INTO t1 VALUES('abc'); + INSERT INTO t1 VALUES('def'); +} {wal} +faultsim_save_and_close + +do_test 4.0.1 { + faultsim_restore_and_reopen + db eval { SELECT * FROM sqlite_master } + sqlite3_snapshot_recover db main +} {} +db close + +do_faultsim_test 4.0 -faults oom* -prep { + faultsim_restore_and_reopen + db eval { SELECT * FROM sqlite_master } +} -body { + sqlite3_snapshot_recover db main +} -test { + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} {1 SQLITE_IOERR_NOMEM} +} + +# The following test cases contrive to call sqlite3_snapshot_recover() +# before all pages of the *-shm file have been mapped. This tests an +# extra branch of error handling logic in snapshot_recover(). +# +reset_db +do_execsql_test 4.1.0 { + PRAGMA page_size = 512; + PRAGMA journal_mode = wal; + PRAGMA wal_autocheckpoint = 0; + CREATE TABLE t1(zzz); + INSERT INTO t1 VALUES(randomblob( 500 * 9500 )); + PRAGMA user_version = 211; +} {wal 0} + +do_test 4.1.1 { + list [file size test.db-shm] [file size test.db] +} {98304 512} + +faultsim_save_and_close +do_faultsim_test 4.1 -faults shm* -prep { + catch { db2 close } + catch { db close } + faultsim_restore_and_reopen + sqlite3 db2 test.db + db2 eval { SELECT * FROM sqlite_master } + db eval BEGIN + sqlite3_snapshot_get_blob db main + db eval COMMIT +} -body { + sqlite3_snapshot_recover db main +} -test { + faultsim_test_result {0 {}} {1 SQLITE_IOERR} +} finish_test