ADDED ext/rbu/rbuC.test Index: ext/rbu/rbuC.test ================================================================== --- /dev/null +++ ext/rbu/rbuC.test @@ -0,0 +1,142 @@ +# 2016 March 7 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# Tests for RBU focused on the REPLACE operation (rbu_control column +# contains integer value 2). +# + +source [file join [file dirname [info script]] rbu_common.tcl] +set ::testprefix rbuC + +#------------------------------------------------------------------------- +# This test is actually of an UPDATE directive. Just to establish that +# these work with UNIQUE indexes before preceding to REPLACE. +# +do_execsql_test 1.0 { + CREATE TABLE t1(i INTEGER PRIMARY KEY, a, b, c UNIQUE); + INSERT INTO t1 VALUES(1, 'a', 'b', 'c'); +} + +forcedelete rbu.db +do_execsql_test 1.1 { + ATTACH 'rbu.db' AS rbu; + CREATE TABLE rbu.data_t1(i, a, b, c, rbu_control); + INSERT INTO data_t1 VALUES(1, 'a', 'b', 'c', '.xxx'); +} + +do_test 1.2 { + step_rbu test.db rbu.db +} {SQLITE_DONE} + +do_execsql_test 1.3 { + SELECT * FROM t1 +} { + 1 a b c +} + +#------------------------------------------------------------------------- +# +foreach {tn schema} { + 1 { + CREATE TABLE t1(i INTEGER PRIMARY KEY, a, b, c UNIQUE); + CREATE INDEX t1a ON t1(a); + } + 2 { + CREATE TABLE t1(i PRIMARY KEY, a, b, c UNIQUE); + CREATE INDEX t1a ON t1(a); + } + 3 { + CREATE TABLE t1(i PRIMARY KEY, a, b, c UNIQUE) WITHOUT ROWID; + CREATE INDEX t1a ON t1(a); + } +} { + reset_db + forcedelete rbu.db + execsql $schema + + do_execsql_test 2.$tn.0 { + INSERT INTO t1 VALUES(1, 'a', 'b', 'c'); + INSERT INTO t1 VALUES(2, 'b', 'c', 'd'); + INSERT INTO t1 VALUES(3, 'c', 'd', 'e'); + } + + do_execsql_test 2.$tn.1 { + ATTACH 'rbu.db' AS rbu; + CREATE TABLE rbu.data_t1(i, a, b, c, rbu_control); + INSERT INTO data_t1 VALUES(1, 1, 2, 3, 2); + INSERT INTO data_t1 VALUES(3, 'c', 'd', 'e', 2); + INSERT INTO data_t1 VALUES(4, 'd', 'e', 'f', 2); + } + + do_test 2.$tn.2 { + step_rbu test.db rbu.db + } {SQLITE_DONE} + + do_execsql_test 2.$tn.3 { + SELECT * FROM t1 ORDER BY i + } { + 1 1 2 3 + 2 b c d + 3 c d e + 4 d e f + } + + integrity_check 2.$tn.4 +} + +foreach {tn schema} { + 1 { + CREATE TABLE t1(a, b, c UNIQUE); + CREATE INDEX t1a ON t1(a); + } + + 2 { + CREATE VIRTUAL TABLE t1 USING fts5(a, b, c); + } +} { + if {$tn==2} { ifcapable !fts5 break } + reset_db + forcedelete rbu.db + execsql $schema + + do_execsql_test 3.$tn.0 { + INSERT INTO t1 VALUES('a', 'b', 'c'); + INSERT INTO t1 VALUES('b', 'c', 'd'); + INSERT INTO t1 VALUES('c', 'd', 'e'); + } + + do_execsql_test 3.$tn.1 { + ATTACH 'rbu.db' AS rbu; + CREATE TABLE rbu.data_t1(rbu_rowid, a, b, c, rbu_control); + INSERT INTO data_t1 VALUES(1, 1, 2, 3, 2); + INSERT INTO data_t1 VALUES(3, 'c', 'd', 'e', 2); + INSERT INTO data_t1 VALUES(4, 'd', 'e', 'f', 2); + } + + do_test 3.$tn.2 { + step_rbu test.db rbu.db + } {SQLITE_DONE} + + do_execsql_test 3.$tn.3 { + SELECT rowid, * FROM t1 ORDER BY 1 + } { + 1 1 2 3 + 2 b c d + 3 c d e + 4 d e f + } + + integrity_check 3.$tn.4 +} + + + +finish_test + Index: ext/rbu/sqlite3rbu.c ================================================================== --- ext/rbu/sqlite3rbu.c +++ ext/rbu/sqlite3rbu.c @@ -278,14 +278,15 @@ ** Within the RBU_STAGE_OAL stage, each call to sqlite3rbu_step() performs ** one of the following operations. */ #define RBU_INSERT 1 /* Insert on a main table b-tree */ #define RBU_DELETE 2 /* Delete a row from a main table b-tree */ -#define RBU_IDX_DELETE 3 /* Delete a row from an aux. index b-tree */ -#define RBU_IDX_INSERT 4 /* Insert on an aux. index b-tree */ -#define RBU_UPDATE 5 /* Update a row in a main table b-tree */ +#define RBU_REPLACE 3 /* Delete and then insert a row */ +#define RBU_IDX_DELETE 4 /* Delete a row from an aux. index b-tree */ +#define RBU_IDX_INSERT 5 /* Insert on an aux. index b-tree */ +#define RBU_UPDATE 6 /* Update a row in a main table b-tree */ /* ** A single step of an incremental checkpoint - frame iWalFrame of the wal ** file should be copied to page iDbPage of the database file. */ @@ -1907,17 +1908,17 @@ zCollist, p->zStateDb, pIter->zDataTbl, zCollist, zLimit ); }else{ zSql = sqlite3_mprintf( + "SELECT %s, rbu_control FROM %s.'rbu_tmp_%q' " + "UNION ALL " "SELECT %s, rbu_control FROM '%q' " "WHERE typeof(rbu_control)='integer' AND rbu_control!=1 " - "UNION ALL " - "SELECT %s, rbu_control FROM %s.'rbu_tmp_%q' " "ORDER BY %s%s", - zCollist, pIter->zDataTbl, zCollist, p->zStateDb, pIter->zDataTbl, + zCollist, pIter->zDataTbl, zCollist, zLimit ); } p->rc = prepareFreeAndCollectError(p->dbRbu, &pIter->pSelect, pz, zSql); } @@ -1979,21 +1980,21 @@ ); rbuMPrintfExec(p, p->dbMain, "CREATE TEMP TRIGGER rbu_delete_tr BEFORE DELETE ON \"%s%w\" " "BEGIN " - " SELECT rbu_tmp_insert(2, %s);" + " SELECT rbu_tmp_insert(3, %s);" "END;" "CREATE TEMP TRIGGER rbu_update1_tr BEFORE UPDATE ON \"%s%w\" " "BEGIN " - " SELECT rbu_tmp_insert(2, %s);" + " SELECT rbu_tmp_insert(3, %s);" "END;" "CREATE TEMP TRIGGER rbu_update2_tr AFTER UPDATE ON \"%s%w\" " "BEGIN " - " SELECT rbu_tmp_insert(3, %s);" + " SELECT rbu_tmp_insert(4, %s);" "END;", zWrite, zTbl, zOldlist, zWrite, zTbl, zOldlist, zWrite, zTbl, zNewlist ); @@ -2507,18 +2508,16 @@ int res = 0; /* Return value */ switch( sqlite3_column_type(p->objiter.pSelect, iCol) ){ case SQLITE_INTEGER: { int iVal = sqlite3_column_int(p->objiter.pSelect, iCol); - if( iVal==0 ){ - res = RBU_INSERT; - }else if( iVal==1 ){ - res = RBU_DELETE; - }else if( iVal==2 ){ - res = RBU_IDX_DELETE; - }else if( iVal==3 ){ - res = RBU_IDX_INSERT; + switch( iVal ){ + case 0: res = RBU_INSERT; break; + case 1: res = RBU_DELETE; break; + case 2: res = RBU_REPLACE; break; + case 3: res = RBU_IDX_DELETE; break; + case 4: res = RBU_IDX_INSERT; break; } break; } case SQLITE_TEXT: { @@ -2552,10 +2551,71 @@ assert( 0==sqlite3_stricmp(zName, zCol) ); } #else # define assertColumnName(x,y,z) #endif + +/* +** Argument eType must be one of RBU_INSERT, RBU_DELETE, RBU_IDX_INSERT or +** RBU_IDX_DELETE. This function performs the work of a single +** sqlite3rbu_step() call for the type of operation specified by eType. +*/ +static void rbuStepOneOp(sqlite3rbu *p, int eType){ + RbuObjIter *pIter = &p->objiter; + sqlite3_value *pVal; + sqlite3_stmt *pWriter; + int i; + + assert( p->rc==SQLITE_OK ); + assert( eType!=RBU_DELETE || pIter->zIdx==0 ); + + if( eType==RBU_IDX_DELETE || eType==RBU_DELETE ){ + pWriter = pIter->pDelete; + }else{ + pWriter = pIter->pInsert; + } + + for(i=0; inCol; i++){ + /* If this is an INSERT into a table b-tree and the table has an + ** explicit INTEGER PRIMARY KEY, check that this is not an attempt + ** to write a NULL into the IPK column. That is not permitted. */ + if( eType==RBU_INSERT + && pIter->zIdx==0 && pIter->eType==RBU_PK_IPK && pIter->abTblPk[i] + && sqlite3_column_type(pIter->pSelect, i)==SQLITE_NULL + ){ + p->rc = SQLITE_MISMATCH; + p->zErrmsg = sqlite3_mprintf("datatype mismatch"); + return; + } + + if( eType==RBU_DELETE && pIter->abTblPk[i]==0 ){ + continue; + } + + pVal = sqlite3_column_value(pIter->pSelect, i); + 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 , 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( p->rc==SQLITE_OK ){ + sqlite3_step(pWriter); + p->rc = resetAndCollectError(pWriter, &p->zErrmsg); + } +} /* ** This function does the work for an sqlite3rbu_step() call. ** ** The object-iterator (p->objiter) currently points to a valid object, @@ -2567,82 +2627,36 @@ ** returned. */ static int rbuStep(sqlite3rbu *p){ RbuObjIter *pIter = &p->objiter; const char *zMask = 0; - int i; int eType = rbuStepType(p, &zMask); if( eType ){ + assert( eType==RBU_INSERT || eType==RBU_DELETE + || eType==RBU_REPLACE || eType==RBU_IDX_DELETE + || eType==RBU_IDX_INSERT || eType==RBU_UPDATE + ); assert( eType!=RBU_UPDATE || pIter->zIdx==0 ); if( pIter->zIdx==0 && eType==RBU_IDX_DELETE ){ rbuBadControlError(p); } - else if( - eType==RBU_INSERT - || eType==RBU_DELETE - || eType==RBU_IDX_DELETE - || eType==RBU_IDX_INSERT - ){ - sqlite3_value *pVal; - sqlite3_stmt *pWriter; - - assert( eType!=RBU_UPDATE ); - assert( eType!=RBU_DELETE || pIter->zIdx==0 ); - - if( eType==RBU_IDX_DELETE || eType==RBU_DELETE ){ - pWriter = pIter->pDelete; - }else{ - pWriter = pIter->pInsert; - } - - for(i=0; inCol; i++){ - /* If this is an INSERT into a table b-tree and the table has an - ** explicit INTEGER PRIMARY KEY, check that this is not an attempt - ** to write a NULL into the IPK column. That is not permitted. */ - if( eType==RBU_INSERT - && pIter->zIdx==0 && pIter->eType==RBU_PK_IPK && pIter->abTblPk[i] - && sqlite3_column_type(pIter->pSelect, i)==SQLITE_NULL - ){ - p->rc = SQLITE_MISMATCH; - p->zErrmsg = sqlite3_mprintf("datatype mismatch"); - goto step_out; - } - - if( eType==RBU_DELETE && pIter->abTblPk[i]==0 ){ - continue; - } - - pVal = sqlite3_column_value(pIter->pSelect, i); - p->rc = sqlite3_bind_value(pWriter, i+1, pVal); - if( p->rc ) goto step_out; - } - 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 , 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( p->rc==SQLITE_OK ){ - sqlite3_step(pWriter); - p->rc = resetAndCollectError(pWriter, &p->zErrmsg); - } - }else{ + else if( eType==RBU_REPLACE ){ + if( pIter->zIdx==0 ) rbuStepOneOp(p, RBU_DELETE); + if( p->rc==SQLITE_OK ) rbuStepOneOp(p, RBU_INSERT); + } + else if( eType!=RBU_UPDATE ){ + rbuStepOneOp(p, eType); + } + else{ sqlite3_value *pVal; sqlite3_stmt *pUpdate = 0; assert( eType==RBU_UPDATE ); rbuGetUpdateStmt(p, pIter, zMask, &pUpdate); if( pUpdate ){ + int i; for(i=0; p->rc==SQLITE_OK && inCol; i++){ char c = zMask[pIter->aiSrcOrder[i]]; pVal = sqlite3_column_value(pIter->pSelect, i); if( pIter->abTblPk[i] || c!='.' ){ p->rc = sqlite3_bind_value(pUpdate, i+1, pVal);