Index: Makefile.in ================================================================== --- Makefile.in +++ Makefile.in @@ -460,10 +460,11 @@ $(TOP)/ext/misc/percentile.c \ $(TOP)/ext/misc/prefixes.c \ $(TOP)/ext/misc/regexp.c \ $(TOP)/ext/misc/remember.c \ $(TOP)/ext/misc/series.c \ + $(TOP)/ext/misc/snapshotrevert.c \ $(TOP)/ext/misc/spellfix.c \ $(TOP)/ext/misc/totype.c \ $(TOP)/ext/misc/unionvtab.c \ $(TOP)/ext/misc/wholenumber.c \ $(TOP)/ext/misc/zipfile.c \ Index: Makefile.msc ================================================================== --- Makefile.msc +++ Makefile.msc @@ -1580,10 +1580,11 @@ $(TOP)\ext\misc\percentile.c \ $(TOP)\ext\misc\prefixes.c \ $(TOP)\ext\misc\regexp.c \ $(TOP)\ext\misc\remember.c \ $(TOP)\ext\misc\series.c \ + $(TOP)\ext\misc\snapshotrevert.c \ $(TOP)\ext\misc\spellfix.c \ $(TOP)\ext\misc\totype.c \ $(TOP)\ext\misc\unionvtab.c \ $(TOP)\ext\misc\wholenumber.c ADDED ext/misc/snapshotrevert.c Index: ext/misc/snapshotrevert.c ================================================================== --- /dev/null +++ ext/misc/snapshotrevert.c @@ -0,0 +1,174 @@ +/* +** 2021 March 25 +** +** 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. +** +************************************************************************* +** +** Code to revert a database to a snapshot. The procedure for reverting a +** live database to the supplied snapshot is: +** +** 1. Open snapshot for reading. +** 2. Take exclusive CHECKPOINTER lock. +** 3. Take exclusive WRITER lock. +** 4. Clobber the current wal-index header with the snapshot. +** 5. Set nBackfill to 0. nBackfillAttempted is not modified. +** 6. Truncate wal file. +** 7. Release write lock. +** 8. Release checkpoint lock. +** 9. Close snapshot transaction. +** +** This extension exports a single API function: +** +** int sqlite3_snapshot_revert( +** sqlite3 *db, +** const char *zDb, +** sqlite3_snapshot *pSnap +** ); +** +** See comments above the implementation of this function below for details. +*/ +#include + +#include +#include + +#if !defined(SQLITE_TEST) || defined(SQLITE_ENABLE_SNAPSHOT) + +/* +** Values for the eLock parameter accepted by snapshotRevertLock() and +** snapshotRevertUnlock(). +*/ +#define SNAPSHOT_REVERT_CHECKPOINTER 2 +#define SNAPSHOT_REVERT_WRITER 0 + +static int snapshotRevertLock(sqlite3_file *pFd, int eLock){ + int f = SQLITE_SHM_LOCK | SQLITE_SHM_EXCLUSIVE; + return pFd->pMethods->xShmLock(pFd, eLock, 1, f); +} + +static int snapshotRevertUnlock(sqlite3_file *pFd, int eLock){ + int f = SQLITE_SHM_UNLOCK | SQLITE_SHM_EXCLUSIVE; + return pFd->pMethods->xShmLock(pFd, eLock, 1, f); +} + +/* +** Revert database zDb of connection db to the state it was in when snapshot +** pSnap was taken. The database handle must be in auto-commit mode and +** not have an open read or write transction on zDb when this function is +** called. +** +** This function uses normal SQLite locks to ensure that the database is +** not corrupted by a simultaneous writer or checkpointer. However, the +** effects of a successful call to this function on readers that are +** reading from a snapshot newer than the snapshot supplied as the +** third argument are undefined. +** +** Return SQLITE_OK if successful, or an SQLite error code otherwise. +*/ +int sqlite3_snapshot_revert( + sqlite3 *db, + const char *zDb, + sqlite3_snapshot *pSnap +){ + sqlite3_file *pDbFd = 0; + sqlite3_file *pWalFd = 0; + int rc; + volatile void *pShm = 0; + sqlite3_stmt *pCommit = 0; + int nLock = 0; /* Successful snapshotRevertLock() calls */ + + /* Put the db handle in non-auto-commit mode, as required by the + ** sqlite3_snapshot_open() API. + ** + ** Also prepare a "COMMIT" command to end the transaction. Such a VM does not + ** need to allocate memory or do anything else that is likely to fail, + ** so we ignore the error code when it is eventually executed and assume + ** that the transaction was successfully closed. */ + rc = sqlite3_prepare_v2(db, "COMMIT", -1, &pCommit, 0); + if( rc==SQLITE_OK ){ + rc = sqlite3_exec(db, "BEGIN", 0, 0, 0); + } + if( rc!=SQLITE_OK ){ + sqlite3_finalize(pCommit); + return rc; + } + + /* 1. Open snapshot for reading */ + rc = sqlite3_snapshot_open(db, zDb, pSnap); + + /* Obtain pointers to the database file handle, the shared-memory mapping, + ** and the wal file handle. */ + if( rc==SQLITE_OK ){ + const int op = SQLITE_FCNTL_FILE_POINTER; + rc = sqlite3_file_control(db, zDb, op, (void*)&pDbFd); + } + if( rc==SQLITE_OK ){ + rc = pDbFd->pMethods->xShmMap(pDbFd, 0, 32*1024, 1, &pShm); + } + if( rc==SQLITE_OK ){ + const int op = SQLITE_FCNTL_JOURNAL_POINTER; + rc = sqlite3_file_control(db, zDb, op, (void*)&pWalFd); + } + + /* 2. Take exclusive CHECKPOINTER lock */ + if( rc==SQLITE_OK ){ + rc = snapshotRevertLock(pDbFd, SNAPSHOT_REVERT_CHECKPOINTER); + if( rc==SQLITE_OK ) nLock = 1; + } + + /* 3. Take exclusive WRITER lock */ + if( rc==SQLITE_OK ){ + rc = snapshotRevertLock(pDbFd, SNAPSHOT_REVERT_WRITER); + if( rc==SQLITE_OK ) nLock = 2; + } + + if( rc==SQLITE_OK ){ + /* Constants from https://www.sqlite.org/walformat.html#walidxfmt */ + const int nWalHdrSz = 32; /* Size of wal file header */ + const int nIdxHdrSz = 48; /* Size of each WalIndexHdr */ + const int nFrameHdrSz = 24; /* Size of each frame header */ + const int iHdrOff1 = 0; /* Offset of first WalIndexHdr */ + const int iHdrOff2 = 48; /* Offset of second WalIndexHdr */ + const int iBackfillOff = 96; /* offset of 32-bit nBackfill value */ + const int iPgszOff = 14; /* Offset of 16-bit page-size value */ + const int iMxFrameOff = 16; /* Offset of 32-bit mxFrame value */ + + unsigned char *a = (unsigned char*)pShm; + int pgsz; /* Database page size */ + int mxFrame; /* Valid frames in wal file after revert */ + sqlite3_int64 szWal; /* Size in bytes to truncate wal file to */ + + /* 4. Clobber the current wal-index header with the snapshot. */ + memcpy(&a[iHdrOff1], pSnap, nIdxHdrSz); + memcpy(&a[iHdrOff2], pSnap, nIdxHdrSz); + + /* 5. Set nBackfill to 0. nBackfillAttempted is not modified. */ + *(int*)&a[iBackfillOff] = 0; + + /* 6. Truncate the wal file */ + assert( sizeof(unsigned short int)==2 ); + pgsz = *(unsigned short int*)&a[iPgszOff]; + if( pgsz==1 ) pgsz = 65536; + mxFrame = *(int*)&a[iMxFrameOff]; + szWal = (sqlite3_int64)mxFrame * (pgsz + nFrameHdrSz) + nWalHdrSz; + rc = pWalFd->pMethods->xTruncate(pWalFd, szWal); + } + + /* Steps 8 and 9 - drop locks if they were acquired */ + if( nLock==2 ) snapshotRevertUnlock(pDbFd, SNAPSHOT_REVERT_WRITER); + if( nLock>0 ) snapshotRevertUnlock(pDbFd, SNAPSHOT_REVERT_CHECKPOINTER); + + /* End the snapshot transaction, if one was opened. */ + sqlite3_step(pCommit); + sqlite3_finalize(pCommit); + + return rc; +} + +#endif /* !defined(SQLITE_TEST) || defined(SQLITE_ENABLE_SNAPSHOT) */ Index: main.mk ================================================================== --- main.mk +++ main.mk @@ -377,10 +377,11 @@ $(TOP)/ext/misc/percentile.c \ $(TOP)/ext/misc/prefixes.c \ $(TOP)/ext/misc/regexp.c \ $(TOP)/ext/misc/remember.c \ $(TOP)/ext/misc/series.c \ + $(TOP)/ext/misc/snapshotrevert.c \ $(TOP)/ext/misc/spellfix.c \ $(TOP)/ext/misc/totype.c \ $(TOP)/ext/misc/unionvtab.c \ $(TOP)/ext/misc/wholenumber.c \ $(TOP)/ext/misc/zipfile.c \ Index: src/test1.c ================================================================== --- src/test1.c +++ src/test1.c @@ -2572,10 +2572,45 @@ Tcl_SetObjResult(interp, Tcl_NewIntObj(res)); return TCL_OK; } #endif /* SQLITE_ENABLE_SNAPSHOT */ +#ifdef SQLITE_ENABLE_SNAPSHOT +/* +** Usage: sqlite3_snapshot_revert DB DBNAME SNAPSHOT +*/ +static int SQLITE_TCLAPI test_snapshot_revert( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + extern int sqlite3_snapshot_revert(sqlite3*, const char*, sqlite3_snapshot*); + 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_revert(db, zName, pSnapshot); + if( rc!=SQLITE_OK ){ + Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1)); + return TCL_ERROR; + }else{ + Tcl_ResetResult(interp); + } + return TCL_OK; +} +#endif + /* ** Usage: sqlite3_delete_database FILENAME */ int sqlite3_delete_database(const char*); /* in test_delete.c */ static int SQLITE_TCLAPI test_delete_database( @@ -8523,10 +8558,11 @@ { "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 }, + { "sqlite3_snapshot_revert", test_snapshot_revert, 0 }, #endif { "sqlite3_delete_database", test_delete_database, 0 }, { "sqlite3_wal_info", test_wal_info, 0 }, { "atomic_batch_write", test_atomic_batch_write, 0 }, { "sqlite3_mmap_warm", test_mmap_warm, 0 }, ADDED test/snapshot_revert.test Index: test/snapshot_revert.test ================================================================== --- /dev/null +++ test/snapshot_revert.test @@ -0,0 +1,157 @@ +# 2021 March 25 +# +# 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. +# +#*********************************************************************** +# +# Tests for the sqlite3_snapshot_revert() extension. +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +ifcapable !snapshot {finish_test; return} +set testprefix snapshot_revert + +do_execsql_test 1.0 { + CREATE TABLE t1(a, b, c); + PRAGMA journal_mode = wal; + INSERT INTO t1 VALUES(1, 2, 3); + INSERT INTO t1 VALUES(4, 5, 6); + INSERT INTO t1 VALUES(7, 8, 9); +} {wal} + +do_test 1.1 { + execsql BEGIN + set ::snap1 [sqlite3_snapshot_get db main] + execsql COMMIT +} {} + +do_execsql_test 1.2 { + INSERT INTO t1 VALUES(10, 11, 12); + INSERT INTO t1 VALUES(13, 14, 15); +} + +sqlite3 db2 test.db +do_execsql_test -db db2 1.3 { + SELECT * FROM t1 +} {1 2 3 4 5 6 7 8 9 10 11 12 13 14 15} + +do_test 1.4 { + sqlite3_snapshot_revert db main $::snap1 +} {} +sqlite3_snapshot_free $::snap1 + +do_execsql_test -db db2 1.5 { + SELECT * FROM t1 +} {1 2 3 4 5 6 7 8 9} +db2 close + +db_save_and_close +db_restore_and_reopen + +do_execsql_test 1.6 { + SELECT * FROM t1 +} {1 2 3 4 5 6 7 8 9} + +#------------------------------------------------------------------------- +# Test some error conditions: +# +# 2.1.* Error - already in non-auto-commit mode. +# 2.2.* Error - snapshot has already been checkpointed away. +# 2.3.* Error - cannot get WRITER lock. +# 2.4.* Check that CHECKPOINTER is released if cannot get WRITER +# +reset_db +do_execsql_test 2.0.1 { + PRAGMA auto_vacuum = 0; + PRAGMA journal_mode = wal; + CREATE TABLE x1(x, y); + INSERT INTO x1 VALUES('A', 'B'), ('C', 'D'); +} {wal} +do_test 2.0.2 { + execsql BEGIN + set ::snap1 [sqlite3_snapshot_get db main] + execsql COMMIT +} {} + +do_test 2.1.0 { + execsql BEGIN + list [catch { sqlite3_snapshot_revert db main $::snap1 } msg] $msg +} {1 SQLITE_ERROR} +do_test 2.1.1 { + execsql COMMIT + list [catch { sqlite3_snapshot_revert db main $::snap1 } msg] $msg +} {0 {}} + +do_test 2.2.0 { + execsql { + INSERT INTO x1 VALUES('E', 'F'); + DELETE FROM x1 WHERE x='A'; + PRAGMA wal_checkpoint; + } + list [catch { sqlite3_snapshot_revert db main $::snap1 } msg] $msg +} {1 SQLITE_ERROR_SNAPSHOT} +sqlite3_snapshot_free $::snap1 + +sqlite3 db2 test.db +do_test 2.3.0 { + execsql { + INSERT INTO x1 VALUES('G', 'H'); + BEGIN; + } + set ::snap1 [sqlite3_snapshot_get db main] + execsql { + DELETE FROM x1 WHERE x='C'; + COMMIT; + } +} {} +do_test 2.3.1 { + execsql { BEGIN EXCLUSIVE } db2 + list [catch { sqlite3_snapshot_revert db main $::snap1 } msg] $msg +} {1 SQLITE_BUSY} +do_execsql_test 2.3.2 { + SELECT * FROM x1 +} {E F G H} +do_test 2.3.3 { + execsql { COMMIT } db2 + list [catch { sqlite3_snapshot_revert db main $::snap1 } msg] $msg +} {0 {}} +do_execsql_test 2.3.4 { + SELECT * FROM x1 +} {C D E F G H} +sqlite3_snapshot_free $::snap1 + +do_test 2.4.0 { + execsql { + BEGIN; + } + set ::snap1 [sqlite3_snapshot_get db main] + execsql { + INSERT INTO x1 VALUES('I', 'J'); + DELETE FROM x1 WHERE x IN ('C', 'E'); + COMMIT; + } +} {} +do_execsql_test -db db2 2.4.1 { + BEGIN EXCLUSIVE; + SELECT * FROM x1; +} {G H I J} +do_test 2.4.2 { + list [catch { sqlite3_snapshot_revert db main $::snap1 } msg] $msg +} {1 SQLITE_BUSY} +do_execsql_test -db db2 2.4.3 { + COMMIT; + PRAGMA wal_checkpoint; +} {0 2 2} +sqlite3_snapshot_free $::snap1 + + +db close +db2 close +finish_test +