diff options
47 files changed, 11956 insertions, 140 deletions
diff --git a/Makefile.in b/Makefile.in index 750b8fd8b..3da95d9de 100644 --- a/Makefile.in +++ b/Makefile.in @@ -30,7 +30,8 @@ BCC = @BUILD_CC@ @BUILD_CFLAGS@ # CC = @CC@ CFLAGS = @CPPFLAGS@ @CFLAGS@ -TCC = $(CC) $(CFLAGS) -I. -I${TOP}/src -I${TOP}/ext/rtree -I${TOP}/ext/fts3 +TCC = ${CC} ${CFLAGS} -I. -I${TOP}/src -I${TOP}/ext/rtree -I${TOP}/ext/icu +TCC += -I${TOP}/ext/fts3 -I${TOP}/ext/async -I${TOP}/ext/session # Define this for the autoconf-based build, so that the code knows it can # include the generated config.h @@ -80,6 +81,7 @@ TEMP_STORE = -DSQLITE_TEMP_STORE=@TEMP_STORE@ # The same set of OMIT and ENABLE flags should be passed to the # LEMON parser generator and the mkkeywordhash tool as well. OPT_FEATURE_FLAGS = @OPT_FEATURE_FLAGS@ +OPT_FEATURE_FLAGS += -DSQLITE_ENABLE_SESSION -DSQLITE_ENABLE_PREUPDATE_HOOK TCC += $(OPT_FEATURE_FLAGS) @@ -180,7 +182,8 @@ LIBOBJS0 = alter.lo analyze.lo attach.lo auth.lo \ mutex.lo mutex_noop.lo mutex_unix.lo mutex_w32.lo \ notify.lo opcodes.lo os.lo os_unix.lo os_win.lo \ pager.lo parse.lo pcache.lo pcache1.lo pragma.lo prepare.lo printf.lo \ - random.lo resolve.lo rowset.lo rtree.lo select.lo status.lo \ + random.lo resolve.lo rowset.lo rtree.lo \ + sqlite3session.lo select.lo status.lo \ table.lo threads.lo tokenize.lo trigger.lo \ update.lo util.lo vacuum.lo \ vdbe.lo vdbeapi.lo vdbeaux.lo vdbeblob.lo vdbemem.lo vdbesort.lo \ @@ -337,6 +340,10 @@ SRC += \ SRC += \ $(TOP)/ext/rtree/rtree.h \ $(TOP)/ext/rtree/rtree.c +SRC += \ + $(TOP)/ext/session/sqlite3session.c \ + $(TOP)/ext/session/sqlite3session.h + # Generated source code files @@ -394,7 +401,8 @@ TESTSRC = \ $(TOP)/src/test_vfs.c \ $(TOP)/src/test_wsd.c \ $(TOP)/ext/fts3/fts3_term.c \ - $(TOP)/ext/fts3/fts3_test.c + $(TOP)/ext/fts3/fts3_test.c \ + $(TOP)/ext/session/test_session.c # Statically linked extensions # @@ -455,7 +463,8 @@ TESTSRC2 = \ $(TOP)/ext/fts3/fts3_term.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/session/sqlite3session.c # Header files used by all library source files. # @@ -562,6 +571,7 @@ mptest: mptester$(EXE) sqlite3.c: .target_source $(TOP)/tool/mksqlite3c.tcl $(TCLSH_CMD) $(TOP)/tool/mksqlite3c.tcl cp tsrc/shell.c tsrc/sqlite3ext.h . + cp $(TOP)/ext/session/sqlite3session.h . tclsqlite3.c: sqlite3.c echo '#ifndef USE_SYSTEM_SQLITE' >tclsqlite3.c @@ -913,6 +923,9 @@ fts3_write.lo: $(TOP)/ext/fts3/fts3_write.c $(HDR) $(EXTHDR) rtree.lo: $(TOP)/ext/rtree/rtree.c $(HDR) $(EXTHDR) $(LTCOMPILE) -DSQLITE_CORE -c $(TOP)/ext/rtree/rtree.c +sqlite3session.lo: $(TOP)/ext/session/sqlite3session.c $(HDR) $(EXTHDR) + $(LTCOMPILE) -DSQLITE_CORE -c $(TOP)/ext/session/sqlite3session.c + # Rules to build the 'testfixture' application. # @@ -988,6 +1001,9 @@ showjournal$(TEXE): $(TOP)/tool/showjournal.c sqlite3.lo showwal$(TEXE): $(TOP)/tool/showwal.c sqlite3.lo $(LTLINK) -o $@ $(TOP)/tool/showwal.c sqlite3.lo $(TLIBS) +changeset$(TEXE): $(TOP)/ext/session/changeset.c sqlite3.lo + $(LTLINK) -o $@ $(TOP)/ext/session/changeset.c sqlite3.lo $(TLIBS) + rollback-test$(TEXE): $(TOP)/tool/rollback-test.c sqlite3.lo $(LTLINK) -o $@ $(TOP)/tool/rollback-test.c sqlite3.lo $(TLIBS) @@ -1069,7 +1085,7 @@ clean: rm -f testfixture$(TEXE) test.db rm -f LogEst$(TEXE) fts3view$(TEXE) rollback-test$(TEXE) showdb$(TEXE) rm -f showjournal$(TEXE) showstat4$(TEXE) showwal$(TEXE) speedtest1$(TEXE) - rm -f wordcount$(TEXE) + rm -f wordcount$(TEXE) changeset$(TEXE) rm -f sqlite3.dll sqlite3.lib sqlite3.exp sqlite3.def rm -f sqlite3.c rm -f sqlite3rc.h diff --git a/Makefile.msc b/Makefile.msc index 4621661ee..bd8917e04 100644 --- a/Makefile.msc +++ b/Makefile.msc @@ -340,6 +340,8 @@ TCC = $(TCC) -I$(TOP)\ext\fts3 RCC = $(RCC) -I$(TOP)\ext\fts3 TCC = $(TCC) -I$(TOP)\ext\rtree RCC = $(RCC) -I$(TOP)\ext\rtree +TCC = $(TCC) -I$(TOP)\ext\session +RCC = $(RCC) -I$(TOP)\ext\session !ENDIF # The mksqlite3c.tcl script accepts some options on the command @@ -502,6 +504,14 @@ RCC = $(RCC) -DSQLITE_TEMP_STORE=1 # The same set of OMIT and ENABLE flags should be passed to the # LEMON parser generator and the mkkeywordhash tool as well. +# BEGIN standard options +OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_FTS3=1 +OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_RTREE=1 +OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_COLUMN_METADATA=1 +OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_SESSION=1 +OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_PREUPDATE_HOOK=1 +# END standard options + # These are the required SQLite compilation options used when compiling for # the Windows platform. # @@ -686,7 +696,8 @@ LIBOBJS0 = vdbe.lo parse.lo alter.lo analyze.lo attach.lo auth.lo \ mutex.lo mutex_noop.lo mutex_unix.lo mutex_w32.lo \ notify.lo opcodes.lo os.lo os_unix.lo os_win.lo \ pager.lo pcache.lo pcache1.lo pragma.lo prepare.lo printf.lo \ - random.lo resolve.lo rowset.lo rtree.lo select.lo status.lo \ + random.lo resolve.lo rowset.lo rtree.lo \ + sqlite3session.lo select.lo status.lo \ table.lo threads.lo tokenize.lo trigger.lo \ update.lo util.lo vacuum.lo \ vdbeapi.lo vdbeaux.lo vdbeblob.lo vdbemem.lo vdbesort.lo \ @@ -852,6 +863,9 @@ SRC4 = \ $(TOP)\ext\icu\icu.c \ $(TOP)\ext\rtree\rtree.h \ $(TOP)\ext\rtree\rtree.c +SRC = $(SRC) \ + $(TOP)\ext\session\sqlite3session.h \ + $(TOP)\ext\session\sqlite3session.c # Generated source code files @@ -912,7 +926,8 @@ TESTSRC = \ $(TOP)\src\test_vfs.c \ $(TOP)\src\test_wsd.c \ $(TOP)\ext\fts3\fts3_term.c \ - $(TOP)\ext\fts3\fts3_test.c + $(TOP)\ext\fts3\fts3_test.c \ + $(TOP)\ext\session\test_session.c # Statically linked extensions # @@ -932,6 +947,7 @@ TESTEXT = \ # Source code to the library files needed by the test fixture +# (non-amalgamation) # TESTSRC2 = \ $(TOP)\src\attach.c \ @@ -977,7 +993,14 @@ TESTSRC2 = \ $(TOP)\ext\fts3\fts3_unicode.c \ $(TOP)\ext\fts3\fts3_unicode2.c \ $(TOP)\ext\fts3\fts3_write.c \ - $(TOP)\ext\async\sqlite3async.c + $(TOP)\ext\async\sqlite3async.c \ + $(TOP)\ext\session\sqlite3session.c + +# Source code to the library files needed by the test fixture +# (amalgamation) +# +TESTSRC3 = + # Header files used by all library source files. # @@ -1027,6 +1050,8 @@ EXTHDR = $(EXTHDR) \ $(TOP)\ext\icu\sqliteicu.h EXTHDR = $(EXTHDR) \ $(TOP)\ext\rtree\sqlite3rtree.h +EXTHDR = $(EXTHDR) \ + $(TOP)\ext\session\sqlite3session.h # This is the default Makefile target. The objects listed here # are what get build when you type just "make" with no arguments. @@ -1070,6 +1095,7 @@ sqlite3.c: .target_source $(TOP)\tool\mksqlite3c.tcl $(TCLSH_CMD) $(TOP)\tool\mksqlite3c.tcl $(MKSQLITE3C_ARGS) copy tsrc\shell.c . copy tsrc\sqlite3ext.h . + copy $(TOP)\ext\session\sqlite3session.h . sqlite3-all.c: sqlite3.c $(TOP)\tool\split-sqlite3c.tcl $(TCLSH_CMD) $(TOP)\tool\split-sqlite3c.tcl @@ -1439,6 +1465,9 @@ fts3_write.lo: $(TOP)\ext\fts3\fts3_write.c $(HDR) $(EXTHDR) rtree.lo: $(TOP)\ext\rtree\rtree.c $(HDR) $(EXTHDR) $(LTCOMPILE) -DSQLITE_CORE -c $(TOP)\ext\rtree\rtree.c +sqlite3session.lo: $(TOP)\ext\session\sqlite3sesion.c $(HDR) $(EXTHDR) + $(LTCOMPILE) -DSQLITE_CORE -c $(TOP)\ext\session\sqlite3session.c + # Rules to build the 'testfixture' application. # @@ -1452,7 +1481,7 @@ TESTFIXTURE_FLAGS = $(TESTFIXTURE_FLAGS) -DSQLITE_SERVER=1 -DSQLITE_PRIVATE="" TESTFIXTURE_FLAGS = $(TESTFIXTURE_FLAGS) -DSQLITE_CORE $(NO_WARN) TESTFIXTURE_SRC0 = $(TESTEXT) $(TESTSRC2) libsqlite3.lib -TESTFIXTURE_SRC1 = $(TESTEXT) $(SQLITE3C) +TESTFIXTURE_SRC1 = $(TESTEXT) $(TESTSRC3) $(SQLITE3C) !IF $(USE_AMALGAMATION)==0 TESTFIXTURE_SRC = $(TESTSRC) $(TOP)\src\tclsqlite.c $(TESTFIXTURE_SRC0) !ELSE @@ -1516,6 +1545,10 @@ showwal.exe: $(TOP)\tool\showwal.c $(SQLITE3C) $(LTLINK) -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION -Fe$@ \ $(TOP)\tool\showwal.c $(SQLITE3C) +changeset.exe: $(TOP)\ext\session\changeset.c $(SQLITE3C) + $(LTLINK) -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION -Fe$@ \ + $(TOP)\ext\session\changeset.c $(SQLITE3C) + fts3view.exe: $(TOP)\ext\fts3\tool\fts3view.c $(SQLITE3C) $(LTLINK) -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION -Fe$@ \ $(TOP)\ext\fts3\tool\fts3view.c $(SQLITE3C) @@ -1552,7 +1585,7 @@ clean: del /Q tclsqlite3.exe tclsqlite3.exp del /Q testloadext.dll testloadext.exp del /Q testfixture.exe testfixture.exp test.db - del /Q LogEst.exe fts3view.exe rollback-test.exe showdb.exe + del /Q LogEst.exe fts3view.exe rollback-test.exe showdb.exe changeset.exe del /Q showjournal.exe showstat4.exe showwal.exe speedtest1.exe del /Q wordcount.exe del /Q sqlite3.dll sqlite3.lib sqlite3.exp sqlite3.def diff --git a/ext/session/changeset.c b/ext/session/changeset.c new file mode 100644 index 000000000..edb43ff8e --- /dev/null +++ b/ext/session/changeset.c @@ -0,0 +1,416 @@ +/* +** 2014-08-18 +** +** The author disclaims copyright to this source code. In place of +** a legal notice, here is a blessing: +** +** May you do good and not evil. +** May you find forgiveness for yourself and forgive others. +** May you share freely, never taking more than you give. +** +************************************************************************* +** This file contains code to implement the "changeset" command line +** utility for displaying and transforming changesets generated by +** the Sessions extension. +*/ +#include "sqlite3.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <assert.h> +#include <ctype.h> + + +/* +** Show a usage message on stderr then quit. +*/ +static void usage(const char *argv0){ + fprintf(stderr, "Usage: %s FILENAME COMMAND ...\n", argv0); + fprintf(stderr, + "COMMANDs:\n" + " apply DB Apply the changeset to database file DB\n" + " concat FILE2 OUT Concatenate FILENAME and FILE2 into OUT\n" + " dump Show the complete content of the changeset\n" + " invert OUT Write an inverted changeset into file OUT\n" + " sql Give a pseudo-SQL rendering of the changeset\n" + ); + exit(1); +} + +/* +** Read the content of a disk file into an in-memory buffer +*/ +static void readFile(const char *zFilename, int *pSz, void **ppBuf){ + FILE *f; + int sz; + void *pBuf; + f = fopen(zFilename, "rb"); + if( f==0 ){ + fprintf(stderr, "cannot open \"%s\" for reading\n", zFilename); + exit(1); + } + fseek(f, 0, SEEK_END); + sz = (int)ftell(f); + rewind(f); + pBuf = sqlite3_malloc( sz ? sz : 1 ); + if( pBuf==0 ){ + fprintf(stderr, "cannot allocate %d to hold content of \"%s\"\n", + sz, zFilename); + exit(1); + } + if( sz>0 ){ + if( fread(pBuf, sz, 1, f)!=1 ){ + fprintf(stderr, "cannot read all %d bytes of \"%s\"\n", sz, zFilename); + exit(1); + } + fclose(f); + } + *pSz = sz; + *ppBuf = pBuf; +} + +/* Array for converting from half-bytes (nybbles) into ASCII hex +** digits. */ +static const char hexdigits[] = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' +}; + +/* +** Render an sqlite3_value as an SQL string. +*/ +static void renderValue(sqlite3_value *pVal){ + switch( sqlite3_value_type(pVal) ){ + case SQLITE_FLOAT: { + double r1; + char zBuf[50]; + r1 = sqlite3_value_double(pVal); + sqlite3_snprintf(sizeof(zBuf), zBuf, "%!.15g", r1); + printf("%s", zBuf); + break; + } + case SQLITE_INTEGER: { + printf("%lld", sqlite3_value_int64(pVal)); + break; + } + case SQLITE_BLOB: { + char const *zBlob = sqlite3_value_blob(pVal); + int nBlob = sqlite3_value_bytes(pVal); + int i; + printf("x'"); + for(i=0; i<nBlob; i++){ + putchar(hexdigits[(zBlob[i]>>4)&0x0F]); + putchar(hexdigits[(zBlob[i])&0x0F]); + } + putchar('\''); + break; + } + case SQLITE_TEXT: { + const unsigned char *zArg = sqlite3_value_text(pVal); + putchar('\''); + while( zArg[0] ){ + putchar(zArg[0]); + if( zArg[0]=='\'' ) putchar(zArg[0]); + zArg++; + } + putchar('\''); + break; + } + default: { + assert( sqlite3_value_type(pVal)==SQLITE_NULL ); + printf("NULL"); + break; + } + } +} + +/* +** Number of conflicts seen +*/ +static int nConflict = 0; + +/* +** The conflict callback +*/ +static int conflictCallback( + void *pCtx, + int eConflict, + sqlite3_changeset_iter *pIter +){ + int op, bIndirect, nCol, i; + const char *zTab; + unsigned char *abPK; + const char *zType = ""; + const char *zOp = ""; + const char *zSep = " "; + + nConflict++; + sqlite3changeset_op(pIter, &zTab, &nCol, &op, &bIndirect); + sqlite3changeset_pk(pIter, &abPK, 0); + switch( eConflict ){ + case SQLITE_CHANGESET_DATA: zType = "DATA"; break; + case SQLITE_CHANGESET_NOTFOUND: zType = "NOTFOUND"; break; + case SQLITE_CHANGESET_CONFLICT: zType = "PRIMARY KEY"; break; + case SQLITE_CHANGESET_FOREIGN_KEY: zType = "FOREIGN KEY"; break; + case SQLITE_CHANGESET_CONSTRAINT: zType = "CONSTRAINT"; break; + } + switch( op ){ + case SQLITE_UPDATE: zOp = "UPDATE of"; break; + case SQLITE_INSERT: zOp = "INSERT into"; break; + case SQLITE_DELETE: zOp = "DELETE from"; break; + } + printf("%s conflict on %s table %s with primary key", zType, zOp, zTab); + for(i=0; i<nCol; i++){ + sqlite3_value *pVal; + if( abPK[i]==0 ) continue; + printf("%s", zSep); + if( op==SQLITE_INSERT ){ + sqlite3changeset_new(pIter, i, &pVal); + }else{ + sqlite3changeset_old(pIter, i, &pVal); + } + renderValue(pVal); + zSep = ","; + } + printf("\n"); + return SQLITE_CHANGESET_OMIT; +} + +int main(int argc, char **argv){ + int sz, rc; + void *pBuf = 0; + if( argc<3 ) usage(argv[0]); + readFile(argv[1], &sz, &pBuf); + + /* changeset FILENAME apply DB + ** Apply the changeset in FILENAME to the database file DB + */ + if( strcmp(argv[2],"apply")==0 ){ + sqlite3 *db; + if( argc!=4 ) usage(argv[0]); + rc = sqlite3_open(argv[3], &db); + if( rc!=SQLITE_OK ){ + fprintf(stderr, "unable to open database file \"%s\": %s\n", + argv[3], sqlite3_errmsg(db)); + sqlite3_close(db); + exit(1); + } + sqlite3_exec(db, "BEGIN", 0, 0, 0); + nConflict = 0; + rc = sqlite3changeset_apply(db, sz, pBuf, 0, conflictCallback, 0); + if( rc ){ + fprintf(stderr, "sqlite3changeset_apply() returned %d\n", rc); + } + if( nConflict ){ + fprintf(stderr, "%d conflicts - no changes applied\n", nConflict); + sqlite3_exec(db, "ROLLBACK", 0, 0, 0); + }else if( rc ){ + fprintf(stderr, "sqlite3changeset_apply() returns %d " + "- no changes applied\n", rc); + sqlite3_exec(db, "ROLLBACK", 0, 0, 0); + }else{ + sqlite3_exec(db, "COMMIT", 0, 0, 0); + } + sqlite3_close(db); + }else + + /* changeset FILENAME concat FILE2 OUT + ** Add changeset FILE2 onto the end of the changeset in FILENAME + ** and write the result into OUT. + */ + if( strcmp(argv[2],"concat")==0 ){ + int szB; + void *pB; + int szOut; + void *pOutBuf; + FILE *out; + const char *zOut = argv[4]; + if( argc!=5 ) usage(argv[0]); + out = fopen(zOut, "wb"); + if( out==0 ){ + fprintf(stderr, "cannot open \"%s\" for writing\n", zOut); + exit(1); + } + readFile(argv[3], &szB, &pB); + rc = sqlite3changeset_concat(sz, pBuf, szB, pB, &szOut, &pOutBuf); + if( rc!=SQLITE_OK ){ + fprintf(stderr, "sqlite3changeset_concat() returns %d\n", rc); + }else if( szOut>0 && fwrite(pOutBuf, szOut, 1, out)!=1 ){ + fprintf(stderr, "unable to write all %d bytes of output to \"%s\"\n", + szOut, zOut); + } + fclose(out); + sqlite3_free(pOutBuf); + sqlite3_free(pB); + }else + + /* changeset FILENAME dump + ** Show the complete content of the changeset in FILENAME + */ + if( strcmp(argv[2],"dump")==0 ){ + int cnt = 0; + int i; + sqlite3_changeset_iter *pIter; + rc = sqlite3changeset_start(&pIter, sz, pBuf); + if( rc!=SQLITE_OK ){ + fprintf(stderr, "sqlite3changeset_start() returns %d\n", rc); + exit(1); + } + while( sqlite3changeset_next(pIter)==SQLITE_ROW ){ + int op, bIndirect, nCol; + const char *zTab; + unsigned char *abPK; + sqlite3changeset_op(pIter, &zTab, &nCol, &op, &bIndirect); + cnt++; + printf("%d: %s table=[%s] indirect=%d nColumn=%d\n", + cnt, op==SQLITE_INSERT ? "INSERT" : + op==SQLITE_UPDATE ? "UPDATE" : "DELETE", + zTab, bIndirect, nCol); + sqlite3changeset_pk(pIter, &abPK, 0); + for(i=0; i<nCol; i++){ + sqlite3_value *pVal; + pVal = 0; + sqlite3changeset_old(pIter, i, &pVal); + if( pVal ){ + printf(" old[%d]%s = ", i, abPK[i] ? "pk" : " "); + renderValue(pVal); + printf("\n"); + } + pVal = 0; + sqlite3changeset_new(pIter, i, &pVal); + if( pVal ){ + printf(" new[%d]%s = ", i, abPK[i] ? "pk" : " "); + renderValue(pVal); + printf("\n"); + } + } + } + sqlite3changeset_finalize(pIter); + }else + + /* changeset FILENAME invert OUT + ** Invert the changes in FILENAME and writes the result on OUT + */ + if( strcmp(argv[2],"invert")==0 ){ + FILE *out; + int szOut = 0; + void *pOutBuf = 0; + const char *zOut = argv[3]; + if( argc!=4 ) usage(argv[0]); + out = fopen(zOut, "wb"); + if( out==0 ){ + fprintf(stderr, "cannot open \"%s\" for writing\n", zOut); + exit(1); + } + rc = sqlite3changeset_invert(sz, pBuf, &szOut, &pOutBuf); + if( rc!=SQLITE_OK ){ + fprintf(stderr, "sqlite3changeset_invert() returns %d\n", rc); + }else if( szOut>0 && fwrite(pOutBuf, szOut, 1, out)!=1 ){ + fprintf(stderr, "unable to write all %d bytes of output to \"%s\"\n", + szOut, zOut); + } + fclose(out); + sqlite3_free(pOutBuf); + }else + + /* changeset FILE sql + ** Show the content of the changeset as pseudo-SQL + */ + if( strcmp(argv[2],"sql")==0 ){ + int cnt = 0; + char *zPrevTab = 0; + char *zSQLTabName = 0; + sqlite3_changeset_iter *pIter = 0; + rc = sqlite3changeset_start(&pIter, sz, pBuf); + if( rc!=SQLITE_OK ){ + fprintf(stderr, "sqlite3changeset_start() returns %d\n", rc); + exit(1); + } + printf("BEGIN;\n"); + while( sqlite3changeset_next(pIter)==SQLITE_ROW ){ + int op, bIndirect, nCol; + const char *zTab; + sqlite3changeset_op(pIter, &zTab, &nCol, &op, &bIndirect); + cnt++; + if( zPrevTab==0 || strcmp(zPrevTab,zTab)!=0 ){ + sqlite3_free(zPrevTab); + sqlite3_free(zSQLTabName); + zPrevTab = sqlite3_mprintf("%s", zTab); + if( !isalnum(zTab[0]) || sqlite3_strglob("*[^a-zA-Z0-9]*",zTab)==0 ){ + zSQLTabName = sqlite3_mprintf("\"%w\"", zTab); + }else{ + zSQLTabName = sqlite3_mprintf("%s", zTab); + } + printf("/****** Changes for table %s ***************/\n", zSQLTabName); + } + switch( op ){ + case SQLITE_DELETE: { + unsigned char *abPK; + int i; + const char *zSep = " "; + sqlite3changeset_pk(pIter, &abPK, 0); + printf("/* %d */ DELETE FROM %s WHERE", cnt, zSQLTabName); + for(i=0; i<nCol; i++){ + sqlite3_value *pVal; + if( abPK[i]==0 ) continue; + printf("%sc%d=", zSep, i+1); + zSep = " AND "; + sqlite3changeset_old(pIter, i, &pVal); + renderValue(pVal); + } + printf(";\n"); + break; + } + case SQLITE_UPDATE: { + unsigned char *abPK; + int i; + const char *zSep = " "; + sqlite3changeset_pk(pIter, &abPK, 0); + printf("/* %d */ UPDATE %s SET", cnt, zSQLTabName); + for(i=0; i<nCol; i++){ + sqlite3_value *pVal = 0; + sqlite3changeset_new(pIter, i, &pVal); + if( pVal ){ + printf("%sc%d=", zSep, i+1); + zSep = ", "; + renderValue(pVal); + } + } + printf(" WHERE"); + zSep = " "; + for(i=0; i<nCol; i++){ + sqlite3_value *pVal; + if( abPK[i]==0 ) continue; + printf("%sc%d=", zSep, i+1); + zSep = " AND "; + sqlite3changeset_old(pIter, i, &pVal); + renderValue(pVal); + } + printf(";\n"); + break; + } + case SQLITE_INSERT: { + int i; + printf("/* %d */ INSERT INTO %s VALUES", cnt, zSQLTabName); + for(i=0; i<nCol; i++){ + sqlite3_value *pVal; + printf("%c", i==0 ? '(' : ','); + sqlite3changeset_new(pIter, i, &pVal); + renderValue(pVal); + } + printf(");\n"); + break; + } + } + } + printf("COMMIT;\n"); + sqlite3changeset_finalize(pIter); + sqlite3_free(zPrevTab); + sqlite3_free(zSQLTabName); + }else + + /* If nothing else matches, show the usage comment */ + usage(argv[0]); + sqlite3_free(pBuf); + return 0; +} diff --git a/ext/session/session1.test b/ext/session/session1.test new file mode 100644 index 000000000..1c853a97a --- /dev/null +++ b/ext/session/session1.test @@ -0,0 +1,553 @@ +# 2011 March 07 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# This file implements regression tests for SQLite library. +# + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. test] +} +source [file join [file dirname [info script]] session_common.tcl] +source $testdir/tester.tcl +ifcapable !session {finish_test; return} + +set testprefix session1 + +proc do_changeset_test {tn session res} { + set r [list] + foreach x $res {lappend r $x} + uplevel do_test $tn [list [subst -nocommands { + set x [list] + sqlite3session_foreach c [$session changeset] { lappend x [set c] } + set x + }]] [list $r] +} + +proc do_changeset_invert_test {tn session res} { + set r [list] + foreach x $res {lappend r $x} + uplevel do_test $tn [list [subst -nocommands { + set x [list] + set changeset [sqlite3changeset_invert [$session changeset]] + sqlite3session_foreach c [set changeset] { lappend x [set c] } + set x + }]] [list $r] +} + +do_execsql_test 1.0 { + CREATE TABLE t1(x PRIMARY KEY, y); + INSERT INTO t1 VALUES('abc', 'def'); +} + +#------------------------------------------------------------------------- +# Test creating, attaching tables to and deleting session objects. +# +do_test 1.1 { sqlite3session S db main } {S} +do_test 1.2 { S delete } {} +do_test 1.3 { sqlite3session S db main } {S} +do_test 1.4 { S attach t1 } {} +do_test 1.5 { S delete } {} +do_test 1.6 { sqlite3session S db main } {S} +do_test 1.7 { S attach t1 ; S attach t2 ; S attach t3 } {} +do_test 1.8 { S attach t1 ; S attach t2 ; S attach t3 } {} +do_test 1.9 { S delete } {} +do_test 1.10 { + sqlite3session S db main + S attach t1 + execsql { INSERT INTO t1 VALUES('ghi', 'jkl') } +} {} +do_test 1.11 { S delete } {} +do_test 1.12 { + sqlite3session S db main + S attach t1 + execsql { INSERT INTO t1 VALUES('mno', 'pqr') } + execsql { UPDATE t1 SET x = 111 WHERE rowid = 1 } + execsql { DELETE FROM t1 WHERE rowid = 2 } +} {} +do_test 1.13 { + S changeset + S delete +} {} + +#------------------------------------------------------------------------- +# Simple changeset tests. Also test the sqlite3changeset_invert() +# function. +# +do_test 2.1.1 { + execsql { DELETE FROM t1 } + sqlite3session S db main + S attach t1 + execsql { INSERT INTO t1 VALUES(1, 'Sukhothai') } + execsql { INSERT INTO t1 VALUES(2, 'Ayutthaya') } + execsql { INSERT INTO t1 VALUES(3, 'Thonburi') } +} {} +do_changeset_test 2.1.2 S { + {INSERT t1 0 X. {} {i 1 t Sukhothai}} + {INSERT t1 0 X. {} {i 2 t Ayutthaya}} + {INSERT t1 0 X. {} {i 3 t Thonburi}} +} +do_changeset_invert_test 2.1.3 S { + {DELETE t1 0 X. {i 1 t Sukhothai} {}} + {DELETE t1 0 X. {i 2 t Ayutthaya} {}} + {DELETE t1 0 X. {i 3 t Thonburi} {}} +} +do_test 2.1.4 { S delete } {} + +do_test 2.2.1 { + sqlite3session S db main + S attach t1 + execsql { DELETE FROM t1 WHERE 1 } +} {} +do_changeset_test 2.2.2 S { + {DELETE t1 0 X. {i 1 t Sukhothai} {}} + {DELETE t1 0 X. {i 2 t Ayutthaya} {}} + {DELETE t1 0 X. {i 3 t Thonburi} {}} +} +do_changeset_invert_test 2.2.3 S { + {INSERT t1 0 X. {} {i 1 t Sukhothai}} + {INSERT t1 0 X. {} {i 2 t Ayutthaya}} + {INSERT t1 0 X. {} {i 3 t Thonburi}} +} +do_test 2.2.4 { S delete } {} + +do_test 2.3.1 { + execsql { DELETE FROM t1 } + sqlite3session S db main + execsql { INSERT INTO t1 VALUES(1, 'Sukhothai') } + execsql { INSERT INTO t1 VALUES(2, 'Ayutthaya') } + execsql { INSERT INTO t1 VALUES(3, 'Thonburi') } + S attach t1 + execsql { + UPDATE t1 SET x = 10 WHERE x = 1; + UPDATE t1 SET y = 'Surin' WHERE x = 2; + UPDATE t1 SET x = 20, y = 'Thapae' WHERE x = 3; + } +} {} + +do_changeset_test 2.3.2 S { + {INSERT t1 0 X. {} {i 10 t Sukhothai}} + {DELETE t1 0 X. {i 1 t Sukhothai} {}} + {UPDATE t1 0 X. {i 2 t Ayutthaya} {{} {} t Surin}} + {DELETE t1 0 X. {i 3 t Thonburi} {}} + {INSERT t1 0 X. {} {i 20 t Thapae}} +} + +do_changeset_invert_test 2.3.3 S { + {DELETE t1 0 X. {i 10 t Sukhothai} {}} + {INSERT t1 0 X. {} {i 1 t Sukhothai}} + {UPDATE t1 0 X. {i 2 t Surin} {{} {} t Ayutthaya}} + {INSERT t1 0 X. {} {i 3 t Thonburi}} + {DELETE t1 0 X. {i 20 t Thapae} {}} +} +do_test 2.3.4 { S delete } {} + +do_test 2.4.1 { + sqlite3session S db main + S attach t1 + execsql { INSERT INTO t1 VALUES(100, 'Bangkok') } + execsql { DELETE FROM t1 WHERE x = 100 } +} {} +do_changeset_test 2.4.2 S {} +do_changeset_invert_test 2.4.3 S {} +do_test 2.4.4 { S delete } {} + +#------------------------------------------------------------------------- +# Test the application of simple changesets. These tests also test that +# the conflict callback is invoked correctly. For these tests, the +# conflict callback always returns OMIT. +# +db close +forcedelete test.db test.db2 +sqlite3 db test.db +sqlite3 db2 test.db2 + +proc xConflict {args} { + lappend ::xConflict $args + return "" +} + +proc bgerror {args} { set ::background_error $args } + +proc do_conflict_test {tn args} { + set O(-tables) [list] + set O(-sql) [list] + set O(-conflicts) [list] + + array set V $args + foreach key [array names V] { + if {![info exists O($key)]} {error "no such option: $key"} + } + array set O $args + + sqlite3session S db main + foreach t $O(-tables) { S attach $t } + execsql $O(-sql) + set ::xConflict [list] + sqlite3changeset_apply db2 [S changeset] xConflict + + set conflicts [list] + foreach c $O(-conflicts) { + lappend conflicts $c + } + + after 1 {set go 1} + vwait go + + uplevel do_test $tn [list { set ::xConflict }] [list $conflicts] + S delete +} + +proc do_db2_test {testname sql {result {}}} { + uplevel do_test $testname [list "execsql {$sql} db2"] [list [list {*}$result]] +} + +# Test INSERT changesets. +# +do_test 3.1.0 { + execsql { CREATE TABLE t1(a PRIMARY KEY, b NOT NULL) } db2 + execsql { + CREATE TABLE t1(a PRIMARY KEY, b); + INSERT INTO t1 VALUES(1, 'one'); + INSERT INTO t1 VALUES(2, 'two'); + } db +} {} +do_db2_test 3.1.1 "INSERT INTO t1 VALUES(6, 'VI')" +do_conflict_test 3.1.2 -tables t1 -sql { + INSERT INTO t1 VALUES(3, 'three'); + INSERT INTO t1 VALUES(4, 'four'); + INSERT INTO t1 VALUES(5, 'five'); + INSERT INTO t1 VALUES(6, 'six'); + INSERT INTO t1 VALUES(7, 'seven'); + INSERT INTO t1 VALUES(8, NULL); +} -conflicts { + {INSERT t1 CONSTRAINT {i 8 n {}}} + {INSERT t1 CONFLICT {i 6 t six} {i 6 t VI}} +} + +do_db2_test 3.1.3 "SELECT * FROM t1" { + 6 VI 3 three 4 four 5 five 7 seven +} +do_execsql_test 3.1.4 "SELECT * FROM t1" { + 1 one 2 two 3 three 4 four 5 five 6 six 7 seven 8 {} +} + +# Test DELETE changesets. +# +do_execsql_test 3.2.1 { + PRAGMA foreign_keys = on; + CREATE TABLE t2(a PRIMARY KEY, b); + CREATE TABLE t3(c, d REFERENCES t2); + INSERT INTO t2 VALUES(1, 'one'); + INSERT INTO t2 VALUES(2, 'two'); + INSERT INTO t2 VALUES(3, 'three'); + INSERT INTO t2 VALUES(4, 'four'); +} +do_db2_test 3.2.2 { + PRAGMA foreign_keys = on; + CREATE TABLE t2(a PRIMARY KEY, b); + CREATE TABLE t3(c, d REFERENCES t2); + INSERT INTO t2 VALUES(1, 'one'); + INSERT INTO t2 VALUES(2, 'two'); + INSERT INTO t2 VALUES(4, 'five'); + INSERT INTO t3 VALUES('i', 1); +} +do_conflict_test 3.2.3 -tables t2 -sql { + DELETE FROM t2 WHERE a = 1; + DELETE FROM t2 WHERE a = 2; + DELETE FROM t2 WHERE a = 3; + DELETE FROM t2 WHERE a = 4; +} -conflicts { + {DELETE t2 NOTFOUND {i 3 t three}} + {DELETE t2 DATA {i 4 t four} {i 4 t five}} + {FOREIGN_KEY 1} +} +do_execsql_test 3.2.4 "SELECT * FROM t2" {} +do_db2_test 3.2.5 "SELECT * FROM t2" {4 five} + +# Test UPDATE changesets. +# +do_execsql_test 3.3.1 { + CREATE TABLE t4(a, b, c, PRIMARY KEY(b, c)); + INSERT INTO t4 VALUES(1, 2, 3); + INSERT INTO t4 VALUES(4, 5, 6); + INSERT INTO t4 VALUES(7, 8, 9); + INSERT INTO t4 VALUES(10, 11, 12); +} +do_db2_test 3.3.2 { + CREATE TABLE t4(a NOT NULL, b, c, PRIMARY KEY(b, c)); + INSERT INTO t4 VALUES(0, 2, 3); + INSERT INTO t4 VALUES(4, 5, 7); + INSERT INTO t4 VALUES(7, 8, 9); + INSERT INTO t4 VALUES(10, 11, 12); +} +do_conflict_test 3.3.3 -tables t4 -sql { + UPDATE t4 SET a = -1 WHERE b = 2; + UPDATE t4 SET a = -1 WHERE b = 5; + UPDATE t4 SET a = NULL WHERE c = 9; + UPDATE t4 SET a = 'x' WHERE b = 11; +} -conflicts { + {UPDATE t4 CONSTRAINT {i 7 i 8 i 9} {n {} {} {} {} {}}} + {UPDATE t4 DATA {i 1 i 2 i 3} {i -1 {} {} {} {}} {i 0 i 2 i 3}} + {UPDATE t4 NOTFOUND {i 4 i 5 i 6} {i -1 {} {} {} {}}} +} +do_db2_test 3.3.4 { SELECT * FROM t4 } {0 2 3 4 5 7 7 8 9 x 11 12} +do_execsql_test 3.3.5 { SELECT * FROM t4 } {-1 2 3 -1 5 6 {} 8 9 x 11 12} + +#------------------------------------------------------------------------- +# This next block of tests verifies that values returned by the conflict +# handler are intepreted correctly. +# + +proc test_reset {} { + db close + db2 close + forcedelete test.db test.db2 + sqlite3 db test.db + sqlite3 db2 test.db2 +} + +proc xConflict {args} { + lappend ::xConflict $args + return $::conflict_return +} + +foreach {tn conflict_return after} { + 1 OMIT {1 2 value1 4 5 7 10 x x} + 2 REPLACE {1 2 value1 4 5 value2 10 8 9} +} { + test_reset + + do_test 4.$tn.1 { + foreach db {db db2} { + execsql { + CREATE TABLE t1(a, b, c, PRIMARY KEY(a)); + INSERT INTO t1 VALUES(1, 2, 3); + INSERT INTO t1 VALUES(4, 5, 6); + INSERT INTO t1 VALUES(7, 8, 9); + } $db + } + execsql { + REPLACE INTO t1 VALUES(4, 5, 7); + REPLACE INTO t1 VALUES(10, 'x', 'x'); + } db2 + } {} + + do_conflict_test 4.$tn.2 -tables t1 -sql { + UPDATE t1 SET c = 'value1' WHERE a = 1; -- no conflict + UPDATE t1 SET c = 'value2' WHERE a = 4; -- DATA conflict + UPDATE t1 SET a = 10 WHERE a = 7; -- CONFLICT conflict + } -conflicts { + {INSERT t1 CONFLICT {i 10 i 8 i 9} {i 10 t x t x}} + {UPDATE t1 DATA {i 4 {} {} i 6} {{} {} {} {} t value2} {i 4 i 5 i 7}} + } + + do_db2_test 4.$tn.3 "SELECT * FROM t1 ORDER BY a" $after +} + +foreach {tn conflict_return} { + 1 OMIT + 2 REPLACE +} { + test_reset + + do_test 5.$tn.1 { + # Create an identical schema in both databases. + set schema { + CREATE TABLE "'foolish name'"(x, y, z, PRIMARY KEY(x, y)); + } + execsql $schema db + execsql $schema db2 + + # Add some rows to [db2]. These rows will cause conflicts later + # on when the changeset from [db] is applied to it. + execsql { + INSERT INTO "'foolish name'" VALUES('one', 'one', 'ii'); + INSERT INTO "'foolish name'" VALUES('one', 'two', 'i'); + INSERT INTO "'foolish name'" VALUES('two', 'two', 'ii'); + } db2 + + } {} + + do_conflict_test 5.$tn.2 -tables {{'foolish name'}} -sql { + INSERT INTO "'foolish name'" VALUES('one', 'two', 2); + } -conflicts { + {INSERT {'foolish name'} CONFLICT {t one t two i 2} {t one t two t i}} + } + + set res(REPLACE) {one one ii one two 2 two two ii} + set res(OMIT) {one one ii one two i two two ii} + do_db2_test 5.$tn.3 { + SELECT * FROM "'foolish name'" ORDER BY x, y + } $res($conflict_return) + + + do_test 5.$tn.1 { + set schema { + CREATE TABLE d1("z""z" PRIMARY KEY, y); + INSERT INTO d1 VALUES(1, 'one'); + INSERT INTO d1 VALUES(2, 'two'); + } + execsql $schema db + execsql $schema db2 + + execsql { + UPDATE d1 SET y = 'TWO' WHERE "z""z" = 2; + } db2 + + } {} + + do_conflict_test 5.$tn.2 -tables d1 -sql { + DELETE FROM d1 WHERE "z""z" = 2; + } -conflicts { + {DELETE d1 DATA {i 2 t two} {i 2 t TWO}} + } + + set res(REPLACE) {1 one} + set res(OMIT) {1 one 2 TWO} + do_db2_test 5.$tn.3 "SELECT * FROM d1" $res($conflict_return) +} + +#------------------------------------------------------------------------- +# Test that two tables can be monitored by a single session object. +# +test_reset +set schema { + CREATE TABLE t1(a COLLATE nocase PRIMARY KEY, b); + CREATE TABLE t2(a, b PRIMARY KEY); +} +do_test 6.0 { + execsql $schema db + execsql $schema db2 + execsql { + INSERT INTO t1 VALUES('a', 'b'); + INSERT INTO t2 VALUES('a', 'b'); + } db2 +} {} + +set conflict_return "" +do_conflict_test 6.1 -tables {t1 t2} -sql { + INSERT INTO t1 VALUES('1', '2'); + INSERT INTO t1 VALUES('A', 'B'); + INSERT INTO t2 VALUES('A', 'B'); +} -conflicts { + {INSERT t1 CONFLICT {t A t B} {t a t b}} +} + +do_db2_test 6.2 "SELECT * FROM t1" {a b 1 2} +do_db2_test 6.3 "SELECT * FROM t2" {a b A B} + +#------------------------------------------------------------------------- +# Test that session objects are not confused by changes to table in +# other databases. +# +catch { db2 close } +drop_all_tables +forcedelete test.db2 +do_iterator_test 7.1 * { + ATTACH 'test.db2' AS aux; + CREATE TABLE main.t1(x PRIMARY KEY, y); + CREATE TABLE aux.t1(x PRIMARY KEY, y); + + INSERT INTO main.t1 VALUES('one', 1); + INSERT INTO main.t1 VALUES('two', 2); + INSERT INTO aux.t1 VALUES('three', 3); + INSERT INTO aux.t1 VALUES('four', 4); +} { + {INSERT t1 0 X. {} {t two i 2}} + {INSERT t1 0 X. {} {t one i 1}} +} + +#------------------------------------------------------------------------- +# Test the sqlite3session_isempty() function. +# +do_test 8.1 { + execsql { + CREATE TABLE t5(x PRIMARY KEY, y); + CREATE TABLE t6(x PRIMARY KEY, y); + INSERT INTO t5 VALUES('a', 'b'); + INSERT INTO t6 VALUES('a', 'b'); + } + sqlite3session S db main + S attach * + + S isempty +} {1} +do_test 8.2 { + execsql { DELETE FROM t5 } + S isempty +} {0} +do_test 8.3 { + S delete + sqlite3session S db main + S attach t5 + execsql { DELETE FROM t5 } + S isempty +} {1} +do_test 8.4 { S delete } {} + +do_test 8.5 { + sqlite3session S db main + S attach t5 + S attach t6 + execsql { INSERT INTO t5 VALUES(1, 2) } + S isempty +} {0} + +do_test 8.6 { + S delete + sqlite3session S db main + S attach t5 + S attach t6 + execsql { INSERT INTO t6 VALUES(1, 2) } + S isempty +} {0} +do_test 8.7 { S delete } {} + +#------------------------------------------------------------------------- +# +do_execsql_test 9.1 { + CREATE TABLE t7(a, b, c, d, e PRIMARY KEY, f, g); + INSERT INTO t7 VALUES(1, 1, 1, 1, 1, 1, 1); +} +do_test 9.2 { + sqlite3session S db main + S attach * + execsql { UPDATE t7 SET b=2, d=2 } +} {} +do_changeset_test 9.2 S {{UPDATE t7 0 ....X.. {{} {} i 1 {} {} i 1 i 1 {} {} {} {}} {{} {} i 2 {} {} i 2 {} {} {} {} {} {}}}} +S delete +catch { db2 close } + +#------------------------------------------------------------------------- +# Test a really long table name. +# +reset_db +set tblname [string repeat tblname123 100] +do_test 10.1.1 { + execsql " + CREATE TABLE $tblname (a PRIMARY KEY, b); + INSERT INTO $tblname VALUES('xyz', 'def'); + " + sqlite3session S db main + S attach $tblname + execsql " + INSERT INTO $tblname VALUES('uvw', 'abc'); + DELETE FROM $tblname WHERE a = 'xyz'; + " +} {} +breakpoint +do_changeset_test 10.1.2 S " + {INSERT $tblname 0 X. {} {t uvw t abc}} + {DELETE $tblname 0 X. {t xyz t def} {}} +" +do_test 10.1.4 { S delete } {} + + +finish_test diff --git a/ext/session/session2.test b/ext/session/session2.test new file mode 100644 index 000000000..04be5f091 --- /dev/null +++ b/ext/session/session2.test @@ -0,0 +1,581 @@ +# 2011 Mar 16 +# +# 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. +# +#*********************************************************************** +# +# The focus of this file is testing the session module. +# + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. test] +} +source [file join [file dirname [info script]] session_common.tcl] +source $testdir/tester.tcl +ifcapable !session {finish_test; return} + +set testprefix session2 + +proc test_reset {} { + catch { db close } + catch { db2 close } + forcedelete test.db test.db2 + sqlite3 db test.db + sqlite3 db2 test.db2 +} + +########################################################################## +# End of proc definitions. Start of tests. +########################################################################## + +test_reset +do_execsql_test 1.0 { + CREATE TABLE t1(a PRIMARY KEY, b); + INSERT INTO t1 VALUES('i', 'one'); +} +do_iterator_test 1.1 t1 { + DELETE FROM t1 WHERE a = 'i'; + INSERT INTO t1 VALUES('ii', 'two'); +} { + {DELETE t1 0 X. {t i t one} {}} + {INSERT t1 0 X. {} {t ii t two}} +} + +do_iterator_test 1.2 t1 { + INSERT INTO t1 VALUES(1.5, 99.9) +} { + {INSERT t1 0 X. {} {f 1.5 f 99.9}} +} + +do_iterator_test 1.3 t1 { + UPDATE t1 SET b = 100.1 WHERE a = 1.5; + UPDATE t1 SET b = 99.9 WHERE a = 1.5; +} { } + +do_iterator_test 1.4 t1 { + UPDATE t1 SET b = 100.1 WHERE a = 1.5; +} { + {UPDATE t1 0 X. {f 1.5 f 99.9} {{} {} f 100.1}} +} + + +# Execute each of the following blocks of SQL on database [db1]. Collect +# changes using a session object. Apply the resulting changeset to +# database [db2]. Then check that the contents of the two databases are +# identical. +# + +set set_of_tests { + 1 { INSERT INTO %T1% VALUES(1, 2) } + + 2 { + INSERT INTO %T2% VALUES(1, NULL); + INSERT INTO %T2% VALUES(2, NULL); + INSERT INTO %T2% VALUES(3, NULL); + DELETE FROM %T2% WHERE a = 2; + INSERT INTO %T2% VALUES(4, NULL); + UPDATE %T2% SET b=0 WHERE b=1; + } + + 3 { INSERT INTO %T3% SELECT *, NULL FROM %T2% } + + 4 { + INSERT INTO %T3% SELECT a||a, b||b, NULL FROM %T3%; + DELETE FROM %T3% WHERE rowid%2; + } + + 5 { UPDATE %T3% SET c = a||b } + + 6 { UPDATE %T1% SET a = 32 } + + 7 { + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + DELETE FROM %T1% WHERE (rowid%3)==0; + } + + 8 { + BEGIN; + INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%; + ROLLBACK; + } + 9 { + BEGIN; + UPDATE %T1% SET b = 'xxx'; + ROLLBACK; + } + 10 { + BEGIN; + DELETE FROM %T1% WHERE 1; + ROLLBACK; + } + 11 { + INSERT INTO %T1% VALUES(randomblob(21000), randomblob(0)); + INSERT INTO %T1% VALUES(1.5, 1.5); + INSERT INTO %T1% VALUES(4.56, -99.999999999999999999999); + } + 12 { + INSERT INTO %T2% VALUES(NULL, NULL); + } + + 13 { + DELETE FROM %T1% WHERE 1; + + -- Insert many rows with real primary keys. Enough to force the session + -- objects hash table to resize. + INSERT INTO %T1% VALUES(0.1, 0.1); + INSERT INTO %T1% SELECT a+0.1, b+0.1 FROM %T1%; + INSERT INTO %T1% SELECT a+0.2, b+0.2 FROM %T1%; + INSERT INTO %T1% SELECT a+0.4, b+0.4 FROM %T1%; + INSERT INTO %T1% SELECT a+0.8, b+0.8 FROM %T1%; + INSERT INTO %T1% SELECT a+1.6, b+1.6 FROM %T1%; + INSERT INTO %T1% SELECT a+3.2, b+3.2 FROM %T1%; + INSERT INTO %T1% SELECT a+6.4, b+6.4 FROM %T1%; + INSERT INTO %T1% SELECT a+12.8, b+12.8 FROM %T1%; + INSERT INTO %T1% SELECT a+25.6, b+25.6 FROM %T1%; + INSERT INTO %T1% SELECT a+51.2, b+51.2 FROM %T1%; + INSERT INTO %T1% SELECT a+102.4, b+102.4 FROM %T1%; + INSERT INTO %T1% SELECT a+204.8, b+204.8 FROM %T1%; + } + + 14 { + DELETE FROM %T1% WHERE 1; + } + + 15 { + INSERT INTO %T1% VALUES(1, 1); + INSERT INTO %T1% SELECT a+2, b+2 FROM %T1%; + INSERT INTO %T1% SELECT a+4, b+4 FROM %T1%; + INSERT INTO %T1% SELECT a+8, b+8 FROM %T1%; + INSERT INTO %T1% SELECT a+256, b+256 FROM %T1%; + } + + 16 { + INSERT INTO %T4% VALUES('abc', 'def'); + INSERT INTO %T4% VALUES('def', 'abc'); + } + 17 { UPDATE %T4% SET b = 1 } + 18 { DELETE FROM %T4% WHERE 1 } +} + +test_reset +do_common_sql { + CREATE TABLE t1(a PRIMARY KEY, b); + CREATE TABLE t2(a, b INTEGER PRIMARY KEY); + CREATE TABLE t3(a, b, c, PRIMARY KEY(a, b)); + CREATE TABLE t4(a, b, PRIMARY KEY(b, a)); +} + +foreach {tn sql} [string map {%T1% t1 %T2% t2 %T3% t3 %T4% t4} $set_of_tests] { + do_then_apply_sql $sql + do_test 2.$tn { compare_db db db2 } {} +} + +# The following block of tests is similar to the last, except that the +# session object is recording changes made to an attached database. The +# main database contains a table of the same name as the table being +# modified within the attached db. +# +test_reset +forcedelete test.db3 +sqlite3 db3 test.db3 +do_test 3.0 { + execsql { + ATTACH 'test.db3' AS 'aux'; + CREATE TABLE t1(a, b PRIMARY KEY); + CREATE TABLE t2(x, y, z); + CREATE TABLE t3(a); + + CREATE TABLE aux.t1(a PRIMARY KEY, b); + CREATE TABLE aux.t2(a, b INTEGER PRIMARY KEY); + CREATE TABLE aux.t3(a, b, c, PRIMARY KEY(a, b)); + CREATE TABLE aux.t4(a, b, PRIMARY KEY(b, a)); + } + execsql { + CREATE TABLE t1(a PRIMARY KEY, b); + CREATE TABLE t2(a, b INTEGER PRIMARY KEY); + CREATE TABLE t3(a, b, c, PRIMARY KEY(a, b)); + CREATE TABLE t4(a, b, PRIMARY KEY(b, a)); + } db2 +} {} + +proc xTrace {args} { puts $args } + +foreach {tn sql} [ + string map {%T1% aux.t1 %T2% aux.t2 %T3% aux.t3 %T4% aux.t4} $set_of_tests +] { + do_then_apply_sql $sql aux + do_test 3.$tn { compare_db db2 db3 } {} +} +catch {db3 close} + + +#------------------------------------------------------------------------- +# The following tests verify that NULL values in primary key columns are +# handled correctly by the session module. +# +test_reset +do_execsql_test 4.0 { + CREATE TABLE t1(a PRIMARY KEY); + CREATE TABLE t2(a, b, c, PRIMARY KEY(c, b)); + CREATE TABLE t3(a, b INTEGER PRIMARY KEY); +} + +foreach {tn sql changeset} { + 1 { + INSERT INTO t1 VALUES(123); + INSERT INTO t1 VALUES(NULL); + INSERT INTO t1 VALUES(456); + } { + {INSERT t1 0 X {} {i 456}} + {INSERT t1 0 X {} {i 123}} + } + + 2 { + UPDATE t1 SET a = NULL; + } { + {DELETE t1 0 X {i 456} {}} + {DELETE t1 0 X {i 123} {}} + } + + 3 { DELETE FROM t1 } { } + + 4 { + INSERT INTO t3 VALUES(NULL, NULL) + } { + {INSERT t3 0 .X {} {n {} i 1}} + } + + 5 { INSERT INTO t2 VALUES(1, 2, NULL) } { } + 6 { INSERT INTO t2 VALUES(1, NULL, 3) } { } + 7 { INSERT INTO t2 VALUES(1, NULL, NULL) } { } + 8 { INSERT INTO t2 VALUES(1, 2, 3) } { {INSERT t2 0 .XX {} {i 1 i 2 i 3}} } + 9 { DELETE FROM t2 WHERE 1 } { {DELETE t2 0 .XX {i 1 i 2 i 3} {}} } + +} { + do_iterator_test 4.$tn {t1 t2 t3} $sql $changeset +} + + +#------------------------------------------------------------------------- +# Test that if NULL is passed to sqlite3session_attach(), all database +# tables are attached to the session object. +# +test_reset +do_execsql_test 5.0 { + CREATE TABLE t1(a PRIMARY KEY); + CREATE TABLE t2(x, y PRIMARY KEY); +} + +foreach {tn sql changeset} { + 1 { INSERT INTO t1 VALUES(35) } { {INSERT t1 0 X {} {i 35}} } + 2 { INSERT INTO t2 VALUES(36, 37) } { {INSERT t2 0 .X {} {i 36 i 37}} } + 3 { + DELETE FROM t1 WHERE 1; + UPDATE t2 SET x = 34; + } { + {UPDATE t2 0 .X {i 36 i 37} {i 34 {} {}}} + {DELETE t1 0 X {i 35} {}} + } +} { + do_iterator_test 5.$tn * $sql $changeset +} + +#------------------------------------------------------------------------- +# The next block of tests verify that the "indirect" flag is set +# correctly within changesets. The indirect flag is set for a change +# if either of the following are true: +# +# * The sqlite3session_indirect() API has been used to set the session +# indirect flag to true, or +# * The change was made by a trigger. +# +# If the same row is updated more than once during a session, then the +# change is considered indirect only if all changes meet the criteria +# above. +# +test_reset +db function indirect [list S indirect] + +do_execsql_test 6.0 { + CREATE TABLE t1(a PRIMARY KEY, b, c); + + CREATE TABLE t2(x PRIMARY KEY, y); + CREATE TRIGGER AFTER INSERT ON t2 WHEN new.x%2 BEGIN + INSERT INTO t2 VALUES(new.x+1, NULL); + END; +} + +do_iterator_test 6.1.1 * { + INSERT INTO t1 VALUES(1, 'one', 'i'); + SELECT indirect(1); + INSERT INTO t1 VALUES(2, 'two', 'ii'); + SELECT indirect(0); + INSERT INTO t1 VALUES(3, 'three', 'iii'); +} { + {INSERT t1 0 X.. {} {i 1 t one t i}} + {INSERT t1 1 X.. {} {i 2 t two t ii}} + {INSERT t1 0 X.. {} {i 3 t three t iii}} +} + +do_iterator_test 6.1.2 * { + SELECT indirect(1); + UPDATE t1 SET c = 'I' WHERE a = 1; + SELECT indirect(0); +} { + {UPDATE t1 1 X.. {i 1 {} {} t i} {{} {} {} {} t I}} +} +do_iterator_test 6.1.3 * { + SELECT indirect(1); + UPDATE t1 SET c = '.' WHERE a = 1; + SELECT indirect(0); + UPDATE t1 SET c = 'o' WHERE a = 1; +} { + {UPDATE t1 0 X.. {i 1 {} {} t I} {{} {} {} {} t o}} +} +do_iterator_test 6.1.4 * { + SELECT indirect(0); + UPDATE t1 SET c = 'x' WHERE a = 1; + SELECT indirect(1); + UPDATE t1 SET c = 'i' WHERE a = 1; +} { + {UPDATE t1 0 X.. {i 1 {} {} t o} {{} {} {} {} t i}} +} +do_iterator_test 6.1.4 * { + SELECT indirect(1); + UPDATE t1 SET c = 'y' WHERE a = 1; + SELECT indirect(1); + UPDATE t1 SET c = 'I' WHERE a = 1; +} { + {UPDATE t1 1 X.. {i 1 {} {} t i} {{} {} {} {} t I}} +} + +do_iterator_test 6.1.5 * { + INSERT INTO t2 VALUES(1, 'x'); +} { + {INSERT t2 0 X. {} {i 1 t x}} + {INSERT t2 1 X. {} {i 2 n {}}} +} + +do_iterator_test 6.1.6 * { + SELECT indirect(1); + INSERT INTO t2 VALUES(3, 'x'); + SELECT indirect(0); + UPDATE t2 SET y = 'y' WHERE x>2; +} { + {INSERT t2 0 X. {} {i 3 t y}} + {INSERT t2 0 X. {} {i 4 t y}} +} + +do_iterator_test 6.1.7 * { + SELECT indirect(1); + DELETE FROM t2 WHERE x = 4; + SELECT indirect(0); + INSERT INTO t2 VALUES(4, 'new'); +} { + {UPDATE t2 0 X. {i 4 t y} {{} {} t new}} +} + +do_iterator_test 6.1.8 * { + CREATE TABLE t3(a, b PRIMARY KEY); + CREATE TABLE t4(a, b PRIMARY KEY); + CREATE TRIGGER t4t AFTER UPDATE ON t4 BEGIN + UPDATE t3 SET a = new.a WHERE b = new.b; + END; + + SELECT indirect(1); + INSERT INTO t3 VALUES('one', 1); + INSERT INTO t4 VALUES('one', 1); + SELECT indirect(0); + UPDATE t4 SET a = 'two' WHERE b = 1; +} { + {INSERT t4 0 .X {} {t two i 1}} + {INSERT t3 1 .X {} {t two i 1}} +} + +sqlite3session S db main +do_execsql_test 6.2.1 { + SELECT indirect(0); + SELECT indirect(-1); + SELECT indirect(45); + SELECT indirect(-100); +} {0 0 1 1} +S delete + +#------------------------------------------------------------------------- +# Test that if a conflict-handler that has been passed either NOTFOUND or +# CONSTRAINT returns REPLACE - the sqlite3changeset_apply() call returns +# MISUSE and rolls back any changes made so far. +# +# 7.1.*: NOTFOUND conflict-callback. +# 7.2.*: CONSTRAINT conflict-callback. +# +proc xConflict {args} {return REPLACE} +test_reset + +do_execsql_test 7.1.1 { + CREATE TABLE t1(a PRIMARY KEY, b); + INSERT INTO t1 VALUES(1, 'one'); + INSERT INTO t1 VALUES(2, 'two'); +} +do_test 7.1.2 { + execsql { + CREATE TABLE t1(a PRIMARY KEY, b NOT NULL); + INSERT INTO t1 VALUES(1, 'one'); + } db2 +} {} +do_test 7.1.3 { + set changeset [changeset_from_sql { + UPDATE t1 SET b = 'five' WHERE a = 1; + UPDATE t1 SET b = 'six' WHERE a = 2; + }] + set x [list] + sqlite3session_foreach c $changeset { lappend x $c } + set x +} [list \ + {UPDATE t1 0 X. {i 1 t one} {{} {} t five}} \ + {UPDATE t1 0 X. {i 2 t two} {{} {} t six}} \ +] +do_test 7.1.4 { + list [catch {sqlite3changeset_apply db2 $changeset xConflict} msg] $msg +} {1 SQLITE_MISUSE} +do_test 7.1.5 { execsql { SELECT * FROM t1 } db2 } {1 one} + +do_test 7.2.1 { + set changeset [changeset_from_sql { UPDATE t1 SET b = NULL WHERE a = 1 }] + + set x [list] + sqlite3session_foreach c $changeset { lappend x $c } + set x +} [list \ + {UPDATE t1 0 X. {i 1 t five} {{} {} n {}}} \ +] +do_test 7.2.2 { + list [catch {sqlite3changeset_apply db2 $changeset xConflict} msg] $msg +} {1 SQLITE_MISUSE} +do_test 7.2.3 { execsql { SELECT * FROM t1 } db2 } {1 one} + +#------------------------------------------------------------------------- +# Test that if a conflict-handler returns ABORT, application of the +# changeset is rolled back and the sqlite3changeset_apply() method returns +# SQLITE_ABORT. +# +# Also test that the same thing happens if a conflict handler returns an +# unrecognized integer value. Except, in this case SQLITE_MISUSE is returned +# instead of SQLITE_ABORT. +# +foreach {tn conflict_return apply_return} { + 1 ABORT SQLITE_ABORT + 2 567 SQLITE_MISUSE +} { + test_reset + proc xConflict {args} [list return $conflict_return] + + do_test 8.$tn.0 { + do_common_sql { + CREATE TABLE t1(x, y, PRIMARY KEY(x, y)); + INSERT INTO t1 VALUES('x', 'y'); + } + execsql { INSERT INTO t1 VALUES('w', 'w') } + + set changeset [changeset_from_sql { DELETE FROM t1 WHERE 1 }] + + set x [list] + sqlite3session_foreach c $changeset { lappend x $c } + set x + } [list \ + {DELETE t1 0 XX {t w t w} {}} \ + {DELETE t1 0 XX {t x t y} {}} \ + ] + + do_test 8.$tn.1 { + list [catch {sqlite3changeset_apply db2 $changeset xConflict} msg] $msg + } [list 1 $apply_return] + + do_test 8.$tn.2 { + execsql {SELECT * FROM t1} db2 + } {x y} +} + + +#------------------------------------------------------------------------- +# Try to cause an infinite loop as follows: +# +# 1. Have a changeset insert a row that causes a CONFLICT callback, +# 2. Have the conflict handler return REPLACE, +# 3. After the session module deletes the conflicting row, have a trigger +# re-insert it. +# 4. Goto step 1... +# +# This doesn't work, as the second invocation of the conflict handler is a +# CONSTRAINT, not a CONFLICT. There is at most one CONFLICT callback for +# each change in the changeset. +# +test_reset +proc xConflict {type args} { + if {$type == "CONFLICT"} { return REPLACE } + return OMIT +} +do_test 9.1 { + execsql { + CREATE TABLE t1(a PRIMARY KEY, b); + } + execsql { + CREATE TABLE t1(a PRIMARY KEY, b); + INSERT INTO t1 VALUES('x', 2); + CREATE TRIGGER tr1 AFTER DELETE ON t1 BEGIN + INSERT INTO t1 VALUES(old.a, old.b); + END; + } db2 +} {} +do_test 9.2 { + set changeset [changeset_from_sql { INSERT INTO t1 VALUES('x', 1) }] + sqlite3changeset_apply db2 $changeset xConflict +} {} +do_test 9.3 { + execsql { SELECT * FROM t1 } db2 +} {x 2} + +#------------------------------------------------------------------------- +# +test_reset +db function enable [list S enable] + +do_common_sql { + CREATE TABLE t1(a PRIMARY KEY, b); + INSERT INTO t1 VALUES('x', 'X'); +} + +do_iterator_test 10.1 t1 { + INSERT INTO t1 VALUES('y', 'Y'); + SELECT enable(0); + INSERT INTO t1 VALUES('z', 'Z'); + SELECT enable(1); +} { + {INSERT t1 0 X. {} {t y t Y}} +} + +sqlite3session S db main +do_execsql_test 10.2 { + SELECT enable(0); + SELECT enable(-1); + SELECT enable(1); + SELECT enable(-1); +} {0 0 1 1} +S delete + +finish_test diff --git a/ext/session/session3.test b/ext/session/session3.test new file mode 100644 index 000000000..e15407c2e --- /dev/null +++ b/ext/session/session3.test @@ -0,0 +1,211 @@ +# 2011 March 24 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# This file implements regression tests for the session module. More +# specifically, it focuses on testing the session modules response to +# database schema modifications and mismatches. +# + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. test] +} +source [file join [file dirname [info script]] session_common.tcl] +source $testdir/tester.tcl +ifcapable !session {finish_test; return} + +set testprefix session3 + +#------------------------------------------------------------------------- +# These tests - session3-1.* - verify that the session module behaves +# correctly when confronted with a schema mismatch when applying a +# changeset (in function sqlite3changeset_apply()). +# +# session3-1.1.*: Table does not exist in target db. +# session3-1.2.*: Table has wrong number of columns in target db. +# session3-1.3.*: Table has wrong PK columns in target db. +# +db close +sqlite3_shutdown +test_sqlite3_log log +sqlite3 db test.db + +proc log {code msg} { lappend ::log $code $msg } + +forcedelete test.db2 +sqlite3 db2 test.db2 + +do_execsql_test 1.0 { + CREATE TABLE t1(a PRIMARY KEY, b); +} +do_test 1.1 { + set ::log {} + do_then_apply_sql { + INSERT INTO t1 VALUES(1, 2); + INSERT INTO t1 VALUES(3, 4); + } + set ::log +} {SQLITE_SCHEMA {sqlite3changeset_apply(): no such table: t1}} + +do_test 1.2.0 { + execsql { CREATE TABLE t1(a PRIMARY KEY, b, c) } db2 +} {} +do_test 1.2.1 { + set ::log {} + do_then_apply_sql { + INSERT INTO t1 VALUES(5, 6); + INSERT INTO t1 VALUES(7, 8); + } + set ::log +} {SQLITE_SCHEMA {sqlite3changeset_apply(): table t1 has 3 columns, expected 2}} + +do_test 1.3.0 { + execsql { + DROP TABLE t1; + CREATE TABLE t1(a, b PRIMARY KEY); + } db2 +} {} +do_test 1.3.1 { + set ::log {} + do_then_apply_sql { + INSERT INTO t1 VALUES(9, 10); + INSERT INTO t1 VALUES(11, 12); + } + set ::log +} {SQLITE_SCHEMA {sqlite3changeset_apply(): primary key mismatch for table t1}} + +#------------------------------------------------------------------------- +# These tests - session3-2.* - verify that the session module behaves +# correctly when the schema of an attached table is modified during the +# session. +# +# session3-2.1.*: Table is dropped midway through the session. +# session3-2.2.*: Table is dropped and recreated with a different # cols. +# session3-2.3.*: Table is dropped and recreated with a different PK. +# +# In all of these scenarios, the call to sqlite3session_changeset() will +# return SQLITE_SCHEMA. Also: +# +# session3-2.4.*: Table is dropped and recreated with an identical schema. +# In this case sqlite3session_changeset() returns SQLITE_OK. +# + +do_test 2.1 { + execsql { CREATE TABLE t2(a, b PRIMARY KEY) } + sqlite3session S db main + S attach t2 + execsql { + INSERT INTO t2 VALUES(1, 2); + DROP TABLE t2; + } + list [catch { S changeset } msg] $msg +} {1 SQLITE_SCHEMA} + +do_test 2.2.1 { + S delete + sqlite3session S db main + execsql { CREATE TABLE t2(a, b PRIMARY KEY, c) } + S attach t2 + execsql { + INSERT INTO t2 VALUES(1, 2, 3); + DROP TABLE t2; + CREATE TABLE t2(a, b PRIMARY KEY); + } + list [catch { S changeset } msg] $msg +} {1 SQLITE_SCHEMA} +do_test 2.2.2 { + S delete + sqlite3session S db main + execsql { + DROP TABLE t2; + CREATE TABLE t2(a, b PRIMARY KEY, c); + } + S attach t2 + execsql { + INSERT INTO t2 VALUES(1, 2, 3); + DROP TABLE t2; + CREATE TABLE t2(a, b PRIMARY KEY, c, d); + } + list [catch { S changeset } msg] $msg +} {1 SQLITE_SCHEMA} +do_test 2.2.3 { + S delete + sqlite3session S db main + execsql { + DROP TABLE t2; + CREATE TABLE t2(a, b PRIMARY KEY, c); + } + S attach t2 + execsql { + INSERT INTO t2 VALUES(1, 2, 3); + DROP TABLE t2; + CREATE TABLE t2(a, b PRIMARY KEY); + INSERT INTO t2 VALUES(4, 5); + } + list [catch { S changeset } msg] $msg +} {1 SQLITE_SCHEMA} +do_test 2.2.4 { + S delete + sqlite3session S db main + execsql { + DROP TABLE t2; + CREATE TABLE t2(a, b PRIMARY KEY, c); + } + S attach t2 + execsql { + INSERT INTO t2 VALUES(1, 2, 3); + DROP TABLE t2; + CREATE TABLE t2(a, b PRIMARY KEY, c, d); + INSERT INTO t2 VALUES(4, 5, 6, 7); + } + list [catch { S changeset } msg] $msg +} {1 SQLITE_SCHEMA} + +do_test 2.3 { + S delete + sqlite3session S db main + execsql { + DROP TABLE t2; + CREATE TABLE t2(a, b PRIMARY KEY); + } + S attach t2 + execsql { + INSERT INTO t2 VALUES(1, 2); + DROP TABLE t2; + CREATE TABLE t2(a PRIMARY KEY, b); + } + list [catch { S changeset } msg] $msg +} {1 SQLITE_SCHEMA} + +do_test 2.4 { + S delete + sqlite3session S db main + execsql { + DROP TABLE t2; + CREATE TABLE t2(a, b PRIMARY KEY); + } + S attach t2 + execsql { + INSERT INTO t2 VALUES(1, 2); + DROP TABLE t2; + CREATE TABLE t2(a, b PRIMARY KEY); + } + list [catch { S changeset } msg] $msg +} {0 {}} + +S delete + + +catch { db close } +catch { db2 close } +sqlite3_shutdown +test_sqlite3_log +sqlite3_initialize + +finish_test diff --git a/ext/session/session4.test b/ext/session/session4.test new file mode 100644 index 000000000..8e179baf6 --- /dev/null +++ b/ext/session/session4.test @@ -0,0 +1,68 @@ +# 2011 March 25 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# This file implements regression tests for the session module. +# + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. test] +} +source [file join [file dirname [info script]] session_common.tcl] +source $testdir/tester.tcl +ifcapable !session {finish_test; return} + +set testprefix session4 + +do_test 1.0 { + execsql { + CREATE TABLE x(a, b, c, d, e, PRIMARY KEY(c, e)); + INSERT INTO x VALUES(65.21, X'28B0', 16.35, NULL, 'doers'); + INSERT INTO x VALUES(NULL, 78.49, 2, X'60', -66); + INSERT INTO x VALUES('cathedral', NULL, 35, NULL, X'B220937E80A2D8'); + INSERT INTO x VALUES(NULL, 'masking', -91.37, NULL, X'596D'); + INSERT INTO x VALUES(19, 'domains', 'espouse', -94, 'throw'); + } + + sqlite3session S db main + set changeset [changeset_from_sql { + DELETE FROM x WHERE e = -66; + UPDATE x SET a = 'parameterizable', b = 31.8 WHERE c = 35; + INSERT INTO x VALUES(-75.61, -17, 16.85, NULL, X'D73DB02678'); + }] + set {} {} +} {} + + +# This currently causes crashes. sqlite3changeset_invert() does not handle +# corrupt changesets well. +if 0 { + do_test 1.1 { + for {set i 0} {$i < [string length $changeset]} {incr i} { + set before [string range $changeset 0 [expr $i-1]] + set after [string range $changeset [expr $i+1] end] + for {set j 10} {$j < 260} {incr j} { + set x [binary format "a*ca*" $before $j $after] + catch { sqlite3changeset_invert $x } + } + } + } {} +} + +do_test 1.2 { + set x [binary format "ca*" 0 [string range $changeset 1 end]] + list [catch { sqlite3changeset_invert $x } msg] $msg +} {1 SQLITE_CORRUPT} + +do_test 1.3 { + set x [binary format "ca*" 0 [string range $changeset 1 end]] + list [catch { sqlite3changeset_apply db $x xConflict } msg] $msg +} {1 SQLITE_CORRUPT} + +finish_test diff --git a/ext/session/session5.test b/ext/session/session5.test new file mode 100644 index 000000000..9b8f9ffe1 --- /dev/null +++ b/ext/session/session5.test @@ -0,0 +1,408 @@ +# 2011 April 13 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# This file implements regression tests for the session module. +# Specifically, for the sqlite3changeset_concat() command. +# + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. test] +} +source [file join [file dirname [info script]] session_common.tcl] +source $testdir/tester.tcl +ifcapable !session {finish_test; return} + +set testprefix session5 + +# Organization of tests: +# +# session5-1.*: Simple tests to check the concat() function produces +# correct results. +# +# session5-2.*: More complicated tests. +# +# session5-3.*: Schema mismatch errors. +# +# session5-4.*: Test the concat cases that indicate that the database +# was modified in between recording of the two changesets +# being concatenated (i.e. two changesets that INSERT rows +# with the same PK values). +# + +proc do_concat_test {tn args} { + + set subtest 0 + foreach sql $args { + incr subtest + sqlite3session S db main ; S attach * + execsql $sql + + set c [S changeset] + if {[info commands s_prev] != ""} { + set c_concat [sqlite3changeset_concat $c_prev $c] + set c_two [s_prev changeset] + s_prev delete + + set h_concat [changeset_to_list $c_concat] + set h_two [changeset_to_list $c_two] + + do_test $tn.$subtest [list set {} $h_concat] $h_two + } + set c_prev $c + rename S s_prev + } + + catch { s_prev delete } +} + +#------------------------------------------------------------------------- +# Test cases session5-1.* - simple tests. +# +do_execsql_test 1.0 { + CREATE TABLE t1(a PRIMARY KEY, b); +} + +do_concat_test 1.1.1 { + INSERT INTO t1 VALUES(1, 'one'); +} { + INSERT INTO t1 VALUES(2, 'two'); +} + +do_concat_test 1.1.2 { + UPDATE t1 SET b = 'five' WHERE a = 1; +} { + UPDATE t1 SET b = 'six' WHERE a = 2; +} + +do_concat_test 1.1.3 { + DELETE FROM t1 WHERE a = 1; +} { + DELETE FROM t1 WHERE a = 2; +} + + +# 1.2.1: INSERT + DELETE -> (none) +# 1.2.2: INSERT + UPDATE -> INSERT +# +# 1.2.3: DELETE + INSERT (matching data) -> (none) +# 1.2.4: DELETE + INSERT (non-matching data) -> UPDATE +# +# 1.2.5: UPDATE + UPDATE (matching data) -> (none) +# 1.2.6: UPDATE + UPDATE (non-matching data) -> UPDATE +# 1.2.7: UPDATE + DELETE -> DELETE +# +do_concat_test 1.2.1 { + INSERT INTO t1 VALUES('x', 'y'); +} { + DELETE FROM t1 WHERE a = 'x'; +} +do_concat_test 1.2.2 { + INSERT INTO t1 VALUES(5.0, 'five'); +} { + UPDATE t1 SET b = 'six' WHERE a = 5.0; +} + +do_execsql_test 1.2.3.1 "INSERT INTO t1 VALUES('I', 'one')" +do_concat_test 1.2.3.2 { + DELETE FROM t1 WHERE a = 'I'; +} { + INSERT INTO t1 VALUES('I', 'one'); +} +do_concat_test 1.2.4 { + DELETE FROM t1 WHERE a = 'I'; +} { + INSERT INTO t1 VALUES('I', 'two'); +} +do_concat_test 1.2.5 { + UPDATE t1 SET b = 'five' WHERE a = 'I'; +} { + UPDATE t1 SET b = 'two' WHERE a = 'I'; +} +do_concat_test 1.2.6 { + UPDATE t1 SET b = 'six' WHERE a = 'I'; +} { + UPDATE t1 SET b = 'seven' WHERE a = 'I'; +} +do_concat_test 1.2.7 { + UPDATE t1 SET b = 'eight' WHERE a = 'I'; +} { + DELETE FROM t1 WHERE a = 'I'; +} + + +#------------------------------------------------------------------------- +# Test cases session5-2.* - more complex tests. +# +db function indirect indirect +proc indirect {{x -1}} { + S indirect $x + s_prev indirect $x +} +do_concat_test 2.1 { + CREATE TABLE abc(a, b, c PRIMARY KEY); + INSERT INTO abc VALUES(NULL, NULL, 1); + INSERT INTO abc VALUES('abcdefghijkl', NULL, 2); +} { + DELETE FROM abc WHERE c = 1; + UPDATE abc SET c = 1 WHERE c = 2; +} { + INSERT INTO abc VALUES('abcdefghijkl', NULL, 2); + INSERT INTO abc VALUES(1.0, 2.0, 3); +} { + UPDATE abc SET a = a-1; +} { + CREATE TABLE def(d, e, f, PRIMARY KEY(e, f)); + INSERT INTO def VALUES('x', randomblob(11000), 67); + INSERT INTO def SELECT d, e, f+1 FROM def; + INSERT INTO def SELECT d, e, f+2 FROM def; + INSERT INTO def SELECT d, e, f+4 FROM def; +} { + DELETE FROM def WHERE rowid>4; +} { + INSERT INTO def SELECT d, e, f+4 FROM def; +} { + INSERT INTO abc VALUES(22, 44, -1); +} { + UPDATE abc SET c=-2 WHERE c=-1; + UPDATE abc SET c=-3 WHERE c=-2; +} { + UPDATE abc SET c=-4 WHERE c=-3; +} { + UPDATE abc SET a=a+1 WHERE c=-3; + UPDATE abc SET a=a+1 WHERE c=-3; +} { + UPDATE abc SET a=a+1 WHERE c=-3; + UPDATE abc SET a=a+1 WHERE c=-3; +} { + INSERT INTO abc VALUES('one', 'two', 'three'); +} { + SELECT indirect(1); + UPDATE abc SET a='one point five' WHERE c = 'three'; +} { + SELECT indirect(0); + UPDATE abc SET a='one point six' WHERE c = 'three'; +} { + CREATE TABLE x1(a, b, PRIMARY KEY(a)); + SELECT indirect(1); + INSERT INTO x1 VALUES(1, 2); +} { + SELECT indirect(1); + UPDATE x1 SET b = 3 WHERE a = 1; +} + +catch {db close} +forcedelete test.db +sqlite3 db test.db +do_concat_test 2.2 { + CREATE TABLE t1(a, b, PRIMARY KEY(b)); + CREATE TABLE t2(a PRIMARY KEY, b); + INSERT INTO t1 VALUES('string', 1); + INSERT INTO t1 VALUES(4, 2); + INSERT INTO t1 VALUES(X'FFAAFFAAFFAA', 3); +} { + INSERT INTO t2 VALUES('one', 'two'); + INSERT INTO t2 VALUES(1, NULL); + UPDATE t1 SET a = 5 WHERE a = 2; +} { + DELETE FROM t2 WHERE a = 1; + UPDATE t1 SET a = 4 WHERE a = 2; + INSERT INTO t2 VALUES('x', 'y'); +} + +do_test 2.3.0 { + catch {db close} + forcedelete test.db + sqlite3 db test.db + + set sql1 "" + set sql2 "" + for {set i 1} {$i < 120} {incr i} { + append sql1 "INSERT INTO x1 VALUES($i*4, $i);" + } + for {set i 1} {$i < 120} {incr i} { + append sql2 "DELETE FROM x1 WHERE a = $i*4;" + } + set {} {} +} {} +do_concat_test 2.3 { + CREATE TABLE x1(a PRIMARY KEY, b) +} $sql1 $sql2 $sql1 $sql2 + +do_concat_test 2.4 { + CREATE TABLE x2(a PRIMARY KEY, b); + CREATE TABLE x3(a PRIMARY KEY, b); + + INSERT INTO x2 VALUES('a', 'b'); + INSERT INTO x2 VALUES('x', 'y'); + INSERT INTO x3 VALUES('a', 'b'); +} { + INSERT INTO x2 VALUES('c', 'd'); + INSERT INTO x3 VALUES('e', 'f'); + INSERT INTO x3 VALUES('x', 'y'); +} + +do_concat_test 2.5 { + UPDATE x3 SET b = 'Y' WHERE a = 'x' +} { + DELETE FROM x3 WHERE a = 'x' +} { + DELETE FROM x2 WHERE a = 'a' +} { + INSERT INTO x2 VALUES('a', 'B'); +} + +for {set k 1} {$k <=10} {incr k} { + do_test 2.6.$k.1 { + drop_all_tables + set sql1 "" + set sql2 "" + for {set i 1} {$i < 120} {incr i} { + append sql1 "INSERT INTO x1 VALUES(randomblob(20+(random()%10)), $i);" + } + for {set i 1} {$i < 120} {incr i} { + append sql2 "DELETE FROM x1 WHERE rowid = $i;" + } + set {} {} + } {} + do_concat_test 2.6.$k { + CREATE TABLE x1(a PRIMARY KEY, b) + } $sql1 $sql2 $sql1 $sql2 +} + +for {set k 1} {$k <=10} {incr k} { + do_test 2.7.$k.1 { + drop_all_tables + set sql1 "" + set sql2 "" + for {set i 1} {$i < 120} {incr i} { + append sql1 { + INSERT INTO x1 VALUES( + CASE WHEN random()%2 THEN random() ELSE randomblob(20+random()%10) END, + CASE WHEN random()%2 THEN random() ELSE randomblob(20+random()%10) END + ); + } + } + for {set i 1} {$i < 120} {incr i} { + append sql2 "DELETE FROM x1 WHERE rowid = $i;" + } + set {} {} + } {} + do_concat_test 2.7.$k { + CREATE TABLE x1(a PRIMARY KEY, b) + } $sql1 $sql2 $sql1 $sql2 +} + + +#------------------------------------------------------------------------- +# Test that schema incompatibilities are detected correctly. +# +# session5-3.1: Incompatible number of columns. +# session5-3.2: Incompatible PK definition. +# + +do_test 3.1 { + db close + forcedelete test.db + sqlite3 db test.db + + execsql { CREATE TABLE t1(a PRIMARY KEY, b) } + set c1 [changeset_from_sql { INSERT INTO t1 VALUES(1, 2) }] + execsql { + DROP TABLE t1; + CREATE TABLE t1(a PRIMARY KEY, b, c); + } + set c2 [changeset_from_sql { INSERT INTO t1 VALUES(2, 3, 4) }] + + list [catch { sqlite3changeset_concat $c1 $c2 } msg] $msg +} {1 SQLITE_SCHEMA} + +do_test 3.2 { + db close + forcedelete test.db + sqlite3 db test.db + + execsql { CREATE TABLE t1(a PRIMARY KEY, b) } + set c1 [changeset_from_sql { INSERT INTO t1 VALUES(1, 2) }] + execsql { + DROP TABLE t1; + CREATE TABLE t1(a, b PRIMARY KEY); + } + set c2 [changeset_from_sql { INSERT INTO t1 VALUES(2, 3) }] + + list [catch { sqlite3changeset_concat $c1 $c2 } msg] $msg +} {1 SQLITE_SCHEMA} + +#------------------------------------------------------------------------- +# Test that concat() handles these properly: +# +# session5-4.1: INSERT + INSERT +# session5-4.2: UPDATE + INSERT +# session5-4.3: DELETE + UPDATE +# session5-4.4: DELETE + DELETE +# + +proc do_concat_test2 {tn sql1 sqlX sql2 expected} { + sqlite3session S db main ; S attach * + execsql $sql1 + set ::c1 [S changeset] + S delete + + execsql $sqlX + + sqlite3session S db main ; S attach * + execsql $sql2 + set ::c2 [S changeset] + S delete + + uplevel do_test $tn [list { + changeset_to_list [sqlite3changeset_concat $::c1 $::c2] + }] [list [normalize_list $expected]] +} + +drop_all_tables db +do_concat_test2 4.1 { + CREATE TABLE t1(a PRIMARY KEY, b); + INSERT INTO t1 VALUES('key', 'value'); +} { + DELETE FROM t1 WHERE a = 'key'; +} { + INSERT INTO t1 VALUES('key', 'xxx'); +} { + {INSERT t1 0 X. {} {t key t value}} +} +do_concat_test2 4.2 { + UPDATE t1 SET b = 'yyy'; +} { + DELETE FROM t1 WHERE a = 'key'; +} { + INSERT INTO t1 VALUES('key', 'value'); +} { + {UPDATE t1 0 X. {t key t xxx} {{} {} t yyy}} +} +do_concat_test2 4.3 { + DELETE FROM t1 WHERE a = 'key'; +} { + INSERT INTO t1 VALUES('key', 'www'); +} { + UPDATE t1 SET b = 'valueX' WHERE a = 'key'; +} { + {DELETE t1 0 X. {t key t value} {}} +} +do_concat_test2 4.4 { + DELETE FROM t1 WHERE a = 'key'; +} { + INSERT INTO t1 VALUES('key', 'ttt'); +} { + DELETE FROM t1 WHERE a = 'key'; +} { + {DELETE t1 0 X. {t key t valueX} {}} +} + +finish_test diff --git a/ext/session/session6.test b/ext/session/session6.test new file mode 100644 index 000000000..8a1f172cd --- /dev/null +++ b/ext/session/session6.test @@ -0,0 +1,90 @@ +# 2011 July 11 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# This file implements regression tests for SQLite sessions extension. +# Specifically, it tests that sessions work when the database is modified +# using incremental blob handles. +# + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. test] +} +source [file join [file dirname [info script]] session_common.tcl] +source $testdir/tester.tcl +ifcapable !session {finish_test; return} + +set testprefix session6 + +proc do_then_apply_tcl {tcl {dbname main}} { + proc xConflict args { return "OMIT" } + set rc [catch { + sqlite3session S db $dbname + db eval "SELECT name FROM $dbname.sqlite_master WHERE type = 'table'" { + S attach $name + } + eval $tcl + sqlite3changeset_apply db2 [S changeset] xConflict + } msg] + + catch { S delete } + if {$rc} {error $msg} +} + +test_sqlite3_log x +proc x {args} {puts $args} + +forcedelete test.db2 +sqlite3 db2 test.db2 + +do_common_sql { + CREATE TABLE t1(a PRIMARY KEY, b); + CREATE TABLE t2(c PRIMARY KEY, d); +} + +# Test a blob update. +# +do_test 1.1 { + do_then_apply_tcl { + db eval { INSERT INTO t1 VALUES(1, 'helloworld') } + db eval { INSERT INTO t2 VALUES(2, 'onetwothree') } + } + compare_db db db2 +} {} +do_test 1.2 { + do_then_apply_tcl { + set fd [db incrblob t1 b 1] + puts -nonewline $fd 1234567890 + close $fd + } + compare_db db db2 +} {} + +# Test an attached database. +# +do_test 2.1 { + forcedelete test.db3 + file copy test.db2 test.db3 + execsql { ATTACH 'test.db3' AS aux; } + + do_then_apply_tcl { + set fd [db incrblob aux t2 d 1] + puts -nonewline $fd fourfivesix + close $fd + } aux + + sqlite3 db3 test.db3 + compare_db db2 db3 +} {} + + +db3 close +db2 close + +finish_test diff --git a/ext/session/session8.test b/ext/session/session8.test new file mode 100644 index 000000000..9f70fe282 --- /dev/null +++ b/ext/session/session8.test @@ -0,0 +1,91 @@ +# 2011 July 13 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# This file implements regression tests for SQLite library. +# + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. test] +} +source [file join [file dirname [info script]] session_common.tcl] +source $testdir/tester.tcl +ifcapable !session {finish_test; return} + +set testprefix session8 + +proc noop {args} {} + +# Like [dbcksum] in tester.tcl. Except this version is not sensitive +# to changes in the value of implicit IPK columns. +# +proc udbcksum {db dbname} { + if {$dbname=="temp"} { + set master sqlite_temp_master + } else { + set master $dbname.sqlite_master + } + set alltab [$db eval "SELECT name FROM $master WHERE type='table'"] + set txt [$db eval "SELECT * FROM $master"]\n + foreach tab $alltab { + append txt [lsort [$db eval "SELECT * FROM $dbname.$tab"]]\n + } + return [md5 $txt] +} + +proc do_then_undo {tn sql} { + set ck1 [udbcksum db main] + + sqlite3session S db main + S attach * + db eval $sql + + set ck2 [udbcksum db main] + + set invert [sqlite3changeset_invert [S changeset]] + S delete + sqlite3changeset_apply db $invert noop + + set ck3 [udbcksum db main] + + set a [expr {$ck1==$ck2}] + set b [expr {$ck1==$ck3}] + uplevel [list do_test $tn.1 "set {} $a" 0] + uplevel [list do_test $tn.2 "set {} $b" 1] +} + +do_execsql_test 1.1 { + CREATE TABLE t1(a PRIMARY KEY, b); + INSERT INTO t1 VALUES(1, 2); + INSERT INTO t1 VALUES("abc", "xyz"); +} +do_then_undo 1.2 { INSERT INTO t1 VALUES(3, 4); } +do_then_undo 1.3 { DELETE FROM t1 WHERE b=2; } +do_then_undo 1.4 { UPDATE t1 SET b = 3 WHERE a = 1; } + +do_execsql_test 2.1 { + CREATE TABLE t2(a, b PRIMARY KEY); + INSERT INTO t2 VALUES(1, 2); + INSERT INTO t2 VALUES('abc', 'xyz'); +} +do_then_undo 1.2 { INSERT INTO t2 VALUES(3, 4); } +do_then_undo 1.3 { DELETE FROM t2 WHERE b=2; } +do_then_undo 1.4 { UPDATE t1 SET a = '123' WHERE b = 'xyz'; } + +do_execsql_test 3.1 { + CREATE TABLE t3(a, b, c, d, e, PRIMARY KEY(c, e)); + INSERT INTO t3 VALUES('x', 45, 0.0, 'abcdef', 12); + INSERT INTO t3 VALUES(45, 0.0, 'abcdef', 12, 'x'); + INSERT INTO t3 VALUES(0.0, 'abcdef', 12, 'x', 45); +} + +do_then_undo 3.2 { UPDATE t3 SET b=b||b WHERE e!='x' } +do_then_undo 3.3 { UPDATE t3 SET a = 46 } + +finish_test diff --git a/ext/session/session9.test b/ext/session/session9.test new file mode 100644 index 000000000..ebb88ffad --- /dev/null +++ b/ext/session/session9.test @@ -0,0 +1,287 @@ +# 2013 July 04 +# +# 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 tests that the sessions module handles foreign key constraint +# violations when applying changesets as required. +# + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. test] +} +source [file join [file dirname [info script]] session_common.tcl] +source $testdir/tester.tcl +ifcapable !session {finish_test; return} +set testprefix session9 + + +#-------------------------------------------------------------------- +# Basic tests. +# +proc populate_db {} { + drop_all_tables + execsql { + PRAGMA foreign_keys = 1; + CREATE TABLE p1(a PRIMARY KEY, b); + CREATE TABLE c1(a PRIMARY KEY, b REFERENCES p1); + CREATE TABLE c2(a PRIMARY KEY, + b REFERENCES p1 DEFERRABLE INITIALLY DEFERRED + ); + + INSERT INTO p1 VALUES(1, 'one'); + INSERT INTO p1 VALUES(2, 'two'); + INSERT INTO p1 VALUES(3, 'three'); + INSERT INTO p1 VALUES(4, 'four'); + } +} + +proc capture_changeset {sql} { + sqlite3session S db main + + foreach t [db eval {SELECT name FROM sqlite_master WHERE type='table'}] { + S attach $t + } + execsql $sql + set ret [S changeset] + S delete + + return $ret +} + +do_test 1.1 { + populate_db + set cc [capture_changeset { + INSERT INTO c1 VALUES('ii', 2); + INSERT INTO c2 VALUES('iii', 3); + }] + set {} {} +} {} + +proc xConflict {args} { + lappend ::xConflict {*}$args + return $::conflictret +} + +foreach {tn delrow trans conflictargs conflictret} { + 1 2 0 {FOREIGN_KEY 1} OMIT + 2 3 0 {FOREIGN_KEY 1} OMIT + 3 2 1 {FOREIGN_KEY 1} OMIT + 4 3 1 {FOREIGN_KEY 1} OMIT + 5 2 0 {FOREIGN_KEY 1} ABORT + 6 3 0 {FOREIGN_KEY 1} ABORT + 7 2 1 {FOREIGN_KEY 1} ABORT + 8 3 1 {FOREIGN_KEY 1} ABORT +} { + + set A(OMIT) {0 {}} + set A(ABORT) {1 SQLITE_CONSTRAINT} + do_test 1.2.$tn.1 { + populate_db + execsql { DELETE FROM p1 WHERE a=($delrow+0) } + if {$trans} { execsql BEGIN } + + set ::xConflict [list] + list [catch {sqlite3changeset_apply db $::cc xConflict} msg] $msg + } $A($conflictret) + + do_test 1.2.$tn.2 { set ::xConflict } $conflictargs + + set A(OMIT) {1 1} + set A(ABORT) {0 0} + do_test 1.2.$tn.3 { + execsql { SELECT count(*) FROM c1 UNION ALL SELECT count(*) FROM c2 } + } $A($conflictret) + + do_test 1.2.$tn.4 { expr ![sqlite3_get_autocommit db] } $trans + do_test 1.2.$tn.5 { + if { $trans } { execsql COMMIT } + } {} +} + +#-------------------------------------------------------------------- +# Test that closing a transaction clears the defer_foreign_keys flag. +# +foreach {tn open noclose close} { + 1 BEGIN {} COMMIT + 2 BEGIN {} ROLLBACK + + 3 {SAVEPOINT one} {} {RELEASE one} + 4 {SAVEPOINT one} {ROLLBACK TO one} {RELEASE one} +} { + execsql $open + do_execsql_test 2.$tn.1 { PRAGMA defer_foreign_keys } {0} + + do_execsql_test 2.$tn.2 { + PRAGMA defer_foreign_keys = 1; + PRAGMA defer_foreign_keys; + } {1} + + execsql $noclose + do_execsql_test 2.$tn.3 { PRAGMA defer_foreign_keys } {1} + + execsql $close + do_execsql_test 2.$tn.4 { PRAGMA defer_foreign_keys } {0} +} + +#-------------------------------------------------------------------- +# Test that a cyclic relationship can be inserted and deleted. +# +# This situation does not come up in practice, but testing it serves to +# show that it does not matter which order parent and child keys +# are processed in internally when applying a changeset. +# +drop_all_tables + +do_execsql_test 3.1 { + CREATE TABLE t1(a PRIMARY KEY, b); + CREATE TABLE t2(x PRIMARY KEY, y); +} + +# Create changesets as follows: +# +# $cc1 - Insert a row into t1. +# $cc2 - Insert a row into t2. +# $cc - Combination of $cc1 and $cc2. +# +# $ccdel1 - Delete the row from t1. +# $ccdel2 - Delete the row from t2. +# $ccdel - Combination of $cc1 and $cc2. +# +do_test 3.2 { + set cc1 [capture_changeset { + INSERT INTO t1 VALUES('one', 'value one'); + }] + set ccdel1 [capture_changeset { DELETE FROM t1; }] + set cc2 [capture_changeset { + INSERT INTO t2 VALUES('value one', 'one'); + }] + set ccdel2 [capture_changeset { DELETE FROM t2; }] + set cc [capture_changeset { + INSERT INTO t1 VALUES('one', 'value one'); + INSERT INTO t2 VALUES('value one', 'one'); + }] + set ccdel [capture_changeset { + DELETE FROM t1; + DELETE FROM t2; + }] + set {} {} +} {} + +# Now modify the database schema to create a cyclic foreign key dependency +# between tables t1 and t2. This means that although changesets $cc and +# $ccdel can be applied, none of the others may without violating the +# foreign key constraints. +# +do_test 3.3 { + + drop_all_tables + execsql { + CREATE TABLE t1(a PRIMARY KEY, b REFERENCES t2); + CREATE TABLE t2(x PRIMARY KEY, y REFERENCES t1); + } + + + proc conflict_handler {args} { return "ABORT" } + sqlite3changeset_apply db $cc conflict_handler + + execsql { + SELECT * FROM t1; + SELECT * FROM t2; + } +} {one {value one} {value one} one} + +do_test 3.3.1 { + list [catch {sqlite3changeset_apply db $::ccdel1 conflict_handler} msg] $msg +} {1 SQLITE_CONSTRAINT} + +do_test 3.3.2 { + list [catch {sqlite3changeset_apply db $::ccdel2 conflict_handler} msg] $msg +} {1 SQLITE_CONSTRAINT} + +do_test 3.3.4.1 { + list [catch {sqlite3changeset_apply db $::ccdel conflict_handler} msg] $msg +} {0 {}} +do_execsql_test 3.3.4.2 { + SELECT * FROM t1; + SELECT * FROM t2; +} {} + +do_test 3.5.1 { + list [catch {sqlite3changeset_apply db $::cc1 conflict_handler} msg] $msg +} {1 SQLITE_CONSTRAINT} +do_test 3.5.2 { + list [catch {sqlite3changeset_apply db $::cc2 conflict_handler} msg] $msg +} {1 SQLITE_CONSTRAINT} + +#-------------------------------------------------------------------- +# Test that if a change that affects FK processing is not applied +# due to a separate constraint, SQLite does not get confused and +# increment FK counters anyway. +# +drop_all_tables +do_execsql_test 4.1 { + CREATE TABLE p1(x PRIMARY KEY, y); + CREATE TABLE c1(a PRIMARY KEY, b REFERENCES p1); + INSERT INTO p1 VALUES(1,1); +} + +do_execsql_test 4.2.1 { + BEGIN; + PRAGMA defer_foreign_keys = 1; + INSERT INTO c1 VALUES('x', 'x'); +} +do_catchsql_test 4.2.2 { COMMIT } {1 {FOREIGN KEY constraint failed}} +do_catchsql_test 4.2.3 { ROLLBACK } {0 {}} + +do_execsql_test 4.3.1 { + BEGIN; + PRAGMA defer_foreign_keys = 1; + INSERT INTO c1 VALUES(1, 1); +} +do_catchsql_test 4.3.2 { + INSERT INTO c1 VALUES(1, 'x') +} {1 {UNIQUE constraint failed: c1.a}} + +do_catchsql_test 4.3.3 { COMMIT } {0 {}} +do_catchsql_test 4.3.4 { BEGIN ; COMMIT } {0 {}} + +#-------------------------------------------------------------------- +# Test that if a DELETE change cannot be applied due to an +# SQLITE_CONSTRAINT error thrown by a trigger program, things do not +# go awry. + +drop_all_tables +reset_db +do_execsql_test 5.1 { + CREATE TABLE x1(x PRIMARY KEY, y); + CREATE TABLE x2(x PRIMARY KEY, y); + INSERT INTO x2 VALUES(1, 1); + INSERT INTO x1 VALUES(1, 1); +} + +set ::cc [changeset_from_sql { DELETE FROM x1; }] + +do_execsql_test 5.2 { + INSERT INTO x1 VALUES(1, 1); + CREATE TRIGGER tr1 AFTER DELETE ON x1 BEGIN + INSERT INTO x2 VALUES(old.x, old.y); + END; +} {} + +proc conflict_handler {args} { return "ABORT" } +do_test 5.3 { + list [catch {sqlite3changeset_apply db $::cc conflict_handler} msg] $msg +} {1 SQLITE_ABORT} + +do_execsql_test 5.4 { + SELECT * FROM X1; +} {1 1} + +finish_test diff --git a/ext/session/sessionA.test b/ext/session/sessionA.test new file mode 100644 index 000000000..0e0a14e38 --- /dev/null +++ b/ext/session/sessionA.test @@ -0,0 +1,106 @@ +# 2013 July 04 +# +# 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 tests that filter callbacks work as required. +# + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. test] +} +source [file join [file dirname [info script]] session_common.tcl] +source $testdir/tester.tcl +ifcapable !session {finish_test; return} +set testprefix sessionA + + +forcedelete test.db2 +sqlite3 db2 test.db2 +foreach {tn db} {1 db 2 db2} { + do_test 1.$tn.1 { + execsql { + CREATE TABLE t1(a PRIMARY KEY, b); + CREATE TABLE t2(a PRIMARY KEY, b); + CREATE TABLE t3(a PRIMARY KEY, b); + } $db + } {} +} + +proc tbl_filter {zTbl} { + return $::table_filter($zTbl) +} + +do_test 2.1 { + set ::table_filter(t1) 1 + set ::table_filter(t2) 0 + set ::table_filter(t3) 1 + + sqlite3session S db main + S table_filter tbl_filter + + execsql { + INSERT INTO t1 VALUES('a', 'b'); + INSERT INTO t2 VALUES('c', 'd'); + INSERT INTO t3 VALUES('e', 'f'); + } + + set changeset [S changeset] + S delete + sqlite3changeset_apply db2 $changeset xConflict + + execsql { + SELECT * FROM t1; + SELECT * FROM t2; + SELECT * FROM t3; + } db2 +} {a b e f} + +#------------------------------------------------------------------------- +# Test that filter callbacks passed to sqlite3changeset_apply() are +# invoked correctly. +# +reset_db +do_execsql_test 3.1 { + CREATE TABLE t1(a PRIMARY KEY, b); + CREATE TABLE t2(x PRIMARY KEY, y); +} + +do_test 3.2 { + execsql BEGIN + set ::cs [changeset_from_sql { + INSERT INTO t1 VALUES(1, 2); + INSERT INTO t2 VALUES('x', 'y'); + }] + execsql ROLLBACK + set {} {} +} {} + +proc filter {x y} { + return [string equal $x $y] +} + +do_test 3.3 { + sqlite3changeset_apply db $::cs {} [list filter t1] + execsql { + SELECT * FROM t1; + SELECT * FROM t2; + } +} {1 2} + +do_test 3.4 { + execsql { DELETE FROM t1 } + sqlite3changeset_apply db $::cs {} [list filter t2] + execsql { + SELECT * FROM t1; + SELECT * FROM t2; + } +} {x y} + +finish_test diff --git a/ext/session/sessionB.test b/ext/session/sessionB.test new file mode 100644 index 000000000..f1af5d213 --- /dev/null +++ b/ext/session/sessionB.test @@ -0,0 +1,518 @@ +# 2014 August 16 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# +# This file implements regression tests for sessions SQLite extension. +# Specifically, this file contains tests for "patchset" changes. +# + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. test] +} +source [file join [file dirname [info script]] session_common.tcl] +source $testdir/tester.tcl +ifcapable !session {finish_test; return} + +set testprefix sessionB + +# +# 1.*: Test that the blobs returned by the session_patchset() API are +# as expected. Also the sqlite3_changeset_iter functions. +# +# 2.*: Test that patchset blobs are handled by sqlite3changeset_apply(). +# +# 3.*: Test that sqlite3changeset_invert() works with patchset blobs. +# Correct behaviour is to return SQLITE_CORRUPT. + +proc do_patchset_test {tn session res} { + set r [list] + foreach x $res {lappend r $x} + uplevel do_test $tn [list [subst -nocommands { + set x [list] + sqlite3session_foreach c [$session patchset] { lappend x [set c] } + set x + }]] [list $r] +} + +proc do_sql2patchset_test {tn sql res} { + sqlite3session S db main + S attach * + execsql $sql + uplevel [list do_patchset_test $tn S $res] + S delete +} + +#------------------------------------------------------------------------- +# Run simple tests of the _patchset() API. +# +do_execsql_test 1.0 { + CREATE TABLE t1(a, b, c, d, PRIMARY KEY(d, a)); + INSERT INTO t1 VALUES(1, 2, 3, 4); + INSERT INTO t1 VALUES(5, 6, 7, 8); + INSERT INTO t1 VALUES(9, 10, 11, 12); +} + +do_test 1.1 { + sqlite3session S db main + S attach t1 + execsql { + INSERT INTO t1 VALUES('w', 'x', 'y', 'z'); + DELETE FROM t1 WHERE d=4; + UPDATE t1 SET c = 14 WHERE a=5; + } +} {} + +do_patchset_test 1.2 S { + {UPDATE t1 0 X..X {i 5 {} {} {} {} i 8} {{} {} {} {} i 14 {} {}}} + {INSERT t1 0 X..X {} {t w t x t y t z}} + {DELETE t1 0 X..X {i 1 {} {} {} {} i 4} {}} +} + +do_test 1.3 { + S delete +} {} + +do_sql2patchset_test 1.4 { + DELETE FROM t1; +} { + {DELETE t1 0 X..X {i 5 {} {} {} {} i 8} {}} + {DELETE t1 0 X..X {t w {} {} {} {} t z} {}} + {DELETE t1 0 X..X {i 9 {} {} {} {} i 12} {}} +} + +do_sql2patchset_test 1.5 { + INSERT INTO t1 VALUES(X'61626364', NULL, NULL, 4.2); + INSERT INTO t1 VALUES(4.2, NULL, NULL, X'61626364'); +} { + {INSERT t1 0 X..X {} {f 4.2 n {} n {} b abcd}} + {INSERT t1 0 X..X {} {b abcd n {} n {} f 4.2}} +} + +do_sql2patchset_test 1.6 { + UPDATE t1 SET b=45 WHERE typeof(a)=='blob'; + UPDATE t1 SET c='zzzz' WHERE typeof(a)!='blob'; +} { + {UPDATE t1 0 X..X {f 4.2 {} {} {} {} b abcd} {{} {} {} {} t zzzz {} {}}} + {UPDATE t1 0 X..X {b abcd {} {} {} {} f 4.2} {{} {} i 45 {} {} {} {}}} +} + +do_sql2patchset_test 1.7 { + UPDATE t1 SET b='xyz' WHERE typeof(a)=='blob'; + UPDATE t1 SET c='xyz' WHERE typeof(a)!='blob'; + UPDATE t1 SET b=45 WHERE typeof(a)=='blob'; + UPDATE t1 SET c='zzzz' WHERE typeof(a)!='blob'; +} { +} + +do_sql2patchset_test 1.8 { + DELETE FROM t1; +} { + {DELETE t1 0 X..X {f 4.2 {} {} {} {} b abcd} {}} + {DELETE t1 0 X..X {b abcd {} {} {} {} f 4.2} {}} +} + +#------------------------------------------------------------------------- +# Run simple tests of _apply() with patchset objects. +# +reset_db + +proc noop {args} { error $args } +proc exec_rollback_replay {sql} { + sqlite3session S db main + S attach * + execsql BEGIN + execsql $sql + set patchset [S patchset] + S delete + execsql ROLLBACK + sqlite3changeset_apply db $patchset noop +} + +do_execsql_test 2.0 { + CREATE TABLE t2(a, b, c, d, PRIMARY KEY(b,c)); + CREATE TABLE t3(w, x, y, z, PRIMARY KEY(w)); +} + +do_test 2.1 { + exec_rollback_replay { + INSERT INTO t2 VALUES(1, 2, 3, 4); + INSERT INTO t2 VALUES('w', 'x', 'y', 'z'); + } + execsql { SELECT * FROM t2 } +} {1 2 3 4 w x y z} + +do_test 2.2 { + exec_rollback_replay { + DELETE FROM t2 WHERE a=1; + UPDATE t2 SET d = 'a'; + } + execsql { SELECT * FROM t2 } +} {w x y a} + +#------------------------------------------------------------------------- +# sqlite3changeset_invert() +# +reset_db + +do_execsql_test 3.1 { CREATE TABLE t1(x PRIMARY KEY, y) } +do_test 3.2 { + sqlite3session S db main + S attach * + execsql { INSERT INTO t1 VALUES(1, 2) } + set patchset [S patchset] + S delete + list [catch { sqlite3changeset_invert $patchset } msg] [set msg] +} {1 SQLITE_CORRUPT} + + +#------------------------------------------------------------------------- +# sqlite3changeset_concat() +# +reset_db + +proc do_patchconcat_test {tn args} { + set bRevert 0 + if {[lindex $args 0] == "-revert"} { + set bRevert 1 + set args [lrange $args 1 end] + } + set nSql [expr [llength $args]-1] + set res [lindex $args $nSql] + set patchlist [list] + + execsql BEGIN + if {$bRevert} { execsql { SAVEPOINT x } } + foreach sql [lrange $args 0 end-1] { + sqlite3session S db main + S attach * + execsql $sql + lappend patchlist [S patchset] + S delete + if {$bRevert} { execsql { ROLLBACK TO x } } + } + execsql ROLLBACK + + set patch [lindex $patchlist 0] + foreach p [lrange $patchlist 1 end] { + set patch [sqlite3changeset_concat $patch $p] + } + + set x [list] + sqlite3session_foreach c $patch { lappend x $c } + + uplevel [list do_test $tn [list set {} $x] [list {*}$res]] +} + +do_execsql_test 4.1.1 { + CREATE TABLE t1(x PRIMARY KEY, y, z); +} +do_patchconcat_test 4.1.2 { + INSERT INTO t1 VALUES(1, 2, 3); +} { + INSERT INTO t1 VALUES(4, 5, 6); +} { + {INSERT t1 0 X.. {} {i 1 i 2 i 3}} + {INSERT t1 0 X.. {} {i 4 i 5 i 6}} +} + +do_execsql_test 4.2.1 { + INSERT INTO t1 VALUES(1, 2, 3); + INSERT INTO t1 VALUES(4, 5, 6); +} + +do_patchconcat_test 4.2.2 { + UPDATE t1 SET z = 'abc' WHERE x=1 +} { + UPDATE t1 SET z = 'def' WHERE x=4 +} { + {UPDATE t1 0 X.. {i 1 {} {} {} {}} {{} {} {} {} t abc}} + {UPDATE t1 0 X.. {i 4 {} {} {} {}} {{} {} {} {} t def}} +} + +do_patchconcat_test 4.2.3 { + DELETE FROM t1 WHERE x=1; +} { + DELETE FROM t1 WHERE x=4; +} { + {DELETE t1 0 X.. {i 1 {} {} {} {}} {}} + {DELETE t1 0 X.. {i 4 {} {} {} {}} {}} +} + + +do_execsql_test 4.3.1 { + CREATE TABLE t2(a, b, c, d, PRIMARY KEY(c, b)); + INSERT INTO t2 VALUES('.', 1, 1, '.'); + INSERT INTO t2 VALUES('.', 1, 2, '.'); + INSERT INTO t2 VALUES('.', 2, 1, '.'); + INSERT INTO t2 VALUES('.', 2, 2, '.'); +} + +# INSERT + INSERT +do_patchconcat_test 4.3.2 -revert { + INSERT INTO t2 VALUES('a', 'a', 'a', 'a'); +} { + INSERT INTO t2 VALUES('b', 'a', 'a', 'b'); +} { + {INSERT t2 0 .XX. {} {t a t a t a t a}} +} + +# INSERT + DELETE +do_patchconcat_test 4.3.3 { + INSERT INTO t2 VALUES('a', 'a', 'a', 'a'); +} { + DELETE FROM t2 WHERE c = 'a'; +} { +} + +# INSERT + UPDATE +do_patchconcat_test 4.3.4 { + INSERT INTO t2 VALUES('a', 'a', 'a', 'a'); +} { + UPDATE t2 SET d = 'b' WHERE c='a'; +} { + {INSERT t2 0 .XX. {} {t a t a t a t b}} +} + +# UPDATE + UPDATE +do_patchconcat_test 4.3.5 { + UPDATE t2 SET a = 'a' WHERE c=1 AND b=2; +} { + UPDATE t2 SET d = 'd' WHERE c=1 AND b=2; +} { + {UPDATE t2 0 .XX. {{} {} i 2 i 1 {} {}} {t a {} {} {} {} t d}} +} + +# UPDATE + DELETE +do_patchconcat_test 4.3.6 { + UPDATE t2 SET a = 'a' WHERE c=1 AND b=2; +} { + DELETE FROM t2 WHERE c=1 AND b=2; +} { + {DELETE t2 0 .XX. {{} {} i 2 i 1 {} {}} {}} +} + +# DELETE + INSERT +do_patchconcat_test 4.3.7 { + DELETE FROM t2 WHERE b=1; +} { + INSERT INTO t2 VALUES('x', 1, 2, '.'); +} { + {DELETE t2 0 .XX. {{} {} i 1 i 1 {} {}} {}} + {UPDATE t2 0 .XX. {{} {} i 1 i 2 {} {}} {t x {} {} {} {} t .}} +} + +# DELETE + UPDATE +do_patchconcat_test 4.3.8 -revert { + DELETE FROM t2 WHERE b=1 AND c=2; +} { + UPDATE t2 SET a=5 WHERE b=1 AND c=2; +} { + {DELETE t2 0 .XX. {{} {} i 1 i 2 {} {}} {}} +} + +# DELETE + UPDATE +do_patchconcat_test 4.3.9 -revert { + DELETE FROM t2 WHERE b=1 AND c=2; +} { + DELETE FROM t2 WHERE b=1; +} { + {DELETE t2 0 .XX. {{} {} i 1 i 1 {} {}} {}} + {DELETE t2 0 .XX. {{} {} i 1 i 2 {} {}} {}} +} + +#------------------------------------------------------------------------- +# More rigorous testing of the _patchset(), _apply and _concat() APIs. +# +# The inputs to each test are a populate database and a list of DML +# statements. This test determines that the final database is the same +# if: +# +# 1) the statements are executed directly on the database. +# +# 2) a single patchset is collected while executing the statements and +# then applied to a copy of the original database file. +# +# 3) individual patchsets are collected for statement while executing +# them and concatenated together before being applied to a copy of +# the original database. The concatenation is done in a couple of +# different ways - linear, pairwise etc. +# +# All tests, as it happens, are run with both changesets and patchsets. +# But the focus is on patchset capabilities. +# + +# Return a checksum of the contents of the database file. Implicit IPK +# columns are not included in the checksum - just modifying rowids does +# not change the database checksum. +# +proc databasecksum {db} { + set alltab [$db eval {SELECT name FROM sqlite_master WHERE type='table'}] + foreach tab $alltab { + $db eval "SELECT * FROM $tab LIMIT 1" res { } + set slist [list] + foreach col [lsort $res(*)] { + lappend slist "quote($col)" + } + set sql "SELECT [join $slist ,] FROM $tab" + append txt "[lsort [$db eval $sql]]\n" + } + return [md5 $txt] +} + +proc do_patchset_test {tn tstcmd lSql} { + if {$tstcmd != "patchset" && $tstcmd != "changeset"} { + error "have $tstcmd: must be patchset or changeset" + } + + foreach fname {test.db2 test.db3 test.db4 test.db5} { + forcedelete $fname + forcecopy test.db $fname + } + + # Execute the SQL statements on [db]. Collect a patchset for each + # individual statement, as well as a single patchset for the entire + # operation. + sqlite3session S db main + S attach * + foreach sql $lSql { + sqlite3session T db main + T attach * + db eval $sql + lappend lPatch [T $tstcmd] + T delete + } + set patchset [S $tstcmd] + S delete + + # Calculate a checksum for the final database. + set cksum [databasecksum db] + + # 1. Apply the single large patchset to test.db2 + sqlite3 db2 test.db2 + sqlite3changeset_apply db2 $patchset noop + uplevel [list do_test $tn.1 { databasecksum db2 } $cksum ] + db2 close + + # 2. Apply each of the single-statement patchsets to test.db3 + sqlite3 db2 test.db3 + foreach p $lPatch { + sqlite3changeset_apply db2 $p noop + } + uplevel [list do_test $tn.2 { databasecksum db2 } $cksum ] + db2 close + + # 3. Concatenate all single-statement patchsets into a single large + # patchset, then apply it to test.db4. + # + sqlite3 db2 test.db4 + set big "" + foreach p $lPatch { + set big [sqlite3changeset_concat $big $p] + } + sqlite3changeset_apply db2 $big noop + uplevel [list do_test $tn.3 { databasecksum db2 } $cksum ] + db2 close + + # 4. Concatenate all single-statement patchsets pairwise into a single + # large patchset, then apply it to test.db5. Pairwise concatenation: + # + # a b c d e f g h i j k + # -> {a b} {c d} {e f} {g h} {i j} k + # -> {a b c d} {e f g h} {i j k} + # -> {a b c d e f g h} {i j k} + # -> {a b c d e f g h i j k} + # -> APPLY! + # + sqlite3 db2 test.db5 + set L $lPatch + while {[llength $L] > 1} { + set O [list] + for {set i 0} {$i < [llength $L]} {incr i 2} { + if {$i==[llength $L]-1} { + lappend O [lindex $L $i] + } else { + set i1 [expr $i+1] + lappend O [sqlite3changeset_concat [lindex $L $i] [lindex $L $i1]] + } + } + set L $O + } + sqlite3changeset_apply db2 [lindex $L 0] noop + uplevel [list do_test $tn.4 { databasecksum db2 } $cksum ] + db2 close +} + +proc do_patchset_changeset_test {tn initsql args} { + foreach tstcmd {patchset changeset} { + reset_db + execsql $initsql + set x 0 + foreach sql $args { + incr x + set lSql [split $sql ";"] + uplevel [list do_patchset_test $tn.$tstcmd.$x $tstcmd $lSql] + } + } +} + +do_patchset_changeset_test 5.1 { + CREATE TABLE t1(a PRIMARY KEY, b, c); + INSERT INTO t1 VALUES(1, 2, 3); +} { + INSERT INTO t1 VALUES(4, 5, 6); + DELETE FROM t1 WHERE a=1; +} { + INSERT INTO t1 VALUES(7, 8, 9); + UPDATE t1 SET c = 5; + INSERT INTO t1 VALUES(10, 11, 12); + UPDATE t1 SET c = 6; + INSERT INTO t1 VALUES(13, 14, 15); +} { + UPDATE t1 SET c=c+1; + DELETE FROM t1 WHERE (a%2); +} + +do_patchset_changeset_test 5.2 { + CREATE TABLE t1(a PRIMARY KEY, b, c); + CREATE TABLE t2(a, b, c, d, PRIMARY KEY(c, b)); +} { + INSERT INTO t1 VALUES(x'00', 0, 'zero'); + INSERT INTO t1 VALUES(x'01', 1, 'one'); + INSERT INTO t1 VALUES(x'02', 4, 'four'); + INSERT INTO t1 VALUES(x'03', 9, 'nine'); + INSERT INTO t1 VALUES(x'04', 16, 'sixteen'); + INSERT INTO t1 VALUES(x'05', 25, 'twenty-five'); +} { + UPDATE t1 SET a = b WHERE b<=4; + INSERT INTO t2 SELECT NULL, * FROM t1; + DELETE FROM t1 WHERE b=25; +} { + DELETE FROM t2; + INSERT INTO t2 SELECT NULL, * FROM t1; + DELETE FROM t1; + INSERT INTO t1 SELECT b, c, d FROM t2; + UPDATE t1 SET b = b+1; + UPDATE t1 SET b = b+1; + UPDATE t1 SET b = b+1; +} + +set initsql { CREATE TABLE t1(a, b, c, PRIMARY KEY(c, b)); } +for {set i 0} {$i < 1000} {incr i} { + append insert "INSERT INTO t1 VALUES($i, $i, $i);" + append delete "DELETE FROM t1 WHERE b=$i;" +} +do_patchset_changeset_test 5.3 \ + $initsql $insert $delete \ + $insert $delete \ + "$insert $delete" \ + $delete + + +finish_test diff --git a/ext/session/sessionC.test b/ext/session/sessionC.test new file mode 100644 index 000000000..81cc53f5a --- /dev/null +++ b/ext/session/sessionC.test @@ -0,0 +1,190 @@ +# 2014 August 16 +# +# 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. +# +#*********************************************************************** +# +# + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. test] +} +source [file join [file dirname [info script]] session_common.tcl] +source $testdir/tester.tcl +ifcapable !session {finish_test; return} + +set testprefix sessionC + +#------------------------------------------------------------------------- +# Test the outcome of a DELETE operation made as part of applying a +# changeset failing with SQLITE_CONSTRAINT. This may happen if an +# ON DELETE RESTRICT foreign key action is triggered, or if a trigger +# program raises a constraint somehow. +# +do_execsql_test 1.0 { + PRAGMA foreign_keys = 1; + + CREATE TABLE p(a PRIMARY KEY, b, c); + CREATE TABLE c(d PRIMARY KEY, e REFERENCES p ON DELETE RESTRICT); + + INSERT INTO p VALUES('one', 1, 1); + INSERT INTO p VALUES('two', 2, 2); + INSERT INTO p VALUES('three', 3, 3); + + INSERT INTO c VALUES(1, 'one'); + INSERT INTO c VALUES(3, 'three'); +} + +do_test 1.1 { + execsql BEGIN + set C [changeset_from_sql { + INSERT INTO c VALUES(4, 'one'); + DELETE FROM p WHERE a='two'; + }] + execsql ROLLBACK + execsql { + INSERT INTO c VALUES(2, 'two'); + } +} {} + +do_test 1.2.1 { + proc xConflict {args} { return "ABORT" } + catch { sqlite3changeset_apply db $C xConflict } msg + set msg +} {SQLITE_ABORT} +do_execsql_test 1.2.2 { SELECT * FROM c } {1 one 3 three 2 two} + +do_test 1.3.1 { + proc xConflict {args} { return "OMIT" } + catch { sqlite3changeset_apply db $C xConflict } msg + set msg +} {} +do_execsql_test 1.3.2 { SELECT * FROM c } {1 one 3 three 2 two 4 one} +do_execsql_test 1.3.3 { + SELECT * FROM p; +} {one 1 1 two 2 2 three 3 3} + + +#------------------------------------------------------------------------- +# Test that concatenating a changeset with a patchset does not work. +# Any attempt to do so returns SQLITE_ERROR. +# +reset_db +do_execsql_test 2.0 { + CREATE TABLE x1(t, v PRIMARY KEY); + INSERT INTO x1 VALUES(12, 55); + INSERT INTO x1 VALUES(55, 14); +} + +do_test 2.1 { + execsql BEGIN + + sqlite3session S1 db main + S1 attach * + execsql { + UPDATE x1 SET t=13 WHERE v=55; + INSERT INTO x1 VALUES(99, 123); + } + set patchset [S1 patchset] + S1 delete + + sqlite3session S1 db main + S1 attach * + execsql { + UPDATE x1 SET t=56 WHERE v=14; + INSERT INTO x1 VALUES(22, 998); + } + set changeset [S1 changeset] + S1 delete + + execsql ROLLBACK +} {} + +do_test 2.2 { + set rc [catch { sqlite3changeset_concat $patchset $changeset } msg] + list $rc $msg +} {1 SQLITE_ERROR} + +do_test 2.3 { + set rc [catch { sqlite3changeset_concat $changeset $patchset } msg] + list $rc $msg +} {1 SQLITE_ERROR} + +do_test 2.4 { + set rc [catch { sqlite3changeset_concat {} $patchset } msg] + list $rc $msg +} [list 0 $patchset] + +do_test 2.5 { + set rc [catch { sqlite3changeset_concat $patchset {} } msg] + list $rc $msg +} [list 0 $patchset] + +do_test 2.6 { + set rc [catch { sqlite3changeset_concat {} $changeset } msg] + list $rc $msg +} [list 0 $changeset] + +do_test 2.7 { + set rc [catch { sqlite3changeset_concat $changeset {} } msg] + list $rc $msg +} [list 0 $changeset] + +do_test 2.8 { + set rc [catch { sqlite3changeset_concat {} {} } msg] + list $rc $msg +} [list 0 {}] + + +#------------------------------------------------------------------------- +# Test that the xFilter argument to sqlite3changeset_apply() works. +# +reset_db +do_execsql_test 3.0 { + CREATE TABLE t1(a PRIMARY KEY, b); + CREATE TABLE t2(a PRIMARY KEY, b); + CREATE TABLE t3(a PRIMARY KEY, b); +} +do_test 3.1 { + execsql BEGIN + set changeset [changeset_from_sql { + INSERT INTO t1 VALUES(1, 1); + INSERT INTO t2 VALUES(2, 2); + INSERT INTO t3 VALUES(3, 3); + }] + execsql ROLLBACK +} {} +do_test 3.2 { + proc xFilter {zName} { + if {$zName == "t1"} { return 1 } + return 0 + } + sqlite3changeset_apply db $changeset noop xFilter + execsql { + SELECT * FROM t1; + SELECT * FROM t2; + SELECT * FROM t3; + } +} {1 1} +do_test 3.3 { + proc xFilter {zName} { + if {$zName == "t3"} { return 1 } + return 0 + } + sqlite3changeset_apply db $changeset noop xFilter + execsql { + SELECT * FROM t1; + SELECT * FROM t2; + SELECT * FROM t3; + } +} {1 1 3 3} + + + +finish_test + diff --git a/ext/session/session_common.tcl b/ext/session/session_common.tcl new file mode 100644 index 000000000..d6ce27a85 --- /dev/null +++ b/ext/session/session_common.tcl @@ -0,0 +1,135 @@ + + +proc do_conflict_test {tn args} { + proc xConflict {args} { + lappend ::xConflict $args + return "" + } + proc bgerror {args} { set ::background_error $args } + + + set O(-tables) [list] + set O(-sql) [list] + set O(-conflicts) [list] + + array set V $args + foreach key [array names V] { + if {![info exists O($key)]} {error "no such option: $key"} + } + array set O $args + + sqlite3session S db main + foreach t $O(-tables) { S attach $t } + execsql $O(-sql) + + set ::xConflict [list] + sqlite3changeset_apply db2 [S changeset] xConflict + + set conflicts [list] + foreach c $O(-conflicts) { + lappend conflicts $c + } + + after 1 {set go 1} + vwait go + + uplevel do_test $tn [list { set ::xConflict }] [list $conflicts] + S delete +} + +proc do_common_sql {sql} { + execsql $sql db + execsql $sql db2 +} + +proc changeset_from_sql {sql {dbname main}} { + set rc [catch { + sqlite3session S db $dbname + db eval "SELECT name FROM $dbname.sqlite_master WHERE type = 'table'" { + S attach $name + } + db eval $sql + S changeset + } changeset] + catch { S delete } + + if {$rc} { + error $changeset + } + return $changeset +} + +proc do_then_apply_sql {sql {dbname main}} { + proc xConflict args { return "OMIT" } + set rc [catch { + sqlite3session S db $dbname + db eval "SELECT name FROM $dbname.sqlite_master WHERE type = 'table'" { + S attach $name + } + db eval $sql + sqlite3changeset_apply db2 [S changeset] xConflict + } msg] + + catch { S delete } + + if {$rc} {error $msg} +} + +proc do_iterator_test {tn tbl_list sql res} { + sqlite3session S db main + if {[llength $tbl_list]==0} { S attach * } + foreach t $tbl_list {S attach $t} + + execsql $sql + + set r [list] + foreach v $res { lappend r $v } + + set x [list] + sqlite3session_foreach c [S changeset] { lappend x $c } + uplevel do_test $tn [list [list set {} $x]] [list $r] + + S delete +} + +# Compare the contents of all tables in [db1] and [db2]. Throw an error if +# they are not identical, or return an empty string if they are. +# +proc compare_db {db1 db2} { + + set sql {SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name} + set lot1 [$db1 eval $sql] + set lot2 [$db2 eval $sql] + + if {$lot1 != $lot2} { + puts $lot1 + puts $lot2 + error "databases contain different tables" + } + + foreach tbl $lot1 { + set col1 [list] + set col2 [list] + + $db1 eval "PRAGMA table_info = $tbl" { lappend col1 $name } + $db2 eval "PRAGMA table_info = $tbl" { lappend col2 $name } + if {$col1 != $col2} { error "table $tbl schema mismatch" } + + set sql "SELECT * FROM $tbl ORDER BY [join $col1 ,]" + set data1 [$db1 eval $sql] + set data2 [$db2 eval $sql] + if {$data1 != $data2} { + puts "$data1" + puts "$data2" + error "table $tbl data mismatch" + } + } + + return "" +} + +proc changeset_to_list {c} { + set list [list] + sqlite3session_foreach elem $c { lappend list $elem } + lsort $list +} diff --git a/ext/session/sessionfault.test b/ext/session/sessionfault.test new file mode 100644 index 000000000..39f27bfe6 --- /dev/null +++ b/ext/session/sessionfault.test @@ -0,0 +1,587 @@ +# 2011 Mar 21 +# +# 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. +# +#*********************************************************************** +# +# The focus of this file is testing the session module. +# + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. test] +} +source [file join [file dirname [info script]] session_common.tcl] +source $testdir/tester.tcl + +set testprefix sessionfault + +forcedelete test.db2 +sqlite3 db2 test.db2 +do_common_sql { + CREATE TABLE t1(a, b, c, PRIMARY KEY(a, b)); + INSERT INTO t1 VALUES(1, 2, 3); + INSERT INTO t1 VALUES(4, 5, 6); +} +faultsim_save_and_close +db2 close + +#------------------------------------------------------------------------- +# Test OOM error handling when collecting and applying a simple changeset. +# +# Test 1.1 attaches tables individually by name to the session object. +# Whereas test 1.2 passes NULL to sqlite3session_attach() to attach all +# tables. +# +do_faultsim_test 1.1 -faults oom-* -prep { + catch {db2 close} + catch {db close} + faultsim_restore_and_reopen + sqlite3 db2 test.db2 +} -body { + do_then_apply_sql { + INSERT INTO t1 VALUES('a string value', 8, 9); + UPDATE t1 SET c = 10 WHERE a = 1; + DELETE FROM t1 WHERE a = 4; + } +} -test { + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} + faultsim_integrity_check + if {$testrc==0} { compare_db db db2 } +} + +do_faultsim_test 1.2 -faults oom-* -prep { + catch {db2 close} + catch {db close} + faultsim_restore_and_reopen +} -body { + sqlite3session S db main + S attach * + execsql { + INSERT INTO t1 VALUES('a string value', 8, 9); + UPDATE t1 SET c = 10 WHERE a = 1; + DELETE FROM t1 WHERE a = 4; + } + set ::changeset [S changeset] + set {} {} +} -test { + catch { S delete } + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} + faultsim_integrity_check + if {$testrc==0} { + proc xConflict {args} { return "OMIT" } + sqlite3 db2 test.db2 + sqlite3changeset_apply db2 $::changeset xConflict + compare_db db db2 + } +} + +#------------------------------------------------------------------------- +# The following block of tests - 2.* - are designed to check +# the handling of faults in the sqlite3changeset_apply() function. +# +catch {db close} +catch {db2 close} +forcedelete test.db2 test.db +sqlite3 db2 test.db2 +sqlite3 db test.db +do_common_sql { + CREATE TABLE t1(a, b, c, PRIMARY KEY(a, b)); + INSERT INTO t1 VALUES('apple', 'orange', 'pear'); + + CREATE TABLE t2(x PRIMARY KEY, y); +} +db2 close +faultsim_save_and_close + + +foreach {tn conflict_policy sql sql2} { + 1 OMIT { INSERT INTO t1 VALUES('one text', 'two text', X'00ff00') } {} + 2 OMIT { DELETE FROM t1 WHERE a = 'apple' } {} + 3 OMIT { UPDATE t1 SET c = 'banana' WHERE b = 'orange' } {} + 4 REPLACE { INSERT INTO t2 VALUES('keyvalue', 'value 1') } { + INSERT INTO t2 VALUES('keyvalue', 'value 2'); + } +} { + proc xConflict args [list return $conflict_policy] + + do_faultsim_test 2.$tn -faults oom-transient -prep { + catch {db2 close} + catch {db close} + faultsim_restore_and_reopen + set ::changeset [changeset_from_sql $::sql] + sqlite3 db2 test.db2 + sqlite3_db_config_lookaside db2 0 0 0 + execsql $::sql2 db2 + } -body { + sqlite3changeset_apply db2 $::changeset xConflict + } -test { + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} + faultsim_integrity_check + if {$testrc==0} { compare_db db db2 } + } +} + +#------------------------------------------------------------------------- +# This test case is designed so that a malloc() failure occurs while +# resizing the session object hash-table from 256 to 512 buckets. This +# is not an error, just a sub-optimal condition. +# +do_faultsim_test 3 -faults oom-* -prep { + catch {db2 close} + catch {db close} + faultsim_restore_and_reopen + sqlite3 db2 test.db2 + + sqlite3session S db main + S attach t1 + execsql { BEGIN } + for {set i 0} {$i < 125} {incr i} { + execsql {INSERT INTO t1 VALUES(10+$i, 10+$i, 10+$i)} + } +} -body { + for {set i 125} {$i < 133} {incr i} { + execsql {INSERT INTO t1 VALUES(10+$i, 10+$i, 1-+$i)} + } + S changeset + set {} {} +} -test { + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} + if {$testrc==0} { + sqlite3changeset_apply db2 [S changeset] xConflict + compare_db db db2 + } + catch { S delete } + faultsim_integrity_check +} + +catch { db close } +catch { db2 close } +forcedelete test.db2 test.db +sqlite3 db2 test.db2 +sqlite3 db test.db + +proc xConflict {op tbl type args} { + if { $type=="CONFLICT" || $type=="DATA" } { + return "REPLACE" + } + return "OMIT" +} + +do_test 4.0 { + execsql { + PRAGMA encoding = 'utf16'; + CREATE TABLE t1(a PRIMARY KEY, b); + INSERT INTO t1 VALUES(5, 32); + } + execsql { + PRAGMA encoding = 'utf16'; + CREATE TABLE t1(a PRIMARY KEY, b NOT NULL); + INSERT INTO t1 VALUES(1, 2); + INSERT INTO t1 VALUES(2, 4); + INSERT INTO t1 VALUES(4, 16); + } db2 +} {} + +faultsim_save_and_close +db2 close + +do_faultsim_test 4 -faults oom-* -prep { + catch {db2 close} + catch {db close} + faultsim_restore_and_reopen + sqlite3 db2 test.db2 + sqlite3session S db main + S attach t1 + execsql { + INSERT INTO t1 VALUES(1, 45); + INSERT INTO t1 VALUES(2, 55); + INSERT INTO t1 VALUES(3, 55); + UPDATE t1 SET a = 4 WHERE a = 5; + } +} -body { + sqlite3changeset_apply db2 [S changeset] xConflict +} -test { + catch { S delete } + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} + if {$testrc==0} { compare_db db db2 } +} + +#------------------------------------------------------------------------- +# This block of tests verifies that OOM faults in the +# sqlite3changeset_invert() function are handled correctly. +# +catch {db close} +catch {db2 close} +forcedelete test.db +sqlite3 db test.db +execsql { + CREATE TABLE t1(a, b, PRIMARY KEY(b)); + CREATE TABLE t2(a PRIMARY KEY, b); + INSERT INTO t1 VALUES('string', 1); + INSERT INTO t1 VALUES(4, 2); + INSERT INTO t1 VALUES(X'FFAAFFAAFFAA', 3); +} +set changeset [changeset_from_sql { + INSERT INTO t1 VALUES('xxx', 'yyy'); + DELETE FROM t1 WHERE a = 'string'; + UPDATE t1 SET a = 20 WHERE b = 2; +}] +db close + +do_faultsim_test 5.1 -faults oom* -body { + set ::inverse [sqlite3changeset_invert $::changeset] + set {} {} +} -test { + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} + if {$testrc==0} { + set x [list] + sqlite3session_foreach c $::inverse { lappend x $c } + foreach c { + {DELETE t1 0 .X {t xxx t yyy} {}} + {INSERT t1 0 .X {} {t string i 1}} + {UPDATE t1 0 .X {i 20 i 2} {i 4 {} {}}} + } { lappend y $c } + if {$x != $y} { error "changeset no good" } + } +} + +catch {db close} +catch {db2 close} +forcedelete test.db +sqlite3 db test.db +execsql { + CREATE TABLE t2(a PRIMARY KEY, b); + INSERT INTO t2 VALUES(1, 'abc'); + INSERT INTO t2 VALUES(2, 'def'); +} +set changeset [changeset_from_sql { + UPDATE t2 SET b = (b || b || b || b); + UPDATE t2 SET b = (b || b || b || b); + UPDATE t2 SET b = (b || b || b || b); + UPDATE t2 SET b = (b || b || b || b); +}] +db close +set abc [string repeat abc 256] +set def [string repeat def 256] + +do_faultsim_test 5.2 -faults oom-tra* -body { + set ::inverse [sqlite3changeset_invert $::changeset] + set {} {} +} -test { + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} + if {$testrc==0} { + set x [list] + sqlite3session_foreach c $::inverse { lappend x $c } + foreach c " + {UPDATE t2 0 X. {i 1 t $::abc} {{} {} t abc}} + {UPDATE t2 0 X. {i 2 t $::def} {{} {} t def}} + " { lappend y $c } + if {$x != $y} { error "changeset no good" } + } +} + +catch {db close} +catch {db2 close} +forcedelete test.db +sqlite3 db test.db +set abc [string repeat abc 256] +set def [string repeat def 256] +execsql " + CREATE TABLE t2(a PRIMARY KEY, b); + INSERT INTO t2 VALUES(1, '$abc'); +" +set changeset [changeset_from_sql " + INSERT INTO t2 VALUES(2, '$def'); + DELETE FROM t2 WHERE a = 1; +"] +db close + +do_faultsim_test 5.3 -faults oom-tra* -body { + set ::inverse [sqlite3changeset_invert $::changeset] + set {} {} +} -test { + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} + if {$testrc==0} { + set x [list] + sqlite3session_foreach c $::inverse { lappend x $c } + foreach c " + {INSERT t2 0 X. {} {i 1 t $::abc}} + {DELETE t2 0 X. {i 2 t $::def} {}} + " { lappend y $c } + if {$x != $y} { error "changeset no good" } + } +} + +#------------------------------------------------------------------------- +# Test that OOM errors in sqlite3changeset_concat() are handled correctly. +# +catch {db close} +forcedelete test.db +sqlite3 db test.db +do_execsql_test 5.prep1 { + CREATE TABLE t1(a, b, PRIMARY KEY(b)); + CREATE TABLE t2(a PRIMARY KEY, b); + INSERT INTO t1 VALUES('string', 1); + INSERT INTO t1 VALUES(4, 2); + INSERT INTO t1 VALUES(X'FFAAFFAAFFAA', 3); +} + +do_test 6.prep2 { + sqlite3session M db main + M attach * + set ::c2 [changeset_from_sql { + INSERT INTO t2 VALUES(randomblob(1000), randomblob(1000)); + INSERT INTO t2 VALUES('one', 'two'); + INSERT INTO t2 VALUES(1, NULL); + UPDATE t1 SET a = 5 WHERE a = 2; + }] + set ::c1 [changeset_from_sql { + DELETE FROM t2 WHERE a = 1; + UPDATE t1 SET a = 4 WHERE a = 2; + INSERT INTO t2 VALUES('x', 'y'); + }] + set ::total [changeset_to_list [M changeset]] + M delete +} {} + +do_faultsim_test 6 -faults oom-* -body { + set ::result [sqlite3changeset_concat $::c1 $::c2] + set {} {} +} -test { + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} + if {$testrc==0} { + set v [changeset_to_list $::result] + if {$v != $::total} { error "result no good" } + } +} + +faultsim_delete_and_reopen +do_execsql_test 5.prep1 { + CREATE TABLE t1(a, b, PRIMARY KEY(a)); +} +faultsim_save_and_close + +set res [list] +for {set ::i 0} {$::i < 480} {incr ::i 4} { + lappend res "INSERT t1 0 X. {} {i $::i i $::i}" +} +set res [lsort $res] +do_faultsim_test 7 -faults oom-transient -prep { + faultsim_restore_and_reopen + sqlite3session S db main + S attach * +} -body { + for {set ::i 0} {$::i < 480} {incr ::i 4} { + execsql {INSERT INTO t1 VALUES($::i, $::i)} + } +} -test { + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} + if {$testrc==0} { + set cres [list [catch {changeset_to_list [S changeset]} msg] $msg] + S delete + if {$cres != "1 SQLITE_NOMEM" && $cres != "0 {$::res}"} { + error "Expected {0 $::res} Got {$cres}" + } + } else { + S changeset + S delete + } +} + +faultsim_delete_and_reopen +do_test 8.prep { + sqlite3session S db main + S attach * + execsql { + CREATE TABLE t1(a, b, PRIMARY KEY(a)); + INSERT INTO t1 VALUES(1, 2); + INSERT INTO t1 VALUES(3, 4); + INSERT INTO t1 VALUES(5, 6); + } + set ::changeset [S changeset] + S delete +} {} + +set expected [normalize_list { + {INSERT t1 0 X. {} {i 1 i 2}} + {INSERT t1 0 X. {} {i 3 i 4}} + {INSERT t1 0 X. {} {i 5 i 6}} +}] +do_faultsim_test 8.1 -faults oom* -body { + set ::res [list] + sqlite3session_foreach -next v $::changeset { lappend ::res $v } + normalize_list $::res +} -test { + faultsim_test_result [list 0 $::expected] {1 SQLITE_NOMEM} +} +do_faultsim_test 8.2 -faults oom* -body { + set ::res [list] + sqlite3session_foreach v $::changeset { lappend ::res $v } + normalize_list $::res +} -test { + faultsim_test_result [list 0 $::expected] {1 SQLITE_NOMEM} +} + +faultsim_delete_and_reopen +do_test 9.1.prep { + execsql { + PRAGMA encoding = 'utf16'; + CREATE TABLE t1(a PRIMARY KEY, b); + } +} {} +faultsim_save_and_close + +set answers [list {0 {}} {1 SQLITE_NOMEM} \ + {1 {callback requested query abort}} \ + {1 {abort due to ROLLBACK}}] +do_faultsim_test 9.1 -faults oom-transient -prep { + catch { unset ::c } + faultsim_restore_and_reopen + sqlite3session S db main + S attach * +} -body { + execsql { + INSERT INTO t1 VALUES('abcdefghijklmnopqrstuv', 'ABCDEFGHIJKLMNOPQRSTUV'); + } + set ::c [S changeset] + set {} {} +} -test { + S delete + eval faultsim_test_result $::answers + if {[info exists ::c]} { + set expected [normalize_list { + {INSERT t1 0 X. {} {t abcdefghijklmnopqrstuv t ABCDEFGHIJKLMNOPQRSTUV}} + }] + if { [changeset_to_list $::c] != $expected } { + error "changeset mismatch" + } + } +} + +faultsim_delete_and_reopen +do_test 9.2.prep { + execsql { + PRAGMA encoding = 'utf16'; + CREATE TABLE t1(a PRIMARY KEY, b); + INSERT INTO t1 VALUES('abcdefghij', 'ABCDEFGHIJKLMNOPQRSTUV'); + } +} {} +faultsim_save_and_close + +set answers [list {0 {}} {1 SQLITE_NOMEM} \ + {1 {callback requested query abort}} \ + {1 {abort due to ROLLBACK}}] +do_faultsim_test 9.2 -faults oom-transient -prep { + catch { unset ::c } + faultsim_restore_and_reopen + sqlite3session S db main + S attach * +} -body { + execsql { + UPDATE t1 SET b = 'xyz'; + } + set ::c [S changeset] + set {} {} +} -test { + S delete + eval faultsim_test_result $::answers + if {[info exists ::c]} { + set expected [normalize_list { + {UPDATE t1 0 X. {t abcdefghij t ABCDEFGHIJKLMNOPQRSTUV} {{} {} t xyz}} + }] + if { [changeset_to_list $::c] != $expected } { + error "changeset mismatch" + } + } +} + +#------------------------------------------------------------------------- +# Test that if a conflict-handler encounters an OOM in +# sqlite3_value_text() but goes on to return SQLITE_CHANGESET_REPLACE +# anyway, the OOM is picked up by the sessions module. +set bigstr [string repeat abcdefghij 100] +faultsim_delete_and_reopen +do_test 10.prep.1 { + execsql { + CREATE TABLE t1(a PRIMARY KEY, b); + INSERT INTO t1 VALUES($bigstr, $bigstr); + } + + sqlite3session S db main + S attach * + execsql { UPDATE t1 SET b = b||'x' } + set C [S changeset] + S delete + execsql { UPDATE t1 SET b = b||'xyz' } +} {} +faultsim_save_and_close + +faultsim_restore_and_reopen +do_test 10.prep.2 { + proc xConflict {args} { return "ABORT" } + list [catch { sqlite3changeset_apply db $C xConflict } msg] $msg +} {1 SQLITE_ABORT} +do_execsql_test 10.prep.3 { SELECT b=$bigstr||'x' FROM t1 } 0 +do_test 10.prep.4 { + proc xConflict {args} { return "REPLACE" } + list [catch { sqlite3changeset_apply db $C xConflict } msg] $msg +} {0 {}} +do_execsql_test 10.prep.5 { SELECT b=$bigstr||'x' FROM t1 } 1 +db close + +do_faultsim_test 10 -faults oom-tra* -prep { + faultsim_restore_and_reopen +} -body { + sqlite3changeset_apply_replace_all db $::C +} -test { + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} + if {$testrc==0} { + if {[db one {SELECT b=$bigstr||'x' FROM t1}]==0} { + error "data does not look right" + } + } +} + +#------------------------------------------------------------------------- +# Test an OOM with an sqlite3changeset_apply() filter callback. +# +reset_db +do_test 11.prep { + execsql { + CREATE TABLE t1(a PRIMARY KEY, b); + CREATE TABLE t2(x PRIMARY KEY, y); + BEGIN; + } + + set ::cs [changeset_from_sql { + INSERT INTO t1 VALUES(1, 2); + INSERT INTO t2 VALUES('x', 'y'); + }] + + execsql ROLLBACK + set {} {} +} {} + +proc filter {x} { return [string equal t1 $x] } +faultsim_save_and_close + +do_faultsim_test 11 -faults oom-tra* -prep { + faultsim_restore_and_reopen +} -body { + sqlite3changeset_apply db $::cs {} filter +} -test { + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} + if {$testrc==0} { + if {[db eval {SELECT * FROM t1 UNION ALL SELECT * FROM t2}] != "1 2"} { + error "data does not look right" + } + } +} + + +finish_test diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c new file mode 100644 index 000000000..f861bd1ec --- /dev/null +++ b/ext/session/sqlite3session.c @@ -0,0 +1,4022 @@ + +#if defined(SQLITE_ENABLE_SESSION) && defined(SQLITE_ENABLE_PREUPDATE_HOOK) +#include "sqlite3session.h" +#include <assert.h> +#include <string.h> + +#ifndef SQLITE_AMALGAMATION +# include "sqliteInt.h" +# include "vdbeInt.h" +#endif + +typedef struct SessionTable SessionTable; +typedef struct SessionChange SessionChange; +typedef struct SessionBuffer SessionBuffer; +typedef struct SessionInput SessionInput; + +/* +** Minimum chunk size used by streaming versions of functions. +*/ +#ifndef SESSIONS_STRM_CHUNK_SIZE +# ifdef SQLITE_TEST +# define SESSIONS_STRM_CHUNK_SIZE 64 +# else +# define SESSIONS_STRM_CHUNK_SIZE 1024 +# endif +#endif + +/* +** Session handle structure. +*/ +struct sqlite3_session { + sqlite3 *db; /* Database handle session is attached to */ + char *zDb; /* Name of database session is attached to */ + int bEnable; /* True if currently recording */ + int bIndirect; /* True if all changes are indirect */ + int bAutoAttach; /* True to auto-attach tables */ + int rc; /* Non-zero if an error has occurred */ + void *pFilterCtx; /* First argument to pass to xTableFilter */ + int (*xTableFilter)(void *pCtx, const char *zTab); + sqlite3_session *pNext; /* Next session object on same db. */ + SessionTable *pTable; /* List of attached tables */ +}; + +/* +** Instances of this structure are used to build strings or binary records. +*/ +struct SessionBuffer { + u8 *aBuf; /* Pointer to changeset buffer */ + int nBuf; /* Size of buffer aBuf */ + int nAlloc; /* Size of allocation containing aBuf */ +}; + +/* +** An object of this type is used internally as an abstraction for +** input data. Input data may be supplied either as a single large buffer +** (e.g. sqlite3changeset_start()) or using a stream function (e.g. +** sqlite3changeset_start_strm()). +*/ +struct SessionInput { + int iNext; /* Offset in aData[] of next change */ + u8 *aData; /* Pointer to buffer containing changeset */ + int nData; /* Number of bytes in aData */ + + SessionBuffer buf; /* Current read buffer */ + int (*xInput)(void*, void*, int*); /* Input stream call (or NULL) */ + void *pIn; /* First argument to xInput */ + int bEof; /* Set to true after xInput finished */ +}; + +/* +** Structure for changeset iterators. +*/ +struct sqlite3_changeset_iter { + SessionInput in; /* Input buffer or stream */ + SessionBuffer tblhdr; /* Buffer to hold apValue/zTab/abPK/ */ + int bPatchset; /* True if this is a patchset */ + int rc; /* Iterator error code */ + sqlite3_stmt *pConflict; /* Points to conflicting row, if any */ + char *zTab; /* Current table */ + int nCol; /* Number of columns in zTab */ + int op; /* Current operation */ + int bIndirect; /* True if current change was indirect */ + u8 *abPK; /* Primary key array */ + sqlite3_value **apValue; /* old.* and new.* values */ +}; + +/* +** Each session object maintains a set of the following structures, one +** for each table the session object is monitoring. The structures are +** stored in a linked list starting at sqlite3_session.pTable. +** +** The keys of the SessionTable.aChange[] hash table are all rows that have +** been modified in any way since the session object was attached to the +** table. +** +** The data associated with each hash-table entry is a structure containing +** a subset of the initial values that the modified row contained at the +** start of the session. Or no initial values if the row was inserted. +*/ +struct SessionTable { + SessionTable *pNext; + char *zName; /* Local name of table */ + int nCol; /* Number of columns in table zName */ + const char **azCol; /* Column names */ + u8 *abPK; /* Array of primary key flags */ + int nEntry; /* Total number of entries in hash table */ + int nChange; /* Size of apChange[] array */ + SessionChange **apChange; /* Hash table buckets */ +}; + +/* +** RECORD FORMAT: +** +** The following record format is similar to (but not compatible with) that +** used in SQLite database files. This format is used as part of the +** change-set binary format, and so must be architecture independent. +** +** Unlike the SQLite database record format, each field is self-contained - +** there is no separation of header and data. Each field begins with a +** single byte describing its type, as follows: +** +** 0x00: Undefined value. +** 0x01: Integer value. +** 0x02: Real value. +** 0x03: Text value. +** 0x04: Blob value. +** 0x05: SQL NULL value. +** +** Note that the above match the definitions of SQLITE_INTEGER, SQLITE_TEXT +** and so on in sqlite3.h. For undefined and NULL values, the field consists +** only of the single type byte. For other types of values, the type byte +** is followed by: +** +** Text values: +** A varint containing the number of bytes in the value (encoded using +** UTF-8). Followed by a buffer containing the UTF-8 representation +** of the text value. There is no nul terminator. +** +** Blob values: +** A varint containing the number of bytes in the value, followed by +** a buffer containing the value itself. +** +** Integer values: +** An 8-byte big-endian integer value. +** +** Real values: +** An 8-byte big-endian IEEE 754-2008 real value. +** +** Varint values are encoded in the same way as varints in the SQLite +** record format. +** +** CHANGESET FORMAT: +** +** A changeset is a collection of DELETE, UPDATE and INSERT operations on +** one or more tables. Operations on a single table are grouped together, +** but may occur in any order (i.e. deletes, updates and inserts are all +** mixed together). +** +** Each group of changes begins with a table header: +** +** 1 byte: Constant 0x54 (capital 'T') +** Varint: Big-endian integer set to the number of columns in the table. +** nCol bytes: 0x01 for PK columns, 0x00 otherwise. +** N bytes: Unqualified table name (encoded using UTF-8). Nul-terminated. +** +** Followed by one or more changes to the table. +** +** 1 byte: Either SQLITE_INSERT, UPDATE or DELETE. +** 1 byte: The "indirect-change" flag. +** old.* record: (delete and update only) +** new.* record: (insert and update only) +** +** PATCHSET FORMAT: +** +** A patchset is also a collection of changes. It is similar to a changeset, +** but omits those fields that are not useful if no conflict resolution +** is required when applying the changeset. +** +** Each group of changes begins with a table header: +** +** 1 byte: Constant 0x50 (capital 'P') +** Varint: Big-endian integer set to the number of columns in the table. +** nCol bytes: 0x01 for PK columns, 0x00 otherwise. +** N bytes: Unqualified table name (encoded using UTF-8). Nul-terminated. +** +** Followed by one or more changes to the table. +** +** 1 byte: Either SQLITE_INSERT, UPDATE or DELETE. +** 1 byte: The "indirect-change" flag. +** single record: (PK fields for DELETE, or full record for INSERT/UPDATE). +*/ + +/* +** For each row modified during a session, there exists a single instance of +** this structure stored in a SessionTable.aChange[] hash table. +*/ +struct SessionChange { + int op; /* One of UPDATE, DELETE, INSERT */ + int bIndirect; /* True if this change is "indirect" */ + int nRecord; /* Number of bytes in buffer aRecord[] */ + u8 *aRecord; /* Buffer containing old.* record */ + SessionChange *pNext; /* For hash-table collisions */ +}; + +/* +** Write a varint with value iVal into the buffer at aBuf. Return the +** number of bytes written. +*/ +static int sessionVarintPut(u8 *aBuf, int iVal){ + return putVarint32(aBuf, iVal); +} + +/* +** Return the number of bytes required to store value iVal as a varint. +*/ +static int sessionVarintLen(int iVal){ + return sqlite3VarintLen(iVal); +} + +/* +** Read a varint value from aBuf[] into *piVal. Return the number of +** bytes read. +*/ +static int sessionVarintGet(u8 *aBuf, int *piVal){ + return getVarint32(aBuf, *piVal); +} + +/* +** Read a 64-bit big-endian integer value from buffer aRec[]. Return +** the value read. +*/ +static sqlite3_int64 sessionGetI64(u8 *aRec){ + return (((sqlite3_int64)aRec[0]) << 56) + + (((sqlite3_int64)aRec[1]) << 48) + + (((sqlite3_int64)aRec[2]) << 40) + + (((sqlite3_int64)aRec[3]) << 32) + + (((sqlite3_int64)aRec[4]) << 24) + + (((sqlite3_int64)aRec[5]) << 16) + + (((sqlite3_int64)aRec[6]) << 8) + + (((sqlite3_int64)aRec[7]) << 0); +} + +/* +** Write a 64-bit big-endian integer value to the buffer aBuf[]. +*/ +static void sessionPutI64(u8 *aBuf, sqlite3_int64 i){ + aBuf[0] = (i>>56) & 0xFF; + aBuf[1] = (i>>48) & 0xFF; + aBuf[2] = (i>>40) & 0xFF; + aBuf[3] = (i>>32) & 0xFF; + aBuf[4] = (i>>24) & 0xFF; + aBuf[5] = (i>>16) & 0xFF; + aBuf[6] = (i>> 8) & 0xFF; + aBuf[7] = (i>> 0) & 0xFF; +} + +/* +** This function is used to serialize the contents of value pValue (see +** comment titled "RECORD FORMAT" above). +** +** If it is non-NULL, the serialized form of the value is written to +** buffer aBuf. *pnWrite is set to the number of bytes written before +** returning. Or, if aBuf is NULL, the only thing this function does is +** set *pnWrite. +** +** If no error occurs, SQLITE_OK is returned. Or, if an OOM error occurs +** within a call to sqlite3_value_text() (may fail if the db is utf-16)) +** SQLITE_NOMEM is returned. +*/ +static int sessionSerializeValue( + u8 *aBuf, /* If non-NULL, write serialized value here */ + sqlite3_value *pValue, /* Value to serialize */ + int *pnWrite /* IN/OUT: Increment by bytes written */ +){ + int nByte; /* Size of serialized value in bytes */ + + if( pValue ){ + int eType; /* Value type (SQLITE_NULL, TEXT etc.) */ + + eType = sqlite3_value_type(pValue); + if( aBuf ) aBuf[0] = eType; + + switch( eType ){ + case SQLITE_NULL: + nByte = 1; + break; + + case SQLITE_INTEGER: + case SQLITE_FLOAT: + if( aBuf ){ + /* TODO: SQLite does something special to deal with mixed-endian + ** floating point values (e.g. ARM7). This code probably should + ** too. */ + u64 i; + if( eType==SQLITE_INTEGER ){ + i = (u64)sqlite3_value_int64(pValue); + }else{ + double r; + assert( sizeof(double)==8 && sizeof(u64)==8 ); + r = sqlite3_value_double(pValue); + memcpy(&i, &r, 8); + } + sessionPutI64(&aBuf[1], i); + } + nByte = 9; + break; + + default: { + u8 *z; + int n; + int nVarint; + + assert( eType==SQLITE_TEXT || eType==SQLITE_BLOB ); + if( eType==SQLITE_TEXT ){ + z = (u8 *)sqlite3_value_text(pValue); + }else{ + z = (u8 *)sqlite3_value_blob(pValue); + } + if( z==0 ) return SQLITE_NOMEM; + n = sqlite3_value_bytes(pValue); + nVarint = sessionVarintLen(n); + + if( aBuf ){ + sessionVarintPut(&aBuf[1], n); + memcpy(&aBuf[nVarint + 1], eType==SQLITE_TEXT ? + sqlite3_value_text(pValue) : sqlite3_value_blob(pValue), n + ); + } + + nByte = 1 + nVarint + n; + break; + } + } + }else{ + nByte = 1; + if( aBuf ) aBuf[0] = '\0'; + } + + if( pnWrite ) *pnWrite += nByte; + return SQLITE_OK; +} + + +/* +** This macro is used to calculate hash key values for data structures. In +** order to use this macro, the entire data structure must be represented +** as a series of unsigned integers. In order to calculate a hash-key value +** for a data structure represented as three such integers, the macro may +** then be used as follows: +** +** int hash_key_value; +** hash_key_value = HASH_APPEND(0, <value 1>); +** hash_key_value = HASH_APPEND(hash_key_value, <value 2>); +** hash_key_value = HASH_APPEND(hash_key_value, <value 3>); +** +** In practice, the data structures this macro is used for are the primary +** key values of modified rows. +*/ +#define HASH_APPEND(hash, add) ((hash) << 3) ^ (hash) ^ (unsigned int)(add) + +/* +** Append the hash of the 64-bit integer passed as the second argument to the +** hash-key value passed as the first. Return the new hash-key value. +*/ +static unsigned int sessionHashAppendI64(unsigned int h, i64 i){ + h = HASH_APPEND(h, i & 0xFFFFFFFF); + return HASH_APPEND(h, (i>>32)&0xFFFFFFFF); +} + +/* +** Append the hash of the blob passed via the second and third arguments to +** the hash-key value passed as the first. Return the new hash-key value. +*/ +static unsigned int sessionHashAppendBlob(unsigned int h, int n, const u8 *z){ + int i; + for(i=0; i<n; i++) h = HASH_APPEND(h, z[i]); + return h; +} + +/* +** Append the hash of the data type passed as the second argument to the +** hash-key value passed as the first. Return the new hash-key value. +*/ +static unsigned int sessionHashAppendType(unsigned int h, int eType){ + return HASH_APPEND(h, eType); +} + +/* +** This function may only be called from within a pre-update callback. +** It calculates a hash based on the primary key values of the old.* or +** new.* row currently available and, assuming no error occurs, writes it to +** *piHash before returning. If the primary key contains one or more NULL +** values, *pbNullPK is set to true before returning. +** +** If an error occurs, an SQLite error code is returned and the final values +** of *piHash asn *pbNullPK are undefined. Otherwise, SQLITE_OK is returned +** and the output variables are set as described above. +*/ +static int sessionPreupdateHash( + sqlite3 *db, /* Database handle */ + SessionTable *pTab, /* Session table handle */ + int bNew, /* True to hash the new.* PK */ + int *piHash, /* OUT: Hash value */ + int *pbNullPK /* OUT: True if there are NULL values in PK */ +){ + unsigned int h = 0; /* Hash value to return */ + int i; /* Used to iterate through columns */ + + assert( *pbNullPK==0 ); + assert( pTab->nCol==sqlite3_preupdate_count(db) ); + for(i=0; i<pTab->nCol; i++){ + if( pTab->abPK[i] ){ + int rc; + int eType; + sqlite3_value *pVal; + + if( bNew ){ + rc = sqlite3_preupdate_new(db, i, &pVal); + }else{ + rc = sqlite3_preupdate_old(db, i, &pVal); + } + if( rc!=SQLITE_OK ) return rc; + + eType = sqlite3_value_type(pVal); + h = sessionHashAppendType(h, eType); + if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){ + i64 iVal; + if( eType==SQLITE_INTEGER ){ + iVal = sqlite3_value_int64(pVal); + }else{ + double rVal = sqlite3_value_double(pVal); + assert( sizeof(iVal)==8 && sizeof(rVal)==8 ); + memcpy(&iVal, &rVal, 8); + } + h = sessionHashAppendI64(h, iVal); + }else if( eType==SQLITE_TEXT || eType==SQLITE_BLOB ){ + const u8 *z; + if( eType==SQLITE_TEXT ){ + z = (const u8 *)sqlite3_value_text(pVal); + }else{ + z = (const u8 *)sqlite3_value_blob(pVal); + } + if( !z ) return SQLITE_NOMEM; + h = sessionHashAppendBlob(h, sqlite3_value_bytes(pVal), z); + }else{ + assert( eType==SQLITE_NULL ); + *pbNullPK = 1; + } + } + } + + *piHash = (h % pTab->nChange); + return SQLITE_OK; +} + +/* +** The buffer that the argument points to contains a serialized SQL value. +** Return the number of bytes of space occupied by the value (including +** the type byte). +*/ +static int sessionSerialLen(u8 *a){ + int e = *a; + int n; + if( e==0 ) return 1; + if( e==SQLITE_NULL ) return 1; + if( e==SQLITE_INTEGER || e==SQLITE_FLOAT ) return 9; + return sessionVarintGet(&a[1], &n) + 1 + n; +} + +/* +** Based on the primary key values stored in change aRecord, calculate a +** hash key. Assume the has table has nBucket buckets. The hash keys +** calculated by this function are compatible with those calculated by +** sessionPreupdateHash(). +** +** The bPkOnly argument is non-zero if the record at aRecord[] is from +** a patchset DELETE. In this case the non-PK fields are omitted entirely. +*/ +static unsigned int sessionChangeHash( + SessionTable *pTab, /* Table handle */ + int bPkOnly, /* Record consists of PK fields only */ + u8 *aRecord, /* Change record */ + int nBucket /* Assume this many buckets in hash table */ +){ + unsigned int h = 0; /* Value to return */ + int i; /* Used to iterate through columns */ + u8 *a = aRecord; /* Used to iterate through change record */ + + for(i=0; i<pTab->nCol; i++){ + int eType = *a; + int isPK = pTab->abPK[i]; + if( bPkOnly && isPK==0 ) continue; + + /* It is not possible for eType to be SQLITE_NULL here. The session + ** module does not record changes for rows with NULL values stored in + ** primary key columns. */ + assert( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT + || eType==SQLITE_TEXT || eType==SQLITE_BLOB + || eType==SQLITE_NULL || eType==0 + ); + assert( !isPK || (eType!=0 && eType!=SQLITE_NULL) ); + + if( isPK ){ + a++; + h = sessionHashAppendType(h, eType); + if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){ + h = sessionHashAppendI64(h, sessionGetI64(a)); + a += 8; + }else{ + int n; + a += sessionVarintGet(a, &n); + h = sessionHashAppendBlob(h, n, a); + a += n; + } + }else{ + a += sessionSerialLen(a); + } + } + return (h % nBucket); +} + +/* +** Arguments aLeft and aRight are pointers to change records for table pTab. +** This function returns true if the two records apply to the same row (i.e. +** have the same values stored in the primary key columns), or false +** otherwise. +*/ +static int sessionChangeEqual( + SessionTable *pTab, /* Table used for PK definition */ + int bLeftPkOnly, /* True if aLeft[] contains PK fields only */ + u8 *aLeft, /* Change record */ + int bRightPkOnly, /* True if aRight[] contains PK fields only */ + u8 *aRight /* Change record */ +){ + u8 *a1 = aLeft; /* Cursor to iterate through aLeft */ + u8 *a2 = aRight; /* Cursor to iterate through aRight */ + int iCol; /* Used to iterate through table columns */ + + for(iCol=0; iCol<pTab->nCol; iCol++){ + int n1 = sessionSerialLen(a1); + int n2 = sessionSerialLen(a2); + + if( pTab->abPK[iCol] && (n1!=n2 || memcmp(a1, a2, n1)) ){ + return 0; + } + if( pTab->abPK[iCol] || bLeftPkOnly==0 ) a1 += n1; + if( pTab->abPK[iCol] || bRightPkOnly==0 ) a2 += n2; + } + + return 1; +} + +/* +** Arguments aLeft and aRight both point to buffers containing change +** records with nCol columns. This function "merges" the two records into +** a single records which is written to the buffer at *paOut. *paOut is +** then set to point to one byte after the last byte written before +** returning. +** +** The merging of records is done as follows: For each column, if the +** aRight record contains a value for the column, copy the value from +** their. Otherwise, if aLeft contains a value, copy it. If neither +** record contains a value for a given column, then neither does the +** output record. +*/ +static void sessionMergeRecord( + u8 **paOut, + int nCol, + u8 *aLeft, + u8 *aRight +){ + u8 *a1 = aLeft; /* Cursor used to iterate through aLeft */ + u8 *a2 = aRight; /* Cursor used to iterate through aRight */ + u8 *aOut = *paOut; /* Output cursor */ + int iCol; /* Used to iterate from 0 to nCol */ + + for(iCol=0; iCol<nCol; iCol++){ + int n1 = sessionSerialLen(a1); + int n2 = sessionSerialLen(a2); + if( *a2 ){ + memcpy(aOut, a2, n2); + aOut += n2; + }else{ + memcpy(aOut, a1, n1); + aOut += n1; + } + a1 += n1; + a2 += n2; + } + + *paOut = aOut; +} + +/* +** This is a helper function used by sessionMergeUpdate(). +** +** When this function is called, both *paOne and *paTwo point to a value +** within a change record. Before it returns, both have been advanced so +** as to point to the next value in the record. +** +** If, when this function is called, *paTwo points to a valid value (i.e. +** *paTwo[0] is not 0x00 - the "no value" placeholder), a copy of the *paTwo +** pointer is returned and *pnVal is set to the number of bytes in the +** serialized value. Otherwise, a copy of *paOne is returned and *pnVal +** set to the number of bytes in the value at *paOne. If *paOne points +** to the "no value" placeholder, *pnVal is set to 1. In other words: +** +** if( *paTwo is valid ) return *paTwo; +** return *paOne; +** +*/ +static u8 *sessionMergeValue( + u8 **paOne, /* IN/OUT: Left-hand buffer pointer */ + u8 **paTwo, /* IN/OUT: Right-hand buffer pointer */ + int *pnVal /* OUT: Bytes in returned value */ +){ + u8 *a1 = *paOne; + u8 *a2 = *paTwo; + u8 *pRet = 0; + int n1; + + assert( a1 ); + if( a2 ){ + int n2 = sessionSerialLen(a2); + if( *a2 ){ + *pnVal = n2; + pRet = a2; + } + *paTwo = &a2[n2]; + } + + n1 = sessionSerialLen(a1); + if( pRet==0 ){ + *pnVal = n1; + pRet = a1; + } + *paOne = &a1[n1]; + + return pRet; +} + +/* +** This function is used by changeset_concat() to merge two UPDATE changes +** on the same row. +*/ +static int sessionMergeUpdate( + u8 **paOut, /* IN/OUT: Pointer to output buffer */ + SessionTable *pTab, /* Table change pertains to */ + int bPatchset, /* True if records are patchset records */ + u8 *aOldRecord1, /* old.* record for first change */ + u8 *aOldRecord2, /* old.* record for second change */ + u8 *aNewRecord1, /* new.* record for first change */ + u8 *aNewRecord2 /* new.* record for second change */ +){ + u8 *aOld1 = aOldRecord1; + u8 *aOld2 = aOldRecord2; + u8 *aNew1 = aNewRecord1; + u8 *aNew2 = aNewRecord2; + + u8 *aOut = *paOut; + int i; + + if( bPatchset==0 ){ + int bRequired = 0; + + assert( aOldRecord1 && aNewRecord1 ); + + /* Write the old.* vector first. */ + for(i=0; i<pTab->nCol; i++){ + int nOld; + u8 *aOld; + int nNew; + u8 *aNew; + + aOld = sessionMergeValue(&aOld1, &aOld2, &nOld); + aNew = sessionMergeValue(&aNew1, &aNew2, &nNew); + if( pTab->abPK[i] || nOld!=nNew || memcmp(aOld, aNew, nNew) ){ + if( pTab->abPK[i]==0 ) bRequired = 1; + memcpy(aOut, aOld, nOld); + aOut += nOld; + }else{ + *(aOut++) = '\0'; + } + } + + if( !bRequired ) return 0; + } + + /* Write the new.* vector */ + aOld1 = aOldRecord1; + aOld2 = aOldRecord2; + aNew1 = aNewRecord1; + aNew2 = aNewRecord2; + for(i=0; i<pTab->nCol; i++){ + int nOld; + u8 *aOld; + int nNew; + u8 *aNew; + + aOld = sessionMergeValue(&aOld1, &aOld2, &nOld); + aNew = sessionMergeValue(&aNew1, &aNew2, &nNew); + if( bPatchset==0 + && (pTab->abPK[i] || (nOld==nNew && 0==memcmp(aOld, aNew, nNew))) + ){ + *(aOut++) = '\0'; + }else{ + memcpy(aOut, aNew, nNew); + aOut += nNew; + } + } + + *paOut = aOut; + return 1; +} + +/* +** This function is only called from within a pre-update-hook callback. +** It determines if the current pre-update-hook change affects the same row +** as the change stored in argument pChange. If so, it returns true. Otherwise +** if the pre-update-hook does not affect the same row as pChange, it returns +** false. +*/ +static int sessionPreupdateEqual( + sqlite3 *db, /* Database handle */ + SessionTable *pTab, /* Table associated with change */ + SessionChange *pChange, /* Change to compare to */ + int op /* Current pre-update operation */ +){ + int iCol; /* Used to iterate through columns */ + u8 *a = pChange->aRecord; /* Cursor used to scan change record */ + + assert( op==SQLITE_INSERT || op==SQLITE_UPDATE || op==SQLITE_DELETE ); + for(iCol=0; iCol<pTab->nCol; iCol++){ + if( !pTab->abPK[iCol] ){ + a += sessionSerialLen(a); + }else{ + sqlite3_value *pVal; /* Value returned by preupdate_new/old */ + int rc; /* Error code from preupdate_new/old */ + int eType = *a++; /* Type of value from change record */ + + /* The following calls to preupdate_new() and preupdate_old() can not + ** fail. This is because they cache their return values, and by the + ** time control flows to here they have already been called once from + ** within sessionPreupdateHash(). The first two asserts below verify + ** this (that the method has already been called). */ + if( op==SQLITE_INSERT ){ + assert( db->pPreUpdate->pNewUnpacked || db->pPreUpdate->aNew ); + rc = sqlite3_preupdate_new(db, iCol, &pVal); + }else{ + assert( db->pPreUpdate->pUnpacked ); + rc = sqlite3_preupdate_old(db, iCol, &pVal); + } + assert( rc==SQLITE_OK ); + if( sqlite3_value_type(pVal)!=eType ) return 0; + + /* A SessionChange object never has a NULL value in a PK column */ + assert( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT + || eType==SQLITE_BLOB || eType==SQLITE_TEXT + ); + + if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){ + i64 iVal = sessionGetI64(a); + a += 8; + if( eType==SQLITE_INTEGER ){ + if( sqlite3_value_int64(pVal)!=iVal ) return 0; + }else{ + double rVal; + assert( sizeof(iVal)==8 && sizeof(rVal)==8 ); + memcpy(&rVal, &iVal, 8); + if( sqlite3_value_double(pVal)!=rVal ) return 0; + } + }else{ + int n; + const u8 *z; + a += sessionVarintGet(a, &n); + if( sqlite3_value_bytes(pVal)!=n ) return 0; + if( eType==SQLITE_TEXT ){ + z = sqlite3_value_text(pVal); + }else{ + z = sqlite3_value_blob(pVal); + } + if( memcmp(a, z, n) ) return 0; + a += n; + break; + } + } + } + + return 1; +} + +/* +** If required, grow the hash table used to store changes on table pTab +** (part of the session pSession). If a fatal OOM error occurs, set the +** session object to failed and return SQLITE_ERROR. Otherwise, return +** SQLITE_OK. +** +** It is possible that a non-fatal OOM error occurs in this function. In +** that case the hash-table does not grow, but SQLITE_OK is returned anyway. +** Growing the hash table in this case is a performance optimization only, +** it is not required for correct operation. +*/ +static int sessionGrowHash(int bPatchset, SessionTable *pTab){ + if( pTab->nChange==0 || pTab->nEntry>=(pTab->nChange/2) ){ + int i; + SessionChange **apNew; + int nNew = (pTab->nChange ? pTab->nChange : 128) * 2; + + apNew = (SessionChange **)sqlite3_malloc(sizeof(SessionChange *) * nNew); + if( apNew==0 ){ + if( pTab->nChange==0 ){ + return SQLITE_ERROR; + } + return SQLITE_OK; + } + memset(apNew, 0, sizeof(SessionChange *) * nNew); + + for(i=0; i<pTab->nChange; i++){ + SessionChange *p; + SessionChange *pNext; + for(p=pTab->apChange[i]; p; p=pNext){ + int bPkOnly = (p->op==SQLITE_DELETE && bPatchset); + int iHash = sessionChangeHash(pTab, bPkOnly, p->aRecord, nNew); + pNext = p->pNext; + p->pNext = apNew[iHash]; + apNew[iHash] = p; + } + } + + sqlite3_free(pTab->apChange); + pTab->nChange = nNew; + pTab->apChange = apNew; + } + + return SQLITE_OK; +} + +/* +** This function queries the database for the names of the columns of table +** zThis, in schema zDb. It is expected that the table has nCol columns. If +** not, SQLITE_SCHEMA is returned and none of the output variables are +** populated. +** +** Otherwise, if they are not NULL, variable *pnCol is set to the number +** of columns in the database table and variable *pzTab is set to point to a +** nul-terminated copy of the table name. *pazCol (if not NULL) is set to +** point to an array of pointers to column names. And *pabPK (again, if not +** NULL) is set to point to an array of booleans - true if the corresponding +** column is part of the primary key. +** +** For example, if the table is declared as: +** +** CREATE TABLE tbl1(w, x, y, z, PRIMARY KEY(w, z)); +** +** Then the four output variables are populated as follows: +** +** *pnCol = 4 +** *pzTab = "tbl1" +** *pazCol = {"w", "x", "y", "z"} +** *pabPK = {1, 0, 0, 1} +** +** All returned buffers are part of the same single allocation, which must +** be freed using sqlite3_free() by the caller. If pazCol was not NULL, then +** pointer *pazCol should be freed to release all memory. Otherwise, pointer +** *pabPK. It is illegal for both pazCol and pabPK to be NULL. +*/ +static int sessionTableInfo( + sqlite3 *db, /* Database connection */ + const char *zDb, /* Name of attached database (e.g. "main") */ + const char *zThis, /* Table name */ + int *pnCol, /* OUT: number of columns */ + const char **pzTab, /* OUT: Copy of zThis */ + const char ***pazCol, /* OUT: Array of column names for table */ + u8 **pabPK /* OUT: Array of booleans - true for PK col */ +){ + char *zPragma; + sqlite3_stmt *pStmt; + int rc; + int nByte; + int nDbCol = 0; + int nThis; + int i; + u8 *pAlloc; + char **azCol = 0; + u8 *abPK; + + assert( pazCol && pabPK ); + + nThis = sqlite3Strlen30(zThis); + zPragma = sqlite3_mprintf("PRAGMA '%q'.table_info('%q')", zDb, zThis); + if( !zPragma ) return SQLITE_NOMEM; + + rc = sqlite3_prepare_v2(db, zPragma, -1, &pStmt, 0); + sqlite3_free(zPragma); + if( rc!=SQLITE_OK ) return rc; + + nByte = nThis + 1; + while( SQLITE_ROW==sqlite3_step(pStmt) ){ + nByte += sqlite3_column_bytes(pStmt, 1); + nDbCol++; + } + rc = sqlite3_reset(pStmt); + + if( rc==SQLITE_OK ){ + nByte += nDbCol * (sizeof(const char *) + sizeof(u8) + 1); + pAlloc = sqlite3_malloc(nByte); + if( pAlloc==0 ){ + rc = SQLITE_NOMEM; + } + } + if( rc==SQLITE_OK ){ + azCol = (char **)pAlloc; + pAlloc = (u8 *)&azCol[nDbCol]; + abPK = (u8 *)pAlloc; + pAlloc = &abPK[nDbCol]; + if( pzTab ){ + memcpy(pAlloc, zThis, nThis+1); + *pzTab = (char *)pAlloc; + pAlloc += nThis+1; + } + + i = 0; + while( SQLITE_ROW==sqlite3_step(pStmt) ){ + int nName = sqlite3_column_bytes(pStmt, 1); + const unsigned char *zName = sqlite3_column_text(pStmt, 1); + if( zName==0 ) break; + memcpy(pAlloc, zName, nName+1); + azCol[i] = (char *)pAlloc; + pAlloc += nName+1; + abPK[i] = sqlite3_column_int(pStmt, 5); + i++; + } + rc = sqlite3_reset(pStmt); + + } + + /* If successful, populate the output variables. Otherwise, zero them and + ** free any allocation made. An error code will be returned in this case. + */ + if( rc==SQLITE_OK ){ + *pazCol = (const char **)azCol; + *pabPK = abPK; + *pnCol = nDbCol; + }else{ + *pazCol = 0; + *pabPK = 0; + *pnCol = 0; + if( pzTab ) *pzTab = 0; + sqlite3_free(azCol); + } + sqlite3_finalize(pStmt); + return rc; +} + +/* +** This function is only called from within a pre-update handler for a +** write to table pTab, part of session pSession. If this is the first +** write to this table, set the SessionTable.nCol variable to the number +** of columns in the table. +** +** Otherwise, if this is not the first time this table has been written +** to, check that the number of columns in the table has not changed. If +** it has not, return zero. +** +** If the number of columns in the table has changed since the last write +** was recorded, set the session error-code to SQLITE_SCHEMA and return +** non-zero. Users are not allowed to change the number of columns in a table +** for which changes are being recorded by the session module. If they do so, +** it is an error. +*/ +static int sessionInitTable(sqlite3_session *pSession, SessionTable *pTab){ + if( pTab->nCol==0 ){ + assert( pTab->azCol==0 || pTab->abPK==0 ); + pSession->rc = sessionTableInfo(pSession->db, pSession->zDb, + pTab->zName, &pTab->nCol, 0, &pTab->azCol, &pTab->abPK + ); + } + if( pSession->rc==SQLITE_OK + && pTab->nCol!=sqlite3_preupdate_count(pSession->db) + ){ + pSession->rc = SQLITE_SCHEMA; + } + return pSession->rc; +} + +/* +** This function is only called from with a pre-update-hook reporting a +** change on table pTab (attached to session pSession). The type of change +** (UPDATE, INSERT, DELETE) is specified by the first argument. +** +** Unless one is already present or an error occurs, an entry is added +** to the changed-rows hash table associated with table pTab. +*/ +static void sessionPreupdateOneChange( + int op, /* One of SQLITE_UPDATE, INSERT, DELETE */ + sqlite3_session *pSession, /* Session object pTab is attached to */ + SessionTable *pTab /* Table that change applies to */ +){ + sqlite3 *db = pSession->db; + int iHash; + int bNullPk = 0; + int rc = SQLITE_OK; + + if( pSession->rc ) return; + + /* Load table details if required */ + if( sessionInitTable(pSession, pTab) ) return; + + /* Grow the hash table if required */ + if( sessionGrowHash(0, pTab) ){ + pSession->rc = SQLITE_NOMEM; + return; + } + + /* Calculate the hash-key for this change. If the primary key of the row + ** includes a NULL value, exit early. Such changes are ignored by the + ** session module. */ + rc = sessionPreupdateHash(db, pTab, op==SQLITE_INSERT, &iHash, &bNullPk); + if( rc!=SQLITE_OK ) goto error_out; + + if( bNullPk==0 ){ + /* Search the hash table for an existing record for this row. */ + SessionChange *pC; + for(pC=pTab->apChange[iHash]; pC; pC=pC->pNext){ + if( sessionPreupdateEqual(db, pTab, pC, op) ) break; + } + + if( pC==0 ){ + /* Create a new change object containing all the old values (if + ** this is an SQLITE_UPDATE or SQLITE_DELETE), or just the PK + ** values (if this is an INSERT). */ + SessionChange *pChange; /* New change object */ + int nByte; /* Number of bytes to allocate */ + int i; /* Used to iterate through columns */ + + assert( rc==SQLITE_OK ); + pTab->nEntry++; + + /* Figure out how large an allocation is required */ + nByte = sizeof(SessionChange); + for(i=0; i<pTab->nCol; i++){ + sqlite3_value *p = 0; + if( op!=SQLITE_INSERT ){ + TESTONLY(int trc = ) sqlite3_preupdate_old(pSession->db, i, &p); + assert( trc==SQLITE_OK ); + }else if( pTab->abPK[i] ){ + TESTONLY(int trc = ) sqlite3_preupdate_new(pSession->db, i, &p); + assert( trc==SQLITE_OK ); + } + + /* This may fail if SQLite value p contains a utf-16 string that must + ** be converted to utf-8 and an OOM error occurs while doing so. */ + rc = sessionSerializeValue(0, p, &nByte); + if( rc!=SQLITE_OK ) goto error_out; + } + + /* Allocate the change object */ + pChange = (SessionChange *)sqlite3_malloc(nByte); + if( !pChange ){ + rc = SQLITE_NOMEM; + goto error_out; + }else{ + memset(pChange, 0, sizeof(SessionChange)); + pChange->aRecord = (u8 *)&pChange[1]; + } + + /* Populate the change object. None of the preupdate_old(), + ** preupdate_new() or SerializeValue() calls below may fail as all + ** required values and encodings have already been cached in memory. + ** It is not possible for an OOM to occur in this block. */ + nByte = 0; + for(i=0; i<pTab->nCol; i++){ + sqlite3_value *p = 0; + if( op!=SQLITE_INSERT ){ + sqlite3_preupdate_old(pSession->db, i, &p); + }else if( pTab->abPK[i] ){ + sqlite3_preupdate_new(pSession->db, i, &p); + } + sessionSerializeValue(&pChange->aRecord[nByte], p, &nByte); + } + + /* Add the change to the hash-table */ + if( pSession->bIndirect || sqlite3_preupdate_depth(pSession->db) ){ + pChange->bIndirect = 1; + } + pChange->nRecord = nByte; + pChange->op = op; + pChange->pNext = pTab->apChange[iHash]; + pTab->apChange[iHash] = pChange; + + }else if( pC->bIndirect ){ + /* If the existing change is considered "indirect", but this current + ** change is "direct", mark the change object as direct. */ + if( sqlite3_preupdate_depth(pSession->db)==0 && pSession->bIndirect==0 ){ + pC->bIndirect = 0; + } + } + } + + /* If an error has occurred, mark the session object as failed. */ + error_out: + if( rc!=SQLITE_OK ){ + pSession->rc = rc; + } +} + +/* +** The 'pre-update' hook registered by this module with SQLite databases. +*/ +static void xPreUpdate( + void *pCtx, /* Copy of third arg to preupdate_hook() */ + sqlite3 *db, /* Database handle */ + int op, /* SQLITE_UPDATE, DELETE or INSERT */ + char const *zDb, /* Database name */ + char const *zName, /* Table name */ + sqlite3_int64 iKey1, /* Rowid of row about to be deleted/updated */ + sqlite3_int64 iKey2 /* New rowid value (for a rowid UPDATE) */ +){ + sqlite3_session *pSession; + int nDb = sqlite3Strlen30(zDb); + int nName = sqlite3Strlen30(zName); + + assert( sqlite3_mutex_held(db->mutex) ); + + for(pSession=(sqlite3_session *)pCtx; pSession; pSession=pSession->pNext){ + SessionTable *pTab; + + /* If this session is attached to a different database ("main", "temp" + ** etc.), or if it is not currently enabled, there is nothing to do. Skip + ** to the next session object attached to this database. */ + if( pSession->bEnable==0 ) continue; + if( pSession->rc ) continue; + if( sqlite3_strnicmp(zDb, pSession->zDb, nDb+1) ) continue; + + for(pTab=pSession->pTable; pTab || pSession->bAutoAttach; pTab=pTab->pNext){ + if( !pTab ){ + /* This branch is taken if table zName has not yet been attached to + ** this session and the auto-attach flag is set. */ + + /* If there is a table-filter configured, invoke it. If it returns 0, + ** this change will not be recorded. Break out of the loop early in + ** this case. */ + if( pSession->xTableFilter + && pSession->xTableFilter(pSession->pFilterCtx, zName)==0 + ){ + break; + } + + pSession->rc = sqlite3session_attach(pSession,zName); + if( pSession->rc ) break; + pTab = pSession->pTable; + assert( 0==sqlite3_strnicmp(pTab->zName, zName, nName+1) ); + } + + if( 0==sqlite3_strnicmp(pTab->zName, zName, nName+1) ){ + sessionPreupdateOneChange(op, pSession, pTab); + if( op==SQLITE_UPDATE ){ + sessionPreupdateOneChange(SQLITE_INSERT, pSession, pTab); + } + break; + } + } + } +} + +/* +** Create a session object. This session object will record changes to +** database zDb attached to connection db. +*/ +int sqlite3session_create( + sqlite3 *db, /* Database handle */ + const char *zDb, /* Name of db (e.g. "main") */ + sqlite3_session **ppSession /* OUT: New session object */ +){ + sqlite3_session *pNew; /* Newly allocated session object */ + sqlite3_session *pOld; /* Session object already attached to db */ + int nDb = sqlite3Strlen30(zDb); /* Length of zDb in bytes */ + + /* Zero the output value in case an error occurs. */ + *ppSession = 0; + + /* Allocate and populate the new session object. */ + pNew = (sqlite3_session *)sqlite3_malloc(sizeof(sqlite3_session) + nDb + 1); + if( !pNew ) return SQLITE_NOMEM; + memset(pNew, 0, sizeof(sqlite3_session)); + pNew->db = db; + pNew->zDb = (char *)&pNew[1]; + pNew->bEnable = 1; + memcpy(pNew->zDb, zDb, nDb+1); + + /* Add the new session object to the linked list of session objects + ** attached to database handle $db. Do this under the cover of the db + ** handle mutex. */ + sqlite3_mutex_enter(sqlite3_db_mutex(db)); + pOld = (sqlite3_session*)sqlite3_preupdate_hook(db, xPreUpdate, (void*)pNew); + pNew->pNext = pOld; + sqlite3_mutex_leave(sqlite3_db_mutex(db)); + + *ppSession = pNew; + return SQLITE_OK; +} + +/* +** Free the list of table objects passed as the first argument. The contents +** of the changed-rows hash tables are also deleted. +*/ +static void sessionDeleteTable(SessionTable *pList){ + SessionTable *pNext; + SessionTable *pTab; + + for(pTab=pList; pTab; pTab=pNext){ + int i; + pNext = pTab->pNext; + for(i=0; i<pTab->nChange; i++){ + SessionChange *p; + SessionChange *pNext; + for(p=pTab->apChange[i]; p; p=pNext){ + pNext = p->pNext; + sqlite3_free(p); + } + } + sqlite3_free((char*)pTab->azCol); /* cast works around VC++ bug */ + sqlite3_free(pTab->apChange); + sqlite3_free(pTab); + } +} + +/* +** Delete a session object previously allocated using sqlite3session_create(). +*/ +void sqlite3session_delete(sqlite3_session *pSession){ + sqlite3 *db = pSession->db; + sqlite3_session *pHead; + sqlite3_session **pp; + + /* Unlink the session from the linked list of sessions attached to the + ** database handle. Hold the db mutex while doing so. */ + sqlite3_mutex_enter(sqlite3_db_mutex(db)); + pHead = (sqlite3_session*)sqlite3_preupdate_hook(db, 0, 0); + for(pp=&pHead; (*pp)!=pSession; pp=&((*pp)->pNext)); + *pp = (*pp)->pNext; + if( pHead ) sqlite3_preupdate_hook(db, xPreUpdate, (void *)pHead); + sqlite3_mutex_leave(sqlite3_db_mutex(db)); + + /* Delete all attached table objects. And the contents of their + ** associated hash-tables. */ + sessionDeleteTable(pSession->pTable); + + /* Free the session object itself. */ + sqlite3_free(pSession); +} + +/* +** Set a table filter on a Session Object. +*/ +void sqlite3session_table_filter( + sqlite3_session *pSession, + int(*xFilter)(void*, const char*), + void *pCtx /* First argument passed to xFilter */ +){ + pSession->bAutoAttach = 1; + pSession->pFilterCtx = pCtx; + pSession->xTableFilter = xFilter; +} + +/* +** Attach a table to a session. All subsequent changes made to the table +** while the session object is enabled will be recorded. +** +** Only tables that have a PRIMARY KEY defined may be attached. It does +** not matter if the PRIMARY KEY is an "INTEGER PRIMARY KEY" (rowid alias) +** or not. +*/ +int sqlite3session_attach( + sqlite3_session *pSession, /* Session object */ + const char *zName /* Table name */ +){ + int rc = SQLITE_OK; + sqlite3_mutex_enter(sqlite3_db_mutex(pSession->db)); + + if( !zName ){ + pSession->bAutoAttach = 1; + }else{ + SessionTable *pTab; /* New table object (if required) */ + int nName; /* Number of bytes in string zName */ + + /* First search for an existing entry. If one is found, this call is + ** a no-op. Return early. */ + nName = sqlite3Strlen30(zName); + for(pTab=pSession->pTable; pTab; pTab=pTab->pNext){ + if( 0==sqlite3_strnicmp(pTab->zName, zName, nName+1) ) break; + } + + if( !pTab ){ + /* Allocate new SessionTable object. */ + pTab = (SessionTable *)sqlite3_malloc(sizeof(SessionTable) + nName + 1); + if( !pTab ){ + rc = SQLITE_NOMEM; + }else{ + /* Populate the new SessionTable object and link it into the list. */ + memset(pTab, 0, sizeof(SessionTable)); + pTab->zName = (char *)&pTab[1]; + memcpy(pTab->zName, zName, nName+1); + pTab->pNext = pSession->pTable; + pSession->pTable = pTab; + } + } + } + + sqlite3_mutex_leave(sqlite3_db_mutex(pSession->db)); + return rc; +} + +/* +** Ensure that there is room in the buffer to append nByte bytes of data. +** If not, use sqlite3_realloc() to grow the buffer so that there is. +** +** If successful, return zero. Otherwise, if an OOM condition is encountered, +** set *pRc to SQLITE_NOMEM and return non-zero. +*/ +static int sessionBufferGrow(SessionBuffer *p, int nByte, int *pRc){ + if( *pRc==SQLITE_OK && p->nAlloc-p->nBuf<nByte ){ + u8 *aNew; + int nNew = p->nAlloc ? p->nAlloc : 128; + do { + nNew = nNew*2; + }while( nNew<(p->nBuf+nByte) ); + + aNew = (u8 *)sqlite3_realloc(p->aBuf, nNew); + if( 0==aNew ){ + *pRc = SQLITE_NOMEM; + }else{ + p->aBuf = aNew; + p->nAlloc = nNew; + } + } + return (*pRc!=SQLITE_OK); +} + +/* +** Append the value passed as the second argument to the buffer passed +** as the first. +** +** This function is a no-op if *pRc is non-zero when it is called. +** Otherwise, if an error occurs, *pRc is set to an SQLite error code +** before returning. +*/ +static void sessionAppendValue(SessionBuffer *p, sqlite3_value *pVal, int *pRc){ + int rc = *pRc; + if( rc==SQLITE_OK ){ + int nByte = 0; + rc = sessionSerializeValue(0, pVal, &nByte); + sessionBufferGrow(p, nByte, &rc); + if( rc==SQLITE_OK ){ + rc = sessionSerializeValue(&p->aBuf[p->nBuf], pVal, 0); + p->nBuf += nByte; + }else{ + *pRc = rc; + } + } +} + +/* +** This function is a no-op if *pRc is other than SQLITE_OK when it is +** called. Otherwise, append a single byte to the buffer. +** +** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before +** returning. +*/ +static void sessionAppendByte(SessionBuffer *p, u8 v, int *pRc){ + if( 0==sessionBufferGrow(p, 1, pRc) ){ + p->aBuf[p->nBuf++] = v; + } +} + +/* +** This function is a no-op if *pRc is other than SQLITE_OK when it is +** called. Otherwise, append a single varint to the buffer. +** +** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before +** returning. +*/ +static void sessionAppendVarint(SessionBuffer *p, int v, int *pRc){ + if( 0==sessionBufferGrow(p, 9, pRc) ){ + p->nBuf += sessionVarintPut(&p->aBuf[p->nBuf], v); + } +} + +/* +** This function is a no-op if *pRc is other than SQLITE_OK when it is +** called. Otherwise, append a blob of data to the buffer. +** +** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before +** returning. +*/ +static void sessionAppendBlob( + SessionBuffer *p, + const u8 *aBlob, + int nBlob, + int *pRc +){ + if( 0==sessionBufferGrow(p, nBlob, pRc) ){ + memcpy(&p->aBuf[p->nBuf], aBlob, nBlob); + p->nBuf += nBlob; + } +} + +/* +** This function is a no-op if *pRc is other than SQLITE_OK when it is +** called. Otherwise, append a string to the buffer. All bytes in the string +** up to (but not including) the nul-terminator are written to the buffer. +** +** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before +** returning. +*/ +static void sessionAppendStr( + SessionBuffer *p, + const char *zStr, + int *pRc +){ + int nStr = sqlite3Strlen30(zStr); + if( 0==sessionBufferGrow(p, nStr, pRc) ){ + memcpy(&p->aBuf[p->nBuf], zStr, nStr); + p->nBuf += nStr; + } +} + +/* +** This function is a no-op if *pRc is other than SQLITE_OK when it is +** called. Otherwise, append the string representation of integer iVal +** to the buffer. No nul-terminator is written. +** +** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before +** returning. +*/ +static void sessionAppendInteger( + SessionBuffer *p, /* Buffer to append to */ + int iVal, /* Value to write the string rep. of */ + int *pRc /* IN/OUT: Error code */ +){ + char aBuf[24]; + sqlite3_snprintf(sizeof(aBuf)-1, aBuf, "%d", iVal); + sessionAppendStr(p, aBuf, pRc); +} + +/* +** This function is a no-op if *pRc is other than SQLITE_OK when it is +** called. Otherwise, append the string zStr enclosed in quotes (") and +** with any embedded quote characters escaped to the buffer. No +** nul-terminator byte is written. +** +** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before +** returning. +*/ +static void sessionAppendIdent( + SessionBuffer *p, /* Buffer to a append to */ + const char *zStr, /* String to quote, escape and append */ + int *pRc /* IN/OUT: Error code */ +){ + int nStr = sqlite3Strlen30(zStr)*2 + 2 + 1; + if( 0==sessionBufferGrow(p, nStr, pRc) ){ + char *zOut = (char *)&p->aBuf[p->nBuf]; + const char *zIn = zStr; + *zOut++ = '"'; + while( *zIn ){ + if( *zIn=='"' ) *zOut++ = '"'; + *zOut++ = *(zIn++); + } + *zOut++ = '"'; + p->nBuf = (int)((u8 *)zOut - p->aBuf); + } +} + +/* +** This function is a no-op if *pRc is other than SQLITE_OK when it is +** called. Otherwse, it appends the serialized version of the value stored +** in column iCol of the row that SQL statement pStmt currently points +** to to the buffer. +*/ +static void sessionAppendCol( + SessionBuffer *p, /* Buffer to append to */ + sqlite3_stmt *pStmt, /* Handle pointing to row containing value */ + int iCol, /* Column to read value from */ + int *pRc /* IN/OUT: Error code */ +){ + if( *pRc==SQLITE_OK ){ + int eType = sqlite3_column_type(pStmt, iCol); + sessionAppendByte(p, (u8)eType, pRc); + if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){ + sqlite3_int64 i; + u8 aBuf[8]; + if( eType==SQLITE_INTEGER ){ + i = sqlite3_column_int64(pStmt, iCol); + }else{ + double r = sqlite3_column_double(pStmt, iCol); + memcpy(&i, &r, 8); + } + sessionPutI64(aBuf, i); + sessionAppendBlob(p, aBuf, 8, pRc); + } + if( eType==SQLITE_BLOB || eType==SQLITE_TEXT ){ + u8 *z; + if( eType==SQLITE_BLOB ){ + z = (u8 *)sqlite3_column_blob(pStmt, iCol); + }else{ + z = (u8 *)sqlite3_column_text(pStmt, iCol); + } + if( z ){ + int nByte = sqlite3_column_bytes(pStmt, iCol); + sessionAppendVarint(p, nByte, pRc); + sessionAppendBlob(p, z, nByte, pRc); + }else{ + *pRc = SQLITE_NOMEM; + } + } + } +} + +/* +** +** This function appends an update change to the buffer (see the comments +** under "CHANGESET FORMAT" at the top of the file). An update change +** consists of: +** +** 1 byte: SQLITE_UPDATE (0x17) +** n bytes: old.* record (see RECORD FORMAT) +** m bytes: new.* record (see RECORD FORMAT) +** +** The SessionChange object passed as the third argument contains the +** values that were stored in the row when the session began (the old.* +** values). The statement handle passed as the second argument points +** at the current version of the row (the new.* values). +** +** If all of the old.* values are equal to their corresponding new.* value +** (i.e. nothing has changed), then no data at all is appended to the buffer. +** +** Otherwise, the old.* record contains all primary key values and the +** original values of any fields that have been modified. The new.* record +** contains the new values of only those fields that have been modified. +*/ +static int sessionAppendUpdate( + SessionBuffer *pBuf, /* Buffer to append to */ + int bPatchset, /* True for "patchset", 0 for "changeset" */ + sqlite3_stmt *pStmt, /* Statement handle pointing at new row */ + SessionChange *p, /* Object containing old values */ + u8 *abPK /* Boolean array - true for PK columns */ +){ + int rc = SQLITE_OK; + SessionBuffer buf2 = {0,0,0}; /* Buffer to accumulate new.* record in */ + int bNoop = 1; /* Set to zero if any values are modified */ + int nRewind = pBuf->nBuf; /* Set to zero if any values are modified */ + int i; /* Used to iterate through columns */ + u8 *pCsr = p->aRecord; /* Used to iterate through old.* values */ + + sessionAppendByte(pBuf, SQLITE_UPDATE, &rc); + sessionAppendByte(pBuf, p->bIndirect, &rc); + for(i=0; i<sqlite3_column_count(pStmt); i++){ + int bChanged = 0; + int nAdvance; + int eType = *pCsr; + switch( eType ){ + case SQLITE_NULL: + nAdvance = 1; + if( sqlite3_column_type(pStmt, i)!=SQLITE_NULL ){ + bChanged = 1; + } + break; + + case SQLITE_FLOAT: + case SQLITE_INTEGER: { + nAdvance = 9; + if( eType==sqlite3_column_type(pStmt, i) ){ + sqlite3_int64 iVal = sessionGetI64(&pCsr[1]); + if( eType==SQLITE_INTEGER ){ + if( iVal==sqlite3_column_int64(pStmt, i) ) break; + }else{ + double dVal; + memcpy(&dVal, &iVal, 8); + if( dVal==sqlite3_column_double(pStmt, i) ) break; + } + } + bChanged = 1; + break; + } + + default: { + int nByte; + int nHdr = 1 + sessionVarintGet(&pCsr[1], &nByte); + assert( eType==SQLITE_TEXT || eType==SQLITE_BLOB ); + nAdvance = nHdr + nByte; + if( eType==sqlite3_column_type(pStmt, i) + && nByte==sqlite3_column_bytes(pStmt, i) + && 0==memcmp(&pCsr[nHdr], sqlite3_column_blob(pStmt, i), nByte) + ){ + break; + } + bChanged = 1; + } + } + + /* If at least one field has been modified, this is not a no-op. */ + if( bChanged ) bNoop = 0; + + /* Add a field to the old.* record. This is omitted if this modules is + ** currently generating a patchset. */ + if( bPatchset==0 ){ + if( bChanged || abPK[i] ){ + sessionAppendBlob(pBuf, pCsr, nAdvance, &rc); + }else{ + sessionAppendByte(pBuf, 0, &rc); + } + } + + /* Add a field to the new.* record. Or the only record if currently + ** generating a patchset. */ + if( bChanged || (bPatchset && abPK[i]) ){ + sessionAppendCol(&buf2, pStmt, i, &rc); + }else{ + sessionAppendByte(&buf2, 0, &rc); + } + + pCsr += nAdvance; + } + + if( bNoop ){ + pBuf->nBuf = nRewind; + }else{ + sessionAppendBlob(pBuf, buf2.aBuf, buf2.nBuf, &rc); + } + sqlite3_free(buf2.aBuf); + + return rc; +} + +/* +** Append a DELETE change to the buffer passed as the first argument. Use +** the changeset format if argument bPatchset is zero, or the patchset +** format otherwise. +*/ +static int sessionAppendDelete( + SessionBuffer *pBuf, /* Buffer to append to */ + int bPatchset, /* True for "patchset", 0 for "changeset" */ + SessionChange *p, /* Object containing old values */ + int nCol, /* Number of columns in table */ + u8 *abPK /* Boolean array - true for PK columns */ +){ + int rc = SQLITE_OK; + + sessionAppendByte(pBuf, SQLITE_DELETE, &rc); + sessionAppendByte(pBuf, p->bIndirect, &rc); + + if( bPatchset==0 ){ + sessionAppendBlob(pBuf, p->aRecord, p->nRecord, &rc); + }else{ + int i; + u8 *a = p->aRecord; + for(i=0; i<nCol; i++){ + u8 *pStart = a; + int eType = *a++; + + switch( eType ){ + case 0: + case SQLITE_NULL: + assert( abPK[i]==0 ); + break; + + case SQLITE_FLOAT: + case SQLITE_INTEGER: + a += 8; + break; + + default: { + int n; + a += sessionVarintGet(a, &n); + a += n; + break; + } + } + if( abPK[i] ){ + sessionAppendBlob(pBuf, pStart, (int)(a-pStart), &rc); + } + } + assert( (a - p->aRecord)==p->nRecord ); + } + + return rc; +} + +/* +** Formulate and prepare a SELECT statement to retrieve a row from table +** zTab in database zDb based on its primary key. i.e. +** +** SELECT * FROM zDb.zTab WHERE pk1 = ? AND pk2 = ? AND ... +*/ +static int sessionSelectStmt( + sqlite3 *db, /* Database handle */ + const char *zDb, /* Database name */ + const char *zTab, /* Table name */ + int nCol, /* Number of columns in table */ + const char **azCol, /* Names of table columns */ + u8 *abPK, /* PRIMARY KEY array */ + sqlite3_stmt **ppStmt /* OUT: Prepared SELECT statement */ +){ + int rc = SQLITE_OK; + int i; + const char *zSep = ""; + SessionBuffer buf = {0, 0, 0}; + + sessionAppendStr(&buf, "SELECT * FROM ", &rc); + sessionAppendIdent(&buf, zDb, &rc); + sessionAppendStr(&buf, ".", &rc); + sessionAppendIdent(&buf, zTab, &rc); + sessionAppendStr(&buf, " WHERE ", &rc); + for(i=0; i<nCol; i++){ + if( abPK[i] ){ + sessionAppendStr(&buf, zSep, &rc); + sessionAppendIdent(&buf, azCol[i], &rc); + sessionAppendStr(&buf, " = ?", &rc); + sessionAppendInteger(&buf, i+1, &rc); + zSep = " AND "; + } + } + if( rc==SQLITE_OK ){ + rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, ppStmt, 0); + } + sqlite3_free(buf.aBuf); + return rc; +} + +/* +** Bind the PRIMARY KEY values from the change passed in argument pChange +** to the SELECT statement passed as the first argument. The SELECT statement +** is as prepared by function sessionSelectStmt(). +** +** Return SQLITE_OK if all PK values are successfully bound, or an SQLite +** error code (e.g. SQLITE_NOMEM) otherwise. +*/ +static int sessionSelectBind( + sqlite3_stmt *pSelect, /* SELECT from sessionSelectStmt() */ + int nCol, /* Number of columns in table */ + u8 *abPK, /* PRIMARY KEY array */ + SessionChange *pChange /* Change structure */ +){ + int i; + int rc = SQLITE_OK; + u8 *a = pChange->aRecord; + + for(i=0; i<nCol && rc==SQLITE_OK; i++){ + int eType = *a++; + + switch( eType ){ + case 0: + case SQLITE_NULL: + assert( abPK[i]==0 ); + break; + + case SQLITE_INTEGER: { + if( abPK[i] ){ + i64 iVal = sessionGetI64(a); + rc = sqlite3_bind_int64(pSelect, i+1, iVal); + } + a += 8; + break; + } + + case SQLITE_FLOAT: { + if( abPK[i] ){ + double rVal; + i64 iVal = sessionGetI64(a); + memcpy(&rVal, &iVal, 8); + rc = sqlite3_bind_double(pSelect, i+1, rVal); + } + a += 8; + break; + } + + case SQLITE_TEXT: { + int n; + a += sessionVarintGet(a, &n); + if( abPK[i] ){ + rc = sqlite3_bind_text(pSelect, i+1, (char *)a, n, SQLITE_TRANSIENT); + } + a += n; + break; + } + + default: { + int n; + assert( eType==SQLITE_BLOB ); + a += sessionVarintGet(a, &n); + if( abPK[i] ){ + rc = sqlite3_bind_blob(pSelect, i+1, a, n, SQLITE_TRANSIENT); + } + a += n; + break; + } + } + } + + return rc; +} + +/* +** This function is a no-op if *pRc is set to other than SQLITE_OK when it +** is called. Otherwise, append a serialized table header (part of the binary +** changeset format) to buffer *pBuf. If an error occurs, set *pRc to an +** SQLite error code before returning. +*/ +static void sessionAppendTableHdr( + SessionBuffer *pBuf, /* Append header to this buffer */ + int bPatchset, /* Use the patchset format if true */ + SessionTable *pTab, /* Table object to append header for */ + int *pRc /* IN/OUT: Error code */ +){ + /* Write a table header */ + sessionAppendByte(pBuf, (bPatchset ? 'P' : 'T'), pRc); + sessionAppendVarint(pBuf, pTab->nCol, pRc); + sessionAppendBlob(pBuf, pTab->abPK, pTab->nCol, pRc); + sessionAppendBlob(pBuf, (u8 *)pTab->zName, (int)strlen(pTab->zName)+1, pRc); +} + +/* +** Generate either a changeset (if argument bPatchset is zero) or a patchset +** (if it is non-zero) based on the current contents of the session object +** passed as the first argument. +** +** If no error occurs, SQLITE_OK is returned and the new changeset/patchset +** stored in output variables *pnChangeset and *ppChangeset. Or, if an error +** occurs, an SQLite error code is returned and both output variables set +** to 0. +*/ +int sessionGenerateChangeset( + sqlite3_session *pSession, /* Session object */ + int bPatchset, /* True for patchset, false for changeset */ + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut, /* First argument for xOutput */ + int *pnChangeset, /* OUT: Size of buffer at *ppChangeset */ + void **ppChangeset /* OUT: Buffer containing changeset */ +){ + sqlite3 *db = pSession->db; /* Source database handle */ + SessionTable *pTab; /* Used to iterate through attached tables */ + SessionBuffer buf = {0,0,0}; /* Buffer in which to accumlate changeset */ + int rc; /* Return code */ + + assert( xOutput==0 || (pnChangeset==0 && ppChangeset==0 ) ); + + /* Zero the output variables in case an error occurs. If this session + ** object is already in the error state (sqlite3_session.rc != SQLITE_OK), + ** this call will be a no-op. */ + if( xOutput==0 ){ + *pnChangeset = 0; + *ppChangeset = 0; + } + + if( pSession->rc ) return pSession->rc; + rc = sqlite3_exec(pSession->db, "SAVEPOINT changeset", 0, 0, 0); + if( rc!=SQLITE_OK ) return rc; + + sqlite3_mutex_enter(sqlite3_db_mutex(db)); + + for(pTab=pSession->pTable; rc==SQLITE_OK && pTab; pTab=pTab->pNext){ + if( pTab->nEntry ){ + const char *zName = pTab->zName; + int nCol; /* Number of columns in table */ + u8 *abPK; /* Primary key array */ + const char **azCol = 0; /* Table columns */ + int i; /* Used to iterate through hash buckets */ + sqlite3_stmt *pSel = 0; /* SELECT statement to query table pTab */ + int nRewind = buf.nBuf; /* Initial size of write buffer */ + int nNoop; /* Size of buffer after writing tbl header */ + + /* Check the table schema is still Ok. */ + rc = sessionTableInfo(db, pSession->zDb, zName, &nCol, 0, &azCol, &abPK); + if( !rc && (pTab->nCol!=nCol || memcmp(abPK, pTab->abPK, nCol)) ){ + rc = SQLITE_SCHEMA; + } + + /* Write a table header */ + sessionAppendTableHdr(&buf, bPatchset, pTab, &rc); + + /* Build and compile a statement to execute: */ + if( rc==SQLITE_OK ){ + rc = sessionSelectStmt( + db, pSession->zDb, zName, nCol, azCol, abPK, &pSel); + } + + nNoop = buf.nBuf; + for(i=0; i<pTab->nChange && rc==SQLITE_OK; i++){ + SessionChange *p; /* Used to iterate through changes */ + + for(p=pTab->apChange[i]; rc==SQLITE_OK && p; p=p->pNext){ + rc = sessionSelectBind(pSel, nCol, abPK, p); + if( rc!=SQLITE_OK ) continue; + if( sqlite3_step(pSel)==SQLITE_ROW ){ + if( p->op==SQLITE_INSERT ){ + int iCol; + sessionAppendByte(&buf, SQLITE_INSERT, &rc); + sessionAppendByte(&buf, p->bIndirect, &rc); + for(iCol=0; iCol<nCol; iCol++){ + sessionAppendCol(&buf, pSel, iCol, &rc); + } + }else{ + rc = sessionAppendUpdate(&buf, bPatchset, pSel, p, abPK); + } + }else if( p->op!=SQLITE_INSERT ){ + rc = sessionAppendDelete(&buf, bPatchset, p, nCol, abPK); + } + if( rc==SQLITE_OK ){ + rc = sqlite3_reset(pSel); + } + + /* If the buffer is now larger than SESSIONS_STRM_CHUNK_SIZE, pass + ** its contents to the xOutput() callback. */ + if( xOutput + && rc==SQLITE_OK + && buf.nBuf>nNoop + && buf.nBuf>SESSIONS_STRM_CHUNK_SIZE + ){ + rc = xOutput(pOut, (void*)buf.aBuf, buf.nBuf); + nNoop = -1; + buf.nBuf = 0; + } + + } + } + + sqlite3_finalize(pSel); + if( buf.nBuf==nNoop ){ + buf.nBuf = nRewind; + } + sqlite3_free((char*)azCol); /* cast works around VC++ bug */ + } + } + + if( rc==SQLITE_OK ){ + if( xOutput==0 ){ + *pnChangeset = buf.nBuf; + *ppChangeset = buf.aBuf; + buf.aBuf = 0; + }else if( buf.nBuf>0 ){ + rc = xOutput(pOut, (void*)buf.aBuf, buf.nBuf); + } + } + + sqlite3_free(buf.aBuf); + sqlite3_exec(db, "RELEASE changeset", 0, 0, 0); + sqlite3_mutex_leave(sqlite3_db_mutex(db)); + return rc; +} + +/* +** Obtain a changeset object containing all changes recorded by the +** session object passed as the first argument. +** +** It is the responsibility of the caller to eventually free the buffer +** using sqlite3_free(). +*/ +int sqlite3session_changeset( + sqlite3_session *pSession, /* Session object */ + int *pnChangeset, /* OUT: Size of buffer at *ppChangeset */ + void **ppChangeset /* OUT: Buffer containing changeset */ +){ + return sessionGenerateChangeset(pSession, 0, 0, 0, pnChangeset, ppChangeset); +} + +/* +** Streaming version of sqlite3session_changeset(). +*/ +int sqlite3session_changeset_strm( + sqlite3_session *pSession, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +){ + return sessionGenerateChangeset(pSession, 0, xOutput, pOut, 0, 0); +} + +/* +** Streaming version of sqlite3session_patchset(). +*/ +int sqlite3session_patchset_strm( + sqlite3_session *pSession, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +){ + return sessionGenerateChangeset(pSession, 1, xOutput, pOut, 0, 0); +} + +/* +** Obtain a patchset object containing all changes recorded by the +** session object passed as the first argument. +** +** It is the responsibility of the caller to eventually free the buffer +** using sqlite3_free(). +*/ +int sqlite3session_patchset( + sqlite3_session *pSession, /* Session object */ + int *pnPatchset, /* OUT: Size of buffer at *ppChangeset */ + void **ppPatchset /* OUT: Buffer containing changeset */ +){ + return sessionGenerateChangeset(pSession, 1, 0, 0, pnPatchset, ppPatchset); +} + +/* +** Enable or disable the session object passed as the first argument. +*/ +int sqlite3session_enable(sqlite3_session *pSession, int bEnable){ + int ret; + sqlite3_mutex_enter(sqlite3_db_mutex(pSession->db)); + if( bEnable>=0 ){ + pSession->bEnable = bEnable; + } + ret = pSession->bEnable; + sqlite3_mutex_leave(sqlite3_db_mutex(pSession->db)); + return ret; +} + +/* +** Enable or disable the session object passed as the first argument. +*/ +int sqlite3session_indirect(sqlite3_session *pSession, int bIndirect){ + int ret; + sqlite3_mutex_enter(sqlite3_db_mutex(pSession->db)); + if( bIndirect>=0 ){ + pSession->bIndirect = bIndirect; + } + ret = pSession->bIndirect; + sqlite3_mutex_leave(sqlite3_db_mutex(pSession->db)); + return ret; +} + +/* +** Return true if there have been no changes to monitored tables recorded +** by the session object passed as the only argument. +*/ +int sqlite3session_isempty(sqlite3_session *pSession){ + int ret = 0; + SessionTable *pTab; + + sqlite3_mutex_enter(sqlite3_db_mutex(pSession->db)); + for(pTab=pSession->pTable; pTab && ret==0; pTab=pTab->pNext){ + ret = (pTab->nEntry>0); + } + sqlite3_mutex_leave(sqlite3_db_mutex(pSession->db)); + + return (ret==0); +} + +/* +** Do the work for either sqlite3changeset_start() or start_strm(). +*/ +int sessionChangesetStart( + sqlite3_changeset_iter **pp, /* OUT: Changeset iterator handle */ + int (*xInput)(void *pIn, void *pData, int *pnData), + void *pIn, + int nChangeset, /* Size of buffer pChangeset in bytes */ + void *pChangeset /* Pointer to buffer containing changeset */ +){ + sqlite3_changeset_iter *pRet; /* Iterator to return */ + int nByte; /* Number of bytes to allocate for iterator */ + + assert( xInput==0 || (pChangeset==0 && nChangeset==0) ); + + /* Zero the output variable in case an error occurs. */ + *pp = 0; + + /* Allocate and initialize the iterator structure. */ + nByte = sizeof(sqlite3_changeset_iter); + pRet = (sqlite3_changeset_iter *)sqlite3_malloc(nByte); + if( !pRet ) return SQLITE_NOMEM; + memset(pRet, 0, sizeof(sqlite3_changeset_iter)); + pRet->in.aData = (u8 *)pChangeset; + pRet->in.nData = nChangeset; + pRet->in.xInput = xInput; + pRet->in.pIn = pIn; + pRet->in.iNext = 0; + pRet->in.bEof = (xInput ? 0 : 1); + + /* Populate the output variable and return success. */ + *pp = pRet; + return SQLITE_OK; +} + +/* +** Create an iterator used to iterate through the contents of a changeset. +*/ +int sqlite3changeset_start( + sqlite3_changeset_iter **pp, /* OUT: Changeset iterator handle */ + int nChangeset, /* Size of buffer pChangeset in bytes */ + void *pChangeset /* Pointer to buffer containing changeset */ +){ + return sessionChangesetStart(pp, 0, 0, nChangeset, pChangeset); +} + +/* +** Streaming version of sqlite3changeset_start(). +*/ +int sqlite3changeset_start_strm( + sqlite3_changeset_iter **pp, /* OUT: Changeset iterator handle */ + int (*xInput)(void *pIn, void *pData, int *pnData), + void *pIn +){ + return sessionChangesetStart(pp, xInput, pIn, 0, 0); +} + +/* +** Ensure that there are at least nByte bytes available in the buffer. Or, +** if there are not nByte bytes remaining in the input, that all available +** data is in the buffer. +** +** Return an SQLite error code if an error occurs, or SQLITE_OK otherwise. +*/ +static int sessionInputBuffer(SessionInput *pIn, int nByte){ + int rc = SQLITE_OK; + if( pIn->xInput ){ + while( !pIn->bEof && (pIn->iNext+nByte)>=pIn->nData && rc==SQLITE_OK ){ + int nNew = SESSIONS_STRM_CHUNK_SIZE; + + if( pIn->iNext>=SESSIONS_STRM_CHUNK_SIZE ){ + int nMove = pIn->buf.nBuf - pIn->iNext; + memmove(pIn->buf.aBuf, &pIn->buf.aBuf[pIn->iNext], nMove); + pIn->buf.nBuf -= pIn->iNext; + pIn->iNext = 0; + } + + if( SQLITE_OK==sessionBufferGrow(&pIn->buf, nNew, &rc) ){ + rc = pIn->xInput(pIn->pIn, &pIn->buf.aBuf[pIn->buf.nBuf], &nNew); + if( nNew==0 ){ + pIn->bEof = 1; + }else{ + pIn->buf.nBuf += nNew; + } + } + + pIn->aData = pIn->buf.aBuf; + pIn->nData = pIn->buf.nBuf; + } + } + return rc; +} + +/* +** When this function is called, *ppRec points to the start of a record +** that contains nCol values. This function advances the pointer *ppRec +** until it points to the byte immediately following that record. +*/ +static void sessionSkipRecord( + u8 **ppRec, /* IN/OUT: Record pointer */ + int nCol /* Number of values in record */ +){ + u8 *aRec = *ppRec; + int i; + for(i=0; i<nCol; i++){ + int eType = *aRec++; + if( eType==SQLITE_TEXT || eType==SQLITE_BLOB ){ + int nByte; + aRec += sessionVarintGet((u8*)aRec, &nByte); + aRec += nByte; + }else if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){ + aRec += 8; + } + } + + *ppRec = aRec; +} + +/* +** This function sets the value of the sqlite3_value object passed as the +** first argument to a copy of the string or blob held in the aData[] +** buffer. SQLITE_OK is returned if successful, or SQLITE_NOMEM if an OOM +** error occurs. +*/ +static int sessionValueSetStr( + sqlite3_value *pVal, /* Set the value of this object */ + u8 *aData, /* Buffer containing string or blob data */ + int nData, /* Size of buffer aData[] in bytes */ + u8 enc /* String encoding (0 for blobs) */ +){ + /* In theory this code could just pass SQLITE_TRANSIENT as the final + ** argument to sqlite3ValueSetStr() and have the copy created + ** automatically. But doing so makes it difficult to detect any OOM + ** error. Hence the code to create the copy externally. */ + u8 *aCopy = sqlite3_malloc(nData); + if( aCopy==0 ) return SQLITE_NOMEM; + memcpy(aCopy, aData, nData); + sqlite3ValueSetStr(pVal, nData, (char*)aCopy, enc, sqlite3_free); + return SQLITE_OK; +} + +/* +** Deserialize a single record from a buffer in memory. See "RECORD FORMAT" +** for details. +** +** When this function is called, *paChange points to the start of the record +** to deserialize. Assuming no error occurs, *paChange is set to point to +** one byte after the end of the same record before this function returns. +** If the argument abPK is NULL, then the record contains nCol values. Or, +** if abPK is other than NULL, then the record contains only the PK fields +** (in other words, it is a patchset DELETE record). +** +** If successful, each element of the apOut[] array (allocated by the caller) +** is set to point to an sqlite3_value object containing the value read +** from the corresponding position in the record. If that value is not +** included in the record (i.e. because the record is part of an UPDATE change +** and the field was not modified), the corresponding element of apOut[] is +** set to NULL. +** +** It is the responsibility of the caller to free all sqlite_value structures +** using sqlite3_free(). +** +** If an error occurs, an SQLite error code (e.g. SQLITE_NOMEM) is returned. +** The apOut[] array may have been partially populated in this case. +*/ +static int sessionReadRecord( + SessionInput *pIn, /* Input data */ + int nCol, /* Number of values in record */ + u8 *abPK, /* Array of primary key flags, or NULL */ + sqlite3_value **apOut /* Write values to this array */ +){ + int i; /* Used to iterate through columns */ + int rc = SQLITE_OK; + + for(i=0; i<nCol && rc==SQLITE_OK; i++){ + int eType = 0; /* Type of value (SQLITE_NULL, TEXT etc.) */ + if( abPK && abPK[i]==0 ) continue; + rc = sessionInputBuffer(pIn, 9); + if( rc==SQLITE_OK ){ + eType = pIn->aData[pIn->iNext++]; + } + + assert( apOut[i]==0 ); + if( eType ){ + apOut[i] = sqlite3ValueNew(0); + if( !apOut[i] ) rc = SQLITE_NOMEM; + } + + if( rc==SQLITE_OK ){ + u8 *aVal = &pIn->aData[pIn->iNext]; + if( eType==SQLITE_TEXT || eType==SQLITE_BLOB ){ + int nByte; + pIn->iNext += sessionVarintGet(aVal, &nByte); + rc = sessionInputBuffer(pIn, nByte); + if( rc==SQLITE_OK ){ + u8 enc = (eType==SQLITE_TEXT ? SQLITE_UTF8 : 0); + rc = sessionValueSetStr(apOut[i],&pIn->aData[pIn->iNext],nByte,enc); + } + pIn->iNext += nByte; + } + if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){ + sqlite3_int64 v = sessionGetI64(aVal); + if( eType==SQLITE_INTEGER ){ + sqlite3VdbeMemSetInt64(apOut[i], v); + }else{ + double d; + memcpy(&d, &v, 8); + sqlite3VdbeMemSetDouble(apOut[i], d); + } + pIn->iNext += 8; + } + } + } + + return rc; +} + +/* +** The input pointer currently points to the second byte of a table-header. +** Specifically, to the following: +** +** + number of columns in table (varint) +** + array of PK flags (1 byte per column), +** + table name (nul terminated). +** +** This function ensures that all of the above is present in the input +** buffer (i.e. that it can be accessed without any calls to xInput()). +** If successful, SQLITE_OK is returned. Otherwise, an SQLite error code. +** The input pointer is not moved. +*/ +static int sessionChangesetBufferTblhdr(SessionInput *pIn, int *pnByte){ + int rc = SQLITE_OK; + int nCol = 0; + int nRead = 0; + + rc = sessionInputBuffer(pIn, 9); + if( rc==SQLITE_OK ){ + nRead += sessionVarintGet(&pIn->aData[pIn->iNext + nRead], &nCol); + rc = sessionInputBuffer(pIn, nRead+nCol+100); + nRead += nCol; + } + + while( rc==SQLITE_OK ){ + while( (pIn->iNext + nRead)<pIn->nData && pIn->aData[pIn->iNext + nRead] ){ + nRead++; + } + if( (pIn->iNext + nRead)<pIn->nData ) break; + rc = sessionInputBuffer(pIn, nRead + 100); + } + *pnByte = nRead+1; + return rc; +} + +/* +** The input pointer currently points to the first byte of the first field +** of a record consisting of nCol columns. This function ensures the entire +** record is buffered. It does not move the input pointer. +** +** If successful, SQLITE_OK is returned and *pnByte is set to the size of +** the record in bytes. Otherwise, an SQLite error code is returned. The +** final value of *pnByte is undefined in this case. +*/ +static int sessionChangesetBufferRecord( + SessionInput *pIn, /* Input data */ + int nCol, /* Number of columns in record */ + int *pnByte /* OUT: Size of record in bytes */ +){ + int rc = SQLITE_OK; + int nByte = 0; + int i; + for(i=0; rc==SQLITE_OK && i<nCol; i++){ + int eType; + rc = sessionInputBuffer(pIn, nByte + 10); + if( rc==SQLITE_OK ){ + eType = pIn->aData[pIn->iNext + nByte++]; + if( eType==SQLITE_TEXT || eType==SQLITE_BLOB ){ + int n; + nByte += sessionVarintGet(&pIn->aData[pIn->iNext+nByte], &n); + nByte += n; + rc = sessionInputBuffer(pIn, nByte); + }else if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){ + nByte += 8; + } + } + } + *pnByte = nByte; + return rc; +} + +/* +** The input pointer currently points to the second byte of a table-header. +** Specifically, to the following: +** +** + number of columns in table (varint) +** + array of PK flags (1 byte per column), +** + table name (nul terminated). +** +** This function decodes the table-header and populates the p->nCol, +** p->zTab and p->abPK[] variables accordingly. The p->apValue[] array is +** also allocated or resized according to the new value of p->nCol. The +** input pointer is left pointing to the byte following the table header. +** +** If successful, SQLITE_OK is returned. Otherwise, an SQLite error code +** is returned and the final values of the various fields enumerated above +** are undefined. +*/ +static int sessionChangesetReadTblhdr(sqlite3_changeset_iter *p){ + int rc; + int nCopy; + assert( p->rc==SQLITE_OK ); + + rc = sessionChangesetBufferTblhdr(&p->in, &nCopy); + if( rc==SQLITE_OK ){ + int nByte; + int nVarint; + nVarint = sessionVarintGet(&p->in.aData[p->in.iNext], &p->nCol); + nCopy -= nVarint; + p->in.iNext += nVarint; + nByte = p->nCol * sizeof(sqlite3_value*) * 2 + nCopy; + p->tblhdr.nBuf = 0; + sessionBufferGrow(&p->tblhdr, nByte, &rc); + } + + if( rc==SQLITE_OK ){ + int iPK = sizeof(sqlite3_value*)*p->nCol*2; + memset(p->tblhdr.aBuf, 0, iPK); + memcpy(&p->tblhdr.aBuf[iPK], &p->in.aData[p->in.iNext], nCopy); + p->in.iNext += nCopy; + } + + p->apValue = (sqlite3_value**)p->tblhdr.aBuf; + p->abPK = (u8*)&p->apValue[p->nCol*2]; + p->zTab = (char*)&p->abPK[p->nCol]; + return (p->rc = rc); +} + +/* +** Advance the changeset iterator to the next change. +** +** If both paRec and pnRec are NULL, then this function works like the public +** API sqlite3changeset_next(). If SQLITE_ROW is returned, then the +** sqlite3changeset_new() and old() APIs may be used to query for values. +** +** Otherwise, if paRec and pnRec are not NULL, then a pointer to the change +** record is written to *paRec before returning and the number of bytes in +** the record to *pnRec. +** +** Either way, this function returns SQLITE_ROW if the iterator is +** successfully advanced to the next change in the changeset, an SQLite +** error code if an error occurs, or SQLITE_DONE if there are no further +** changes in the changeset. +*/ +static int sessionChangesetNext( + sqlite3_changeset_iter *p, /* Changeset iterator */ + u8 **paRec, /* If non-NULL, store record pointer here */ + int *pnRec /* If non-NULL, store size of record here */ +){ + int i; + u8 op; + + assert( (paRec==0 && pnRec==0) || (paRec && pnRec) ); + + /* If the iterator is in the error-state, return immediately. */ + if( p->rc!=SQLITE_OK ) return p->rc; + + /* Free the current contents of p->apValue[], if any. */ + if( p->apValue ){ + for(i=0; i<p->nCol*2; i++){ + sqlite3ValueFree(p->apValue[i]); + } + memset(p->apValue, 0, sizeof(sqlite3_value*)*p->nCol*2); + } + + /* Make sure the buffer contains at least 10 bytes of input data, or all + ** remaining data if there are less than 10 bytes available. This is + ** sufficient either for the 'T' or 'P' byte and the varint that follows + ** it, or for the two single byte values otherwise. */ + p->rc = sessionInputBuffer(&p->in, 2); + if( p->rc!=SQLITE_OK ) return p->rc; + + /* If the iterator is already at the end of the changeset, return DONE. */ + if( p->in.iNext>=p->in.nData ){ + return SQLITE_DONE; + } + + op = p->in.aData[p->in.iNext++]; + if( op=='T' || op=='P' ){ + p->bPatchset = (op=='P'); + if( sessionChangesetReadTblhdr(p) ) return p->rc; + if( (p->rc = sessionInputBuffer(&p->in, 2)) ) return p->rc; + op = p->in.aData[p->in.iNext++]; + } + + p->op = op; + p->bIndirect = p->in.aData[p->in.iNext++]; + if( p->op!=SQLITE_UPDATE && p->op!=SQLITE_DELETE && p->op!=SQLITE_INSERT ){ + return (p->rc = SQLITE_CORRUPT_BKPT); + } + + if( paRec ){ + int nVal; /* Number of values to buffer */ + if( p->bPatchset==0 && op==SQLITE_UPDATE ){ + nVal = p->nCol * 2; + }else if( p->bPatchset && op==SQLITE_DELETE ){ + nVal = 0; + for(i=0; i<p->nCol; i++) if( p->abPK[i] ) nVal++; + }else{ + nVal = p->nCol; + } + p->rc = sessionChangesetBufferRecord(&p->in, nVal, pnRec); + if( p->rc!=SQLITE_OK ) return p->rc; + *paRec = &p->in.aData[p->in.iNext]; + p->in.iNext += *pnRec; + }else{ + + /* If this is an UPDATE or DELETE, read the old.* record. */ + if( p->op!=SQLITE_INSERT && (p->bPatchset==0 || p->op==SQLITE_DELETE) ){ + u8 *abPK = p->bPatchset ? p->abPK : 0; + p->rc = sessionReadRecord(&p->in, p->nCol, abPK, p->apValue); + if( p->rc!=SQLITE_OK ) return p->rc; + } + + /* If this is an INSERT or UPDATE, read the new.* record. */ + if( p->op!=SQLITE_DELETE ){ + p->rc = sessionReadRecord(&p->in, p->nCol, 0, &p->apValue[p->nCol]); + if( p->rc!=SQLITE_OK ) return p->rc; + } + + if( p->bPatchset && p->op==SQLITE_UPDATE ){ + /* If this is an UPDATE that is part of a patchset, then all PK and + ** modified fields are present in the new.* record. The old.* record + ** is currently completely empty. This block shifts the PK fields from + ** new.* to old.*, to accommodate the code that reads these arrays. */ + int i; + for(i=0; i<p->nCol; i++){ + assert( p->apValue[i]==0 ); + assert( p->abPK[i]==0 || p->apValue[i+p->nCol] ); + if( p->abPK[i] ){ + p->apValue[i] = p->apValue[i+p->nCol]; + p->apValue[i+p->nCol] = 0; + } + } + } + } + + return SQLITE_ROW; +} + +/* +** Advance an iterator created by sqlite3changeset_start() to the next +** change in the changeset. This function may return SQLITE_ROW, SQLITE_DONE +** or SQLITE_CORRUPT. +** +** This function may not be called on iterators passed to a conflict handler +** callback by changeset_apply(). +*/ +int sqlite3changeset_next(sqlite3_changeset_iter *p){ + return sessionChangesetNext(p, 0, 0); +} + +/* +** The following function extracts information on the current change +** from a changeset iterator. It may only be called after changeset_next() +** has returned SQLITE_ROW. +*/ +int sqlite3changeset_op( + sqlite3_changeset_iter *pIter, /* Iterator handle */ + const char **pzTab, /* OUT: Pointer to table name */ + int *pnCol, /* OUT: Number of columns in table */ + int *pOp, /* OUT: SQLITE_INSERT, DELETE or UPDATE */ + int *pbIndirect /* OUT: True if change is indirect */ +){ + *pOp = pIter->op; + *pnCol = pIter->nCol; + *pzTab = pIter->zTab; + if( pbIndirect ) *pbIndirect = pIter->bIndirect; + return SQLITE_OK; +} + +/* +** Return information regarding the PRIMARY KEY and number of columns in +** the database table affected by the change that pIter currently points +** to. This function may only be called after changeset_next() returns +** SQLITE_ROW. +*/ +int sqlite3changeset_pk( + sqlite3_changeset_iter *pIter, /* Iterator object */ + unsigned char **pabPK, /* OUT: Array of boolean - true for PK cols */ + int *pnCol /* OUT: Number of entries in output array */ +){ + *pabPK = pIter->abPK; + if( pnCol ) *pnCol = pIter->nCol; + return SQLITE_OK; +} + +/* +** This function may only be called while the iterator is pointing to an +** SQLITE_UPDATE or SQLITE_DELETE change (see sqlite3changeset_op()). +** Otherwise, SQLITE_MISUSE is returned. +** +** It sets *ppValue to point to an sqlite3_value structure containing the +** iVal'th value in the old.* record. Or, if that particular value is not +** included in the record (because the change is an UPDATE and the field +** was not modified and is not a PK column), set *ppValue to NULL. +** +** If value iVal is out-of-range, SQLITE_RANGE is returned and *ppValue is +** not modified. Otherwise, SQLITE_OK. +*/ +int sqlite3changeset_old( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int iVal, /* Index of old.* value to retrieve */ + sqlite3_value **ppValue /* OUT: Old value (or NULL pointer) */ +){ + if( pIter->op!=SQLITE_UPDATE && pIter->op!=SQLITE_DELETE ){ + return SQLITE_MISUSE; + } + if( iVal<0 || iVal>=pIter->nCol ){ + return SQLITE_RANGE; + } + *ppValue = pIter->apValue[iVal]; + return SQLITE_OK; +} + +/* +** This function may only be called while the iterator is pointing to an +** SQLITE_UPDATE or SQLITE_INSERT change (see sqlite3changeset_op()). +** Otherwise, SQLITE_MISUSE is returned. +** +** It sets *ppValue to point to an sqlite3_value structure containing the +** iVal'th value in the new.* record. Or, if that particular value is not +** included in the record (because the change is an UPDATE and the field +** was not modified), set *ppValue to NULL. +** +** If value iVal is out-of-range, SQLITE_RANGE is returned and *ppValue is +** not modified. Otherwise, SQLITE_OK. +*/ +int sqlite3changeset_new( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int iVal, /* Index of new.* value to retrieve */ + sqlite3_value **ppValue /* OUT: New value (or NULL pointer) */ +){ + if( pIter->op!=SQLITE_UPDATE && pIter->op!=SQLITE_INSERT ){ + return SQLITE_MISUSE; + } + if( iVal<0 || iVal>=pIter->nCol ){ + return SQLITE_RANGE; + } + *ppValue = pIter->apValue[pIter->nCol+iVal]; + return SQLITE_OK; +} + +/* +** The following two macros are used internally. They are similar to the +** sqlite3changeset_new() and sqlite3changeset_old() functions, except that +** they omit all error checking and return a pointer to the requested value. +*/ +#define sessionChangesetNew(pIter, iVal) (pIter)->apValue[(pIter)->nCol+(iVal)] +#define sessionChangesetOld(pIter, iVal) (pIter)->apValue[(iVal)] + +/* +** This function may only be called with a changeset iterator that has been +** passed to an SQLITE_CHANGESET_DATA or SQLITE_CHANGESET_CONFLICT +** conflict-handler function. Otherwise, SQLITE_MISUSE is returned. +** +** If successful, *ppValue is set to point to an sqlite3_value structure +** containing the iVal'th value of the conflicting record. +** +** If value iVal is out-of-range or some other error occurs, an SQLite error +** code is returned. Otherwise, SQLITE_OK. +*/ +int sqlite3changeset_conflict( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int iVal, /* Index of conflict record value to fetch */ + sqlite3_value **ppValue /* OUT: Value from conflicting row */ +){ + if( !pIter->pConflict ){ + return SQLITE_MISUSE; + } + if( iVal<0 || iVal>=sqlite3_column_count(pIter->pConflict) ){ + return SQLITE_RANGE; + } + *ppValue = sqlite3_column_value(pIter->pConflict, iVal); + return SQLITE_OK; +} + +/* +** This function may only be called with an iterator passed to an +** SQLITE_CHANGESET_FOREIGN_KEY conflict handler callback. In this case +** it sets the output variable to the total number of known foreign key +** violations in the destination database and returns SQLITE_OK. +** +** In all other cases this function returns SQLITE_MISUSE. +*/ +int sqlite3changeset_fk_conflicts( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int *pnOut /* OUT: Number of FK violations */ +){ + if( pIter->pConflict || pIter->apValue ){ + return SQLITE_MISUSE; + } + *pnOut = pIter->nCol; + return SQLITE_OK; +} + + +/* +** Finalize an iterator allocated with sqlite3changeset_start(). +** +** This function may not be called on iterators passed to a conflict handler +** callback by changeset_apply(). +*/ +int sqlite3changeset_finalize(sqlite3_changeset_iter *p){ + int rc = SQLITE_OK; + if( p ){ + int i; /* Used to iterate through p->apValue[] */ + rc = p->rc; + if( p->apValue ){ + for(i=0; i<p->nCol*2; i++) sqlite3ValueFree(p->apValue[i]); + } + sqlite3_free(p->tblhdr.aBuf); + sqlite3_free(p->in.buf.aBuf); + sqlite3_free(p); + } + return rc; +} + +static int sessionChangesetInvert( + SessionInput *pInput, /* Input changeset */ + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut, + int *pnInverted, /* OUT: Number of bytes in output changeset */ + void **ppInverted /* OUT: Inverse of pChangeset */ +){ + int rc = SQLITE_OK; /* Return value */ + SessionBuffer sOut; /* Output buffer */ + int nCol = 0; /* Number of cols in current table */ + u8 *abPK = 0; /* PK array for current table */ + sqlite3_value **apVal = 0; /* Space for values for UPDATE inversion */ + SessionBuffer sPK = {0, 0, 0}; /* PK array for current table */ + + /* Initialize the output buffer */ + memset(&sOut, 0, sizeof(SessionBuffer)); + + /* Zero the output variables in case an error occurs. */ + if( ppInverted ){ + *ppInverted = 0; + *pnInverted = 0; + } + + while( 1 ){ + u8 eType; + + /* Test for EOF. */ + if( (rc = sessionInputBuffer(pInput, 2)) ) goto finished_invert; + if( pInput->iNext>=pInput->nData ) break; + eType = pInput->aData[pInput->iNext]; + + switch( eType ){ + case 'T': { + /* A 'table' record consists of: + ** + ** * A constant 'T' character, + ** * Number of columns in said table (a varint), + ** * An array of nCol bytes (sPK), + ** * A nul-terminated table name. + */ + int nByte; + int nVar; + pInput->iNext++; + if( (rc = sessionChangesetBufferTblhdr(pInput, &nByte)) ){ + goto finished_invert; + } + nVar = sessionVarintGet(&pInput->aData[pInput->iNext], &nCol); + sPK.nBuf = 0; + sessionAppendBlob(&sPK, &pInput->aData[pInput->iNext+nVar], nCol, &rc); + sessionAppendByte(&sOut, eType, &rc); + sessionAppendBlob(&sOut, &pInput->aData[pInput->iNext], nByte, &rc); + if( rc ) goto finished_invert; + + pInput->iNext += nByte; + sqlite3_free(apVal); + apVal = 0; + abPK = sPK.aBuf; + break; + } + + case SQLITE_INSERT: + case SQLITE_DELETE: { + int nByte; + int bIndirect = pInput->aData[pInput->iNext+1]; + int eType2 = (eType==SQLITE_DELETE ? SQLITE_INSERT : SQLITE_DELETE); + pInput->iNext += 2; + assert( rc==SQLITE_OK ); + rc = sessionChangesetBufferRecord(pInput, nCol, &nByte); + sessionAppendByte(&sOut, eType2, &rc); + sessionAppendByte(&sOut, bIndirect, &rc); + sessionAppendBlob(&sOut, &pInput->aData[pInput->iNext], nByte, &rc); + pInput->iNext += nByte; + if( rc ) goto finished_invert; + break; + } + + case SQLITE_UPDATE: { + int iCol; + + if( 0==apVal ){ + apVal = (sqlite3_value **)sqlite3_malloc(sizeof(apVal[0])*nCol*2); + if( 0==apVal ){ + rc = SQLITE_NOMEM; + goto finished_invert; + } + memset(apVal, 0, sizeof(apVal[0])*nCol*2); + } + + /* Write the header for the new UPDATE change. Same as the original. */ + sessionAppendByte(&sOut, eType, &rc); + sessionAppendByte(&sOut, pInput->aData[pInput->iNext+1], &rc); + + /* Read the old.* and new.* records for the update change. */ + pInput->iNext += 2; + rc = sessionReadRecord(pInput, nCol, 0, &apVal[0]); + if( rc==SQLITE_OK ){ + rc = sessionReadRecord(pInput, nCol, 0, &apVal[nCol]); + } + + /* Write the new old.* record. Consists of the PK columns from the + ** original old.* record, and the other values from the original + ** new.* record. */ + for(iCol=0; iCol<nCol; iCol++){ + sqlite3_value *pVal = apVal[iCol + (abPK[iCol] ? 0 : nCol)]; + sessionAppendValue(&sOut, pVal, &rc); + } + + /* Write the new new.* record. Consists of a copy of all values + ** from the original old.* record, except for the PK columns, which + ** are set to "undefined". */ + for(iCol=0; iCol<nCol; iCol++){ + sqlite3_value *pVal = (abPK[iCol] ? 0 : apVal[iCol]); + sessionAppendValue(&sOut, pVal, &rc); + } + + for(iCol=0; iCol<nCol*2; iCol++){ + sqlite3ValueFree(apVal[iCol]); + } + memset(apVal, 0, sizeof(apVal[0])*nCol*2); + if( rc!=SQLITE_OK ){ + goto finished_invert; + } + + break; + } + + default: + rc = SQLITE_CORRUPT_BKPT; + goto finished_invert; + } + + assert( rc==SQLITE_OK ); + if( xOutput && sOut.nBuf>=SESSIONS_STRM_CHUNK_SIZE ){ + rc = xOutput(pOut, sOut.aBuf, sOut.nBuf); + sOut.nBuf = 0; + if( rc!=SQLITE_OK ) goto finished_invert; + } + } + + assert( rc==SQLITE_OK ); + if( pnInverted ){ + *pnInverted = sOut.nBuf; + *ppInverted = sOut.aBuf; + sOut.aBuf = 0; + }else if( sOut.nBuf>0 ){ + rc = xOutput(pOut, sOut.aBuf, sOut.nBuf); + } + + finished_invert: + sqlite3_free(sOut.aBuf); + sqlite3_free(apVal); + sqlite3_free(sPK.aBuf); + return rc; +} + + +/* +** Invert a changeset object. +*/ +int sqlite3changeset_invert( + int nChangeset, /* Number of bytes in input */ + const void *pChangeset, /* Input changeset */ + int *pnInverted, /* OUT: Number of bytes in output changeset */ + void **ppInverted /* OUT: Inverse of pChangeset */ +){ + SessionInput sInput; + + /* Set up the input stream */ + memset(&sInput, 0, sizeof(SessionInput)); + sInput.nData = nChangeset; + sInput.aData = (u8*)pChangeset; + + return sessionChangesetInvert(&sInput, 0, 0, pnInverted, ppInverted); +} + +/* +** Streaming version of sqlite3changeset_invert(). +*/ +int sqlite3changeset_invert_strm( + int (*xInput)(void *pIn, void *pData, int *pnData), + void *pIn, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +){ + SessionInput sInput; + int rc; + + /* Set up the input stream */ + memset(&sInput, 0, sizeof(SessionInput)); + sInput.xInput = xInput; + sInput.pIn = pIn; + + rc = sessionChangesetInvert(&sInput, xOutput, pOut, 0, 0); + sqlite3_free(sInput.buf.aBuf); + return rc; +} + +typedef struct SessionApplyCtx SessionApplyCtx; +struct SessionApplyCtx { + sqlite3 *db; + sqlite3_stmt *pDelete; /* DELETE statement */ + sqlite3_stmt *pUpdate; /* UPDATE statement */ + sqlite3_stmt *pInsert; /* INSERT statement */ + sqlite3_stmt *pSelect; /* SELECT statement */ + int nCol; /* Size of azCol[] and abPK[] arrays */ + const char **azCol; /* Array of column names */ + u8 *abPK; /* Boolean array - true if column is in PK */ +}; + +/* +** Formulate a statement to DELETE a row from database db. Assuming a table +** structure like this: +** +** CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c)); +** +** The DELETE statement looks like this: +** +** DELETE FROM x WHERE a = :1 AND c = :3 AND (:5 OR b IS :2 AND d IS :4) +** +** Variable :5 (nCol+1) is a boolean. It should be set to 0 if we require +** matching b and d values, or 1 otherwise. The second case comes up if the +** conflict handler is invoked with NOTFOUND and returns CHANGESET_REPLACE. +** +** If successful, SQLITE_OK is returned and SessionApplyCtx.pDelete is left +** pointing to the prepared version of the SQL statement. +*/ +static int sessionDeleteRow( + sqlite3 *db, /* Database handle */ + const char *zTab, /* Table name */ + SessionApplyCtx *p /* Session changeset-apply context */ +){ + int i; + const char *zSep = ""; + int rc = SQLITE_OK; + SessionBuffer buf = {0, 0, 0}; + int nPk = 0; + + sessionAppendStr(&buf, "DELETE FROM ", &rc); + sessionAppendIdent(&buf, zTab, &rc); + sessionAppendStr(&buf, " WHERE ", &rc); + + for(i=0; i<p->nCol; i++){ + if( p->abPK[i] ){ + nPk++; + sessionAppendStr(&buf, zSep, &rc); + sessionAppendIdent(&buf, p->azCol[i], &rc); + sessionAppendStr(&buf, " = ?", &rc); + sessionAppendInteger(&buf, i+1, &rc); + zSep = " AND "; + } + } + + if( nPk<p->nCol ){ + sessionAppendStr(&buf, " AND (?", &rc); + sessionAppendInteger(&buf, p->nCol+1, &rc); + sessionAppendStr(&buf, " OR ", &rc); + + zSep = ""; + for(i=0; i<p->nCol; i++){ + if( !p->abPK[i] ){ + sessionAppendStr(&buf, zSep, &rc); + sessionAppendIdent(&buf, p->azCol[i], &rc); + sessionAppendStr(&buf, " IS ?", &rc); + sessionAppendInteger(&buf, i+1, &rc); + zSep = "AND "; + } + } + sessionAppendStr(&buf, ")", &rc); + } + + if( rc==SQLITE_OK ){ + rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, &p->pDelete, 0); + } + sqlite3_free(buf.aBuf); + + return rc; +} + +/* +** Formulate and prepare a statement to UPDATE a row from database db. +** Assuming a table structure like this: +** +** CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c)); +** +** The UPDATE statement looks like this: +** +** UPDATE x SET +** a = CASE WHEN ?2 THEN ?3 ELSE a END, +** b = CASE WHEN ?5 THEN ?6 ELSE b END, +** c = CASE WHEN ?8 THEN ?9 ELSE c END, +** d = CASE WHEN ?11 THEN ?12 ELSE d END +** WHERE a = ?1 AND c = ?7 AND (?13 OR +** (?5==0 OR b IS ?4) AND (?11==0 OR d IS ?10) AND +** ) +** +** For each column in the table, there are three variables to bind: +** +** ?(i*3+1) The old.* value of the column, if any. +** ?(i*3+2) A boolean flag indicating that the value is being modified. +** ?(i*3+3) The new.* value of the column, if any. +** +** Also, a boolean flag that, if set to true, causes the statement to update +** a row even if the non-PK values do not match. This is required if the +** conflict-handler is invoked with CHANGESET_DATA and returns +** CHANGESET_REPLACE. This is variable "?(nCol*3+1)". +** +** If successful, SQLITE_OK is returned and SessionApplyCtx.pUpdate is left +** pointing to the prepared version of the SQL statement. +*/ +static int sessionUpdateRow( + sqlite3 *db, /* Database handle */ + const char *zTab, /* Table name */ + SessionApplyCtx *p /* Session changeset-apply context */ +){ + int rc = SQLITE_OK; + int i; + const char *zSep = ""; + SessionBuffer buf = {0, 0, 0}; + + /* Append "UPDATE tbl SET " */ + sessionAppendStr(&buf, "UPDATE ", &rc); + sessionAppendIdent(&buf, zTab, &rc); + sessionAppendStr(&buf, " SET ", &rc); + + /* Append the assignments */ + for(i=0; i<p->nCol; i++){ + sessionAppendStr(&buf, zSep, &rc); + sessionAppendIdent(&buf, p->azCol[i], &rc); + sessionAppendStr(&buf, " = CASE WHEN ?", &rc); + sessionAppendInteger(&buf, i*3+2, &rc); + sessionAppendStr(&buf, " THEN ?", &rc); + sessionAppendInteger(&buf, i*3+3, &rc); + sessionAppendStr(&buf, " ELSE ", &rc); + sessionAppendIdent(&buf, p->azCol[i], &rc); + sessionAppendStr(&buf, " END", &rc); + zSep = ", "; + } + + /* Append the PK part of the WHERE clause */ + sessionAppendStr(&buf, " WHERE ", &rc); + for(i=0; i<p->nCol; i++){ + if( p->abPK[i] ){ + sessionAppendIdent(&buf, p->azCol[i], &rc); + sessionAppendStr(&buf, " = ?", &rc); + sessionAppendInteger(&buf, i*3+1, &rc); + sessionAppendStr(&buf, " AND ", &rc); + } + } + + /* Append the non-PK part of the WHERE clause */ + sessionAppendStr(&buf, " (?", &rc); + sessionAppendInteger(&buf, p->nCol*3+1, &rc); + sessionAppendStr(&buf, " OR 1", &rc); + for(i=0; i<p->nCol; i++){ + if( !p->abPK[i] ){ + sessionAppendStr(&buf, " AND (?", &rc); + sessionAppendInteger(&buf, i*3+2, &rc); + sessionAppendStr(&buf, "=0 OR ", &rc); + sessionAppendIdent(&buf, p->azCol[i], &rc); + sessionAppendStr(&buf, " IS ?", &rc); + sessionAppendInteger(&buf, i*3+1, &rc); + sessionAppendStr(&buf, ")", &rc); + } + } + sessionAppendStr(&buf, ")", &rc); + + if( rc==SQLITE_OK ){ + rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, &p->pUpdate, 0); + } + sqlite3_free(buf.aBuf); + + return rc; +} + +/* +** Formulate and prepare an SQL statement to query table zTab by primary +** key. Assuming the following table structure: +** +** CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c)); +** +** The SELECT statement looks like this: +** +** SELECT * FROM x WHERE a = ?1 AND c = ?3 +** +** If successful, SQLITE_OK is returned and SessionApplyCtx.pSelect is left +** pointing to the prepared version of the SQL statement. +*/ +static int sessionSelectRow( + sqlite3 *db, /* Database handle */ + const char *zTab, /* Table name */ + SessionApplyCtx *p /* Session changeset-apply context */ +){ + return sessionSelectStmt( + db, "main", zTab, p->nCol, p->azCol, p->abPK, &p->pSelect); +} + +/* +** Formulate and prepare an INSERT statement to add a record to table zTab. +** For example: +** +** INSERT INTO main."zTab" VALUES(?1, ?2, ?3 ...); +** +** If successful, SQLITE_OK is returned and SessionApplyCtx.pInsert is left +** pointing to the prepared version of the SQL statement. +*/ +static int sessionInsertRow( + sqlite3 *db, /* Database handle */ + const char *zTab, /* Table name */ + SessionApplyCtx *p /* Session changeset-apply context */ +){ + int rc = SQLITE_OK; + int i; + SessionBuffer buf = {0, 0, 0}; + + sessionAppendStr(&buf, "INSERT INTO main.", &rc); + sessionAppendIdent(&buf, zTab, &rc); + sessionAppendStr(&buf, " VALUES(?", &rc); + for(i=1; i<p->nCol; i++){ + sessionAppendStr(&buf, ", ?", &rc); + } + sessionAppendStr(&buf, ")", &rc); + + if( rc==SQLITE_OK ){ + rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, &p->pInsert, 0); + } + sqlite3_free(buf.aBuf); + return rc; +} + +/* +** A wrapper around sqlite3_bind_value() that detects an extra problem. +** See comments in the body of this function for details. +*/ +static int sessionBindValue( + sqlite3_stmt *pStmt, /* Statement to bind value to */ + int i, /* Parameter number to bind to */ + sqlite3_value *pVal /* Value to bind */ +){ + int eType = sqlite3_value_type(pVal); + /* COVERAGE: The (pVal->z==0) branch is never true using current versions + ** of SQLite. If a malloc fails in an sqlite3_value_xxx() function, either + ** the (pVal->z) variable remains as it was or the type of the value is + ** set to SQLITE_NULL. */ + if( (eType==SQLITE_TEXT || eType==SQLITE_BLOB) && pVal->z==0 ){ + /* This condition occurs when an earlier OOM in a call to + ** sqlite3_value_text() or sqlite3_value_blob() (perhaps from within + ** a conflict-handler) has zeroed the pVal->z pointer. Return NOMEM. */ + return SQLITE_NOMEM; + } + return sqlite3_bind_value(pStmt, i, pVal); +} + +/* +** Iterator pIter must point to an SQLITE_INSERT entry. This function +** transfers new.* values from the current iterator entry to statement +** pStmt. The table being inserted into has nCol columns. +** +** New.* value $i 0 from the iterator is bound to variable ($i+1) of +** statement pStmt. If parameter abPK is NULL, all values from 0 to (nCol-1) +** are transfered to the statement. Otherwise, if abPK is not NULL, it points +** to an array nCol elements in size. In this case only those values for +** which abPK[$i] is true are read from the iterator and bound to the +** statement. +** +** An SQLite error code is returned if an error occurs. Otherwise, SQLITE_OK. +*/ +static int sessionBindRow( + sqlite3_changeset_iter *pIter, /* Iterator to read values from */ + int(*xValue)(sqlite3_changeset_iter *, int, sqlite3_value **), + int nCol, /* Number of columns */ + u8 *abPK, /* If not NULL, bind only if true */ + sqlite3_stmt *pStmt /* Bind values to this statement */ +){ + int i; + int rc = SQLITE_OK; + + /* Neither sqlite3changeset_old or sqlite3changeset_new can fail if the + ** argument iterator points to a suitable entry. Make sure that xValue + ** is one of these to guarantee that it is safe to ignore the return + ** in the code below. */ + assert( xValue==sqlite3changeset_old || xValue==sqlite3changeset_new ); + + for(i=0; rc==SQLITE_OK && i<nCol; i++){ + if( !abPK || abPK[i] ){ + sqlite3_value *pVal; + (void)xValue(pIter, i, &pVal); + rc = sessionBindValue(pStmt, i+1, pVal); + } + } + return rc; +} + +/* +** SQL statement pSelect is as generated by the sessionSelectRow() function. +** This function binds the primary key values from the change that changeset +** iterator pIter points to to the SELECT and attempts to seek to the table +** entry. If a row is found, the SELECT statement left pointing at the row +** and SQLITE_ROW is returned. Otherwise, if no row is found and no error +** has occured, the statement is reset and SQLITE_OK is returned. If an +** error occurs, the statement is reset and an SQLite error code is returned. +** +** If this function returns SQLITE_ROW, the caller must eventually reset() +** statement pSelect. If any other value is returned, the statement does +** not require a reset(). +** +** If the iterator currently points to an INSERT record, bind values from the +** new.* record to the SELECT statement. Or, if it points to a DELETE or +** UPDATE, bind values from the old.* record. +*/ +static int sessionSeekToRow( + sqlite3 *db, /* Database handle */ + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + u8 *abPK, /* Primary key flags array */ + sqlite3_stmt *pSelect /* SELECT statement from sessionSelectRow() */ +){ + int rc; /* Return code */ + int nCol; /* Number of columns in table */ + int op; /* Changset operation (SQLITE_UPDATE etc.) */ + const char *zDummy; /* Unused */ + + sqlite3changeset_op(pIter, &zDummy, &nCol, &op, 0); + rc = sessionBindRow(pIter, + op==SQLITE_INSERT ? sqlite3changeset_new : sqlite3changeset_old, + nCol, abPK, pSelect + ); + + if( rc==SQLITE_OK ){ + rc = sqlite3_step(pSelect); + if( rc!=SQLITE_ROW ) rc = sqlite3_reset(pSelect); + } + + return rc; +} + +/* +** Invoke the conflict handler for the change that the changeset iterator +** currently points to. +** +** Argument eType must be either CHANGESET_DATA or CHANGESET_CONFLICT. +** If argument pbReplace is NULL, then the type of conflict handler invoked +** depends solely on eType, as follows: +** +** eType value Value passed to xConflict +** ------------------------------------------------- +** CHANGESET_DATA CHANGESET_NOTFOUND +** CHANGESET_CONFLICT CHANGESET_CONSTRAINT +** +** Or, if pbReplace is not NULL, then an attempt is made to find an existing +** record with the same primary key as the record about to be deleted, updated +** or inserted. If such a record can be found, it is available to the conflict +** handler as the "conflicting" record. In this case the type of conflict +** handler invoked is as follows: +** +** eType value PK Record found? Value passed to xConflict +** ---------------------------------------------------------------- +** CHANGESET_DATA Yes CHANGESET_DATA +** CHANGESET_DATA No CHANGESET_NOTFOUND +** CHANGESET_CONFLICT Yes CHANGESET_CONFLICT +** CHANGESET_CONFLICT No CHANGESET_CONSTRAINT +** +** If pbReplace is not NULL, and a record with a matching PK is found, and +** the conflict handler function returns SQLITE_CHANGESET_REPLACE, *pbReplace +** is set to non-zero before returning SQLITE_OK. +** +** If the conflict handler returns SQLITE_CHANGESET_ABORT, SQLITE_ABORT is +** returned. Or, if the conflict handler returns an invalid value, +** SQLITE_MISUSE. If the conflict handler returns SQLITE_CHANGESET_OMIT, +** this function returns SQLITE_OK. +*/ +static int sessionConflictHandler( + int eType, /* Either CHANGESET_DATA or CONFLICT */ + SessionApplyCtx *p, /* changeset_apply() context */ + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int(*xConflict)(void *, int, sqlite3_changeset_iter*), + void *pCtx, /* First argument for conflict handler */ + int *pbReplace /* OUT: Set to true if PK row is found */ +){ + int res; /* Value returned by conflict handler */ + int rc; + int nCol; + int op; + const char *zDummy; + + sqlite3changeset_op(pIter, &zDummy, &nCol, &op, 0); + + assert( eType==SQLITE_CHANGESET_CONFLICT || eType==SQLITE_CHANGESET_DATA ); + assert( SQLITE_CHANGESET_CONFLICT+1==SQLITE_CHANGESET_CONSTRAINT ); + assert( SQLITE_CHANGESET_DATA+1==SQLITE_CHANGESET_NOTFOUND ); + + /* Bind the new.* PRIMARY KEY values to the SELECT statement. */ + if( pbReplace ){ + rc = sessionSeekToRow(p->db, pIter, p->abPK, p->pSelect); + }else{ + rc = SQLITE_OK; + } + + if( rc==SQLITE_ROW ){ + /* There exists another row with the new.* primary key. */ + pIter->pConflict = p->pSelect; + res = xConflict(pCtx, eType, pIter); + pIter->pConflict = 0; + rc = sqlite3_reset(p->pSelect); + }else if( rc==SQLITE_OK ){ + /* No other row with the new.* primary key. */ + res = xConflict(pCtx, eType+1, pIter); + if( res==SQLITE_CHANGESET_REPLACE ) rc = SQLITE_MISUSE; + } + + if( rc==SQLITE_OK ){ + switch( res ){ + case SQLITE_CHANGESET_REPLACE: + assert( pbReplace ); + *pbReplace = 1; + break; + + case SQLITE_CHANGESET_OMIT: + break; + + case SQLITE_CHANGESET_ABORT: + rc = SQLITE_ABORT; + break; + + default: + rc = SQLITE_MISUSE; + break; + } + } + + return rc; +} + +/* +** Attempt to apply the change that the iterator passed as the first argument +** currently points to to the database. If a conflict is encountered, invoke +** the conflict handler callback. +** +** If argument pbRetry is NULL, then ignore any CHANGESET_DATA conflict. If +** one is encountered, update or delete the row with the matching primary key +** instead. Or, if pbRetry is not NULL and a CHANGESET_DATA conflict occurs, +** invoke the conflict handler. If it returns CHANGESET_REPLACE, set *pbRetry +** to true before returning. In this case the caller will invoke this function +** again, this time with pbRetry set to NULL. +** +** If argument pbReplace is NULL and a CHANGESET_CONFLICT conflict is +** encountered invoke the conflict handler with CHANGESET_CONSTRAINT instead. +** Or, if pbReplace is not NULL, invoke it with CHANGESET_CONFLICT. If such +** an invocation returns SQLITE_CHANGESET_REPLACE, set *pbReplace to true +** before retrying. In this case the caller attempts to remove the conflicting +** row before invoking this function again, this time with pbReplace set +** to NULL. +** +** If any conflict handler returns SQLITE_CHANGESET_ABORT, this function +** returns SQLITE_ABORT. Otherwise, if no error occurs, SQLITE_OK is +** returned. +*/ +static int sessionApplyOneOp( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + SessionApplyCtx *p, /* changeset_apply() context */ + int(*xConflict)(void *, int, sqlite3_changeset_iter *), + void *pCtx, /* First argument for the conflict handler */ + int *pbReplace, /* OUT: True to remove PK row and retry */ + int *pbRetry /* OUT: True to retry. */ +){ + const char *zDummy; + int op; + int nCol; + int rc = SQLITE_OK; + + assert( p->pDelete && p->pUpdate && p->pInsert && p->pSelect ); + assert( p->azCol && p->abPK ); + assert( !pbReplace || *pbReplace==0 ); + + sqlite3changeset_op(pIter, &zDummy, &nCol, &op, 0); + + if( op==SQLITE_DELETE ){ + + /* Bind values to the DELETE statement. If conflict handling is required, + ** bind values for all columns and set bound variable (nCol+1) to true. + ** Or, if conflict handling is not required, bind just the PK column + ** values and, if it exists, set (nCol+1) to false. Conflict handling + ** is not required if: + ** + ** * this is a patchset, or + ** * (pbRetry==0), or + ** * all columns of the table are PK columns (in this case there is + ** no (nCol+1) variable to bind to). + */ + u8 *abPK = (pIter->bPatchset ? p->abPK : 0); + rc = sessionBindRow(pIter, sqlite3changeset_old, nCol, abPK, p->pDelete); + if( rc==SQLITE_OK && sqlite3_bind_parameter_count(p->pDelete)>nCol ){ + rc = sqlite3_bind_int(p->pDelete, nCol+1, (pbRetry==0 || abPK)); + } + if( rc!=SQLITE_OK ) return rc; + + sqlite3_step(p->pDelete); + rc = sqlite3_reset(p->pDelete); + if( rc==SQLITE_OK && sqlite3_changes(p->db)==0 ){ + rc = sessionConflictHandler( + SQLITE_CHANGESET_DATA, p, pIter, xConflict, pCtx, pbRetry + ); + }else if( (rc&0xff)==SQLITE_CONSTRAINT ){ + rc = sessionConflictHandler( + SQLITE_CHANGESET_CONFLICT, p, pIter, xConflict, pCtx, 0 + ); + } + + }else if( op==SQLITE_UPDATE ){ + int i; + + /* Bind values to the UPDATE statement. */ + for(i=0; rc==SQLITE_OK && i<nCol; i++){ + sqlite3_value *pOld = sessionChangesetOld(pIter, i); + sqlite3_value *pNew = sessionChangesetNew(pIter, i); + + sqlite3_bind_int(p->pUpdate, i*3+2, !!pNew); + if( pOld ){ + rc = sessionBindValue(p->pUpdate, i*3+1, pOld); + } + if( rc==SQLITE_OK && pNew ){ + rc = sessionBindValue(p->pUpdate, i*3+3, pNew); + } + } + if( rc==SQLITE_OK ){ + sqlite3_bind_int(p->pUpdate, nCol*3+1, pbRetry==0 || pIter->bPatchset); + } + if( rc!=SQLITE_OK ) return rc; + + /* Attempt the UPDATE. In the case of a NOTFOUND or DATA conflict, + ** the result will be SQLITE_OK with 0 rows modified. */ + sqlite3_step(p->pUpdate); + rc = sqlite3_reset(p->pUpdate); + + if( rc==SQLITE_OK && sqlite3_changes(p->db)==0 ){ + /* A NOTFOUND or DATA error. Search the table to see if it contains + ** a row with a matching primary key. If so, this is a DATA conflict. + ** Otherwise, if there is no primary key match, it is a NOTFOUND. */ + + rc = sessionConflictHandler( + SQLITE_CHANGESET_DATA, p, pIter, xConflict, pCtx, pbRetry + ); + + }else if( (rc&0xff)==SQLITE_CONSTRAINT ){ + /* This is always a CONSTRAINT conflict. */ + rc = sessionConflictHandler( + SQLITE_CHANGESET_CONFLICT, p, pIter, xConflict, pCtx, 0 + ); + } + + }else{ + assert( op==SQLITE_INSERT ); + rc = sessionBindRow(pIter, sqlite3changeset_new, nCol, 0, p->pInsert); + if( rc!=SQLITE_OK ) return rc; + + sqlite3_step(p->pInsert); + rc = sqlite3_reset(p->pInsert); + if( (rc&0xff)==SQLITE_CONSTRAINT ){ + rc = sessionConflictHandler( + SQLITE_CHANGESET_CONFLICT, p, pIter, xConflict, pCtx, pbReplace + ); + } + } + + return rc; +} + +/* +** Argument pIter is a changeset iterator that has been initialized, but +** not yet passed to sqlite3changeset_next(). This function applies the +** changeset to the main database attached to handle "db". The supplied +** conflict handler callback is invoked to resolve any conflicts encountered +** while applying the change. +*/ +static int sessionChangesetApply( + sqlite3 *db, /* Apply change to "main" db of this handle */ + sqlite3_changeset_iter *pIter, /* Changeset to apply */ + int(*xFilter)( + void *pCtx, /* Copy of sixth arg to _apply() */ + const char *zTab /* Table name */ + ), + int(*xConflict)( + void *pCtx, /* Copy of fifth arg to _apply() */ + int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *p /* Handle describing change and conflict */ + ), + void *pCtx /* First argument passed to xConflict */ +){ + int schemaMismatch = 0; + int rc; /* Return code */ + const char *zTab = 0; /* Name of current table */ + int nTab = 0; /* Result of sqlite3Strlen30(zTab) */ + SessionApplyCtx sApply; /* changeset_apply() context object */ + + assert( xConflict!=0 ); + + memset(&sApply, 0, sizeof(sApply)); + sqlite3_mutex_enter(sqlite3_db_mutex(db)); + rc = sqlite3_exec(db, "SAVEPOINT changeset_apply", 0, 0, 0); + if( rc==SQLITE_OK ){ + rc = sqlite3_exec(db, "PRAGMA defer_foreign_keys = 1", 0, 0, 0); + } + while( rc==SQLITE_OK && SQLITE_ROW==sqlite3changeset_next(pIter) ){ + int nCol; + int op; + int bReplace = 0; + int bRetry = 0; + const char *zNew; + + sqlite3changeset_op(pIter, &zNew, &nCol, &op, 0); + + if( zTab==0 || sqlite3_strnicmp(zNew, zTab, nTab+1) ){ + u8 *abPK; + + sqlite3_free((char*)sApply.azCol); /* cast works around VC++ bug */ + sqlite3_finalize(sApply.pDelete); + sqlite3_finalize(sApply.pUpdate); + sqlite3_finalize(sApply.pInsert); + sqlite3_finalize(sApply.pSelect); + memset(&sApply, 0, sizeof(sApply)); + sApply.db = db; + + /* If an xFilter() callback was specified, invoke it now. If the + ** xFilter callback returns zero, skip this table. If it returns + ** non-zero, proceed. */ + schemaMismatch = (xFilter && (0==xFilter(pCtx, zNew))); + if( schemaMismatch ){ + zTab = sqlite3_mprintf("%s", zNew); + if( zTab==0 ){ + rc = SQLITE_NOMEM; + break; + } + nTab = (int)strlen(zTab); + sApply.azCol = (const char **)zTab; + }else{ + sqlite3changeset_pk(pIter, &abPK, 0); + rc = sessionTableInfo( + db, "main", zNew, &sApply.nCol, &zTab, &sApply.azCol, &sApply.abPK + ); + if( rc!=SQLITE_OK ) break; + + if( sApply.nCol==0 ){ + schemaMismatch = 1; + sqlite3_log(SQLITE_SCHEMA, + "sqlite3changeset_apply(): no such table: %s", zTab + ); + } + else if( sApply.nCol!=nCol ){ + schemaMismatch = 1; + sqlite3_log(SQLITE_SCHEMA, + "sqlite3changeset_apply(): table %s has %d columns, expected %d", + zTab, sApply.nCol, nCol + ); + } + else if( memcmp(sApply.abPK, abPK, nCol)!=0 ){ + schemaMismatch = 1; + sqlite3_log(SQLITE_SCHEMA, "sqlite3changeset_apply(): " + "primary key mismatch for table %s", zTab + ); + } + else if( + (rc = sessionSelectRow(db, zTab, &sApply)) + || (rc = sessionUpdateRow(db, zTab, &sApply)) + || (rc = sessionDeleteRow(db, zTab, &sApply)) + || (rc = sessionInsertRow(db, zTab, &sApply)) + ){ + break; + } + nTab = sqlite3Strlen30(zTab); + } + } + + /* If there is a schema mismatch on the current table, proceed to the + ** next change. A log message has already been issued. */ + if( schemaMismatch ) continue; + + rc = sessionApplyOneOp(pIter, &sApply, xConflict, pCtx, &bReplace, &bRetry); + + if( rc==SQLITE_OK && bRetry ){ + rc = sessionApplyOneOp(pIter, &sApply, xConflict, pCtx, &bReplace, 0); + } + + if( bReplace ){ + assert( pIter->op==SQLITE_INSERT ); + rc = sqlite3_exec(db, "SAVEPOINT replace_op", 0, 0, 0); + if( rc==SQLITE_OK ){ + rc = sessionBindRow(pIter, + sqlite3changeset_new, sApply.nCol, sApply.abPK, sApply.pDelete); + sqlite3_bind_int(sApply.pDelete, sApply.nCol+1, 1); + } + if( rc==SQLITE_OK ){ + sqlite3_step(sApply.pDelete); + rc = sqlite3_reset(sApply.pDelete); + } + if( rc==SQLITE_OK ){ + rc = sessionApplyOneOp(pIter, &sApply, xConflict, pCtx, 0, 0); + } + if( rc==SQLITE_OK ){ + rc = sqlite3_exec(db, "RELEASE replace_op", 0, 0, 0); + } + } + } + + if( rc==SQLITE_OK ){ + rc = sqlite3changeset_finalize(pIter); + }else{ + sqlite3changeset_finalize(pIter); + } + + if( rc==SQLITE_OK ){ + int nFk, notUsed; + sqlite3_db_status(db, SQLITE_DBSTATUS_DEFERRED_FKS, &nFk, ¬Used, 0); + if( nFk!=0 ){ + int res = SQLITE_CHANGESET_ABORT; + sqlite3_changeset_iter sIter; + memset(&sIter, 0, sizeof(sIter)); + sIter.nCol = nFk; + res = xConflict(pCtx, SQLITE_CHANGESET_FOREIGN_KEY, &sIter); + if( res!=SQLITE_CHANGESET_OMIT ){ + rc = SQLITE_CONSTRAINT; + } + } + } + sqlite3_exec(db, "PRAGMA defer_foreign_keys = 0", 0, 0, 0); + + if( rc==SQLITE_OK ){ + rc = sqlite3_exec(db, "RELEASE changeset_apply", 0, 0, 0); + }else{ + sqlite3_exec(db, "ROLLBACK TO changeset_apply", 0, 0, 0); + sqlite3_exec(db, "RELEASE changeset_apply", 0, 0, 0); + } + + sqlite3_finalize(sApply.pInsert); + sqlite3_finalize(sApply.pDelete); + sqlite3_finalize(sApply.pUpdate); + sqlite3_finalize(sApply.pSelect); + sqlite3_free((char*)sApply.azCol); /* cast works around VC++ bug */ + sqlite3_mutex_leave(sqlite3_db_mutex(db)); + return rc; +} + +/* +** Apply the changeset passed via pChangeset/nChangeset to the main database +** attached to handle "db". Invoke the supplied conflict handler callback +** to resolve any conflicts encountered while applying the change. +*/ +int sqlite3changeset_apply( + sqlite3 *db, /* Apply change to "main" db of this handle */ + int nChangeset, /* Size of changeset in bytes */ + void *pChangeset, /* Changeset blob */ + int(*xFilter)( + void *pCtx, /* Copy of sixth arg to _apply() */ + const char *zTab /* Table name */ + ), + int(*xConflict)( + void *pCtx, /* Copy of fifth arg to _apply() */ + int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *p /* Handle describing change and conflict */ + ), + void *pCtx /* First argument passed to xConflict */ +){ + sqlite3_changeset_iter *pIter; /* Iterator to skip through changeset */ + int rc = sqlite3changeset_start(&pIter, nChangeset, pChangeset); + if( rc==SQLITE_OK ){ + rc = sessionChangesetApply(db, pIter, xFilter, xConflict, pCtx); + } + return rc; +} + +/* +** Apply the changeset passed via xInput/pIn to the main database +** attached to handle "db". Invoke the supplied conflict handler callback +** to resolve any conflicts encountered while applying the change. +*/ +int sqlite3changeset_apply_strm( + sqlite3 *db, /* Apply change to "main" db of this handle */ + int (*xInput)(void *pIn, void *pData, int *pnData), /* Input function */ + void *pIn, /* First arg for xInput */ + int(*xFilter)( + void *pCtx, /* Copy of sixth arg to _apply() */ + const char *zTab /* Table name */ + ), + int(*xConflict)( + void *pCtx, /* Copy of sixth arg to _apply() */ + int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *p /* Handle describing change and conflict */ + ), + void *pCtx /* First argument passed to xConflict */ +){ + sqlite3_changeset_iter *pIter; /* Iterator to skip through changeset */ + int rc = sqlite3changeset_start_strm(&pIter, xInput, pIn); + if( rc==SQLITE_OK ){ + rc = sessionChangesetApply(db, pIter, xFilter, xConflict, pCtx); + } + return rc; +} + +/* +** This function is called to merge two changes to the same row together as +** part of an sqlite3changeset_concat() operation. A new change object is +** allocated and a pointer to it stored in *ppNew. +*/ +static int sessionChangeMerge( + SessionTable *pTab, /* Table structure */ + int bPatchset, /* True for patchsets */ + SessionChange *pExist, /* Existing change */ + int op2, /* Second change operation */ + int bIndirect, /* True if second change is indirect */ + u8 *aRec, /* Second change record */ + int nRec, /* Number of bytes in aRec */ + SessionChange **ppNew /* OUT: Merged change */ +){ + SessionChange *pNew = 0; + + if( !pExist ){ + pNew = (SessionChange *)sqlite3_malloc(sizeof(SessionChange) + nRec); + if( !pNew ){ + return SQLITE_NOMEM; + } + memset(pNew, 0, sizeof(SessionChange)); + pNew->op = op2; + pNew->bIndirect = bIndirect; + pNew->nRecord = nRec; + pNew->aRecord = (u8*)&pNew[1]; + memcpy(pNew->aRecord, aRec, nRec); + }else{ + int op1 = pExist->op; + + /* + ** op1=INSERT, op2=INSERT -> Unsupported. Discard op2. + ** op1=INSERT, op2=UPDATE -> INSERT. + ** op1=INSERT, op2=DELETE -> (none) + ** + ** op1=UPDATE, op2=INSERT -> Unsupported. Discard op2. + ** op1=UPDATE, op2=UPDATE -> UPDATE. + ** op1=UPDATE, op2=DELETE -> DELETE. + ** + ** op1=DELETE, op2=INSERT -> UPDATE. + ** op1=DELETE, op2=UPDATE -> Unsupported. Discard op2. + ** op1=DELETE, op2=DELETE -> Unsupported. Discard op2. + */ + if( (op1==SQLITE_INSERT && op2==SQLITE_INSERT) + || (op1==SQLITE_UPDATE && op2==SQLITE_INSERT) + || (op1==SQLITE_DELETE && op2==SQLITE_UPDATE) + || (op1==SQLITE_DELETE && op2==SQLITE_DELETE) + ){ + pNew = pExist; + }else if( op1==SQLITE_INSERT && op2==SQLITE_DELETE ){ + sqlite3_free(pExist); + assert( pNew==0 ); + }else{ + u8 *aExist = pExist->aRecord; + int nByte; + u8 *aCsr; + + /* Allocate a new SessionChange object. Ensure that the aRecord[] + ** buffer of the new object is large enough to hold any record that + ** may be generated by combining the input records. */ + nByte = sizeof(SessionChange) + pExist->nRecord + nRec; + pNew = (SessionChange *)sqlite3_malloc(nByte); + if( !pNew ){ + sqlite3_free(pExist); + return SQLITE_NOMEM; + } + memset(pNew, 0, sizeof(SessionChange)); + pNew->bIndirect = (bIndirect && pExist->bIndirect); + aCsr = pNew->aRecord = (u8 *)&pNew[1]; + + if( op1==SQLITE_INSERT ){ /* INSERT + UPDATE */ + u8 *a1 = aRec; + assert( op2==SQLITE_UPDATE ); + pNew->op = SQLITE_INSERT; + if( bPatchset==0 ) sessionSkipRecord(&a1, pTab->nCol); + sessionMergeRecord(&aCsr, pTab->nCol, aExist, a1); + }else if( op1==SQLITE_DELETE ){ /* DELETE + INSERT */ + assert( op2==SQLITE_INSERT ); + pNew->op = SQLITE_UPDATE; + if( bPatchset ){ + memcpy(aCsr, aRec, nRec); + aCsr += nRec; + }else{ + if( 0==sessionMergeUpdate(&aCsr, pTab, bPatchset, aExist, 0,aRec,0) ){ + sqlite3_free(pNew); + pNew = 0; + } + } + }else if( op2==SQLITE_UPDATE ){ /* UPDATE + UPDATE */ + u8 *a1 = aExist; + u8 *a2 = aRec; + assert( op1==SQLITE_UPDATE ); + if( bPatchset==0 ){ + sessionSkipRecord(&a1, pTab->nCol); + sessionSkipRecord(&a2, pTab->nCol); + } + pNew->op = SQLITE_UPDATE; + if( 0==sessionMergeUpdate(&aCsr, pTab, bPatchset, aRec, aExist,a1,a2) ){ + sqlite3_free(pNew); + pNew = 0; + } + }else{ /* UPDATE + DELETE */ + assert( op1==SQLITE_UPDATE && op2==SQLITE_DELETE ); + pNew->op = SQLITE_DELETE; + if( bPatchset ){ + memcpy(aCsr, aRec, nRec); + aCsr += nRec; + }else{ + sessionMergeRecord(&aCsr, pTab->nCol, aRec, aExist); + } + } + + if( pNew ){ + pNew->nRecord = (int)(aCsr - pNew->aRecord); + } + sqlite3_free(pExist); + } + } + + *ppNew = pNew; + return SQLITE_OK; +} + +/* +** Add all changes in the changeset passed via the first two arguments to +** hash tables. +*/ +static int sessionChangesetToHash( + sqlite3_changeset_iter *pIter, /* Iterator to read from */ + SessionTable **ppTabList /* IN/OUT: List of table objects */ +){ + u8 *aRec; + int nRec; + int rc = SQLITE_OK; + SessionTable *pTab = 0; + + while( SQLITE_ROW==sessionChangesetNext(pIter, &aRec, &nRec) ){ + const char *zNew; + int nCol; + int op; + int iHash; + int bIndirect; + SessionChange *pChange; + SessionChange *pExist = 0; + SessionChange **pp; + + sqlite3changeset_op(pIter, &zNew, &nCol, &op, &bIndirect); + if( !pTab || sqlite3_stricmp(zNew, pTab->zName) ){ + /* Search the list for a matching table */ + int nNew = (int)strlen(zNew); + u8 *abPK; + + sqlite3changeset_pk(pIter, &abPK, 0); + for(pTab = *ppTabList; pTab; pTab=pTab->pNext){ + if( 0==sqlite3_strnicmp(pTab->zName, zNew, nNew+1) ) break; + } + if( !pTab ){ + pTab = sqlite3_malloc(sizeof(SessionTable) + nCol + nNew+1); + if( !pTab ){ + rc = SQLITE_NOMEM; + break; + } + memset(pTab, 0, sizeof(SessionTable)); + pTab->pNext = *ppTabList; + pTab->nCol = nCol; + pTab->abPK = (u8*)&pTab[1]; + memcpy(pTab->abPK, abPK, nCol); + pTab->zName = (char*)&pTab->abPK[nCol]; + memcpy(pTab->zName, zNew, nNew+1); + *ppTabList = pTab; + }else if( pTab->nCol!=nCol || memcmp(pTab->abPK, abPK, nCol) ){ + rc = SQLITE_SCHEMA; + break; + } + } + + if( sessionGrowHash(pIter->bPatchset, pTab) ){ + rc = SQLITE_NOMEM; + break; + } + iHash = sessionChangeHash( + pTab, (pIter->bPatchset && op==SQLITE_DELETE), aRec, pTab->nChange + ); + + /* Search for existing entry. If found, remove it from the hash table. + ** Code below may link it back in. + */ + for(pp=&pTab->apChange[iHash]; *pp; pp=&(*pp)->pNext){ + int bPkOnly1 = 0; + int bPkOnly2 = 0; + if( pIter->bPatchset ){ + bPkOnly1 = (*pp)->op==SQLITE_DELETE; + bPkOnly2 = op==SQLITE_DELETE; + } + if( sessionChangeEqual(pTab, bPkOnly1, (*pp)->aRecord, bPkOnly2, aRec) ){ + pExist = *pp; + *pp = (*pp)->pNext; + pTab->nEntry--; + break; + } + } + + rc = sessionChangeMerge(pTab, + pIter->bPatchset, pExist, op, bIndirect, aRec, nRec, &pChange + ); + if( rc ) break; + if( pChange ){ + pChange->pNext = pTab->apChange[iHash]; + pTab->apChange[iHash] = pChange; + pTab->nEntry++; + } + } + + if( rc==SQLITE_OK ) rc = pIter->rc; + return rc; +} + +/* +** 1. Iterate through the left-hand changeset. Add an entry to a table +** specific hash table for each change in the changeset. The hash table +** key is the PK of the row affected by the change. +** +** 2. Then interate through the right-hand changeset. Attempt to add an +** entry to a hash table for each component change. If a change already +** exists with the same PK values, combine the two into a single change. +** +** 3. Write an output changeset based on the contents of the hash table. +*/ +int sessionChangesetConcat( + sqlite3_changeset_iter *pLeft, + sqlite3_changeset_iter *pRight, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut, + int *pnOut, + void **ppOut +){ + SessionTable *pList = 0; /* List of SessionTable objects */ + int rc; /* Return code */ + int bPatch; /* True for a patchset */ + SessionTable *pTab; + SessionBuffer buf = {0, 0, 0}; + + assert( xOutput==0 || (ppOut==0 && pnOut==0) ); + + assert( pLeft->zTab==0 && pRight->zTab==0 ); + rc = sessionChangesetToHash(pLeft, &pList); + assert( pLeft->zTab || pList==0 ); + if( rc==SQLITE_OK ){ + rc = sessionChangesetToHash(pRight, &pList); + } + bPatch = pLeft->bPatchset || pRight->bPatchset; + + if( pLeft->zTab && pRight->zTab && pLeft->bPatchset!=pRight->bPatchset ){ + rc = SQLITE_ERROR; + } + + /* Create the serialized output changeset based on the contents of the + ** hash tables attached to the SessionTable objects in list pList. + */ + for(pTab=pList; rc==SQLITE_OK && pTab; pTab=pTab->pNext){ + int i; + if( pTab->nEntry==0 ) continue; + + sessionAppendTableHdr(&buf, bPatch, pTab, &rc); + for(i=0; i<pTab->nChange; i++){ + SessionChange *p; + for(p=pTab->apChange[i]; p; p=p->pNext){ + sessionAppendByte(&buf, p->op, &rc); + sessionAppendByte(&buf, p->bIndirect, &rc); + sessionAppendBlob(&buf, p->aRecord, p->nRecord, &rc); + } + } + + if( rc==SQLITE_OK && xOutput && buf.nBuf>=SESSIONS_STRM_CHUNK_SIZE ){ + rc = xOutput(pOut, buf.aBuf, buf.nBuf); + buf.nBuf = 0; + } + } + + if( rc==SQLITE_OK ){ + if( xOutput ){ + if( buf.nBuf>0 ) rc = xOutput(pOut, buf.aBuf, buf.nBuf); + }else{ + *ppOut = buf.aBuf; + *pnOut = buf.nBuf; + buf.aBuf = 0; + } + } + sqlite3_free(buf.aBuf); + + sessionDeleteTable(pList); + return rc; +} + +/* +** Combine two changesets together. +*/ +int sqlite3changeset_concat( + int nLeft, /* Number of bytes in lhs input */ + void *pLeft, /* Lhs input changeset */ + int nRight /* Number of bytes in rhs input */, + void *pRight, /* Rhs input changeset */ + int *pnOut, /* OUT: Number of bytes in output changeset */ + void **ppOut /* OUT: changeset (left <concat> right) */ +){ + sqlite3_changeset_iter *pIter1 = 0; + sqlite3_changeset_iter *pIter2 = 0; + int rc; + + *pnOut = 0; + *ppOut = 0; + rc = sqlite3changeset_start(&pIter1, nLeft, pLeft); + if( rc==SQLITE_OK ){ + rc = sqlite3changeset_start(&pIter2, nRight, pRight); + } + if( rc==SQLITE_OK ){ + rc = sessionChangesetConcat(pIter1, pIter2, 0, 0, pnOut, ppOut); + } + + sqlite3changeset_finalize(pIter1); + sqlite3changeset_finalize(pIter2); + return rc; +} + +/* +** Streaming version of sqlite3changeset_concat(). +*/ +int sqlite3changeset_concat_strm( + int (*xInputA)(void *pIn, void *pData, int *pnData), + void *pInA, + int (*xInputB)(void *pIn, void *pData, int *pnData), + void *pInB, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +){ + sqlite3_changeset_iter *pIter1 = 0; + sqlite3_changeset_iter *pIter2 = 0; + int rc; + + rc = sqlite3changeset_start_strm(&pIter1, xInputA, pInA); + if( rc==SQLITE_OK ){ + rc = sqlite3changeset_start_strm(&pIter2, xInputB, pInB); + } + if( rc==SQLITE_OK ){ + rc = sessionChangesetConcat(pIter1, pIter2, xOutput, pOut, 0, 0); + } + + sqlite3changeset_finalize(pIter1); + sqlite3changeset_finalize(pIter2); + return rc; +} + +#endif /* SQLITE_ENABLE_SESSION && SQLITE_ENABLE_PREUPDATE_HOOK */ diff --git a/ext/session/sqlite3session.h b/ext/session/sqlite3session.h new file mode 100644 index 000000000..e768d6632 --- /dev/null +++ b/ext/session/sqlite3session.h @@ -0,0 +1,1088 @@ + +#ifndef __SQLITESESSION_H_ +#define __SQLITESESSION_H_ 1 + +/* +** Make sure we can call this stuff from C++. +*/ +#ifdef __cplusplus +extern "C" { +#endif + +#include "sqlite3.h" + +/* +** CAPI3REF: Session Object Handle +*/ +typedef struct sqlite3_session sqlite3_session; + +/* +** CAPI3REF: Changeset Iterator Handle +*/ +typedef struct sqlite3_changeset_iter sqlite3_changeset_iter; + +/* +** CAPI3REF: Create A New Session Object +** +** Create a new session object attached to database handle db. If successful, +** a pointer to the new object is written to *ppSession and SQLITE_OK is +** returned. If an error occurs, *ppSession is set to NULL and an SQLite +** error code (e.g. SQLITE_NOMEM) is returned. +** +** It is possible to create multiple session objects attached to a single +** database handle. +** +** Session objects created using this function should be deleted using the +** [sqlite3session_delete()] function before the database handle that they +** are attached to is itself closed. If the database handle is closed before +** the session object is deleted, then the results of calling any session +** module function, including [sqlite3session_delete()] on the session object +** are undefined. +** +** Because the session module uses the [sqlite3_preupdate_hook()] API, it +** is not possible for an application to register a pre-update hook on a +** database handle that has one or more session objects attached. Nor is +** it possible to create a session object attached to a database handle for +** which a pre-update hook is already defined. The results of attempting +** either of these things are undefined. +** +** The session object will be used to create changesets for tables in +** database zDb, where zDb is either "main", or "temp", or the name of an +** attached database. It is not an error if database zDb is not attached +** to the database when the session object is created. +*/ +int sqlite3session_create( + sqlite3 *db, /* Database handle */ + const char *zDb, /* Name of db (e.g. "main") */ + sqlite3_session **ppSession /* OUT: New session object */ +); + +/* +** CAPI3REF: Delete A Session Object +** +** Delete a session object previously allocated using +** [sqlite3session_create()]. Once a session object has been deleted, the +** results of attempting to use pSession with any other session module +** function are undefined. +** +** Session objects must be deleted before the database handle to which they +** are attached is closed. Refer to the documentation for +** [sqlite3session_create()] for details. +*/ +void sqlite3session_delete(sqlite3_session *pSession); + + +/* +** CAPI3REF: Enable Or Disable A Session Object +** +** Enable or disable the recording of changes by a session object. When +** enabled, a session object records changes made to the database. When +** disabled - it does not. A newly created session object is enabled. +** Refer to the documentation for [sqlite3session_changeset()] for further +** details regarding how enabling and disabling a session object affects +** the eventual changesets. +** +** Passing zero to this function disables the session. Passing a value +** greater than zero enables it. Passing a value less than zero is a +** no-op, and may be used to query the current state of the session. +** +** The return value indicates the final state of the session object: 0 if +** the session is disabled, or 1 if it is enabled. +*/ +int sqlite3session_enable(sqlite3_session *pSession, int bEnable); + +/* +** CAPI3REF: Set Or Clear the Indirect Change Flag +** +** Each change recorded by a session object is marked as either direct or +** indirect. A change is marked as indirect if either: +** +** <ul> +** <li> The session object "indirect" flag is set when the change is +** made, or +** <li> The change is made by an SQL trigger or foreign key action +** instead of directly as a result of a users SQL statement. +** </ul> +** +** If a single row is affected by more than one operation within a session, +** then the change is considered indirect if all operations meet the criteria +** for an indirect change above, or direct otherwise. +** +** This function is used to set, clear or query the session object indirect +** flag. If the second argument passed to this function is zero, then the +** indirect flag is cleared. If it is greater than zero, the indirect flag +** is set. Passing a value less than zero does not modify the current value +** of the indirect flag, and may be used to query the current state of the +** indirect flag for the specified session object. +** +** The return value indicates the final state of the indirect flag: 0 if +** it is clear, or 1 if it is set. +*/ +int sqlite3session_indirect(sqlite3_session *pSession, int bIndirect); + +/* +** CAPI3REF: Attach A Table To A Session Object +** +** If argument zTab is not NULL, then it is the name of a table to attach +** to the session object passed as the first argument. All subsequent changes +** made to the table while the session object is enabled will be recorded. See +** documentation for [sqlite3session_changeset()] for further details. +** +** Or, if argument zTab is NULL, then changes are recorded for all tables +** in the database. If additional tables are added to the database (by +** executing "CREATE TABLE" statements) after this call is made, changes for +** the new tables are also recorded. +** +** Changes can only be recorded for tables that have a PRIMARY KEY explicitly +** defined as part of their CREATE TABLE statement. It does not matter if the +** PRIMARY KEY is an "INTEGER PRIMARY KEY" (rowid alias) or not. The PRIMARY +** KEY may consist of a single column, or may be a composite key. +** +** It is not an error if the named table does not exist in the database. Nor +** is it an error if the named table does not have a PRIMARY KEY. However, +** no changes will be recorded in either of these scenarios. +** +** Changes are not recorded for individual rows that have NULL values stored +** in one or more of their PRIMARY KEY columns. +** +** SQLITE_OK is returned if the call completes without error. Or, if an error +** occurs, an SQLite error code (e.g. SQLITE_NOMEM) is returned. +*/ +int sqlite3session_attach( + sqlite3_session *pSession, /* Session object */ + const char *zTab /* Table name */ +); + +/* +** CAPI3REF: Set a table filter on a Session Object. +** +** The second argument (xFilter) is the "filter callback". For changes to rows +** in tables that are not attached to the Session oject, the filter is called +** to determine whether changes to the table's rows should be tracked or not. +** If xFilter returns 0, changes is not tracked. Note that once a table is +** attached, xFilter will not be called again. +*/ +void sqlite3session_table_filter( + sqlite3_session *pSession, /* Session object */ + int(*xFilter)( + void *pCtx, /* Copy of third arg to _filter_table() */ + const char *zTab /* Table name */ + ), + void *pCtx /* First argument passed to xFilter */ +); + +/* +** CAPI3REF: Generate A Changeset From A Session Object +** +** Obtain a changeset containing changes to the tables attached to the +** session object passed as the first argument. If successful, +** set *ppChangeset to point to a buffer containing the changeset +** and *pnChangeset to the size of the changeset in bytes before returning +** SQLITE_OK. If an error occurs, set both *ppChangeset and *pnChangeset to +** zero and return an SQLite error code. +** +** A changeset consists of zero or more INSERT, UPDATE and/or DELETE changes, +** each representing a change to a single row of an attached table. An INSERT +** change contains the values of each field of a new database row. A DELETE +** contains the original values of each field of a deleted database row. An +** UPDATE change contains the original values of each field of an updated +** database row along with the updated values for each updated non-primary-key +** column. It is not possible for an UPDATE change to represent a change that +** modifies the values of primary key columns. If such a change is made, it +** is represented in a changeset as a DELETE followed by an INSERT. +** +** Changes are not recorded for rows that have NULL values stored in one or +** more of their PRIMARY KEY columns. If such a row is inserted or deleted, +** no corresponding change is present in the changesets returned by this +** function. If an existing row with one or more NULL values stored in +** PRIMARY KEY columns is updated so that all PRIMARY KEY columns are non-NULL, +** only an INSERT is appears in the changeset. Similarly, if an existing row +** with non-NULL PRIMARY KEY values is updated so that one or more of its +** PRIMARY KEY columns are set to NULL, the resulting changeset contains a +** DELETE change only. +** +** The contents of a changeset may be traversed using an iterator created +** using the [sqlite3changeset_start()] API. A changeset may be applied to +** a database with a compatible schema using the [sqlite3changeset_apply()] +** API. +** +** Following a successful call to this function, it is the responsibility of +** the caller to eventually free the buffer that *ppChangeset points to using +** [sqlite3_free()]. +** +** <h3>Changeset Generation</h3> +** +** Once a table has been attached to a session object, the session object +** records the primary key values of all new rows inserted into the table. +** It also records the original primary key and other column values of any +** deleted or updated rows. For each unique primary key value, data is only +** recorded once - the first time a row with said primary key is inserted, +** updated or deleted in the lifetime of the session. +** +** There is one exception to the previous paragraph: when a row is inserted, +** updated or deleted, if one or more of its primary key columns contain a +** NULL value, no record of the change is made. +** +** The session object therefore accumulates two types of records - those +** that consist of primary key values only (created when the user inserts +** a new record) and those that consist of the primary key values and the +** original values of other table columns (created when the users deletes +** or updates a record). +** +** When this function is called, the requested changeset is created using +** both the accumulated records and the current contents of the database +** file. Specifically: +** +** <ul> +** <li> For each record generated by an insert, the database is queried +** for a row with a matching primary key. If one is found, an INSERT +** change is added to the changeset. If no such row is found, no change +** is added to the changeset. +** +** <li> For each record generated by an update or delete, the database is +** queried for a row with a matching primary key. If such a row is +** found and one or more of the non-primary key fields have been +** modified from their original values, an UPDATE change is added to +** the changeset. Or, if no such row is found in the table, a DELETE +** change is added to the changeset. If there is a row with a matching +** primary key in the database, but all fields contain their original +** values, no change is added to the changeset. +** </ul> +** +** This means, amongst other things, that if a row is inserted and then later +** deleted while a session object is active, neither the insert nor the delete +** will be present in the changeset. Or if a row is deleted and then later a +** row with the same primary key values inserted while a session object is +** active, the resulting changeset will contain an UPDATE change instead of +** a DELETE and an INSERT. +** +** When a session object is disabled (see the [sqlite3session_enable()] API), +** it does not accumulate records when rows are inserted, updated or deleted. +** This may appear to have some counter-intuitive effects if a single row +** is written to more than once during a session. For example, if a row +** is inserted while a session object is enabled, then later deleted while +** the same session object is disabled, no INSERT record will appear in the +** changeset, even though the delete took place while the session was disabled. +** Or, if one field of a row is updated while a session is disabled, and +** another field of the same row is updated while the session is enabled, the +** resulting changeset will contain an UPDATE change that updates both fields. +*/ +int sqlite3session_changeset( + sqlite3_session *pSession, /* Session object */ + int *pnChangeset, /* OUT: Size of buffer at *ppChangeset */ + void **ppChangeset /* OUT: Buffer containing changeset */ +); + + +/* +** CAPI3REF: Generate A Patchset From A Session Object +** +** The differences between a patchset and a changeset are that: +** +** <ul> +** <li> DELETE records consist of the primary key fields only. The +** original values of other fields are omitted. +** <li> The original values of any modified fields are omitted from +** UPDATE records. +** </ul> +** +** A patchset blob may be used with up to date versions of all +** sqlite3changeset_xxx API functions except for sqlite3changeset_invert(), +** which returns SQLITE_CORRUPT if it is passed a patchset. Similarly, +** attempting to use a patchset blob with old versions of the +** sqlite3changeset_xxx APIs also provokes an SQLITE_CORRUPT error. +** +** Because the non-primary key "old.*" fields are omitted, no +** SQLITE_CHANGESET_DATA conflicts can be detected or reported if a patchset +** is passed to the sqlite3changeset_apply() API. Other conflict types work +** in the same way as for changesets. +*/ +int sqlite3session_patchset( + sqlite3_session *pSession, /* Session object */ + int *pnPatchset, /* OUT: Size of buffer at *ppChangeset */ + void **ppPatchset /* OUT: Buffer containing changeset */ +); + +/* +** CAPI3REF: Test if a changeset has recorded any changes. +** +** Return non-zero if no changes to attached tables have been recorded by +** the session object passed as the first argument. Otherwise, if one or +** more changes have been recorded, return zero. +** +** Even if this function returns zero, it is possible that calling +** [sqlite3session_changeset()] on the session handle may still return a +** changeset that contains no changes. This can happen when a row in +** an attached table is modified and then later on the original values +** are restored. However, if this function returns non-zero, then it is +** guaranteed that a call to sqlite3session_changeset() will return a +** changeset containing zero changes. +*/ +int sqlite3session_isempty(sqlite3_session *pSession); + +/* +** CAPI3REF: Create An Iterator To Traverse A Changeset +** +** Create an iterator used to iterate through the contents of a changeset. +** If successful, *pp is set to point to the iterator handle and SQLITE_OK +** is returned. Otherwise, if an error occurs, *pp is set to zero and an +** SQLite error code is returned. +** +** The following functions can be used to advance and query a changeset +** iterator created by this function: +** +** <ul> +** <li> [sqlite3changeset_next()] +** <li> [sqlite3changeset_op()] +** <li> [sqlite3changeset_new()] +** <li> [sqlite3changeset_old()] +** </ul> +** +** It is the responsibility of the caller to eventually destroy the iterator +** by passing it to [sqlite3changeset_finalize()]. The buffer containing the +** changeset (pChangeset) must remain valid until after the iterator is +** destroyed. +** +** Assuming the changeset blob was created by one of the +** [sqlite3session_changeset()], [sqlite3changeset_concat()] or +** [sqlite3changeset_invert()] functions, all changes within the changeset +** that apply to a single table are grouped together. This means that when +** an application iterates through a changeset using an iterator created by +** this function, all changes that relate to a single table are visted +** consecutively. There is no chance that the iterator will visit a change +** the applies to table X, then one for table Y, and then later on visit +** another change for table X. +*/ +int sqlite3changeset_start( + sqlite3_changeset_iter **pp, /* OUT: New changeset iterator handle */ + int nChangeset, /* Size of changeset blob in bytes */ + void *pChangeset /* Pointer to blob containing changeset */ +); + + +/* +** CAPI3REF: Advance A Changeset Iterator +** +** This function may only be used with iterators created by function +** [sqlite3changeset_start()]. If it is called on an iterator passed to +** a conflict-handler callback by [sqlite3changeset_apply()], SQLITE_MISUSE +** is returned and the call has no effect. +** +** Immediately after an iterator is created by sqlite3changeset_start(), it +** does not point to any change in the changeset. Assuming the changeset +** is not empty, the first call to this function advances the iterator to +** point to the first change in the changeset. Each subsequent call advances +** the iterator to point to the next change in the changeset (if any). If +** no error occurs and the iterator points to a valid change after a call +** to sqlite3changeset_next() has advanced it, SQLITE_ROW is returned. +** Otherwise, if all changes in the changeset have already been visited, +** SQLITE_DONE is returned. +** +** If an error occurs, an SQLite error code is returned. Possible error +** codes include SQLITE_CORRUPT (if the changeset buffer is corrupt) or +** SQLITE_NOMEM. +*/ +int sqlite3changeset_next(sqlite3_changeset_iter *pIter); + +/* +** CAPI3REF: Obtain The Current Operation From A Changeset Iterator +** +** The pIter argument passed to this function may either be an iterator +** passed to a conflict-handler by [sqlite3changeset_apply()], or an iterator +** created by [sqlite3changeset_start()]. In the latter case, the most recent +** call to [sqlite3changeset_next()] must have returned [SQLITE_ROW]. If this +** is not the case, this function returns [SQLITE_MISUSE]. +** +** If argument pzTab is not NULL, then *pzTab is set to point to a +** nul-terminated utf-8 encoded string containing the name of the table +** affected by the current change. The buffer remains valid until either +** sqlite3changeset_next() is called on the iterator or until the +** conflict-handler function returns. If pnCol is not NULL, then *pnCol is +** set to the number of columns in the table affected by the change. If +** pbIncorrect is not NULL, then *pbIndirect is set to true (1) if the change +** is an indirect change, or false (0) otherwise. See the documentation for +** [sqlite3session_indirect()] for a description of direct and indirect +** changes. Finally, if pOp is not NULL, then *pOp is set to one of +** [SQLITE_INSERT], [SQLITE_DELETE] or [SQLITE_UPDATE], depending on the +** type of change that the iterator currently points to. +** +** If no error occurs, SQLITE_OK is returned. If an error does occur, an +** SQLite error code is returned. The values of the output variables may not +** be trusted in this case. +*/ +int sqlite3changeset_op( + sqlite3_changeset_iter *pIter, /* Iterator object */ + const char **pzTab, /* OUT: Pointer to table name */ + int *pnCol, /* OUT: Number of columns in table */ + int *pOp, /* OUT: SQLITE_INSERT, DELETE or UPDATE */ + int *pbIndirect /* OUT: True for an 'indirect' change */ +); + +/* +** CAPI3REF: Obtain The Primary Key Definition Of A Table +** +** For each modified table, a changeset includes the following: +** +** <ul> +** <li> The number of columns in the table, and +** <li> Which of those columns make up the tables PRIMARY KEY. +** </ul> +** +** This function is used to find which columns comprise the PRIMARY KEY of +** the table modified by the change that iterator pIter currently points to. +** If successful, *pabPK is set to point to an array of nCol entries, where +** nCol is the number of columns in the table. Elements of *pabPK are set to +** 0x01 if the corresponding column is part of the tables primary key, or +** 0x00 if it is not. +** +** If argumet pnCol is not NULL, then *pnCol is set to the number of columns +** in the table. +** +** If this function is called when the iterator does not point to a valid +** entry, SQLITE_MISUSE is returned and the output variables zeroed. Otherwise, +** SQLITE_OK is returned and the output variables populated as described +** above. +*/ +int sqlite3changeset_pk( + sqlite3_changeset_iter *pIter, /* Iterator object */ + unsigned char **pabPK, /* OUT: Array of boolean - true for PK cols */ + int *pnCol /* OUT: Number of entries in output array */ +); + +/* +** CAPI3REF: Obtain old.* Values From A Changeset Iterator +** +** The pIter argument passed to this function may either be an iterator +** passed to a conflict-handler by [sqlite3changeset_apply()], or an iterator +** created by [sqlite3changeset_start()]. In the latter case, the most recent +** call to [sqlite3changeset_next()] must have returned SQLITE_ROW. +** Furthermore, it may only be called if the type of change that the iterator +** currently points to is either [SQLITE_DELETE] or [SQLITE_UPDATE]. Otherwise, +** this function returns [SQLITE_MISUSE] and sets *ppValue to NULL. +** +** Argument iVal must be greater than or equal to 0, and less than the number +** of columns in the table affected by the current change. Otherwise, +** [SQLITE_RANGE] is returned and *ppValue is set to NULL. +** +** If successful, this function sets *ppValue to point to a protected +** sqlite3_value object containing the iVal'th value from the vector of +** original row values stored as part of the UPDATE or DELETE change and +** returns SQLITE_OK. The name of the function comes from the fact that this +** is similar to the "old.*" columns available to update or delete triggers. +** +** If some other error occurs (e.g. an OOM condition), an SQLite error code +** is returned and *ppValue is set to NULL. +*/ +int sqlite3changeset_old( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int iVal, /* Column number */ + sqlite3_value **ppValue /* OUT: Old value (or NULL pointer) */ +); + +/* +** CAPI3REF: Obtain new.* Values From A Changeset Iterator +** +** The pIter argument passed to this function may either be an iterator +** passed to a conflict-handler by [sqlite3changeset_apply()], or an iterator +** created by [sqlite3changeset_start()]. In the latter case, the most recent +** call to [sqlite3changeset_next()] must have returned SQLITE_ROW. +** Furthermore, it may only be called if the type of change that the iterator +** currently points to is either [SQLITE_UPDATE] or [SQLITE_INSERT]. Otherwise, +** this function returns [SQLITE_MISUSE] and sets *ppValue to NULL. +** +** Argument iVal must be greater than or equal to 0, and less than the number +** of columns in the table affected by the current change. Otherwise, +** [SQLITE_RANGE] is returned and *ppValue is set to NULL. +** +** If successful, this function sets *ppValue to point to a protected +** sqlite3_value object containing the iVal'th value from the vector of +** new row values stored as part of the UPDATE or INSERT change and +** returns SQLITE_OK. If the change is an UPDATE and does not include +** a new value for the requested column, *ppValue is set to NULL and +** SQLITE_OK returned. The name of the function comes from the fact that +** this is similar to the "new.*" columns available to update or delete +** triggers. +** +** If some other error occurs (e.g. an OOM condition), an SQLite error code +** is returned and *ppValue is set to NULL. +*/ +int sqlite3changeset_new( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int iVal, /* Column number */ + sqlite3_value **ppValue /* OUT: New value (or NULL pointer) */ +); + +/* +** CAPI3REF: Obtain Conflicting Row Values From A Changeset Iterator +** +** This function should only be used with iterator objects passed to a +** conflict-handler callback by [sqlite3changeset_apply()] with either +** [SQLITE_CHANGESET_DATA] or [SQLITE_CHANGESET_CONFLICT]. If this function +** is called on any other iterator, [SQLITE_MISUSE] is returned and *ppValue +** is set to NULL. +** +** Argument iVal must be greater than or equal to 0, and less than the number +** of columns in the table affected by the current change. Otherwise, +** [SQLITE_RANGE] is returned and *ppValue is set to NULL. +** +** If successful, this function sets *ppValue to point to a protected +** sqlite3_value object containing the iVal'th value from the +** "conflicting row" associated with the current conflict-handler callback +** and returns SQLITE_OK. +** +** If some other error occurs (e.g. an OOM condition), an SQLite error code +** is returned and *ppValue is set to NULL. +*/ +int sqlite3changeset_conflict( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int iVal, /* Column number */ + sqlite3_value **ppValue /* OUT: Value from conflicting row */ +); + +/* +** CAPI3REF: Determine The Number Of Foreign Key Constraint Violations +** +** This function may only be called with an iterator passed to an +** SQLITE_CHANGESET_FOREIGN_KEY conflict handler callback. In this case +** it sets the output variable to the total number of known foreign key +** violations in the destination database and returns SQLITE_OK. +** +** In all other cases this function returns SQLITE_MISUSE. +*/ +int sqlite3changeset_fk_conflicts( + sqlite3_changeset_iter *pIter, /* Changeset iterator */ + int *pnOut /* OUT: Number of FK violations */ +); + + +/* +** CAPI3REF: Finalize A Changeset Iterator +** +** This function is used to finalize an iterator allocated with +** [sqlite3changeset_start()]. +** +** This function should only be called on iterators created using the +** [sqlite3changeset_start()] function. If an application calls this +** function with an iterator passed to a conflict-handler by +** [sqlite3changeset_apply()], [SQLITE_MISUSE] is immediately returned and the +** call has no effect. +** +** If an error was encountered within a call to an sqlite3changeset_xxx() +** function (for example an [SQLITE_CORRUPT] in [sqlite3changeset_next()] or an +** [SQLITE_NOMEM] in [sqlite3changeset_new()]) then an error code corresponding +** to that error is returned by this function. Otherwise, SQLITE_OK is +** returned. This is to allow the following pattern (pseudo-code): +** +** sqlite3changeset_start(); +** while( SQLITE_ROW==sqlite3changeset_next() ){ +** // Do something with change. +** } +** rc = sqlite3changeset_finalize(); +** if( rc!=SQLITE_OK ){ +** // An error has occurred +** } +*/ +int sqlite3changeset_finalize(sqlite3_changeset_iter *pIter); + +/* +** CAPI3REF: Invert A Changeset +** +** This function is used to "invert" a changeset object. Applying an inverted +** changeset to a database reverses the effects of applying the uninverted +** changeset. Specifically: +** +** <ul> +** <li> Each DELETE change is changed to an INSERT, and +** <li> Each INSERT change is changed to a DELETE, and +** <li> For each UPDATE change, the old.* and new.* values are exchanged. +** </ul> +** +** If successful, a pointer to a buffer containing the inverted changeset +** is stored in *ppOut, the size of the same buffer is stored in *pnOut, and +** SQLITE_OK is returned. If an error occurs, both *pnOut and *ppOut are +** zeroed and an SQLite error code returned. +** +** It is the responsibility of the caller to eventually call sqlite3_free() +** on the *ppOut pointer to free the buffer allocation following a successful +** call to this function. +** +** WARNING/TODO: This function currently assumes that the input is a valid +** changeset. If it is not, the results are undefined. +*/ +int sqlite3changeset_invert( + int nIn, const void *pIn, /* Input changeset */ + int *pnOut, void **ppOut /* OUT: Inverse of input */ +); + +/* +** CAPI3REF: Concatenate Two Changeset Objects +** +** This function is used to concatenate two changesets, A and B, into a +** single changeset. The result is a changeset equivalent to applying +** changeset A followed by changeset B. +** +** Rows are identified by the values in their PRIMARY KEY columns. A change +** in changeset A is considered to apply to the same row as a change in +** changeset B if the two rows have the same primary key. +** +** Changes to rows that appear only in changeset A or B are copied into the +** output changeset. Or, if both changeset A and B contain a change that +** applies to a single row, the output depends on the type of each change, +** as follows: +** +** <table border=1 style="margin-left:8ex;margin-right:8ex"> +** <tr><th style="white-space:pre">Change A </th> +** <th style="white-space:pre">Change B </th> +** <th>Output Change +** <tr><td>INSERT <td>INSERT <td> +** Change A is copied into the output changeset. Change B is discarded. +** This case does not occur if changeset B is recorded immediately after +** changeset A. +** <tr><td>INSERT <td>UPDATE <td> +** An INSERT change is copied into the output changeset. The values in +** the INSERT change are as if the row was inserted by change A and then +** updated according to change B. +** <tr><td>INSERT <td>DELETE <td> +** No change at all is copied into the output changeset. +** <tr><td>UPDATE <td>INSERT <td> +** Change A is copied into the output changeset. Change B is discarded. +** This case does not occur if changeset B is recorded immediately after +** changeset A. +** <tr><td>UPDATE <td>UPDATE <td> +** A single UPDATE is copied into the output changeset. The accompanying +** values are as if the row was updated once by change A and then again +** by change B. +** <tr><td>UPDATE <td>DELETE <td> +** A single DELETE is copied into the output changeset. +** <tr><td>DELETE <td>INSERT <td> +** If one or more of the column values in the row inserted by change +** B differ from those in the row deleted by change A, an UPDATE +** change is added to the output changeset. Otherwise, if the inserted +** row is exactly the same as the deleted row, no change is added to +** the output changeset. +** <tr><td>DELETE <td>UPDATE <td> +** Change A is copied into the output changeset. Change B is discarded. +** This case does not occur if changeset B is recorded immediately after +** changeset A. +** <tr><td>DELETE <td>DELETE <td> +** Change A is copied into the output changeset. Change B is discarded. +** This case does not occur if changeset B is recorded immediately after +** changeset A. +** </table> +** +** If the two changesets contain changes to the same table, then the number +** of columns and the position of the primary key columns for the table must +** be the same in each changeset. If this is not the case, attempting to +** concatenate the two changesets together fails and this function returns +** SQLITE_SCHEMA. If either of the two input changesets appear to be corrupt, +** and the corruption is detected, SQLITE_CORRUPT is returned. Or, if an +** out-of-memory condition occurs during processing, this function returns +** SQLITE_NOMEM. +** +** If none of the above errors occur, SQLITE_OK is returned and *ppOut set +** to point to a buffer containing the output changeset. It is the +** responsibility of the caller to eventually call sqlite3_free() on *ppOut +** to release memory allocated for the buffer. *pnOut is set to the number +** of bytes in the output changeset. If an error does occur, both *ppOut and +** *pnOut are set to zero before returning. +*/ +int sqlite3changeset_concat( + int nA, /* Number of bytes in buffer pA */ + void *pA, /* Pointer to buffer containing changeset A */ + int nB, /* Number of bytes in buffer pB */ + void *pB, /* Pointer to buffer containing changeset B */ + int *pnOut, /* OUT: Number of bytes in output changeset */ + void **ppOut /* OUT: Buffer containing output changeset */ +); + +/* +** CAPI3REF: Apply A Changeset To A Database +** +** Apply a changeset to a database. This function attempts to update the +** "main" database attached to handle db with the changes found in the +** changeset passed via the second and third arguments. +** +** The fourth argument (xFilter) passed to this function is the "filter +** callback". If it is not NULL, then for each table affected by at least one +** change in the changeset, the filter callback is invoked with +** the table name as the second argument, and a copy of the context pointer +** passed as the sixth argument to this function as the first. If the "filter +** callback" returns zero, then no attempt is made to apply any changes to +** the table. Otherwise, if the return value is non-zero or the xFilter +** argument to this function is NULL, all changes related to the table are +** attempted. +** +** For each table that is not excluded by the filter callback, this function +** tests that the target database contains a compatible table. A table is +** considered compatible if all of the following are true: +** +** <ul> +** <li> The table has the same name as the name recorded in the +** changeset, and +** <li> The table has the same number of columns as recorded in the +** changeset, and +** <li> The table has primary key columns in the same position as +** recorded in the changeset. +** </ul> +** +** If there is no compatible table, it is not an error, but none of the +** changes associated with the table are applied. A warning message is issued +** via the sqlite3_log() mechanism with the error code SQLITE_SCHEMA. At most +** one such warning is issued for each table in the changeset. +** +** For each change for which there is a compatible table, an attempt is made +** to modify the table contents according to the UPDATE, INSERT or DELETE +** change. If a change cannot be applied cleanly, the conflict handler +** function passed as the fifth argument to sqlite3changeset_apply() may be +** invoked. A description of exactly when the conflict handler is invoked for +** each type of change is below. +** +** Unlike the xFilter argument, xConflict may not be passed NULL. The results +** of passing anything other than a valid function pointer as the xConflict +** argument are undefined. +** +** Each time the conflict handler function is invoked, it must return one +** of [SQLITE_CHANGESET_OMIT], [SQLITE_CHANGESET_ABORT] or +** [SQLITE_CHANGESET_REPLACE]. SQLITE_CHANGESET_REPLACE may only be returned +** if the second argument passed to the conflict handler is either +** SQLITE_CHANGESET_DATA or SQLITE_CHANGESET_CONFLICT. If the conflict-handler +** returns an illegal value, any changes already made are rolled back and +** the call to sqlite3changeset_apply() returns SQLITE_MISUSE. Different +** actions are taken by sqlite3changeset_apply() depending on the value +** returned by each invocation of the conflict-handler function. Refer to +** the documentation for the three +** [SQLITE_CHANGESET_OMIT|available return values] for details. +** +** <dl> +** <dt>DELETE Changes<dd> +** For each DELETE change, this function checks if the target database +** contains a row with the same primary key value (or values) as the +** original row values stored in the changeset. If it does, and the values +** stored in all non-primary key columns also match the values stored in +** the changeset the row is deleted from the target database. +** +** If a row with matching primary key values is found, but one or more of +** the non-primary key fields contains a value different from the original +** row value stored in the changeset, the conflict-handler function is +** invoked with [SQLITE_CHANGESET_DATA] as the second argument. +** +** If no row with matching primary key values is found in the database, +** the conflict-handler function is invoked with [SQLITE_CHANGESET_NOTFOUND] +** passed as the second argument. +** +** If the DELETE operation is attempted, but SQLite returns SQLITE_CONSTRAINT +** (which can only happen if a foreign key constraint is violated), the +** conflict-handler function is invoked with [SQLITE_CHANGESET_CONSTRAINT] +** passed as the second argument. This includes the case where the DELETE +** operation is attempted because an earlier call to the conflict handler +** function returned [SQLITE_CHANGESET_REPLACE]. +** +** <dt>INSERT Changes<dd> +** For each INSERT change, an attempt is made to insert the new row into +** the database. +** +** If the attempt to insert the row fails because the database already +** contains a row with the same primary key values, the conflict handler +** function is invoked with the second argument set to +** [SQLITE_CHANGESET_CONFLICT]. +** +** If the attempt to insert the row fails because of some other constraint +** violation (e.g. NOT NULL or UNIQUE), the conflict handler function is +** invoked with the second argument set to [SQLITE_CHANGESET_CONSTRAINT]. +** This includes the case where the INSERT operation is re-attempted because +** an earlier call to the conflict handler function returned +** [SQLITE_CHANGESET_REPLACE]. +** +** <dt>UPDATE Changes<dd> +** For each UPDATE change, this function checks if the target database +** contains a row with the same primary key value (or values) as the +** original row values stored in the changeset. If it does, and the values +** stored in all non-primary key columns also match the values stored in +** the changeset the row is updated within the target database. +** +** If a row with matching primary key values is found, but one or more of +** the non-primary key fields contains a value different from an original +** row value stored in the changeset, the conflict-handler function is +** invoked with [SQLITE_CHANGESET_DATA] as the second argument. Since +** UPDATE changes only contain values for non-primary key fields that are +** to be modified, only those fields need to match the original values to +** avoid the SQLITE_CHANGESET_DATA conflict-handler callback. +** +** If no row with matching primary key values is found in the database, +** the conflict-handler function is invoked with [SQLITE_CHANGESET_NOTFOUND] +** passed as the second argument. +** +** If the UPDATE operation is attempted, but SQLite returns +** SQLITE_CONSTRAINT, the conflict-handler function is invoked with +** [SQLITE_CHANGESET_CONSTRAINT] passed as the second argument. +** This includes the case where the UPDATE operation is attempted after +** an earlier call to the conflict handler function returned +** [SQLITE_CHANGESET_REPLACE]. +** </dl> +** +** It is safe to execute SQL statements, including those that write to the +** table that the callback related to, from within the xConflict callback. +** This can be used to further customize the applications conflict +** resolution strategy. +** +** All changes made by this function are enclosed in a savepoint transaction. +** If any other error (aside from a constraint failure when attempting to +** write to the target database) occurs, then the savepoint transaction is +** rolled back, restoring the target database to its original state, and an +** SQLite error code returned. +*/ +int sqlite3changeset_apply( + sqlite3 *db, /* Apply change to "main" db of this handle */ + int nChangeset, /* Size of changeset in bytes */ + void *pChangeset, /* Changeset blob */ + int(*xFilter)( + void *pCtx, /* Copy of sixth arg to _apply() */ + const char *zTab /* Table name */ + ), + int(*xConflict)( + void *pCtx, /* Copy of sixth arg to _apply() */ + int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *p /* Handle describing change and conflict */ + ), + void *pCtx /* First argument passed to xConflict */ +); + +/* +** CAPI3REF: Constants Passed To The Conflict Handler +** +** Values that may be passed as the second argument to a conflict-handler. +** +** <dl> +** <dt>SQLITE_CHANGESET_DATA<dd> +** The conflict handler is invoked with CHANGESET_DATA as the second argument +** when processing a DELETE or UPDATE change if a row with the required +** PRIMARY KEY fields is present in the database, but one or more other +** (non primary-key) fields modified by the update do not contain the +** expected "before" values. +** +** The conflicting row, in this case, is the database row with the matching +** primary key. +** +** <dt>SQLITE_CHANGESET_NOTFOUND<dd> +** The conflict handler is invoked with CHANGESET_NOTFOUND as the second +** argument when processing a DELETE or UPDATE change if a row with the +** required PRIMARY KEY fields is not present in the database. +** +** There is no conflicting row in this case. The results of invoking the +** sqlite3changeset_conflict() API are undefined. +** +** <dt>SQLITE_CHANGESET_CONFLICT<dd> +** CHANGESET_CONFLICT is passed as the second argument to the conflict +** handler while processing an INSERT change if the operation would result +** in duplicate primary key values. +** +** The conflicting row in this case is the database row with the matching +** primary key. +** +** <dt>SQLITE_CHANGESET_FOREIGN_KEY<dd> +** If foreign key handling is enabled, and applying a changeset leaves the +** database in a state containing foreign key violations, the conflict +** handler is invoked with CHANGESET_FOREIGN_KEY as the second argument +** exactly once before the changeset is committed. If the conflict handler +** returns CHANGESET_OMIT, the changes, including those that caused the +** foreign key constraint violation, are committed. Or, if it returns +** CHANGESET_ABORT, the changeset is rolled back. +** +** No current or conflicting row information is provided. The only function +** it is possible to call on the supplied sqlite3_changeset_iter handle +** is sqlite3changeset_fk_conflicts(). +** +** <dt>SQLITE_CHANGESET_CONSTRAINT<dd> +** If any other constraint violation occurs while applying a change (i.e. +** a UNIQUE, CHECK or NOT NULL constraint), the conflict handler is +** invoked with CHANGESET_CONSTRAINT as the second argument. +** +** There is no conflicting row in this case. The results of invoking the +** sqlite3changeset_conflict() API are undefined. +** +** </dl> +*/ +#define SQLITE_CHANGESET_DATA 1 +#define SQLITE_CHANGESET_NOTFOUND 2 +#define SQLITE_CHANGESET_CONFLICT 3 +#define SQLITE_CHANGESET_CONSTRAINT 4 +#define SQLITE_CHANGESET_FOREIGN_KEY 5 + +/* +** CAPI3REF: Constants Returned By The Conflict Handler +** +** A conflict handler callback must return one of the following three values. +** +** <dl> +** <dt>SQLITE_CHANGESET_OMIT<dd> +** If a conflict handler returns this value no special action is taken. The +** change that caused the conflict is not applied. The session module +** continues to the next change in the changeset. +** +** <dt>SQLITE_CHANGESET_REPLACE<dd> +** This value may only be returned if the second argument to the conflict +** handler was SQLITE_CHANGESET_DATA or SQLITE_CHANGESET_CONFLICT. If this +** is not the case, any changes applied so far are rolled back and the +** call to sqlite3changeset_apply() returns SQLITE_MISUSE. +** +** If CHANGESET_REPLACE is returned by an SQLITE_CHANGESET_DATA conflict +** handler, then the conflicting row is either updated or deleted, depending +** on the type of change. +** +** If CHANGESET_REPLACE is returned by an SQLITE_CHANGESET_CONFLICT conflict +** handler, then the conflicting row is removed from the database and a +** second attempt to apply the change is made. If this second attempt fails, +** the original row is restored to the database before continuing. +** +** <dt>SQLITE_CHANGESET_ABORT<dd> +** If this value is returned, any changes applied so far are rolled back +** and the call to sqlite3changeset_apply() returns SQLITE_ABORT. +** </dl> +*/ +#define SQLITE_CHANGESET_OMIT 0 +#define SQLITE_CHANGESET_REPLACE 1 +#define SQLITE_CHANGESET_ABORT 2 + +/* +** CAPI3REF: Streaming Versions of API functions. +** +** The six streaming API xxx_strm() functions serve similar purposes to the +** corresponding non-streaming API functions: +** +** <table border=1 style="margin-left:8ex;margin-right:8ex"> +** <tr><th>Streaming function<th>Non-streaming equivalent</th> +** <tr><td>sqlite3changeset_apply_str<td>[sqlite3changeset_apply] +** <tr><td>sqlite3changeset_concat_str<td>[sqlite3changeset_concat] +** <tr><td>sqlite3changeset_invert_str<td>[sqlite3changeset_invert] +** <tr><td>sqlite3changeset_start_str<td>[sqlite3changeset_start] +** <tr><td>sqlite3session_changeset_str<td>[sqlite3session_changeset] +** <tr><td>sqlite3session_patchset_str<td>[sqlite3session_patchset] +** </table> +** +** Non-streaming functions that accept changesets (or patchsets) as input +** require that the entire changeset be stored in a single buffer in memory. +** Similarly, those that return a changeset or patchset do so by returning +** a pointer to a single large buffer allocated using sqlite3_malloc(). +** Normally this is convenient. However, if an application running in a +** low-memory environment is required to handle very large changesets, the +** large contiguous memory allocations required can become onerous. +** +** In order to avoid this problem, instead of a single large buffer, input +** is passed to a streaming API functions by way of a callback function that +** the sessions module invokes to incrementally request input data as it is +** required. In all cases, a pair of API function parameters such as +** +** <pre> +** int nChangeset, +** void *pChangeset, +** </pre> +** +** Is replaced by: +** +** <pre> +** int (*xInput)(void *pIn, void *pData, int *pnData), +** void *pIn, +** </pre> +** +** Each time the xInput callback is invoked by the sessions module, the first +** argument passed is a copy of the supplied pIn context pointer. The second +** argument, pData, points to a buffer (*pnData) bytes in size. Assuming no +** error occurs the xInput method should copy up to (*pnData) bytes of data +** into the buffer and set (*pnData) to the actual number of bytes copied +** before returning SQLITE_OK. If the input is completely exhausted, (*pnData) +** should be set to zero to indicate this. Or, if an error occurs, an SQLite +** error code should be returned. In all cases, if an xInput callback returns +** an error, all processing is abandoned and the streaming API function +** returns a copy of the error code to the caller. +** +** In the case of sqlite3changeset_start_strm(), the xInput callback may be +** invoked by the sessions module at any point during the lifetime of the +** iterator. If such an xInput callback returns an error, the iterator enters +** an error state, whereby all subsequent calls to iterator functions +** immediately fail with the same error code as returned by xInput. +** +** Similarly, streaming API functions that return changesets (or patchsets) +** return them in chunks by way of a callback function instead of via a +** pointer to a single large buffer. In this case, a pair of parameters such +** as: +** +** <pre> +** int *pnChangeset, +** void **ppChangeset, +** </pre> +** +** Is replaced by: +** +** <pre> +** int (*xOutput)(void *pOut, const void *pData, int nData), +** void *pOut +** </pre> +** +** The xOutput callback is invoked zero or more times to return data to +** the application. The first parameter passed to each call is a copy of the +** pOut pointer supplied by the application. The second parameter, pData, +** points to a buffer nData bytes in size containing the chunk of output +** data being returned. If the xOutput callback successfully processes the +** supplied data, it should return SQLITE_OK to indicate success. Otherwise, +** it should return some other SQLite error code. In this case processing +** is immediately abandoned and the streaming API function returns a copy +** of the xOutput error code to the application. +** +** The sessions module never invokes an xOutput callback with the third +** parameter set to a value less than or equal to zero. Other than this, +** no guarantees are made as to the size of the chunks of data returned. +*/ +int sqlite3changeset_apply_strm( + sqlite3 *db, /* Apply change to "main" db of this handle */ + int (*xInput)(void *pIn, void *pData, int *pnData), /* Input function */ + void *pIn, /* First arg for xInput */ + int(*xFilter)( + void *pCtx, /* Copy of sixth arg to _apply() */ + const char *zTab /* Table name */ + ), + int(*xConflict)( + void *pCtx, /* Copy of sixth arg to _apply() */ + int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *p /* Handle describing change and conflict */ + ), + void *pCtx /* First argument passed to xConflict */ +); +int sqlite3changeset_concat_strm( + int (*xInputA)(void *pIn, void *pData, int *pnData), + void *pInA, + int (*xInputB)(void *pIn, void *pData, int *pnData), + void *pInB, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +); +int sqlite3changeset_invert_strm( + int (*xInput)(void *pIn, void *pData, int *pnData), + void *pIn, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +); +int sqlite3changeset_start_strm( + sqlite3_changeset_iter **pp, + int (*xInput)(void *pIn, void *pData, int *pnData), + void *pIn +); +int sqlite3session_changeset_strm( + sqlite3_session *pSession, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +); +int sqlite3session_patchset_strm( + sqlite3_session *pSession, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +); + + +/* +** Make sure we can call this stuff from C++. +*/ +#ifdef __cplusplus +} +#endif + +#endif /* SQLITE_ENABLE_SESSION && SQLITE_ENABLE_PREUPDATE_HOOK */ diff --git a/ext/session/test_session.c b/ext/session/test_session.c new file mode 100644 index 000000000..6c538bcec --- /dev/null +++ b/ext/session/test_session.c @@ -0,0 +1,917 @@ + +#if defined(SQLITE_TEST) && defined(SQLITE_ENABLE_SESSION) \ + && defined(SQLITE_ENABLE_PREUPDATE_HOOK) + +#include "sqlite3session.h" +#include <assert.h> +#include <string.h> +#include <tcl.h> + +typedef struct TestSession TestSession; +struct TestSession { + sqlite3_session *pSession; + Tcl_Interp *interp; + Tcl_Obj *pFilterScript; +}; + +typedef struct TestStreamInput TestStreamInput; +struct TestStreamInput { + int nStream; /* Maximum chunk size */ + unsigned char *aData; /* Pointer to buffer containing data */ + int nData; /* Size of buffer aData in bytes */ + int iData; /* Bytes of data already read by sessions */ +}; + +#define SESSION_STREAM_TCL_VAR "sqlite3session_streams" + +/* +** Attempt to find the global variable zVar within interpreter interp +** and extract an integer value from it. Return this value. +** +** If the named variable cannot be found, or if it cannot be interpreted +** as a integer, return 0. +*/ +static int test_tcl_integer(Tcl_Interp *interp, const char *zVar){ + Tcl_Obj *pObj; + int iVal = 0; + pObj = Tcl_ObjGetVar2(interp, Tcl_NewStringObj(zVar, -1), 0, TCL_GLOBAL_ONLY); + if( pObj ) Tcl_GetIntFromObj(0, pObj, &iVal); + return iVal; +} + +static int test_session_error(Tcl_Interp *interp, int rc){ + extern const char *sqlite3ErrName(int); + Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1)); + return TCL_ERROR; +} + +static int test_table_filter(void *pCtx, const char *zTbl){ + TestSession *p = (TestSession*)pCtx; + Tcl_Obj *pEval; + int rc; + int bRes = 0; + + pEval = Tcl_DuplicateObj(p->pFilterScript); + Tcl_IncrRefCount(pEval); + rc = Tcl_ListObjAppendElement(p->interp, pEval, Tcl_NewStringObj(zTbl, -1)); + if( rc==TCL_OK ){ + rc = Tcl_EvalObjEx(p->interp, pEval, TCL_EVAL_GLOBAL); + } + if( rc==TCL_OK ){ + rc = Tcl_GetBooleanFromObj(p->interp, Tcl_GetObjResult(p->interp), &bRes); + } + if( rc!=TCL_OK ){ + /* printf("error: %s\n", Tcl_GetStringResult(p->interp)); */ + Tcl_BackgroundError(p->interp); + } + Tcl_DecrRefCount(pEval); + + return bRes; +} + +struct TestSessionsBlob { + void *p; + int n; +}; +typedef struct TestSessionsBlob TestSessionsBlob; + +static int testStreamOutput( + void *pCtx, + const void *pData, + int nData +){ + TestSessionsBlob *pBlob = (TestSessionsBlob*)pCtx; + char *pNew; + + assert( nData>0 ); + pNew = (char*)sqlite3_realloc(pBlob->p, pBlob->n + nData); + if( pNew==0 ){ + return SQLITE_NOMEM; + } + pBlob->p = (void*)pNew; + memcpy(&pNew[pBlob->n], pData, nData); + pBlob->n += nData; + return SQLITE_OK; +} + +/* +** Tclcmd: $session attach TABLE +** $session changeset +** $session delete +** $session enable BOOL +** $session indirect INTEGER +** $session patchset +** $session table_filter SCRIPT +*/ +static int test_session_cmd( + void *clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + TestSession *p = (TestSession*)clientData; + sqlite3_session *pSession = p->pSession; + struct SessionSubcmd { + const char *zSub; + int nArg; + const char *zMsg; + int iSub; + } aSub[] = { + { "attach", 1, "TABLE", }, /* 0 */ + { "changeset", 0, "", }, /* 1 */ + { "delete", 0, "", }, /* 2 */ + { "enable", 1, "BOOL", }, /* 3 */ + { "indirect", 1, "BOOL", }, /* 4 */ + { "isempty", 0, "", }, /* 5 */ + { "table_filter", 1, "SCRIPT", }, /* 6 */ + { "patchset", 0, "", }, /* 7 */ + { 0 } + }; + int iSub; + int rc; + + if( objc<2 ){ + Tcl_WrongNumArgs(interp, 1, objv, "SUBCOMMAND ..."); + return TCL_ERROR; + } + rc = Tcl_GetIndexFromObjStruct(interp, + objv[1], aSub, sizeof(aSub[0]), "sub-command", 0, &iSub + ); + if( rc!=TCL_OK ) return rc; + if( objc!=2+aSub[iSub].nArg ){ + Tcl_WrongNumArgs(interp, 2, objv, aSub[iSub].zMsg); + return TCL_ERROR; + } + + switch( iSub ){ + case 0: { /* attach */ + char *zArg = Tcl_GetString(objv[2]); + if( zArg[0]=='*' && zArg[1]=='\0' ) zArg = 0; + rc = sqlite3session_attach(pSession, zArg); + if( rc!=SQLITE_OK ){ + return test_session_error(interp, rc); + } + break; + } + + case 7: /* patchset */ + case 1: { /* changeset */ + TestSessionsBlob o = {0, 0}; + if( test_tcl_integer(interp, SESSION_STREAM_TCL_VAR) ){ + void *pCtx = (void*)&o; + if( iSub==7 ){ + rc = sqlite3session_patchset_strm(pSession, testStreamOutput, pCtx); + }else{ + rc = sqlite3session_changeset_strm(pSession, testStreamOutput, pCtx); + } + }else{ + if( iSub==7 ){ + rc = sqlite3session_patchset(pSession, &o.n, &o.p); + }else{ + rc = sqlite3session_changeset(pSession, &o.n, &o.p); + } + } + if( rc==SQLITE_OK ){ + Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(o.p, o.n)); + } + sqlite3_free(o.p); + if( rc!=SQLITE_OK ){ + return test_session_error(interp, rc); + } + break; + } + + case 2: /* delete */ + Tcl_DeleteCommand(interp, Tcl_GetString(objv[0])); + break; + + case 3: { /* enable */ + int val; + if( Tcl_GetIntFromObj(interp, objv[2], &val) ) return TCL_ERROR; + val = sqlite3session_enable(pSession, val); + Tcl_SetObjResult(interp, Tcl_NewBooleanObj(val)); + break; + } + + case 4: { /* indirect */ + int val; + if( Tcl_GetIntFromObj(interp, objv[2], &val) ) return TCL_ERROR; + val = sqlite3session_indirect(pSession, val); + Tcl_SetObjResult(interp, Tcl_NewBooleanObj(val)); + break; + } + + case 5: { /* isempty */ + int val; + val = sqlite3session_isempty(pSession); + Tcl_SetObjResult(interp, Tcl_NewBooleanObj(val)); + break; + } + + case 6: { /* table_filter */ + if( p->pFilterScript ) Tcl_DecrRefCount(p->pFilterScript); + p->interp = interp; + p->pFilterScript = Tcl_DuplicateObj(objv[2]); + Tcl_IncrRefCount(p->pFilterScript); + sqlite3session_table_filter(pSession, test_table_filter, clientData); + break; + } + } + + return TCL_OK; +} + +static void test_session_del(void *clientData){ + TestSession *p = (TestSession*)clientData; + if( p->pFilterScript ) Tcl_DecrRefCount(p->pFilterScript); + sqlite3session_delete(p->pSession); + ckfree((char*)p); +} + +/* +** Tclcmd: sqlite3session CMD DB-HANDLE DB-NAME +*/ +static int test_sqlite3session( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + sqlite3 *db; + Tcl_CmdInfo info; + int rc; /* sqlite3session_create() return code */ + TestSession *p; /* New wrapper object */ + + if( objc!=4 ){ + Tcl_WrongNumArgs(interp, 1, objv, "CMD DB-HANDLE DB-NAME"); + return TCL_ERROR; + } + + if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(objv[2]), &info) ){ + Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(objv[2]), 0); + return TCL_ERROR; + } + db = *(sqlite3 **)info.objClientData; + + p = (TestSession*)ckalloc(sizeof(TestSession)); + memset(p, 0, sizeof(TestSession)); + rc = sqlite3session_create(db, Tcl_GetString(objv[3]), &p->pSession); + if( rc!=SQLITE_OK ){ + ckfree((char*)p); + return test_session_error(interp, rc); + } + + Tcl_CreateObjCommand( + interp, Tcl_GetString(objv[1]), test_session_cmd, (ClientData)p, + test_session_del + ); + Tcl_SetObjResult(interp, objv[1]); + return TCL_OK; +} + +static void test_append_value(Tcl_Obj *pList, sqlite3_value *pVal){ + if( pVal==0 ){ + Tcl_ListObjAppendElement(0, pList, Tcl_NewObj()); + Tcl_ListObjAppendElement(0, pList, Tcl_NewObj()); + }else{ + Tcl_Obj *pObj; + switch( sqlite3_value_type(pVal) ){ + case SQLITE_NULL: + Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("n", 1)); + pObj = Tcl_NewObj(); + break; + case SQLITE_INTEGER: + Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("i", 1)); + pObj = Tcl_NewWideIntObj(sqlite3_value_int64(pVal)); + break; + case SQLITE_FLOAT: + Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("f", 1)); + pObj = Tcl_NewDoubleObj(sqlite3_value_double(pVal)); + break; + case SQLITE_TEXT: { + const char *z = (char*)sqlite3_value_blob(pVal); + int n = sqlite3_value_bytes(pVal); + Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("t", 1)); + pObj = Tcl_NewStringObj(z, n); + break; + } + case SQLITE_BLOB: + Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("b", 1)); + pObj = Tcl_NewByteArrayObj( + sqlite3_value_blob(pVal), + sqlite3_value_bytes(pVal) + ); + break; + } + Tcl_ListObjAppendElement(0, pList, pObj); + } +} + +typedef struct TestConflictHandler TestConflictHandler; +struct TestConflictHandler { + Tcl_Interp *interp; + Tcl_Obj *pConflictScript; + Tcl_Obj *pFilterScript; +}; + +static int test_obj_eq_string(Tcl_Obj *p, const char *z){ + int n; + int nObj; + char *zObj; + + n = (int)strlen(z); + zObj = Tcl_GetStringFromObj(p, &nObj); + + return (nObj==n && (n==0 || 0==memcmp(zObj, z, n))); +} + +static int test_filter_handler( + void *pCtx, /* Pointer to TestConflictHandler structure */ + const char *zTab /* Table name */ +){ + TestConflictHandler *p = (TestConflictHandler *)pCtx; + int res = 1; + Tcl_Obj *pEval; + Tcl_Interp *interp = p->interp; + + pEval = Tcl_DuplicateObj(p->pFilterScript); + Tcl_IncrRefCount(pEval); + + if( TCL_OK!=Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj(zTab, -1)) + || TCL_OK!=Tcl_EvalObjEx(interp, pEval, TCL_EVAL_GLOBAL) + || TCL_OK!=Tcl_GetIntFromObj(interp, Tcl_GetObjResult(interp), &res) + ){ + Tcl_BackgroundError(interp); + } + + Tcl_DecrRefCount(pEval); + return res; +} + +static int test_conflict_handler( + void *pCtx, /* Pointer to TestConflictHandler structure */ + int eConf, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *pIter /* Handle describing change and conflict */ +){ + TestConflictHandler *p = (TestConflictHandler *)pCtx; + Tcl_Obj *pEval; + Tcl_Interp *interp = p->interp; + int ret = 0; /* Return value */ + + int op; /* SQLITE_UPDATE, DELETE or INSERT */ + const char *zTab; /* Name of table conflict is on */ + int nCol; /* Number of columns in table zTab */ + + pEval = Tcl_DuplicateObj(p->pConflictScript); + Tcl_IncrRefCount(pEval); + + sqlite3changeset_op(pIter, &zTab, &nCol, &op, 0); + + if( eConf==SQLITE_CHANGESET_FOREIGN_KEY ){ + int nFk; + sqlite3changeset_fk_conflicts(pIter, &nFk); + Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj("FOREIGN_KEY", -1)); + Tcl_ListObjAppendElement(0, pEval, Tcl_NewIntObj(nFk)); + }else{ + + /* Append the operation type. */ + Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj( + op==SQLITE_INSERT ? "INSERT" : + op==SQLITE_UPDATE ? "UPDATE" : + "DELETE", -1 + )); + + /* Append the table name. */ + Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj(zTab, -1)); + + /* Append the conflict type. */ + switch( eConf ){ + case SQLITE_CHANGESET_DATA: + Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("DATA",-1)); + break; + case SQLITE_CHANGESET_NOTFOUND: + Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("NOTFOUND",-1)); + break; + case SQLITE_CHANGESET_CONFLICT: + Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("CONFLICT",-1)); + break; + case SQLITE_CHANGESET_CONSTRAINT: + Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("CONSTRAINT",-1)); + break; + } + + /* If this is not an INSERT, append the old row */ + if( op!=SQLITE_INSERT ){ + int i; + Tcl_Obj *pOld = Tcl_NewObj(); + for(i=0; i<nCol; i++){ + sqlite3_value *pVal; + sqlite3changeset_old(pIter, i, &pVal); + test_append_value(pOld, pVal); + } + Tcl_ListObjAppendElement(0, pEval, pOld); + } + + /* If this is not a DELETE, append the new row */ + if( op!=SQLITE_DELETE ){ + int i; + Tcl_Obj *pNew = Tcl_NewObj(); + for(i=0; i<nCol; i++){ + sqlite3_value *pVal; + sqlite3changeset_new(pIter, i, &pVal); + test_append_value(pNew, pVal); + } + Tcl_ListObjAppendElement(0, pEval, pNew); + } + + /* If this is a CHANGESET_DATA or CHANGESET_CONFLICT conflict, append + ** the conflicting row. */ + if( eConf==SQLITE_CHANGESET_DATA || eConf==SQLITE_CHANGESET_CONFLICT ){ + int i; + Tcl_Obj *pConflict = Tcl_NewObj(); + for(i=0; i<nCol; i++){ + int rc; + sqlite3_value *pVal; + rc = sqlite3changeset_conflict(pIter, i, &pVal); + assert( rc==SQLITE_OK ); + test_append_value(pConflict, pVal); + } + Tcl_ListObjAppendElement(0, pEval, pConflict); + } + + /*********************************************************************** + ** This block is purely for testing some error conditions. + */ + if( eConf==SQLITE_CHANGESET_CONSTRAINT + || eConf==SQLITE_CHANGESET_NOTFOUND + ){ + sqlite3_value *pVal; + int rc = sqlite3changeset_conflict(pIter, 0, &pVal); + assert( rc==SQLITE_MISUSE ); + }else{ + sqlite3_value *pVal; + int rc = sqlite3changeset_conflict(pIter, -1, &pVal); + assert( rc==SQLITE_RANGE ); + rc = sqlite3changeset_conflict(pIter, nCol, &pVal); + assert( rc==SQLITE_RANGE ); + } + if( op==SQLITE_DELETE ){ + sqlite3_value *pVal; + int rc = sqlite3changeset_new(pIter, 0, &pVal); + assert( rc==SQLITE_MISUSE ); + }else{ + sqlite3_value *pVal; + int rc = sqlite3changeset_new(pIter, -1, &pVal); + assert( rc==SQLITE_RANGE ); + rc = sqlite3changeset_new(pIter, nCol, &pVal); + assert( rc==SQLITE_RANGE ); + } + if( op==SQLITE_INSERT ){ + sqlite3_value *pVal; + int rc = sqlite3changeset_old(pIter, 0, &pVal); + assert( rc==SQLITE_MISUSE ); + }else{ + sqlite3_value *pVal; + int rc = sqlite3changeset_old(pIter, -1, &pVal); + assert( rc==SQLITE_RANGE ); + rc = sqlite3changeset_old(pIter, nCol, &pVal); + assert( rc==SQLITE_RANGE ); + } + if( eConf!=SQLITE_CHANGESET_FOREIGN_KEY ){ + /* eConf!=FOREIGN_KEY is always true at this point. The condition is + ** just there to make it clearer what is being tested. */ + int nDummy; + int rc = sqlite3changeset_fk_conflicts(pIter, &nDummy); + assert( rc==SQLITE_MISUSE ); + } + /* End of testing block + ***********************************************************************/ + } + + if( TCL_OK!=Tcl_EvalObjEx(interp, pEval, TCL_EVAL_GLOBAL) ){ + Tcl_BackgroundError(interp); + }else{ + Tcl_Obj *pRes = Tcl_GetObjResult(interp); + if( test_obj_eq_string(pRes, "OMIT") || test_obj_eq_string(pRes, "") ){ + ret = SQLITE_CHANGESET_OMIT; + }else if( test_obj_eq_string(pRes, "REPLACE") ){ + ret = SQLITE_CHANGESET_REPLACE; + }else if( test_obj_eq_string(pRes, "ABORT") ){ + ret = SQLITE_CHANGESET_ABORT; + }else{ + Tcl_GetIntFromObj(0, pRes, &ret); + } + } + + Tcl_DecrRefCount(pEval); + return ret; +} + +/* +** The conflict handler used by sqlite3changeset_apply_replace_all(). +** This conflict handler calls sqlite3_value_text16() on all available +** sqlite3_value objects and then returns CHANGESET_REPLACE, or +** CHANGESET_OMIT if REPLACE is not applicable. This is used to test the +** effect of a malloc failure within an sqlite3_value_xxx() function +** invoked by a conflict-handler callback. +*/ +static int replace_handler( + void *pCtx, /* Pointer to TestConflictHandler structure */ + int eConf, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *pIter /* Handle describing change and conflict */ +){ + int op; /* SQLITE_UPDATE, DELETE or INSERT */ + const char *zTab; /* Name of table conflict is on */ + int nCol; /* Number of columns in table zTab */ + int i; + int x = 0; + + sqlite3changeset_op(pIter, &zTab, &nCol, &op, 0); + + if( op!=SQLITE_INSERT ){ + for(i=0; i<nCol; i++){ + sqlite3_value *pVal; + sqlite3changeset_old(pIter, i, &pVal); + sqlite3_value_text16(pVal); + x++; + } + } + + if( op!=SQLITE_DELETE ){ + for(i=0; i<nCol; i++){ + sqlite3_value *pVal; + sqlite3changeset_new(pIter, i, &pVal); + sqlite3_value_text16(pVal); + x++; + } + } + + if( eConf==SQLITE_CHANGESET_DATA ){ + return SQLITE_CHANGESET_REPLACE; + } + return SQLITE_CHANGESET_OMIT; +} + +static int testStreamInput( + void *pCtx, /* Context pointer */ + void *pData, /* Buffer to populate */ + int *pnData /* IN/OUT: Bytes requested/supplied */ +){ + TestStreamInput *p = (TestStreamInput*)pCtx; + int nReq = *pnData; /* Bytes of data requested */ + int nRem = p->nData - p->iData; /* Bytes of data available */ + int nRet = p->nStream; /* Bytes actually returned */ + + /* Allocate and free some space. There is no point to this, other than + ** that it allows the regular OOM fault-injection tests to cause an error + ** in this function. */ + void *pAlloc = sqlite3_malloc(10); + if( pAlloc==0 ) return SQLITE_NOMEM; + sqlite3_free(pAlloc); + + if( nRet>nReq ) nRet = nReq; + if( nRet>nRem ) nRet = nRem; + + assert( nRet>=0 ); + if( nRet>0 ){ + memcpy(pData, &p->aData[p->iData], nRet); + p->iData += nRet; + } + + *pnData = nRet; + return SQLITE_OK; +} + + +/* +** sqlite3changeset_apply DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT? +*/ +static int test_sqlite3changeset_apply( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + sqlite3 *db; /* Database handle */ + Tcl_CmdInfo info; /* Database Tcl command (objv[1]) info */ + int rc; /* Return code from changeset_invert() */ + void *pChangeset; /* Buffer containing changeset */ + int nChangeset; /* Size of buffer aChangeset in bytes */ + TestConflictHandler ctx; + TestStreamInput sStr; + + memset(&sStr, 0, sizeof(sStr)); + sStr.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR); + + if( objc!=4 && objc!=5 ){ + Tcl_WrongNumArgs(interp, 1, objv, + "DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT?" + ); + return TCL_ERROR; + } + if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(objv[1]), &info) ){ + Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(objv[2]), 0); + return TCL_ERROR; + } + db = *(sqlite3 **)info.objClientData; + pChangeset = (void *)Tcl_GetByteArrayFromObj(objv[2], &nChangeset); + ctx.pConflictScript = objv[3]; + ctx.pFilterScript = objc==5 ? objv[4] : 0; + ctx.interp = interp; + + if( sStr.nStream==0 ){ + rc = sqlite3changeset_apply(db, nChangeset, pChangeset, + (objc==5) ? test_filter_handler : 0, test_conflict_handler, (void *)&ctx + ); + }else{ + sStr.aData = (unsigned char*)pChangeset; + sStr.nData = nChangeset; + rc = sqlite3changeset_apply_strm(db, testStreamInput, (void*)&sStr, + (objc==5) ? test_filter_handler : 0, test_conflict_handler, (void *)&ctx + ); + } + + if( rc!=SQLITE_OK ){ + return test_session_error(interp, rc); + } + Tcl_ResetResult(interp); + return TCL_OK; +} + +/* +** sqlite3changeset_apply_replace_all DB CHANGESET +*/ +static int test_sqlite3changeset_apply_replace_all( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + sqlite3 *db; /* Database handle */ + Tcl_CmdInfo info; /* Database Tcl command (objv[1]) info */ + int rc; /* Return code from changeset_invert() */ + void *pChangeset; /* Buffer containing changeset */ + int nChangeset; /* Size of buffer aChangeset in bytes */ + + if( objc!=3 ){ + Tcl_WrongNumArgs(interp, 1, objv, "DB CHANGESET"); + return TCL_ERROR; + } + if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(objv[1]), &info) ){ + Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(objv[2]), 0); + return TCL_ERROR; + } + db = *(sqlite3 **)info.objClientData; + pChangeset = (void *)Tcl_GetByteArrayFromObj(objv[2], &nChangeset); + + rc = sqlite3changeset_apply(db, nChangeset, pChangeset, 0, replace_handler,0); + if( rc!=SQLITE_OK ){ + return test_session_error(interp, rc); + } + Tcl_ResetResult(interp); + return TCL_OK; +} + + +/* +** sqlite3changeset_invert CHANGESET +*/ +static int test_sqlite3changeset_invert( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + int rc; /* Return code from changeset_invert() */ + TestStreamInput sIn; /* Input stream */ + TestSessionsBlob sOut; /* Output blob */ + + if( objc!=2 ){ + Tcl_WrongNumArgs(interp, 1, objv, "CHANGESET"); + return TCL_ERROR; + } + + memset(&sIn, 0, sizeof(sIn)); + memset(&sOut, 0, sizeof(sOut)); + sIn.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR); + sIn.aData = Tcl_GetByteArrayFromObj(objv[1], &sIn.nData); + + if( sIn.nStream ){ + rc = sqlite3changeset_invert_strm( + testStreamInput, (void*)&sIn, testStreamOutput, (void*)&sOut + ); + }else{ + rc = sqlite3changeset_invert(sIn.nData, sIn.aData, &sOut.n, &sOut.p); + } + if( rc!=SQLITE_OK ){ + rc = test_session_error(interp, rc); + }else{ + Tcl_SetObjResult(interp,Tcl_NewByteArrayObj((unsigned char*)sOut.p,sOut.n)); + } + sqlite3_free(sOut.p); + return rc; +} + +/* +** sqlite3changeset_concat LEFT RIGHT +*/ +static int test_sqlite3changeset_concat( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + int rc; /* Return code from changeset_invert() */ + + TestStreamInput sLeft; /* Input stream */ + TestStreamInput sRight; /* Input stream */ + TestSessionsBlob sOut = {0,0}; /* Output blob */ + + if( objc!=3 ){ + Tcl_WrongNumArgs(interp, 1, objv, "LEFT RIGHT"); + return TCL_ERROR; + } + + memset(&sLeft, 0, sizeof(sLeft)); + memset(&sRight, 0, sizeof(sRight)); + sLeft.aData = Tcl_GetByteArrayFromObj(objv[1], &sLeft.nData); + sRight.aData = Tcl_GetByteArrayFromObj(objv[2], &sRight.nData); + sLeft.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR); + sRight.nStream = sLeft.nStream; + + if( sLeft.nStream>0 ){ + rc = sqlite3changeset_concat_strm( + testStreamInput, (void*)&sLeft, + testStreamInput, (void*)&sRight, + testStreamOutput, (void*)&sOut + ); + }else{ + rc = sqlite3changeset_concat( + sLeft.nData, sLeft.aData, sRight.nData, sRight.aData, &sOut.n, &sOut.p + ); + } + + if( rc!=SQLITE_OK ){ + rc = test_session_error(interp, rc); + }else{ + Tcl_SetObjResult(interp,Tcl_NewByteArrayObj((unsigned char*)sOut.p,sOut.n)); + } + sqlite3_free(sOut.p); + return rc; +} + +/* +** sqlite3session_foreach VARNAME CHANGESET SCRIPT +*/ +static int test_sqlite3session_foreach( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + void *pChangeset; + int nChangeset; + sqlite3_changeset_iter *pIter; + int rc; + Tcl_Obj *pVarname; + Tcl_Obj *pCS; + Tcl_Obj *pScript; + int isCheckNext = 0; + + TestStreamInput sStr; + memset(&sStr, 0, sizeof(sStr)); + + if( objc>1 ){ + char *zOpt = Tcl_GetString(objv[1]); + isCheckNext = (strcmp(zOpt, "-next")==0); + } + if( objc!=4+isCheckNext ){ + Tcl_WrongNumArgs(interp, 1, objv, "?-next? VARNAME CHANGESET SCRIPT"); + return TCL_ERROR; + } + + pVarname = objv[1+isCheckNext]; + pCS = objv[2+isCheckNext]; + pScript = objv[3+isCheckNext]; + + pChangeset = (void *)Tcl_GetByteArrayFromObj(pCS, &nChangeset); + sStr.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR); + if( sStr.nStream==0 ){ + rc = sqlite3changeset_start(&pIter, nChangeset, pChangeset); + }else{ + sStr.aData = (unsigned char*)pChangeset; + sStr.nData = nChangeset; + rc = sqlite3changeset_start_strm(&pIter, testStreamInput, (void*)&sStr); + } + if( rc!=SQLITE_OK ){ + return test_session_error(interp, rc); + } + + while( SQLITE_ROW==sqlite3changeset_next(pIter) ){ + int nCol; /* Number of columns in table */ + int nCol2; /* Number of columns in table */ + int op; /* SQLITE_INSERT, UPDATE or DELETE */ + const char *zTab; /* Name of table change applies to */ + Tcl_Obj *pVar; /* Tcl value to set $VARNAME to */ + Tcl_Obj *pOld; /* Vector of old.* values */ + Tcl_Obj *pNew; /* Vector of new.* values */ + int bIndirect; + + char *zPK; + unsigned char *abPK; + int i; + + /* Test that _fk_conflicts() returns SQLITE_MISUSE if called on this + ** iterator. */ + int nDummy; + if( SQLITE_MISUSE!=sqlite3changeset_fk_conflicts(pIter, &nDummy) ){ + sqlite3changeset_finalize(pIter); + return TCL_ERROR; + } + + sqlite3changeset_op(pIter, &zTab, &nCol, &op, &bIndirect); + pVar = Tcl_NewObj(); + Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj( + op==SQLITE_INSERT ? "INSERT" : + op==SQLITE_UPDATE ? "UPDATE" : + "DELETE", -1 + )); + + Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj(zTab, -1)); + Tcl_ListObjAppendElement(0, pVar, Tcl_NewBooleanObj(bIndirect)); + + zPK = ckalloc(nCol+1); + memset(zPK, 0, nCol+1); + sqlite3changeset_pk(pIter, &abPK, &nCol2); + assert( nCol==nCol2 ); + for(i=0; i<nCol; i++){ + zPK[i] = (abPK[i] ? 'X' : '.'); + } + Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj(zPK, -1)); + ckfree(zPK); + + pOld = Tcl_NewObj(); + if( op!=SQLITE_INSERT ){ + int i; + for(i=0; i<nCol; i++){ + sqlite3_value *pVal; + sqlite3changeset_old(pIter, i, &pVal); + test_append_value(pOld, pVal); + } + } + pNew = Tcl_NewObj(); + if( op!=SQLITE_DELETE ){ + int i; + for(i=0; i<nCol; i++){ + sqlite3_value *pVal; + sqlite3changeset_new(pIter, i, &pVal); + test_append_value(pNew, pVal); + } + } + Tcl_ListObjAppendElement(0, pVar, pOld); + Tcl_ListObjAppendElement(0, pVar, pNew); + + Tcl_ObjSetVar2(interp, pVarname, 0, pVar, 0); + rc = Tcl_EvalObjEx(interp, pScript, 0); + if( rc!=TCL_OK && rc!=TCL_CONTINUE ){ + sqlite3changeset_finalize(pIter); + return rc==TCL_BREAK ? TCL_OK : rc; + } + } + + if( isCheckNext ){ + int rc2 = sqlite3changeset_next(pIter); + rc = sqlite3changeset_finalize(pIter); + assert( (rc2==SQLITE_DONE && rc==SQLITE_OK) || rc2==rc ); + }else{ + rc = sqlite3changeset_finalize(pIter); + } + if( rc!=SQLITE_OK ){ + return test_session_error(interp, rc); + } + + return TCL_OK; +} + +int TestSession_Init(Tcl_Interp *interp){ + Tcl_CreateObjCommand(interp, "sqlite3session", test_sqlite3session, 0, 0); + Tcl_CreateObjCommand( + interp, "sqlite3session_foreach", test_sqlite3session_foreach, 0, 0 + ); + Tcl_CreateObjCommand( + interp, "sqlite3changeset_invert", test_sqlite3changeset_invert, 0, 0 + ); + Tcl_CreateObjCommand( + interp, "sqlite3changeset_concat", test_sqlite3changeset_concat, 0, 0 + ); + Tcl_CreateObjCommand( + interp, "sqlite3changeset_apply", test_sqlite3changeset_apply, 0, 0 + ); + Tcl_CreateObjCommand( + interp, "sqlite3changeset_apply_replace_all", + test_sqlite3changeset_apply_replace_all, 0, 0 + ); + return TCL_OK; +} + +#endif /* SQLITE_TEST && SQLITE_SESSION && SQLITE_PREUPDATE_HOOK */ @@ -47,6 +47,7 @@ TCCX = $(TCC) $(OPTS) -I. -I$(TOP)/src -I$(TOP) TCCX += -I$(TOP)/ext/rtree -I$(TOP)/ext/icu -I$(TOP)/ext/fts3 TCCX += -I$(TOP)/ext/async -I$(TOP)/ext/userauth +TCCX += -I$(TOP)/ext/session # Object files for the SQLite library. # @@ -71,6 +72,8 @@ LIBOBJ+= vdbe.o parse.o \ vdbeapi.o vdbeaux.o vdbeblob.o vdbemem.o vdbesort.o \ vdbetrace.o wal.o walker.o where.o utf.o vtab.o +LIBOBJ += sqlite3session.o + # All of the source code files. @@ -217,6 +220,9 @@ SRC += \ $(TOP)/ext/rtree/rtree.h \ $(TOP)/ext/rtree/rtree.c SRC += \ + $(TOP)/ext/session/sqlite3session.c \ + $(TOP)/ext/session/sqlite3session.h +SRC += \ $(TOP)/ext/userauth/userauth.c \ $(TOP)/ext/userauth/sqlite3userauth.h @@ -337,7 +343,9 @@ 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/session/sqlite3session.c \ + $(TOP)/ext/session/test_session.c # Header files used by all library source files. # @@ -427,6 +435,7 @@ target_source: $(SRC) $(TOP)/tool/vdbe-compress.tcl sqlite3.c: target_source $(TOP)/tool/mksqlite3c.tcl tclsh $(TOP)/tool/mksqlite3c.tcl cp tsrc/shell.c tsrc/sqlite3ext.h . + cp $(TOP)/ext/session/sqlite3session.h . echo '#ifndef USE_SYSTEM_SQLITE' >tclsqlite3.c cat sqlite3.c >>tclsqlite3.c echo '#endif /* USE_SYSTEM_SQLITE */' >>tclsqlite3.c @@ -571,6 +580,9 @@ rtree.o: $(TOP)/ext/rtree/rtree.c $(HDR) $(EXTHDR) userauth.o: $(TOP)/ext/userauth/userauth.c $(HDR) $(EXTHDR) $(TCCX) -DSQLITE_CORE -c $(TOP)/ext/userauth/userauth.c +sqlite3session.o: $(TOP)/ext/session/sqlite3session.c $(HDR) $(EXTHDR) + $(TCCX) -DSQLITE_CORE -c $(TOP)/ext/session/sqlite3session.c + # Rules for building test programs and for running tests # @@ -578,7 +590,8 @@ tclsqlite3: $(TOP)/src/tclsqlite.c libsqlite3.a $(TCCX) $(TCL_FLAGS) -DTCLSH=1 -o tclsqlite3 \ $(TOP)/src/tclsqlite.c libsqlite3.a $(LIBTCL) $(THREADLIB) -sqlite3_analyzer.c: sqlite3.c $(TOP)/src/test_stat.c $(TOP)/src/tclsqlite.c $(TOP)/tool/spaceanal.tcl +sqlite3_analyzer.c: sqlite3.c $(TOP)/src/test_stat.c $(TOP)/src/tclsqlite.c \ + $(TOP)/tool/spaceanal.tcl echo "#define TCLSH 2" > $@ cat sqlite3.c $(TOP)/src/test_stat.c $(TOP)/src/tclsqlite.c >> $@ echo "static const char *tclsh_main_loop(void){" >> $@ @@ -599,9 +612,11 @@ testfixture$(EXE): $(TESTSRC2) libsqlite3.a $(TESTSRC) $(TOP)/src/tclsqlite.c $(TESTSRC) $(TESTSRC2) $(TOP)/src/tclsqlite.c \ -o testfixture$(EXE) $(LIBTCL) libsqlite3.a $(THREADLIB) -amalgamation-testfixture$(EXE): sqlite3.c $(TESTSRC) $(TOP)/src/tclsqlite.c +amalgamation-testfixture$(EXE): sqlite3.c $(TESTSRC) $(TOP)/src/tclsqlite.c \ + $(TOP)/ext/session/test_session.c $(TCCX) $(TCL_FLAGS) -DTCLSH=1 $(TESTFIXTURE_FLAGS) \ $(TESTSRC) $(TOP)/src/tclsqlite.c sqlite3.c \ + $(TOP)/ext/session/test_session.c \ -o testfixture$(EXE) $(LIBTCL) $(THREADLIB) fts3-testfixture$(EXE): sqlite3.c fts3amal.c $(TESTSRC) $(TOP)/src/tclsqlite.c @@ -665,6 +680,10 @@ showwal$(EXE): $(TOP)/tool/showwal.c sqlite3.o $(TCC) -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION -o showwal$(EXE) \ $(TOP)/tool/showwal.c sqlite3.o $(THREADLIB) +changeset$(EXE): $(TOP)/ext/session/changeset.c sqlite3.o + $(TCC) -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION -o changeset$(EXE) \ + $(TOP)/ext/session/changeset.c sqlite3.o $(THREADLIB) + fts3view$(EXE): $(TOP)/ext/fts3/tool/fts3view.c sqlite3.o $(TCC) -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION -o fts3view$(EXE) \ $(TOP)/ext/fts3/tool/fts3view.c sqlite3.o $(THREADLIB) @@ -723,6 +742,7 @@ clean: rm -f showjournal showjournal.exe rm -f showstat4 showstat4.exe rm -f showwal showwal.exe + rm -f changeset changeset.exe rm -f speedtest1 speedtest1.exe rm -f wordcount wordcount.exe rm -f sqlite3.c sqlite3-*.c fts?amal.c tclsqlite3.c @@ -1,9 +1,9 @@ -C Fix\sa\scompiler\swarning\sassociated\swith\sUSE_PREAD64. -D 2015-02-21T00:56:05.294 +C Merge\sall\srecent\strunk\schanges\sinto\sthe\ssessions\sbranch. +D 2015-02-21T15:13:50.198 F Makefile.arm-wince-mingw32ce-gcc d6df77f1f48d690bd73162294bbba7f59507c72f -F Makefile.in 6b9e7677829aa94b9f30949656e27312aefb9a46 +F Makefile.in e63f98067fef9d9112380e3aec9881f56d49f77e F Makefile.linux-gcc 91d710bdc4998cb015f39edf3cb314ec4f4d7e23 -F Makefile.msc fc6b0b233b5621f3e56298e4d6a0b3f6c936c520 +F Makefile.msc bed794fe507466c950f2b5c2d6d70b4e1e9bf14f F Makefile.vxworks e1b65dea203f054e71653415bd8f96dcaed47858 F README.md d58e3bebc0a4145e0f2a87994015fdb575a8e866 F VERSION d846487aff892625eb8e75960234e7285f0462fe @@ -146,13 +146,30 @@ F ext/rtree/rtree_util.tcl 06aab2ed5b826545bf215fff90ecb9255a8647ea F ext/rtree/sqlite3rtree.h 83349d519fe5f518b3ea025d18dd1fe51b1684bd F ext/rtree/tkt3363.test 142ab96eded44a3615ec79fba98c7bde7d0f96de F ext/rtree/viewrtree.tcl eea6224b3553599ae665b239bd827e182b466024 +F ext/session/changeset.c 4ccbaa4531944c24584bf6a61ba3a39c62b6267a +F ext/session/session1.test 4653867f32a98ce4bbb4a181aac6debe51ca4dfb +F ext/session/session2.test 99ca0da7ddb617d42bafd83adccf99f18ae0384b +F ext/session/session3.test a7a9ce59b8d1e49e2cc23d81421ac485be0eea01 +F ext/session/session4.test a6ed685da7a5293c5d6f99855bcf41dbc352ca84 +F ext/session/session5.test 716bc6fafd625ce60dfa62ae128971628c1a1169 +F ext/session/session6.test 443789bc2fca12e4f7075cf692c60b8a2bea1a26 +F ext/session/session8.test 8e194b3f655d861ca36de5d4de53f702751bab3b +F ext/session/session9.test 5409d90d8141881d08285ed1c2c0d8d10fb92069 +F ext/session/sessionA.test 1feeab0b8e03527f08f2f1defb442da25480138f +F ext/session/sessionB.test 06961b7c3641151f5d23088250ecad132501113c +F ext/session/sessionC.test 3982f8577b0744c5ce3aaef7cfeb5bd903f17fe4 +F ext/session/session_common.tcl 9de0451b6a47218fc16b9ed8876b6238a0a3d88d +F ext/session/sessionfault.test bef044d0952c0d62c31c8d2400be72c8684545cc +F ext/session/sqlite3session.c 838050c4c217d2843e4705b14be25d8f0457f155 +F ext/session/sqlite3session.h 16608d29879a0ed3c6be6b7fb18dcdb5c707aaef +F ext/session/test_session.c a28352e99bc6a83b94e4cce99a7bf25c73d6d489 F ext/userauth/sqlite3userauth.h 19cb6f0e31316d0ee4afdfb7a85ef9da3333a220 F ext/userauth/user-auth.txt e6641021a9210364665fe625d067617d03f27b04 F ext/userauth/userauth.c 5fa3bdb492f481bbc1709fc83c91ebd13460c69e F install-sh 9d4de14ab9fb0facae2f48780b874848cbf2f895 x F ltmain.sh 3ff0879076df340d2e23ae905484d8c15d5fdea8 F magic.txt 8273bf49ba3b0c8559cb2774495390c31fd61c60 -F main.mk 0bae136db3f3ce451079ae335124b46163d37020 +F main.mk 381e4d0e86c82cfe40bc7b2f742bb8547d253d90 F mkopcodec.awk c2ff431854d702cdd2d779c9c0d1f58fa16fa4ea F mkopcodeh.awk c6b3fa301db6ef7ac916b14c60868aeaec1337b5 F mkso.sh fd21c06b063bb16a5d25deea1752c2da6ac3ed83 @@ -181,7 +198,7 @@ F src/callback.c 7b44ce59674338ad48b0e84e7b72f935ea4f68b0 F src/complete.c 198a0066ba60ab06fc00fba1998d870a4d575463 F src/ctime.c 98f89724adc891a1a4c655bee04e33e716e05887 F src/date.c e4d50b3283696836ec1036b695ead9a19e37a5ac -F src/delete.c 37964e6c1d73ff49cbea9ff690c9605fb15f600e +F src/delete.c 5075d88557eb4e2a7fdb2b61a96142830d8589b8 F src/expr.c 3ef111b88ae2941b84b6b6ea4be8d501ba1af0cb F src/fault.c 160a0c015b6c2629d3899ed2daf63d75754a32bb F src/fkey.c e0444b61bed271a76840cbe6182df93a9baa3f12 @@ -190,12 +207,12 @@ F src/global.c 4f77cadbc5427d00139ba43d0f3979804cbb700e F src/hash.c 4263fbc955f26c2e8cdc0cf214bc42435aa4e4f5 F src/hash.h c8f3c31722cf3277d03713909761e152a5b81094 F src/hwtime.h d32741c8f4df852c7d959236615444e2b1063b08 -F src/insert.c 5b9243a33726008cc4132897d2be371db12a13be +F src/insert.c 4f6df86bbed2d7b59e4601730407876825dd7b71 F src/journal.c b4124532212b6952f42eb2c12fa3c25701d8ba8d F src/legacy.c ba1863ea58c4c840335a84ec276fc2b25e22bc4e F src/lempar.c 7274c97d24bb46631e504332ccd3bd1b37841770 F src/loadext.c 86bd4e2fccd520b748cba52492ab60c4a770f660 -F src/main.c 80edeba383aac89f72498b2572a115e21d0ecbbd +F src/main.c d01a9b0a8cee34d6a533148bcdc94d65e9205a33 F src/malloc.c 740db54387204c9a2eb67c6d98e68b08e9ef4eab F src/mem0.c 6a55ebe57c46ca1a7d98da93aaa07f99f1059645 F src/mem1.c abe6ee469b6c5a35c7f22bfeb9c9bac664a1c987 @@ -231,15 +248,15 @@ F src/random.c ba2679f80ec82c4190062d756f22d0c358180696 F src/resolve.c f4d79e31ffa5820c2e3d1740baa5e9b190425f2b F src/rowset.c eccf6af6d620aaa4579bd3b72c1b6395d9e9fa1e F src/select.c e46cef4c224549b439384c88fc7f57ba064dad54 -F src/shell.c 6276582ee4e9114e0bb0795772414caaf21c0f8e -F src/sqlite.h.in b02d8d19c5adc73bd02b225054103247aff64425 +F src/shell.c 75e9d6dc85586c4a8e52f3be44c3a23dd3ef8d0d +F src/sqlite.h.in e26cf36c3dc7099d6fcef864e362ee26261cc082 F src/sqlite3.rc 992c9f5fb8285ae285d6be28240a7e8d3a7f2bad F src/sqlite3ext.h 17d487c3c91b0b8c584a32fbeb393f6f795eea7d -F src/sqliteInt.h 57a405ae6d2ed10fff52de376d18f21e04d96609 +F src/sqliteInt.h 1e1c08093c57dbd7c5d0b6d945f1c1d039f1128e F src/sqliteLimit.h 164b0e6749d31e0daa1a4589a169d31c0dec7b3d F src/status.c 81712116e826b0089bb221b018929536b2b5406f F src/table.c e7a09215315a978057fb42c640f890160dbcc45e -F src/tclsqlite.c b8014393a96a9781bb635c8b1f52fc9b77a2bfcf +F src/tclsqlite.c 8dd6e1241a88a33db11b82b4c8f3a7511259c815 F src/test1.c 90fbedce75330d48d99eadb7d5f4223e86969585 F src/test2.c 577961fe48961b2f2e5c8b56ee50c3f459d3359d F src/test3.c 64d2afdd68feac1bb5e2ffb8226c8c639f798622 @@ -254,7 +271,7 @@ F src/test_autoext.c dea8a01a7153b9adc97bd26161e4226329546e12 F src/test_backup.c 2e6e6a081870150f20c526a2e9d0d29cda47d803 F src/test_blob.c 1f2e3e25255b731c4fcf15ee7990d06347cb6c09 F src/test_btree.c 2e9978eca99a9a4bfa8cae949efb00886860a64f -F src/test_config.c e7b2e1634324d746aa5e1c7e0929470e8be27953 +F src/test_config.c fc2616bded292c77e7c13e5aa62dd2732a613ba2 F src/test_demovfs.c 0de72c2c89551629f58486fde5734b7d90758852 F src/test_devsym.c e7498904e72ba7491d142d5c83b476c4e76993bc F src/test_fs.c ced436e3d4b8e4681328409b8081051ce614e28f @@ -289,17 +306,17 @@ F src/test_wsd.c 41cadfd9d97fe8e3e4e44f61a4a8ccd6f7ca8fe9 F src/threads.c 6bbcc9fe50c917864d48287b4792d46d6e873481 F src/tokenize.c 05e52378c46efbc1fd63cbbbf7f3c555f840f4bf F src/trigger.c 25571661fdeae8c7f975ff40ffec205520a3f92f -F src/update.c 3c4ecc282accf12d39edb8d524cf089645e55a13 +F src/update.c d207deb7a031f698104bee879de0632b611e72dd F src/utf.c fc6b889ba0779b7722634cdeaa25f1930d93820c F src/util.c 98a7627ca48ad3265b6940915a1d08355eb3fc7e F src/vacuum.c 9460b9de7b2d4e34b0d374894aa6c8a0632be8ec -F src/vdbe.c ddfc977981cd6324668aa6b114045eb1c677421a -F src/vdbe.h 6fc69d9c5e146302c56e163cb4b31d1ee64a18c3 -F src/vdbeInt.h bb56fd199d8af1a2c1b9639ee2f70724b4338e3a -F src/vdbeapi.c 3d88089b10f71750b019a806224f0277d371a072 -F src/vdbeaux.c 97911edb61074b871ec4aa2d6bb779071643dee5 -F src/vdbeblob.c 4f2e8e075d238392df98c5e03a64342465b03f90 -F src/vdbemem.c 31d8eabb0cd78bfeab4e5124c7363c3e9e54db9f +F src/vdbe.c d18d7bbe5099b4ae7573673232eb29c12075f174 +F src/vdbe.h b434bb75fbec973d18d49225a59833ae39ee2afc +F src/vdbeInt.h 50fc184e25bfe1b39b8e508082cd3f0cd8a29b3d +F src/vdbeapi.c 1f5af816d4206a48c2ed610be7a44ca613673521 +F src/vdbeaux.c fbc0e6dfcf472e76001f7684b3b534710a645138 +F src/vdbeblob.c ab33f9b57cfce7dddb23853090186da614be4846 +F src/vdbemem.c 96e41193b4affd9ebc0eea2fa628879dac88c744 F src/vdbesort.c 6d64c5448b64851b99931ede980addc3af70d5e2 F src/vdbetrace.c 7e4222955e07dd707a2f360c0eb73452be1cb010 F src/vtab.c 699f2b8d509cfe379c33dde33827875d5b030e01 @@ -504,7 +521,7 @@ F test/fkey2.test 1db212cda86b0d3ce72714001f7b6381c321341c F test/fkey3.test 76d475c80b84ee7a5d062e56ccb6ea68882e2b49 F test/fkey4.test 86446017011273aad8f9a99c1a65019e7bd9ca9d F test/fkey5.test 488601fbda8350619b3029487e56830447056fd2 -F test/fkey6.test abb59f866c1b44926fd02d1fdd217d831fe04f48 +F test/fkey6.test 6697550baa38505c9952eff130ab26a2d156c0cc F test/fkey7.test 72e915890ee4a005daaf3002cb208e8fe973ac13 F test/fkey8.test 8f08203458321e6c19a263829de4cfc936274ab0 F test/fkey_malloc.test 594a7ea1fbab553c036c70813cd8bd9407d63749 @@ -629,7 +646,7 @@ F test/fuzzer1.test d4c52aaf3ef923da293a2653cfab33d02f718a36 F test/fuzzerfault.test 8792cd77fd5bce765b05d0c8e01b9edcf8af8536 F test/genesis.tcl 1e2e2e8e5cc4058549a154ff1892fe5c9de19f98 F test/hexlit.test f9ecde8145bfc2341573473256c74ae37a200497 -F test/hook.test 162d7cef7a2d2b04839fe14402934e6a1b79442f +F test/hook.test aa41c095d26822b8a51aa4c82904a14347961be6 F test/icu.test 70df4faca133254c042d02ae342c0a141f2663f4 F test/imposter1.test c3f1db2d3db2c24611a6596a3fc0ffc14f1466c8 F test/in.test 047c4671328e9032ab95666a67021adbbd36e98e @@ -786,7 +803,7 @@ F test/pagesize.test 1dd51367e752e742f58e861e65ed7390603827a0 F test/pcache.test b09104b03160aca0d968d99e8cd2c5b1921a993d F test/pcache2.test a83efe2dec0d392f814bfc998def1d1833942025 F test/percentile.test 4243af26b8f3f4555abe166f723715a1f74c77ff -F test/permutations.test f9cc1dd987986c9d4949211c7a4ed55ec9aecba1 +F test/permutations.test e5e9f708879336e10095ded8d61966007f4a2c5c F test/pragma.test 6cf0f0ce4618e841457aa42745afda55ddbc95fe F test/pragma2.test aea7b3d82c76034a2df2b38a13745172ddc0bc13 F test/pragma3.test 6f849ccffeee7e496d2f2b5e74152306c0b8757c @@ -846,6 +863,7 @@ F test/selectE.test fc02a1eb04c8eb537091482644b7d778ae8759b7 F test/selectF.test 21c94e6438f76537b72532fa9fd4710cdd455fc3 F test/selectG.test e8600e379589e85e9fefd2fe4d44a4cdd63f6982 F test/server1.test 46803bd3fe8b99b30dbc5ff38ffc756f5c13a118 +F test/session.test 78fa2365e93d3663a6e933f86e7afc395adf18be F test/shared.test 1da9dbad400cee0d93f252ccf76e1ae007a63746 F test/shared2.test 03eb4a8d372e290107d34b6ce1809919a698e879 F test/shared3.test fcd65cb11d189eff5f5c85cc4fad246fb0933108 @@ -904,11 +922,11 @@ F test/sysfault.test fa776e60bf46bdd3ae69f0b73e46ee3977a58ae6 F test/table.test 06271d61eb13871490d38168433c1ef3dd82bb2a F test/tableapi.test 2674633fa95d80da917571ebdd759a14d9819126 F test/tableopts.test dba698ba97251017b7c80d738c198d39ab747930 -F test/tclsqlite.test 37a61c2da7e3bfe3b8c1a2867199f6b860df5d43 +F test/tclsqlite.test a7308276aad2e6c0bfb5b0414424dd0d9cc0cad7 F test/tempdb.test 19d0f66e2e3eeffd68661a11c83ba5e6ace9128c F test/temptable.test d2c9b87a54147161bcd1822e30c1d1cd891e5b30 F test/temptrigger.test 8ec228b0db5d7ebc4ee9b458fc28cb9e7873f5e1 -F test/tester.tcl ed77454e6c7b40eb501db7e79d1c6fbfd3eebbff +F test/tester.tcl 655afed0715958ec50fd575549e6c4e57311ff18 F test/thread001.test 9f22fd3525a307ff42a326b6bc7b0465be1745a5 F test/thread002.test e630504f8a06c00bf8bbe68528774dd96aeb2e58 F test/thread003.test ee4c9efc3b86a6a2767516a37bd64251272560a7 @@ -1206,8 +1224,8 @@ F tool/mkopts.tcl 66ac10d240cc6e86abd37dc908d50382f84ff46e F tool/mkpragmatab.tcl 94f196c9961e0ca3513e29f57125a3197808be2d F tool/mkspeedsql.tcl a1a334d288f7adfe6e996f2e712becf076745c97 F tool/mksqlite3c-noext.tcl 9ef48e1748dce7b844f67e2450ff9dfeb0fb4ab5 -F tool/mksqlite3c.tcl 6b8e572a90eb4e0086e3ba90d88b76c085919863 -F tool/mksqlite3h.tcl ba24038056f51fde07c0079c41885ab85e2cff12 +F tool/mksqlite3c.tcl 66b14dbaa7322a24eb3055da884244c832118bae +F tool/mksqlite3h.tcl 2d0f1b3768f8d000b7881217d5fd4c776eb27467 F tool/mksqlite3internalh.tcl eb994013e833359137eb53a55acdad0b5ae1049b F tool/mkvsix.tcl 52a4c613707ac34ae9c226e5ccc69cb948556105 F tool/offsets.c fe4262fdfa378e8f5499a42136d17bf3b98f6091 @@ -1231,7 +1249,7 @@ F tool/speedtest8inst1.c 293327bc76823f473684d589a8160bde1f52c14e F tool/split-sqlite3c.tcl d9be87f1c340285a3e081eb19b4a247981ed290c F tool/stack_usage.tcl f8e71b92cdb099a147dad572375595eae55eca43 F tool/symbols-mingw.sh 4dbcea7e74768305384c9fd2ed2b41bbf9f0414d -F tool/symbols.sh fec58532668296d7c7dc48be9c87f75ccdb5814f +F tool/symbols.sh c5a617b8c61a0926747a56c65f5671ef8ac0e148 F tool/tostr.awk e75472c2f98dd76e06b8c9c1367f4ab07e122d06 F tool/varint.c 5d94cb5003db9dbbcbcc5df08d66f16071aee003 F tool/vdbe-compress.tcl 5926c71f9c12d2ab73ef35c29376e756eb68361c @@ -1239,7 +1257,7 @@ F tool/vdbe_profile.tcl 67746953071a9f8f2f668b73fe899074e2c6d8c1 F tool/warnings-clang.sh f6aa929dc20ef1f856af04a730772f59283631d4 F tool/warnings.sh 0abfd78ceb09b7f7c27c688c8e3fe93268a13b32 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f -P 7560a9fa50236ecaa0617f1ab5bb5662f4a61c72 -R afd779aaccfbda112c52de91ea006a71 +P ae7eef117f28a5dae7a05805f2d31ac532a9fcc5 c299e55a661c04f71ab43cb8aed04f8ece6e0567 +R b7f8fa563aa6e3bf4b2e063495f61f72 U drh -Z 8991d0e22753a2d2a931885c01f739fa +Z 87039a26f4b6e7975795e319beedb047 diff --git a/manifest.uuid b/manifest.uuid index 799d2f85a..d66ebf70a 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -c299e55a661c04f71ab43cb8aed04f8ece6e0567
\ No newline at end of file +f36bb5fa5c5e3430808fe35d58f45559a216d341
\ No newline at end of file diff --git a/src/delete.c b/src/delete.c index ef6aace1c..4f8db796f 100644 --- a/src/delete.c +++ b/src/delete.c @@ -358,8 +358,15 @@ void sqlite3DeleteFrom( /* Special case: A DELETE without a WHERE clause deletes everything. ** It is easier just to erase the whole table. Prior to version 3.6.5, ** this optimization caused the row change count (the value returned by - ** API function sqlite3_count_changes) to be set incorrectly. */ - if( rcauth==SQLITE_OK && pWhere==0 && !pTrigger && !IsVirtual(pTab) + ** API function sqlite3_count_changes) to be set incorrectly. + */ + if( rcauth==SQLITE_OK + && pWhere==0 + && !pTrigger + && !IsVirtual(pTab) +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + && db->xPreUpdateCallback==0 +#endif && 0==sqlite3FkRequired(pParse, pTab, 0, 0) ){ assert( !isView ); @@ -671,13 +678,18 @@ void sqlite3GenerateRowDelete( /* Delete the index and table entries. Skip this step if pTab is really ** a view (in which case the only effect of the DELETE statement is to - ** fire the INSTEAD OF triggers). */ + ** fire the INSTEAD OF triggers). + ** + ** If variable 'count' is non-zero, then this OP_Delete instruction should + ** invoke the update-hook. The pre-update-hook, on the other hand should + ** be invoked unless table pTab is a system table. The difference is that + ** the update-hook is not invoked for rows removed by REPLACE, but the + ** pre-update-hook is. + */ if( pTab->pSelect==0 ){ sqlite3GenerateRowIndexDelete(pParse, pTab, iDataCur, iIdxCur, 0); sqlite3VdbeAddOp2(v, OP_Delete, iDataCur, (count?OPFLAG_NCHANGE:0)); - if( count ){ - sqlite3VdbeChangeP4(v, -1, pTab->zName, P4_TRANSIENT); - } + sqlite3VdbeChangeP4(v, -1, (char*)pTab, P4_TABLE); } /* Do any ON CASCADE, SET NULL or SET DEFAULT operations required to diff --git a/src/insert.c b/src/insert.c index a5c3f3e92..c7618428f 100644 --- a/src/insert.c +++ b/src/insert.c @@ -1332,9 +1332,21 @@ void sqlite3GenerateConstraintChecks( sqlite3MultiWrite(pParse); sqlite3GenerateRowDelete(pParse, pTab, pTrigger, iDataCur, iIdxCur, regNewData, 1, 0, OE_Replace, 1); - }else if( pTab->pIndex ){ - sqlite3MultiWrite(pParse); - sqlite3GenerateRowIndexDelete(pParse, pTab, iDataCur, iIdxCur, 0); + }else{ +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + if( HasRowid(pTab) ){ + /* This OP_Delete opcode fires the pre-update-hook only. It does + ** not modify the b-tree. It is more efficient to let the coming + ** OP_Insert replace the existing entry than it is to delete the + ** existing entry and then insert a new one. */ + sqlite3VdbeAddOp2(v, OP_Delete, iDataCur, OPFLAG_ISNOOP); + sqlite3VdbeChangeP4(v, -1, (char *)pTab, P4_TABLE); + } +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + if( pTab->pIndex ){ + sqlite3MultiWrite(pParse); + sqlite3GenerateRowIndexDelete(pParse, pTab, iDataCur, iIdxCur, 0); + } } seenReplace = 1; break; @@ -1590,7 +1602,7 @@ void sqlite3CompleteInsertion( } sqlite3VdbeAddOp3(v, OP_Insert, iDataCur, regRec, regNewData); if( !pParse->nested ){ - sqlite3VdbeChangeP4(v, -1, pTab->zName, P4_TRANSIENT); + sqlite3VdbeChangeP4(v, -1, (char *)pTab, P4_TABLE); } sqlite3VdbeChangeP5(v, pik_flags); } @@ -1978,7 +1990,7 @@ static int xferOptimization( sqlite3VdbeAddOp2(v, OP_RowData, iSrc, regData); sqlite3VdbeAddOp3(v, OP_Insert, iDest, regData, regRowid); sqlite3VdbeChangeP5(v, OPFLAG_NCHANGE|OPFLAG_LASTROWID|OPFLAG_APPEND); - sqlite3VdbeChangeP4(v, -1, pDest->zName, 0); + sqlite3VdbeChangeP4(v, -1, (char*)pDest, P4_TABLE); sqlite3VdbeAddOp2(v, OP_Next, iSrc, addr1); VdbeCoverage(v); sqlite3VdbeAddOp2(v, OP_Close, iSrc, 0); sqlite3VdbeAddOp2(v, OP_Close, iDest, 0); diff --git a/src/main.c b/src/main.c index 371bb1fed..9c85e0ae9 100644 --- a/src/main.c +++ b/src/main.c @@ -1846,6 +1846,27 @@ void *sqlite3_rollback_hook( return pRet; } +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** Register a callback to be invoked each time a row is updated, +** inserted or deleted using this database connection. +*/ +void *sqlite3_preupdate_hook( + sqlite3 *db, /* Attach the hook to this database */ + void(*xCallback)( /* Callback function */ + void*,sqlite3*,int,char const*,char const*,sqlite3_int64,sqlite3_int64), + void *pArg /* First callback argument */ +){ + void *pRet; + sqlite3_mutex_enter(db->mutex); + pRet = db->pPreUpdateArg; + db->xPreUpdateCallback = xCallback; + db->pPreUpdateArg = pArg; + sqlite3_mutex_leave(db->mutex); + return pRet; +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + #ifndef SQLITE_OMIT_WAL /* ** The sqlite3_wal_hook() callback registered by sqlite3_wal_autocheckpoint(). diff --git a/src/shell.c b/src/shell.c index e0d0b78b7..4b05c1984 100644 --- a/src/shell.c +++ b/src/shell.c @@ -464,6 +464,19 @@ static char *one_input_line(FILE *in, char *zPrior, int isContinuation){ return zResult; } +#if defined(SQLITE_ENABLE_SESSION) +/* +** State information for a single open session +*/ +typedef struct OpenSession OpenSession; +struct OpenSession { + char *zName; /* Symbolic name for this session */ + int nFilter; /* Number of xFilter rejection GLOB patterns */ + char **azFilter; /* Array of xFilter rejection GLOB patterns */ + sqlite3_session *p; /* The open session */ +}; +#endif + /* ** Shell output mode information from before ".explain on", ** saved so that it can be restored by ".explain off" @@ -513,6 +526,10 @@ struct ShellState { int *aiIndent; /* Array of indents used in MODE_Explain */ int nIndent; /* Size of array aiIndent[] */ int iIndent; /* Index of current op in aiIndent[] */ +#if defined(SQLITE_ENABLE_SESSION) + int nSession; /* Number of active sessions */ + OpenSession aSession[4]; /* Array of sessions. [0] is in focus. */ +#endif }; /* @@ -1782,6 +1799,9 @@ static char zHelp[] = " LIKE pattern TABLE.\n" ".separator COL ?ROW? Change the column separator and optionally the row\n" " separator for both the output mode and .import\n" +#if defined(SQLITE_ENABLE_SESSION) + ".session CMD ... Create or control sessions\n" +#endif ".shell CMD ARGS... Run CMD ARGS... in a system shell\n" ".show Show the current values for various settings\n" ".stats on|off Turn stats on or off\n" @@ -1797,6 +1817,30 @@ static char zHelp[] = " Negative values right-justify\n" ; +#if defined(SQLITE_ENABLE_SESSION) +/* +** Print help information for the ".sessions" command +*/ +void session_help(ShellState *p){ + fprintf(p->out, + ".session ?NAME? SUBCOMMAND ?ARGS...?\n" + "If ?NAME? is omitted, the first defined session is used.\n" + "Subcommands:\n" + " attach TABLE Attach TABLE\n" + " changeset FILE Write a changeset into FILE\n" + " close Close one session\n" + " enable ?BOOLEAN? Set or query the enable bit\n" + " filter GLOB... Reject tables matching GLOBs\n" + " indirect ?BOOLEAN? Mark or query the indirect status\n" + " isempty Query whether the session is empty\n" + " list List currently open session names\n" + " open DB NAME Open a new session on DB\n" + " patchset FILE Write a patchset into FILE\n" + ); +} +#endif + + /* Forward reference */ static int process_input(ShellState *p, FILE *in); /* @@ -1860,6 +1904,51 @@ static void writefileFunc( sqlite3_result_int64(context, rc); } +#if defined(SQLITE_ENABLE_SESSION) +/* +** Close a single OpenSession object and release all of its associated +** resources. +*/ +static void session_close(OpenSession *pSession){ + int i; + sqlite3session_delete(pSession->p); + sqlite3_free(pSession->zName); + for(i=0; i<pSession->nFilter; i++){ + sqlite3_free(pSession->azFilter[i]); + } + sqlite3_free(pSession->azFilter); + memset(pSession, 0, sizeof(OpenSession)); +} +#endif + +/* +** Close all OpenSession objects and release all assocaited resources. +*/ +static void session_close_all(ShellState *p){ +#if defined(SQLITE_ENABLE_SESSION) + int i; + for(i=0; i<p->nSession; i++){ + session_close(&p->aSession[i]); + } + p->nSession = 0; +#endif +} + +/* +** Implementation of the xFilter function for an open session. Omit +** any tables named by ".session filter" but let all other table through. +*/ +#if defined(SQLITE_ENABLE_SESSION) +static int session_filter(void *pCtx, const char *zTab){ + OpenSession *pSession = (OpenSession*)pCtx; + int i; + for(i=0; i<pSession->nFilter; i++){ + if( sqlite3_strglob(pSession->azFilter[i], zTab)==0 ) return 0; + } + return 1; +} +#endif + /* ** Make sure the database is open. If it is not, then open it. If ** the database fails to open, print an error message and exit. @@ -3206,6 +3295,7 @@ static int do_meta_command(char *zLine, ShellState *p){ } open_db(p, 1); if( p->db!=0 ){ + session_close_all(p); sqlite3_close(savedDb); sqlite3_free(p->zFreeOnClose); p->zFreeOnClose = zNewFilename; @@ -3440,7 +3530,6 @@ static int do_meta_command(char *zLine, ShellState *p){ } }else - #if defined(SQLITE_DEBUG) && defined(SQLITE_ENABLE_SELECTTRACE) if( c=='s' && n==11 && strncmp(azArg[0], "selecttrace", n)==0 ){ extern int sqlite3SelectTrace; @@ -3448,6 +3537,198 @@ static int do_meta_command(char *zLine, ShellState *p){ }else #endif +#if defined(SQLITE_ENABLE_SESSION) + if( c=='s' && strncmp(azArg[0],"session",n)==0 && n>=3 ){ + OpenSession *pSession = &p->aSession[0]; + char **azCmd = &azArg[1]; + int iSes = 0; + int nCmd = nArg - 1; + int i; + if( nArg<=1 ) goto session_syntax_error; + open_db(p, 0); + if( nArg>=3 ){ + for(iSes=0; iSes<p->nSession; iSes++){ + if( strcmp(p->aSession[iSes].zName, azArg[1])==0 ) break; + } + if( iSes<p->nSession ){ + pSession = &p->aSession[iSes]; + azCmd++; + nCmd--; + }else{ + pSession = &p->aSession[0]; + iSes = 0; + } + } + + /* .session attach TABLE + ** Invoke the sqlite3session_attach() interface to attach a particular + ** table so that it is never filtered. + */ + if( strcmp(azCmd[0],"attach")==0 ){ + if( nCmd!=2 ) goto session_syntax_error; + if( pSession->p==0 ){ + session_not_open: + fprintf(stderr, "ERROR: No sessions are open\n"); + }else{ + rc = sqlite3session_attach(pSession->p, azCmd[1]); + if( rc ){ + fprintf(stderr, "ERROR: sqlite3session_attach() returns %d\n", rc); + rc = 0; + } + } + }else + + /* .session changeset FILE + ** .session patchset FILE + ** Write a changeset or patchset into a file. The file is overwritten. + */ + if( strcmp(azCmd[0],"changeset")==0 || strcmp(azCmd[0],"patchset")==0 ){ + FILE *out = 0; + if( nCmd!=2 ) goto session_syntax_error; + if( pSession->p==0 ) goto session_not_open; + out = fopen(azCmd[1], "wb"); + if( out==0 ){ + fprintf(stderr, "ERROR: cannot open \"%s\" for writing\n", azCmd[1]); + }else{ + int szChng; + void *pChng; + if( azCmd[0][0]=='c' ){ + rc = sqlite3session_changeset(pSession->p, &szChng, &pChng); + }else{ + rc = sqlite3session_patchset(pSession->p, &szChng, &pChng); + } + if( rc ){ + printf("Error: error code %d\n", rc); + rc = 0; + } + if( pChng + && fwrite(pChng, szChng, 1, out)!=1 ){ + fprintf(stderr, "ERROR: Failed to write entire %d-byte output\n", + szChng); + } + sqlite3_free(pChng); + fclose(out); + } + }else + + /* .session close + ** Close the identified session + */ + if( strcmp(azCmd[0], "close")==0 ){ + if( nCmd!=1 ) goto session_syntax_error; + if( p->nSession ){ + session_close(pSession); + p->aSession[iSes] = p->aSession[--p->nSession]; + } + }else + + /* .session enable ?BOOLEAN? + ** Query or set the enable flag + */ + if( strcmp(azCmd[0], "enable")==0 ){ + int ii; + if( nCmd>2 ) goto session_syntax_error; + ii = nCmd==1 ? -1 : booleanValue(azCmd[1]); + if( p->nSession ){ + ii = sqlite3session_enable(pSession->p, ii); + fprintf(p->out, "session %s enable flag = %d\n", pSession->zName, ii); + } + }else + + /* .session filter GLOB .... + ** Set a list of GLOB patterns of table names to be excluded. + */ + if( strcmp(azCmd[0], "filter")==0 ){ + int ii, nByte; + if( nCmd<2 ) goto session_syntax_error; + if( p->nSession ){ + for(ii=0; ii<pSession->nFilter; ii++){ + sqlite3_free(pSession->azFilter[ii]); + } + sqlite3_free(pSession->azFilter); + nByte = sizeof(pSession->azFilter[0])*(nCmd-1); + pSession->azFilter = sqlite3_malloc( nByte ); + if( pSession->azFilter==0 ){ + fprintf(stderr, "Error: out or memory\n"); + exit(1); + } + for(ii=1; ii<nCmd; ii++){ + pSession->azFilter[ii-1] = sqlite3_mprintf("%s", azCmd[ii]); + } + pSession->nFilter = ii-1; + } + }else + + /* .session indirect ?BOOLEAN? + ** Query or set the indirect flag + */ + if( strcmp(azCmd[0], "indirect")==0 ){ + int ii; + if( nCmd>2 ) goto session_syntax_error; + ii = nCmd==1 ? -1 : booleanValue(azCmd[1]); + if( p->nSession ){ + ii = sqlite3session_indirect(pSession->p, ii); + fprintf(p->out, "session %s indirect flag = %d\n", pSession->zName,ii); + } + }else + + /* .session isempty + ** Determine if the session is empty + */ + if( strcmp(azCmd[0], "isempty")==0 ){ + int ii; + if( nCmd!=1 ) goto session_syntax_error; + if( p->nSession ){ + ii = sqlite3session_isempty(pSession->p); + fprintf(p->out, "session %s isempty flag = %d\n", pSession->zName, ii); + } + }else + + /* .session list + ** List all currently open sessions + */ + if( strcmp(azCmd[0],"list")==0 ){ + for(i=0; i<p->nSession; i++){ + fprintf(p->out, "%d %s\n", i, p->aSession[i].zName); + } + }else + + /* .session open DB NAME + ** Open a new session called NAME on the attached database DB. + ** DB is normally "main". + */ + if( strcmp(azCmd[0],"open")==0 ){ + char *zName; + if( nCmd!=3 ) goto session_syntax_error; + zName = azCmd[2]; + if( zName[0]==0 ) goto session_syntax_error; + for(i=0; i<p->nSession; i++){ + if( strcmp(p->aSession[i].zName,zName)==0 ){ + fprintf(stderr, "Session \"%s\" already exists\n", zName); + goto meta_command_exit; + } + } + if( p->nSession>=ArraySize(p->aSession) ){ + fprintf(stderr, "Maximum of %d sessions\n", ArraySize(p->aSession)); + goto meta_command_exit; + } + pSession = &p->aSession[p->nSession]; + rc = sqlite3session_create(p->db, azCmd[1], &pSession->p); + if( rc ){ + fprintf(stderr, "Cannot open session: error code=%d\n", rc); + rc = 0; + goto meta_command_exit; + } + pSession->nFilter = 0; + sqlite3session_table_filter(pSession->p, session_filter, pSession); + p->nSession++; + pSession->zName = sqlite3_mprintf("%s", zName); + }else + /* If no command name matches, show a syntax error */ + session_syntax_error: + session_help(p); + }else +#endif #ifdef SQLITE_DEBUG /* Undocumented commands for internal testing. Subject to change @@ -4651,6 +4932,7 @@ int main(int argc, char **argv){ } set_table_name(&data, 0); if( data.db ){ + session_close_all(&data); sqlite3_close(data.db); } sqlite3_free(data.zFreeOnClose); diff --git a/src/sqlite.h.in b/src/sqlite.h.in index 8a358dbb2..fccbe2a19 100644 --- a/src/sqlite.h.in +++ b/src/sqlite.h.in @@ -5034,8 +5034,8 @@ void *sqlite3_rollback_hook(sqlite3*, void(*)(void *), void*); ** on the same [database connection] D, or NULL for ** the first call on D. ** -** See also the [sqlite3_commit_hook()] and [sqlite3_rollback_hook()] -** interfaces. +** See also the [sqlite3_commit_hook()], [sqlite3_rollback_hook()], +** and [sqlite3_preupdate_hook()] interfaces. */ void *sqlite3_update_hook( sqlite3*, @@ -6502,7 +6502,7 @@ int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int resetFlg); ** database file in rollback mode databases. Any pages written as part of ** transaction rollback or database recovery operations are not included. ** If an IO or other error occurs while writing a page to disk, the effect -** on subsequent SQLITE_DBSTATUS_CACHE_WRITE requests is undefined.)^ ^The +** on subsequent SQLITE_DBSTATUS_CACHE_WRITE requests is undefined). ^The ** highwater mark associated with SQLITE_DBSTATUS_CACHE_WRITE is always 0. ** </dd> ** @@ -7593,6 +7593,101 @@ SQLITE_EXPERIMENTAL void sqlite3_stmt_scanstatus_reset(sqlite3_stmt*); /* +** CAPI3REF: The pre-update hook. +** EXPERIMENTAL +** +** ^These interfaces are only available if SQLite is compiled using the +** [SQLITE_ENABLE_UPDATE_HOOK] compile-time option. +** +** ^The [sqlite3_preupdate_hook()] interface registers a callback function +** that is invoked prior to each [INSERT], [UPDATE], and [DELETE] operation. +** ^At most one preupdate hook may be registered at a time on a single +** [database connection]; each call to [sqlite3_preupdate_hook()] overrides +** the previous setting. +** ^The preupdate hook is disabled by invoking [sqlite3_preupdate_hook()] +** with a NULL pointer as the second parameter. +** ^The third parameter to [sqlite3_preupdate_hook()] is passed through as +** the first parameter to callbacks. +** +** ^The preupdate hook only fires for changes to real tables; the preupdate +** hook is not invoked for changes to virtual tables. +** +** ^The second parameter to the preupdate callback is a pointer to +** the [database connection] that registered the preupdate hook. +** ^The third parameter to the preupdate callback is one of the constants +** [SQLITE_INSERT], [SQLITE_DELETE], or [SQLITE_UPDATE] to indentify the +** kind of update operation that is about to occur. +** ^(The fourth parameter to the preupdate callback is the name of the +** database within the database connection that is being modified. This +** will be "main" for the main database or "temp" for TEMP tables or +** the name given after the AS keyword in the [ATTACH] statement for attached +** databases.)^ +** ^The fifth parameter to the preupdate callback is the name of the +** table that is being modified. +** ^The sixth parameter to the preupdate callback is the initial [rowid] of the +** row being changes for SQLITE_UPDATE and SQLITE_DELETE changes and is +** undefined for SQLITE_INSERT changes. +** ^The seventh parameter to the preupdate callback is the final [rowid] of +** the row being changed for SQLITE_UPDATE and SQLITE_INSERT changes and is +** undefined for SQLITE_DELETE changes. +** +** The [sqlite3_preupdate_old()], [sqlite3_preupdate_new()], +** [sqlite3_preupdate_count()], and [sqlite3_preupdate_depth()] interfaces +** provide additional information about a preupdate event. These routines +** may only be called from within a preupdate callback. Invoking any of +** these routines from outside of a preupdate callback or with a +** [database connection] pointer that is different from the one supplied +** to the preupdate callback results in undefined and probably undesirable +** behavior. +** +** ^The [sqlite3_preupdate_count(D)] interface returns the number of columns +** in the row that is being inserted, updated, or deleted. +** +** ^The [sqlite3_preupdate_old(D,N,P)] interface writes into P a pointer to +** a [protected sqlite3_value] that contains the value of the Nth column of +** the table row before it is updated. The N parameter must be between 0 +** and one less than the number of columns or the behavior will be +** undefined. This must only be used within SQLITE_UPDATE and SQLITE_DELETE +** preupdate callbacks; if it is used by an SQLITE_INSERT callback then the +** behavior is undefined. The [sqlite3_value] that P points to +** will be destroyed when the preupdate callback returns. +** +** ^The [sqlite3_preupdate_new(D,N,P)] interface writes into P a pointer to +** a [protected sqlite3_value] that contains the value of the Nth column of +** the table row after it is updated. The N parameter must be between 0 +** and one less than the number of columns or the behavior will be +** undefined. This must only be used within SQLITE_INSERT and SQLITE_UPDATE +** preupdate callbacks; if it is used by an SQLITE_DELETE callback then the +** behavior is undefined. The [sqlite3_value] that P points to +** will be destroyed when the preupdate callback returns. +** +** ^The [sqlite3_preupdate_depth(D)] interface returns 0 if the preupdate +** callback was invoked as a result of a direct insert, update, or delete +** operation; or 1 for inserts, updates, or deletes invoked by top-level +** triggers; or 2 for changes resulting from triggers called by top-level +** triggers; and so forth. +** +** See also: [sqlite3_update_hook()] +*/ +SQLITE_EXPERIMENTAL void *sqlite3_preupdate_hook( + sqlite3 *db, + void(*xPreUpdate)( + void *pCtx, /* Copy of third arg to preupdate_hook() */ + sqlite3 *db, /* Database handle */ + int op, /* SQLITE_UPDATE, DELETE or INSERT */ + char const *zDb, /* Database name */ + char const *zName, /* Table name */ + sqlite3_int64 iKey1, /* Rowid of row about to be deleted/updated */ + sqlite3_int64 iKey2 /* New rowid value (for a rowid UPDATE) */ + ), + void* +); +SQLITE_EXPERIMENTAL int sqlite3_preupdate_old(sqlite3 *, int, sqlite3_value **); +SQLITE_EXPERIMENTAL int sqlite3_preupdate_count(sqlite3 *); +SQLITE_EXPERIMENTAL int sqlite3_preupdate_depth(sqlite3 *); +SQLITE_EXPERIMENTAL int sqlite3_preupdate_new(sqlite3 *, int, sqlite3_value **); + +/* ** Undo the hack that converts floating point types to integer for ** builds on processors without floating point support. */ diff --git a/src/sqliteInt.h b/src/sqliteInt.h index 3f9a3a7cc..f41a1260e 100644 --- a/src/sqliteInt.h +++ b/src/sqliteInt.h @@ -856,6 +856,7 @@ typedef struct LookasideSlot LookasideSlot; typedef struct Module Module; typedef struct NameContext NameContext; typedef struct Parse Parse; +typedef struct PreUpdate PreUpdate; typedef struct PrintfArguments PrintfArguments; typedef struct RowSet RowSet; typedef struct Savepoint Savepoint; @@ -1105,6 +1106,13 @@ struct sqlite3 { void (*xRollbackCallback)(void*); /* Invoked at every commit. */ void *pUpdateArg; void (*xUpdateCallback)(void*,int, const char*,const char*,sqlite_int64); +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + void *pPreUpdateArg; /* First argument to xPreUpdateCallback */ + void (*xPreUpdateCallback)( /* Registered using sqlite3_preupdate_hook() */ + void*,sqlite3*,int,char const*,char const*,sqlite3_int64,sqlite3_int64 + ); + PreUpdate *pPreUpdate; /* Context for active pre-update callback */ +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ #ifndef SQLITE_OMIT_WAL int (*xWalCallback)(void *, sqlite3 *, const char *, int); void *pWalArg; @@ -2677,6 +2685,9 @@ struct AuthContext { /* ** Bitfield flags for P5 value in various opcodes. +** +** Note that the values for ISNOOP and LENGTHARG are the same. But as +** those bits are never used on the same opcode, the overlap is harmless. */ #define OPFLAG_NCHANGE 0x01 /* Set to update db->nChange */ #define OPFLAG_EPHEM 0x01 /* OP_Column: Ephemeral output is ok */ @@ -2684,6 +2695,7 @@ struct AuthContext { #define OPFLAG_ISUPDATE 0x04 /* This OP_Insert is an sql UPDATE */ #define OPFLAG_APPEND 0x08 /* This is likely to be an append */ #define OPFLAG_USESEEKRESULT 0x10 /* Try to avoid a seek in BtreeInsert() */ +#define OPFLAG_ISNOOP 0x40 /* OP_Delete does pre-update-hook only */ #define OPFLAG_LENGTHARG 0x40 /* OP_Column only used for length() */ #define OPFLAG_TYPEOFARG 0x80 /* OP_Column only used for typeof() */ #define OPFLAG_BULKCSR 0x01 /* OP_Open** used to open bulk cursor */ diff --git a/src/tclsqlite.c b/src/tclsqlite.c index b1d4dc413..2ffb6e928 100644 --- a/src/tclsqlite.c +++ b/src/tclsqlite.c @@ -140,6 +140,7 @@ struct SqliteDb { char *zNull; /* Text to substitute for an SQL NULL value */ SqlFunc *pFunc; /* List of SQL functions */ Tcl_Obj *pUpdateHook; /* Update hook script (if any) */ + Tcl_Obj *pPreUpdateHook; /* Pre-update hook script (if any) */ Tcl_Obj *pRollbackHook; /* Rollback hook script (if any) */ Tcl_Obj *pWalHook; /* WAL hook script (if any) */ Tcl_Obj *pUnlockNotify; /* Unlock notify script (if any) */ @@ -518,6 +519,9 @@ static void DbDeleteCmd(void *db){ if( pDb->pUpdateHook ){ Tcl_DecrRefCount(pDb->pUpdateHook); } + if( pDb->pPreUpdateHook ){ + Tcl_DecrRefCount(pDb->pPreUpdateHook); + } if( pDb->pRollbackHook ){ Tcl_DecrRefCount(pDb->pRollbackHook); } @@ -685,6 +689,42 @@ static void DbUnlockNotify(void **apArg, int nArg){ } #endif +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** Pre-update hook callback. +*/ +static void DbPreUpdateHandler( + void *p, + sqlite3 *db, + int op, + const char *zDb, + const char *zTbl, + sqlite_int64 iKey1, + sqlite_int64 iKey2 +){ + SqliteDb *pDb = (SqliteDb *)p; + Tcl_Obj *pCmd; + static const char *azStr[] = {"DELETE", "INSERT", "UPDATE"}; + + assert( (SQLITE_DELETE-1)/9 == 0 ); + assert( (SQLITE_INSERT-1)/9 == 1 ); + assert( (SQLITE_UPDATE-1)/9 == 2 ); + assert( pDb->pPreUpdateHook ); + assert( db==pDb->db ); + assert( op==SQLITE_INSERT || op==SQLITE_UPDATE || op==SQLITE_DELETE ); + + pCmd = Tcl_DuplicateObj(pDb->pPreUpdateHook); + Tcl_IncrRefCount(pCmd); + Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(azStr[(op-1)/9], -1)); + Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(zDb, -1)); + Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(zTbl, -1)); + Tcl_ListObjAppendElement(0, pCmd, Tcl_NewWideIntObj(iKey1)); + Tcl_ListObjAppendElement(0, pCmd, Tcl_NewWideIntObj(iKey2)); + Tcl_EvalObjEx(pDb->interp, pCmd, TCL_EVAL_DIRECT); + Tcl_DecrRefCount(pCmd); +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + static void DbUpdateHandler( void *p, int op, @@ -694,14 +734,18 @@ static void DbUpdateHandler( ){ SqliteDb *pDb = (SqliteDb *)p; Tcl_Obj *pCmd; + static const char *azStr[] = {"DELETE", "INSERT", "UPDATE"}; + + assert( (SQLITE_DELETE-1)/9 == 0 ); + assert( (SQLITE_INSERT-1)/9 == 1 ); + assert( (SQLITE_UPDATE-1)/9 == 2 ); assert( pDb->pUpdateHook ); assert( op==SQLITE_INSERT || op==SQLITE_UPDATE || op==SQLITE_DELETE ); pCmd = Tcl_DuplicateObj(pDb->pUpdateHook); Tcl_IncrRefCount(pCmd); - Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj( - ( (op==SQLITE_INSERT)?"INSERT":(op==SQLITE_UPDATE)?"UPDATE":"DELETE"), -1)); + Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(azStr[(op-1)/9], -1)); Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(zDb, -1)); Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(zTbl, -1)); Tcl_ListObjAppendElement(0, pCmd, Tcl_NewWideIntObj(rowid)); @@ -1614,6 +1658,46 @@ static int DbEvalNextCmd( } /* +** This function is used by the implementations of the following database +** handle sub-commands: +** +** $db update_hook ?SCRIPT? +** $db wal_hook ?SCRIPT? +** $db commit_hook ?SCRIPT? +** $db preupdate hook ?SCRIPT? +*/ +static void DbHookCmd( + Tcl_Interp *interp, /* Tcl interpreter */ + SqliteDb *pDb, /* Database handle */ + Tcl_Obj *pArg, /* SCRIPT argument (or NULL) */ + Tcl_Obj **ppHook /* Pointer to member of SqliteDb */ +){ + sqlite3 *db = pDb->db; + + if( *ppHook ){ + Tcl_SetObjResult(interp, *ppHook); + if( pArg ){ + Tcl_DecrRefCount(*ppHook); + *ppHook = 0; + } + } + if( pArg ){ + assert( !(*ppHook) ); + if( Tcl_GetCharLength(pArg)>0 ){ + *ppHook = pArg; + Tcl_IncrRefCount(*ppHook); + } + } + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + sqlite3_preupdate_hook(db, (pDb->pPreUpdateHook?DbPreUpdateHandler:0), pDb); +#endif + sqlite3_update_hook(db, (pDb->pUpdateHook?DbUpdateHandler:0), pDb); + sqlite3_rollback_hook(db, (pDb->pRollbackHook?DbRollbackHandler:0), pDb); + sqlite3_wal_hook(db, (pDb->pWalHook?DbWalHandler:0), pDb); +} + +/* ** The "sqlite" command below creates a new Tcl command for each ** connection it opens to an SQLite database. This routine is invoked ** whenever one of those connection-specific commands is executed @@ -1638,11 +1722,12 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){ "errorcode", "eval", "exists", "function", "incrblob", "interrupt", "last_insert_rowid", "nullvalue", "onecolumn", - "profile", "progress", "rekey", - "restore", "rollback_hook", "status", - "timeout", "total_changes", "trace", - "transaction", "unlock_notify", "update_hook", - "version", "wal_hook", 0 + "preupdate", "profile", "progress", + "rekey", "restore", "rollback_hook", + "status", "timeout", "total_changes", + "trace", "transaction", "unlock_notify", + "update_hook", "version", "wal_hook", + 0 }; enum DB_enum { DB_AUTHORIZER, DB_BACKUP, DB_BUSY, @@ -1652,11 +1737,11 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){ DB_ERRORCODE, DB_EVAL, DB_EXISTS, DB_FUNCTION, DB_INCRBLOB, DB_INTERRUPT, DB_LAST_INSERT_ROWID, DB_NULLVALUE, DB_ONECOLUMN, - DB_PROFILE, DB_PROGRESS, DB_REKEY, - DB_RESTORE, DB_ROLLBACK_HOOK, DB_STATUS, - DB_TIMEOUT, DB_TOTAL_CHANGES, DB_TRACE, - DB_TRANSACTION, DB_UNLOCK_NOTIFY, DB_UPDATE_HOOK, - DB_VERSION, DB_WAL_HOOK + DB_PREUPDATE, DB_PROFILE, DB_PROGRESS, + DB_REKEY, DB_RESTORE, DB_ROLLBACK_HOOK, + DB_STATUS, DB_TIMEOUT, DB_TOTAL_CHANGES, + DB_TRACE, DB_TRANSACTION, DB_UNLOCK_NOTIFY, + DB_UPDATE_HOOK, DB_VERSION, DB_WAL_HOOK, }; /* don't leave trailing commas on DB_enum, it confuses the AIX xlc compiler */ @@ -2838,6 +2923,90 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){ } /* + ** $db preupdate_hook count + ** $db preupdate_hook hook ?SCRIPT? + ** $db preupdate_hook new INDEX + ** $db preupdate_hook old INDEX + */ + case DB_PREUPDATE: { +#ifndef SQLITE_ENABLE_PREUPDATE_HOOK + Tcl_AppendResult(interp, "preupdate_hook was omitted at compile-time"); + rc = TCL_ERROR; +#else + static const char *azSub[] = {"count", "depth", "hook", "new", "old", 0}; + enum DbPreupdateSubCmd { + PRE_COUNT, PRE_DEPTH, PRE_HOOK, PRE_NEW, PRE_OLD + }; + int iSub; + + if( objc<3 ){ + Tcl_WrongNumArgs(interp, 2, objv, "SUB-COMMAND ?ARGS?"); + } + if( Tcl_GetIndexFromObj(interp, objv[2], azSub, "sub-command", 0, &iSub) ){ + return TCL_ERROR; + } + + switch( (enum DbPreupdateSubCmd)iSub ){ + case PRE_COUNT: { + int nCol = sqlite3_preupdate_count(pDb->db); + Tcl_SetObjResult(interp, Tcl_NewIntObj(nCol)); + break; + } + + case PRE_HOOK: { + if( objc>4 ){ + Tcl_WrongNumArgs(interp, 2, objv, "hook ?SCRIPT?"); + return TCL_ERROR; + } + DbHookCmd(interp, pDb, (objc==4 ? objv[3] : 0), &pDb->pPreUpdateHook); + break; + } + + case PRE_DEPTH: { + Tcl_Obj *pRet; + if( objc!=3 ){ + Tcl_WrongNumArgs(interp, 3, objv, ""); + return TCL_ERROR; + } + pRet = Tcl_NewIntObj(sqlite3_preupdate_depth(pDb->db)); + Tcl_SetObjResult(interp, pRet); + break; + } + + case PRE_NEW: + case PRE_OLD: { + int iIdx; + sqlite3_value *pValue; + if( objc!=4 ){ + Tcl_WrongNumArgs(interp, 3, objv, "INDEX"); + return TCL_ERROR; + } + if( Tcl_GetIntFromObj(interp, objv[3], &iIdx) ){ + return TCL_ERROR; + } + + if( iSub==PRE_OLD ){ + rc = sqlite3_preupdate_old(pDb->db, iIdx, &pValue); + }else{ + assert( iSub==PRE_NEW ); + rc = sqlite3_preupdate_new(pDb->db, iIdx, &pValue); + } + + if( rc==SQLITE_OK ){ + Tcl_Obj *pObj; + pObj = Tcl_NewStringObj((char*)sqlite3_value_text(pValue), -1); + Tcl_SetObjResult(interp, pObj); + }else{ + Tcl_AppendResult(interp, sqlite3_errmsg(pDb->db), 0); + return TCL_ERROR; + } + } + } +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + break; + } + + /* ** $db wal_hook ?script? ** $db update_hook ?script? ** $db rollback_hook ?script? @@ -2845,42 +3014,19 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){ case DB_WAL_HOOK: case DB_UPDATE_HOOK: case DB_ROLLBACK_HOOK: { - /* set ppHook to point at pUpdateHook or pRollbackHook, depending on ** whether [$db update_hook] or [$db rollback_hook] was invoked. */ Tcl_Obj **ppHook; - if( choice==DB_UPDATE_HOOK ){ - ppHook = &pDb->pUpdateHook; - }else if( choice==DB_WAL_HOOK ){ - ppHook = &pDb->pWalHook; - }else{ - ppHook = &pDb->pRollbackHook; - } - - if( objc!=2 && objc!=3 ){ + if( choice==DB_WAL_HOOK ) ppHook = &pDb->pWalHook; + if( choice==DB_UPDATE_HOOK ) ppHook = &pDb->pUpdateHook; + if( choice==DB_ROLLBACK_HOOK ) ppHook = &pDb->pRollbackHook; + if( objc>3 ){ Tcl_WrongNumArgs(interp, 2, objv, "?SCRIPT?"); return TCL_ERROR; } - if( *ppHook ){ - Tcl_SetObjResult(interp, *ppHook); - if( objc==3 ){ - Tcl_DecrRefCount(*ppHook); - *ppHook = 0; - } - } - if( objc==3 ){ - assert( !(*ppHook) ); - if( Tcl_GetCharLength(objv[2])>0 ){ - *ppHook = objv[2]; - Tcl_IncrRefCount(*ppHook); - } - } - - sqlite3_update_hook(pDb->db, (pDb->pUpdateHook?DbUpdateHandler:0), pDb); - sqlite3_rollback_hook(pDb->db,(pDb->pRollbackHook?DbRollbackHandler:0),pDb); - sqlite3_wal_hook(pDb->db,(pDb->pWalHook?DbWalHandler:0),pDb); + DbHookCmd(interp, pDb, (objc==3 ? objv[2] : 0), ppHook); break; } @@ -3756,6 +3902,9 @@ static void init_all(Tcl_Interp *interp){ extern int Sqlitemultiplex_Init(Tcl_Interp*); extern int SqliteSuperlock_Init(Tcl_Interp*); extern int SqlitetestSyscall_Init(Tcl_Interp*); +#if defined(SQLITE_ENABLE_SESSION) && defined(SQLITE_ENABLE_PREUPDATE_HOOK) + extern int TestSession_Init(Tcl_Interp*); +#endif #if defined(SQLITE_ENABLE_FTS3) || defined(SQLITE_ENABLE_FTS4) extern int Sqlitetestfts3_Init(Tcl_Interp *interp); @@ -3799,6 +3948,9 @@ static void init_all(Tcl_Interp *interp){ Sqlitemultiplex_Init(interp); SqliteSuperlock_Init(interp); SqlitetestSyscall_Init(interp); +#if defined(SQLITE_ENABLE_SESSION) && defined(SQLITE_ENABLE_PREUPDATE_HOOK) + TestSession_Init(interp); +#endif #if defined(SQLITE_ENABLE_FTS3) || defined(SQLITE_ENABLE_FTS4) Sqlitetestfts3_Init(interp); diff --git a/src/test_config.c b/src/test_config.c index 25e6a5698..559e4b7e8 100644 --- a/src/test_config.c +++ b/src/test_config.c @@ -131,6 +131,12 @@ static void set_options(Tcl_Interp *interp){ Tcl_SetVar2(interp, "sqlite_options", "mem5", "0", TCL_GLOBAL_ONLY); #endif +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + Tcl_SetVar2(interp, "sqlite_options", "preupdate", "1", TCL_GLOBAL_ONLY); +#else + Tcl_SetVar2(interp, "sqlite_options", "preupdate", "0", TCL_GLOBAL_ONLY); +#endif + #ifdef SQLITE_MUTEX_OMIT Tcl_SetVar2(interp, "sqlite_options", "mutex", "0", TCL_GLOBAL_ONLY); #else @@ -479,6 +485,12 @@ Tcl_SetVar2(interp, "sqlite_options", "mergesort", "1", TCL_GLOBAL_ONLY); Tcl_SetVar2(interp, "sqlite_options", "schema_version", "1", TCL_GLOBAL_ONLY); #endif +#ifdef SQLITE_ENABLE_SESSION + Tcl_SetVar2(interp, "sqlite_options", "session", "1", TCL_GLOBAL_ONLY); +#else + Tcl_SetVar2(interp, "sqlite_options", "session", "0", TCL_GLOBAL_ONLY); +#endif + #ifdef SQLITE_ENABLE_STAT4 Tcl_SetVar2(interp, "sqlite_options", "stat4", "1", TCL_GLOBAL_ONLY); #else diff --git a/src/update.c b/src/update.c index 3af4017f1..7e7ceeb16 100644 --- a/src/update.c +++ b/src/update.c @@ -584,10 +584,23 @@ void sqlite3Update( VdbeCoverageNeverTaken(v); } sqlite3GenerateRowIndexDelete(pParse, pTab, iDataCur, iIdxCur, aRegIdx); - - /* If changing the record number, delete the old record. */ - if( hasFK || chngKey || pPk!=0 ){ - sqlite3VdbeAddOp2(v, OP_Delete, iDataCur, 0); + + /* If changing the rowid value, or if there are foreign key constraints + ** to process, delete the old record. Otherwise, add a noop OP_Delete + ** to invoke the pre-update hook. + ** + ** That (regNew==regnewRowid+1) is true is also important for the + ** pre-update hook. If the caller invokes preupdate_new(), the returned + ** value is copied from memory cell (regNewRowid+1+iCol), where iCol + ** is the column index supplied by the user. + */ + assert( regNew==regNewRowid+1 ); + sqlite3VdbeAddOp3(v, OP_Delete, iDataCur, + OPFLAG_ISUPDATE | ((hasFK || chngKey || pPk!=0) ? 0 : OPFLAG_ISNOOP), + regNewRowid + ); + if( !pParse->nested ){ + sqlite3VdbeChangeP4(v, -1, (char*)pTab, P4_TABLE); } if( bReplace || chngKey ){ sqlite3VdbeJumpHere(v, j1); diff --git a/src/vdbe.c b/src/vdbe.c index 1e0ff96af..16b7ad600 100644 --- a/src/vdbe.c +++ b/src/vdbe.c @@ -87,6 +87,16 @@ static void updateMaxBlobsize(Mem *p){ #endif /* +** This macro evaluates to true if either the update hook or the preupdate +** hook are enabled for database connect DB. +*/ +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +# define HAS_UPDATE_HOOK(DB) ((DB)->xPreUpdateCallback||(DB)->xUpdateCallback) +#else +# define HAS_UPDATE_HOOK(DB) ((DB)->xUpdateCallback) +#endif + +/* ** The next global variable is incremented each time the OP_Found opcode ** is executed. This is used to test whether or not the foreign key ** operation implemented using OP_FkIsZero is working. This variable @@ -2780,7 +2790,7 @@ case OP_Savepoint: { }else{ db->nSavepoint++; } - + /* Link the new savepoint into the database handle's list. */ pNew->pNext = db->pSavepoint; db->pSavepoint = pNew; @@ -4079,9 +4089,9 @@ case OP_NewRowid: { /* out2-prerelease */ ** is part of an INSERT operation. The difference is only important to ** the update hook. ** -** Parameter P4 may point to a string containing the table-name, or -** may be NULL. If it is not NULL, then the update-hook -** (sqlite3.xUpdateCallback) is invoked following a successful insert. +** Parameter P4 may point to a Table structure, or may be NULL. If it is +** not NULL, then the update-hook (sqlite3.xUpdateCallback) is invoked +** following a successful insert. ** ** (WARNING/TODO: If P1 is a pseudo-cursor and P2 is dynamically ** allocated, then ownership of P2 is transferred to the pseudo-cursor @@ -4107,8 +4117,8 @@ case OP_InsertInt: { int nZero; /* Number of zero-bytes to append */ int seekResult; /* Result of prior seek or 0 if no USESEEKRESULT flag */ const char *zDb; /* database name - used by the update hook */ - const char *zTbl; /* Table name - used by the opdate hook */ - int op; /* Opcode for update hook: SQLITE_UPDATE or SQLITE_INSERT */ + Table *pTab; /* Table structure - used by update and pre-update hooks */ + int op = 0; /* Opcode for update hook: SQLITE_UPDATE or SQLITE_INSERT */ pData = &aMem[pOp->p2]; assert( pOp->p1>=0 && pOp->p1<p->nCursor ); @@ -4118,6 +4128,7 @@ case OP_InsertInt: { assert( pC->pCursor!=0 ); assert( pC->pseudoTableReg==0 ); assert( pC->isTable ); + assert( pOp->p4type==P4_TABLE || pOp->p4type>=P4_STATIC ); REGISTER_TRACE(pOp->p2, pData); if( pOp->opcode==OP_Insert ){ @@ -4131,6 +4142,25 @@ case OP_InsertInt: { iKey = pOp->p3; } + if( pOp->p4type==P4_TABLE && HAS_UPDATE_HOOK(db) ){ + assert( pC->isTable ); + assert( pC->iDb>=0 ); + zDb = db->aDb[pC->iDb].zName; + pTab = pOp->p4.pTab; + op = ((pOp->p5 & OPFLAG_ISUPDATE) ? SQLITE_UPDATE : SQLITE_INSERT); + } + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + /* Invoke the pre-update hook, if any */ + if( db->xPreUpdateCallback + && pOp->p4type==P4_TABLE + && !(pOp->p5 & OPFLAG_ISUPDATE) + && HasRowid(pTab) + ){ + sqlite3VdbePreUpdateHook(p, pC, SQLITE_INSERT, zDb, pTab, iKey, pOp->p2); + } +#endif + if( pOp->p5 & OPFLAG_NCHANGE ) p->nChange++; if( pOp->p5 & OPFLAG_LASTROWID ) db->lastRowid = lastRowid = iKey; if( pData->flags & MEM_Null ){ @@ -4153,18 +4183,13 @@ case OP_InsertInt: { pC->cacheStatus = CACHE_STALE; /* Invoke the update-hook if required. */ - if( rc==SQLITE_OK && db->xUpdateCallback && pOp->p4.z ){ - zDb = db->aDb[pC->iDb].zName; - zTbl = pOp->p4.z; - op = ((pOp->p5 & OPFLAG_ISUPDATE) ? SQLITE_UPDATE : SQLITE_INSERT); - assert( pC->isTable ); - db->xUpdateCallback(db->pUpdateArg, op, zDb, zTbl, iKey); - assert( pC->iDb>=0 ); + if( rc==SQLITE_OK && db->xUpdateCallback && op && HasRowid(pTab) ){ + db->xUpdateCallback(db->pUpdateArg, op, zDb, pTab->zName, iKey); } break; } -/* Opcode: Delete P1 P2 * P4 * +/* Opcode: Delete P1 P2 P3 P4 * ** ** Delete the record at which the P1 cursor is currently pointing. ** @@ -4179,14 +4204,24 @@ case OP_InsertInt: { ** P1 must not be pseudo-table. It has to be a real table with ** multiple rows. ** -** If P4 is not NULL, then it is the name of the table that P1 is -** pointing to. The update hook will be invoked, if it exists. -** If P4 is not NULL then the P1 cursor must have been positioned -** using OP_NotFound prior to invoking this opcode. +** If P4 is not NULL then it points to a Table struture. In this case either +** the update or pre-update hook, or both, may be invoked. The P1 cursor must +** have been positioned using OP_NotFound prior to invoking this opcode in +** this case. Specifically, if one is configured, the pre-update hook is +** invoked if P4 is not NULL. The update-hook is invoked if one is configured, +** P4 is not NULL, and the OPFLAG_NCHANGE flag is set in P2. +** +** If the OPFLAG_ISUPDATE flag is set in P2, then P3 contains the address +** of the memory cell that contains the value that the rowid of the row will +** be set to by the update. */ case OP_Delete: { VdbeCursor *pC; + const char *zDb; + Table *pTab; + int opflags; + opflags = pOp->p2; assert( pOp->p1>=0 && pOp->p1<p->nCursor ); pC = p->apCsr[pOp->p1]; assert( pC!=0 ); @@ -4194,26 +4229,51 @@ case OP_Delete: { assert( pC->deferredMoveto==0 ); #ifdef SQLITE_DEBUG - /* The seek operation that positioned the cursor prior to OP_Delete will - ** have also set the pC->movetoTarget field to the rowid of the row that - ** is being deleted */ - if( pOp->p4.z && pC->isTable ){ + if( pOp->p4type==P4_TABLE && HasRowid(pOp->p4.pTab) ){ + /* The seek operation that positioned the cursor prior to OP_Delete will + ** have also set the pC->movetoTarget field to the rowid of the row that + ** is being deleted */ i64 iKey = 0; sqlite3BtreeKeySize(pC->pCursor, &iKey); - assert( pC->movetoTarget==iKey ); + assert( pC->movetoTarget==iKey ); } #endif + + /* If the update-hook or pre-update-hook will be invoked, set iKey to + ** the rowid of the row being deleted. Set zDb and zTab as well. + */ + if( pOp->p4.z && HAS_UPDATE_HOOK(db) ){ + assert( pC->iDb>=0 ); + zDb = db->aDb[pC->iDb].zName; + pTab = pOp->p4.pTab; + } + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + /* Invoke the pre-update-hook if required. */ + if( db->xPreUpdateCallback && pOp->p4.z && HasRowid(pTab) ){ + assert( !(opflags & OPFLAG_ISUPDATE) || (aMem[pOp->p3].flags & MEM_Int) ); + sqlite3VdbePreUpdateHook(p, pC, + (opflags & OPFLAG_ISUPDATE) ? SQLITE_UPDATE : SQLITE_DELETE, + zDb, pTab, pC->movetoTarget, + pOp->p3 + ); + } +#endif + + if( opflags & OPFLAG_ISNOOP ) break; rc = sqlite3BtreeDelete(pC->pCursor); pC->cacheStatus = CACHE_STALE; - /* Invoke the update-hook if required. */ - if( rc==SQLITE_OK && db->xUpdateCallback && pOp->p4.z && pC->isTable ){ - db->xUpdateCallback(db->pUpdateArg, SQLITE_DELETE, - db->aDb[pC->iDb].zName, pOp->p4.z, pC->movetoTarget); - assert( pC->iDb>=0 ); + /* Update the change-counter and invoke the update-hook if required. */ + if( opflags & OPFLAG_NCHANGE ){ + p->nChange++; + assert( pOp->p4.z ); + if( rc==SQLITE_OK && db->xUpdateCallback && HasRowid(pTab) ){ + db->xUpdateCallback(db->pUpdateArg, SQLITE_DELETE, zDb, pTab->zName, + pC->movetoTarget); + } } - if( pOp->p2 & OPFLAG_NCHANGE ) p->nChange++; break; } /* Opcode: ResetCount * * * * * diff --git a/src/vdbe.h b/src/vdbe.h index b715241b4..b7822a2e7 100644 --- a/src/vdbe.h +++ b/src/vdbe.h @@ -59,6 +59,7 @@ struct VdbeOp { KeyInfo *pKeyInfo; /* Used when p4type is P4_KEYINFO */ int *ai; /* Used when p4type is P4_INTARRAY */ SubProgram *pProgram; /* Used when p4type is P4_SUBPROGRAM */ + Table *pTab; /* Used when p4type is P4_TABLE */ int (*xAdvance)(BtCursor *, int *); } p4; #ifdef SQLITE_ENABLE_EXPLAIN_COMMENTS @@ -119,6 +120,7 @@ typedef struct VdbeOpList VdbeOpList; #define P4_INTARRAY (-15) /* P4 is a vector of 32-bit integers */ #define P4_SUBPROGRAM (-18) /* P4 is a pointer to a SubProgram structure */ #define P4_ADVANCE (-19) /* P4 is a pointer to BtreeNext() or BtreePrev() */ +#define P4_TABLE (-20) /* P4 is a pointer to a Table structure */ /* Error message codes for OP_Halt */ #define P5_ConstraintNotNull 1 diff --git a/src/vdbeInt.h b/src/vdbeInt.h index 877da1143..8e1c9f457 100644 --- a/src/vdbeInt.h +++ b/src/vdbeInt.h @@ -398,6 +398,25 @@ struct Vdbe { #define VDBE_MAGIC_DEAD 0xb606c3c8 /* The VDBE has been deallocated */ /* +** Structure used to store the context required by the +** sqlite3_preupdate_*() API functions. +*/ +struct PreUpdate { + Vdbe *v; + VdbeCursor *pCsr; /* Cursor to read old values from */ + int op; /* One of SQLITE_INSERT, UPDATE, DELETE */ + u8 *aRecord; /* old.* database record */ + KeyInfo keyinfo; + UnpackedRecord *pUnpacked; /* Unpacked version of aRecord[] */ + UnpackedRecord *pNewUnpacked; /* Unpacked version of new.* record */ + int iNewReg; /* Register for new.* values */ + i64 iKey1; /* First key value passed to hook */ + i64 iKey2; /* Second key value passed to hook */ + int iPKey; /* If not negative index of IPK column */ + Mem *aNew; /* Array of new.* values */ +}; + +/* ** Function prototypes */ void sqlite3VdbeFreeCursor(Vdbe *, VdbeCursor*); @@ -456,6 +475,8 @@ int sqlite3VdbeMemClearAndResize(Mem *pMem, int n); int sqlite3VdbeCloseStatement(Vdbe *, int); void sqlite3VdbeFrameDelete(VdbeFrame*); int sqlite3VdbeFrameRestore(VdbeFrame *); +void sqlite3VdbePreUpdateHook( + Vdbe *, VdbeCursor *, int, const char*, Table *, i64, int); int sqlite3VdbeTransferError(Vdbe *p); int sqlite3VdbeSorterInit(sqlite3 *, int, VdbeCursor *); diff --git a/src/vdbeapi.c b/src/vdbeapi.c index b29338eb3..6bc186e0b 100644 --- a/src/vdbeapi.c +++ b/src/vdbeapi.c @@ -1484,6 +1484,187 @@ int sqlite3_stmt_status(sqlite3_stmt *pStmt, int op, int resetFlag){ return (int)v; } +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** Allocate and populate an UnpackedRecord structure based on the serialized +** record in nKey/pKey. Return a pointer to the new UnpackedRecord structure +** if successful, or a NULL pointer if an OOM error is encountered. +*/ +static UnpackedRecord *vdbeUnpackRecord( + KeyInfo *pKeyInfo, + int nKey, + const void *pKey +){ + char *dummy; /* Dummy argument for AllocUnpackedRecord() */ + UnpackedRecord *pRet; /* Return value */ + + pRet = sqlite3VdbeAllocUnpackedRecord(pKeyInfo, 0, 0, &dummy); + if( pRet ){ + memset(pRet->aMem, 0, sizeof(Mem)*(pKeyInfo->nField+1)); + sqlite3VdbeRecordUnpack(pKeyInfo, nKey, pKey, pRet); + } + return pRet; +} + +/* +** This function is called from within a pre-update callback to retrieve +** a field of the row currently being updated or deleted. +*/ +int sqlite3_preupdate_old(sqlite3 *db, int iIdx, sqlite3_value **ppValue){ + PreUpdate *p = db->pPreUpdate; + int rc = SQLITE_OK; + + /* Test that this call is being made from within an SQLITE_DELETE or + ** SQLITE_UPDATE pre-update callback, and that iIdx is within range. */ + if( !p || p->op==SQLITE_INSERT ){ + rc = SQLITE_MISUSE_BKPT; + goto preupdate_old_out; + } + if( iIdx>=p->pCsr->nField || iIdx<0 ){ + rc = SQLITE_RANGE; + goto preupdate_old_out; + } + + /* If the old.* record has not yet been loaded into memory, do so now. */ + if( p->pUnpacked==0 ){ + u32 nRec; + u8 *aRec; + + rc = sqlite3BtreeDataSize(p->pCsr->pCursor, &nRec); + if( rc!=SQLITE_OK ) goto preupdate_old_out; + aRec = sqlite3DbMallocRaw(db, nRec); + if( !aRec ) goto preupdate_old_out; + rc = sqlite3BtreeData(p->pCsr->pCursor, 0, nRec, aRec); + if( rc==SQLITE_OK ){ + p->pUnpacked = vdbeUnpackRecord(&p->keyinfo, nRec, aRec); + if( !p->pUnpacked ) rc = SQLITE_NOMEM; + } + if( rc!=SQLITE_OK ){ + sqlite3DbFree(db, aRec); + goto preupdate_old_out; + } + p->aRecord = aRec; + } + + if( iIdx>=p->pUnpacked->nField ){ + *ppValue = (sqlite3_value *)columnNullValue(); + }else{ + *ppValue = &p->pUnpacked->aMem[iIdx]; + if( iIdx==p->iPKey ){ + sqlite3VdbeMemSetInt64(*ppValue, p->iKey1); + } + } + + preupdate_old_out: + sqlite3Error(db, rc); + return sqlite3ApiExit(db, rc); +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** This function is called from within a pre-update callback to retrieve +** the number of columns in the row being updated, deleted or inserted. +*/ +int sqlite3_preupdate_count(sqlite3 *db){ + PreUpdate *p = db->pPreUpdate; + return (p ? p->keyinfo.nField : 0); +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** This function is designed to be called from within a pre-update callback +** only. It returns zero if the change that caused the callback was made +** immediately by a user SQL statement. Or, if the change was made by a +** trigger program, it returns the number of trigger programs currently +** on the stack (1 for a top-level trigger, 2 for a trigger fired by a +** top-level trigger etc.). +** +** For the purposes of the previous paragraph, a foreign key CASCADE, SET NULL +** or SET DEFAULT action is considered a trigger. +*/ +int sqlite3_preupdate_depth(sqlite3 *db){ + PreUpdate *p = db->pPreUpdate; + return (p ? p->v->nFrame : 0); +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** This function is called from within a pre-update callback to retrieve +** a field of the row currently being updated or inserted. +*/ +int sqlite3_preupdate_new(sqlite3 *db, int iIdx, sqlite3_value **ppValue){ + PreUpdate *p = db->pPreUpdate; + int rc = SQLITE_OK; + Mem *pMem; + + if( !p || p->op==SQLITE_DELETE ){ + rc = SQLITE_MISUSE_BKPT; + goto preupdate_new_out; + } + if( iIdx>=p->pCsr->nField || iIdx<0 ){ + rc = SQLITE_RANGE; + goto preupdate_new_out; + } + + if( p->op==SQLITE_INSERT ){ + /* For an INSERT, memory cell p->iNewReg contains the serialized record + ** that is being inserted. Deserialize it. */ + UnpackedRecord *pUnpack = p->pNewUnpacked; + if( !pUnpack ){ + Mem *pData = &p->v->aMem[p->iNewReg]; + rc = sqlite3VdbeMemExpandBlob(pData); + if( rc!=SQLITE_OK ) goto preupdate_new_out; + pUnpack = vdbeUnpackRecord(&p->keyinfo, pData->n, pData->z); + if( !pUnpack ){ + rc = SQLITE_NOMEM; + goto preupdate_new_out; + } + p->pNewUnpacked = pUnpack; + } + if( iIdx>=pUnpack->nField ){ + pMem = (sqlite3_value *)columnNullValue(); + }else{ + pMem = &pUnpack->aMem[iIdx]; + if( iIdx==p->iPKey ){ + sqlite3VdbeMemSetInt64(pMem, p->iKey2); + } + } + }else{ + /* For an UPDATE, memory cell (p->iNewReg+1+iIdx) contains the required + ** value. Make a copy of the cell contents and return a pointer to it. + ** It is not safe to return a pointer to the memory cell itself as the + ** caller may modify the value text encoding. + */ + assert( p->op==SQLITE_UPDATE ); + if( !p->aNew ){ + p->aNew = (Mem *)sqlite3DbMallocZero(db, sizeof(Mem) * p->pCsr->nField); + if( !p->aNew ){ + rc = SQLITE_NOMEM; + goto preupdate_new_out; + } + } + assert( iIdx>=0 && iIdx<p->pCsr->nField ); + pMem = &p->aNew[iIdx]; + if( pMem->flags==0 ){ + if( iIdx==p->iPKey ){ + sqlite3VdbeMemSetInt64(pMem, p->iKey2); + }else{ + rc = sqlite3VdbeMemCopy(pMem, &p->v->aMem[p->iNewReg+1+iIdx]); + if( rc!=SQLITE_OK ) goto preupdate_new_out; + } + } + } + *ppValue = pMem; + + preupdate_new_out: + sqlite3Error(db, rc); + return sqlite3ApiExit(db, rc); +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + #ifdef SQLITE_ENABLE_STMT_SCANSTATUS /* ** Return status data for a single loop within query pStmt. diff --git a/src/vdbeaux.c b/src/vdbeaux.c index cd9ecaa69..aaed25383 100644 --- a/src/vdbeaux.c +++ b/src/vdbeaux.c @@ -66,6 +66,7 @@ const char *sqlite3_sql(sqlite3_stmt *pStmt){ void sqlite3VdbeSwap(Vdbe *pA, Vdbe *pB){ Vdbe tmp, *pTmp; char *zTmp; + assert( pA->db==pB->db ); tmp = *pA; *pA = *pB; *pB = tmp; @@ -3237,6 +3238,7 @@ void sqlite3VdbeRecordUnpack( pMem->db = pKeyInfo->db; /* pMem->flags = 0; // sqlite3VdbeSerialGet() will set this for us */ pMem->szMalloc = 0; + pMem->z = 0; d += sqlite3VdbeSerialGet(&aKey[d], serial_type, pMem); pMem++; if( (++u)>=p->nField ) break; @@ -4183,3 +4185,85 @@ void sqlite3VtabImportErrmsg(Vdbe *p, sqlite3_vtab *pVtab){ pVtab->zErrMsg = 0; } #endif /* SQLITE_OMIT_VIRTUALTABLE */ + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + +/* +** If the second argument is not NULL, release any allocations associated +** with the memory cells in the p->aMem[] array. Also free the UnpackedRecord +** structure itself, using sqlite3DbFree(). +** +** This function is used to free UnpackedRecord structures allocated by +** the vdbeUnpackRecord() function found in vdbeapi.c. +*/ +static void vdbeFreeUnpacked(sqlite3 *db, UnpackedRecord *p){ + if( p ){ + int i; + for(i=0; i<p->nField; i++){ + Mem *pMem = &p->aMem[i]; + if( pMem->zMalloc ) sqlite3VdbeMemRelease(pMem); + } + sqlite3DbFree(db, p); + } +} + +/* +** Invoke the pre-update hook. If this is an UPDATE or DELETE pre-update call, +** then cursor passed as the second argument should point to the row about +** to be update or deleted. If the application calls sqlite3_preupdate_old(), +** the required value will be read from the row the cursor points to. +*/ +void sqlite3VdbePreUpdateHook( + Vdbe *v, /* Vdbe pre-update hook is invoked by */ + VdbeCursor *pCsr, /* Cursor to grab old.* values from */ + int op, /* SQLITE_INSERT, UPDATE or DELETE */ + const char *zDb, /* Database name */ + Table *pTab, /* Modified table */ + i64 iKey1, /* Initial key value */ + int iReg /* Register for new.* record */ +){ + sqlite3 *db = v->db; + i64 iKey2; + PreUpdate preupdate; + const char *zTbl = pTab->zName; + static const u8 fakeSortOrder = 0; + + assert( db->pPreUpdate==0 ); + memset(&preupdate, 0, sizeof(PreUpdate)); + if( op==SQLITE_UPDATE ){ + iKey2 = v->aMem[iReg].u.i; + }else{ + iKey2 = iKey1; + } + + assert( pCsr->nField==pTab->nCol + || (pCsr->nField==pTab->nCol+1 && op==SQLITE_DELETE && iReg==-1) + ); + + preupdate.v = v; + preupdate.pCsr = pCsr; + preupdate.op = op; + preupdate.iNewReg = iReg; + preupdate.keyinfo.db = db; + preupdate.keyinfo.enc = ENC(db); + preupdate.keyinfo.nField = pTab->nCol; + preupdate.keyinfo.aSortOrder = (u8*)&fakeSortOrder; + preupdate.iKey1 = iKey1; + preupdate.iKey2 = iKey2; + preupdate.iPKey = pTab->iPKey; + + db->pPreUpdate = &preupdate; + db->xPreUpdateCallback(db->pPreUpdateArg, db, op, zDb, zTbl, iKey1, iKey2); + db->pPreUpdate = 0; + sqlite3DbFree(db, preupdate.aRecord); + vdbeFreeUnpacked(db, preupdate.pUnpacked); + vdbeFreeUnpacked(db, preupdate.pNewUnpacked); + if( preupdate.aNew ){ + int i; + for(i=0; i<pCsr->nField; i++){ + sqlite3VdbeMemRelease(&preupdate.aNew[i]); + } + sqlite3DbFree(db, preupdate.aNew); + } +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ diff --git a/src/vdbeblob.c b/src/vdbeblob.c index ea01f5ce8..522213a0b 100644 --- a/src/vdbeblob.c +++ b/src/vdbeblob.c @@ -30,6 +30,8 @@ struct Incrblob { BtCursor *pCsr; /* Cursor pointing at blob row */ sqlite3_stmt *pStmt; /* Statement holding cursor open */ sqlite3 *db; /* The associated database */ + char *zDb; /* Database name */ + Table *pTab; /* Table object */ }; @@ -205,6 +207,8 @@ int sqlite3_blob_open( sqlite3BtreeLeaveAll(db); goto blob_open_out; } + pBlob->pTab = pTab; + pBlob->zDb = db->aDb[sqlite3SchemaToIndex(db, pTab->pSchema)].zName; /* Now search pTab for the exact column. */ for(iCol=0; iCol<pTab->nCol; iCol++) { @@ -392,6 +396,30 @@ static int blobReadWrite( */ assert( db == v->db ); sqlite3BtreeEnterCursor(p->pCsr); + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + if( xCall==sqlite3BtreePutData && db->xPreUpdateCallback ){ + /* If a pre-update hook is registered and this is a write cursor, + ** invoke it here. + ** + ** TODO: The preupdate-hook is passed SQLITE_DELETE, even though this + ** operation should really be an SQLITE_UPDATE. This is probably + ** incorrect, but is convenient because at this point the new.* values + ** are not easily obtainable. And for the sessions module, an + ** SQLITE_UPDATE where the PK columns do not change is handled in the + ** same way as an SQLITE_DELETE (the SQLITE_DELETE code is actually + ** slightly more efficient). Since you cannot write to a PK column + ** using the incremental-blob API, this works. For the sessions module + ** anyhow. + */ + sqlite3_int64 iKey; + sqlite3BtreeKeySize(p->pCsr, &iKey); + sqlite3VdbePreUpdateHook( + v, v->apCsr[0], SQLITE_DELETE, p->zDb, p->pTab, iKey, -1 + ); + } +#endif + rc = xCall(p->pCsr, iOffset+p->iOffset, n, z); sqlite3BtreeLeaveCursor(p->pCsr); if( rc==SQLITE_ABORT ){ diff --git a/src/vdbemem.c b/src/vdbemem.c index 870fb5bd8..db527327e 100644 --- a/src/vdbemem.c +++ b/src/vdbemem.c @@ -799,7 +799,6 @@ void sqlite3VdbeMemShallowCopy(Mem *pTo, const Mem *pFrom, int srcType){ int sqlite3VdbeMemCopy(Mem *pTo, const Mem *pFrom){ int rc = SQLITE_OK; - assert( pTo->db==pFrom->db ); assert( (pFrom->flags & MEM_RowSet)==0 ); if( VdbeMemDynamic(pTo) ) vdbeMemClearExternAndSetNull(pTo); memcpy(pTo, pFrom, MEMCELLSIZE); diff --git a/test/fkey6.test b/test/fkey6.test index 6fc3de211..7c24704e0 100644 --- a/test/fkey6.test +++ b/test/fkey6.test @@ -1,4 +1,4 @@ -# 2013-07-11 +# 2012 December 17 # # The author disclaims copyright to this source code. In place of # a legal notice, here is a blessing: diff --git a/test/hook.test b/test/hook.test index de6fbdd25..27705ff8c 100644 --- a/test/hook.test +++ b/test/hook.test @@ -21,6 +21,7 @@ set testdir [file dirname $argv0] source $testdir/tester.tcl +set ::testprefix hook do_test hook-1.2 { db commit_hook @@ -196,10 +197,13 @@ do_test hook-4.1.2 { # EVIDENCE-OF: R-33257-44249 The update hook is not invoked when WITHOUT # ROWID tables are modified. # +breakpoint do_test hook-4.1.2w { set ::update_hook {} execsql { INSERT INTO t1w VALUES(4, 'four'); +PRAGMA vdbe_debug=on; +PRAGMA vdbe_addoptrace=on; DELETE FROM t1w WHERE b = 'two'; UPDATE t1w SET b = '' WHERE a = 1 OR a = 3; DELETE FROM t1w WHERE 1; -- Avoid the truncate optimization (for now) @@ -438,4 +442,413 @@ do_test hook-6.2 { } {COMMIT ROLLBACK} unset ::hooks +#---------------------------------------------------------------------------- +# The following tests - hook-7.* - test the pre-update hook. +# +ifcapable !preupdate { + finish_test + return +} +# +# 7.1.1 - INSERT statement. +# 7.1.2 - INSERT INTO ... SELECT statement. +# 7.1.3 - REPLACE INTO ... (rowid conflict) +# 7.1.4 - REPLACE INTO ... (other index conflicts) +# 7.1.5 - REPLACE INTO ... (both rowid and other index conflicts) +# +# 7.2.1 - DELETE statement. +# 7.2.2 - DELETE statement that uses the truncate optimization. +# +# 7.3.1 - UPDATE statement. +# 7.3.2 - UPDATE statement that modifies the rowid. +# 7.3.3 - UPDATE OR REPLACE ... (rowid conflict). +# 7.3.4 - UPDATE OR REPLACE ... (other index conflicts) +# 7.3.4 - UPDATE OR REPLACE ... (both rowid and other index conflicts) +# +# 7.4.1 - Test that the pre-update-hook is invoked only once if a row being +# deleted is removed by a BEFORE trigger. +# +# 7.4.2 - Test that the pre-update-hook is invoked if a BEFORE trigger +# removes a row being updated. In this case the update hook should +# be invoked with SQLITE_INSERT as the opcode when inserting the +# new version of the row. +# +# TODO: Short records (those created before a column is added to a table +# using ALTER TABLE) +# + +proc do_preupdate_test {tn sql x} { + set X [list] + foreach elem $x {lappend X $elem} + uplevel do_test $tn [list " + set ::preupdate \[list\] + execsql { $sql } + set ::preupdate + "] [list $X] +} + +proc preupdate_hook {args} { + set type [lindex $args 0] + eval lappend ::preupdate $args + if {$type != "INSERT"} { + for {set i 0} {$i < [db preupdate count]} {incr i} { + lappend ::preupdate [db preupdate old $i] + } + } + if {$type != "DELETE"} { + for {set i 0} {$i < [db preupdate count]} {incr i} { + set rc [catch { db preupdate new $i } v] + lappend ::preupdate $v + } + } +} + +db close +forcedelete test.db +sqlite3 db test.db +db preupdate hook preupdate_hook + +# Set up a schema to use for tests 7.1.* to 7.3.*. +do_execsql_test 7.0 { + CREATE TABLE t1(a, b); + CREATE TABLE t2(x, y); + CREATE TABLE t3(i, j, UNIQUE(i)); + + INSERT INTO t2 VALUES('a', 'b'); + INSERT INTO t2 VALUES('c', 'd'); + + INSERT INTO t3 VALUES(4, 16); + INSERT INTO t3 VALUES(5, 25); + INSERT INTO t3 VALUES(6, 36); +} + +do_preupdate_test 7.1.1 { + INSERT INTO t1 VALUES('x', 'y') +} {INSERT main t1 1 1 x y} + +# 7.1.2.1 does not use the xfer optimization. 7.1.2.2 does. +do_preupdate_test 7.1.2.1 { + INSERT INTO t1 SELECT y, x FROM t2; +} {INSERT main t1 2 2 b a INSERT main t1 3 3 d c} +do_preupdate_test 7.1.2.2 { + INSERT INTO t1 SELECT * FROM t2; +} {INSERT main t1 4 4 a b INSERT main t1 5 5 c d} + +do_preupdate_test 7.1.3 { + REPLACE INTO t1(rowid, a, b) VALUES(1, 1, 1); +} { + DELETE main t1 1 1 x y + INSERT main t1 1 1 1 1 +} + +do_preupdate_test 7.1.4 { + REPLACE INTO t3 VALUES(4, NULL); +} { + DELETE main t3 1 1 4 16 + INSERT main t3 4 4 4 {} +} + +do_preupdate_test 7.1.5 { + REPLACE INTO t3(rowid, i, j) VALUES(2, 6, NULL); +} { + DELETE main t3 2 2 5 25 + DELETE main t3 3 3 6 36 + INSERT main t3 2 2 6 {} +} + +do_execsql_test 7.2.0 { SELECT rowid FROM t1 } {1 2 3 4 5} + +do_preupdate_test 7.2.1 { + DELETE FROM t1 WHERE rowid = 3 +} { + DELETE main t1 3 3 d c +} +do_preupdate_test 7.2.2 { + DELETE FROM t1 +} { + DELETE main t1 1 1 1 1 + DELETE main t1 2 2 b a + DELETE main t1 4 4 a b + DELETE main t1 5 5 c d +} + +do_execsql_test 7.3.0 { + DELETE FROM t1; + DELETE FROM t2; + DELETE FROM t3; + + INSERT INTO t2 VALUES('a', 'b'); + INSERT INTO t2 VALUES('c', 'd'); + + INSERT INTO t3 VALUES(4, 16); + INSERT INTO t3 VALUES(5, 25); + INSERT INTO t3 VALUES(6, 36); +} + +do_preupdate_test 7.3.1 { + UPDATE t2 SET y = y||y; +} { + UPDATE main t2 1 1 a b a bb + UPDATE main t2 2 2 c d c dd +} + +do_preupdate_test 7.3.2 { + UPDATE t2 SET rowid = rowid-1; +} { + UPDATE main t2 1 0 a bb a bb + UPDATE main t2 2 1 c dd c dd +} + +do_preupdate_test 7.3.3 { + UPDATE OR REPLACE t2 SET rowid = 1 WHERE x = 'a' +} { + DELETE main t2 1 1 c dd + UPDATE main t2 0 1 a bb a bb +} + +do_preupdate_test 7.3.4.1 { + UPDATE OR REPLACE t3 SET i = 5 WHERE i = 6 +} { + DELETE main t3 2 2 5 25 + UPDATE main t3 3 3 6 36 5 36 +} + +do_execsql_test 7.3.4.2 { + INSERT INTO t3 VALUES(10, 100); + SELECT rowid, * FROM t3; +} {1 4 16 3 5 36 4 10 100} + +do_preupdate_test 7.3.5 { + UPDATE OR REPLACE t3 SET rowid = 1, i = 5 WHERE j = 100; +} { + DELETE main t3 1 1 4 16 + DELETE main t3 3 3 5 36 + UPDATE main t3 4 1 10 100 5 100 +} + +do_execsql_test 7.4.1.0 { + CREATE TABLE t4(a, b); + INSERT INTO t4 VALUES('a', 1); + INSERT INTO t4 VALUES('b', 2); + INSERT INTO t4 VALUES('c', 3); + + CREATE TRIGGER t4t BEFORE DELETE ON t4 BEGIN + DELETE FROM t4 WHERE b = 1; + END; +} + +do_preupdate_test 7.4.1.1 { + DELETE FROM t4 WHERE b = 3 +} { + DELETE main t4 1 1 a 1 + DELETE main t4 3 3 c 3 +} + +do_execsql_test 7.4.1.2 { + INSERT INTO t4(rowid, a, b) VALUES(1, 'a', 1); + INSERT INTO t4(rowid, a, b) VALUES(3, 'c', 3); +} +do_preupdate_test 7.4.1.3 { + DELETE FROM t4 WHERE b = 1 +} { + DELETE main t4 1 1 a 1 +} + +do_execsql_test 7.4.2.0 { + CREATE TABLE t5(a, b); + INSERT INTO t5 VALUES('a', 1); + INSERT INTO t5 VALUES('b', 2); + INSERT INTO t5 VALUES('c', 3); + + CREATE TRIGGER t5t BEFORE UPDATE ON t5 BEGIN + DELETE FROM t5 WHERE b = 1; + END; +} +do_preupdate_test 7.4.2.1 { + UPDATE t5 SET b = 4 WHERE a = 'c' +} { + DELETE main t5 1 1 a 1 + UPDATE main t5 3 3 c 3 c 4 +} + +do_execsql_test 7.4.2.2 { + INSERT INTO t5(rowid, a, b) VALUES(1, 'a', 1); +} + +do_preupdate_test 7.4.2.3 { + UPDATE t5 SET b = 5 WHERE a = 'a' +} { + DELETE main t5 1 1 a 1 +} + +do_execsql_test 7.5.1.0 { + CREATE TABLE t7(a, b); + INSERT INTO t7 VALUES('one', 'two'); + INSERT INTO t7 VALUES('three', 'four'); + ALTER TABLE t7 ADD COLUMN c DEFAULT NULL; +} + +do_preupdate_test 7.5.1.1 { + DELETE FROM t7 WHERE a = 'one' +} { + DELETE main t7 1 1 one two {} +} + +do_preupdate_test 7.5.1.2 { + UPDATE t7 SET b = 'five' +} { + UPDATE main t7 2 2 three four {} three five {} +} + +do_execsql_test 7.5.2.0 { + CREATE TABLE t8(a, b); + INSERT INTO t8 VALUES('one', 'two'); + INSERT INTO t8 VALUES('three', 'four'); + ALTER TABLE t8 ADD COLUMN c DEFAULT 'xxx'; +} + +ifcapable !session { + # At time of writing, these two are broken. They demonstrate that the + # sqlite3_preupdate_old() method does not handle the case where ALTER TABLE + # has been used to add a column with a default value other than NULL. + # + do_preupdate_test 7.5.2.1 { + DELETE FROM t8 WHERE a = 'one' + } { + DELETE main t8 1 1 one two xxx + } + do_preupdate_test 7.5.2.2 { + UPDATE t8 SET b = 'five' + } { + UPDATE main t8 2 2 three four xxx three five xxx + } +} + +# This block of tests verifies that IPK values are correctly reported +# by the sqlite3_preupdate_old() and sqlite3_preupdate_new() functions. +# +do_execsql_test 7.6.1 { CREATE TABLE t9(a, b INTEGER PRIMARY KEY, c) } +do_preupdate_test 7.6.2 { + INSERT INTO t9 VALUES(1, 2, 3); + UPDATE t9 SET b = b+1, c = c+1; + DELETE FROM t9 WHERE a = 1; +} { + INSERT main t9 2 2 1 2 3 + UPDATE main t9 2 3 1 2 3 1 3 4 + DELETE main t9 3 3 1 3 4 +} + +#-------------------------------------------------------------------------- +# Test that the sqlite3_preupdate_depth() API seems to work. +# +proc preupdate_hook {args} { + set type [lindex $args 0] + eval lappend ::preupdate $args + eval lappend ::preupdate [db preupdate depth] + + if {$type != "INSERT"} { + for {set i 0} {$i < [db preupdate count]} {incr i} { + lappend ::preupdate [db preupdate old $i] + } + } + if {$type != "DELETE"} { + for {set i 0} {$i < [db preupdate count]} {incr i} { + set rc [catch { db preupdate new $i } v] + lappend ::preupdate $v + } + } +} + +db close +forcedelete test.db +sqlite3 db test.db +db preupdate hook preupdate_hook + +do_execsql_test 7.6.1 { + CREATE TABLE t1(x PRIMARY KEY); + CREATE TABLE t2(x PRIMARY KEY); + CREATE TABLE t3(x PRIMARY KEY); + CREATE TABLE t4(x PRIMARY KEY); + + CREATE TRIGGER a AFTER INSERT ON t1 BEGIN INSERT INTO t2 VALUES(new.x); END; + CREATE TRIGGER b AFTER INSERT ON t2 BEGIN INSERT INTO t3 VALUES(new.x); END; + CREATE TRIGGER c AFTER INSERT ON t3 BEGIN INSERT INTO t4 VALUES(new.x); END; + + CREATE TRIGGER d AFTER UPDATE ON t1 BEGIN UPDATE t2 SET x = new.x; END; + CREATE TRIGGER e AFTER UPDATE ON t2 BEGIN UPDATE t3 SET x = new.x; END; + CREATE TRIGGER f AFTER UPDATE ON t3 BEGIN UPDATE t4 SET x = new.x; END; + + CREATE TRIGGER g AFTER DELETE ON t1 BEGIN DELETE FROM t2 WHERE 1; END; + CREATE TRIGGER h AFTER DELETE ON t2 BEGIN DELETE FROM t3 WHERE 1; END; + CREATE TRIGGER i AFTER DELETE ON t3 BEGIN DELETE FROM t4 WHERE 1; END; +} + +do_preupdate_test 7.6.2 { + INSERT INTO t1 VALUES('xyz'); +} { + INSERT main t1 1 1 0 xyz + INSERT main t2 1 1 1 xyz + INSERT main t3 1 1 2 xyz + INSERT main t4 1 1 3 xyz +} +do_preupdate_test 7.6.3 { + UPDATE t1 SET x = 'abc'; +} { + UPDATE main t1 1 1 0 xyz abc + UPDATE main t2 1 1 1 xyz abc + UPDATE main t3 1 1 2 xyz abc + UPDATE main t4 1 1 3 xyz abc +} +do_preupdate_test 7.6.4 { + DELETE FROM t1 WHERE 1; +} { + DELETE main t1 1 1 0 abc + DELETE main t2 1 1 1 abc + DELETE main t3 1 1 2 abc + DELETE main t4 1 1 3 abc +} + +do_execsql_test 7.6.5 { + DROP TRIGGER a; DROP TRIGGER b; DROP TRIGGER c; + DROP TRIGGER d; DROP TRIGGER e; DROP TRIGGER f; + DROP TRIGGER g; DROP TRIGGER h; DROP TRIGGER i; + + CREATE TRIGGER a BEFORE INSERT ON t1 BEGIN INSERT INTO t2 VALUES(new.x); END; + CREATE TRIGGER b BEFORE INSERT ON t2 BEGIN INSERT INTO t3 VALUES(new.x); END; + CREATE TRIGGER c BEFORE INSERT ON t3 BEGIN INSERT INTO t4 VALUES(new.x); END; + + CREATE TRIGGER d BEFORE UPDATE ON t1 BEGIN UPDATE t2 SET x = new.x; END; + CREATE TRIGGER e BEFORE UPDATE ON t2 BEGIN UPDATE t3 SET x = new.x; END; + CREATE TRIGGER f BEFORE UPDATE ON t3 BEGIN UPDATE t4 SET x = new.x; END; + + CREATE TRIGGER g BEFORE DELETE ON t1 BEGIN DELETE FROM t2 WHERE 1; END; + CREATE TRIGGER h BEFORE DELETE ON t2 BEGIN DELETE FROM t3 WHERE 1; END; + CREATE TRIGGER i BEFORE DELETE ON t3 BEGIN DELETE FROM t4 WHERE 1; END; +} + +do_preupdate_test 7.6.6 { + INSERT INTO t1 VALUES('xyz'); +} { + INSERT main t4 1 1 3 xyz + INSERT main t3 1 1 2 xyz + INSERT main t2 1 1 1 xyz + INSERT main t1 1 1 0 xyz +} +do_preupdate_test 7.6.3 { + UPDATE t1 SET x = 'abc'; +} { + UPDATE main t4 1 1 3 xyz abc + UPDATE main t3 1 1 2 xyz abc + UPDATE main t2 1 1 1 xyz abc + UPDATE main t1 1 1 0 xyz abc +} +do_preupdate_test 7.6.4 { + DELETE FROM t1 WHERE 1; +} { + DELETE main t4 1 1 3 abc + DELETE main t3 1 1 2 abc + DELETE main t2 1 1 1 abc + DELETE main t1 1 1 0 abc +} + finish_test diff --git a/test/permutations.test b/test/permutations.test index 44f62e806..6a4b0e4dd 100644 --- a/test/permutations.test +++ b/test/permutations.test @@ -89,6 +89,9 @@ foreach f [glob $testdir/*.test] { lappend alltests [file tail $f] } foreach f [glob -nocomplain $testdir/../ext/rtree/*.test] { lappend alltests $f } +foreach f [glob -nocomplain $testdir/../ext/session/*.test] { + lappend alltests $f +} if {$::tcl_platform(platform)!="unix"} { set alltests [test_set $alltests -exclude crash.test crash2.test] @@ -96,7 +99,7 @@ if {$::tcl_platform(platform)!="unix"} { set alltests [test_set $alltests -exclude { all.test async.test quick.test veryquick.test memleak.test permutations.test soak.test fts3.test - mallocAll.test rtree.test full.test + mallocAll.test rtree.test full.test session.test }] set allquicktests [test_set $alltests -exclude { @@ -939,6 +942,26 @@ test_suite "rtree" -description { All R-tree related tests. Provides coverage of source file rtree.c. } -files [glob -nocomplain $::testdir/../ext/rtree/*.test] +test_suite "session" -description { + All session module related tests. +} -files [glob -nocomplain $::testdir/../ext/session/*.test] + +test_suite "session_eec" -description { + All session module related tests with sqlite3_extended_result_codes() set. +} -files [ + glob -nocomplain $::testdir/../ext/session/*.test +] -dbconfig { + sqlite3_extended_result_codes $::dbhandle 1 +} + +test_suite "session_strm" -description { + All session module related tests using the streaming APIs. +} -files [ + glob -nocomplain $::testdir/../ext/session/*.test +] -dbconfig { + set ::sqlite3session_streams 1 +} + test_suite "no_optimization" -description { Run test scripts with optimizations disabled using the sqlite3_test_control(SQLITE_TESTCTRL_OPTIMIZATIONS) interface. diff --git a/test/session.test b/test/session.test new file mode 100644 index 000000000..4fd1ec018 --- /dev/null +++ b/test/session.test @@ -0,0 +1,22 @@ +# 2008 June 23 +# +# 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 runs all rtree related tests. +# + +set testdir [file dirname $argv0] +source $testdir/permutations.test + +ifcapable session { + # First run tests with sqlite3_extended_error_codes() set, then + # again with it clear. + run_test_suite session_eec + run_test_suite session + run_test_suite session_strm +} + +finish_test diff --git a/test/tclsqlite.test b/test/tclsqlite.test index 3d9cd46ac..d54e4aa8d 100644 --- a/test/tclsqlite.test +++ b/test/tclsqlite.test @@ -35,7 +35,7 @@ do_test tcl-1.1 { do_test tcl-1.2 { set v [catch {db bogus} msg] lappend v $msg -} {1 {bad option "bogus": must be authorizer, backup, busy, cache, changes, close, collate, collation_needed, commit_hook, complete, copy, enable_load_extension, errorcode, eval, exists, function, incrblob, interrupt, last_insert_rowid, nullvalue, onecolumn, profile, progress, rekey, restore, rollback_hook, status, timeout, total_changes, trace, transaction, unlock_notify, update_hook, version, or wal_hook}} +} {1 {bad option "bogus": must be authorizer, backup, busy, cache, changes, close, collate, collation_needed, commit_hook, complete, copy, enable_load_extension, errorcode, eval, exists, function, incrblob, interrupt, last_insert_rowid, nullvalue, onecolumn, preupdate, profile, progress, rekey, restore, rollback_hook, status, timeout, total_changes, trace, transaction, unlock_notify, update_hook, version, or wal_hook}} do_test tcl-1.2.1 { set v [catch {db cache bogus} msg] lappend v $msg diff --git a/test/tester.tcl b/test/tester.tcl index dad22661b..05c2aaeb0 100644 --- a/test/tester.tcl +++ b/test/tester.tcl @@ -710,6 +710,12 @@ proc fix_testname {varname} { } } +proc normalize_list {L} { + set L2 [list] + foreach l $L {lappend L2 $l} + set L2 +} + proc do_execsql_test {testname sql {result {}}} { fix_testname testname uplevel do_test [list $testname] [list "execsql {$sql}"] [list [list {*}$result]] diff --git a/tool/mksqlite3c.tcl b/tool/mksqlite3c.tcl index 4ab8b12b4..5786c7c7b 100644 --- a/tool/mksqlite3c.tcl +++ b/tool/mksqlite3c.tcl @@ -112,6 +112,7 @@ foreach hdr { pcache.h pragma.h rtree.h + sqlite3session.h sqlite3ext.h sqlite3.h sqliteicu.h @@ -125,6 +126,7 @@ foreach hdr { set available_hdr($hdr) 1 } set available_hdr(sqliteInt.h) 0 +set available_hdr(sqlite3session.h) 0 # 78 stars used for comment formatting. set s78 \ @@ -194,7 +196,7 @@ proc copy_file {filename} { if {[regexp $declpattern $line all funcname]} { # Add the SQLITE_PRIVATE or SQLITE_API keyword before functions. # so that linkage can be modified at compile-time. - if {[regexp {^sqlite3_} $funcname]} { + if {[regexp {^sqlite3[a-z]*_} $funcname]} { puts $out "SQLITE_API $line" } else { puts $out "SQLITE_PRIVATE $line" @@ -334,6 +336,8 @@ foreach file { rtree.c icu.c fts3_icu.c + + sqlite3session.c } { copy_file tsrc/$file } diff --git a/tool/mksqlite3h.tcl b/tool/mksqlite3h.tcl index a89b9f9be..323111bb3 100644 --- a/tool/mksqlite3h.tcl +++ b/tool/mksqlite3h.tcl @@ -71,12 +71,16 @@ fconfigure stdout -translation lf set filelist [subst { $TOP/src/sqlite.h.in $TOP/ext/rtree/sqlite3rtree.h + $TOP/ext/session/sqlite3session.h }] # Process the source files. # foreach file $filelist { set in [open $file] + if {![regexp {sqlite\.h\.in} $file]} { + puts "/******** Begin file [file tail $file] *********/" + } while {![eof $in]} { set line [gets $in] @@ -108,4 +112,7 @@ foreach file $filelist { puts $line } close $in + if {![regexp {sqlite\.h\.in} $file]} { + puts "/******** End of [file tail $file] *********/" + } } diff --git a/tool/symbols.sh b/tool/symbols.sh index befffce5c..5e80078fa 100644 --- a/tool/symbols.sh +++ b/tool/symbols.sh @@ -10,7 +10,7 @@ gcc -c -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_RTREE \ -DSQLITE_ENABLE_MEMORY_MANAGEMENT -DSQLITE_ENABLE_STAT3 \ -DSQLITE_ENABLE_MEMSYS5 -DSQLITE_ENABLE_UNLOCK_NOTIFY \ -DSQLITE_ENABLE_COLUMN_METADATA -DSQLITE_ENABLE_ATOMIC_WRITE \ - -DSQLITE_ENABLE_ICU \ + -DSQLITE_ENABLE_ICU -DSQLITE_ENABLE_PREUPDATE_HOOK -DSQLITE_ENABLE_SESSION \ sqlite3.c nm sqlite3.o | grep ' [TD] ' | sort -k 3 |