]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Add the experimental sqlite3rbu_vacuum() API function. For opening an RBU handle...
authordan <dan@noemail.net>
Fri, 15 Apr 2016 20:46:41 +0000 (20:46 +0000)
committerdan <dan@noemail.net>
Fri, 15 Apr 2016 20:46:41 +0000 (20:46 +0000)
FossilOrigin-Name: 0216b48f28042ad86711e00802c2da8ce9be3044

ext/rbu/rbuvacuum.test [new file with mode: 0644]
ext/rbu/sqlite3rbu.c
ext/rbu/sqlite3rbu.h
ext/rbu/test_rbu.c
manifest
manifest.uuid

diff --git a/ext/rbu/rbuvacuum.test b/ext/rbu/rbuvacuum.test
new file mode 100644 (file)
index 0000000..045b2d1
--- /dev/null
@@ -0,0 +1,181 @@
+# 2016 April 15
+#
+# 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 tests for the RBU module. More specifically, it
+# contains tests to ensure that the sqlite3rbu_vacuum() API works as
+# expected.
+#
+
+source [file join [file dirname [info script]] rbu_common.tcl]
+set ::testprefix rbuvacuum
+
+proc do_rbu_vacuum_test {tn} {
+  uplevel [list do_test $tn.1 {
+    forcedelete state.db
+    if {$::step==0} { sqlite3rbu_vacuum rbu test.db state.db }
+    while 1 {
+      if {$::step==1} { sqlite3rbu_vacuum rbu test.db state.db }
+      set rc [rbu step]
+      if {$rc!="SQLITE_OK"} break
+      if {$::step==1} { rbu close }
+    }
+    rbu close
+  } {SQLITE_DONE}]
+
+  uplevel [list do_execsql_test $tn.2 {
+    PRAGMA integrity_check
+  } ok]
+}
+
+foreach step {0 1} {
+
+  set ::testprefix rbuvacuum-step=$step
+  reset_db
+
+  # Simplest possible vacuum.
+  do_execsql_test 1.0 {
+    PRAGMA page_size = 1024;
+    CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c);
+    INSERT INTO t1 VALUES(1, 2, 3);
+    INSERT INTO t1 VALUES(4, 5, 6);
+    INSERT INTO t1 VALUES(7, 8, 9);
+    PRAGMA integrity_check;
+  } {ok}
+  do_rbu_vacuum_test 1.1
+
+  # A vacuum that actually reclaims space.
+  do_execsql_test 1.2.1 {
+    INSERT INTO t1 VALUES(8, randomblob(900), randomblob(900));
+    INSERT INTO t1 VALUES(9, randomblob(900), randomblob(900));
+    INSERT INTO t1 VALUES(10, randomblob(900), randomblob(900));
+    INSERT INTO t1 VALUES(11, randomblob(900), randomblob(900));
+    INSERT INTO t1 VALUES(12, randomblob(900), randomblob(900));
+    PRAGMA page_count;
+  } {12}
+  do_execsql_test 1.2.2 {
+    DELETE FROM t1 WHERE rowid BETWEEN 8 AND 11;
+    PRAGMA page_count;
+  } {12}
+  do_rbu_vacuum_test 1.2.3
+  do_execsql_test 1.2.4 {
+    PRAGMA page_count;
+  } {3}
+  
+  # Add an index to the table.
+  do_execsql_test 1.3.1 {
+    CREATE INDEX t1b ON t1(b);
+    INSERT INTO t1 VALUES(13, randomblob(900), randomblob(900));
+    INSERT INTO t1 VALUES(14, randomblob(900), randomblob(900));
+    INSERT INTO t1 VALUES(15, randomblob(900), randomblob(900));
+    INSERT INTO t1 VALUES(16, randomblob(900), randomblob(900));
+    PRAGMA page_count;
+  } {18}
+  do_execsql_test 1.3.2 {
+    DELETE FROM t1 WHERE rowid BETWEEN 12 AND 15;
+    PRAGMA page_count;
+  } {18}
+  do_rbu_vacuum_test 1.3.3
+  do_execsql_test 1.3.4 {
+    PRAGMA page_count;
+  } {5}
+
+  # WITHOUT ROWID table.
+  do_execsql_test 1.4.1 {
+    CREATE TABLE t2(a, b, c, PRIMARY KEY(a, b)) WITHOUT ROWID;
+
+    INSERT INTO t2 VALUES(randomblob(900), 1, randomblob(900));
+    INSERT INTO t2 VALUES(randomblob(900), 2, randomblob(900));
+    INSERT INTO t2 VALUES(randomblob(900), 3, randomblob(900));
+    INSERT INTO t2 VALUES(randomblob(900), 4, randomblob(900));
+    INSERT INTO t2 VALUES(randomblob(900), 6, randomblob(900));
+    INSERT INTO t2 VALUES(randomblob(900), 7, randomblob(900));
+    INSERT INTO t2 VALUES(randomblob(900), 8, randomblob(900));
+
+    DELETE FROM t2 WHERE b BETWEEN 2 AND 7;
+    PRAGMA page_count;
+  } {20}
+  do_rbu_vacuum_test 1.4.2
+  do_execsql_test 1.4.3 {
+    PRAGMA page_count;
+  } {10}
+  
+  # WITHOUT ROWID table with an index.
+  do_execsql_test 1.4.1 {
+    CREATE INDEX t2c ON t2(c);
+
+    INSERT INTO t2 VALUES(randomblob(900), 9, randomblob(900));
+    INSERT INTO t2 VALUES(randomblob(900), 10, randomblob(900));
+    INSERT INTO t2 VALUES(randomblob(900), 11, randomblob(900));
+    INSERT INTO t2 VALUES(randomblob(900), 12, randomblob(900));
+    INSERT INTO t2 VALUES(randomblob(900), 13, randomblob(900));
+
+    DELETE FROM t2 WHERE b BETWEEN 8 AND 12;
+    PRAGMA page_count;
+  } {35}
+  do_rbu_vacuum_test 1.4.2
+  do_execsql_test 1.4.3 {
+    PRAGMA page_count;
+  } {15}
+  do_execsql_test 1.4.4 {
+    VACUUM;
+    PRAGMA page_count;
+  } {15}
+
+  do_execsql_test 1.5.1 {
+    CREATE TABLE t3(a, b, c);
+    INSERT INTO t3 VALUES('a', 'b', 'c');
+    INSERT INTO t3 VALUES('d', 'e', 'f');
+    INSERT INTO t3 VALUES('g', 'h', 'i');
+  }
+  do_rbu_vacuum_test 1.5.2
+  do_execsql_test 1.5.3 {
+    SELECT * FROM t3
+  } {a b c d e f g h i}
+  do_execsql_test 1.5.4 {
+    CREATE INDEX t3a ON t3(a);
+    CREATE INDEX t3b ON t3(b);
+    CREATE INDEX t3c ON t3(c);
+    INSERT INTO t3 VALUES('j', 'k', 'l');
+    DELETE FROM t3 WHERE a = 'g';
+  }
+  do_rbu_vacuum_test 1.5.5
+  do_execsql_test 1.5.6 {
+    SELECT rowid, * FROM t3 ORDER BY b
+  } {1 a b c 2 d e f 4 j k l}
+
+  do_execsql_test 1.6.1 {
+    CREATE TABLE t4(a PRIMARY KEY, b, c);
+    INSERT INTO t4 VALUES('a', 'b', 'c');
+    INSERT INTO t4 VALUES('d', 'e', 'f');
+    INSERT INTO t4 VALUES('g', 'h', 'i');
+  }
+  do_rbu_vacuum_test 1.6.2
+  do_execsql_test 1.6.3 {
+    SELECT * FROM t4
+  } {a b c d e f g h i}
+  do_execsql_test 1.6.4 {
+    CREATE INDEX t4a ON t4(a);
+    CREATE INDEX t4b ON t4(b);
+    CREATE INDEX t4c ON t4(c);
+    
+    INSERT INTO t4 VALUES('j', 'k', 'l');
+    DELETE FROM t4 WHERE a='g';
+  }
+  do_rbu_vacuum_test 1.6.5
+  do_execsql_test 1.6.6 {
+    SELECT * FROM t4 ORDER BY b
+  } {a b c d e f j k l}
+
+}
+
+catch { db close }
+finish_test
+
index f553ebdac5f6a22e622455a264bd14e5572e926e..e71690769bf5d3757e1583049af07533a7060da8 100644 (file)
@@ -176,6 +176,7 @@ typedef struct RbuUpdateStmt RbuUpdateStmt;
 
 #if !defined(SQLITE_AMALGAMATION)
 typedef unsigned int u32;
