From: dan Date: Tue, 2 Sep 2014 19:59:40 +0000 (+0000) Subject: Add an experimental extension for applying bulk updates to databases. X-Git-Tag: version-3.8.11~252^2~117 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b0083756f274674359f3d8b2e1aef3c3ef7530af;p=thirdparty%2Fsqlite.git Add an experimental extension for applying bulk updates to databases. FossilOrigin-Name: 2954ab501049968430011b63d046eb42ff37a56c --- diff --git a/ext/ota/ota1.test b/ext/ota/ota1.test new file mode 100644 index 0000000000..aa334b9581 --- /dev/null +++ b/ext/ota/ota1.test @@ -0,0 +1,181 @@ +# 2014 August 30 +# +# 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. +# +#*********************************************************************** +# + +set testdir [file join [file dirname $argv0] .. .. test] +source $testdir/tester.tcl +set ::testprefix ota1 + + +# Create a simple OTA database. That expects to write to a table: +# +# CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c); +# +proc create_ota1 {filename} { + forcedelete $filename + sqlite3 ota1 $filename + ota1 eval { + CREATE TABLE data_t1(a, b, c, ota_control); + INSERT INTO data_t1 VALUES(1, 2, 3, 0); + INSERT INTO data_t1 VALUES(2, 'two', 'three', 0); + INSERT INTO data_t1 VALUES(3, NULL, 8.2, 0); + } + ota1 close + return $filename +} + +# Create an empty target database suitable for the OTA created by +# [create_ota1]. +# +# CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c); +# +proc create_target1 {filename} { + forcedelete $filename + sqlite3 target1 $filename + target1 eval { CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c) } + target1 close + return $filename +} + +# Run the OTA in file $ota on target database $target until completion. +# +proc run_ota {target ota} { + sqlite3ota ota $target $ota + while { [ota step]=="SQLITE_OK" } {} + ota close +} + +proc step_ota {target ota} { + while 1 { + sqlite3ota ota $target $ota + set rc [ota step] + ota close + if {$rc != "SQLITE_OK"} break + } + set rc +} + +foreach {tn2 cmd} {1 step_ota 2 run_ota} { + foreach {tn schema} { + 1 { + CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c); + } + 2 { + CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c); + CREATE INDEX i1 ON t1(b); + } + 3 { + CREATE TABLE t1(a PRIMARY KEY, b, c) WITHOUT ROWID; + } + 4 { + CREATE TABLE t1(a PRIMARY KEY, b, c) WITHOUT ROWID; + CREATE INDEX i1 ON t1(b); + } + 5 { + CREATE TABLE t1(a, b, c, PRIMARY KEY(a, c)) WITHOUT ROWID; + CREATE INDEX i1 ON t1(b); + } + 6 { + CREATE TABLE t1(a, b, c, PRIMARY KEY(c)) WITHOUT ROWID; + CREATE INDEX i1 ON t1(b, a); + } + 7 { + CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c); + CREATE INDEX i1 ON t1(b, c); + CREATE INDEX i2 ON t1(c, b); + CREATE INDEX i3 ON t1(a, b, c, a, b, c); + } + } { + reset_db + execsql $schema + + do_test 1.$tn2.$tn.1 { + create_ota1 ota.db + $cmd test.db ota.db + } {SQLITE_DONE} + + do_execsql_test 1.$tn2.$tn.2 { + SELECT * FROM t1 ORDER BY a ASC; + } { + 1 2 3 + 2 two three + 3 {} 8.2 + } + + do_execsql_test 1.$tn2.$tn.3 { PRAGMA integrity_check } ok + } +} + +#------------------------------------------------------------------------- +# Check that an OTA cannot be applied to a table that has no PK. +# +reset_db +create_ota1 ota.db +do_execsql_test 2.1 { CREATE TABLE t1(a, b, c) } +do_test 2.2 { + sqlite3ota ota test.db ota.db + ota step +} {SQLITE_ERROR} +do_test 2.3 { + list [catch { ota close } msg] $msg +} {1 {SQLITE_ERROR - table t1 has no PRIMARY KEY}} + +reset_db +do_execsql_test 2.4 { CREATE TABLE t1(a PRIMARY KEY, b, c) } +do_test 2.5 { + sqlite3ota ota test.db ota.db + ota step +} {SQLITE_ERROR} +do_test 2.6 { + list [catch { ota close } msg] $msg +} {1 {SQLITE_ERROR - table t1 has no PRIMARY KEY}} + +#------------------------------------------------------------------------- +# Check that if a UNIQUE constraint is violated the current and all +# subsequent [ota step] calls return SQLITE_CONSTRAINT. And that the OTA +# transaction is rolled back by the [ota close] that deletes the ota +# handle. +# +foreach {tn errcode errmsg schema} { + 1 SQLITE_CONSTRAINT "UNIQUE constraint failed: t1.a" { + CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c); + INSERT INTO t1 VALUES(3, 2, 1); + } + + 2 SQLITE_CONSTRAINT "UNIQUE constraint failed: t1.c" { + CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c UNIQUE); + INSERT INTO t1 VALUES(4, 2, 'three'); + } + +} { + reset_db + execsql $schema + set cksum [dbcksum db main] + + do_test 3.$tn.1 { + create_ota1 ota.db + sqlite3ota ota test.db ota.db + while {[set res [ota step]]=="SQLITE_OK"} {} + set res + } $errcode + + do_test 3.$tn.2 { ota step } $errcode + + do_test 3.$tn.3 { + list [catch { ota close } msg] $msg + } [list 1 "$errcode - $errmsg"] + + do_test 3.$tn.4 { dbcksum db main } $cksum +} + + +finish_test + diff --git a/ext/ota/ota2.test b/ext/ota/ota2.test new file mode 100644 index 0000000000..dfa4a48e9d --- /dev/null +++ b/ext/ota/ota2.test @@ -0,0 +1,62 @@ +# 2014 August 30 +# +# 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. +# +#*********************************************************************** +# + +set testdir [file join [file dirname $argv0] .. .. test] +source $testdir/tester.tcl +set ::testprefix ota2 + + +do_execsql_test 1.0 { + PRAGMA ota_mode = 1; + PRAGMA journal_mode = wal; + CREATE TABLE t1(a, b); + BEGIN; + INSERT INTO t1 VALUES(1, 2); +} {wal} + +do_test 1.1 { + set state [sqlite3_transaction_save db] + db close + file exists test.db-wal +} {1} + +do_test 1.2 { + sqlite3 db test.db + db eval {SELECT * FROM t1} +} {} + +do_test 1.3 { + execsql {BEGIN IMMEDIATE} + sqlite3_transaction_restore db $::state + db eval {SELECT * FROM t1} +} {1 2} + +do_test 1.4 { + execsql { + INSERT INTO t1 VALUES(3, 4); + COMMIT; + SELECT * FROM t1; + } +} {1 2 3 4} + +do_test 1.5 { + db close + file exists test.db-wal +} {0} + +do_test 1.5 { + sqlite3 db test.db + db eval {SELECT * FROM t1} +} {1 2 3 4} + +finish_test + diff --git a/ext/ota/sqlite3ota.c b/ext/ota/sqlite3ota.c new file mode 100644 index 0000000000..5483b9049f --- /dev/null +++ b/ext/ota/sqlite3ota.c @@ -0,0 +1,811 @@ +/* +** 2014 August 30 +** +** 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. +** +************************************************************************* +*/ + +#include +#include + +#include "sqlite3.h" +#include "sqlite3ota.h" + + +/* +** The ota_state table is used to save the state of a partially applied +** update so that it can be resumed later. The table contains at most a +** single row: +** +** "wal_state" -> Blob to use with sqlite3_transaction_restore(). +** +** "tbl" -> Table currently being written (target database names). +** +** "idx" -> Index currently being written (target database names). +** Or, if the main table is being written, a NULL value. +** +** "row" -> Last rowid processed from ota database table (i.e. data_%). +** +** "progress" -> total number of key/value b-tree operations performed +** so far as part of this ota update. +*/ +#define OTA_CREATE_STATE "CREATE TABLE IF NOT EXISTS ota_state" \ + "(wal_state, tbl, idx, row, progress)" + +typedef struct OtaTblIter OtaTblIter; +typedef struct OtaIdxIter OtaIdxIter; + +/* +** Iterator used to iterate through all data tables in the OTA. As follows: +** +** OtaTblIter iter; +** for(rc=tblIterFirst(db, &iter); +** rc==SQLITE_OK && iter.zTarget; +** rc=tblIterNext(&iter) +** ){ +** } +*/ +struct OtaTblIter { + sqlite3_stmt *pTblIter; /* Iterate through tables */ + int iEntry; /* Index of current entry (from 1) */ + + /* Output varibles. zTarget==0 implies EOF. */ + const char *zTarget; /* Name of target table */ + const char *zSource; /* Name of source table */ + + /* Useful things populated by a call to tblIterPrepareAll() */ + int nCol; /* Number of columns in this table */ + char **azCol; /* Array of quoted column names */ + sqlite3_stmt *pSelect; /* PK b-tree SELECT statement */ + sqlite3_stmt *pInsert; /* PK b-tree INSERT statement */ +}; + +/* +** API is: +** +** idxIterFirst() +** idxIterNext() +** idxIterFinalize() +** idxIterPrepareAll() +*/ +struct OtaIdxIter { + sqlite3_stmt *pIdxIter; /* Iterate through indexes */ + int iEntry; /* Index of current entry (from 1) */ + + /* Output varibles. zTarget==0 implies EOF. */ + const char *zIndex; /* Name of index */ + + int nCol; /* Number of columns in index */ + int *aiCol; /* Array of column indexes */ + sqlite3_stmt *pWriter; /* Index writer */ + sqlite3_stmt *pSelect; /* Select to read values in index order */ +}; + + +struct sqlite3ota { + sqlite3 *dbDest; /* Target db */ + sqlite3 *dbOta; /* Ota db */ + + int rc; /* Value returned by last ota_step() call */ + char *zErrmsg; /* Error message if rc!=SQLITE_OK */ + + OtaTblIter tbliter; /* Used to iterate through tables */ + OtaIdxIter idxiter; /* Used to iterate through indexes */ +}; + +static int prepareAndCollectError( + sqlite3 *db, + const char *zSql, + sqlite3_stmt **ppStmt, + char **pzErrmsg +){ + int rc = sqlite3_prepare_v2(db, zSql, -1, ppStmt, 0); + if( rc!=SQLITE_OK ){ + *pzErrmsg = sqlite3_mprintf("%s", sqlite3_errmsg(db)); + *ppStmt = 0; + } + return rc; +} + +/* +** Unless it is NULL, argument zSql points to a buffer allocated using +** sqlite3_malloc containing an SQL statement. This function prepares the SQL +** statement against database db and frees the buffer. If statement +** compilation is successful, *ppStmt is set to point to the new statement +** handle and SQLITE_OK is returned. +** +** Otherwise, if an error occurs, *ppStmt is set to NULL and an error code +** returned. In this case, *pzErrmsg may also be set to point to an error +** message. It is the responsibility of the caller to free this error message +** buffer using sqlite3_free(). +** +** If argument zSql is NULL, this function assumes that an OOM has occurred. +** In this case SQLITE_NOMEM is returned and *ppStmt set to NULL. +*/ +static int prepareFreeAndCollectError( + sqlite3 *db, + char *zSql, + sqlite3_stmt **ppStmt, + char **pzErrmsg +){ + int rc; + assert( *pzErrmsg==0 ); + if( zSql==0 ){ + rc = SQLITE_NOMEM; + *ppStmt = 0; + }else{ + rc = prepareAndCollectError(db, zSql, ppStmt, pzErrmsg); + sqlite3_free(zSql); + } + return rc; +} + +static char *quoteSqlName(const char *zName){ + int nName = strlen(zName); + char *zRet = sqlite3_malloc(nName * 2 + 2 + 1); + if( zRet ){ + int i; + char *p = zRet; + *p++ = '"'; + for(i=0; itbliter; + int rc = SQLITE_OK; + char *zCol = 0; + char *zBindings = 0; + char *zSql; + sqlite3_stmt *pPragma = 0; + int i; + int bSeenPk = 0; /* Set to true once PK column seen */ + + /* Allocate and populate the azCol[] array */ + zSql = sqlite3_mprintf("PRAGMA main.table_info(%Q)", pIter->zTarget); + rc = prepareFreeAndCollectError(p->dbDest, zSql, &pPragma, &p->zErrmsg); + pIter->nCol = 0; + if( rc==SQLITE_OK ){ + while( SQLITE_ROW==sqlite3_step(pPragma) ){ + const char *zName = (const char*)sqlite3_column_text(pPragma, 1); + if( (pIter->nCol % 4)==0 ){ + int nByte = sizeof(char*) * (pIter->nCol+4); + char **azNew = (char**)sqlite3_realloc(pIter->azCol, nByte); + if( azNew==0 ){ + rc = SQLITE_NOMEM; + break; + } + pIter->azCol = azNew; + } + pIter->azCol[pIter->nCol] = quoteSqlName(zName); + if( pIter->azCol[pIter->nCol]==0 ){ + rc = SQLITE_NOMEM; + break; + } + pIter->nCol++; + if( sqlite3_column_int(pPragma, 5) ) bSeenPk = 1; + } + if( rc==SQLITE_OK ){ + rc = sqlite3_finalize(pPragma); + }else{ + sqlite3_finalize(pPragma); + } + } + + /* If the table has no PRIMARY KEY, throw an exception. */ + if( bSeenPk==0 ){ + p->zErrmsg = sqlite3_mprintf("table %s has no PRIMARY KEY", pIter->zTarget); + rc = SQLITE_ERROR; + } + + /* Populate the zCol variable */ + for(i=0; rc==SQLITE_OK && inCol; i++){ + zCol = sqlite3_mprintf("%z%s%s", zCol, (i==0?"":", "), pIter->azCol[i]); + if( zCol==0 ){ + rc = SQLITE_NOMEM; + } + } + + /* Allocate and populate zBindings */ + if( rc==SQLITE_OK ){ + zBindings = (char*)sqlite3_malloc(pIter->nCol * 2); + if( zBindings==0 ){ + rc = SQLITE_NOMEM; + }else{ + int i; + for(i=0; inCol; i++){ + zBindings[i*2] = '?'; + zBindings[i*2+1] = ','; + } + zBindings[pIter->nCol*2-1] = '\0'; + } + } + + /* Create OtaTblIter.pSelect */ + if( rc==SQLITE_OK ){ + zSql = sqlite3_mprintf("SELECT rowid, %s FROM %Q", zCol, pIter->zSource); + rc = prepareFreeAndCollectError(p->dbOta,zSql,&pIter->pSelect, &p->zErrmsg); + } + + /* Create OtaTblIter.pInsert */ + if( rc==SQLITE_OK ){ + zSql = sqlite3_mprintf("INSERT INTO %Q(%s) VALUES(%s)", + pIter->zTarget, zCol, zBindings + ); + rc = prepareFreeAndCollectError(p->dbDest,zSql,&pIter->pInsert,&p->zErrmsg); + } + + sqlite3_free(zCol); + sqlite3_free(zBindings); + return rc; +} + +static void tblIterFreeAll(OtaTblIter *pIter){ + int i; + + sqlite3_finalize(pIter->pSelect); + sqlite3_finalize(pIter->pInsert); + for(i=0; inCol; i++) sqlite3_free(pIter->azCol[i]); + sqlite3_free(pIter->azCol); + pIter->azCol = 0; + pIter->pSelect = 0; + pIter->pInsert = 0; + pIter->nCol = 0; +} + +static int tblIterNext(OtaTblIter *pIter){ + int rc; + + tblIterFreeAll(pIter); + assert( pIter->pTblIter ); + rc = sqlite3_step(pIter->pTblIter); + if( rc==SQLITE_ROW ){ + pIter->zSource = (const char*)sqlite3_column_text(pIter->pTblIter, 0); + pIter->zTarget = &pIter->zSource[5]; assert( 5==strlen("data_") ); + pIter->iEntry++; + }else{ + pIter->zSource = 0; + pIter->zTarget = 0; + } + + if( rc==SQLITE_ROW || rc==SQLITE_DONE ) rc = SQLITE_OK; + return rc; +} + +static int tblIterFirst(sqlite3 *db, OtaTblIter *pIter){ + int rc; /* return code */ + memset(pIter, 0, sizeof(OtaTblIter)); + rc = sqlite3_prepare_v2(db, + "SELECT name FROM sqlite_master " + "WHERE type='table' AND name LIKE 'data_%'", -1, &pIter->pTblIter, 0 + ); + if( rc==SQLITE_OK ){ + rc = tblIterNext(pIter); + } + return rc; +} + + +static void tblIterFinalize(OtaTblIter *pIter){ + tblIterFreeAll(pIter); + sqlite3_finalize(pIter->pTblIter); + memset(pIter, 0, sizeof(OtaTblIter)); +} + +static void idxIterFreeAll(OtaIdxIter *pIter){ + sqlite3_finalize(pIter->pWriter); + sqlite3_finalize(pIter->pSelect); + pIter->pWriter = 0; + pIter->pSelect = 0; + pIter->aiCol = 0; + pIter->nCol = 0; +} + +static int idxIterPrepareAll(sqlite3ota *p){ + int rc; + int i; /* Iterator variable */ + char *zSql = 0; + char *zCols = 0; /* Columns list */ + OtaIdxIter *pIter = &p->idxiter; + + /* Prepare the writer statement to write (insert) entries into the index. */ + rc = sqlite3_index_writer( + p->dbDest, 0, pIter->zIndex, &pIter->pWriter, &pIter->aiCol, &pIter->nCol + ); + + /* Prepare a SELECT statement to read values from the source table in + ** the same order as they are stored in the current index. The statement + ** is: + ** + ** SELECT rowid, FROM data_ ORDER BY + */ + for(i=0; rc==SQLITE_OK && inCol; i++){ + const char *zQuoted = p->tbliter.azCol[ pIter->aiCol[i] ]; + zCols = sqlite3_mprintf("%z%s%s", zCols, zCols?", ":"", zQuoted); + if( !zCols ){ + rc = SQLITE_NOMEM; + } + } + if( rc==SQLITE_OK ){ + const char *zFmt = "SELECT rowid, %s FROM %Q ORDER BY %s"; + zSql = sqlite3_mprintf(zFmt, zCols, p->tbliter.zSource, zCols); + if( zSql ){ + sqlite3_stmt **pp = &p->idxiter.pSelect; + rc = prepareFreeAndCollectError(p->dbOta, zSql, pp, &p->zErrmsg); + }else{ + rc = SQLITE_NOMEM; + } + } + + sqlite3_free(zCols); + return rc; +} + +static int idxIterNext(OtaIdxIter *pIter){ + int rc; + + idxIterFreeAll(pIter); + assert( pIter->pIdxIter ); + rc = sqlite3_step(pIter->pIdxIter); + if( rc==SQLITE_ROW ){ + pIter->zIndex = (const char*)sqlite3_column_text(pIter->pIdxIter, 0); + pIter->iEntry++; + }else{ + pIter->zIndex = 0; + rc = sqlite3_finalize(pIter->pIdxIter); + pIter->pIdxIter = 0; + } + + if( rc==SQLITE_ROW ) rc = SQLITE_OK; + return rc; +} + +static int idxIterFirst(sqlite3 *db, const char *zTable, OtaIdxIter *pIter){ + int rc; /* return code */ + memset(pIter, 0, sizeof(OtaIdxIter)); + rc = sqlite3_prepare_v2(db, + "SELECT name FROM sqlite_master " + "WHERE type='index' AND tbl_name = ?", -1, &pIter->pIdxIter, 0 + ); + if( rc==SQLITE_OK ){ + rc = sqlite3_bind_text(pIter->pIdxIter, 1, zTable, -1, SQLITE_TRANSIENT); + } + if( rc==SQLITE_OK ){ + rc = idxIterNext(pIter); + } + return rc; +} + +static void idxIterFinalize(OtaIdxIter *pIter){ + idxIterFreeAll(pIter); + sqlite3_finalize(pIter->pIdxIter); + memset(pIter, 0, sizeof(OtaIdxIter)); +} + +/* +** Call sqlite3_reset() on the SQL statement passed as the second argument. +** If it returns anything other than SQLITE_OK, store the error code and +** error message in the OTA handle. +*/ +static void otaResetStatement(sqlite3ota *p, sqlite3_stmt *pStmt){ + assert( p->rc==SQLITE_OK ); + assert( p->zErrmsg==0 ); + p->rc = sqlite3_reset(pStmt); + if( p->rc!=SQLITE_OK ){ + sqlite3 *db = sqlite3_db_handle(pStmt); + p->zErrmsg = sqlite3_mprintf("%s", sqlite3_errmsg(db)); + } +} + +/* +** Check that all SQL statements required to process the current +** table and index have been prepared. If not, prepare them. If +** an error occurs, store the error code and message in the OTA +** handle before returning. +*/ +static int otaPrepareAll(sqlite3ota *p){ + assert( p->rc==SQLITE_OK ); + assert( p->zErrmsg==0 ); + assert( p->tbliter.zTarget ); + + if( p->tbliter.pSelect==0 ){ + p->rc = tblIterPrepareAll(p); + } + if( p->rc==SQLITE_OK && p->idxiter.zIndex && 0==p->idxiter.pSelect ){ + p->rc = idxIterPrepareAll(p); + } + return p->rc; +} + +int sqlite3ota_step(sqlite3ota *p){ + if( p ){ + while( p && p->rc==SQLITE_OK && p->tbliter.zTarget ){ + sqlite3_stmt *pSelect; + int i; + + otaPrepareAll(p); + pSelect = (p->idxiter.zIndex ? p->idxiter.pSelect : p->tbliter.pSelect); + + /* Advance to the next input row. */ + if( p->rc==SQLITE_OK ){ + int rc = sqlite3_step(pSelect); + if( rc!=SQLITE_ROW ){ + otaResetStatement(p, pSelect); + + /* Go to the next index. */ + if( p->rc==SQLITE_OK ){ + if( p->idxiter.zIndex ){ + p->rc = idxIterNext(&p->idxiter); + }else{ + p->rc = idxIterFirst(p->dbDest, p->tbliter.zTarget, &p->idxiter); + } + } + + /* If there is no next index, go to the next table. */ + if( p->rc==SQLITE_OK && p->idxiter.zIndex==0 ){ + p->rc = tblIterNext(&p->tbliter); + } + continue; + } + } + + /* Update the target database PK table according to the row that + ** tbliter.pSelect currently points to. + ** + ** todo: For now, we assume all rows are INSERT commands - this will + ** change. */ + if( p->rc==SQLITE_OK ){ + sqlite3_stmt *pInsert; + int nCol; + if( p->idxiter.zIndex ){ + pInsert = p->idxiter.pWriter; + nCol = p->idxiter.nCol; + }else{ + pInsert = p->tbliter.pInsert; + nCol = p->tbliter.nCol; + } + + for(i=0; irc==SQLITE_OK && p->tbliter.zTarget==0 ) p->rc = SQLITE_DONE; + } + + return (p ? p->rc : SQLITE_NOMEM); +} + +static void otaOpenDatabase(sqlite3ota *p, sqlite3 **pDb, const char *zFile){ + if( p->rc==SQLITE_OK ){ + p->rc = sqlite3_open(zFile, pDb); + if( p->rc ){ + const char *zErr = sqlite3_errmsg(*pDb); + p->zErrmsg = sqlite3_mprintf("sqlite3_open(): %s", zErr); + } + } +} + +static void otaSaveTransactionState(sqlite3ota *p){ + sqlite3_stmt *pStmt = 0; + void *pWalState = 0; + int nWalState = 0; + int rc; + + const char *zInsert = + "INSERT INTO ota_state(wal_state, tbl, idx, row, progress)" + "VALUES(:wal_state, :tbl, :idx, :row, :progress)"; + + rc = sqlite3_transaction_save(p->dbDest, &pWalState, &nWalState); + if( rc==SQLITE_OK ){ + rc = sqlite3_exec(p->dbOta, "DELETE FROM ota_state", 0, 0, 0); + } + if( rc==SQLITE_OK ){ + rc = prepareAndCollectError(p->dbOta, zInsert, &pStmt, &p->zErrmsg); + } + if( rc==SQLITE_OK ){ + sqlite3_stmt *pSelect; + pSelect = (p->idxiter.zIndex ? p->idxiter.pSelect : p->tbliter.pSelect); + sqlite3_bind_blob(pStmt, 1, pWalState, nWalState, SQLITE_STATIC); + sqlite3_bind_text(pStmt, 2, p->tbliter.zTarget, -1, SQLITE_STATIC); + if( p->idxiter.zIndex ){ + sqlite3_bind_text(pStmt, 3, p->idxiter.zIndex, -1, SQLITE_STATIC); + } + sqlite3_bind_int64(pStmt, 4, sqlite3_column_int64(pSelect, 0)); + sqlite3_step(pStmt); + rc = sqlite3_finalize(pStmt); + if( rc==SQLITE_OK ){ + rc = sqlite3_exec(p->dbOta, "COMMIT", 0, 0, 0); + } + if( rc!=SQLITE_OK ){ + p->zErrmsg = sqlite3_mprintf("%s", sqlite3_errmsg(p->dbOta)); + } + } + sqlite3_free(pWalState); + assert( p->rc==SQLITE_OK ); + p->rc = rc; +} + +static void otaLoadTransactionState(sqlite3ota *p){ + sqlite3_stmt *pStmt = 0; + int rc; + + const char *zSelect = + "SELECT wal_state, tbl, idx, row, progress FROM ota_state"; + + rc = prepareAndCollectError(p->dbOta, zSelect, &pStmt, &p->zErrmsg); + if( rc==SQLITE_OK ){ + if( SQLITE_ROW==sqlite3_step(pStmt) ){ + const void *pWalState = 0; + int nWalState = 0; + const char *zTbl; + const char *zIdx; + sqlite3_int64 iRowid; + + pWalState = sqlite3_column_blob(pStmt, 0); + nWalState = sqlite3_column_bytes(pStmt, 0); + zTbl = (const char*)sqlite3_column_text(pStmt, 1); + zIdx = (const char*)sqlite3_column_text(pStmt, 2); + iRowid = sqlite3_column_int64(pStmt, 3); + rc = sqlite3_transaction_restore(p->dbDest, pWalState, nWalState); + + while( rc==SQLITE_OK + && p->tbliter.zTarget + && sqlite3_stricmp(p->tbliter.zTarget, zTbl) + ){ + rc = tblIterNext(&p->tbliter); + } + if( rc==SQLITE_OK && !p->tbliter.zTarget ){ + rc = SQLITE_ERROR; + p->zErrmsg = sqlite3_mprintf("ota_state mismatch error"); + } + + if( rc==SQLITE_OK && zIdx ){ + rc = idxIterFirst(p->dbDest, p->tbliter.zTarget, &p->idxiter); + while( rc==SQLITE_OK + && p->idxiter.zIndex + && sqlite3_stricmp(p->idxiter.zIndex, zIdx) + ){ + rc = idxIterNext(&p->idxiter); + } + if( rc==SQLITE_OK && !p->idxiter.zIndex ){ + rc = SQLITE_ERROR; + p->zErrmsg = sqlite3_mprintf("ota_state mismatch error"); + } + } + + if( rc==SQLITE_OK ){ + rc = otaPrepareAll(p); + } + + if( rc==SQLITE_OK ){ + sqlite3_stmt *pSelect; + pSelect = (p->idxiter.zIndex ? p->idxiter.pSelect : p->tbliter.pSelect); + while( sqlite3_column_int64(pSelect, 0)!=iRowid ){ + rc = sqlite3_step(pSelect); + if( rc!=SQLITE_ROW ) break; + } + if( rc==SQLITE_ROW ){ + rc = SQLITE_OK; + }else{ + rc = SQLITE_ERROR; + p->zErrmsg = sqlite3_mprintf("ota_state mismatch error"); + } + } + } + if( rc==SQLITE_OK ){ + rc = sqlite3_finalize(pStmt); + }else{ + sqlite3_finalize(pStmt); + } + } + p->rc = rc; +} + + +/* +** Open and return a new OTA handle. +*/ +sqlite3ota *sqlite3ota_open(const char *zTarget, const char *zOta){ + sqlite3ota *p; + + p = (sqlite3ota*)sqlite3_malloc(sizeof(sqlite3ota)); + if( p ){ + + /* Open the target database */ + memset(p, 0, sizeof(sqlite3ota)); + otaOpenDatabase(p, &p->dbDest, zTarget); + otaOpenDatabase(p, &p->dbOta, zOta); + + /* If it has not already been created, create the ota_state table */ + if( p->rc==SQLITE_OK ){ + p->rc = sqlite3_exec(p->dbOta, OTA_CREATE_STATE, 0, 0, &p->zErrmsg); + } + + if( p->rc==SQLITE_OK ){ + const char *zScript = + "PRAGMA ota_mode=1;" + "PRAGMA journal_mode=wal;" + "BEGIN IMMEDIATE;" + ; + p->rc = sqlite3_exec(p->dbDest, zScript, 0, 0, &p->zErrmsg); + } + + if( p->rc==SQLITE_OK ){ + const char *zScript = "BEGIN IMMEDIATE"; + p->rc = sqlite3_exec(p->dbOta, zScript, 0, 0, &p->zErrmsg); + } + + /* Point the table iterator at the first table */ + if( p->rc==SQLITE_OK ){ + p->rc = tblIterFirst(p->dbOta, &p->tbliter); + } + + if( p->rc==SQLITE_OK ){ + otaLoadTransactionState(p); + } + } + + return p; +} + +static void otaCloseHandle(sqlite3 *db){ + int rc = sqlite3_close(db); + assert( rc==SQLITE_OK ); +} + +int sqlite3ota_close(sqlite3ota *p, char **pzErrmsg){ + int rc; + if( p ){ + + /* If the update has not been fully applied, save the state in + ** the ota db. If successful, this call also commits the open + ** transaction on the ota db. */ + assert( p->rc!=SQLITE_ROW ); + if( p->rc==SQLITE_OK ){ + assert( p->zErrmsg==0 ); + otaSaveTransactionState(p); + } + + /* Close all open statement handles. */ + tblIterFinalize(&p->tbliter); + idxIterFinalize(&p->idxiter); + + /* If the ota update has been fully applied, commit the transaction + ** on the target database. */ + if( p->rc==SQLITE_DONE ){ + rc = sqlite3_exec(p->dbDest, "COMMIT", 0, 0, &p->zErrmsg); + if( rc!=SQLITE_OK ) p->rc = rc; + } + + rc = p->rc; + *pzErrmsg = p->zErrmsg; + otaCloseHandle(p->dbDest); + otaCloseHandle(p->dbOta); + sqlite3_free(p); + }else{ + rc = SQLITE_NOMEM; + *pzErrmsg = 0; + } + return rc; +} + + +/**************************************************************************/ + +#ifdef SQLITE_TEST + +#include + +/* From main.c (apparently...) */ +extern const char *sqlite3ErrName(int); + +static int test_sqlite3ota_cmd( + ClientData clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + int ret = TCL_OK; + sqlite3ota *pOta = (sqlite3ota*)clientData; + const char *azMethod[] = { "step", "close", 0 }; + int iMethod; + + if( objc!=2 ){ + Tcl_WrongNumArgs(interp, 1, objv, "METHOD"); + return TCL_ERROR; + } + if( Tcl_GetIndexFromObj(interp, objv[1], azMethod, "method", 0, &iMethod) ){ + return TCL_ERROR; + } + + switch( iMethod ){ + case 0: /* step */ { + int rc = sqlite3ota_step(pOta); + Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1)); + break; + } + + case 1: /* close */ { + char *zErrmsg = 0; + int rc; + Tcl_DeleteCommand(interp, Tcl_GetString(objv[0])); + rc = sqlite3ota_close(pOta, &zErrmsg); + if( rc==SQLITE_OK || rc==SQLITE_DONE ){ + Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1)); + assert( zErrmsg==0 ); + }else{ + Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1)); + if( zErrmsg ){ + Tcl_AppendResult(interp, " - ", zErrmsg, 0); + sqlite3_free(zErrmsg); + } + ret = TCL_ERROR; + } + break; + } + + default: /* seems unlikely */ + assert( !"cannot happen" ); + break; + } + + return ret; +} + +/* +** Tclcmd: sqlite3ota CMD +*/ +static int test_sqlite3ota( + ClientData clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + sqlite3ota *pOta = 0; + const char *zCmd; + const char *zTarget; + const char *zOta; + + if( objc!=4 ){ + Tcl_WrongNumArgs(interp, 1, objv, "NAME TARGET-DB OTA-DB"); + return TCL_ERROR; + } + zCmd = Tcl_GetString(objv[1]); + zTarget = Tcl_GetString(objv[2]); + zOta = Tcl_GetString(objv[3]); + + pOta = sqlite3ota_open(zTarget, zOta); + Tcl_CreateObjCommand(interp, zCmd, test_sqlite3ota_cmd, (ClientData)pOta, 0); + Tcl_SetObjResult(interp, objv[1]); + return TCL_OK; +} + +int SqliteOta_Init(Tcl_Interp *interp){ + Tcl_CreateObjCommand(interp, "sqlite3ota", test_sqlite3ota, 0, 0); + return TCL_OK; +} + +#endif /* ifdef SQLITE_TEST */ + + + diff --git a/ext/ota/sqlite3ota.h b/ext/ota/sqlite3ota.h new file mode 100644 index 0000000000..0e577520b2 --- /dev/null +++ b/ext/ota/sqlite3ota.h @@ -0,0 +1,212 @@ +/* +** 2014 August 30 +** +** 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 contains the public interface for the OTA extension. +*/ + +/* +** SUMMARY +** +** Writing a transaction containing a large number of operations on +** b-tree indexes that are collectively larger than the available cache +** memory can be very inefficient. +** +** The problem is that in order to update a b-tree, the leaf page (at least) +** containing the entry being inserted or deleted must be modified. If the +** working set of leaves is larger than the available cache memory, then a +** single leaf that is modified more than once as part of the transaction +** may be loaded from or written to the persistent media more than once. +** Additionally, because the index updates are likely to be applied in +** random order, access to pages within the databse is also likely to be in +** random order, which is itself quite inefficient. +** +** One way to improve the situation is to sort the operations on each index +** by index key before applying them to the b-tree. This leads to an IO +** pattern that resembles a single linear scan through the index b-tree, +** and all but guarantees each modified leaf page is loaded and stored +** exactly once. SQLite uses this trick to improve the performance of +** CREATE INDEX commands. This extension allows it to be used to improve +** the performance of large transactions on existing databases. +** +** Additionally, this extension allows the work involved in writing the +** large transaction to be broken down into sub-transactions performed +** sequentially by separate processes. This is useful if the system cannot +** guarantee that a single update process may run for long enough to apply +** the entire update, for example because the update is running on a mobile +** device that is frequently rebooted. Even after the writer process has +** committed one or more sub-transactions, other database clients continue +** to read from the original database snapshot. In other words, partially +** applied transactions are not visible to other clients. +** +** "OTA" stands for "Over The Air" update. As in a large database update +** transmitted via a wireless network to a mobile device. A transaction +** applied using this extension is hence refered to as an "OTA update". +** +** +** LIMITATIONS +** +** An "OTA update" transaction is subject to the following limitations: +** +** * The transaction must consist of INSERT, UPDATE and DELETE operations +** only. +** +** * INSERT statements may not use any default values. +** +** * UPDATE and DELETE statements must identify their target rows by +** real PRIMARY KEY values - i.e. INTEGER PRIMARY KEY columns or +** by the PRIMARY KEY columns of WITHOUT ROWID tables. +** +** * UPDATE statements may not modify real PRIMARY KEY columns. +** +** * No triggers will be fired. +** +** * No foreign key violations are detected or reported. +** +** * No constraint handling mode except for "OR ROLLBACK" is supported. +** +** +** PREPARATION +** +** An "OTA update" is stored as a separate SQLite database. A database +** containing an OTA update is an "OTA database". For each table in the +** target database to be updated, the OTA database should contain a table +** named "data_" containing the same set of columns as the +** target table, and one more - "ota_control". The data_% table should +** have no PRIMARY KEY or UNIQUE constraints, but each column should have +** the same type as the corresponding column in the target database. +** The "ota_control" column should have no type at all. For example, if +** the target database contains: +** +** CREATE TABLE t1(a INTEGER PRIMARY KEY, b TEXT, c UNIQUE); +** +** Then the OTA database should contain: +** +** CREATE TABLE data_t1(a INTEGER, b TEXT, c, ota_control); +** +** The order of the columns in the data_% table does not matter. +** +** For each row to INSERT into the target database as part of the OTA +** update, the corresponding data_% table should contain a single record +** with the "ota_control" column set to contain integer value 0. The +** other columns should be set to the values that make up the new record +** to insert. +** +** For each row to DELETE from the target database as part of the OTA +** update, the corresponding data_% table should contain a single record +** with the "ota_control" column set to contain integer value 1. The +** real primary key values of the row to delete should be stored in the +** corresponding columns of the data_% table. The values stored in the +** other columns are not used. +** +** For each row to DELETE from the target database as part of the OTA +** update, the corresponding data_% table should contain a single record +** with the "ota_control" column set to contain a value of type text. +** The real primary key values identifying the row to update should be +** stored in the corresponding columns of the data_% table row, as should +** the new values of all columns being update. The text value in the +** "ota_control" column must contain the same number of characters as +** there are column in the target database table, and must consist entirely +** of "x" and "." characters. For each column that is being updated, +** the corresponding character is set to "x". For those that remain as +** they are, the corresponding character of the ota_control value should +** be set to ".". For example, given the tables above, the update +** statement: +** +** UPDATE t1 SET c = 'usa' WHERE a = 4; +** +** is represented by the data_t1 row created by: +** +** INSERT INTO data_t1(a, b, c, ota_control) VALUES(4, NULL, 'usa', '..x'); +** +** +** USAGE +** +** The API declared below allows an application to apply an OTA update +** stored on disk to an existing target database. Essentially, the +** application: +** +** 1) Opens an OTA handle using the sqlite3ota_open() function. +** +** 2) Calls the sqlite3ota_step() function one or more times on +** the new handle. Each call to sqlite3ota_step() performs a single +** b-tree operation, so thousands of calls may be required to apply +** a complete update. +** +** 3) Calls sqlite3ota_close() to close the OTA update handle. If +** sqlite3ota_step() has been called enough times to completely +** apply the update to the target database, then it is committed +** and made visible to other database clients at this point. +** Otherwise, the state of the OTA update application is saved +** in the OTA database for later resumption. +** +** See comments below for more detail on APIs. +** +** If an update is only partially applied to the target database by the +** time sqlite3ota_close() is called, various state information is saved +** within the OTA database. This allows subsequent processes to automatically +** resume the OTA update from where it left off. +** +** To remove all OTA extension state information, returning an OTA database +** to its original contents, it is sufficient to drop all tables that begin +** with the prefix "ota_" +*/ + +#ifndef _SQLITE3OTA_H +#define _SQLITE3OTA_H + +typedef struct sqlite3ota sqlite3ota; + +/* +** Open an OTA handle. +** +** Argument zTarget is the path to the target database. Argument zOta is +** the path to the OTA database. Each call to this function must be matched +** by a call to sqlite3ota_close(). +*/ +sqlite3ota *sqlite3ota_open(const char *zTarget, const char *zOta); + +/* +** Do some work towards applying the OTA update to the target db. +** +** Return SQLITE_DONE if the update has been completely applied, or +** SQLITE_OK if no error occurs but there remains work to do to apply +** the OTA update. If an error does occur, some other error code is +** returned. +** +** Once a call to sqlite3ota_step() has returned a value other than +** SQLITE_OK, all subsequent calls on the same OTA handle are no-ops +** that immediately return the same value. +*/ +int sqlite3ota_step(sqlite3ota *pOta); + +/* +** Close an OTA handle. +** +** If the OTA update has been completely applied, commit it to the target +** database. Otherwise, assuming no error has occurred, save the current +** state of the OTA update appliation to the OTA database. +** +** If an error has already occurred as part of an sqlite3ota_step() +** or sqlite3ota_open() call, or if one occurs within this function, an +** SQLite error code is returned. Additionally, *pzErrmsg may be set to +** point to a buffer containing a utf-8 formatted English language error +** message. It is the responsibility of the caller to eventually free any +** such buffer using sqlite3_free(). +** +** Otherwise, if no error occurs, this function returns SQLITE_OK if the +** update has been partially applied, or SQLITE_DONE if it has been +** completely applied. +*/ +int sqlite3ota_close(sqlite3ota *pOta, char **pzErrmsg); + +#endif /* _SQLITE3OTA_H */ + diff --git a/main.mk b/main.mk index 7a20373abc..b225e4ecd0 100644 --- a/main.mk +++ b/main.mk @@ -331,7 +331,8 @@ TESTSRC2 = \ $(TOP)/ext/fts3/fts3_expr.c \ $(TOP)/ext/fts3/fts3_tokenizer.c \ $(TOP)/ext/fts3/fts3_write.c \ - $(TOP)/ext/async/sqlite3async.c + $(TOP)/ext/async/sqlite3async.c \ + $(TOP)/ext/ota/sqlite3ota.c # Header files used by all library source files. # diff --git a/manifest b/manifest index 58fd42dc62..da360992c2 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Update\scomments\sin\sthe\sANALYZE\scommand\sthat\sdescribe\show\sthe\sStat4Accum\nobjecct\sis\spassed\saround\swithin\sthe\sVDBE.\s\sNo\schanges\sto\sfunctional\scode. -D 2014-09-01T23:06:44.401 +C Add\san\sexperimental\sextension\sfor\sapplying\sbulk\supdates\sto\sdatabases. +D 2014-09-02T19:59:40.729 F Makefile.arm-wince-mingw32ce-gcc d6df77f1f48d690bd73162294bbba7f59507c72f F Makefile.in cf57f673d77606ab0f2d9627ca52a9ba1464146a F Makefile.linux-gcc 91d710bdc4998cb015f39edf3cb314ec4f4d7e23 @@ -121,6 +121,10 @@ F ext/misc/totype.c 4a167594e791abeed95e0a8db028822b5e8fe512 F ext/misc/vfslog.c fe40fab5c077a40477f7e5eba994309ecac6cc95 F ext/misc/vtshim.c babb0dc2bf116029e3e7c9a618b8a1377045303e F ext/misc/wholenumber.c 784b12543d60702ebdd47da936e278aa03076212 +F ext/ota/ota1.test ea2865997ce573fadaf12eb0a0f80ef22d9dd77f +F ext/ota/ota2.test 4f7abfe1dfb7c3709bf45e94f3e65f3839b4f115 +F ext/ota/sqlite3ota.c ad55821883e4110367a30ffca282032d2bf36e45 +F ext/ota/sqlite3ota.h d3187a98fe1e3445c58f7a27d96ac385b78486a1 F ext/rtree/README 6315c0d73ebf0ec40dedb5aa0e942bc8b54e3761 F ext/rtree/rtree.c 57bec53e1a677ab74217fe1f20a58c3a47261d6b F ext/rtree/rtree.h 834dbcb82dc85b2481cde6a07cdadfddc99e9b9e @@ -147,7 +151,7 @@ F ext/rtree/viewrtree.tcl eea6224b3553599ae665b239bd827e182b466024 F install-sh 9d4de14ab9fb0facae2f48780b874848cbf2f895 x F ltmain.sh 3ff0879076df340d2e23ae905484d8c15d5fdea8 F magic.txt 8273bf49ba3b0c8559cb2774495390c31fd61c60 -F main.mk 9b5ccf1097050b1f16681f7d4beeea4f7f7ac2c3 +F main.mk 566c36f247d19525264e584466b5f07c0a48302e F mkopcodec.awk c2ff431854d702cdd2d779c9c0d1f58fa16fa4ea F mkopcodeh.awk c6b3fa301db6ef7ac916b14c60868aeaec1337b5 F mkso.sh fd21c06b063bb16a5d25deea1752c2da6ac3ed83 @@ -168,7 +172,7 @@ F src/auth.c 523da7fb4979469955d822ff9298352d6b31de34 F src/backup.c a31809c65623cc41849b94d368917f8bb66e6a7e F src/bitvec.c 19a4ba637bd85f8f63fc8c9bae5ade9fb05ec1cb F src/btmutex.c ec9d3f1295dafeb278c3830211cc5584132468f4 -F src/btree.c 2a483a8045118faa99867a8679da42754b532318 +F src/btree.c c46043fbb09c18a19bdb96eadde6e724901d6fcf F src/btree.h a79aa6a71e7f1055f01052b7f821bd1c2dce95c8 F src/btreeInt.h cf180d86b2e9e418f638d65baa425c4c69c0e0e3 F src/build.c c26b233dcdb1e2c8f468d49236c266f9f3de96d8 @@ -185,12 +189,12 @@ F src/global.c 1e4bd956dc2f608f87d2a929abc4a20db65f30e4 F src/hash.c 4263fbc955f26c2e8cdc0cf214bc42435aa4e4f5 F src/hash.h c8f3c31722cf3277d03713909761e152a5b81094 F src/hwtime.h d32741c8f4df852c7d959236615444e2b1063b08 -F src/insert.c d1a104e67b33314d4cc5c1356147446086ab9fc8 +F src/insert.c 62b0ceab1720dc74ed1fbcf953224132245704d8 F src/journal.c b4124532212b6952f42eb2c12fa3c25701d8ba8d F src/legacy.c 87c92f4a08e2f70220e3b22a9c3b2482d36a134a F src/lempar.c cdf0a000315332fc9b50b62f3b5e22e080a0952b F src/loadext.c 31c2122b7dd05a179049bbf163fd4839f181cbab -F src/main.c d2ef03a45552e11813c68326d5edfda992e319d4 +F src/main.c c9802dc99c019fbba516202300d56be2c478fa93 F src/malloc.c 954de5f998c23237e04474a3f2159bf483bba65a F src/mem0.c 6a55ebe57c46ca1a7d98da93aaa07f99f1059645 F src/mem1.c c0c990fcaddff810ea277b4fb5d9138603dd5d4b @@ -211,30 +215,30 @@ F src/os_setup.h c9d4553b5aaa6f73391448b265b89bed0b890faa F src/os_unix.c 8525ca79457c5b4673a5fda2774ee39fe155f40f F src/os_win.c 2aa8aa7780d7cf03e912d2088ab2ec5c32f33dc5 F src/os_win.h 09e751b20bbc107ffbd46e13555dc73576d88e21 -F src/pager.c 3e732d2bbdd8d8d95fed0c5ae7e718d73153c4c5 -F src/pager.h ffd5607f7b3e4590b415b007a4382f693334d428 +F src/pager.c a3caa08db8227c5a32f388be67f33d8cb44d5e35 +F src/pager.h 1acd367a0ffb63026b0461ea5eaeeb8046414a71 F src/parse.y 22d6a074e5f5a7258947a1dc55a9bf946b765dd0 F src/pcache.c 3b3791297e8977002e56b4a9b8916f2039abad9b F src/pcache.h 9b559127b83f84ff76d735c8262f04853be0c59a F src/pcache1.c c5af6403a55178c9d1c09e4f77b0f9c88822762c -F src/pragma.c 14bcdb504128a476cce5bbc086d5226c5e46c225 -F src/prepare.c 3842c1dfc0b053458e3adcf9f6efc48e03e3fe3d +F src/pragma.c d252459fb3ce19448d1a2f41000c780fac4c0c26 +F src/prepare.c 314961aa6650cc860394cb2f31931cf2de93baa8 F src/printf.c 00986c86ddfffefc2fd3c73667ff51b3b9709c74 F src/random.c d10c1f85b6709ca97278428fd5db5bbb9c74eece F src/resolve.c 0ea356d32a5e884add23d1b9b4e8736681dd5697 F src/rowset.c a9c9aae3234b44a6d7c6f5a3cadf90dce1e627be F src/select.c 89e569b263535662f54b537eb9118b2c554ae7aa F src/shell.c 713cef4d73c05fc8e12f4960072329d767a05d50 -F src/sqlite.h.in 43852c8b68b4c579948cb37182918078836c5c06 +F src/sqlite.h.in 706b420dc3390532435a3bd196360a940805f02f F src/sqlite3.rc 992c9f5fb8285ae285d6be28240a7e8d3a7f2bad F src/sqlite3ext.h 886f5a34de171002ad46fae8c36a7d8051c190fc -F src/sqliteInt.h 6244ee9052752e26d1275ab20c9b774385aa57d2 +F src/sqliteInt.h 7c090825333d91ca392c2479a9e835e7b6a5eb12 F src/sqliteLimit.h 164b0e6749d31e0daa1a4589a169d31c0dec7b3d F src/status.c 7ac05a5c7017d0b9f0b4bcd701228b784f987158 F src/table.c 2cd62736f845d82200acfa1287e33feb3c15d62e -F src/tclsqlite.c 7d100e2e7aad614bb3d7026a41a0e3827dbaaebc -F src/test1.c 363a5089230a92cf0aaa7a2945da7f2bf3b0a8d3 -F src/test2.c 98049e51a17dc62606a99a9eb95ee477f9996712 +F src/tclsqlite.c 29357f2be7b0d00e8ea900eaf727e0c5ffeaa660 +F src/test1.c e9a0e5804b078532e69e69ec14c8326bf2cfc318 +F src/test2.c 84f6a786aa7ffa12fff83acb52660e337ffe642a F src/test3.c 1c0e5d6f080b8e33c1ce8b3078e7013fdbcd560c F src/test4.c 9b32d22f5f150abe23c1830e2057c4037c45b3df F src/test5.c 5a34feec76d9b3a86aab30fd4f6cc9c48cbab4c1 @@ -290,13 +294,13 @@ F src/vdbe.h c63fad052c9e7388d551e556e119c0bcf6bebdf8 F src/vdbeInt.h cdc8e421f85beb1ac9b4669ec5beadab6faa15e0 F src/vdbeapi.c 09677a53dd8c71bcd670b0bd073bb9aefa02b441 F src/vdbeaux.c cef5d34a64ae3a65b56d96d3fd663246ec8e1c36 -F src/vdbeblob.c 848238dc73e93e48432991bb5651bf87d865eca4 +F src/vdbeblob.c 0bc9d22578d87ad9ff1c16e20a36863326f34fd7 F src/vdbemem.c 921d5468a68ac06f369810992e84ca22cc730a62 F src/vdbesort.c 02646a9f86421776ae5d7594f620f9ed669d3698 F src/vdbetrace.c 6f52bc0c51e144b7efdcfb2a8f771167a8816767 F src/vtab.c 019dbfd0406a7447c990e1f7bd1dfcdb8895697f -F src/wal.c 264df50a1b33124130b23180ded2e2c5663c652a -F src/wal.h df01efe09c5cb8c8e391ff1715cca294f89668a4 +F src/wal.c 93b4fcb56a98f435a2cb66024bb2b12d66d1ff53 +F src/wal.h 237bc4484f7c289f094ecb6efb2b6c02005484e1 F src/walker.c 11edb74d587bc87b33ca96a5173e3ec1b8389e45 F src/where.c d9eae96b2cbbe4842eac3ee156ccd1b933d802c4 F src/whereInt.h 923820bee9726033a501a08d2fc69b9c1ee4feb3 @@ -1193,7 +1197,10 @@ F tool/vdbe_profile.tcl 67746953071a9f8f2f668b73fe899074e2c6d8c1 F tool/warnings-clang.sh f6aa929dc20ef1f856af04a730772f59283631d4 F tool/warnings.sh 0abfd78ceb09b7f7c27c688c8e3fe93268a13b32 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f -P 4cae93f8ae8fb3fe38fd5dc7d3a5ea0d11552841 -R fe37d23fb1f96547febf2ff7c0ba49b9 -U drh -Z 29e2a2847ff584a00093c5b89ffca704 +P 9779c7a9eb1e2bd36e9286331a9314f064014d80 +R 5df4197c767caad19feca7391f16a42d +T *branch * experimental-bulk-update +T *sym-experimental-bulk-update * +T -sym-trunk * +U dan +Z 195de077202fec58ba90fc725d7305e7 diff --git a/manifest.uuid b/manifest.uuid index af2604f469..4ea970c6b0 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -9779c7a9eb1e2bd36e9286331a9314f064014d80 \ No newline at end of file +2954ab501049968430011b63d046eb42ff37a56c \ No newline at end of file diff --git a/src/btree.c b/src/btree.c index a04302225d..e9d737b6a0 100644 --- a/src/btree.c +++ b/src/btree.c @@ -151,7 +151,8 @@ static int hasSharedCacheTableLock( ** and has the read-uncommitted flag set, then no lock is required. ** Return true immediately. */ - if( (pBtree->sharable==0) + if( (pBtree->db->flags & SQLITE_OtaMode) + || (pBtree->sharable==0) || (eLockType==READ_LOCK && (pBtree->db->flags & SQLITE_ReadUncommitted)) ){ return 1; @@ -2045,7 +2046,7 @@ int sqlite3BtreeOpen( btree_open_out: if( rc!=SQLITE_OK ){ if( pBt && pBt->pPager ){ - sqlite3PagerClose(pBt->pPager); + sqlite3PagerClose(pBt->pPager, 0); } sqlite3_free(pBt); sqlite3_free(p); @@ -2174,7 +2175,7 @@ int sqlite3BtreeClose(Btree *p){ ** Clean out and delete the BtShared object. */ assert( !pBt->pCursor ); - sqlite3PagerClose(pBt->pPager); + sqlite3PagerClose(pBt->pPager, (p->db->flags & SQLITE_OtaMode)!=0); if( pBt->xFreeSchema && pBt->pSchema ){ pBt->xFreeSchema(pBt->pSchema); } diff --git a/src/insert.c b/src/insert.c index 3e6982d836..2ca212c0cf 100644 --- a/src/insert.c +++ b/src/insert.c @@ -1365,6 +1365,10 @@ void sqlite3GenerateConstraintChecks( int iThisCur; /* Cursor for this UNIQUE index */ int addrUniqueOk; /* Jump here if the UNIQUE constraint is satisfied */ + /* If the "ota_mode" flag is set, ignore all indexes except the PK + ** index of WITHOUT ROWID tables. */ + if( (db->flags & SQLITE_OtaMode) && pIdx!=pPk) continue; + if( aRegIdx[ix]==0 ) continue; /* Skip indices that do not change */ if( bAffinityDone==0 ){ sqlite3TableAffinity(v, pTab, regNewData+1); @@ -1556,6 +1560,15 @@ void sqlite3CompleteInsertion( assert( pTab->pSelect==0 ); /* This table is not a VIEW */ for(i=0, pIdx=pTab->pIndex; pIdx; pIdx=pIdx->pNext, i++){ if( aRegIdx[i]==0 ) continue; + + /* If the "ota_mode" flag is set, ignore all indexes except the PK + ** index of WITHOUT ROWID tables. */ + if( (pParse->db->flags & SQLITE_OtaMode) + && (pTab->iPKey>=0 || pIdx->idxType!=SQLITE_IDXTYPE_PRIMARYKEY) + ){ + continue; + } + bAffinityDone = 1; if( pIdx->pPartIdxWhere ){ sqlite3VdbeAddOp2(v, OP_IsNull, aRegIdx[i], sqlite3VdbeCurrentAddr(v)+2); diff --git a/src/main.c b/src/main.c index fd7151b174..f73817e29b 100644 --- a/src/main.c +++ b/src/main.c @@ -3470,3 +3470,15 @@ int sqlite3_db_readonly(sqlite3 *db, const char *zDbName){ Btree *pBt = sqlite3DbNameToBtree(db, zDbName); return pBt ? sqlite3BtreeIsReadonly(pBt) : -1; } + +int sqlite3_transaction_save(sqlite3 *db, void **ppState, int *pnState){ + Pager *pPager = sqlite3BtreePager(db->aDb[0].pBt); + return sqlite3PagerSaveState(pPager, ppState, pnState); +} + +int sqlite3_transaction_restore(sqlite3 *db, const void *pState, int nState){ + Pager *pPager = sqlite3BtreePager(db->aDb[0].pBt); + return sqlite3PagerRestoreState(pPager, pState, nState); +} + + diff --git a/src/pager.c b/src/pager.c index 3ef54d98e0..a54ae816e3 100644 --- a/src/pager.c +++ b/src/pager.c @@ -3947,7 +3947,7 @@ static void pagerFreeMapHdrs(Pager *pPager){ ** a hot journal may be left in the filesystem but no error is returned ** to the caller. */ -int sqlite3PagerClose(Pager *pPager){ +int sqlite3PagerClose(Pager *pPager, int bOtaMode){ u8 *pTmp = (u8 *)pPager->pTmpSpace; assert( assert_pager_state(pPager) ); @@ -3957,7 +3957,9 @@ int sqlite3PagerClose(Pager *pPager){ /* pPager->errCode = 0; */ pPager->exclusiveMode = 0; #ifndef SQLITE_OMIT_WAL - sqlite3WalClose(pPager->pWal, pPager->ckptSyncFlags, pPager->pageSize, pTmp); + sqlite3WalClose( + pPager->pWal, pPager->ckptSyncFlags, pPager->pageSize, (bOtaMode?0:pTmp) + ); pPager->pWal = 0; #endif pager_reset(pPager); @@ -7217,6 +7219,41 @@ int sqlite3PagerCloseWal(Pager *pPager){ return rc; } +int sqlite3PagerSaveState(Pager *pPager, void **ppState, int *pnState){ + int rc = SQLITE_OK; + *ppState = 0; + *pnState = 0; + if( pPager->pWal==0 || pPager->eStatepPCache); + rc = sqlite3WalFrames(pPager->pWal, + pPager->pageSize, pList, 0, 0, pPager->walSyncFlags + ); + if( rc==SQLITE_OK ){ + rc = sqlite3WalSaveState(pPager->pWal, ppState, pnState); + } + } + return rc; +} + +int sqlite3PagerRestoreState(Pager *pPager, const void *pState, int nState){ + int rc = SQLITE_OK; + if( pPager->pWal==0 + || pPager->eStatepPCache) + ){ + rc = SQLITE_ERROR; + }else{ + sqlite3PcacheTruncate(pPager->pPCache, 1); + rc = sqlite3WalRestoreState(pPager->pWal, pState, nState); + pPager->eState = PAGER_WRITER_CACHEMOD; + } + + return rc; +} + #endif /* !SQLITE_OMIT_WAL */ #ifdef SQLITE_ENABLE_ZIPVFS diff --git a/src/pager.h b/src/pager.h index c9ca8553b9..79ffa04db8 100644 --- a/src/pager.h +++ b/src/pager.h @@ -112,7 +112,7 @@ int sqlite3PagerOpen( int, void(*)(DbPage*) ); -int sqlite3PagerClose(Pager *pPager); +int sqlite3PagerClose(Pager *pPager, int); int sqlite3PagerReadFileheader(Pager*, int, unsigned char*); /* Functions used to configure a Pager object. */ @@ -207,4 +207,7 @@ void *sqlite3PagerCodec(DbPage *); # define enable_simulated_io_errors() #endif +int sqlite3PagerSaveState(Pager *pPager, void **ppState, int *pnState); +int sqlite3PagerRestoreState(Pager *pPager, const void *pState, int nState); + #endif /* _PAGER_H_ */ diff --git a/src/pragma.c b/src/pragma.c index 12446125fb..3f06a51839 100644 --- a/src/pragma.c +++ b/src/pragma.c @@ -308,6 +308,12 @@ static const struct sPragmaNames { /* ePragTyp: */ PragTyp_MMAP_SIZE, /* ePragFlag: */ 0, /* iArg: */ 0 }, +#endif + { /* zName: */ "ota_mode", + /* ePragTyp: */ PragTyp_FLAG, + /* ePragFlag: */ 0, + /* iArg: */ SQLITE_OtaMode }, +#if !defined(SQLITE_OMIT_PAGER_PRAGMAS) { /* zName: */ "page_count", /* ePragTyp: */ PragTyp_PAGE_COUNT, /* ePragFlag: */ PragFlag_NeedSchema, @@ -1467,7 +1473,11 @@ void sqlite3Pragma( }else if( pPk==0 ){ k = 1; }else{ - for(k=1; ALWAYS(k<=pTab->nCol) && pPk->aiColumn[k-1]!=i; k++){} + if( (db->flags & SQLITE_OtaMode) && HasRowid(pTab) ){ + k = 0; + }else{ + for(k=1; ALWAYS(k<=pTab->nCol) && pPk->aiColumn[k-1]!=i; k++){} + } } sqlite3VdbeAddOp2(v, OP_Integer, k, 6); sqlite3VdbeAddOp2(v, OP_ResultRow, 1, 6); diff --git a/src/prepare.c b/src/prepare.c index 5b92e88513..44efe8c8fb 100644 --- a/src/prepare.c +++ b/src/prepare.c @@ -796,7 +796,6 @@ int sqlite3_prepare_v2( return rc; } - #ifndef SQLITE_OMIT_UTF16 /* ** Compile the UTF-16 encoded SQL statement zSql into a statement handle. diff --git a/src/sqlite.h.in b/src/sqlite.h.in index d446d2ea98..7011b67fd4 100644 --- a/src/sqlite.h.in +++ b/src/sqlite.h.in @@ -7363,7 +7363,79 @@ int sqlite3_vtab_on_conflict(sqlite3 *); /* #define SQLITE_ABORT 4 // Also an error code */ #define SQLITE_REPLACE 5 +/* +** Allocate a statement handle that may be used to write directly to an +** index b-tree. This allows the user to create a corrupt database. Once +** the statement handle is allocated, it may be used with the same APIs +** as any statement handle created with sqlite3_prepare(). +** +** The statement writes to the index specified by parameter zIndex, which +** must be in the "main" database. If argument bDelete is false, then each +** time the statement is sqlite3_step()ed, an entry is inserted into the +** b-tree index. If it is true, then an entry may be deleted (or may not, if +** the specified key is not found) each time the statement is +** sqlite3_step()ed. +** +** If statement compilation is successful, *ppStmt is set to point to the +** new statement handle and SQLITE_OK is returned. Otherwise, if an error +** occurs, *ppStmt is set to NULL and an error code returned. An error +** message may be left in the database handle in this case. +** +** If statement compilation succeeds, output variable *pnCol is set to the +** total number of columns in the index, including the primary key columns +** at the end. Variable *paiCol is set to point to an array *pnCol entries +** in size. Each entry is the table column index, numbered from zero from left +** to right, of the corresponding index column. For example, if: +** +** CREATE TABLE t1(a, b, c, d); +** CREATE INDEX i1 ON t1(b, c); +** +** then *pnCol is 3 and *paiCol points to an array containing {1, 2, -1}. +** If table t1 had an explicit INTEGER PRIMARY KEY, then the "-1" in the +** *paiCol array would be replaced by its column index. Or if: +** +** CREATE TABLE t2(a, b, c, d, PRIMARY KEY(d, c)) WITHOUT ROWID; +** CREATE INDEX i2 ON t2(a); +** +** then (*pnCol) is 3 and *paiCol points to an array containing {0, 3, 2}. +** +** The lifetime of the array is the same as that of the statement handle - +** it is automatically freed when the statement handle is passed to +** sqlite3_finalize(). +** +** The statement has (*pnCol) SQL variables that values may be bound to. +** They correspond to the values used to create the index key that is +** inserted or deleted when the statement is stepped. +** +** If the index is a UNIQUE index, the usual checking and error codes apply +** to insert operations. +*/ +int sqlite3_index_writer( + sqlite3 *db, + int bDelete, /* Zero for insert, non-zero for delete */ + const char *zIndex, /* Index to write to */ + sqlite3_stmt**, /* OUT: New statement handle */ + int **paiCol, int *pnCol /* OUT: See above */ +); + +/* +** This function is used to save the state of an ongoing WAL mode write +** transaction on the "main" database of the supplied database handle. +** +** If successful, SQLITE_OK is returned and output variable (*ppState) +** is set to point to a buffer containing the transaction state data. +** (*pnState) is set to the size of that buffer in bytes. Otherwise, if +** an error occurs, an SQLite error code is returned and both output +** variables are zeroed. +** +** A transaction state may be saved if: +** +** * the transaction does not contain any schema modifications. +** * there are no open sub-transactions. +*/ +int sqlite3_transaction_save(sqlite3 *db, void **ppState, int *pnState); +int sqlite3_transaction_restore(sqlite3 *db, const void *pState, int nState); /* ** Undo the hack that converts floating point types to integer for diff --git a/src/sqliteInt.h b/src/sqliteInt.h index 7fd999d9ee..11396c651b 100644 --- a/src/sqliteInt.h +++ b/src/sqliteInt.h @@ -1141,6 +1141,8 @@ struct sqlite3 { #define SQLITE_QueryOnly 0x02000000 /* Disable database changes */ #define SQLITE_VdbeEQP 0x04000000 /* Debug EXPLAIN QUERY PLAN */ +#define SQLITE_OtaMode 0x08000000 /* True in "ota mode" */ + /* ** Bits of the sqlite3.dbOptFlags field that are used by the @@ -3726,6 +3728,7 @@ SQLITE_EXTERN void (*sqlite3IoTrace)(const char*,...); #define MEMTYPE_PCACHE 0x08 /* Page cache allocations */ #define MEMTYPE_DB 0x10 /* Uses sqlite3DbMalloc, not sqlite_malloc */ + /* ** Threading interface */ diff --git a/src/tclsqlite.c b/src/tclsqlite.c index 2b98b6aab4..ce88109007 100644 --- a/src/tclsqlite.c +++ b/src/tclsqlite.c @@ -3699,6 +3699,8 @@ static void init_all(Tcl_Interp *interp){ extern int SqliteSuperlock_Init(Tcl_Interp*); extern int SqlitetestSyscall_Init(Tcl_Interp*); + extern int SqliteOta_Init(Tcl_Interp*); + #if defined(SQLITE_ENABLE_FTS3) || defined(SQLITE_ENABLE_FTS4) extern int Sqlitetestfts3_Init(Tcl_Interp *interp); #endif @@ -3740,6 +3742,7 @@ static void init_all(Tcl_Interp *interp){ Sqlitemultiplex_Init(interp); SqliteSuperlock_Init(interp); SqlitetestSyscall_Init(interp); + SqliteOta_Init(interp); #if defined(SQLITE_ENABLE_FTS3) || defined(SQLITE_ENABLE_FTS4) Sqlitetestfts3_Init(interp); diff --git a/src/test1.c b/src/test1.c index 34faeaadf8..d75f268deb 100644 --- a/src/test1.c +++ b/src/test1.c @@ -6497,6 +6497,70 @@ static int sorter_test_sort4_helper( } +/* +** tclcmd: sqlite3_transaction_save DB +*/ +static int testTransactionSave( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + void *pState; + int nState; + sqlite3 *db; + int rc; + + if( objc!=2 ){ + Tcl_WrongNumArgs(interp, 1, objv, "DB"); + return TCL_ERROR; + } + if( getDbPointer(interp, Tcl_GetString(objv[1]), &db) ) return TCL_ERROR; + + rc = sqlite3_transaction_save(db, &pState, &nState); + if( rc==SQLITE_OK ){ + Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(pState, nState)); + }else{ + Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1)); + return TCL_ERROR; + } + + sqlite3_free(pState); + return TCL_OK; +} + +/* +** tclcmd: sqlite3_transaction_restore DB BLOB +*/ +static int testTransactionRestore( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + void *pState; + int nState; + sqlite3 *db; + int rc; + + if( objc!=3 ){ + Tcl_WrongNumArgs(interp, 1, objv, "DB BLOB"); + return TCL_ERROR; + } + if( getDbPointer(interp, Tcl_GetString(objv[1]), &db) ) return TCL_ERROR; + pState = (void*)Tcl_GetByteArrayFromObj(objv[2], &nState); + + rc = sqlite3_transaction_restore(db, pState, nState); + if( rc==SQLITE_OK ){ + Tcl_ResetResult(interp); + }else{ + Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1)); + return TCL_ERROR; + } + + return TCL_OK; +} + /* ** Register commands with the TCL interpreter. */ @@ -6734,6 +6798,8 @@ int Sqlitetest1_Init(Tcl_Interp *interp){ { "load_static_extension", tclLoadStaticExtensionCmd }, { "sorter_test_fakeheap", sorter_test_fakeheap }, { "sorter_test_sort4_helper", sorter_test_sort4_helper }, + { "sqlite3_transaction_save", testTransactionSave }, + { "sqlite3_transaction_restore", testTransactionRestore }, }; static int bitmask_size = sizeof(Bitmask)*8; int i; diff --git a/src/test2.c b/src/test2.c index 58f271ff27..6d655d6473 100644 --- a/src/test2.c +++ b/src/test2.c @@ -89,7 +89,7 @@ static int pager_close( return TCL_ERROR; } pPager = sqlite3TestTextToPtr(argv[1]); - rc = sqlite3PagerClose(pPager); + rc = sqlite3PagerClose(pPager, 0); if( rc!=SQLITE_OK ){ Tcl_AppendResult(interp, sqlite3ErrName(rc), 0); return TCL_ERROR; diff --git a/src/vdbeblob.c b/src/vdbeblob.c index 71bd8816d5..11e018e19c 100644 --- a/src/vdbeblob.c +++ b/src/vdbeblob.c @@ -463,4 +463,116 @@ int sqlite3_blob_reopen(sqlite3_blob *pBlob, sqlite3_int64 iRow){ return rc; } +int sqlite3_index_writer( + sqlite3 *db, + int bDelete, + const char *zIndex, + sqlite3_stmt **ppStmt, + int **paiCol, int *pnCol +){ + int rc = SQLITE_OK; + Parse *pParse = 0; + Index *pIdx = 0; /* The index to write to */ + Table *pTab; + int i; /* Used to iterate through index columns */ + Vdbe *v = 0; + int regRec; /* Register to assemble record in */ + int *aiCol = 0; + + sqlite3_mutex_enter(db->mutex); + sqlite3BtreeEnterAll(db); + + /* Allocate the parse context */ + pParse = sqlite3StackAllocRaw(db, sizeof(*pParse)); + if( !pParse ) goto index_writer_out; + memset(pParse, 0, sizeof(Parse)); + pParse->db = db; + + /* Allocate the Vdbe */ + v = sqlite3GetVdbe(pParse); + if( v==0 ) goto index_writer_out; + + /* Find the index to write to */ + pIdx = sqlite3FindIndex(db, zIndex, "main"); + if( pIdx==0 ){ + sqlite3ErrorMsg(pParse, "no such index: %s", zIndex); + goto index_writer_out; + } + pTab = pIdx->pTable; + + /* Populate the two output variables, *pnCol and *pnAiCol. */ + *pnCol = pIdx->nColumn; + *paiCol = aiCol = sqlite3DbMallocZero(db, sizeof(int) * pIdx->nColumn); + if( aiCol==0 ){ + rc = SQLITE_NOMEM; + goto index_writer_out; + } + for(i=0; inKeyCol; i++){ + aiCol[i] = pIdx->aiColumn[i]; + } + if( !HasRowid(pTab) ){ + Index *pPk = sqlite3PrimaryKeyIndex(pIdx->pTable); + assert( pIdx->nColumn==pIdx->nKeyCol+pPk->nKeyCol ); + if( pPk==pIdx ){ + rc = SQLITE_ERROR; + goto index_writer_out; + } + for(i=0; inKeyCol; i++){ + aiCol[pIdx->nKeyCol+i] = pPk->aiColumn[i]; + } + }else{ + assert( pIdx->nColumn==pIdx->nKeyCol+1 ); + aiCol[i] = pTab->iPKey; + } + + /* Add an OP_Noop to the VDBE program. Then store a pointer to the + ** output array *paiCol as its P4 value. This is so that the array + ** is automatically deleted when the user finalizes the statement. The + ** OP_Noop serves no other purpose. */ + sqlite3VdbeAddOp0(v, OP_Noop); + sqlite3VdbeChangeP4(v, -1, (const char*)aiCol, P4_INTARRAY); + + sqlite3BeginWriteOperation(pParse, 0, 0); + + /* Open a write cursor on the index */ + pParse->nTab = 1; + sqlite3VdbeAddOp3(v, OP_OpenWrite, 0, pIdx->tnum, 0); + sqlite3VdbeSetP4KeyInfo(pParse, pIdx); + + /* Create the record to insert into the index. Store it in register regRec. */ + pParse->nVar = pIdx->nColumn; + pParse->nMem = pIdx->nColumn; + for(i=1; i<=pIdx->nColumn; i++){ + sqlite3VdbeAddOp2(v, OP_Variable, i, i); + } + regRec = ++pParse->nMem; + sqlite3VdbeAddOp3(v, OP_MakeRecord, 1, pIdx->nColumn, regRec); + + /* If this is a UNIQUE index, check the constraint. */ + if( pIdx->onError ){ + int addr = sqlite3VdbeAddOp4Int(v, OP_NoConflict, 0, 0, 1, pIdx->nKeyCol); + sqlite3UniqueConstraint(pParse, SQLITE_ABORT, pIdx); + sqlite3VdbeJumpHere(v, addr); + } + + /* Code the IdxInsert to write to the b-tree index. */ + sqlite3VdbeAddOp2(v, OP_IdxInsert, 0, regRec); + sqlite3FinishCoding(pParse); + +index_writer_out: + if( rc==SQLITE_OK && db->mallocFailed==0 ){ + *ppStmt = (sqlite3_stmt*)v; + }else{ + *ppStmt = 0; + if( v ) sqlite3VdbeFinalize(v); + } + + sqlite3ParserReset(pParse); + sqlite3StackFree(db, pParse); + sqlite3BtreeLeaveAll(db); + rc = sqlite3ApiExit(db, rc); + sqlite3_mutex_leave(db->mutex); + return rc; +} + #endif /* #ifndef SQLITE_OMIT_INCRBLOB */ diff --git a/src/wal.c b/src/wal.c index 70395f8bee..a030824cf7 100644 --- a/src/wal.c +++ b/src/wal.c @@ -1046,6 +1046,62 @@ static int walIndexAppend(Wal *pWal, u32 iFrame, u32 iPage){ return rc; } +static int walFileReadHdr(Wal *pWal, int *pbValid){ + u8 aBuf[WAL_HDRSIZE]; /* Buffer to load WAL header into */ + int rc; /* Return code */ + u32 magic; /* Magic value read from WAL header */ + int szPage; /* Page size according to the log */ + u32 version; /* Magic value read from WAL header */ + + *pbValid = 0; + + /* Read in the WAL header. */ + rc = sqlite3OsRead(pWal->pWalFd, aBuf, WAL_HDRSIZE, 0); + if( rc!=SQLITE_OK ){ + return rc; + } + + /* If the database page size is not a power of two, or is greater than + ** SQLITE_MAX_PAGE_SIZE, conclude that the WAL file contains no valid + ** data. Similarly, if the 'magic' value is invalid, ignore the whole + ** WAL file. + */ + magic = sqlite3Get4byte(&aBuf[0]); + szPage = sqlite3Get4byte(&aBuf[8]); + if( (magic&0xFFFFFFFE)!=WAL_MAGIC + || szPage&(szPage-1) + || szPage>SQLITE_MAX_PAGE_SIZE + || szPage<512 + ){ + return SQLITE_OK; + } + + pWal->hdr.bigEndCksum = (u8)(magic&0x00000001); + pWal->szPage = szPage; + pWal->nCkpt = sqlite3Get4byte(&aBuf[12]); + memcpy(&pWal->hdr.aSalt, &aBuf[16], 8); + + /* Verify that the WAL header checksum is correct */ + walChecksumBytes(pWal->hdr.bigEndCksum==SQLITE_BIGENDIAN, + aBuf, WAL_HDRSIZE-2*4, 0, pWal->hdr.aFrameCksum + ); + if( pWal->hdr.aFrameCksum[0]!=sqlite3Get4byte(&aBuf[24]) + || pWal->hdr.aFrameCksum[1]!=sqlite3Get4byte(&aBuf[28]) + ){ + return SQLITE_OK; + } + + /* Verify that the version number on the WAL format is one that + ** are able to understand */ + version = sqlite3Get4byte(&aBuf[4]); + if( version!=WAL_MAX_VERSION ){ + return SQLITE_CANTOPEN_BKPT; + } + + *pbValid = 1; + return SQLITE_OK; +} + /* ** Recover the wal-index by reading the write-ahead log file. @@ -1090,59 +1146,18 @@ static int walIndexRecover(Wal *pWal){ } if( nSize>WAL_HDRSIZE ){ - u8 aBuf[WAL_HDRSIZE]; /* Buffer to load WAL header into */ u8 *aFrame = 0; /* Malloc'd buffer to load entire frame */ int szFrame; /* Number of bytes in buffer aFrame[] */ u8 *aData; /* Pointer to data part of aFrame buffer */ int iFrame; /* Index of last frame read */ i64 iOffset; /* Next offset to read from log file */ int szPage; /* Page size according to the log */ - u32 magic; /* Magic value read from WAL header */ - u32 version; /* Magic value read from WAL header */ int isValid; /* True if this frame is valid */ - /* Read in the WAL header. */ - rc = sqlite3OsRead(pWal->pWalFd, aBuf, WAL_HDRSIZE, 0); - if( rc!=SQLITE_OK ){ - goto recovery_error; - } - - /* If the database page size is not a power of two, or is greater than - ** SQLITE_MAX_PAGE_SIZE, conclude that the WAL file contains no valid - ** data. Similarly, if the 'magic' value is invalid, ignore the whole - ** WAL file. - */ - magic = sqlite3Get4byte(&aBuf[0]); - szPage = sqlite3Get4byte(&aBuf[8]); - if( (magic&0xFFFFFFFE)!=WAL_MAGIC - || szPage&(szPage-1) - || szPage>SQLITE_MAX_PAGE_SIZE - || szPage<512 - ){ - goto finished; - } - pWal->hdr.bigEndCksum = (u8)(magic&0x00000001); - pWal->szPage = szPage; - pWal->nCkpt = sqlite3Get4byte(&aBuf[12]); - memcpy(&pWal->hdr.aSalt, &aBuf[16], 8); - - /* Verify that the WAL header checksum is correct */ - walChecksumBytes(pWal->hdr.bigEndCksum==SQLITE_BIGENDIAN, - aBuf, WAL_HDRSIZE-2*4, 0, pWal->hdr.aFrameCksum - ); - if( pWal->hdr.aFrameCksum[0]!=sqlite3Get4byte(&aBuf[24]) - || pWal->hdr.aFrameCksum[1]!=sqlite3Get4byte(&aBuf[28]) - ){ - goto finished; - } - - /* Verify that the version number on the WAL format is one that - ** are able to understand */ - version = sqlite3Get4byte(&aBuf[4]); - if( version!=WAL_MAX_VERSION ){ - rc = SQLITE_CANTOPEN_BKPT; - goto finished; - } + rc = walFileReadHdr(pWal, &isValid); + if( rc!=SQLITE_OK ) goto recovery_error; + if( isValid==0 ) goto finished; + szPage = pWal->szPage; /* Malloc a buffer to read frames into. */ szFrame = szPage + WAL_FRAME_HDRSIZE; @@ -1837,32 +1852,34 @@ int sqlite3WalClose( ** ** The EXCLUSIVE lock is not released before returning. */ - rc = sqlite3OsLock(pWal->pDbFd, SQLITE_LOCK_EXCLUSIVE); - if( rc==SQLITE_OK ){ - if( pWal->exclusiveMode==WAL_NORMAL_MODE ){ - pWal->exclusiveMode = WAL_EXCLUSIVE_MODE; - } - rc = sqlite3WalCheckpoint( - pWal, SQLITE_CHECKPOINT_PASSIVE, 0, 0, sync_flags, nBuf, zBuf, 0, 0 - ); + if( zBuf ){ + rc = sqlite3OsLock(pWal->pDbFd, SQLITE_LOCK_EXCLUSIVE); if( rc==SQLITE_OK ){ - int bPersist = -1; - sqlite3OsFileControlHint( - pWal->pDbFd, SQLITE_FCNTL_PERSIST_WAL, &bPersist + if( pWal->exclusiveMode==WAL_NORMAL_MODE ){ + pWal->exclusiveMode = WAL_EXCLUSIVE_MODE; + } + rc = sqlite3WalCheckpoint( + pWal, SQLITE_CHECKPOINT_PASSIVE, 0, 0, sync_flags, nBuf, zBuf, 0, 0 ); - if( bPersist!=1 ){ - /* Try to delete the WAL file if the checkpoint completed and - ** fsyned (rc==SQLITE_OK) and if we are not in persistent-wal - ** mode (!bPersist) */ - isDelete = 1; - }else if( pWal->mxWalSize>=0 ){ - /* Try to truncate the WAL file to zero bytes if the checkpoint - ** completed and fsynced (rc==SQLITE_OK) and we are in persistent - ** WAL mode (bPersist) and if the PRAGMA journal_size_limit is a - ** non-negative value (pWal->mxWalSize>=0). Note that we truncate - ** to zero bytes as truncating to the journal_size_limit might - ** leave a corrupt WAL file on disk. */ - walLimitSize(pWal, 0); + if( rc==SQLITE_OK ){ + int bPersist = -1; + sqlite3OsFileControlHint( + pWal->pDbFd, SQLITE_FCNTL_PERSIST_WAL, &bPersist + ); + if( bPersist!=1 ){ + /* Try to delete the WAL file if the checkpoint completed and + ** fsyned (rc==SQLITE_OK) and if we are not in persistent-wal + ** mode (!bPersist) */ + isDelete = 1; + }else if( pWal->mxWalSize>=0 ){ + /* Try to truncate the WAL file to zero bytes if the checkpoint + ** completed and fsynced (rc==SQLITE_OK) and we are in persistent + ** WAL mode (bPersist) and if the PRAGMA journal_size_limit is a + ** non-negative value (pWal->mxWalSize>=0). Note that we truncate + ** to zero bytes as truncating to the journal_size_limit might + ** leave a corrupt WAL file on disk. */ + walLimitSize(pWal, 0); + } } } } @@ -3079,6 +3096,157 @@ int sqlite3WalHeapMemory(Wal *pWal){ return (pWal && pWal->exclusiveMode==WAL_HEAPMEMORY_MODE ); } +/* +** Save current transaction state. +** +** The transaction state consists of a series of 32-bit big-endian integers: +** +** * initial number of frames in WAL file. +** * initial checksum values (2 integers). +** * current number of frames. +** * current checksum values (2 integers). +*/ +int sqlite3WalSaveState(Wal *pWal, void **ppState, int *pnState){ + int rc = SQLITE_OK; + + *ppState = 0; + *pnState = 0; + if( pWal->writeLock==0 ){ + /* Must be in a write transaction to call this function. */ + rc = SQLITE_ERROR; + }else{ + WalIndexHdr *pOrig = (WalIndexHdr*)walIndexHdr(pWal); + int nBuf = 6 * 4; /* Bytes of space to allocate */ + u8 *aBuf; + + aBuf = sqlite3_malloc(nBuf); + if( aBuf==0 ){ + rc = SQLITE_NOMEM; + }else{ + sqlite3Put4byte(&aBuf[0], pOrig->mxFrame); + sqlite3Put4byte(&aBuf[4], pOrig->aFrameCksum[0]); + sqlite3Put4byte(&aBuf[8], pOrig->aFrameCksum[1]); + sqlite3Put4byte(&aBuf[12], pWal->hdr.mxFrame); + sqlite3Put4byte(&aBuf[16], pWal->hdr.aFrameCksum[0]); + sqlite3Put4byte(&aBuf[20], pWal->hdr.aFrameCksum[1]); + *ppState = (void*)aBuf; + *pnState = nBuf; + } + } + + return rc; +} + +static int walUndoNoop(void *pUndoCtx, Pgno pgno){ + UNUSED_PARAMETER(pUndoCtx); + UNUSED_PARAMETER(pgno); + return SQLITE_OK; +} + +/* +** If possible, restore the state of the curent transaction to that +** described by the second and third arguments. +*/ +int sqlite3WalRestoreState(Wal *pWal, const void *pState, int nState){ + int rc = SQLITE_OK; + + if( pWal->writeLock==0 ){ + /* Must have opened a write transaction to call this */ + rc = SQLITE_ERROR; + }else{ + u8 *aBuf = (u8*)pState; + int szFrame; /* Size of each frame in WAL file */ + u8 *aFrame = 0; /* Buffer to read data into */ + u8 *aData; /* Data part of aFrame[] buffer */ + u32 mxFrame; /* Maximum frame following restoration */ + int i; /* Iterator variable */ + + WalIndexHdr *pOrig = (WalIndexHdr*)walIndexHdr(pWal); + + /* Check that no dirty pages have been written to the WAL file since + ** the current transaction was opened. */ + if( pOrig->mxFrame!=pWal->hdr.mxFrame + || pOrig->aFrameCksum[0]!=pWal->hdr.aFrameCksum[0] + || pOrig->aFrameCksum[1]!=pWal->hdr.aFrameCksum[1] + ){ + rc = SQLITE_ERROR; + } + + /* Check that the WAL file is in the same state that it was when the + ** transaction was saved. If not, return SQLITE_MISMATCH - cannot + ** resume this transaction */ + if( rc==SQLITE_OK && ( + pWal->hdr.mxFrame!=sqlite3Get4byte(&aBuf[0]) + || pWal->hdr.aFrameCksum[0]!=sqlite3Get4byte(&aBuf[4]) + || pWal->hdr.aFrameCksum[1]!=sqlite3Get4byte(&aBuf[8]) + )){ + rc = SQLITE_MISMATCH; + } + + if( rc==SQLITE_OK && pWal->readLock==0 ){ + int cnt = 0; + walUnlockShared(pWal, WAL_READ_LOCK(0)); + pWal->readLock = -1; + do{ + int notUsed; + rc = walTryBeginRead(pWal, ¬Used, 1, ++cnt); + }while( rc==WAL_RETRY ); + + if( rc==SQLITE_OK ){ + int bValid; + rc = walFileReadHdr(pWal, &bValid); + if( rc==SQLITE_OK && bValid==0 ) rc = SQLITE_MISMATCH; + pWal->hdr.szPage = (u16)((pWal->szPage&0xff00) | (pWal->szPage>>16)); + } + } + + /* Malloc a buffer to read frames into. */ + if( rc==SQLITE_OK ){ + szFrame = pWal->szPage + WAL_FRAME_HDRSIZE; + aFrame = (u8*)sqlite3_malloc(szFrame); + if( !aFrame ){ + rc = SQLITE_NOMEM; + }else{ + aData = &aFrame[WAL_FRAME_HDRSIZE]; + } + } + + mxFrame = sqlite3Get4byte(&aBuf[12]); + for(i=pWal->hdr.mxFrame+1; rc==SQLITE_OK && i<=mxFrame; i++){ + sqlite3_int64 iOff = walFrameOffset(i, pWal->szPage); + rc = sqlite3OsRead(pWal->pWalFd, aFrame, szFrame, iOff); + if( rc==SQLITE_OK ){ + u32 iPg; + u32 dummy; + if( 0==walDecodeFrame(pWal, &iPg, &dummy, aData, aFrame) ){ + rc = SQLITE_MISMATCH; + }else{ + rc = walIndexAppend(pWal, i, iPg); + if( iPg>pWal->hdr.nPage ) pWal->hdr.nPage = iPg; + } + pWal->hdr.mxFrame = i; + } + } + sqlite3_free(aFrame); + + if( rc==SQLITE_OK ){ + assert( pWal->hdr.mxFrame==mxFrame ); + if( pWal->hdr.aFrameCksum[0]!=sqlite3Get4byte(&aBuf[16]) + || pWal->hdr.aFrameCksum[1]!=sqlite3Get4byte(&aBuf[20]) + ){ + rc = SQLITE_MISMATCH; + } + } + + + if( rc!=SQLITE_OK ){ + sqlite3WalUndo(pWal, walUndoNoop, 0); + } + } + + return rc; +} + #ifdef SQLITE_ENABLE_ZIPVFS /* ** If the argument is not NULL, it points to a Wal object that holds a diff --git a/src/wal.h b/src/wal.h index 092546354b..08006fafe9 100644 --- a/src/wal.h +++ b/src/wal.h @@ -126,6 +126,9 @@ int sqlite3WalExclusiveMode(Wal *pWal, int op); */ int sqlite3WalHeapMemory(Wal *pWal); +int sqlite3WalSaveState(Wal *pWal, void **ppState, int *pnState); +int sqlite3WalRestoreState(Wal *pWal, const void *pState, int nState); + #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).