+typedef unsigned short u16;
 typedef unsigned char u8;
 typedef sqlite3_int64 i64;
 #endif
@@ -402,6 +403,11 @@ struct rbu_file {
   rbu_file *pMainNext;            /* Next MAIN_DB file */
 };
 
+/*
+** True for an RBU vacuum handle, or false otherwise.
+*/
+#define rbuIsVacuum(p) ((p)->zTarget==0)
+
 
 /*************************************************************************
 ** The following three functions, found below:
@@ -850,8 +856,11 @@ static int rbuObjIterNext(sqlite3rbu *p, RbuObjIter *pIter){
 
 /*
 ** The implementation of the rbu_target_name() SQL function. This function
-** accepts one argument - the name of a table in the RBU database. If the
-** table name matches the pattern:
+** accepts one or two arguments. The first argument is the name of a table -
+** the name of a table in the RBU database.  The second, if it is present, is 1
+** for a view or 0 for a table. 
+**
+** For a non-vacuum RBU handle, if the table name matches the pattern:
 **
 **     data[0-9]_<name>
 **
@@ -862,21 +871,33 @@ static int rbuObjIterNext(sqlite3rbu *p, RbuObjIter *pIter){
 **     "data_t1"     -> "t1"
 **     "data0123_t2" -> "t2"
 **     "dataAB_t3"   -> NULL
+**
+** For an rbu vacuum handle, a copy of the first argument is returned if
+** the second argument is either missing or 0 (not a view).
 */
 static void rbuTargetNameFunc(
-  sqlite3_context *context,
+  sqlite3_context *pCtx,
   int argc,
   sqlite3_value **argv
 ){
+  sqlite3rbu *p = sqlite3_user_data(pCtx);
   const char *zIn;
-  assert( argc==1 );
+  assert( argc==1 || argc==2 );
 
   zIn = (const char*)sqlite3_value_text(argv[0]);
-  if( zIn && strlen(zIn)>4 && memcmp("data", zIn, 4)==0 ){
-    int i;
-    for(i=4; zIn[i]>='0' && zIn[i]<='9'; i++);
-    if( zIn[i]=='_' && zIn[i+1] ){
-      sqlite3_result_text(context, &zIn[i+1], -1, SQLITE_STATIC);
+  if( zIn ){
+    if( rbuIsVacuum(p) ){
+      if( argc==1 || 0==sqlite3_value_int(argv[1]) ){
+        sqlite3_result_text(pCtx, zIn, -1, SQLITE_STATIC);
+      }
+    }else{
+      if( strlen(zIn)>4 && memcmp("data", zIn, 4)==0 ){
+        int i;
+        for(i=4; zIn[i]>='0' && zIn[i]<='9'; i++);
+        if( zIn[i]=='_' && zIn[i+1] ){
+          sqlite3_result_text(pCtx, &zIn[i+1], -1, SQLITE_STATIC);
+        }
+      }
     }
   }
 }
@@ -894,7 +915,8 @@ static int rbuObjIterFirst(sqlite3rbu *p, RbuObjIter *pIter){
   memset(pIter, 0, sizeof(RbuObjIter));
 
   rc = prepareAndCollectError(p->dbRbu, &pIter->pTblIter, &p->zErrmsg, 
-      "SELECT rbu_target_name(name) AS target, name FROM sqlite_master "
+      "SELECT rbu_target_name(name, type='view') AS target, name "
+      "FROM sqlite_master "
       "WHERE type IN ('table', 'view') AND target IS NOT NULL "
       "ORDER BY name"
   );
@@ -1270,6 +1292,7 @@ static int rbuObjIterCacheTableInfo(sqlite3rbu *p, RbuObjIter *pIter){
     pStmt = 0;
 
     if( p->rc==SQLITE_OK
+     && rbuIsVacuum(p)==0
      && bRbuRowid!=(pIter->eType==RBU_PK_VTAB || pIter->eType==RBU_PK_NONE)
     ){
       p->rc = SQLITE_ERROR;
@@ -1409,6 +1432,8 @@ static char *rbuObjIterGetIndexCols(
         for(i=0; pIter->abTblPk[i]==0; i++);
         assert( i<pIter->nTblCol );
         zCol = pIter->azTblCol[i];
+      }else if( rbuIsVacuum(p) ){
+        zCol = "_rowid_";
       }else{
         zCol = "rbu_rowid";
       }
@@ -1949,7 +1974,7 @@ static int rbuObjIterPrepareAll(
       }
 
       /* And to delete index entries */
-      if( p->rc==SQLITE_OK ){
+      if( rbuIsVacuum(p)==0 && p->rc==SQLITE_OK ){
         p->rc = prepareFreeAndCollectError(
             p->dbMain, &pIter->pDelete, &p->zErrmsg,
           sqlite3_mprintf("DELETE FROM \"rbu_imp_%w\" WHERE %s", zTbl, zWhere)
@@ -1959,6 +1984,15 @@ static int rbuObjIterPrepareAll(
       /* Create the SELECT statement to read keys in sorted order */
       if( p->rc==SQLITE_OK ){
         char *zSql;
+        if( rbuIsVacuum(p) ){
+          zSql = sqlite3_mprintf(
+              "SELECT %s, 0 AS rbu_control FROM '%q' ORDER BY %s%s",
+              zCollist, 
+              pIter->zDataTbl,
+              zCollist, zLimit
+          );
+        }else
+
         if( pIter->eType==RBU_PK_EXTERNAL || pIter->eType==RBU_PK_NONE ){
           zSql = sqlite3_mprintf(
               "SELECT %s, rbu_control FROM %s.'rbu_tmp_%q' ORDER BY %s%s",
@@ -1985,7 +2019,9 @@ static int rbuObjIterPrepareAll(
       sqlite3_free(zWhere);
       sqlite3_free(zBind);
     }else{
-      int bRbuRowid = (pIter->eType==RBU_PK_VTAB || pIter->eType==RBU_PK_NONE);
+      int bRbuRowid = (pIter->eType==RBU_PK_VTAB)
+                    ||(pIter->eType==RBU_PK_NONE)
+                    ||(pIter->eType==RBU_PK_EXTERNAL && rbuIsVacuum(p));
       const char *zTbl = pIter->zTbl;       /* Table this step applies to */
       const char *zWrite;                   /* Imposter table name */
 
@@ -2012,8 +2048,10 @@ static int rbuObjIterPrepareAll(
         );
       }
 
-      /* Create the DELETE statement to write to the target PK b-tree */
-      if( p->rc==SQLITE_OK ){
+      /* Create the DELETE statement to write to the target PK b-tree.
+      ** Because it only performs INSERT operations, this is not required for
+      ** an rbu vacuum handle.  */
+      if( rbuIsVacuum(p)==0 && p->rc==SQLITE_OK ){
         p->rc = prepareFreeAndCollectError(p->dbMain, &pIter->pDelete, pz,
             sqlite3_mprintf(
               "DELETE FROM \"%s%w\" WHERE %s", zWrite, zTbl, zWhere
@@ -2021,7 +2059,7 @@ static int rbuObjIterPrepareAll(
         );
       }
 
-      if( pIter->abIndexed ){
+      if( rbuIsVacuum(p)==0 && pIter->abIndexed ){
         const char *zRbuRowid = "";
         if( pIter->eType==RBU_PK_EXTERNAL || pIter->eType==RBU_PK_NONE ){
           zRbuRowid = ", rbu_rowid";
@@ -2071,10 +2109,16 @@ static int rbuObjIterPrepareAll(
 
       /* Create the SELECT statement to read keys from data_xxx */
       if( p->rc==SQLITE_OK ){
+        const char *zRbuRowid = "";
+        if( bRbuRowid ){
+          zRbuRowid = rbuIsVacuum(p) ? ",_rowid_ " : ",rbu_rowid";
+        }
         p->rc = prepareFreeAndCollectError(p->dbRbu, &pIter->pSelect, pz,
             sqlite3_mprintf(
-              "SELECT %s, rbu_control%s FROM '%q'%s", 
-              zCollist, (bRbuRowid ? ", rbu_rowid" : ""), 
+              "SELECT %s,%s rbu_control%s FROM '%q'%s", 
+              zCollist, 
+              (rbuIsVacuum(p) ? "0 AS " : ""),
+              zRbuRowid,
               pIter->zDataTbl, zLimit
             )
         );
@@ -2183,6 +2227,95 @@ static sqlite3 *rbuOpenDbhandle(sqlite3rbu *p, const char *zName){
   return db;
 }
 
+/*
+** Free an RbuState object allocated by rbuLoadState().
+*/
+static void rbuFreeState(RbuState *p){
+  if( p ){
+    sqlite3_free(p->zTbl);
+    sqlite3_free(p->zIdx);
+    sqlite3_free(p);
+  }
+}
+
+/*
+** Allocate an RbuState object and load the contents of the rbu_state 
+** table into it. Return a pointer to the new object. It is the 
+** responsibility of the caller to eventually free the object using
+** sqlite3_free().
+**
+** If an error occurs, leave an error code and message in the rbu handle
+** and return NULL.
+*/
+static RbuState *rbuLoadState(sqlite3rbu *p){
+  RbuState *pRet = 0;
+  sqlite3_stmt *pStmt = 0;
+  int rc;
+  int rc2;
+
+  pRet = (RbuState*)rbuMalloc(p, sizeof(RbuState));
+  if( pRet==0 ) return 0;
+
+  rc = prepareFreeAndCollectError(p->dbRbu, &pStmt, &p->zErrmsg, 
+      sqlite3_mprintf("SELECT k, v FROM %s.rbu_state", p->zStateDb)
+  );
+  while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){
+    switch( sqlite3_column_int(pStmt, 0) ){
+      case RBU_STATE_STAGE:
+        pRet->eStage = sqlite3_column_int(pStmt, 1);
+        if( pRet->eStage!=RBU_STAGE_OAL
+         && pRet->eStage!=RBU_STAGE_MOVE
+         && pRet->eStage!=RBU_STAGE_CKPT
+        ){
+          p->rc = SQLITE_CORRUPT;
+        }
+        break;
+
+      case RBU_STATE_TBL:
+        pRet->zTbl = rbuStrndup((char*)sqlite3_column_text(pStmt, 1), &rc);
+        break;
+
+      case RBU_STATE_IDX:
+        pRet->zIdx = rbuStrndup((char*)sqlite3_column_text(pStmt, 1), &rc);
+        break;
+
+      case RBU_STATE_ROW:
+        pRet->nRow = sqlite3_column_int(pStmt, 1);
+        break;
+
+      case RBU_STATE_PROGRESS:
+        pRet->nProgress = sqlite3_column_int64(pStmt, 1);
+        break;
+
+      case RBU_STATE_CKPT:
+        pRet->iWalCksum = sqlite3_column_int64(pStmt, 1);
+        break;
+
+      case RBU_STATE_COOKIE:
+        pRet->iCookie = (u32)sqlite3_column_int64(pStmt, 1);
+        break;
+
+      case RBU_STATE_OALSZ:
+        pRet->iOalSz = (u32)sqlite3_column_int64(pStmt, 1);
+        break;
+
+      case RBU_STATE_PHASEONESTEP:
+        pRet->nPhaseOneStep = sqlite3_column_int64(pStmt, 1);
+        break;
+
+      default:
+        rc = SQLITE_CORRUPT;
+        break;
+    }
+  }
+  rc2 = sqlite3_finalize(pStmt);
+  if( rc==SQLITE_OK ) rc = rc2;
+
+  p->rc = rc;
+  return pRet;
+}
+
+
 /*
 ** Open the database handle and attach the RBU database as "rbu". If an
 ** error occurs, leave an error code and message in the RBU handle.
@@ -2191,8 +2324,7 @@ static void rbuOpenDatabase(sqlite3rbu *p){
   assert( p->rc==SQLITE_OK );
   assert( p->dbMain==0 && p->dbRbu==0 );
 
-  p->eStage = 0;
-  p->dbMain = rbuOpenDbhandle(p, p->zTarget);
+  /* Open the RBU database */
   p->dbRbu = rbuOpenDbhandle(p, p->zRbu);
 
   /* If using separate RBU and state databases, attach the state database to
@@ -2204,6 +2336,38 @@ static void rbuOpenDatabase(sqlite3rbu *p){
     memcpy(p->zStateDb, "main", 4);
   }
 
+  /* If it has not already been created, create the rbu_state table */
+  rbuMPrintfExec(p, p->dbRbu, RBU_CREATE_STATE, p->zStateDb);
+
+  if( rbuIsVacuum(p) ){
+    int bOpen = 0;
+    if( p->eStage>=RBU_STAGE_MOVE ){
+      bOpen = 1;
+    }else{
+      RbuState *pState = rbuLoadState(p);
+      if( pState ){
+        bOpen = (pState->eStage>RBU_STAGE_MOVE);
+        rbuFreeState(pState);
+      }
+    }
+    if( bOpen ) p->dbMain = rbuOpenDbhandle(p, p->zRbu);
+  }
+
+  p->eStage = 0;
+  if( p->dbMain==0 ){
+    if( p->zTarget ){
+      p->dbMain = rbuOpenDbhandle(p, p->zTarget);
+    }else{
+      char *zTarget = sqlite3_mprintf("%s-vacuum", p->zRbu);
+      if( zTarget==0 ){
+        p->rc = SQLITE_NOMEM;
+        return;
+      }
+      p->dbMain = rbuOpenDbhandle(p, zTarget);
+      sqlite3_free(zTarget);
+    }
+  }
+
   if( p->rc==SQLITE_OK ){
     p->rc = sqlite3_create_function(p->dbMain, 
         "rbu_tmp_insert", -1, SQLITE_UTF8, (void*)p, rbuTmpInsertFunc, 0, 0
@@ -2218,7 +2382,7 @@ static void rbuOpenDatabase(sqlite3rbu *p){
 
   if( p->rc==SQLITE_OK ){
     p->rc = sqlite3_create_function(p->dbRbu, 
-        "rbu_target_name", 1, SQLITE_UTF8, (void*)p, rbuTargetNameFunc, 0, 0
+        "rbu_target_name", -1, SQLITE_UTF8, (void*)p, rbuTargetNameFunc, 0, 0
     );
   }
 
@@ -2477,9 +2641,14 @@ static LPWSTR rbuWinUtf8ToUnicode(const char *zFilename){
 */
 static void rbuMoveOalFile(sqlite3rbu *p){
   const char *zBase = sqlite3_db_filename(p->dbMain, "main");
-
-  char *zWal = sqlite3_mprintf("%s-wal", zBase);
   char *zOal = sqlite3_mprintf("%s-oal", zBase);
+  char *zWal;
+
+  if( rbuIsVacuum(p) ){
+    zWal = sqlite3_mprintf("%s-wal", sqlite3_db_filename(p->dbRbu, "main"));
+  }else{
+    zWal = sqlite3_mprintf("%s-wal", zBase);
+  }
 
   assert( p->eStage==RBU_STAGE_MOVE );
   assert( p->rc==SQLITE_OK && p->zErrmsg==0 );
@@ -2500,8 +2669,8 @@ static void rbuMoveOalFile(sqlite3rbu *p){
 
       /* Re-open the databases. */
       rbuObjIterFinalize(&p->objiter);
-      sqlite3_close(p->dbMain);
       sqlite3_close(p->dbRbu);
+      sqlite3_close(p->dbMain);
       p->dbMain = 0;
       p->dbRbu = 0;
 
@@ -2663,19 +2832,24 @@ static void rbuStepOneOp(sqlite3rbu *p, int eType){
     p->rc = sqlite3_bind_value(pWriter, i+1, pVal);
     if( p->rc ) return;
   }
-  if( pIter->zIdx==0
-   && (pIter->eType==RBU_PK_VTAB || pIter->eType==RBU_PK_NONE) 
-  ){
-    /* For a virtual table, or a table with no primary key, the 
-    ** SELECT statement is:
-    **
-    **   SELECT <cols>, rbu_control, rbu_rowid FROM ....
-    **
-    ** Hence column_value(pIter->nCol+1).
-    */
-    assertColumnName(pIter->pSelect, pIter->nCol+1, "rbu_rowid");
-    pVal = sqlite3_column_value(pIter->pSelect, pIter->nCol+1);
-    p->rc = sqlite3_bind_value(pWriter, pIter->nCol+1, pVal);
+  if( pIter->zIdx==0 ){
+    if( pIter->eType==RBU_PK_VTAB 
+     || pIter->eType==RBU_PK_NONE 
+     || (pIter->eType==RBU_PK_EXTERNAL && rbuIsVacuum(p)) 
+    ){
+      /* For a virtual table, or a table with no primary key, the 
+      ** SELECT statement is:
+      **
+      **   SELECT <cols>, rbu_control, rbu_rowid FROM ....
+      **
+      ** Hence column_value(pIter->nCol+1).
+      */
+      assertColumnName(pIter->pSelect, pIter->nCol+1, 
+          rbuIsVacuum(p) ? "rowid" : "rbu_rowid"
+      );
+      pVal = sqlite3_column_value(pIter->pSelect, pIter->nCol+1);
+      p->rc = sqlite3_bind_value(pWriter, pIter->nCol+1, pVal);
+    }
   }
   if( p->rc==SQLITE_OK ){
     sqlite3_step(pWriter);
@@ -2840,7 +3014,7 @@ int sqlite3rbu_step(sqlite3rbu *p){
             /* Clean up the rbu_tmp_xxx table for the previous table. It 
             ** cannot be dropped as there are currently active SQL statements.
             ** But the contents can be deleted.  */
-            if( pIter->abIndexed ){
+            if( rbuIsVacuum(p)==0 && pIter->abIndexed ){
               rbuMPrintfExec(p, p->dbRbu, 
                   "DELETE FROM %s.'rbu_tmp_%q'", p->zStateDb, pIter->zDataTbl
               );
@@ -2927,94 +3101,6 @@ int sqlite3rbu_step(sqlite3rbu *p){
   }
 }
 
-/*
-** Free an RbuState object allocated by rbuLoadState().
-*/
-static void rbuFreeState(RbuState *p){
-  if( p ){
-    sqlite3_free(p->zTbl);
-    sqlite3_free(p->zIdx);
-    sqlite3_free(p);
-  }
-}
-
-/*
-** Allocate an RbuState object and load the contents of the rbu_state 
-** table into it. Return a pointer to the new object. It is the 
-** responsibility of the caller to eventually free the object using
-** sqlite3_free().
-**
-** If an error occurs, leave an error code and message in the rbu handle
-** and return NULL.
-*/
-static RbuState *rbuLoadState(sqlite3rbu *p){
-  RbuState *pRet = 0;
-  sqlite3_stmt *pStmt = 0;
-  int rc;
-  int rc2;
-
-  pRet = (RbuState*)rbuMalloc(p, sizeof(RbuState));
-  if( pRet==0 ) return 0;
-
-  rc = prepareFreeAndCollectError(p->dbRbu, &pStmt, &p->zErrmsg, 
-      sqlite3_mprintf("SELECT k, v FROM %s.rbu_state", p->zStateDb)
-  );
-  while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){
-    switch( sqlite3_column_int(pStmt, 0) ){
-      case RBU_STATE_STAGE:
-        pRet->eStage = sqlite3_column_int(pStmt, 1);
-        if( pRet->eStage!=RBU_STAGE_OAL
-         && pRet->eStage!=RBU_STAGE_MOVE
-         && pRet->eStage!=RBU_STAGE_CKPT
-        ){
-          p->rc = SQLITE_CORRUPT;
-        }
-        break;
-
-      case RBU_STATE_TBL:
-        pRet->zTbl = rbuStrndup((char*)sqlite3_column_text(pStmt, 1), &rc);
-        break;
-
-      case RBU_STATE_IDX:
-        pRet->zIdx = rbuStrndup((char*)sqlite3_column_text(pStmt, 1), &rc);
-        break;
-
-      case RBU_STATE_ROW:
-        pRet->nRow = sqlite3_column_int(pStmt, 1);
-        break;
-
-      case RBU_STATE_PROGRESS:
-        pRet->nProgress = sqlite3_column_int64(pStmt, 1);
-        break;
-
-      case RBU_STATE_CKPT:
-        pRet->iWalCksum = sqlite3_column_int64(pStmt, 1);
-        break;
-
-      case RBU_STATE_COOKIE:
-        pRet->iCookie = (u32)sqlite3_column_int64(pStmt, 1);
-        break;
-
-      case RBU_STATE_OALSZ:
-        pRet->iOalSz = (u32)sqlite3_column_int64(pStmt, 1);
-        break;
-
-      case RBU_STATE_PHASEONESTEP:
-        pRet->nPhaseOneStep = sqlite3_column_int64(pStmt, 1);
-        break;
-
-      default:
-        rc = SQLITE_CORRUPT;
-        break;
-    }
-  }
-  rc2 = sqlite3_finalize(pStmt);
-  if( rc==SQLITE_OK ) rc = rc2;
-
-  p->rc = rc;
-  return pRet;
-}
-
 /*
 ** Compare strings z1 and z2, returning 0 if they are identical, or non-zero
 ** otherwise. Either or both argument may be NULL. Two NULL values are
@@ -3205,15 +3291,63 @@ static void rbuInitPhaseOneSteps(sqlite3rbu *p){
 }
 
 /*
-** Open and return a new RBU handle. 
+** The RBU handle passed as the only argument has just been opened and 
+** the state database is empty. If this RBU handle was opened for an
+** RBU vacuum operation, create the schema in the target db.
 */
-sqlite3rbu *sqlite3rbu_open(
+static void rbuCreateTargetSchema(sqlite3rbu *p){
+  sqlite3_stmt *pSql = 0;
+  sqlite3_stmt *pInsert = 0;
+  int rc2;
+
+  assert( rbuIsVacuum(p) );
+
+  p->rc = prepareAndCollectError(p->dbRbu, &pSql, &p->zErrmsg, 
+    "SELECT sql FROM sqlite_master WHERE sql!='' AND rootpage!=0"
+    " ORDER BY type DESC"
+  );
+  while( p->rc==SQLITE_OK && sqlite3_step(pSql)==SQLITE_ROW ){
+    const char *zSql = sqlite3_column_text(pSql, 0);
+    p->rc = sqlite3_exec(p->dbMain, zSql, 0, 0, &p->zErrmsg);
+  }
+  rbuFinalize(p, pSql);
+  if( p->rc!=SQLITE_OK ) return;
+
+  p->rc = sqlite3_exec(p->dbMain, "PRAGMA writable_schema=1", 0,0, &p->zErrmsg);
+
+  if( p->rc==SQLITE_OK ){
+    p->rc = prepareAndCollectError(p->dbRbu, &pSql, &p->zErrmsg, 
+        "SELECT * FROM sqlite_master WHERE rootpage=0 OR rootpage IS NULL" 
+    );
+  }
+
+  if( p->rc==SQLITE_OK ){
+    p->rc = prepareAndCollectError(p->dbMain, &pInsert, &p->zErrmsg, 
+        "INSERT INTO sqlite_master VALUES(?,?,?,?,?)"
+    );
+  }
+
+  while( p->rc==SQLITE_OK && sqlite3_step(pSql)==SQLITE_ROW ){
+    int i;
+    for(i=0; i<5; i++){
+      sqlite3_bind_value(pInsert, i+1, sqlite3_column_value(pSql, i));
+    }
+    sqlite3_step(pInsert);
+    p->rc = sqlite3_reset(pInsert);
+  }
+
+  rbuFinalize(p, pSql);
+  rbuFinalize(p, pInsert);
+}
+
+
+static sqlite3rbu *openRbuHandle(
   const char *zTarget, 
   const char *zRbu,
   const char *zState
 ){
   sqlite3rbu *p;
-  size_t nTarget = strlen(zTarget);
+  size_t nTarget = zTarget ? strlen(zTarget) : 0;
   size_t nRbu = strlen(zRbu);
   size_t nState = zState ? strlen(zState) : 0;
   size_t nByte = sizeof(sqlite3rbu) + nTarget+1 + nRbu+1+ nState+1;
@@ -3226,22 +3360,24 @@ sqlite3rbu *sqlite3rbu_open(
     memset(p, 0, sizeof(sqlite3rbu));
     rbuCreateVfs(p);
 
-    /* Open the target database */
+    /* Open the target, RBU and state databases */
     if( p->rc==SQLITE_OK ){
-      p->zTarget = (char*)&p[1];
-      memcpy(p->zTarget, zTarget, nTarget+1);
-      p->zRbu = &p->zTarget[nTarget+1];
+      char *pCsr = (char*)&p[1];
+      if( zTarget ){
+        p->zTarget = pCsr;
+        memcpy(p->zTarget, zTarget, nTarget+1);
+        pCsr += nTarget+1;
+      }
+      p->zRbu = pCsr;
       memcpy(p->zRbu, zRbu, nRbu+1);
+      pCsr += nRbu+1;
       if( zState ){
-        p->zState = &p->zRbu[nRbu+1];
+        p->zState = pCsr;
         memcpy(p->zState, zState, nState+1);
       }
       rbuOpenDatabase(p);
     }
 
-    /* If it has not already been created, create the rbu_state table */
-    rbuMPrintfExec(p, p->dbRbu, RBU_CREATE_STATE, p->zStateDb);
-
     if( p->rc==SQLITE_OK ){
       pState = rbuLoadState(p);
       assert( pState || p->rc!=SQLITE_OK );
@@ -3304,6 +3440,12 @@ sqlite3rbu *sqlite3rbu_open(
           }
         }
 
+        /* If this is an RBU vacuum operation and the state table was empty
+        ** when this handle was opened, create the target database schema. */
+        if( pState->eStage==0 && rbuIsVacuum(p) ){
+          rbuCreateTargetSchema(p);
+        }
+
         /* Point the object iterator at the first object */
         if( p->rc==SQLITE_OK ){
           p->rc = rbuObjIterFirst(p, &p->objiter);
@@ -3336,6 +3478,28 @@ sqlite3rbu *sqlite3rbu_open(
   return p;
 }
 
+/*
+** Open and return a new RBU handle. 
+*/
+sqlite3rbu *sqlite3rbu_open(
+  const char *zTarget, 
+  const char *zRbu,
+  const char *zState
+){
+  /* TODO: Check that zTarget and zRbu are non-NULL */
+  return openRbuHandle(zTarget, zRbu, zState);
+}
+
+/*
+** Open a handle to begin or resume an RBU VACUUM operation.
+*/
+sqlite3rbu *sqlite3rbu_vacuum(
+  const char *zTarget, 
+  const char *zState
+){
+  /* TODO: Check that both arguments are non-NULL */
+  return openRbuHandle(0, zTarget, zState);
+}
 
 /*
 ** Return the database handle used by pRbu.
@@ -3391,8 +3555,8 @@ int sqlite3rbu_close(sqlite3rbu *p, char **pzErrmsg){
     rbuObjIterFinalize(&p->objiter);
 
     /* Close the open database handle and VFS object. */
-    sqlite3_close(p->dbMain);
     sqlite3_close(p->dbRbu);
+    sqlite3_close(p->dbMain);
     rbuDeleteVfs(p);
     sqlite3_free(p->aBuf);
     sqlite3_free(p->aFrame);
@@ -3594,6 +3758,26 @@ static u32 rbuGetU32(u8 *aBuf){
        + ((u32)aBuf[3]);
 }
 
+/*
+** Write an unsigned 32-bit value in big-endian format to the supplied
+** buffer.
+*/
+static void rbuPutU32(u8 *aBuf, u32 iVal){
+  aBuf[0] = (iVal >> 24) & 0xFF;
+  aBuf[1] = (iVal >> 16) & 0xFF;
+  aBuf[2] = (iVal >>  8) & 0xFF;
+  aBuf[3] = (iVal >>  0) & 0xFF;
+}
+
+/*
+** Write an unsigned 16-bit value in big-endian format to the supplied
+** buffer.
+*/
+static void rbuPutU16(u8 *aBuf, u16 iVal){
+  aBuf[0] = (iVal >>  8) & 0xFF;
+  aBuf[1] = (iVal >>  0) & 0xFF;
+}
+
 /*
 ** Read data from an rbuVfs-file.
 */
@@ -3619,6 +3803,34 @@ static int rbuVfsRead(
       memset(zBuf, 0, iAmt);
     }else{
       rc = p->pReal->pMethods->xRead(p->pReal, zBuf, iAmt, iOfst);
+      /* If this is being called to read the first page of the target 
+      ** database as part of an rbu vacuum operation, synthesize the 
+      ** contents of the first page if it does not yet exist. Otherwise,
+      ** SQLite will not check for a *-wal file.  */
+      if( p->pRbu && rbuIsVacuum(p->pRbu) 
+       && rc==SQLITE_IOERR_SHORT_READ && iOfst==0
+       && (p->openFlags & SQLITE_OPEN_MAIN_DB)
+      ){
+        sqlite3_file *pFd = 0;
+        rc = sqlite3_file_control(
+            p->pRbu->dbRbu, "main", SQLITE_FCNTL_FILE_POINTER, (void*)&pFd
+        );
+        if( rc==SQLITE_OK ){
+          rc = pFd->pMethods->xRead(pFd, zBuf, iAmt, iOfst);
+        }
+        if( rc==SQLITE_OK ){
+          rbuPutU32(&zBuf[52], 0);          /* largest root page number */
+          rbuPutU32(&zBuf[36], 0);          /* number of free pages */
+          rbuPutU32(&zBuf[32], 0);          /* first page on free list trunk */
+          rbuPutU32(&zBuf[28], 1);          /* size of db file in pages */
+
+          if( iAmt>100 ){
+            assert( iAmt>=101 );
+            memset(&zBuf[101], 0, iAmt-101);
+            rbuPutU16(&zBuf[105], iAmt & 0xFFFF);
+          }
+        }
+      }
     }
     if( rc==SQLITE_OK && iOfst==0 && (p->openFlags & SQLITE_OPEN_MAIN_DB) ){
       /* These look like magic numbers. But they are stable, as they are part
@@ -3693,7 +3905,20 @@ static int rbuVfsSync(sqlite3_file *pFile, int flags){
 */
 static int rbuVfsFileSize(sqlite3_file *pFile, sqlite_int64 *pSize){
   rbu_file *p = (rbu_file *)pFile;
-  return p->pReal->pMethods->xFileSize(p->pReal, pSize);
+  int rc;
+  rc = p->pReal->pMethods->xFileSize(p->pReal, pSize);
+
+  /* If this is an RBU vacuum operation and this is the target database,
+  ** pretend that it has at least one page. Otherwise, SQLite will not
+  ** check for the existance of a *-wal file. rbuVfsRead() contains 
+  ** similar logic.  */
+  if( rc==SQLITE_OK && *pSize==0 
+   && p->pRbu && rbuIsVacuum(p->pRbu) 
+   && (p->openFlags & SQLITE_OPEN_MAIN_DB)
+  ){
+    *pSize = 1024;
+  }
+  return rc;
 }
 
 /*
index f379bb5b41003069d90bc6043b18fa83cd1626e8..b910866293b21f76e166fe666506a11e2ce73b5a 100644 (file)
@@ -314,6 +314,14 @@ sqlite3rbu *sqlite3rbu_open(
   const char *zState
 );
 
+/*
+** Open an RBU handle to perform an RBU vacuum database file zTarget.
+*/
+sqlite3rbu *sqlite3rbu_vacuum(
+  const char *zTarget, 
+  const char *zState
+);
+
 /*
 ** Internally, each RBU connection uses a separate SQLite database 
 ** connection to access the target and rbu update databases. This
index 5e8640a9f8b3f321c3d27db9608195fcc06a8cf0..629a33cd0e718e6ceff3832204ac4e2410d89ecb 100644 (file)
@@ -187,6 +187,34 @@ static int test_sqlite3rbu(
   return TCL_OK;
 }
 
+/*
+** Tclcmd: sqlite3rbu_vacuum CMD <target-db> <state-db>
+*/
+static int test_sqlite3rbu_vacuum(
+  ClientData clientData,
+  Tcl_Interp *interp,
+  int objc,
+  Tcl_Obj *CONST objv[]
+){
+  sqlite3rbu *pRbu = 0;
+  const char *zCmd;
+  const char *zTarget;
+  const char *zStateDb = 0;
+
+  if( objc!=4 ){
+    Tcl_WrongNumArgs(interp, 1, objv, "NAME TARGET-DB STATE-DB");
+    return TCL_ERROR;
+  }
+  zCmd = Tcl_GetString(objv[1]);
+  zTarget = Tcl_GetString(objv[2]);
+  zStateDb = Tcl_GetString(objv[3]);
+
+  pRbu = sqlite3rbu_vacuum(zTarget, zStateDb);
+  Tcl_CreateObjCommand(interp, zCmd, test_sqlite3rbu_cmd, (ClientData)pRbu, 0);
+  Tcl_SetObjResult(interp, objv[1]);
+  return TCL_OK;
+}
+
 /*
 ** Tclcmd: sqlite3rbu_create_vfs ?-default? NAME PARENT
 */
@@ -274,6 +302,7 @@ int SqliteRbu_Init(Tcl_Interp *interp){
      Tcl_ObjCmdProc *xProc;
   } aObjCmd[] = {
     { "sqlite3rbu", test_sqlite3rbu },
+    { "sqlite3rbu_vacuum", test_sqlite3rbu_vacuum },
     { "sqlite3rbu_create_vfs", test_sqlite3rbu_create_vfs },
     { "sqlite3rbu_destroy_vfs", test_sqlite3rbu_destroy_vfs },
     { "sqlite3rbu_internal_test", test_sqlite3rbu_internal_test },
index c039180bbfed0bb0a9e4b3dffb8c9d0630cda39a..91d704122185d135e0e7f466b9207d6c4d996064 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C CLI\senhancement:\s\sAdd\sthe\s".eqp\sfull"\soption,\sthat\sshows\sboth\sthe\sEXPLAIN\nQUERY\sPLAN\sand\sthe\sEXPLAIN\soutput\sfor\seach\scommand\srun.\s\sAlso\sdisable\nany\s".wheretrace"\sand\s".selecttrace"\swhen\sshowing\sEQP\soutput.
-D 2016-04-15T15:03:27.144
+C Add\sthe\sexperimental\ssqlite3rbu_vacuum()\sAPI\sfunction.\sFor\sopening\san\sRBU\shandle\sthat\srebuilds\sa\sdatabase\sfrom\sscratch.
+D 2016-04-15T20:46:41.467
 F Makefile.in eba680121821b8a60940a81454316f47a341487a
 F Makefile.linux-gcc 7bc79876b875010e8c8f9502eb935ca92aa3c434
 F Makefile.msc 71b8b16cf9393f68e2e2035486ca104872558836
@@ -246,9 +246,10 @@ F ext/rbu/rbufault2.test 9a7f19edd6ea35c4c9f807d8a3db0a03a5670c06
 F ext/rbu/rbufts.test 828cd689da825f0a7b7c53ffc1f6f7fdb6fa5bda
 F ext/rbu/rbuprogress.test 2023a7df2c523e3df1cb532eff811cda385a789a
 F ext/rbu/rbusave.test 0f43b6686084f426ddd040b878426452fd2c2f48
-F ext/rbu/sqlite3rbu.c 9097f1d95666dbef72ca61d5b6a13a84660735ac
-F ext/rbu/sqlite3rbu.h d7cc99350c10134f358fe1a8997d9225b3f712b2
-F ext/rbu/test_rbu.c 3505641a78b723589b8780d5f9b2faeeb73e037d
+F ext/rbu/rbuvacuum.test 75b4231f85622859e814c7f028afad0303f72f60
+F ext/rbu/sqlite3rbu.c 79b8be4a0c8276b2b2b24c88edf3944216ccd35b
+F ext/rbu/sqlite3rbu.h 1342ab6121e715b8da59ec35c5b5c16060be7a6b
+F ext/rbu/test_rbu.c 430b8b9520c233505371d564d3561e0b554355f4
 F ext/rtree/README 6315c0d73ebf0ec40dedb5aa0e942bc8b54e3761
 F ext/rtree/rtree.c 0b870ccb7b58b734a2a8e1e2755a7c0ded070920
 F ext/rtree/rtree.h 834dbcb82dc85b2481cde6a07cdadfddc99e9b9e
@@ -1482,7 +1483,10 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
 F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
 F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
-P d23e581351fb8eea28e7b13b3dcadfc817c3a05f
-R e880e1f26ac9fef78137656188a6e0aa
-U drh
-Z dc2294cde78bca18f07e3e2fb59a4dd5
+P 3e217d6265ecd16db783bed7ce1d9d0f9c4828bb
+R d2c575fe1215bc1b298a105940b3f837
+T *branch * rbu-vacuum
+T *sym-rbu-vacuum *
+T -sym-trunk *
+U dan
+Z af92587ff7c9268b9a60aaab80a1a468
index 453393af12087ce826ead2e3270532b36a25b068..66b85c22ccee24bd595f1666d3e355365e527f82 100644 (file)
@@ -1 +1 @@
-3e217d6265ecd16db783bed7ce1d9d0f9c4828bb
\ No newline at end of file
+0216b48f28042ad86711e00802c2da8ce9be3044
\ No newline at end of file