summaryrefslogtreecommitdiffstats
path: root/ext/session
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-05 17:28:19 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-05 17:28:19 +0000
commit18657a960e125336f704ea058e25c27bd3900dcb (patch)
tree17b438b680ed45a996d7b59951e6aa34023783f2 /ext/session
parentInitial commit. (diff)
downloadsqlite3-18657a960e125336f704ea058e25c27bd3900dcb.tar.xz
sqlite3-18657a960e125336f704ea058e25c27bd3900dcb.zip
Adding upstream version 3.40.1.upstream/3.40.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ext/session')
-rw-r--r--ext/session/changeset.c417
-rw-r--r--ext/session/changesetfuzz.c1238
-rw-r--r--ext/session/changesetfuzz1.test84
-rw-r--r--ext/session/session1.test689
-rw-r--r--ext/session/session2.test639
-rw-r--r--ext/session/session3.test214
-rw-r--r--ext/session/session4.test146
-rw-r--r--ext/session/session5.test408
-rw-r--r--ext/session/session6.test91
-rw-r--r--ext/session/session8.test91
-rw-r--r--ext/session/session9.test287
-rw-r--r--ext/session/sessionA.test106
-rw-r--r--ext/session/sessionB.test507
-rw-r--r--ext/session/sessionC.test197
-rw-r--r--ext/session/sessionD.test257
-rw-r--r--ext/session/sessionE.test113
-rw-r--r--ext/session/sessionF.test294
-rw-r--r--ext/session/sessionG.test250
-rw-r--r--ext/session/sessionH.test84
-rw-r--r--ext/session/session_common.tcl215
-rw-r--r--ext/session/session_speed_test.c358
-rw-r--r--ext/session/sessionat.test254
-rw-r--r--ext/session/sessionbig.test108
-rw-r--r--ext/session/sessiondiff.test114
-rw-r--r--ext/session/sessionfault.test589
-rw-r--r--ext/session/sessionfault2.test284
-rw-r--r--ext/session/sessioninvert.test183
-rw-r--r--ext/session/sessionmem.test57
-rw-r--r--ext/session/sessionnoop.test187
-rw-r--r--ext/session/sessionrebase.test476
-rw-r--r--ext/session/sessionsize.test131
-rw-r--r--ext/session/sessionstat1.test310
-rw-r--r--ext/session/sessionwor.test123
-rw-r--r--ext/session/sqlite3session.c5820
-rw-r--r--ext/session/sqlite3session.h1721
-rw-r--r--ext/session/test_session.c1463
36 files changed, 18505 insertions, 0 deletions
diff --git a/ext/session/changeset.c b/ext/session/changeset.c
new file mode 100644
index 0000000..9cf6294
--- /dev/null
+++ b/ext/session/changeset.c
@@ -0,0 +1,417 @@
+/*
+** 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;
+ sqlite3_int64 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 = ftell(f);
+ rewind(f);
+ pBuf = sqlite3_malloc64( sz ? sz : 1 );
+ if( pBuf==0 ){
+ fprintf(stderr, "cannot allocate %d to hold content of \"%s\"\n",
+ (int)sz, zFilename);
+ exit(1);
+ }
+ if( sz>0 ){
+ if( fread(pBuf, (size_t)sz, 1, f)!=1 ){
+ fprintf(stderr, "cannot read all %d bytes of \"%s\"\n",
+ (int)sz, zFilename);
+ exit(1);
+ }
+ fclose(f);
+ }
+ *pSz = (int)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/changesetfuzz.c b/ext/session/changesetfuzz.c
new file mode 100644
index 0000000..bbd72f6
--- /dev/null
+++ b/ext/session/changesetfuzz.c
@@ -0,0 +1,1238 @@
+/*
+** 2018-11-01
+**
+** 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 "changesetfuzz" command
+** line utility for fuzzing changeset blobs without corrupting them.
+*/
+
+
+/************************************************************************
+** USAGE:
+**
+** This program may be invoked in two ways:
+**
+** changesetfuzz INPUT
+** changesetfuzz INPUT SEED N
+**
+** Argument INPUT must be the name of a file containing a binary changeset.
+** In the first form above, this program outputs a human-readable version
+** of the same changeset. This is chiefly for debugging.
+**
+** As well as changesets, this program can also dump and fuzz patchsets.
+** The term "changeset" is used for both patchsets and changesets from this
+** point on.
+**
+** In the second form, arguments SEED and N must both be integers. In this
+** case, this program writes N binary changesets to disk. Each output
+** changeset is a slightly modified - "fuzzed" - version of the input.
+** The output changesets are written to files name "INPUT-$n", where $n is
+** an integer between 0 and N-1, inclusive. Output changesets are always
+** well-formed. Parameter SEED is used to seed the PRNG - any two
+** invocations of this program with the same SEED and input changeset create
+** the same N output changesets.
+**
+** The ways in which an input changeset may be fuzzed are as follows:
+**
+** 1. Any two values within the changeset may be exchanged.
+**
+** 2. Any TEXT, BLOB, INTEGER or REAL value within the changeset
+** may have a single bit of its content flipped.
+**
+** 3. Any value within a changeset may be replaced by a pseudo-randomly
+** generated value.
+**
+** The above operations never set a PRIMARY KEY column to NULL. Nor do they
+** set any value to "undefined", or replace any "undefined" value with
+** another. Any such operation risks producing a changeset that is not
+** well-formed.
+**
+** 4. A single change may be duplicated.
+**
+** 5. A single change may be removed, so long as this does not mean that
+** there are zero changes following a table-header within the changeset.
+**
+** 6. A single change may have its type (INSERT, DELETE, UPDATE) changed.
+** If an INSERT is changed to a DELETE (or vice versa), the type is
+** simply changed - no other modifications are required. If an INSERT
+** or DELETE is changed to an UPDATE, then the single record is duplicated
+** (as both the old.* and new.* records of the new UPDATE change). If an
+** UPDATE is changed to a DELETE or INSERT, the new.* record is discarded
+** and any "undefined" fields replaced with pseudo-randomly generated
+** values.
+**
+** 7. An UPDATE change that modifies N table columns may be modified so
+** that it updates N-1 columns, so long as (N>1).
+**
+** 8. The "indirect" flag may be toggled for any change.
+**
+** Entire group of changes may also be operated on:
+**
+** 9. Duplicate an existing group.
+**
+** 10. Remove an existing group.
+**
+** 11. The positions of two groups may be exchanged.
+**
+** There are also schema changes:
+**
+** 12. A non-PK column may be added to a table. In this case a NULL
+** value is appended to all records.
+**
+** 13. A PK column may be added to a table. In this case a non-NULL
+** value is appended to all INSERT, DELETE and UPDATE old.* records.
+** An "undefined" is appended to new.* UPDATE records.
+**
+** 14. A column may be removed from a table, provided that it is not the
+** only PRIMARY KEY column in the table. In this case the corresponding
+** field is removed from all records. In cases where this leaves an UPDATE
+** with no non-PK, non-undefined fields, the entire change is removed.
+*/
+
+#include "sqlite3.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <ctype.h>
+
+#define FUZZ_VALUE_SUB 1 /* Replace one value with a copy of another */
+#define FUZZ_VALUE_MOD 2 /* Modify content by 1 bit */
+#define FUZZ_VALUE_RND 3 /* Replace with pseudo-random value */
+
+#define FUZZ_CHANGE_DUP 4 /* Duplicate an existing change */
+#define FUZZ_CHANGE_DEL 5 /* Completely remove one change */
+#define FUZZ_CHANGE_TYPE 6 /* Change the type of one change */
+#define FUZZ_CHANGE_FIELD 7 /* Change an UPDATE to modify fewer columns */
+#define FUZZ_CHANGE_INDIRECT 8 /* Toggle the "indirect" flag of a change */
+
+#define FUZZ_GROUP_DUP 9 /* Duplicate a change group */
+#define FUZZ_GROUP_DEL 10 /* Delete an entire change group */
+#define FUZZ_GROUP_SWAP 11 /* Exchange the position of two groups */
+
+#define FUZZ_COLUMN_ADD 12 /* Add column to table definition */
+#define FUZZ_COLUMN_ADDPK 13 /* Add PK column to table definition */
+#define FUZZ_COLUMN_DEL 14 /* Remove column from table definition */
+
+
+
+typedef unsigned char u8;
+typedef sqlite3_uint64 u64;
+typedef sqlite3_int64 i64;
+typedef unsigned int u32;
+
+/*
+** Show a usage message on stderr then quit.
+*/
+static void usage(const char *argv0){
+ fprintf(stderr, "Usage: %s FILENAME ?SEED N?\n", argv0);
+ exit(1);
+}
+
+/*
+** Read the content of a disk file into an in-memory buffer
+*/
+static void fuzzReadFile(const char *zFilename, int *pSz, void **ppBuf){
+ FILE *f;
+ sqlite3_int64 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 = ftell(f);
+ rewind(f);
+ pBuf = sqlite3_malloc64( sz ? sz : 1 );
+ if( pBuf==0 ){
+ fprintf(stderr, "cannot allocate %d to hold content of \"%s\"\n",
+ (int)sz, zFilename);
+ exit(1);
+ }
+ if( sz>0 ){
+ if( fread(pBuf, (size_t)sz, 1, f)!=1 ){
+ fprintf(stderr, "cannot read all %d bytes of \"%s\"\n",
+ (int)sz, zFilename);
+ exit(1);
+ }
+ fclose(f);
+ }
+ *pSz = (int)sz;
+ *ppBuf = pBuf;
+}
+
+/*
+** Write the contents of buffer pBuf, size nBuf bytes, into file zFilename
+** on disk. zFilename, if it already exists, is clobbered.
+*/
+static void fuzzWriteFile(const char *zFilename, void *pBuf, int nBuf){
+ FILE *f;
+ f = fopen(zFilename, "wb");
+ if( f==0 ){
+ fprintf(stderr, "cannot open \"%s\" for writing\n", zFilename);
+ exit(1);
+ }
+ if( fwrite(pBuf, nBuf, 1, f)!=1 ){
+ fprintf(stderr, "cannot write to \"%s\"\n", zFilename);
+ exit(1);
+ }
+ fclose(f);
+}
+
+static int fuzzCorrupt(){
+ return SQLITE_CORRUPT;
+}
+
+/*************************************************************************
+** The following block is a copy of the implementation of SQLite function
+** sqlite3_randomness. This version has two important differences:
+**
+** 1. It always uses the same seed. So the sequence of random data output
+** is the same for every run of the program.
+**
+** 2. It is not threadsafe.
+*/
+static struct sqlite3PrngType {
+ unsigned char i, j; /* State variables */
+ unsigned char s[256]; /* State variables */
+} sqlite3Prng = {
+ 0xAF, 0x28,
+ {
+ 0x71, 0xF5, 0xB4, 0x6E, 0x80, 0xAB, 0x1D, 0xB8,
+ 0xFB, 0xB7, 0x49, 0xBF, 0xFF, 0x72, 0x2D, 0x14,
+ 0x79, 0x09, 0xE3, 0x78, 0x76, 0xB0, 0x2C, 0x0A,
+ 0x8E, 0x23, 0xEE, 0xDF, 0xE0, 0x9A, 0x2F, 0x67,
+ 0xE1, 0xBE, 0x0E, 0xA7, 0x08, 0x97, 0xEB, 0x77,
+ 0x78, 0xBA, 0x9D, 0xCA, 0x49, 0x4C, 0x60, 0x9A,
+ 0xF6, 0xBD, 0xDA, 0x7F, 0xBC, 0x48, 0x58, 0x52,
+ 0xE5, 0xCD, 0x83, 0x72, 0x23, 0x52, 0xFF, 0x6D,
+ 0xEF, 0x0F, 0x82, 0x29, 0xA0, 0x83, 0x3F, 0x7D,
+ 0xA4, 0x88, 0x31, 0xE7, 0x88, 0x92, 0x3B, 0x9B,
+ 0x3B, 0x2C, 0xC2, 0x4C, 0x71, 0xA2, 0xB0, 0xEA,
+ 0x36, 0xD0, 0x00, 0xF1, 0xD3, 0x39, 0x17, 0x5D,
+ 0x2A, 0x7A, 0xE4, 0xAD, 0xE1, 0x64, 0xCE, 0x0F,
+ 0x9C, 0xD9, 0xF5, 0xED, 0xB0, 0x22, 0x5E, 0x62,
+ 0x97, 0x02, 0xA3, 0x8C, 0x67, 0x80, 0xFC, 0x88,
+ 0x14, 0x0B, 0x15, 0x10, 0x0F, 0xC7, 0x40, 0xD4,
+ 0xF1, 0xF9, 0x0E, 0x1A, 0xCE, 0xB9, 0x1E, 0xA1,
+ 0x72, 0x8E, 0xD7, 0x78, 0x39, 0xCD, 0xF4, 0x5D,
+ 0x2A, 0x59, 0x26, 0x34, 0xF2, 0x73, 0x0B, 0xA0,
+ 0x02, 0x51, 0x2C, 0x03, 0xA3, 0xA7, 0x43, 0x13,
+ 0xE8, 0x98, 0x2B, 0xD2, 0x53, 0xF8, 0xEE, 0x91,
+ 0x7D, 0xE7, 0xE3, 0xDA, 0xD5, 0xBB, 0xC0, 0x92,
+ 0x9D, 0x98, 0x01, 0x2C, 0xF9, 0xB9, 0xA0, 0xEB,
+ 0xCF, 0x32, 0xFA, 0x01, 0x49, 0xA5, 0x1D, 0x9A,
+ 0x76, 0x86, 0x3F, 0x40, 0xD4, 0x89, 0x8F, 0x9C,
+ 0xE2, 0xE3, 0x11, 0x31, 0x37, 0xB2, 0x49, 0x28,
+ 0x35, 0xC0, 0x99, 0xB6, 0xD0, 0xBC, 0x66, 0x35,
+ 0xF7, 0x83, 0x5B, 0xD7, 0x37, 0x1A, 0x2B, 0x18,
+ 0xA6, 0xFF, 0x8D, 0x7C, 0x81, 0xA8, 0xFC, 0x9E,
+ 0xC4, 0xEC, 0x80, 0xD0, 0x98, 0xA7, 0x76, 0xCC,
+ 0x9C, 0x2F, 0x7B, 0xFF, 0x8E, 0x0E, 0xBB, 0x90,
+ 0xAE, 0x13, 0x06, 0xF5, 0x1C, 0x4E, 0x52, 0xF7
+ }
+};
+
+/*
+** Generate and return single random byte
+*/
+static unsigned char fuzzRandomByte(void){
+ unsigned char t;
+ sqlite3Prng.i++;
+ t = sqlite3Prng.s[sqlite3Prng.i];
+ sqlite3Prng.j += t;
+ sqlite3Prng.s[sqlite3Prng.i] = sqlite3Prng.s[sqlite3Prng.j];
+ sqlite3Prng.s[sqlite3Prng.j] = t;
+ t += sqlite3Prng.s[sqlite3Prng.i];
+ return sqlite3Prng.s[t];
+}
+
+/*
+** Return N random bytes.
+*/
+static void fuzzRandomBlob(int nBuf, unsigned char *zBuf){
+ int i;
+ for(i=0; i<nBuf; i++){
+ zBuf[i] = fuzzRandomByte();
+ }
+}
+
+/*
+** Return a random integer between 0 and nRange (not inclusive).
+*/
+static unsigned int fuzzRandomInt(unsigned int nRange){
+ unsigned int ret;
+ assert( nRange>0 );
+ fuzzRandomBlob(sizeof(ret), (unsigned char*)&ret);
+ return (ret % nRange);
+}
+
+static u64 fuzzRandomU64(){
+ u64 ret;
+ fuzzRandomBlob(sizeof(ret), (unsigned char*)&ret);
+ return ret;
+}
+
+static void fuzzRandomSeed(unsigned int iSeed){
+ int i;
+ for(i=0; i<256; i+=4){
+ sqlite3Prng.s[i] ^= ((iSeed >> 24) & 0xFF);
+ sqlite3Prng.s[i+1] ^= ((iSeed >> 16) & 0xFF);
+ sqlite3Prng.s[i+2] ^= ((iSeed >> 8) & 0xFF);
+ sqlite3Prng.s[i+3] ^= ((iSeed >> 0) & 0xFF);
+ }
+}
+/*
+** End of code for generating pseudo-random values.
+*************************************************************************/
+
+typedef struct FuzzChangeset FuzzChangeset;
+typedef struct FuzzChangesetGroup FuzzChangesetGroup;
+typedef struct FuzzChange FuzzChange;
+
+/*
+** Object containing partially parsed changeset.
+*/
+struct FuzzChangeset {
+ int bPatchset; /* True for a patchset */
+ FuzzChangesetGroup **apGroup; /* Array of groups in changeset */
+ int nGroup; /* Number of items in list pGroup */
+ u8 **apVal; /* Array of all values in changeset */
+ int nVal; /* Number of used slots in apVal[] */
+ int nChange; /* Number of changes in changeset */
+ int nUpdate; /* Number of UPDATE changes in changeset */
+};
+
+/*
+** There is one object of this type for each change-group (table header)
+** in the input changeset.
+*/
+struct FuzzChangesetGroup {
+ const char *zTab; /* Name of table */
+ int nCol; /* Number of columns in table */
+ u8 *aPK; /* PK array for this table */
+ u8 *aChange; /* Buffer containing array of changes */
+ int szChange; /* Size of buffer aChange[] in bytes */
+ int nChange; /* Number of changes in buffer aChange[] */
+};
+
+/*
+** Description of a fuzz change to be applied to a changeset.
+*/
+struct FuzzChange {
+ int eType; /* One of the FUZZ_* constants above */
+ int iChange; /* Change or UPDATE to modify */
+ int iGroup; /* Group to modify */
+ int iDelete; /* Field to remove (FUZZ_COLUMN_DEL) */
+ u8 *pSub1; /* Replace this value with pSub2 */
+ u8 *pSub2; /* And this one with pSub1 */
+ u8 aSub[128]; /* Buffer for substitute value */
+ int iCurrent; /* Current change number */
+};
+
+/*
+** Allocate and return nByte bytes of zeroed memory.
+*/
+static void *fuzzMalloc(sqlite3_int64 nByte){
+ void *pRet = sqlite3_malloc64(nByte);
+ if( pRet ){
+ memset(pRet, 0, (size_t)nByte);
+ }
+ return pRet;
+}
+
+/*
+** Free the buffer indicated by the first argument. This function is used
+** to free buffers allocated by fuzzMalloc().
+*/
+static void fuzzFree(void *p){
+ sqlite3_free(p);
+}
+
+/*
+** Argument p points to a buffer containing an SQLite varint that, assuming the
+** input is not corrupt, may be between 0 and 0x7FFFFFFF, inclusive. Before
+** returning, this function sets (*pnVal) to the value of that varint, and
+** returns the number of bytes of space that it takes up.
+*/
+static int fuzzGetVarint(u8 *p, int *pnVal){
+ int i;
+ sqlite3_uint64 nVal = 0;
+ for(i=0; i<9; i++){
+ nVal = (nVal<<7) + (p[i] & 0x7F);
+ if( (p[i] & 0x80)==0 ){
+ i++;
+ break;
+ }
+ }
+ *pnVal = (int)nVal;
+ return i;
+}
+
+/*
+** Write value nVal into the buffer indicated by argument p as an SQLite
+** varint. nVal is guaranteed to be between 0 and (2^21-1), inclusive.
+** Return the number of bytes written to buffer p.
+*/
+static int fuzzPutVarint(u8 *p, int nVal){
+ assert( nVal>0 && nVal<2097152 );
+ if( nVal<128 ){
+ p[0] = (u8)nVal;
+ return 1;
+ }
+ if( nVal<16384 ){
+ p[0] = ((nVal >> 7) & 0x7F) | 0x80;
+ p[1] = (nVal & 0x7F);
+ return 2;
+ }
+
+ p[0] = ((nVal >> 14) & 0x7F) | 0x80;
+ p[1] = ((nVal >> 7) & 0x7F) | 0x80;
+ p[2] = (nVal & 0x7F);
+ return 3;
+}
+
+/*
+** Read a 64-bit big-endian integer value from buffer aRec[]. Return
+** the value read.
+*/
+static i64 fuzzGetI64(u8 *aRec){
+ return (i64)(
+ (((u64)aRec[0]) << 56)
+ + (((u64)aRec[1]) << 48)
+ + (((u64)aRec[2]) << 40)
+ + (((u64)aRec[3]) << 32)
+ + (((u64)aRec[4]) << 24)
+ + (((u64)aRec[5]) << 16)
+ + (((u64)aRec[6]) << 8)
+ + (((u64)aRec[7]) << 0)
+ );
+}
+
+/*
+** Write value iVal to buffer aRec[] as an unsigned 64-bit big-endian integer.
+*/
+static void fuzzPutU64(u8 *aRec, u64 iVal){
+ aRec[0] = (iVal>>56) & 0xFF;
+ aRec[1] = (iVal>>48) & 0xFF;
+ aRec[2] = (iVal>>40) & 0xFF;
+ aRec[3] = (iVal>>32) & 0xFF;
+ aRec[4] = (iVal>>24) & 0xFF;
+ aRec[5] = (iVal>>16) & 0xFF;
+ aRec[6] = (iVal>> 8) & 0xFF;
+ aRec[7] = (iVal) & 0xFF;
+}
+
+/*
+** Parse a single table-header from the input. Allocate a new change-group
+** object with the results. Return SQLITE_OK if successful, or an error code
+** otherwise.
+*/
+static int fuzzParseHeader(
+ FuzzChangeset *pParse, /* Changeset parse object */
+ u8 **ppHdr, /* IN/OUT: Iterator */
+ u8 *pEnd, /* 1 byte past EOF */
+ FuzzChangesetGroup **ppGrp /* OUT: New change-group object */
+){
+ int rc = SQLITE_OK;
+ FuzzChangesetGroup *pGrp;
+ u8 cHdr = (pParse->bPatchset ? 'P' : 'T');
+
+ assert( pEnd>(*ppHdr) );
+ pGrp = (FuzzChangesetGroup*)fuzzMalloc(sizeof(FuzzChangesetGroup));
+ if( !pGrp ){
+ rc = SQLITE_NOMEM;
+ }else{
+ u8 *p = *ppHdr;
+ if( p[0]!=cHdr ){
+ rc = fuzzCorrupt();
+ }else{
+ p++;
+ p += fuzzGetVarint(p, &pGrp->nCol);
+ pGrp->aPK = p;
+ p += pGrp->nCol;
+ pGrp->zTab = (const char*)p;
+ p = &p[strlen((const char*)p)+1];
+
+ if( p>=pEnd ){
+ rc = fuzzCorrupt();
+ }
+ }
+ *ppHdr = p;
+ }
+
+ if( rc!=SQLITE_OK ){
+ fuzzFree(pGrp);
+ pGrp = 0;
+ }
+
+ *ppGrp = pGrp;
+ return rc;
+}
+
+/*
+** Argument p points to a buffer containing a single changeset-record value.
+** This function attempts to determine the size of the value in bytes. If
+** successful, it sets (*pSz) to the size and returns SQLITE_OK. Or, if the
+** buffer does not contain a valid value, SQLITE_CORRUPT is returned and
+** the final value of (*pSz) is undefined.
+*/
+static int fuzzChangeSize(u8 *p, int *pSz){
+ u8 eType = p[0];
+ switch( eType ){
+ case 0x00: /* undefined */
+ case 0x05: /* null */
+ *pSz = 1;
+ break;
+
+ case 0x01: /* integer */
+ case 0x02: /* real */
+ *pSz = 9;
+ break;
+
+ case 0x03: /* text */
+ case 0x04: { /* blob */
+ int nTxt;
+ int sz;
+ sz = fuzzGetVarint(&p[1], &nTxt);
+ *pSz = 1 + sz + nTxt;
+ break;
+ }
+
+ default:
+ return fuzzCorrupt();
+ }
+ return SQLITE_OK;
+}
+
+/*
+** When this function is called, (*ppRec) points to the start of a
+** record in a changeset being parsed. This function adds entries
+** to the pParse->apVal[] array for all values and advances (*ppRec)
+** to one byte past the end of the record. Argument pEnd points to
+** one byte past the end of the input changeset.
+**
+** Argument bPkOnly is true if the record being parsed is part of
+** a DELETE record in a patchset. In this case, all non-primary-key
+** fields have been omitted from the record.
+**
+** SQLITE_OK is returned if successful, or an SQLite error code otherwise.
+*/
+static int fuzzParseRecord(
+ u8 **ppRec, /* IN/OUT: Iterator */
+ u8 *pEnd, /* One byte after end of input data */
+ FuzzChangeset *pParse, /* Changeset parse context */
+ int bPkOnly /* True if non-PK fields omitted */
+){
+ int rc = SQLITE_OK;
+ FuzzChangesetGroup *pGrp = pParse->apGroup[pParse->nGroup-1];
+ int i;
+ u8 *p = *ppRec;
+
+ for(i=0; rc==SQLITE_OK && i<pGrp->nCol; i++){
+ if( bPkOnly==0 || pGrp->aPK[i] ){
+ int sz;
+ if( p>=pEnd ) break;
+ if( (pParse->nVal & (pParse->nVal-1))==0 ){
+ int nNew = pParse->nVal ? pParse->nVal*2 : 4;
+ u8 **apNew = (u8**)sqlite3_realloc(pParse->apVal, nNew*sizeof(u8*));
+ if( apNew==0 ) return SQLITE_NOMEM;
+ pParse->apVal = apNew;
+ }
+ pParse->apVal[pParse->nVal++] = p;
+ rc = fuzzChangeSize(p, &sz);
+ p += sz;
+ }
+ }
+
+ if( rc==SQLITE_OK && i<pGrp->nCol ){
+ rc = fuzzCorrupt();
+ }
+
+ *ppRec = p;
+ return rc;
+}
+
+/*
+** Parse the array of changes starting at (*ppData) and add entries for
+** all values to the pParse->apVal[] array. Argument pEnd points to one byte
+** past the end of the input changeset. If successful, set (*ppData) to point
+** to one byte past the end of the change array and return SQLITE_OK.
+** Otherwise, return an SQLite error code. The final value of (*ppData) is
+** undefined in this case.
+*/
+static int fuzzParseChanges(u8 **ppData, u8 *pEnd, FuzzChangeset *pParse){
+ u8 cHdr = (pParse->bPatchset ? 'P' : 'T');
+ FuzzChangesetGroup *pGrp = pParse->apGroup[pParse->nGroup-1];
+ int rc = SQLITE_OK;
+ u8 *p = *ppData;
+
+ pGrp->aChange = p;
+ while( rc==SQLITE_OK && p<pEnd && p[0]!=cHdr ){
+ u8 eOp = p[0];
+ u8 bIndirect = p[1];
+
+ p += 2;
+ if( eOp==SQLITE_UPDATE ){
+ pParse->nUpdate++;
+ if( pParse->bPatchset==0 ){
+ rc = fuzzParseRecord(&p, pEnd, pParse, 0);
+ }
+ }else if( eOp!=SQLITE_INSERT && eOp!=SQLITE_DELETE ){
+ rc = fuzzCorrupt();
+ }
+ if( rc==SQLITE_OK ){
+ int bPkOnly = (eOp==SQLITE_DELETE && pParse->bPatchset);
+ rc = fuzzParseRecord(&p, pEnd, pParse, bPkOnly);
+ }
+ pGrp->nChange++;
+ pParse->nChange++;
+ }
+ pGrp->szChange = p - pGrp->aChange;
+
+ *ppData = p;
+ return rc;
+}
+
+/*
+** Parse the changeset stored in buffer pChangeset (nChangeset bytes in
+** size). If successful, write the results into (*pParse) and return
+** SQLITE_OK. Or, if an error occurs, return an SQLite error code. The
+** final state of (*pParse) is undefined in this case.
+*/
+static int fuzzParseChangeset(
+ u8 *pChangeset, /* Buffer containing changeset */
+ int nChangeset, /* Size of buffer in bytes */
+ FuzzChangeset *pParse /* OUT: Results of parse */
+){
+ u8 *pEnd = &pChangeset[nChangeset];
+ u8 *p = pChangeset;
+ int rc = SQLITE_OK;
+
+ memset(pParse, 0, sizeof(FuzzChangeset));
+ if( nChangeset>0 ){
+ pParse->bPatchset = (pChangeset[0]=='P');
+ }
+
+ while( rc==SQLITE_OK && p<pEnd ){
+ FuzzChangesetGroup *pGrp = 0;
+
+ /* Read a table-header from the changeset */
+ rc = fuzzParseHeader(pParse, &p, pEnd, &pGrp);
+ assert( (rc==SQLITE_OK)==(pGrp!=0) );
+
+ /* If the table-header was successfully parsed, add the new change-group
+ ** to the array and parse the associated changes. */
+ if( rc==SQLITE_OK ){
+ FuzzChangesetGroup **apNew = (FuzzChangesetGroup**)sqlite3_realloc64(
+ pParse->apGroup, sizeof(FuzzChangesetGroup*)*(pParse->nGroup+1)
+ );
+ if( apNew==0 ){
+ rc = SQLITE_NOMEM;
+ }else{
+ apNew[pParse->nGroup] = pGrp;
+ pParse->apGroup = apNew;
+ pParse->nGroup++;
+ }
+ rc = fuzzParseChanges(&p, pEnd, pParse);
+ }
+ }
+
+ return rc;
+}
+
+/*
+** When this function is called, (*ppRec) points to the first byte of
+** a record that is part of change-group pGrp. This function attempts
+** to output a human-readable version of the record to stdout and advance
+** (*ppRec) to point to the first byte past the end of the record before
+** returning. If successful, SQLITE_OK is returned. Otherwise, an SQLite
+** error code.
+**
+** If parameter bPkOnly is non-zero, then all non-primary-key fields have
+** been omitted from the record. This occurs for records that are part
+** of DELETE changes in patchsets.
+*/
+static int fuzzPrintRecord(FuzzChangesetGroup *pGrp, u8 **ppRec, int bPKOnly){
+ int rc = SQLITE_OK;
+ u8 *p = *ppRec;
+ int i;
+ const char *zPre = " (";
+
+ for(i=0; i<pGrp->nCol; i++){
+ if( bPKOnly==0 || pGrp->aPK[i] ){
+ u8 eType = p++[0];
+ switch( eType ){
+ case 0x00: /* undefined */
+ printf("%sn/a", zPre);
+ break;
+
+ case 0x01: { /* integer */
+ sqlite3_int64 iVal = 0;
+ iVal = fuzzGetI64(p);
+ printf("%s%lld", zPre, iVal);
+ p += 8;
+ break;
+ }
+
+ case 0x02: { /* real */
+ sqlite3_int64 iVal = 0;
+ double fVal = 0.0;
+ iVal = fuzzGetI64(p);
+ memcpy(&fVal, &iVal, 8);
+ printf("%s%f", zPre, fVal);
+ p += 8;
+ break;
+ }
+
+ case 0x03: /* text */
+ case 0x04: { /* blob */
+ int nTxt;
+ p += fuzzGetVarint(p, &nTxt);
+ printf("%s%s", zPre, eType==0x03 ? "'" : "X'");
+ for(i=0; i<nTxt; i++){
+ if( eType==0x03 ){
+ printf("%c", p[i]);
+ }else{
+ char aHex[16] = {'0', '1', '2', '3', '4', '5', '6', '7',
+ '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+ };
+ printf("%c", aHex[ p[i]>>4 ]);
+ printf("%c", aHex[ p[i] & 0x0F ]);
+ }
+ }
+ printf("'");
+ p += nTxt;
+ break;
+ }
+
+ case 0x05: /* null */
+ printf("%sNULL", zPre);
+ break;
+ }
+ zPre = ", ";
+ }
+ }
+ printf(")");
+
+ *ppRec = p;
+ return rc;
+}
+
+/*
+** Print a human-readable version of the table-header and all changes in the
+** change-group passed as the second argument.
+*/
+static void fuzzPrintGroup(FuzzChangeset *pParse, FuzzChangesetGroup *pGrp){
+ int i;
+ u8 *p;
+
+ /* The table header */
+ printf("TABLE: %s nCol=%d aPK=", pGrp->zTab, pGrp->nCol);
+ for(i=0; i<pGrp->nCol; i++){
+ printf("%d", (int)pGrp->aPK[i]);
+ }
+ printf("\n");
+
+ /* The array of changes */
+ p = pGrp->aChange;
+ for(i=0; i<pGrp->nChange; i++){
+ u8 eType = p[0];
+ u8 bIndirect = p[1];
+ printf("%s (ind=%d):",
+ (eType==SQLITE_INSERT) ? "INSERT" :
+ (eType==SQLITE_DELETE ? "DELETE" : "UPDATE"),
+ bIndirect
+ );
+ p += 2;
+
+ if( pParse->bPatchset==0 && eType==SQLITE_UPDATE ){
+ fuzzPrintRecord(pGrp, &p, 0);
+ }
+ fuzzPrintRecord(pGrp, &p, eType==SQLITE_DELETE && pParse->bPatchset);
+ printf("\n");
+ }
+}
+
+/*
+** Initialize the object passed as the second parameter with details
+** of the change that will be attempted (type of change, to which part of the
+** changeset it applies etc.). If successful, return SQLITE_OK. Or, if an
+** error occurs, return an SQLite error code.
+**
+** If a negative value is returned, then the selected change would have
+** produced a non-well-formed changeset. In this case the caller should
+** call this function again.
+*/
+static int fuzzSelectChange(FuzzChangeset *pParse, FuzzChange *pChange){
+ int iSub;
+
+ memset(pChange, 0, sizeof(FuzzChange));
+ pChange->eType = fuzzRandomInt(FUZZ_COLUMN_DEL) + 1;
+
+ assert( pChange->eType==FUZZ_VALUE_SUB
+ || pChange->eType==FUZZ_VALUE_MOD
+ || pChange->eType==FUZZ_VALUE_RND
+ || pChange->eType==FUZZ_CHANGE_DUP
+ || pChange->eType==FUZZ_CHANGE_DEL
+ || pChange->eType==FUZZ_CHANGE_TYPE
+ || pChange->eType==FUZZ_CHANGE_FIELD
+ || pChange->eType==FUZZ_CHANGE_INDIRECT
+ || pChange->eType==FUZZ_GROUP_DUP
+ || pChange->eType==FUZZ_GROUP_DEL
+ || pChange->eType==FUZZ_GROUP_SWAP
+ || pChange->eType==FUZZ_COLUMN_ADD
+ || pChange->eType==FUZZ_COLUMN_ADDPK
+ || pChange->eType==FUZZ_COLUMN_DEL
+ );
+
+ pChange->iGroup = fuzzRandomInt(pParse->nGroup);
+ pChange->iChange = fuzzRandomInt(pParse->nChange);
+ if( pChange->eType==FUZZ_CHANGE_FIELD ){
+ if( pParse->nUpdate==0 ) return -1;
+ pChange->iChange = fuzzRandomInt(pParse->nUpdate);
+ }
+
+ pChange->iDelete = -1;
+ if( pChange->eType==FUZZ_COLUMN_DEL ){
+ FuzzChangesetGroup *pGrp = pParse->apGroup[pChange->iGroup];
+ int i;
+ pChange->iDelete = fuzzRandomInt(pGrp->nCol);
+ for(i=pGrp->nCol-1; i>=0; i--){
+ if( pGrp->aPK[i] && pChange->iDelete!=i ) break;
+ }
+ if( i<0 ) return -1;
+ }
+
+ if( pChange->eType==FUZZ_GROUP_SWAP ){
+ FuzzChangesetGroup *pGrp;
+ int iGrp = pChange->iGroup;
+ if( pParse->nGroup==1 ) return -1;
+ while( iGrp==pChange->iGroup ){
+ iGrp = fuzzRandomInt(pParse->nGroup);
+ }
+ pGrp = pParse->apGroup[pChange->iGroup];
+ pParse->apGroup[pChange->iGroup] = pParse->apGroup[iGrp];
+ pParse->apGroup[iGrp] = pGrp;
+ }
+
+ if( pChange->eType==FUZZ_VALUE_SUB
+ || pChange->eType==FUZZ_VALUE_MOD
+ || pChange->eType==FUZZ_VALUE_RND
+ ){
+ iSub = fuzzRandomInt(pParse->nVal);
+ pChange->pSub1 = pParse->apVal[iSub];
+ if( pChange->eType==FUZZ_VALUE_SUB ){
+ iSub = fuzzRandomInt(pParse->nVal);
+ pChange->pSub2 = pParse->apVal[iSub];
+ }else{
+ pChange->pSub2 = pChange->aSub;
+ }
+
+ if( pChange->eType==FUZZ_VALUE_RND ){
+ pChange->aSub[0] = (u8)(fuzzRandomInt(5) + 1);
+ switch( pChange->aSub[0] ){
+ case 0x01: { /* integer */
+ u64 iVal = fuzzRandomU64();
+ fuzzPutU64(&pChange->aSub[1], iVal);
+ break;
+ }
+
+ case 0x02: { /* real */
+ u64 iVal1 = fuzzRandomU64();
+ u64 iVal2 = fuzzRandomU64();
+ double d = (double)iVal1 / (double)iVal2;
+ memcpy(&iVal1, &d, sizeof(iVal1));
+ fuzzPutU64(&pChange->aSub[1], iVal1);
+ break;
+ }
+
+ case 0x03: /* text */
+ case 0x04: { /* blob */
+ int nByte = fuzzRandomInt(48);
+ pChange->aSub[1] = (u8)nByte;
+ fuzzRandomBlob(nByte, &pChange->aSub[2]);
+ if( pChange->aSub[0]==0x03 ){
+ int i;
+ for(i=0; i<nByte; i++){
+ pChange->aSub[2+i] &= 0x7F;
+ }
+ }
+ break;
+ }
+ }
+ }
+ if( pChange->eType==FUZZ_VALUE_MOD ){
+ int sz;
+ int iMod = -1;
+ fuzzChangeSize(pChange->pSub1, &sz);
+ memcpy(pChange->aSub, pChange->pSub1, sz);
+ switch( pChange->aSub[0] ){
+ case 0x01:
+ case 0x02:
+ iMod = fuzzRandomInt(8) + 1;
+ break;
+
+ case 0x03: /* text */
+ case 0x04: { /* blob */
+ int nByte;
+ int iFirst = 1 + fuzzGetVarint(&pChange->aSub[1], &nByte);
+ if( nByte>0 ){
+ iMod = fuzzRandomInt(nByte) + iFirst;
+ }
+ break;
+ }
+ }
+
+ if( iMod>=0 ){
+ u8 mask = (1 << fuzzRandomInt(8 - (pChange->aSub[0]==0x03)));
+ pChange->aSub[iMod] ^= mask;
+ }
+ }
+ }
+
+ return SQLITE_OK;
+}
+
+/*
+** Copy a single change from the input to the output changeset, making
+** any modifications specified by (*pFuzz).
+*/
+static int fuzzCopyChange(
+ FuzzChangeset *pParse,
+ int iGrp,
+ FuzzChange *pFuzz,
+ u8 **pp, u8 **ppOut /* IN/OUT: Input and output pointers */
+){
+ int bPS = pParse->bPatchset;
+ FuzzChangesetGroup *pGrp = pParse->apGroup[iGrp];
+ u8 *p = *pp;
+ u8 *pOut = *ppOut;
+ u8 eType = p++[0];
+ int iRec;
+ int nRec = ((eType==SQLITE_UPDATE && !bPS) ? 2 : 1);
+ int iUndef = -1;
+ int nUpdate = 0;
+
+ u8 eNew = eType;
+ if( pFuzz->iCurrent==pFuzz->iChange && pFuzz->eType==FUZZ_CHANGE_TYPE ){
+ switch( eType ){
+ case SQLITE_INSERT:
+ eNew = SQLITE_DELETE;
+ break;
+ case SQLITE_DELETE:
+ eNew = SQLITE_UPDATE;
+ break;
+ case SQLITE_UPDATE:
+ eNew = SQLITE_INSERT;
+ break;
+ }
+ }
+
+ if( pFuzz->iCurrent==pFuzz->iChange
+ && pFuzz->eType==FUZZ_CHANGE_FIELD && eType==SQLITE_UPDATE
+ ){
+ int sz;
+ int i;
+ int nDef = 0;
+ u8 *pCsr = p+1;
+ for(i=0; i<pGrp->nCol; i++){
+ if( pCsr[0] && pGrp->aPK[i]==0 ) nDef++;
+ fuzzChangeSize(pCsr, &sz);
+ pCsr += sz;
+ }
+ if( nDef<=1 ) return -1;
+ nDef = fuzzRandomInt(nDef);
+ pCsr = p+1;
+ for(i=0; i<pGrp->nCol; i++){
+ if( pCsr[0] && pGrp->aPK[i]==0 ){
+ if( nDef==0 ) iUndef = i;
+ nDef--;
+ }
+ fuzzChangeSize(pCsr, &sz);
+ pCsr += sz;
+ }
+ }
+
+ /* Copy the change type and indirect flag. If the fuzz mode is
+ ** FUZZ_CHANGE_INDIRECT, and the current change is the one selected for
+ ** fuzzing, invert the indirect flag. */
+ *(pOut++) = eNew;
+ if( pFuzz->eType==FUZZ_CHANGE_INDIRECT && pFuzz->iCurrent==pFuzz->iChange ){
+ *(pOut++) = !(*(p++));
+ }else{
+ *(pOut++) = *(p++);
+ }
+
+ for(iRec=0; iRec<nRec; iRec++){
+ int i;
+
+ /* Copy the next record from the output to the input.
+ */
+ for(i=0; i<pGrp->nCol; i++){
+ int sz;
+ u8 *pCopy = p;
+
+ /* If this is a patchset, and the input is a DELETE, then the only
+ ** fields present are the PK fields. So, if this is not a PK, skip to
+ ** the next column. If the current fuzz is FUZZ_CHANGE_TYPE, then
+ ** write a randomly selected value to the output. */
+ if( bPS && eType==SQLITE_DELETE && pGrp->aPK[i]==0 ){
+ if( eType!=eNew ){
+ assert( eNew==SQLITE_UPDATE );
+ do {
+ pCopy = pParse->apVal[fuzzRandomInt(pParse->nVal)];
+ }while( pCopy[0]==0x00 );
+ fuzzChangeSize(pCopy, &sz);
+ memcpy(pOut, pCopy, sz);
+ pOut += sz;
+ }
+ continue;
+ }
+
+ if( p==pFuzz->pSub1 ){
+ pCopy = pFuzz->pSub2;
+ }else if( p==pFuzz->pSub2 ){
+ pCopy = pFuzz->pSub1;
+ }else if( i==iUndef ){
+ pCopy = (u8*)"\0";
+ }
+
+ if( pCopy[0]==0x00 && eNew!=eType && eType==SQLITE_UPDATE && iRec==0 ){
+ while( pCopy[0]==0x00 ){
+ pCopy = pParse->apVal[fuzzRandomInt(pParse->nVal)];
+ }
+ }else if( p[0]==0x00 && pCopy[0]!=0x00 ){
+ return -1;
+ }else{
+ if( pGrp->aPK[i]>0 && pCopy[0]==0x05 ) return -1;
+ }
+
+ if( (pFuzz->iGroup!=iGrp || i!=pFuzz->iDelete)
+ && (eNew==eType || eType!=SQLITE_UPDATE || iRec==0)
+ && (eNew==eType || eNew!=SQLITE_DELETE || !bPS || pGrp->aPK[i])
+ ){
+ fuzzChangeSize(pCopy, &sz);
+ memcpy(pOut, pCopy, sz);
+ pOut += sz;
+ nUpdate += (pGrp->aPK[i]==0 && pCopy[0]!=0x00);
+ }
+
+ fuzzChangeSize(p, &sz);
+ p += sz;
+ }
+
+ if( iGrp==pFuzz->iGroup ){
+ if( pFuzz->eType==FUZZ_COLUMN_ADD ){
+ if( !bPS || eType!=SQLITE_DELETE ) *(pOut++) = 0x05;
+ }else if( pFuzz->eType==FUZZ_COLUMN_ADDPK ){
+ if( iRec==1 ){
+ *(pOut++) = 0x00;
+ }else{
+ u8 *pNew;
+ int szNew;
+ do {
+ pNew = pParse->apVal[fuzzRandomInt(pParse->nVal)];
+ }while( pNew[0]==0x00 || pNew[0]==0x05 );
+ fuzzChangeSize(pNew, &szNew);
+ memcpy(pOut, pNew, szNew);
+ pOut += szNew;
+ }
+ }
+ }
+ }
+
+ if( pFuzz->iCurrent==pFuzz->iChange ){
+ if( pFuzz->eType==FUZZ_CHANGE_DUP ){
+ int nByte = pOut - (*ppOut);
+ memcpy(pOut, *ppOut, nByte);
+ pOut += nByte;
+ }
+
+ if( pFuzz->eType==FUZZ_CHANGE_DEL ){
+ pOut = *ppOut;
+ }
+ if( eNew!=eType && eNew==SQLITE_UPDATE && !bPS ){
+ int i;
+ u8 *pCsr = (*ppOut) + 2;
+ for(i=0; i<pGrp->nCol; i++){
+ int sz;
+ u8 *pCopy = pCsr;
+ if( pGrp->aPK[i] ) pCopy = (u8*)"\0";
+ fuzzChangeSize(pCopy, &sz);
+ memcpy(pOut, pCopy, sz);
+ pOut += sz;
+ fuzzChangeSize(pCsr, &sz);
+ pCsr += sz;
+ }
+ }
+ }
+
+ /* If a column is being deleted from this group, and this change was an
+ ** UPDATE, and there are now no non-PK, non-undefined columns in the
+ ** change, remove it altogether. */
+ if( pFuzz->eType==FUZZ_COLUMN_DEL && pFuzz->iGroup==iGrp
+ && eType==SQLITE_UPDATE && nUpdate==0
+ ){
+ pOut = *ppOut;
+ }
+
+ *pp = p;
+ *ppOut = pOut;
+ pFuzz->iCurrent += (eType==SQLITE_UPDATE || pFuzz->eType!=FUZZ_CHANGE_FIELD);
+ return SQLITE_OK;
+}
+
+/*
+** Fuzz the changeset parsed into object pParse and write the results
+** to file zOut on disk. Argument pBuf points to a buffer that is guaranteed
+** to be large enough to hold the fuzzed changeset.
+**
+** Return SQLITE_OK if successful, or an SQLite error code if an error occurs.
+*/
+static int fuzzDoOneFuzz(
+ const char *zOut, /* Filename to write modified changeset to */
+ u8 *pBuf, /* Buffer to use for modified changeset */
+ FuzzChangeset *pParse /* Parse of input changeset */
+){
+ FuzzChange change;
+ int iGrp;
+ int rc = -1;
+
+ while( rc<0 ){
+ u8 *pOut = pBuf;
+ rc = fuzzSelectChange(pParse, &change);
+ for(iGrp=0; rc==SQLITE_OK && iGrp<pParse->nGroup; iGrp++){
+ FuzzChangesetGroup *pGrp = pParse->apGroup[iGrp];
+ int nTab = strlen(pGrp->zTab) + 1;
+ int j;
+ int nRep = 1;
+
+ /* If this is the group to delete for a FUZZ_GROUP_DEL change, jump to
+ ** the next group. Unless this is the only group in the changeset - in
+ ** that case this change cannot be applied.
+ **
+ ** Or, if this is a FUZZ_GROUP_DUP, set nRep to 2 to output two
+ ** copies of the group. */
+ if( change.iGroup==iGrp ){
+ if( change.eType==FUZZ_GROUP_DEL ){
+ if( pParse->nGroup==1 ) rc = -1;
+ continue;
+ }
+ else if( change.eType==FUZZ_GROUP_DUP ){
+ nRep = 2;
+ }
+ }
+
+ for(j=0; j<nRep; j++){
+ int i;
+ u8 *pSaved;
+ u8 *p = pGrp->aChange;
+ int nCol = pGrp->nCol;
+ int iPKDel = 0;
+ if( iGrp==change.iGroup ){
+ if( change.eType==FUZZ_COLUMN_ADD
+ || change.eType==FUZZ_COLUMN_ADDPK
+ ){
+ nCol++;
+ }else if( change.eType==FUZZ_COLUMN_DEL ){
+ nCol--;
+ iPKDel = pGrp->aPK[change.iDelete];
+ }
+ }
+
+ /* Output a table header */
+ pOut++[0] = pParse->bPatchset ? 'P' : 'T';
+ pOut += fuzzPutVarint(pOut, nCol);
+
+ for(i=0; i<pGrp->nCol; i++){
+ if( iGrp!=change.iGroup || i!=change.iDelete ){
+ u8 v = pGrp->aPK[i];
+ if( iPKDel && v>iPKDel ) v--;
+ *(pOut++) = v;
+ }
+ }
+ if( nCol>pGrp->nCol ){
+ if( change.eType==FUZZ_COLUMN_ADD ){
+ *(pOut++) = 0x00;
+ }else{
+ u8 max = 0;
+ for(i=0; i<pGrp->nCol; i++){
+ if( pGrp->aPK[i]>max ) max = pGrp->aPK[i];
+ }
+ *(pOut++) = max+1;
+ }
+ }
+ memcpy(pOut, pGrp->zTab, nTab);
+ pOut += nTab;
+
+ /* Output the change array. */
+ pSaved = pOut;
+ for(i=0; rc==SQLITE_OK && i<pGrp->nChange; i++){
+ rc = fuzzCopyChange(pParse, iGrp, &change, &p, &pOut);
+ }
+ if( pOut==pSaved ) rc = -1;
+ }
+ }
+ if( rc==SQLITE_OK ){
+ fuzzWriteFile(zOut, pBuf, pOut-pBuf);
+ }
+ }
+
+ return rc;
+}
+
+int main(int argc, char **argv){
+ int nRepeat = 0; /* Number of output files */
+ int iSeed = 0; /* Value of PRNG seed */
+ const char *zInput; /* Name of input file */
+ void *pChangeset = 0; /* Input changeset */
+ int nChangeset = 0; /* Size of input changeset in bytes */
+ int i; /* Current output file */
+ FuzzChangeset changeset; /* Partially parsed changeset */
+ int rc;
+ u8 *pBuf = 0;
+
+ if( argc!=4 && argc!=2 ) usage(argv[0]);
+ zInput = argv[1];
+
+ fuzzReadFile(zInput, &nChangeset, &pChangeset);
+ rc = fuzzParseChangeset(pChangeset, nChangeset, &changeset);
+
+ if( rc==SQLITE_OK ){
+ if( argc==2 ){
+ for(i=0; i<changeset.nGroup; i++){
+ fuzzPrintGroup(&changeset, changeset.apGroup[i]);
+ }
+ }else{
+ pBuf = (u8*)fuzzMalloc((sqlite3_int64)nChangeset*2 + 1024);
+ if( pBuf==0 ){
+ rc = SQLITE_NOMEM;
+ }else{
+ iSeed = atoi(argv[2]);
+ nRepeat = atoi(argv[3]);
+ fuzzRandomSeed((unsigned int)iSeed);
+ for(i=0; rc==SQLITE_OK && i<nRepeat; i++){
+ char *zOut = sqlite3_mprintf("%s-%d", zInput, i);
+ rc = fuzzDoOneFuzz(zOut, pBuf, &changeset);
+ sqlite3_free(zOut);
+ }
+ fuzzFree(pBuf);
+ }
+ }
+ }
+
+ if( rc!=SQLITE_OK ){
+ fprintf(stderr, "error while processing changeset: %d\n", rc);
+ }
+
+ return rc;
+}
diff --git a/ext/session/changesetfuzz1.test b/ext/session/changesetfuzz1.test
new file mode 100644
index 0000000..20f5ac6
--- /dev/null
+++ b/ext/session/changesetfuzz1.test
@@ -0,0 +1,84 @@
+# 2018 November 08
+#
+# 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 changesetfuzz1
+
+
+set CF [test_find_binary changesetfuzz]
+if {$CF==""} {
+ finish_test
+ return
+}
+
+proc writefile {zFile data} {
+ set fd [open $zFile w]
+ fconfigure $fd -translation binary -encoding binary
+ puts -nonewline $fd $data
+ close $fd
+}
+
+do_execsql_test 1.0 {
+ CREATE TABLE t1(a, b, c, d, PRIMARY KEY(c, d));
+ CREATE TABLE t2(a INTEGER PRIMARY KEY, b, c);
+
+ INSERT INTO t1 VALUES ('one', 'two', 'three', 'four'),
+ ('five', 'six', 'seven', 'eight'),
+ ('nine', 'ten', 'eleven', 'twelve');
+ INSERT INTO t2 VALUES (1, 2, 3), (4, 5, 6), (7, 8, 9);
+}
+
+set C [changeset_from_sql {
+ INSERT INTO t2 VALUES(10, 11, 12);
+ DELETE FROM t2 WHERE a=1;
+ UPDATE t1 SET b='forty-five' WHERE a='one';
+ UPDATE t1 SET a='twenty-nine', b='seventy' WHERE a='five';
+}]
+writefile c1.changeset $C
+
+do_test 1.1 {
+ for {set j 0} {$j < 200} {incr j} {
+ forcecopy c1.changeset input.changeset
+ for {set i 0} {$i < 6} {incr i} {
+ exec $CF input.changeset $i 1
+ exec $CF input.changeset-0
+ forcecopy input.changeset-0 input.changeset
+ }
+ }
+} {}
+
+set P [patchset_from_sql {
+ INSERT INTO t2 VALUES(13, 14, 15);
+ DELETE FROM t2 WHERE a=4;
+ UPDATE t1 SET b='thirteen' WHERE a='one';
+ UPDATE t1 SET a='ninety-seven', b='twenty' WHERE a='five';
+}]
+writefile p1.patchset $P
+do_test 1.2 {
+ for {set j 0} {$j < 200} {incr j} {
+ forcecopy p1.patchset input.patchset
+ for {set i 0} {$i < 6} {incr i} {
+ exec $CF input.patchset $i 1
+ exec $CF input.patchset-0
+ forcecopy input.patchset-0 input.patchset
+ }
+ }
+} {}
+
+
+finish_test
+
diff --git a/ext/session/session1.test b/ext/session/session1.test
new file mode 100644
index 0000000..bcd7b03
--- /dev/null
+++ b/ext/session/session1.test
@@ -0,0 +1,689 @@
+# 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
+
+# Run all tests in this file twice. Once with "WITHOUT ROWID", and once
+# with regular rowid tables.
+#
+foreach {tn trailing} {
+ 1 ""
+ 2 " WITHOUT ROWID "
+} {
+eval [string map [list %WR% $trailing] {
+
+db close
+forcedelete test.db test.db2
+reset_db
+
+do_execsql_test $tn.1.0 {
+ CREATE TABLE t1(x PRIMARY KEY, y) %WR%;
+ INSERT INTO t1 VALUES('abc', 'def');
+}
+
+#-------------------------------------------------------------------------
+# Test creating, attaching tables to and deleting session objects.
+#
+do_test $tn.1.1 { sqlite3session S db main } {S}
+do_test $tn.1.2 { S delete } {}
+do_test $tn.1.3 { sqlite3session S db main } {S}
+do_test $tn.1.4 { S attach t1 } {}
+do_test $tn.1.5 { S delete } {}
+do_test $tn.1.6 { sqlite3session S db main } {S}
+do_test $tn.1.7 { S attach t1 ; S attach t2 ; S attach t3 } {}
+do_test $tn.1.8 { S attach t1 ; S attach t2 ; S attach t3 } {}
+do_test $tn.1.9 { S delete } {}
+do_test $tn.1.10 {
+ sqlite3session S db main
+ S attach t1
+ execsql { INSERT INTO t1 VALUES('ghi', 'jkl') }
+} {}
+do_test $tn.1.11 { S delete } {}
+if {$tn==1} {
+ do_test $tn.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 $tn.1.13 {
+ S changeset
+ S delete
+ } {}
+}
+
+#-------------------------------------------------------------------------
+# Simple changeset tests. Also test the sqlite3changeset_invert()
+# function.
+#
+do_test $tn.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 $tn.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 $tn.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 $tn.2.1.4 { S delete } {}
+
+do_test $tn.2.2.1 {
+ sqlite3session S db main
+ S attach t1
+ execsql { DELETE FROM t1 WHERE 1 }
+} {}
+do_changeset_test $tn.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 $tn.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 $tn.2.2.4 { S delete } {}
+
+do_test $tn.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 $tn.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 $tn.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 $tn.2.3.4 { S delete } {}
+
+do_test $tn.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 $tn.2.4.2 S {}
+do_changeset_invert_test $tn.2.4.3 S {}
+do_test $tn.2.4.4 { S delete } {}
+
+do_execsql_test $tn.2.5.0 {
+ SELECT * FROM t1 ORDER BY x
+} {
+ 2 Surin
+ 10 Sukhothai
+ 20 Thapae
+}
+
+do_test $tn.2.5.1 {
+ sqlite3session S db main
+ S attach t1
+ execsql { DELETE FROM t1 }
+} {}
+do_changeset_test $tn.2.5.2 S {
+ {DELETE t1 0 X. {i 10 t Sukhothai} {}}
+ {DELETE t1 0 X. {i 2 t Surin} {}}
+ {DELETE t1 0 X. {i 20 t Thapae} {}}
+}
+do_test $tn.2.5.3 { 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 $tn.3.1.0 {
+ execsql { CREATE TABLE t1(a PRIMARY KEY, b NOT NULL) %WR% } db2
+ execsql {
+ CREATE TABLE t1(a PRIMARY KEY, b) %WR%;
+ INSERT INTO t1 VALUES(1, 'one');
+ INSERT INTO t1 VALUES(2, 'two');
+ } db
+} {}
+do_db2_test $tn.3.1.1 "INSERT INTO t1 VALUES(6, 'VI')"
+do_conflict_test $tn.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 CONFLICT {i 6 t six} {i 6 t VI}}
+ {INSERT t1 CONSTRAINT {i 8 n {}}}
+}
+
+do_db2_test $tn.3.1.3 "SELECT * FROM t1 ORDER BY a" {
+ 3 three 4 four 5 five 6 VI 7 seven
+}
+do_execsql_test $tn.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 $tn.3.2.1 {
+ PRAGMA foreign_keys = on;
+ CREATE TABLE t2(a PRIMARY KEY, b)%WR%;
+ 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 $tn.3.2.2 {
+ PRAGMA foreign_keys = on;
+ CREATE TABLE t2(a PRIMARY KEY, b)%WR%;
+ 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 $tn.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 $tn.3.2.4 "SELECT * FROM t2" {}
+do_db2_test $tn.3.2.5 "SELECT * FROM t2" {4 five}
+
+# Test UPDATE changesets.
+#
+do_execsql_test $tn.3.3.1 {
+ CREATE TABLE t4(a, b, c, PRIMARY KEY(b, c))%WR%;
+ 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 $tn.3.3.2 {
+ CREATE TABLE t4(a NOT NULL, b, c, PRIMARY KEY(b, c))%WR%;
+ 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 $tn.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 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 {} {} {} {}}}
+ {UPDATE t4 CONSTRAINT {i 7 i 8 i 9} {n {} {} {} {} {}}}
+}
+do_db2_test $tn.3.3.4 { SELECT * FROM t4 } {0 2 3 4 5 7 7 8 9 x 11 12}
+do_execsql_test $tn.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 {tn2 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 $tn.4.$tn2.1 {
+ foreach db {db db2} {
+ execsql {
+ CREATE TABLE t1(a, b, c, PRIMARY KEY(a))%WR%;
+ 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 $tn.4.$tn2.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 $tn.4.$tn2.3 "SELECT * FROM t1 ORDER BY a" $after
+}
+
+foreach {tn2 conflict_return} {
+ 1 OMIT
+ 2 REPLACE
+} {
+ test_reset
+
+ do_test $tn.5.$tn2.1 {
+ # Create an identical schema in both databases.
+ set schema {
+ CREATE TABLE "'foolish name'"(x, y, z, PRIMARY KEY(x, y))%WR%;
+ }
+ 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 $tn.5.$tn2.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 $tn.5.$tn2.3 {
+ SELECT * FROM "'foolish name'" ORDER BY x, y
+ } $res($conflict_return)
+
+
+ do_test $tn.5.$tn2.1 {
+ set schema {
+ CREATE TABLE d1("z""z" PRIMARY KEY, y)%WR%;
+ 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 $tn.5.$tn2.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 $tn.5.$tn2.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)%WR%;
+ CREATE TABLE t2(a, b PRIMARY KEY)%WR%;
+}
+do_test $tn.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 $tn.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 $tn.6.2 "SELECT * FROM t1 ORDER BY a" {1 2 a b}
+do_db2_test $tn.6.3 "SELECT * FROM t2 ORDER BY a" {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 $tn.7.1 * {
+ ATTACH 'test.db2' AS aux;
+ CREATE TABLE main.t1(x PRIMARY KEY, y)%WR%;
+ CREATE TABLE aux.t1(x PRIMARY KEY, y)%WR%;
+
+ 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 $tn.8.1 {
+ execsql {
+ CREATE TABLE t5(x PRIMARY KEY, y)%WR%;
+ CREATE TABLE t6(x PRIMARY KEY, y)%WR%;
+ INSERT INTO t5 VALUES('a', 'b');
+ INSERT INTO t6 VALUES('a', 'b');
+ }
+ sqlite3session S db main
+ S attach *
+
+ S isempty
+} {1}
+do_test $tn.8.2 {
+ execsql { DELETE FROM t5 }
+ S isempty
+} {0}
+do_test $tn.8.3 {
+ S delete
+ sqlite3session S db main
+ S attach t5
+ execsql { DELETE FROM t5 }
+ S isempty
+} {1}
+do_test $tn.8.4 { S delete } {}
+
+do_test $tn.8.5 {
+ sqlite3session S db main
+ S attach t5
+ S attach t6
+ execsql { INSERT INTO t5 VALUES(1, 2) }
+ S isempty
+} {0}
+
+do_test $tn.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 $tn.8.7 { S delete } {}
+
+#-------------------------------------------------------------------------
+#
+do_execsql_test $tn.9.1 {
+ CREATE TABLE t7(a, b, c, d, e PRIMARY KEY, f, g)%WR%;
+ INSERT INTO t7 VALUES(1, 1, 1, 1, 1, 1, 1);
+}
+do_test $tn.9.2 {
+ sqlite3session S db main
+ S attach *
+ execsql { UPDATE t7 SET b=2, d=2 }
+} {}
+do_changeset_test $tn.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 $tn.10.1.1 {
+ execsql "
+ CREATE TABLE $tblname (a PRIMARY KEY, b)%WR%;
+ 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';
+ "
+} {}
+do_changeset_test $tn.10.1.2 S "
+ {INSERT $tblname 0 X. {} {t uvw t abc}}
+ {DELETE $tblname 0 X. {t xyz t def} {}}
+"
+do_test $tn.10.1.4 { S delete } {}
+
+#---------------------------------------------------------------
+reset_db
+do_execsql_test $tn.11.1 {
+ CREATE TABLE t1(a, b);
+}
+do_test $tn.11.2 {
+ sqlite3session S db main
+ S attach t1
+ execsql {
+ INSERT INTO t1 VALUES(1, 2);
+ }
+ S changeset
+} {}
+
+S delete
+
+
+#-------------------------------------------------------------------------
+# Test a really long table name.
+#
+reset_db
+set tblname [string repeat tblname123 100]
+do_test $tn.10.1.1 {
+ execsql "
+ CREATE TABLE $tblname (a PRIMARY KEY, b)%WR%;
+ 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';
+ "
+} {}
+do_changeset_test $tn.10.1.2 S "
+ {INSERT $tblname 0 X. {} {t uvw t abc}}
+ {DELETE $tblname 0 X. {t xyz t def} {}}
+"
+do_test $tn.10.1.4 { S delete } {}
+
+#-------------------------------------------------------------------------
+# Test the effect of updating a column from 0.0 to 0.0.
+#
+reset_db
+do_execsql_test $tn.11.1 {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b REAL)%WR%;
+ INSERT INTO t1 VALUES(1, 0.0);
+}
+do_iterator_test $tn.11.2 * {
+ UPDATE t1 SET b = 0.0;
+} {
+}
+
+reset_db
+do_execsql_test $tn.12.1 {
+ CREATE TABLE t1(r INTEGER PRIMARY KEY, a, b)%WR%;
+ CREATE INDEX i1 ON t1(a);
+ INSERT INTO t1 VALUES(1, 1, 1);
+ INSERT INTO t1 VALUES(2, 1, 2);
+ INSERT INTO t1 VALUES(3, 1, 3);
+}
+
+do_iterator_test $tn.12.2 * {
+ UPDATE t1 SET b='one' WHERE a=1;
+} {
+ {UPDATE t1 0 X.. {i 1 {} {} i 1} {{} {} {} {} t one}}
+ {UPDATE t1 0 X.. {i 2 {} {} i 2} {{} {} {} {} t one}}
+ {UPDATE t1 0 X.. {i 3 {} {} i 3} {{} {} {} {} t one}}
+}
+
+#-------------------------------------------------------------------------
+# Test that no savepoint is used if -nosavepoint is specified.
+#
+do_execsql_test $tn.13.1 {
+ CREATE TABLE x1(a INTEGER PRIMARY KEY, b)%WR%;
+}
+do_test $tn.13.2 {
+ execsql BEGIN
+ set C [changeset_from_sql {
+ INSERT INTO x1 VALUES(1, 'one');
+ INSERT INTO x1 VALUES(2, 'two');
+ INSERT INTO x1 VALUES(3, 'three');
+ }]
+ execsql ROLLBACK
+ execsql {
+ INSERT INTO x1 VALUES(1, 'i');
+ INSERT INTO x1 VALUES(2, 'ii');
+ INSERT INTO x1 VALUES(3, 'iii');
+ }
+} {}
+
+proc xConflict {args} {
+ set ret [lindex $::CONFLICT_HANDLERS 0]
+ set ::CONFLICT_HANDLERS [lrange $::CONFLICT_HANDLERS 1 end]
+ set ret
+}
+do_test $tn.13.3 {
+ set CONFLICT_HANDLERS [list REPLACE REPLACE ABORT]
+ execsql BEGIN
+ catch { sqlite3changeset_apply_v2 db $C xConflict } msg
+ execsql {
+ SELECT * FROM x1
+ }
+} {1 i 2 ii 3 iii}
+do_test $tn.13.3 {
+ set CONFLICT_HANDLERS [list REPLACE REPLACE ABORT]
+ execsql ROLLBACK
+ execsql BEGIN
+ catch { sqlite3changeset_apply_v2 -nosavepoint db $C xConflict } msg
+ execsql { SELECT * FROM x1 }
+} {1 one 2 two 3 iii}
+execsql ROLLBACK
+
+do_test $tn.14.1 { sqlite3session_config strm_size -1 } 64
+do_test $tn.14.2 { sqlite3session_config strm_size 65536 } 65536
+do_test $tn.14.3 { sqlite3session_config strm_size 64 } 64
+do_test $tn.14.4 {
+ list [catch {sqlite3session_config invalid 123} msg] $msg
+} {1 SQLITE_MISUSE}
+
+}]
+}
+
+
+finish_test
diff --git a/ext/session/session2.test b/ext/session/session2.test
new file mode 100644
index 0000000..8066877
--- /dev/null
+++ b/ext/session/session2.test
@@ -0,0 +1,639 @@
+# 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 INT 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 }
+
+ 19 {
+ INSERT INTO t1 VALUES('', '');
+ INSERT INTO t1 VALUES(X'', X'');
+ }
+ 20 {
+ DELETE FROM t1;
+ INSERT INTO t1 VALUES('', NULL);
+ }
+}
+
+test_reset
+do_common_sql {
+ CREATE TABLE t1(a int 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 int, b PRIMARY KEY);
+ CREATE TABLE t2(x, y, z);
+ CREATE TABLE t3(a);
+
+ CREATE TABLE aux.t1(a int 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 int 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;
+ } {
+ {DELETE t1 0 X {i 35} {}}
+ {UPDATE t2 0 .X {i 36 i 37} {i 34 {} {}}}
+ }
+} {
+ 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 t3 1 .X {} {t two i 1}}
+ {INSERT t4 0 .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
+
+#-------------------------------------------------------------------------
+test_reset
+do_common_sql {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c, d, e, f);
+ WITH s(i) AS (
+ SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<32
+ )
+ INSERT INTO t1 SELECT NULL, 0, 0, 0, 0, 0 FROM s
+}
+
+do_then_apply_sql {
+ UPDATE t1 SET f=f+1 WHERE a=1;
+ UPDATE t1 SET e=e+1 WHERE a=2;
+ UPDATE t1 SET e=e+1, f=f+1 WHERE a=3;
+ UPDATE t1 SET d=d+1 WHERE a=4;
+ UPDATE t1 SET d=d+1, f=f+1 WHERE a=5;
+ UPDATE t1 SET d=d+1, e=e+1 WHERE a=6;
+ UPDATE t1 SET d=d+1, e=e+1, f=f+1 WHERE a=7;
+ UPDATE t1 SET c=c+1 WHERE a=8;
+ UPDATE t1 SET c=c+1, f=f+1 WHERE a=9;
+ UPDATE t1 SET c=c+1, e=e+1 WHERE a=10;
+ UPDATE t1 SET c=c+1, e=e+1, f=f+1 WHERE a=11;
+ UPDATE t1 SET c=c+1, d=d+1 WHERE a=12;
+ UPDATE t1 SET c=c+1, d=d+1, f=f+1 WHERE a=13;
+ UPDATE t1 SET c=c+1, d=d+1, e=e+1 WHERE a=14;
+ UPDATE t1 SET c=c+1, d=d+1, e=e+1, f=f+1 WHERE a=15;
+ UPDATE t1 SET d=d+1 WHERE a=16;
+ UPDATE t1 SET d=d+1, f=f+1 WHERE a=17;
+ UPDATE t1 SET d=d+1, e=e+1 WHERE a=18;
+ UPDATE t1 SET d=d+1, e=e+1, f=f+1 WHERE a=19;
+ UPDATE t1 SET d=d+1, d=d+1 WHERE a=20;
+ UPDATE t1 SET d=d+1, d=d+1, f=f+1 WHERE a=21;
+ UPDATE t1 SET d=d+1, d=d+1, e=e+1 WHERE a=22;
+ UPDATE t1 SET d=d+1, d=d+1, e=e+1, f=f+1 WHERE a=23;
+ UPDATE t1 SET d=d+1, c=c+1 WHERE a=24;
+ UPDATE t1 SET d=d+1, c=c+1, f=f+1 WHERE a=25;
+ UPDATE t1 SET d=d+1, c=c+1, e=e+1 WHERE a=26;
+ UPDATE t1 SET d=d+1, c=c+1, e=e+1, f=f+1 WHERE a=27;
+ UPDATE t1 SET d=d+1, c=c+1, d=d+1 WHERE a=28;
+ UPDATE t1 SET d=d+1, c=c+1, d=d+1, f=f+1 WHERE a=29;
+ UPDATE t1 SET d=d+1, c=c+1, d=d+1, e=e+1 WHERE a=30;
+ UPDATE t1 SET d=d+1, c=c+1, d=d+1, e=e+1, f=f+1 WHERE a=31;
+}
+
+do_test 11.0 {
+ compare_db db db2
+} {}
+
+finish_test
diff --git a/ext/session/session3.test b/ext/session/session3.test
new file mode 100644
index 0000000..ba31634
--- /dev/null
+++ b/ext/session/session3.test
@@ -0,0 +1,214 @@
+# 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
+} {}
+do_test 1.2.2 {
+ db2 eval { SELECT * FROM t1 }
+} {5 6 {} 7 8 {}}
+
+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 0000000..de183a6
--- /dev/null
+++ b/ext/session/session4.test
@@ -0,0 +1,146 @@
+# 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.
+#
+
+package require Tcl 8.6
+
+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');
+ }
+
+ 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}
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 2.0 {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY,b,c,d);
+ CREATE TABLE t2(e TEXT PRIMARY KEY NOT NULL,f,g);
+ CREATE TABLE t3(w REAL PRIMARY KEY NOT NULL,x,y);
+ CREATE TABLE t4(z PRIMARY KEY) WITHOUT ROWID;
+}
+
+foreach {tn blob} {
+ 1 54010174340012000000
+ 2 54fefe8bcb0012000300
+ 3 5480809280808001017434001200fb
+ 4 50af9c939c9c9cb09c9c6400b09c9c6400
+ 5 12000300
+ 6 09847304
+ 7 5401017434001208
+ 8 54010174340012fc0386868600
+ 9 54010174340012FC0386868600
+ 10 548894FEFE
+ 11 54010171340012E703ABFA7433FD1200
+ 12 540101743400120003FFED00010000000000000002120002400C00000000000054040100000074310017000100000000000000050100000000000000030100000000000000040000010000000000000004010000000000000003001700010000000000000007030378797A01000000000000000F000001000000000000000F030378797A005403010000743200090003037838790100000000800000000200000000000000000900030378327902400C0000000000000304666F7572
+ 13 540101743400120003001200010000000000000002120002400C0000000000005404010000007431001700010000000000000005010000000000000003010000000000000004000001000000000000000401000000000000000300170001000000000000000703FC87797A01000000000000000F000001000000000000000F030378797A005403010000743200090003037838790100000000800000000200000000000000000900030378327902400C0000000000000304666F7572
+ 14 540101743400120003001200010000000000000002120002400C00000000000054040100000074310017000100000000000000050100000000000000030100000000000000040000010000000000000004010000000000000003001700010000000000000007030378797A01000000000000000F000001000000000000000F03FC87797A005403010000743200090003037838790100000000800000000200000000000000000900030378327902400C0000000000000304666F7572
+ 15 540101743400120003001200010000000000000002120002400C00000000000054040100000074310017000100000000000000050100000000000000030100000000000000040000010000000000000004010000000000000003001700010000000000000007030378797A01000000000000000F000001000000000000000F030378797A005403010000743200090003FC8738790100000000800000000200000000000000000900030378327902400C0000000000000304666F7572
+ 16 540101743400120003001200010000000000000002120002400C00000000000054040100000074310017000100000000000000050100000000000000030100000000000000040000010000000000000004010000000000000003001700010000000000000007030378797A01000000000000000F000001000000000000000F030378797A00540301000074320009000303783879010000000080000000020000000000000000090003FC87327902400C0000000000000304666F7572
+ 17 540101743400120003FFE3000412F7010000E600000000021202120002400C0000000000005B0401000000743100171C0304646F750002400C000000000000540401000000D3310017000100000000000000050100000000000378797A405403000002F10100000100000000000004090001000100000007030378797A0100000000000D0007000001000000002300000F1B0378797A405403013900743200090003038C3879010000000000000000000002120002400C0000000000005B0401000000743117170003047C5E00FF
+ 18 54010174340012000300120001000000E6FF100000120002401E00000000000054040100000074310017000100040000010000000000000004FFFF7FFF0000000000010000010000001000000007030378797A01000000000000000F000000000000FA0304666F7572
+ 19 540101743400120003001200010000000000000002121B02400C00000000000054040000000074310017000100000000000000050100000000000000030100000000000000040000010000000000000004010000000000000003001700010000000000000007030378817A01000000000000000F000001000000000100000F030378797A005403010000743200090003FFE809000303780000000000000304666F7572
+ 20 5401017D3400120003001200010000000000000002120002400CFC00000000005404010000007431001700010000000000000005010000000000000003010000000000000004000001000000000000000401000000000000000300170001000000000000000703FFFF797A01000000000000000F000001000000000000000F030378797A005403010000743200090003037838790100000000800000000200000000000000000900030378326C02400C0000000000000304666F7572
+ 21 5401017434001200030012000100FFE20000000002120002400C00000000000054040100E0007431001700010000E99D000000020000000003FFE70009000303783279020004000001030000000000002117000003001700012701000100000000743100000100000000008000090003037F387901000000008000000002000000000400000009005303010A00FF7FFFFF00000000000304664F6572
+ 22 540101743400120003FFFF7FFF0000000000000002120002400C00000000000054040100000074310017000100000000000000050100000000000000030100010000000000000000040000010000000000000004010000000000000003001700010000000000000007030378797A01000000000000000F000001000000000000000F030378797A005403010000743200090003037838790100000000800000000200000000000000000900030378327902400C0000000000000304666F7572
+ 23 540101742700120100120003F5FF0300
+ 24 5401017434E312540101743400120003FFFC00
+ 25 540101743400540101743D3D3D3D3D3D3D3D3D3D3D3D3D3400120003FFED000300
+ 26 5401017446EA5301743D1D3D3D01743D1D3D3DCF3D3D3D1A3D3D3D3D3400120003FFFF000000
+ 27 540101743400540101743D3D3D3D3D3D3D3D3D3D251000120003FF81000000000000
+ 28 540101340012000397FF3D7F3D3400120003001200540101743D3D3D3D3D3D393D3D3D12000300
+ 29 500174340050010F74340012000300120003FFE5
+ 30 5004007233E900177FEF0054257F0002EF001200031E12000300
+ 31 5001015001015252525250010174340012EF039A9A0100E351525D52525252525252525252525252525252525250010174340012EF039A0100009A9A9A9A9A9BA3B200120003010040743400
+ 32 5401017400123400120003FFFC00
+ 33 540101743400120003001200010000000000004002120002400C0000000000005404010000007431001700010000000000000005010000000000000003010000000000000004000001000000000000000401000000000000000300170001000000000000000703FC87797A01000000000000000F000001000000000000000F030378797A005403010000743200090003037838790100000000800000000200000000000000000900030378327902400C0000000000000304666F7572
+ 34 54040100000074310017000100000002000015050100000000000000030100000000140000040000010000000000000004010000000000000003001700010000000000000007030378797A01000000000000000F000001000000000000000F030378797A0054030100007432000900030378387901000000008E000000020000000000000000090003FFFF000002400C0000000000000304666F7572
+ 35 540101743400120003001200010000000000000002120002400C00000000000050060100000074310017000100000000000000050100000000000000030100000003001700010000666F7572
+ 36 540101743400120003001200010000000000000002120002400C00000000000050050100000074310017000100000000000000050100000000000000030100000003001700010000666F7572
+ 37 540101743400120003001200010000000000000002120002400C00000000000050040100008074310017000100000000000000050100000000000000030100000003001700010000666F7572
+ 38 540101743400120003001200010000000000000002120002400C00000000000050040100000074310017000000000000000000050100000000000000030100000003001700010000666F7572
+ 39 540101743400120003001200010000000000000002120002400C00000000000050040100018074310017000100000000000000050100000000000000030100000003001700010000666F7572
+ 40 540101743400120003001200010000000000000002120002400C0000000000005004FEFFFFFF74310017000100000000000000050100000000000000030100000003001700010000666F7572
+ 41 540101743400120003001200010000000000000002120002400C00000000000050040100000074310017000004000000000000050100000000000000030100000003001700010000666F7572
+ 42 540101743400120003001200010000000000000002120002400C0000000000005005FFFF050074310017000100000000000000050100000000000000030100000003001700010000666F7572
+ 43 540101743400120003001200010000000000000002120002400C000000000000500401006E0074310017000300000000001221050100000000000000030100000003001700010000666F7572
+ 44 540101743400120003001200010000000000020000120002400C00000000000050050100000074310017000100000000000000050100004000000000030100000025001700010000666F7572
+ 45 540101743400120003001200010000000000ECFF02120002400C000000000000500401F9FF00743100170001000000000000000500E1000000000000030100000003000000000000666F7572
+ 46 54010174340B0B0B0B0B0B0B0B0B0B0B0B0B0B0B00120003001200010000000000000002120002400C00000000000050040100000074310017010000000000000000050100FFE900000000030100000003007F00000000666F7572
+ 47 54010103001200010000000000020002120002400C0000000000005004010000F374310017000100000000000000050100000000000000030100000003001700010000666F8E72
+ 48 540101743400120003001200010000000000000002120002400C00000000000050030012000174310017000700000000000000050100002000000001000000000003001700010000666F7572
+ 49 540101743400120004001200010000000000000002120002400C0000000000005004010000FC733100170001000000000000000501000000000000000301000000F6FF17000100007C6F7572
+ 50 54010174FFDDFF8003001200010000100000000002120002400C000000000000500401000000743100170000000005010000000000000000000003010072
+ 51 540101743200120003001200010000000000000002120002400C00000000000050040100001074310017000000000003010000120300170100000000000000050100000000000000030100000003001700010000666F7572
+ 52 540101745401017434001200010000000000001702120002400C00000000000050040100001A74310017000100000000000100000100000000000000030100000003001700010000666F7572
+ 53 540101743400120003001200010000000000000002120002400C000000000000500401000000743100170001000002400C00000000000050040110000074310017000000000000050100000000000000030100000003001700010000666F7572
+ 54 540101743400120003001200010000000000000002120002400C000000000002120002400C00000000000050040100000074310017FF0050040100000074310017FF7F00000000000000050100000000000000030100000003001700010000666F7572
+ 55 540101743400120003001200010000000000000002120002400C00000000000050040100000074310017000100010080000001000000020003010100000300170100000003001700010000666F7572
+ 56 5487ffffff7f
+} {
+ do_test 2.$tn {
+ set changeset [binary decode hex $blob]
+#set fd [open x.change w+]
+#fconfigure $fd -encoding binary -translation binary
+#puts -nonewline $fd $changeset
+#close $fd
+ list [catch { sqlite3changeset_apply db $changeset 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 0000000..9b8f9ff
--- /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 0000000..22fa93c
--- /dev/null
+++ b/ext/session/session6.test
@@ -0,0 +1,91 @@
+# 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}
+ifcapable !incrblob {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 0000000..884da0e
--- /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 0000000..ebb88ff
--- /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 0000000..0e0a14e
--- /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 0000000..2c103d5
--- /dev/null
+++ b/ext/session/sessionB.test
@@ -0,0 +1,507 @@
+# 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_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 0000000..74370cb
--- /dev/null
+++ b/ext/session/sessionC.test
@@ -0,0 +1,197 @@
+# 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.
+#
+# UPDATE: The above is no longer true, as "PRAGMA defer_foreign_keys"
+# now disables "RESTRICT" processing. The test below has been rewritten
+# to use a trigger instead of a foreign key to test this case.
+#
+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 */);
+
+ CREATE TRIGGER restrict_trig BEFORE DELETE ON p BEGIN
+ SELECT raise(ABORT, 'error!') FROM c WHERE e=old.a;
+ END;
+
+ 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/sessionD.test b/ext/session/sessionD.test
new file mode 100644
index 0000000..ec652e2
--- /dev/null
+++ b/ext/session/sessionD.test
@@ -0,0 +1,257 @@
+# 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 focuses on the sqlite3session_diff() function.
+#
+
+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 sessionD
+
+proc scksum {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 ORDER BY type,name,sql"]
+ foreach tab $alltab {
+ set cols [list]
+ db eval "PRAGMA $dbname.table_info = $tab" x {
+ lappend cols "quote($x(name))"
+ }
+ set cols [join $cols ,]
+ append txt [db eval "SELECT $cols FROM $tab ORDER BY $cols"]
+ }
+ return [md5 $txt]
+}
+
+proc do_diff_test {tn setup} {
+ reset_db
+ forcedelete test.db2
+ execsql { ATTACH 'test.db2' AS aux }
+ execsql $setup
+
+ sqlite3session S db main
+ foreach tbl [db eval {SELECT name FROM sqlite_master WHERE type='table'}] {
+ S attach $tbl
+ S diff aux $tbl
+ }
+
+ set C [S changeset]
+ S delete
+
+ sqlite3 db2 test.db2
+ sqlite3changeset_apply db2 $C ""
+ uplevel do_test $tn.1 [list {execsql { PRAGMA integrity_check } db2}] ok
+ db2 close
+
+ set cksum [scksum db main]
+ uplevel do_test $tn.2 [list {scksum db aux}] [list $cksum]
+}
+
+# Ensure that the diff produced by comparing the current contents of [db]
+# with itself is empty.
+proc do_empty_diff_test {tn} {
+ forcedelete test.db2
+ forcecopy test.db test.db2
+
+ execsql { ATTACH 'test.db2' AS aux }
+ sqlite3session S db main
+ foreach tbl [db eval {SELECT name FROM sqlite_master WHERE type='table'}] {
+ S attach $tbl
+ S diff aux $tbl
+ }
+
+ set ::C [S changeset]
+ S delete
+
+ uplevel [list do_test $tn {string length $::C} 0]
+}
+
+
+forcedelete test.db2
+do_execsql_test 1.0 {
+ CREATE TABLE t2(a PRIMARY KEY, b);
+ INSERT INTO t2 VALUES(1, 'one');
+ INSERT INTO t2 VALUES(2, 'two');
+
+ ATTACH 'test.db2' AS aux;
+ CREATE TABLE aux.t2(a PRIMARY KEY, b);
+}
+
+do_test 1.1 {
+ sqlite3session S db main
+ S attach t2
+ S diff aux t2
+ set C [S changeset]
+ S delete
+} {}
+
+do_test 1.2 {
+ sqlite3 db2 test.db2
+ sqlite3changeset_apply db2 $C ""
+ db2 close
+ db eval { SELECT * FROM aux.t2 }
+} {1 one 2 two}
+
+do_diff_test 2.1 {
+ CREATE TABLE aux.t1(x, y, PRIMARY KEY(y));
+ CREATE TABLE t1(x, y, PRIMARY KEY(y));
+
+ INSERT INTO t1 VALUES(1, 2);
+ INSERT INTO t1 VALUES(NULL, 'xyz');
+ INSERT INTO t1 VALUES(4.5, 5.5);
+}
+
+do_diff_test 2.2 {
+ CREATE TABLE aux.t1(x, y, PRIMARY KEY(y));
+ CREATE TABLE t1(x, y, PRIMARY KEY(y));
+
+ INSERT INTO aux.t1 VALUES(1, 2);
+ INSERT INTO aux.t1 VALUES(NULL, 'xyz');
+ INSERT INTO aux.t1 VALUES(4.5, 5.5);
+}
+
+do_diff_test 2.3 {
+ CREATE TABLE aux.t1(a PRIMARY KEY, b TEXT);
+ CREATE TABLE t1(a PRIMARY KEY, b TEXT);
+
+ INSERT INTO aux.t1 VALUES(1, 'one');
+ INSERT INTO aux.t1 VALUES(2, 'two');
+ INSERT INTO aux.t1 VALUES(3, 'three');
+
+ INSERT INTO t1 VALUES(1, 'I');
+ INSERT INTO t1 VALUES(2, 'two');
+ INSERT INTO t1 VALUES(3, 'III');
+}
+
+do_diff_test 2.4 {
+ CREATE TABLE aux.t1(a, b, c, d, PRIMARY KEY(c, b, a));
+ CREATE TABLE t1(a, b, c, d, PRIMARY KEY(c, b, a));
+
+ INSERT INTO t1 VALUES('hvkzyipambwdqlvwv','',-458331.50,X'DA51ED5E84');
+ INSERT INTO t1 VALUES(X'C5C6B5DD','jjxrath',40917,830244);
+ INSERT INTO t1 VALUES(-204877.54,X'1704C253D5F3AFA8',155120.88,NULL);
+ INSERT INTO t1
+ VALUES('ckmqmzoeuvxisxqy',X'EB5A5D3A1DD22FD1','tidhjcbvbppdt',-642987.37);
+ INSERT INTO t1 VALUES(-851726,-161992,-469943,-159541);
+ INSERT INTO t1 VALUES(X'4A6A667F858938',185083,X'7A',NULL);
+
+ INSERT INTO aux.t1 VALUES(415075.74,'auawczkb',X'',X'57B4FAAF2595');
+ INSERT INTO aux.t1 VALUES(727637,711560,-181340,'hphuo');
+ INSERT INTO aux.t1
+ VALUES(-921322.81,662959,'lvlgwdgxaurr','ajjrzrbhqflsutnymgc');
+ INSERT INTO aux.t1 VALUES(-146061,-377892,X'4E','gepvpvvuhszpxabbb');
+ INSERT INTO aux.t1 VALUES(-851726,-161992,-469943,-159541);
+ INSERT INTO aux.t1 VALUES(X'4A6A667F858938',185083,X'7A',NULL);
+ INSERT INTO aux.t1 VALUES(-204877.54,X'1704C253D5F3AFA8',155120.88, 4);
+ INSERT INTO aux.t1
+ VALUES('ckmqmzoeuvxisxqy',X'EB5A5D3A1DD22FD1','tidgtsplhjcbvbppdt',-642987.3);
+}
+
+reset_db
+do_execsql_test 3.0 {
+ 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);
+
+ CREATE TABLE t2(a, b, c, PRIMARY KEY(a, b));
+ INSERT INTO t2 VALUES(1, 2, 3);
+ INSERT INTO t2 VALUES(4, 5, 6);
+ INSERT INTO t2 VALUES(7, 8, 9);
+
+ CREATE TABLE t3(a, b, c, PRIMARY KEY(a, b, c));
+ INSERT INTO t3 VALUES(1, 2, 3);
+ INSERT INTO t3 VALUES(4, 5, 6);
+ INSERT INTO t3 VALUES(7, 8, 9);
+}
+do_empty_diff_test 3.1
+
+
+#-------------------------------------------------------------------------
+# Test some error cases:
+#
+# 1) schema mismatches between the two dbs, and
+# 2) tables with no primary keys. This is not actually an error, but
+# should not add any changes to the session object.
+#
+reset_db
+forcedelete test.db2
+do_execsql_test 4.0 {
+ ATTACH 'test.db2' AS ixua;
+ CREATE TABLE ixua.t1(a, b, c);
+ CREATE TABLE main.t1(a, b, c);
+ INSERT INTO main.t1 VALUES(1, 2, 3);
+
+ CREATE TABLE ixua.t2(a PRIMARY KEY, b, c);
+ CREATE TABLE main.t2(a PRIMARY KEY, b, x);
+}
+
+do_test 4.1.1 {
+ sqlite3session S db main
+ S attach t1
+ list [catch { S diff ixua t1 } msg] $msg
+} {0 {}}
+do_test 4.1.2 {
+ string length [S changeset]
+} {0}
+S delete
+
+do_test 4.2.2 {
+ sqlite3session S db main
+ S attach t2
+ list [catch { S diff ixua t2 } msg] $msg
+} {1 {SQLITE_SCHEMA - table schemas do not match}}
+S delete
+
+do_test 4.3.1 {
+ sqlite3session S db main
+ S attach t4
+ execsql { CREATE TABLE t4(i PRIMARY KEY, b) }
+ list [catch { S diff ixua t4 } msg] $msg
+} {1 {SQLITE_SCHEMA - table schemas do not match}}
+S delete
+do_catchsql_test 4.3.2 {
+ SELECT * FROM ixua.t4;
+} {1 {no such table: ixua.t4}}
+
+do_test 4.4.1 {
+ sqlite3session S db main
+ S attach sqlite_stat1
+ execsql { ANALYZE }
+ execsql { DROP TABLE ixua.sqlite_stat1 }
+ list [catch { S diff ixua sqlite_stat1 } msg] $msg
+} {1 {SQLITE_SCHEMA - table schemas do not match}}
+S delete
+do_catchsql_test 4.4.2 {
+ SELECT * FROM ixua.sqlite_stat1;
+} {1 {no such table: ixua.sqlite_stat1}}
+
+do_test 4.5.1 {
+ sqlite3session S db main
+ S attach t8
+ list [catch { S diff ixua t8 } msg] $msg
+} {0 {}}
+S delete
+do_catchsql_test 4.5.2 {
+ SELECT * FROM ixua.i8;
+} {1 {no such table: ixua.i8}}
+
+finish_test
diff --git a/ext/session/sessionE.test b/ext/session/sessionE.test
new file mode 100644
index 0000000..9cec7d7
--- /dev/null
+++ b/ext/session/sessionE.test
@@ -0,0 +1,113 @@
+# 2015 June 02
+#
+# 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 sessions module.
+# Specifically, it tests that operations on tables without primary keys
+# are ignored.
+#
+
+
+
+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 sessionE
+
+#
+# Test plan:
+#
+# 1.*: Test that non-PK tables are not auto-attached.
+# 2.*: Test that explicitly attaching a non-PK table is a no-op.
+# 3.*: Test that sqlite3session_diff() on a non-PK table is a no-op.
+#
+
+
+#--------------------------------------------------------------------------
+reset_db
+do_execsql_test 1.0 {
+ CREATE TABLE t1(a, b);
+ CREATE TABLE t2(a PRIMARY KEY, b);
+}
+do_test 1.1 {
+ sqlite3session S db main
+ S attach *
+ execsql {
+ INSERT INTO t1 VALUES(1, 2);
+ INSERT INTO t2 VALUES(1, 2);
+ }
+} {}
+do_changeset_test 1.2 S {
+ {INSERT t2 0 X. {} {i 1 i 2}}
+}
+S delete
+
+reset_db
+do_execsql_test 2.0 {
+ CREATE TABLE t1(a, b);
+ CREATE TABLE t2(a PRIMARY KEY, b);
+}
+do_test 2.1 {
+ sqlite3session S db main
+ S attach t1
+ S attach t2
+ execsql {
+ INSERT INTO t1 VALUES(3, 4);
+ INSERT INTO t2 VALUES(3, 4);
+ INSERT INTO t1 VALUES(5, 6);
+ INSERT INTO t2 VALUES(5, 6);
+ }
+} {}
+do_changeset_test 2.2 S {
+ {INSERT t2 0 X. {} {i 3 i 4}}
+ {INSERT t2 0 X. {} {i 5 i 6}}
+}
+S delete
+
+reset_db
+forcedelete test.db2
+do_execsql_test 3.0 {
+ ATTACH 'test.db2' AS aux;
+ CREATE TABLE aux.t1(a, b);
+ CREATE TABLE aux.t2(a PRIMARY KEY, b);
+
+ CREATE TABLE t1(a, b);
+ CREATE TABLE t2(a PRIMARY KEY, b);
+
+ INSERT INTO t1 VALUES(1, 2);
+ INSERT INTO t2 VALUES(3, 4);
+}
+do_test 3.1 {
+ sqlite3session S db main
+ S attach t1
+ S diff aux t1
+
+ S attach t2
+ S diff aux t2
+} {}
+do_changeset_test 3.2 S {
+ {INSERT t2 0 X. {} {i 3 i 4}}
+}
+do_execsql_test 3.3 {
+ INSERT INTO t1 VALUES(5, 6);
+ INSERT INTO t2 VALUES(7, 8);
+}
+do_changeset_test 3.4 S {
+ {INSERT t2 0 X. {} {i 3 i 4}}
+ {INSERT t2 0 X. {} {i 7 i 8}}
+}
+
+
+S delete
+
+finish_test
diff --git a/ext/session/sessionF.test b/ext/session/sessionF.test
new file mode 100644
index 0000000..6a6eabc
--- /dev/null
+++ b/ext/session/sessionF.test
@@ -0,0 +1,294 @@
+# 2015 June 02
+#
+# 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 sessions module.
+# Specifically, it tests that tables appear in the correct order
+# within changesets and patchsets.
+#
+
+
+
+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 sessionF
+
+#
+# Test plan:
+#
+# 1.*: Test that sqlite3session_changeset() and sqlite3session_patchset()
+# output tables in the right order.
+#
+# 2.*: Test that sqlite3session_invert() does not modify the order of
+# tables within a changeset.
+#
+# 3.*: Test that sqlite3session_concat outputs tables in the right order.
+#
+
+# Create a db schema to use.
+#
+do_execsql_test 1.0 {
+ CREATE TABLE t3(e PRIMARY KEY, f);
+ CREATE TABLE t1(a PRIMARY KEY, b);
+ CREATE TABLE t2(c PRIMARY KEY, d);
+}
+
+#-----------------------------------------------------------------------
+# 1.* - changeset() and patchset().
+#
+
+foreach {tn setup result} {
+ 1 {
+ S attach *
+ } {
+ {INSERT t2 0 X. {} {i 2 t two}}
+ {INSERT t1 0 X. {} {i 1 t one}}
+ {INSERT t3 0 X. {} {i 3 t three}}
+ }
+
+ 2 {
+ S attach t1
+ S attach *
+ } {
+ {INSERT t1 0 X. {} {i 1 t one}}
+ {INSERT t2 0 X. {} {i 2 t two}}
+ {INSERT t3 0 X. {} {i 3 t three}}
+ }
+
+ 3 {
+ S attach t3
+ S attach t2
+ S attach t1
+ } {
+ {INSERT t3 0 X. {} {i 3 t three}}
+ {INSERT t2 0 X. {} {i 2 t two}}
+ {INSERT t1 0 X. {} {i 1 t one}}
+ }
+} {
+ execsql {
+ DELETE FROM t1;
+ DELETE FROM t2;
+ DELETE FROM t3;
+ }
+ sqlite3session S db main
+ eval $setup
+
+ do_execsql_test 1.$tn.1 {
+ INSERT INTO t2 VALUES(2, 'two');
+ INSERT INTO t1 VALUES(1, 'one');
+ INSERT INTO t3 VALUES(3, 'three');
+ }
+
+ do_changeset_test 1.1.$tn.2 S $result
+ do_patchset_test 1.1.$tn.3 S $result
+
+ S delete
+}
+
+foreach {tn setup result} {
+ 1 {
+ S attach *
+ } {
+ {INSERT t2 0 X. {} {i 4 t four}}
+ {INSERT t2 0 X. {} {i 5 t five}}
+ {INSERT t1 0 X. {} {i 1 t one}}
+ {INSERT t3 0 X. {} {i 6 t six}}
+ }
+
+ 2 {
+ S attach t1
+ S attach *
+ } {
+ {INSERT t1 0 X. {} {i 1 t one}}
+ {INSERT t2 0 X. {} {i 4 t four}}
+ {INSERT t2 0 X. {} {i 5 t five}}
+ {INSERT t3 0 X. {} {i 6 t six}}
+ }
+
+ 3 {
+ S attach t3
+ S attach t2
+ S attach t1
+ } {
+ {INSERT t3 0 X. {} {i 6 t six}}
+ {INSERT t2 0 X. {} {i 4 t four}}
+ {INSERT t2 0 X. {} {i 5 t five}}
+ {INSERT t1 0 X. {} {i 1 t one}}
+ }
+} {
+ execsql {
+ DELETE FROM t1;
+ DELETE FROM t2;
+ DELETE FROM t3;
+ }
+ sqlite3session S db main
+ eval $setup
+
+ do_execsql_test 1.$tn.1 {
+ INSERT INTO t2 VALUES(2, 'two');
+ INSERT INTO t1 VALUES(1, 'one');
+ DELETE FROM t2;
+ INSERT INTO t2 VALUES(4, 'four');
+ INSERT INTO t2 VALUES(5, 'five');
+ INSERT INTO t3 VALUES(6, 'six');
+ }
+
+ do_changeset_test 1.2.$tn.2 S $result
+ do_patchset_test 1.2.$tn.2 S $result
+
+ S delete
+}
+
+#-------------------------------------------------------------------------
+# 2.* - invert()
+#
+
+foreach {tn setup result} {
+ 1 {
+ S attach *
+ } {
+ {DELETE t2 0 X. {i 4 t four} {}}
+ {DELETE t2 0 X. {i 5 t five} {}}
+ {DELETE t1 0 X. {i 1 t one} {}}
+ {DELETE t3 0 X. {i 6 t six} {}}
+ }
+
+ 2 {
+ S attach t1
+ S attach *
+ } {
+ {DELETE t1 0 X. {i 1 t one} {}}
+ {DELETE t2 0 X. {i 4 t four} {}}
+ {DELETE t2 0 X. {i 5 t five} {}}
+ {DELETE t3 0 X. {i 6 t six} {}}
+ }
+
+ 3 {
+ S attach t3
+ S attach t2
+ S attach t1
+ } {
+ {DELETE t3 0 X. {i 6 t six} {}}
+ {DELETE t2 0 X. {i 4 t four} {}}
+ {DELETE t2 0 X. {i 5 t five} {}}
+ {DELETE t1 0 X. {i 1 t one} {}}
+ }
+} {
+ execsql {
+ DELETE FROM t1;
+ DELETE FROM t2;
+ DELETE FROM t3;
+ }
+ sqlite3session S db main
+ eval $setup
+
+ do_execsql_test 1.$tn.1 {
+ INSERT INTO t2 VALUES(2, 'two');
+ INSERT INTO t1 VALUES(1, 'one');
+ DELETE FROM t2;
+ INSERT INTO t2 VALUES(4, 'four');
+ INSERT INTO t2 VALUES(5, 'five');
+ INSERT INTO t3 VALUES(6, 'six');
+ }
+
+ do_changeset_invert_test 2.$tn.2 S $result
+
+ S delete
+}
+
+#-------------------------------------------------------------------------
+# 3.* - concat()
+#
+foreach {tn setup1 sql1 setup2 sql2 result} {
+ 1 {
+ S attach *
+ } {
+ INSERT INTO t1 VALUES(1, 'one');
+ INSERT INTO t2 VALUES(2, 'two');
+ } {
+ S attach t2
+ S attach t1
+ } {
+ INSERT INTO t1 VALUES(3, 'three');
+ INSERT INTO t2 VALUES(4, 'four');
+ } {
+ {INSERT t1 0 X. {} {i 1 t one}}
+ {INSERT t1 0 X. {} {i 3 t three}}
+ {INSERT t2 0 X. {} {i 2 t two}}
+ {INSERT t2 0 X. {} {i 4 t four}}
+ }
+
+ 1 {
+ S attach t2
+ S attach t1
+ } {
+ INSERT INTO t1 VALUES(1, 'one');
+ INSERT INTO t2 VALUES(2, 'two');
+ } {
+ S attach *
+ } {
+ INSERT INTO t1 VALUES(3, 'three');
+ INSERT INTO t2 VALUES(4, 'four');
+ } {
+ {INSERT t2 0 X. {} {i 2 t two}}
+ {INSERT t2 0 X. {} {i 4 t four}}
+ {INSERT t1 0 X. {} {i 1 t one}}
+ {INSERT t1 0 X. {} {i 3 t three}}
+ }
+
+ 1 {
+ S attach *
+ } {
+ INSERT INTO t2 VALUES(2, 'two');
+ } {
+ S attach *
+ } {
+ INSERT INTO t1 VALUES(3, 'three');
+ INSERT INTO t2 VALUES(4, 'four');
+ INSERT INTO t3 VALUES(5, 'five');
+ } {
+ {INSERT t2 0 X. {} {i 2 t two}}
+ {INSERT t2 0 X. {} {i 4 t four}}
+ {INSERT t1 0 X. {} {i 3 t three}}
+ {INSERT t3 0 X. {} {i 5 t five}}
+ }
+
+} {
+ execsql {
+ DELETE FROM t1;
+ DELETE FROM t2;
+ DELETE FROM t3;
+ }
+ sqlite3session S db main
+ eval $setup1
+ execsql $sql1
+ set c1 [S changeset]
+ S delete
+
+ sqlite3session S db main
+ eval $setup2
+ execsql $sql2
+ set c2 [S changeset]
+ S delete
+
+ set res [list]
+ sqlite3session_foreach x [sqlite3changeset_concat $c1 $c2] {
+ lappend res $x
+ }
+
+ do_test 3.$tn { set res } [list {*}$result]
+}
+
+
+finish_test
diff --git a/ext/session/sessionG.test b/ext/session/sessionG.test
new file mode 100644
index 0000000..58ea17d
--- /dev/null
+++ b/ext/session/sessionG.test
@@ -0,0 +1,250 @@
+# 2016 March 30
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+# This file implements regression tests for the sessions module.
+# Specifically, it tests that UNIQUE constraints are dealt with correctly.
+#
+
+
+
+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 sessionG
+
+
+forcedelete test.db2
+sqlite3 db2 test.db2
+
+do_test 1.0 {
+ do_common_sql {
+ CREATE TABLE t1(a PRIMARY KEY, b UNIQUE);
+ INSERT INTO t1 VALUES(1, 'one');
+ INSERT INTO t1 VALUES(2, 'two');
+ INSERT INTO t1 VALUES(3, 'three');
+ }
+ do_then_apply_sql {
+ DELETE FROM t1 WHERE a=1;
+ INSERT INTO t1 VALUES(4, 'one');
+ }
+ compare_db db db2
+} {}
+
+do_test 1.1 {
+ do_then_apply_sql {
+ DELETE FROM t1 WHERE a=4;
+ INSERT INTO t1 VALUES(1, 'one');
+ }
+ compare_db db db2
+} {}
+
+do_test 1.2 {
+ execsql { INSERT INTO t1 VALUES(5, 'five') } db2
+ do_then_apply_sql {
+ INSERT INTO t1 VALUES(11, 'eleven');
+ INSERT INTO t1 VALUES(12, 'five');
+ }
+ execsql { SELECT * FROM t1 } db2
+} {2 two 3 three 1 one 5 five 11 eleven}
+
+do_test 1.3 {
+ execsql { SELECT * FROM t1 }
+} {2 two 3 three 1 one 11 eleven 12 five}
+
+#-------------------------------------------------------------------------
+#
+reset_db
+db2 close
+forcedelete test.db2
+sqlite3 db2 test.db2
+
+do_test 2.1 {
+ do_common_sql {
+ CREATE TABLE t1(a PRIMARY KEY, b UNIQUE, c UNIQUE);
+ INSERT INTO t1 VALUES(1, 1, 1);
+ INSERT INTO t1 VALUES(2, 2, 2);
+ INSERT INTO t1 VALUES(3, 3, 3);
+ }
+} {}
+
+do_test 2.2.1 {
+ # It is not possible to apply the changeset generated by the following
+ # SQL, as none of the three updated rows may be updated as part of the
+ # first pass.
+ do_then_apply_sql {
+ UPDATE t1 SET b=0 WHERE a=1;
+ UPDATE t1 SET b=1 WHERE a=2;
+ UPDATE t1 SET b=2 WHERE a=3;
+ UPDATE t1 SET b=3 WHERE a=1;
+ }
+ db2 eval { SELECT a, b FROM t1 }
+} {1 1 2 2 3 3}
+do_test 2.2.2 { db eval { SELECT a, b FROM t1 } } {1 3 2 1 3 2}
+
+#-------------------------------------------------------------------------
+#
+reset_db
+db2 close
+forcedelete test.db2
+sqlite3 db2 test.db2
+
+do_test 3.1 {
+ do_common_sql {
+ CREATE TABLE t1(a PRIMARY KEY, b UNIQUE, c UNIQUE);
+ INSERT INTO t1 VALUES(1, 1, 1);
+ INSERT INTO t1 VALUES(2, 2, 2);
+ INSERT INTO t1 VALUES(3, 3, 3);
+ }
+} {}
+
+do_test 3.3 {
+ do_then_apply_sql {
+ UPDATE t1 SET b=4 WHERE a=3;
+ UPDATE t1 SET b=3 WHERE a=2;
+ UPDATE t1 SET b=2 WHERE a=1;
+ }
+ compare_db db db2
+} {}
+
+do_test 3.4 {
+ do_then_apply_sql {
+ UPDATE t1 SET b=1 WHERE a=1;
+ UPDATE t1 SET b=2 WHERE a=2;
+ UPDATE t1 SET b=3 WHERE a=3;
+ }
+ compare_db db db2
+} {}
+
+#-------------------------------------------------------------------------
+#
+reset_db
+db2 close
+forcedelete test.db2
+sqlite3 db2 test.db2
+
+do_test 4.1 {
+ do_common_sql {
+ CREATE TABLE t1(a PRIMARY KEY, b UNIQUE);
+ INSERT INTO t1 VALUES(1, 1);
+ INSERT INTO t1 VALUES(2, 2);
+ INSERT INTO t1 VALUES(3, 3);
+
+ CREATE TABLE t2(a PRIMARY KEY, b UNIQUE);
+ INSERT INTO t2 VALUES(1, 1);
+ INSERT INTO t2 VALUES(2, 2);
+ INSERT INTO t2 VALUES(3, 3);
+ }
+} {}
+
+do_test 4.2 {
+ do_then_apply_sql {
+ UPDATE t1 SET b=4 WHERE a=3;
+ UPDATE t1 SET b=3 WHERE a=2;
+ UPDATE t1 SET b=2 WHERE a=1;
+
+ UPDATE t2 SET b=0 WHERE a=1;
+ UPDATE t2 SET b=1 WHERE a=2;
+ UPDATE t2 SET b=2 WHERE a=3;
+ }
+ compare_db db db2
+} {}
+
+do_test 4.3 {
+ do_then_apply_sql {
+ UPDATE t1 SET b=1 WHERE a=1;
+ UPDATE t1 SET b=2 WHERE a=2;
+ UPDATE t1 SET b=3 WHERE a=3;
+
+ UPDATE t2 SET b=3 WHERE a=3;
+ UPDATE t2 SET b=2 WHERE a=2;
+ UPDATE t2 SET b=1 WHERE a=1;
+ }
+ compare_db db db2
+} {}
+
+#-------------------------------------------------------------------------
+reset_db
+catch { db2 close }
+forcedelete test.db2
+sqlite3 db2 test.db2
+
+do_execsql_test 5.0.1 {
+ CREATE TABLE t1(a PRIMARY KEY, b, c);
+ CREATE TABLE t2(a, b, c PRIMARY KEY);
+ CREATE TABLE t3(a, b PRIMARY KEY, c);
+}
+do_execsql_test -db db2 5.0.2 {
+ CREATE TABLE t1(a PRIMARY KEY, b, c);
+ CREATE TABLE t2(a, b, c);
+ CREATE TABLE t3(a, b PRIMARY KEY, c);
+}
+
+do_test 5.1 {
+ do_then_apply_sql {
+ INSERT INTO t1 VALUES(1, 2, 3);
+ INSERT INTO t2 VALUES(4, 5, 6);
+ INSERT INTO t3 VALUES(7, 8, 9);
+ }
+
+ db2 eval {
+ SELECT * FROM t1;
+ SELECT * FROM t2;
+ SELECT * FROM t3;
+ }
+} {1 2 3 7 8 9}
+
+#-------------------------------------------------------------------------
+
+reset_db
+db func number_name number_name
+do_execsql_test 6.0 {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b);
+ CREATE UNIQUE INDEX t1b ON t1(b);
+ WITH s(i) AS (
+ SELECT 1
+ UNION ALL
+ SELECT i+1 FROM s WHERE i<1000
+ )
+ INSERT INTO t1 SELECT i, number_name(i) FROM s;
+}
+
+do_test 6.1 {
+ db eval BEGIN
+ set ::C [changeset_from_sql {
+ DELETE FROM t1;
+ WITH s(i) AS (
+ SELECT 1
+ UNION ALL
+ SELECT i+1 FROM s WHERE i<1000
+ )
+ INSERT INTO t1 SELECT i, number_name(i+1) FROM s;
+ }]
+ db eval ROLLBACK
+ execsql { SELECT count(*) FROM t1 WHERE number_name(a) IS NOT b }
+} {0}
+
+proc xConflict {args} { exit ; return "OMIT" }
+do_test 6.2 {
+ sqlite3changeset_apply db $C xConflict
+} {}
+
+do_execsql_test 6.3 { SELECT count(*) FROM t1; } {1000}
+do_execsql_test 6.4 {
+ SELECT count(*) FROM t1 WHERE number_name(a+1) IS NOT b;
+} {0}
+
+# db eval { SELECT * FROM t1 } { puts "$a || $b" }
+
+
+finish_test
diff --git a/ext/session/sessionH.test b/ext/session/sessionH.test
new file mode 100644
index 0000000..8ba2311
--- /dev/null
+++ b/ext/session/sessionH.test
@@ -0,0 +1,84 @@
+# 2018 January 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.
+#
+#***********************************************************************
+#
+
+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 sessionH
+
+forcedelete test.db2
+sqlite3 db2 test.db2
+
+do_test 1.0 {
+ do_common_sql {
+ CREATE TABLE t1(a, b, c, PRIMARY KEY(a, b));
+ }
+ do_then_apply_sql {
+ WITH s(i) AS (
+ VALUES(1) UNION ALL SELECT i+1 FROM s WHERe i<10000
+ )
+ INSERT INTO t1 SELECT 'abcde', randomblob(16), i FROM s;
+ }
+ compare_db db db2
+} {}
+
+#------------------------------------------------------------------------
+db2 close
+reset_db
+
+do_execsql_test 2.0 {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c);
+ INSERT INTO main.t1 VALUES(1, 2, 3), (4, 5, 6), (7, 8, 9);
+}
+
+do_test 2.1 {
+ sqlite3session S db main
+ S attach *
+ db eval {
+ BEGIN;
+ INSERT INTO t1 VALUES(10, 11, 12);
+ DELETE FROM t1 WHERE a=1;
+ UPDATE t1 SET b='five', c='six' WHERE a=4;
+ }
+
+ set C [S changeset]
+ db eval ROLLBACK
+ S delete
+ set {} {}
+} {}
+
+do_execsql_test 2.2 {
+ CREATE TEMP TABLE t1(a INTEGER PRIMARY KEY, b, c);
+ INSERT INTO temp.t1 VALUES(1, 2, 3), (4, 5, 6), (7, 8, 9);
+}
+
+set ::conflict [list]
+proc xConflict {args} { lappend ::conflict $args ; return "" }
+do_test 2.3 {
+ sqlite3changeset_apply db $C xConflict
+ set ::conflict
+} {}
+do_execsql_test 2.4 {
+ SELECT * FROM main.t1;
+ SELECT '****';
+ SELECT * FROM temp.t1;
+} {
+ 4 five six 7 8 9 10 11 12
+ ****
+ 1 2 3 4 5 6 7 8 9
+}
+
+
+finish_test
diff --git a/ext/session/session_common.tcl b/ext/session/session_common.tcl
new file mode 100644
index 0000000..c52ac45
--- /dev/null
+++ b/ext/session/session_common.tcl
@@ -0,0 +1,215 @@
+
+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_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_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]
+}
+
+
+proc do_conflict_test {tn args} {
+
+ set O(-tables) [list]
+ set O(-sql) [list]
+ set O(-conflicts) [list]
+ set O(-policy) "OMIT"
+
+ array set V $args
+ foreach key [array names V] {
+ if {![info exists O($key)]} {error "no such option: $key"}
+ }
+ array set O $args
+
+ proc xConflict {args} [subst -nocommands {
+ lappend ::xConflict [set args]
+ return $O(-policy)
+ }]
+ proc bgerror {args} { set ::background_error $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}} {
+ if {$dbname == "main"} {
+ return [sql_exec_changeset db $sql]
+ }
+ 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 patchset_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 patchset
+ } patchset]
+ catch { S delete }
+
+ if {$rc} {
+ error $patchset
+ }
+ return $patchset
+}
+
+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 "$db1: $data1"
+ puts "$db2: $data2"
+ error "table $tbl data mismatch"
+ }
+ }
+
+ return ""
+}
+
+proc changeset_to_list {c} {
+ set list [list]
+ sqlite3session_foreach elem $c { lappend list $elem }
+ lsort $list
+}
+
+set ones {zero one two three four five six seven eight nine
+ ten eleven twelve thirteen fourteen fifteen sixteen seventeen
+ eighteen nineteen}
+set tens {{} ten twenty thirty forty fifty sixty seventy eighty ninety}
+proc number_name {n} {
+ if {$n>=1000} {
+ set txt "[number_name [expr {$n/1000}]] thousand"
+ set n [expr {$n%1000}]
+ } else {
+ set txt {}
+ }
+ if {$n>=100} {
+ append txt " [lindex $::ones [expr {$n/100}]] hundred"
+ set n [expr {$n%100}]
+ }
+ if {$n>=20} {
+ append txt " [lindex $::tens [expr {$n/10}]]"
+ set n [expr {$n%10}]
+ }
+ if {$n>0} {
+ append txt " [lindex $::ones $n]"
+ }
+ set txt [string trim $txt]
+ if {$txt==""} {set txt zero}
+ return $txt
+}
diff --git a/ext/session/session_speed_test.c b/ext/session/session_speed_test.c
new file mode 100644
index 0000000..405001c
--- /dev/null
+++ b/ext/session/session_speed_test.c
@@ -0,0 +1,358 @@
+/*
+** 2017 January 31
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file contains the source code for a standalone program used to
+** test the performance of the sessions module. Compile and run:
+**
+** ./session_speed_test -help
+**
+** for details.
+*/
+
+#include "sqlite3.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stddef.h>
+#include <unistd.h>
+
+/*************************************************************************
+** Start of generic command line parser.
+*/
+#define CMDLINE_BARE 0
+#define CMDLINE_INTEGER 1
+#define CMDLINE_STRING 2
+#define CMDLINE_BOOLEAN 3
+
+typedef struct CmdLineOption CmdLineOption;
+struct CmdLineOption {
+ const char *zText; /* Name of command line option */
+ const char *zHelp; /* Help text for option */
+ int eType; /* One of the CMDLINE_* values */
+ int iOff; /* Offset of output variable */
+};
+
+#define CMDLINE_INT32(x,y,z) {x, y, CMDLINE_INTEGER, z}
+#define CMDLINE_BOOL(x,y,z) {x, y, CMDLINE_BOOLEAN, z}
+#define CMDLINE_TEXT(x,y,z) {x, y, CMDLINE_STRING, z}
+#define CMDLINE_NONE(x,y,z) {x, y, CMDLINE_BARE, z}
+
+static void option_requires_argument_error(CmdLineOption *pOpt){
+ fprintf(stderr, "Option requires a%s argument: %s\n",
+ pOpt->eType==CMDLINE_INTEGER ? "n integer" :
+ pOpt->eType==CMDLINE_STRING ? " string" : " boolean",
+ pOpt->zText
+ );
+ exit(1);
+}
+
+static void ambiguous_option_error(const char *zArg){
+ fprintf(stderr, "Option is ambiguous: %s\n", zArg);
+ exit(1);
+}
+
+static void unknown_option_error(
+ const char *zArg,
+ CmdLineOption *aOpt,
+ const char *zHelp
+){
+ int i;
+ fprintf(stderr, "Unknown option: %s\n", zArg);
+ fprintf(stderr, "\nOptions are:\n");
+ fprintf(stderr, " % -30sEcho command line options\n", "-cmdline:verbose");
+ for(i=0; aOpt[i].zText; i++){
+ int eType = aOpt[i].eType;
+ char *zOpt = sqlite3_mprintf("%s %s", aOpt[i].zText,
+ eType==CMDLINE_BARE ? "" :
+ eType==CMDLINE_INTEGER ? "N" :
+ eType==CMDLINE_BOOLEAN ? "BOOLEAN" : "TEXT"
+ );
+ fprintf(stderr, " % -30s%s\n", zOpt, aOpt[i].zHelp);
+ sqlite3_free(zOpt);
+ }
+ if( zHelp ){
+ fprintf(stderr, "\n%s\n", zHelp);
+ }
+ exit(1);
+}
+
+static int get_integer_option(CmdLineOption *pOpt, const char *zArg){
+ int i = 0;
+ int iRet = 0;
+ int bSign = 1;
+ if( zArg[0]=='-' ){
+ bSign = -1;
+ i = 1;
+ }
+ while( zArg[i] ){
+ if( zArg[i]<'0' || zArg[i]>'9' ) option_requires_argument_error(pOpt);
+ iRet = iRet*10 + (zArg[i] - '0');
+ i++;
+ }
+ return (iRet*bSign);
+}
+
+static int get_boolean_option(CmdLineOption *pOpt, const char *zArg){
+ if( 0==sqlite3_stricmp(zArg, "true") ) return 1;
+ if( 0==sqlite3_stricmp(zArg, "1") ) return 1;
+ if( 0==sqlite3_stricmp(zArg, "0") ) return 0;
+ if( 0==sqlite3_stricmp(zArg, "false") ) return 0;
+ option_requires_argument_error(pOpt);
+ return 0;
+}
+
+static void parse_command_line(
+ int argc,
+ char **argv,
+ int iStart,
+ CmdLineOption *aOpt,
+ void *pStruct,
+ const char *zHelp
+){
+ char *pOut = (char*)pStruct;
+ int bVerbose = 0;
+ int iArg;
+
+ for(iArg=iStart; iArg<argc; iArg++){
+ const char *zArg = argv[iArg];
+ int nArg = strlen(zArg);
+ int nMatch = 0;
+ int iOpt;
+
+ for(iOpt=0; aOpt[iOpt].zText; iOpt++){
+ CmdLineOption *pOpt = &aOpt[iOpt];
+ if( 0==sqlite3_strnicmp(pOpt->zText, zArg, nArg) ){
+ if( nMatch ){
+ ambiguous_option_error(zArg);
+ }
+ nMatch++;
+ if( pOpt->eType==CMDLINE_BARE ){
+ *(int*)(&pOut[pOpt->iOff]) = 1;
+ }else{
+ iArg++;
+ if( iArg==argc ){
+ option_requires_argument_error(pOpt);
+ }
+ switch( pOpt->eType ){
+ case CMDLINE_INTEGER:
+ *(int*)(&pOut[pOpt->iOff]) = get_integer_option(pOpt, argv[iArg]);
+ break;
+ case CMDLINE_STRING:
+ *(const char**)(&pOut[pOpt->iOff]) = argv[iArg];
+ break;
+ case CMDLINE_BOOLEAN:
+ *(int*)(&pOut[pOpt->iOff]) = get_boolean_option(pOpt, argv[iArg]);
+ break;
+ }
+ }
+ }
+ }
+
+ if( nMatch==0 && 0==sqlite3_strnicmp("-cmdline:verbose", zArg, nArg) ){
+ bVerbose = 1;
+ nMatch = 1;
+ }
+
+ if( nMatch==0 ){
+ unknown_option_error(zArg, aOpt, zHelp);
+ }
+ }
+
+ if( bVerbose ){
+ int iOpt;
+ fprintf(stdout, "Options are: ");
+ for(iOpt=0; aOpt[iOpt].zText; iOpt++){
+ CmdLineOption *pOpt = &aOpt[iOpt];
+ if( pOpt->eType!=CMDLINE_BARE || *(int*)(&pOut[pOpt->iOff]) ){
+ fprintf(stdout, "%s ", pOpt->zText);
+ }
+ switch( pOpt->eType ){
+ case CMDLINE_INTEGER:
+ fprintf(stdout, "%d ", *(int*)(&pOut[pOpt->iOff]));
+ break;
+ case CMDLINE_BOOLEAN:
+ fprintf(stdout, "%d ", *(int*)(&pOut[pOpt->iOff]));
+ break;
+ case CMDLINE_STRING:
+ fprintf(stdout, "%s ", *(const char**)(&pOut[pOpt->iOff]));
+ break;
+ }
+ }
+ fprintf(stdout, "\n");
+ }
+}
+/*
+** End of generic command line parser.
+*************************************************************************/
+
+static void abort_due_to_error(int rc){
+ fprintf(stderr, "Error: %d\n");
+ exit(-1);
+}
+
+static void execsql(sqlite3 *db, const char *zSql){
+ int rc = sqlite3_exec(db, zSql, 0, 0, 0);
+ if( rc!=SQLITE_OK ) abort_due_to_error(rc);
+}
+
+static int xConflict(void *pCtx, int eConflict, sqlite3_changeset_iter *p){
+ return SQLITE_CHANGESET_ABORT;
+}
+
+static void run_test(
+ sqlite3 *db,
+ sqlite3 *db2,
+ int nRow,
+ const char *zSql
+){
+ sqlite3_session *pSession = 0;
+ sqlite3_stmt *pStmt = 0;
+ int rc;
+ int i;
+ int nChangeset;
+ void *pChangeset;
+
+ /* Attach a session object to database db */
+ rc = sqlite3session_create(db, "main", &pSession);
+ if( rc!=SQLITE_OK ) abort_due_to_error(rc);
+
+ /* Configure the session to capture changes on all tables */
+ rc = sqlite3session_attach(pSession, 0);
+ if( rc!=SQLITE_OK ) abort_due_to_error(rc);
+
+ /* Prepare the SQL statement */
+ rc = sqlite3_prepare(db, zSql, -1, &pStmt, 0);
+ if( rc!=SQLITE_OK ) abort_due_to_error(rc);
+
+ /* Open a transaction */
+ execsql(db, "BEGIN");
+
+ /* Execute the SQL statement nRow times */
+ for(i=0; i<nRow; i++){
+ sqlite3_bind_int(pStmt, 1, i);
+ sqlite3_step(pStmt);
+ rc = sqlite3_reset(pStmt);
+ if( rc!=SQLITE_OK ) abort_due_to_error(rc);
+ }
+ sqlite3_finalize(pStmt);
+
+ /* Extract a changeset from the sessions object */
+ rc = sqlite3session_changeset(pSession, &nChangeset, &pChangeset);
+ if( rc!=SQLITE_OK ) abort_due_to_error(rc);
+ execsql(db, "COMMIT");
+
+ /* Apply the changeset to the second db */
+ rc = sqlite3changeset_apply(db2, nChangeset, pChangeset, 0, xConflict, 0);
+ if( rc!=SQLITE_OK ) abort_due_to_error(rc);
+
+ /* Cleanup */
+ sqlite3_free(pChangeset);
+ sqlite3session_delete(pSession);
+}
+
+int main(int argc, char **argv){
+ struct Options {
+ int nRow;
+ int bWithoutRowid;
+ int bInteger;
+ int bAll;
+ const char *zDb;
+ };
+ struct Options o = { 2500, 0, 0, 0, "session_speed_test.db" };
+
+ CmdLineOption aOpt[] = {
+ CMDLINE_INT32( "-rows", "number of rows in test",
+ offsetof(struct Options, nRow) ),
+ CMDLINE_BOOL("-without-rowid", "use WITHOUT ROWID tables",
+ offsetof(struct Options, bWithoutRowid) ),
+ CMDLINE_BOOL("-integer", "use integer data (instead of text/blobs)",
+ offsetof(struct Options, bInteger) ),
+ CMDLINE_NONE("-all", "Run all 4 combos of -without-rowid and -integer",
+ offsetof(struct Options, bAll) ),
+ CMDLINE_TEXT("-database", "prefix for database files to use",
+ offsetof(struct Options, zDb) ),
+ {0, 0, 0, 0}
+ };
+
+ const char *azCreate[] = {
+ "CREATE TABLE t1(a PRIMARY KEY, b, c, d)",
+ "CREATE TABLE t1(a PRIMARY KEY, b, c, d) WITHOUT ROWID",
+ };
+
+ const char *azInsert[] = {
+ "INSERT INTO t1 VALUES("
+ "printf('%.8d',?), randomblob(50), randomblob(50), randomblob(50))",
+ "INSERT INTO t1 VALUES(?, random(), random(), random())"
+ };
+
+ const char *azUpdate[] = {
+ "UPDATE t1 SET d = randomblob(50) WHERE a = printf('%.8d',?)",
+ "UPDATE t1 SET d = random() WHERE a = ?"
+ };
+
+ const char *azDelete[] = {
+ "DELETE FROM t1 WHERE a = printf('%.8d',?)",
+ "DELETE FROM t1 WHERE a = ?"
+ };
+
+ int rc;
+ sqlite3 *db;
+ sqlite3 *db2;
+ char *zDb2;
+ int bWithoutRowid;
+ int bInteger;
+
+ parse_command_line(argc, argv, 1, aOpt, (void*)&o,
+ "This program creates two new, empty, databases each containing a single\n"
+ "table. It then does the following:\n\n"
+ " 1. Inserts -rows rows into the first database\n"
+ " 2. Updates each row in the first db\n"
+ " 3. Delete each row from the first db\n\n"
+ "The modifications made by each step are captured in a changeset and\n"
+ "applied to the second database.\n"
+ );
+ zDb2 = sqlite3_mprintf("%s2", o.zDb);
+
+ for(bWithoutRowid=0; bWithoutRowid<2; bWithoutRowid++){
+ for(bInteger=0; bInteger<2; bInteger++){
+ if( o.bAll || (o.bWithoutRowid==bWithoutRowid && o.bInteger==bInteger) ){
+ fprintf(stdout, "Testing %s data with %s table\n",
+ bInteger ? "integer" : "blob/text",
+ bWithoutRowid ? "WITHOUT ROWID" : "rowid"
+ );
+
+ /* Open new database handles on two empty databases */
+ unlink(o.zDb);
+ rc = sqlite3_open(o.zDb, &db);
+ if( rc!=SQLITE_OK ) abort_due_to_error(rc);
+ unlink(zDb2);
+ rc = sqlite3_open(zDb2, &db2);
+ if( rc!=SQLITE_OK ) abort_due_to_error(rc);
+
+ /* Create the schema in both databases. */
+ execsql(db, azCreate[o.bWithoutRowid]);
+ execsql(db2, azCreate[o.bWithoutRowid]);
+
+ /* Run the three tests */
+ run_test(db, db2, o.nRow, azInsert[o.bInteger]);
+ run_test(db, db2, o.nRow, azUpdate[o.bInteger]);
+ run_test(db, db2, o.nRow, azDelete[o.bInteger]);
+
+ /* Close the db handles */
+ sqlite3_close(db);
+ sqlite3_close(db2);
+ }
+ }
+ }
+
+
+ return 0;
+}
diff --git a/ext/session/sessionat.test b/ext/session/sessionat.test
new file mode 100644
index 0000000..8141d92
--- /dev/null
+++ b/ext/session/sessionat.test
@@ -0,0 +1,254 @@
+# 2017 February 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.
+#
+#***********************************************************************
+#
+# Tests for the sessions module. Specifically, that a changeset can
+# be applied after ALTER TABLE ADD COLUMN has been used to add
+# columns to tables.
+#
+
+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 sessionat
+
+# If SQLITE_OMIT_ALTERTABLE is defined, omit this file.
+ifcapable !altertable {
+ finish_test
+ return
+}
+
+db close
+sqlite3_shutdown
+test_sqlite3_log log
+proc log {code msg} { lappend ::log $code $msg }
+
+proc reset_test {} {
+ catch { db close }
+ catch { db2 close }
+ forcedelete test.db test.db2
+ sqlite3 db test.db
+ sqlite3 db2 test.db2
+}
+
+
+# Run all tests in this file twice. Once with "WITHOUT ROWID", and once
+# with regular rowid tables.
+#
+# ?.1.*: Test that PK inconsistencies are detected if one or more of the PK
+# columns are not present in the changeset.
+#
+# ?.2.*: Test that it is not possible to apply a changeset with N columns
+# to a db with fewer than N columns.
+#
+# ?.3.*: Test some INSERT, UPDATE and DELETE operations that do not
+# require conflict handling.
+#
+# ?.4.*: Test some INSERT, UPDATE and DELETE operations that do require
+# conflict handling.
+#
+# ?.5.*: Test that attempting to concat two changesets with different
+# numbers of columns for the same table is an error.
+#
+foreach {tn trailing} {
+ sessionat-ipk ""
+ sessionat-wor " WITHOUT ROWID "
+} {
+eval [string map [list %WR% $trailing] {
+ reset_test
+
+ #-----------------------------------------------------------------------
+ do_execsql_test $tn.1.0 {
+ CREATE TABLE t1(a, b, PRIMARY KEY(a)) %WR%;
+ }
+ do_execsql_test -db db2 $tn.1.1 {
+ CREATE TABLE t1(a, b, c, PRIMARY KEY(a, c)) %WR%;
+ }
+ do_test $tn.1.2 {
+ set ::log {}
+ do_then_apply_sql { INSERT INTO t1 VALUES('one', 'two') }
+ set ::log
+ } [list \
+ SQLITE_SCHEMA {sqlite3changeset_apply(): primary key mismatch for table t1}
+ ]
+ do_execsql_test $tn.1.3 { SELECT * FROM t1 } {one two}
+ do_execsql_test -db db2 $tn.1.4 { SELECT * FROM t1 } {}
+
+ #-----------------------------------------------------------------------
+ do_execsql_test $tn.2.0 {
+ CREATE TABLE t2(x, y, z, PRIMARY KEY(x)) %WR%;
+ }
+ do_execsql_test -db db2 $tn.2.1 {
+ CREATE TABLE t2(x, y, PRIMARY KEY(x)) %WR%;
+ }
+ do_test $tn.2.2 {
+ db cache flush
+ set ::log {}
+ do_then_apply_sql { INSERT INTO t2 VALUES(1, 2, 3) }
+ set ::log
+ } [list SQLITE_SCHEMA \
+ {sqlite3changeset_apply(): table t2 has 2 columns, expected 3 or more}
+ ]
+ do_execsql_test $tn.2.3 { SELECT * FROM t2 } {1 2 3}
+ do_execsql_test -db db2 $tn.2.4 { SELECT * FROM t2 } {}
+
+ #-----------------------------------------------------------------------
+ do_execsql_test $tn.3.0 {
+ CREATE TABLE t3(a, b, PRIMARY KEY(b)) %WR%;
+ }
+ do_execsql_test -db db2 $tn.3.1 {
+ CREATE TABLE t3(a, b, c DEFAULT 'D', PRIMARY KEY(b)) %WR%;
+ }
+ do_test $tn.3.2 {
+ do_then_apply_sql {
+ INSERT INTO t3 VALUES(1, 2);
+ INSERT INTO t3 VALUES(3, 4);
+ INSERT INTO t3 VALUES(5, 6);
+ };
+ db2 eval {SELECT * FROM t3}
+ } {1 2 D 3 4 D 5 6 D}
+ do_test $tn.3.3 {
+ do_then_apply_sql {
+ UPDATE t3 SET a=45 WHERE b=4;
+ DELETE FROM t3 WHERE a=5;
+ };
+ db2 eval {SELECT * FROM t3}
+ } {1 2 D 45 4 D}
+
+ #-----------------------------------------------------------------------
+ # 4.1: INSERT statements
+ # 4.2: DELETE statements
+ # 4.3: UPDATE statements
+ #
+ do_execsql_test $tn.4.1.0 {
+ CREATE TABLE t4(x INTEGER PRIMARY KEY, y) %WR%;
+ }
+ do_execsql_test -db db2 $tn.4.1.1 {
+ CREATE TABLE t4(x INTEGER PRIMARY KEY, y, z) %WR%;
+ INSERT INTO t4 VALUES(1, 2, 3);
+ INSERT INTO t4 VALUES(4, 5, 6);
+ }
+ do_conflict_test $tn.4.1.2 -tables t4 -sql {
+ INSERT INTO t4 VALUES(10, 20);
+ INSERT INTO t4 VALUES(4, 11);
+ } -conflicts {
+ {INSERT t4 CONFLICT {i 4 i 11} {i 4 i 5}}
+ }
+ do_execsql_test -db db2 $tn.4.1.3 {
+ SELECT * FROM t4 ORDER BY x
+ } {1 2 3 4 5 6 10 20 {}}
+ do_conflict_test $tn.4.1.4 -policy REPLACE -tables t4 -sql {
+ INSERT INTO t4 VALUES(1, 11);
+ } -conflicts {
+ {INSERT t4 CONFLICT {i 1 i 11} {i 1 i 2}}
+ }
+ do_execsql_test -db db2 $tn.4.1.5 {
+ SELECT * FROM t4 ORDER BY x
+ } {1 11 {} 4 5 6 10 20 {}}
+
+ do_execsql_test $tn.4.2.0 {
+ DELETE FROM t4;
+ INSERT INTO t4 VALUES(1, 'A');
+ INSERT INTO t4 VALUES(2, 'B');
+ INSERT INTO t4 VALUES(3, 'C');
+ INSERT INTO t4 VALUES(4, 'D');
+ }
+ do_execsql_test -db db2 $tn.4.2.1 {
+ DELETE FROM t4;
+ INSERT INTO t4 VALUES(1, 'A', 'a');
+ INSERT INTO t4 VALUES(3, 'C', 'c');
+ INSERT INTO t4 VALUES(4, 'E', 'd');
+ }
+ do_conflict_test $tn.4.2.2 -tables t4 -sql {
+ DELETE FROM t4 WHERE x=2;
+ DELETE FROM t4 WHERE x=4;
+ } -conflicts {
+ {DELETE t4 NOTFOUND {i 2 t B}}
+ {DELETE t4 DATA {i 4 t D} {i 4 t E}}
+ }
+
+ do_execsql_test $tn.4.3.0 {
+ CREATE TABLE t5(a, b, c PRIMARY KEY) %WR%;
+ INSERT INTO t5 VALUES(1,1,1), (2,2,2), (3,3,3), (4,4,4);
+ }
+ do_execsql_test -db db2 $tn.4.3.1 {
+ CREATE TABLE t5(a, b, c PRIMARY KEY, d CHECK(b!=10)) %WR%;
+ INSERT INTO t5 VALUES (2,2,2,2), (3,8,3,3), (4,4,4,4);
+ }
+ do_conflict_test $tn.4.3.2 -tables t5 -sql {
+ UPDATE t5 SET a=4 WHERE c=1;
+ UPDATE t5 SET b=9 WHERE c=3;
+ UPDATE t5 SET b=10 WHERE c=2;
+ } -conflicts {
+ {UPDATE t5 NOTFOUND {i 1 {} {} i 1} {i 4 {} {} {} {}}}
+ {UPDATE t5 DATA {{} {} i 3 i 3} {{} {} i 9 {} {}} {i 3 i 8 i 3}}
+ {UPDATE t5 CONSTRAINT {{} {} i 2 i 2} {{} {} i 10 {} {}}}
+ }
+
+ #-----------------------------------------------------------------------
+ do_execsql_test $tn.5.0 {
+ CREATE TABLE t6(a, b, c, PRIMARY KEY(a, b)) %WR%;
+ }
+ do_execsql_test -db db2 $tn.5.1 {
+ CREATE TABLE t6(a, b, c, d, e, PRIMARY KEY(a, b)) %WR%;
+ }
+ do_test $tn.5.2 {
+ set c1 [sql_exec_changeset db {
+ INSERT INTO t6 VALUES(1, 1, 1);
+ INSERT INTO t6 VALUES(2, 2, 2);
+ }]
+ set c2 [sql_exec_changeset db2 {
+ INSERT INTO t6 VALUES(3, 3, 3, 3, 3);
+ INSERT INTO t6 VALUES(4, 4, 4, 4, 4);
+ }]
+ list [catch { sqlite3changeset_concat $c1 $c2} msg] $msg
+ } {1 SQLITE_SCHEMA}
+
+ #-----------------------------------------------------------------------
+ db2 close
+ sqlite3 db2 test.db
+ do_execsql_test $tn.6.0 {
+ CREATE TABLE t7(a INTEGER PRIMARY KEY, b) %WR%;
+ INSERT INTO t7 VALUES(1, 1);
+ INSERT INTO t7 VALUES(2, 2);
+ INSERT INTO t7 VALUES(3, 3);
+ }
+
+ do_test $tn.6.1 {
+ set c1 [sql_exec_changeset db {
+ INSERT INTO t7 VALUES(4, 4);
+ DELETE FROM t7 WHERE a=1;
+ UPDATE t7 SET b=222 WHERE a=2;
+ }]
+ set cinv [sqlite3changeset_invert $c1]
+ execsql { SELECT * FROM t7 }
+ } {2 222 3 3 4 4}
+
+ do_execsql_test -db db2 $tn.6.2 {
+ ALTER TABLE t7 ADD COLUMN c DEFAULT 'ccc'
+ }
+
+ proc xConfict {args} { return "OMIT" }
+ do_test $tn.6.3 {
+ sqlite3changeset_apply db $cinv xConflict
+ execsql { SELECT * FROM t7 }
+ } {1 1 ccc 2 2 ccc 3 3 ccc}
+}]
+}
+
+catch { db close }
+catch { db2 close }
+sqlite3_shutdown
+test_sqlite3_log
+
+finish_test
diff --git a/ext/session/sessionbig.test b/ext/session/sessionbig.test
new file mode 100644
index 0000000..80ce00a
--- /dev/null
+++ b/ext/session/sessionbig.test
@@ -0,0 +1,108 @@
+# 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}
+
+if {[permutation]=="session_strm" || [permutation]=="session_eec"} {
+ finish_test
+ return
+}
+
+if {$::tcl_platform(pointerSize)<8} {
+ finish_test
+ return
+}
+
+set testprefix sessionbig
+
+forcedelete test.db2
+sqlite3 db2 test.db2
+
+do_execsql_test 1.0 {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b);
+}
+do_execsql_test -db db2 1.1 {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b);
+}
+
+do_test 1.2 {
+ do_then_apply_sql {
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ }
+} {}
+
+do_test 1.3 {
+ execsql { DELETE FROM t1 }
+ execsql2 { DELETE FROM t1 }
+} {}
+
+do_test 1.4 {
+ set rc [catch {
+ do_then_apply_sql {
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ INSERT INTO t1(b) VALUES( zeroblob(100*1000*1000) );
+ }
+ } msg]
+ list $rc $msg
+} {1 SQLITE_NOMEM}
+
+
+finish_test
+
diff --git a/ext/session/sessiondiff.test b/ext/session/sessiondiff.test
new file mode 100644
index 0000000..b00af0e
--- /dev/null
+++ b/ext/session/sessiondiff.test
@@ -0,0 +1,114 @@
+# 2015-07-31
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+# Tests for the [sqldiff --changeset] command.
+#
+#
+if {![info exists testdir]} {
+ set testdir [file join [file dirname [info script]] .. .. test]
+}
+source $testdir/tester.tcl
+ifcapable !session {finish_test; return}
+set testprefix sessiondiff
+
+set PROG [test_find_sqldiff]
+db close
+
+proc sqlesc {id} {
+ set ret "'[string map {' ''} $id]'"
+ set ret
+}
+
+proc database_cksum {db1} {
+ set txt ""
+
+ sqlite3 dbtmp $db1
+ foreach tbl [dbtmp eval {SELECT name FROM sqlite_master WHERE type='table'}] {
+ set cols [list]
+ dbtmp eval "PRAGMA table_info = [sqlesc $tbl]" {
+ lappend cols "quote( $name )"
+ }
+ append txt [dbtmp eval \
+ "SELECT [join $cols {||'.'||}] FROM [sqlesc $tbl] ORDER BY 1"
+ ]
+ }
+ dbtmp close
+
+ md5 $txt
+}
+
+proc readfile {filename} {
+ set fd [open $filename]
+ fconfigure $fd -translation binary -encoding binary
+ set data [read $fd]
+ close $fd
+ set data
+}
+
+proc get_changeset {db1 db2} {
+ exec $::PROG --changeset changeset.bin $db1 $db2
+ set bin [readfile changeset.bin]
+ return $bin
+}
+
+proc xConflict {args} {
+ return ""
+}
+
+proc do_changeset_test {tn sql1 sql2} {
+ forcedelete test.db123 test.db124
+
+ sqlite3 db test.db123
+ db eval $sql1
+ db close
+
+ sqlite3 db test.db124
+ db eval $sql2
+
+ set cs [get_changeset test.db124 test.db123]
+ sqlite3changeset_apply db $cs xConflict
+ db close
+
+ set database_cksum1 [database_cksum test.db123]
+ set database_cksum2 [database_cksum test.db124]
+
+ uplevel [list \
+ do_test $tn [list string compare $database_cksum1 $database_cksum2] 0
+ ]
+}
+
+do_changeset_test 1.0 {
+ CREATE TABLE t1(x PRIMARY KEY);
+} {
+ CREATE TABLE t1(x PRIMARY KEY);
+}
+
+do_changeset_test 1.1 {
+ CREATE TABLE t1(x PRIMARY KEY);
+ CREATE TABLE t2(x PRIMARY KEY, y);
+ INSERT INTO t2 VALUES(1, 2);
+} {
+ CREATE TABLE t1(x PRIMARY KEY);
+ CREATE TABLE t2(x PRIMARY KEY, y);
+ INSERT INTO t2 VALUES(3, 4);
+}
+
+do_changeset_test 1.2 {
+ CREATE TABLE t2(a, b, c, PRIMARY KEY(b, c));
+ INSERT INTO t2 VALUES(1, 2, 3);
+ INSERT INTO t2 VALUES(4, 5, 6);
+} {
+ CREATE TABLE t2(a, b, c, PRIMARY KEY(b, c));
+ INSERT INTO t2 VALUES(1, 2, 11);
+ INSERT INTO t2 VALUES(7, 8, 9);
+}
+
+finish_test
diff --git a/ext/session/sessionfault.test b/ext/session/sessionfault.test
new file mode 100644
index 0000000..be6c456
--- /dev/null
+++ b/ext/session/sessionfault.test
@@ -0,0 +1,589 @@
+# 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
+ifcapable !session {finish_test; return}
+
+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 7.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 {
+ catch { S delete }
+ 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 {
+ catch { S changeset }
+ catch { 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/sessionfault2.test b/ext/session/sessionfault2.test
new file mode 100644
index 0000000..dd00eaa
--- /dev/null
+++ b/ext/session/sessionfault2.test
@@ -0,0 +1,284 @@
+# 2016 March 31
+#
+# 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 sessionfault2
+
+if 1 {
+
+do_execsql_test 1.0.0 {
+ CREATE TABLE t1(a PRIMARY KEY, b UNIQUE);
+ INSERT INTO t1 VALUES(1, 1);
+ INSERT INTO t1 VALUES(2, 2);
+ INSERT INTO t1 VALUES(3, 3);
+
+ CREATE TABLE t2(a PRIMARY KEY, b UNIQUE);
+ INSERT INTO t2 VALUES(1, 1);
+ INSERT INTO t2 VALUES(2, 2);
+ INSERT INTO t2 VALUES(3, 3);
+}
+faultsim_save_and_close
+
+faultsim_restore_and_reopen
+do_test 1.0.1 {
+ set ::C [changeset_from_sql {
+ UPDATE t1 SET b=4 WHERE a=3;
+ UPDATE t1 SET b=3 WHERE a=2;
+ UPDATE t1 SET b=2 WHERE a=1;
+ UPDATE t2 SET b=0 WHERE a=1;
+ UPDATE t2 SET b=1 WHERE a=2;
+ UPDATE t2 SET b=2 WHERE a=3;
+ }]
+ set {} {}
+} {}
+
+proc xConflict args { return "OMIT" }
+
+do_faultsim_test 1 -faults oom-p* -prep {
+ faultsim_restore_and_reopen
+} -body {
+ sqlite3changeset_apply db $::C xConflict
+} -test {
+ faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
+ faultsim_integrity_check
+
+ catch { db eval ROLLBACK }
+ set res [db eval {
+ SELECT * FROM t1;
+ SELECT * FROM t2;
+ }]
+
+ if {$testrc==0} {
+ if {$res != "1 2 2 3 3 4 1 0 2 1 3 2"} { error "data error" }
+ } else {
+ if {
+ $res != "1 2 2 3 3 4 1 0 2 1 3 2"
+ && $res != "1 1 2 2 3 3 1 1 2 2 3 3"
+ } { error "data error!! $res" }
+ }
+}
+
+#-------------------------------------------------------------------------
+# OOM when applying a changeset for which one of the tables has a name
+# 99 bytes in size. This happens to cause an extra malloc in within the
+# sessions_strm permutation.
+#
+reset_db
+set nm [string repeat t 99]
+do_execsql_test 2.0.0 [string map "%TBL% $nm" {
+ CREATE TABLE %TBL%(a PRIMARY KEY, b UNIQUE);
+}]
+faultsim_save_and_close
+
+faultsim_restore_and_reopen
+do_test 1.0.1 {
+ set ::C [changeset_from_sql [string map "%TBL% $nm" {
+ INSERT INTO %TBL% VALUES(1, 2);
+ INSERT INTO %TBL% VALUES(3, 4);
+ }]]
+ set {} {}
+} {}
+
+proc xConflict args { return "OMIT" }
+do_faultsim_test 2 -faults oom-p* -prep {
+ faultsim_restore_and_reopen
+} -body {
+ sqlite3changeset_apply db $::C xConflict
+} -test {
+ faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
+ faultsim_integrity_check
+}
+
+#-------------------------------------------------------------------------
+# OOM when collecting and apply a changeset that uses sqlite_stat1.
+#
+reset_db
+forcedelete test.db2
+sqlite3 db2 test.db2
+do_common_sql {
+ CREATE TABLE t1(a PRIMARY KEY, b UNIQUE, c);
+ CREATE INDEX i1 ON t1(c);
+ INSERT INTO t1 VALUES(1, 2, 3);
+ INSERT INTO t1 VALUES(4, 5, 6);
+ INSERT INTO t1 VALUES(7, 8, 9);
+ CREATE TABLE t2(a, b, c);
+ INSERT INTO t2 VALUES(1, 2, 3);
+ INSERT INTO t2 VALUES(4, 5, 6);
+ INSERT INTO t2 VALUES(7, 8, 9);
+ ANALYZE;
+}
+faultsim_save_and_close
+db2 close
+
+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 sqlite_stat1 VALUES('x', 'y', 45);
+ UPDATE sqlite_stat1 SET stat = 123 WHERE tbl='t1' AND idx='i1';
+ UPDATE sqlite_stat1 SET stat = 456 WHERE tbl='t2';
+ }
+} -test {
+ faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
+ faultsim_integrity_check
+ if {$testrc==0} { compare_db db db2 }
+}
+
+#-------------------------------------------------------------------------
+# OOM when collecting and using a rebase changeset.
+#
+reset_db
+do_execsql_test 2.0 {
+ CREATE TABLE t3(a, b, c, PRIMARY KEY(b, c));
+ CREATE TABLE t4(x PRIMARY KEY, y, z);
+
+ INSERT INTO t3 VALUES(1, 2, 3);
+ INSERT INTO t3 VALUES(4, 2, 5);
+ INSERT INTO t3 VALUES(7, 2, 9);
+
+ INSERT INTO t4 VALUES('a', 'b', 'c');
+ INSERT INTO t4 VALUES('d', 'e', 'f');
+ INSERT INTO t4 VALUES('g', 'h', 'i');
+}
+faultsim_save_and_close
+db2 close
+
+proc xConflict {ret args} { return $ret }
+
+do_test 2.1 {
+ faultsim_restore_and_reopen
+ set C1 [changeset_from_sql {
+ INSERT INTO t3 VALUES(10, 11, 12);
+ UPDATE t4 SET y='j' WHERE x='g';
+ DELETE FROM t4 WHERE x='a';
+ }]
+
+ faultsim_restore_and_reopen
+ set C2 [changeset_from_sql {
+ INSERT INTO t3 VALUES(1000, 11, 12);
+ DELETE FROM t4 WHERE x='g';
+ }]
+
+ faultsim_restore_and_reopen
+ sqlite3changeset_apply db $C1 [list xConflict OMIT]
+ faultsim_save_and_close
+} {}
+
+do_faultsim_test 2.2 -faults oom* -prep {
+ catch {db2 close}
+ catch {db close}
+ faultsim_restore_and_reopen
+ sqlite3 db2 test.db2
+} -body {
+ set rebase [sqlite3changeset_apply_v2 db $::C2 [list xConflict OMIT]]
+ set {} {}
+} -test {
+ faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
+}
+do_faultsim_test 2.3 -faults oom* -prep {
+ catch {db2 close}
+ catch {db close}
+ faultsim_restore_and_reopen
+ sqlite3 db2 test.db2
+} -body {
+ set rebase [sqlite3changeset_apply_v2 db $::C2 [list xConflict REPLACE]]
+ set {} {}
+} -test {
+ faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
+}
+do_faultsim_test 2.4 -faults oom* -prep {
+ catch {db2 close}
+ catch {db close}
+ faultsim_restore_and_reopen
+ set ::rebase [sqlite3changeset_apply_v2 db $::C2 [list xConflict REPLACE]]
+} -body {
+ sqlite3rebaser_create R
+ R configure $::rebase
+ R rebase $::C1
+ set {} {}
+} -test {
+ catch { R delete }
+ faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
+}
+do_faultsim_test 2.5 -faults oom* -prep {
+ catch {db2 close}
+ catch {db close}
+ faultsim_restore_and_reopen
+ set ::rebase [sqlite3changeset_apply_v2 db $::C2 [list xConflict OMIT]]
+} -body {
+ sqlite3rebaser_create R
+ R configure $::rebase
+ R rebase $::C1
+ set {} {}
+} -test {
+ catch { R delete }
+ faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
+}
+
+}
+
+reset_db
+do_execsql_test 3.0 {
+ CREATE TABLE t1(x PRIMARY KEY, y, z);
+ INSERT INTO t1 VALUES(3, 1, 4);
+ INSERT INTO t1 VALUES(1, 5, 9);
+}
+faultsim_save_and_close
+
+proc xConflict {ret args} { return $ret }
+
+do_test 3.1 {
+ faultsim_restore_and_reopen
+
+ execsql { BEGIN; UPDATE t1 SET z=11; }
+ set C1 [changeset_from_sql {
+ UPDATE t1 SET z=10 WHERE x=1;
+ }]
+ execsql { ROLLBACK }
+
+ execsql { BEGIN; UPDATE t1 SET z=11; }
+ set C2 [changeset_from_sql {
+ UPDATE t1 SET z=55 WHERE x=1;
+ }]
+ execsql { ROLLBACK }
+
+ set ::rebase1 [sqlite3changeset_apply_v2 db $::C1 [list xConflict OMIT]]
+ set ::rebase2 [sqlite3changeset_apply_v2 db $::C2 [list xConflict OMIT]]
+ set {} {}
+ execsql { SELECT * FROM t1 }
+} {3 1 4 1 5 9}
+
+
+do_faultsim_test 3.2 -faults oom* -prep {
+ faultsim_restore_and_reopen
+} -body {
+ sqlite3rebaser_create R
+ R configure $::rebase1
+ R configure $::rebase2
+ set {} {}
+} -test {
+ catch { R delete }
+ faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
+}
+
+
+finish_test
diff --git a/ext/session/sessioninvert.test b/ext/session/sessioninvert.test
new file mode 100644
index 0000000..b7c157d
--- /dev/null
+++ b/ext/session/sessioninvert.test
@@ -0,0 +1,183 @@
+# 2018 October 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 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 sessioninvert
+
+proc iter_invert {C} {
+ set x [list]
+ sqlite3session_foreach -invert c $C { lappend x $c }
+ set x
+}
+
+proc do_invert_test {tn sql {iter {}}} {
+
+ forcecopy test.db test.db2
+ sqlite3 db2 test.db2
+
+ set C [changeset_from_sql $sql]
+
+ forcecopy test.db test.db3
+ sqlite3 db3 test.db3
+ uplevel [list do_test $tn.1 [list compare_db db db3] {}]
+
+ set I [sqlite3changeset_invert $C]
+ sqlite3changeset_apply db $I {}
+ uplevel [list do_test $tn.2 [list compare_db db db2] {}]
+
+ sqlite3changeset_apply_v2 -invert db3 $C {}
+ uplevel [list do_test $tn.3 [list compare_db db db3] {}]
+
+ if {$iter!=""} {
+ uplevel [list do_test $tn.4 [list iter_invert $C] [list {*}$iter]]
+ }
+
+ catch { db2 close }
+ catch { db3 close }
+}
+
+do_execsql_test 1.0 {
+ CREATE TABLE t1(a PRIMARY KEY, b, c);
+ CREATE TABLE t2(d, e, f, PRIMARY KEY(e, f));
+
+ INSERT INTO t1 VALUES(1, 'one', 'i');
+ INSERT INTO t1 VALUES(2, 'two', 'ii');
+ INSERT INTO t1 VALUES(3, 'three', 'iii');
+ INSERT INTO t1 VALUES(4, 'four', 'iv');
+ INSERT INTO t1 VALUES(5, 'five', 'v');
+ INSERT INTO t1 VALUES(6, 'six', 'vi');
+
+ INSERT INTO t2 SELECT * FROM t1;
+}
+
+do_invert_test 1.1 {
+ INSERT INTO t1 VALUES(7, 'seven', 'vii');
+} {
+ {DELETE t1 0 X.. {i 7 t seven t vii} {}}
+}
+
+do_invert_test 1.2 {
+ DELETE FROM t1 WHERE a<4;
+} {
+ {INSERT t1 0 X.. {} {i 1 t one t i}}
+ {INSERT t1 0 X.. {} {i 2 t two t ii}}
+ {INSERT t1 0 X.. {} {i 3 t three t iii}}
+}
+
+do_invert_test 1.3 {
+ UPDATE t1 SET c=5;
+} {
+ {UPDATE t1 0 X.. {i 1 {} {} i 5} {{} {} {} {} t i}}
+ {UPDATE t1 0 X.. {i 2 {} {} i 5} {{} {} {} {} t ii}}
+ {UPDATE t1 0 X.. {i 3 {} {} i 5} {{} {} {} {} t iii}}
+ {UPDATE t1 0 X.. {i 4 {} {} i 5} {{} {} {} {} t iv}}
+ {UPDATE t1 0 X.. {i 5 {} {} i 5} {{} {} {} {} t v}}
+ {UPDATE t1 0 X.. {i 6 {} {} i 5} {{} {} {} {} t vi}}
+}
+
+do_invert_test 1.4 {
+ UPDATE t1 SET b = a+1 WHERE a%2;
+ DELETE FROM t2;
+ INSERT INTO t1 VALUES(10, 'ten', NULL);
+}
+
+do_invert_test 1.5 {
+ UPDATE t2 SET d = d-1;
+} {
+ {UPDATE t2 0 .XX {i 2 t three t iii} {i 3 {} {} {} {}}}
+ {UPDATE t2 0 .XX {i 1 t two t ii} {i 2 {} {} {} {}}}
+ {UPDATE t2 0 .XX {i 5 t six t vi} {i 6 {} {} {} {}}}
+ {UPDATE t2 0 .XX {i 3 t four t iv} {i 4 {} {} {} {}}}
+ {UPDATE t2 0 .XX {i 0 t one t i} {i 1 {} {} {} {}}}
+ {UPDATE t2 0 .XX {i 4 t five t v} {i 5 {} {} {} {}}}
+}
+
+do_execsql_test 2.0 {
+ ANALYZE;
+ PRAGMA writable_schema = 1;
+ DROP TABLE IF EXISTS sqlite_stat4;
+ SELECT * FROM sqlite_stat1;
+} {
+ t2 sqlite_autoindex_t2_1 {6 1 1}
+ t1 sqlite_autoindex_t1_1 {6 1}
+}
+
+do_invert_test 2.1 {
+ INSERT INTO sqlite_stat1 VALUES('t3', 'idx2', '1 2 3');
+} {
+ {DELETE sqlite_stat1 0 XX. {t t3 t idx2 t {1 2 3}} {}}
+}
+
+do_invert_test 2.2 {
+ DELETE FROM sqlite_stat1;
+} {
+ {INSERT sqlite_stat1 0 XX. {} {t t1 t sqlite_autoindex_t1_1 t {6 1}}}
+ {INSERT sqlite_stat1 0 XX. {} {t t2 t sqlite_autoindex_t2_1 t {6 1 1}}}
+}
+
+do_invert_test 2.3 {
+ UPDATE sqlite_stat1 SET stat = 'hello world';
+}
+
+do_test 3.0 {
+ forcecopy test.db test.db2
+ sqlite3 db2 test.db2
+ set P [patchset_from_sql {
+ INSERT INTO t2 VALUES(1, 2, 3);
+ DELETE FROM t2 WHERE d = 3;
+ }]
+
+ list [catch { sqlite3changeset_apply_v2 -invert db2 $P {} } msg] $msg
+} {1 SQLITE_CORRUPT}
+
+do_test 3.1 {
+ list [catch { sqlite3session_foreach -invert db2 $P {} } msg] $msg
+} {1 SQLITE_CORRUPT}
+
+do_test 3.2 {
+ sqlite3changeset_apply_v2 db2 $P {}
+ compare_db db db2
+} {}
+
+#-------------------------------------------------------------------------
+#
+reset_db
+do_execsql_test 4.0 {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b UNIQUE);
+ INSERT INTO t1 VALUES(1, 'one');
+ INSERT INTO t1 VALUES(2, 'two');
+ INSERT INTO t1 VALUES(3, 'three');
+ INSERT INTO t1 VALUES(4, 'four');
+}
+
+do_invert_test 4.1 {
+ DELETE FROM t1;
+ INSERT INTO t1 VALUES(1, 'two');
+ INSERT INTO t1 VALUES(2, 'five');
+ INSERT INTO t1 VALUES(3, 'one');
+ INSERT INTO t1 VALUES(4, 'three');
+} {
+ {UPDATE t1 0 X. {i 1 t two} {{} {} t one}}
+ {UPDATE t1 0 X. {i 2 t five} {{} {} t two}}
+ {UPDATE t1 0 X. {i 3 t one} {{} {} t three}}
+ {UPDATE t1 0 X. {i 4 t three} {{} {} t four}}
+}
+
+
+finish_test
diff --git a/ext/session/sessionmem.test b/ext/session/sessionmem.test
new file mode 100644
index 0000000..75b4862
--- /dev/null
+++ b/ext/session/sessionmem.test
@@ -0,0 +1,57 @@
+# 2020 December 23
+#
+# 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 SQLite sessions module
+# Specifically, for the sqlite3session_memory_used() API.
+#
+
+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 sessionmem
+
+do_execsql_test 1.0 {
+ CREATE TABLE t1(i INTEGER PRIMARY KEY, x, y);
+ CREATE TABLE t2(i INTEGER, x, y, PRIMARY KEY(x, y));
+}
+
+do_test 1.1 {
+ sqlite3session S db main
+ S attach *
+} {}
+
+foreach {tn sql eRes} {
+ 1 { INSERT INTO t1 VALUES(1, 2, 3) } 1
+ 2 { UPDATE t1 SET x=5 } 0
+ 3 { UPDATE t1 SET i=5 } 1
+ 4 { DELETE FROM t1 } 0
+ 5 { INSERT INTO t1 VALUES(1, 2, 3) } 0
+ 6 { INSERT INTO t1 VALUES(5, 2, 3) } 0
+ 7 { INSERT INTO t2 VALUES('a', 'b', 'c') } 1
+ 8 { INSERT INTO t2 VALUES('d', 'e', 'f') } 1
+ 9 { UPDATE t2 SET i='e' } 0
+} {
+ set mem1 [S memory_used]
+ do_test 1.2.$tn.(mu=$mem1) {
+ execsql $sql
+ set mem2 [S memory_used]
+ expr {$mem2 > $mem1}
+ } $eRes
+}
+
+do_test 1.3 {
+ S delete
+} {}
+
+finish_test
diff --git a/ext/session/sessionnoop.test b/ext/session/sessionnoop.test
new file mode 100644
index 0000000..16c60b7
--- /dev/null
+++ b/ext/session/sessionnoop.test
@@ -0,0 +1,187 @@
+# 2021 Februar 20
+#
+# 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 sessionnoop
+
+#-------------------------------------------------------------------------
+# Test plan:
+#
+# 1.*: Test that concatenating changesets cannot produce a noop UPDATE.
+# 2.*: Test that rebasing changesets cannot produce a noop UPDATE.
+# 3.*: Test that sqlite3changeset_apply() ignores noop UPDATE changes.
+#
+
+do_execsql_test 1.0 {
+ CREATE TABLE t1(a PRIMARY KEY, b, c, d);
+ INSERT INTO t1 VALUES(1, 1, 1, 1);
+ INSERT INTO t1 VALUES(2, 2, 2, 2);
+ INSERT INTO t1 VALUES(3, 3, 3, 3);
+}
+
+proc do_concat_test {tn sql1 sql2 res} {
+ uplevel [list do_test $tn [subst -nocommands {
+ set C1 [changeset_from_sql {$sql1}]
+ set C2 [changeset_from_sql {$sql2}]
+ set C3 [sqlite3changeset_concat [set C1] [set C2]]
+ set got [list]
+ sqlite3session_foreach elem [set C3] { lappend got [set elem] }
+ set got
+ }] [list {*}$res]]
+}
+
+do_concat_test 1.1 {
+ UPDATE t1 SET c=c+1;
+} {
+ UPDATE t1 SET c=c-1;
+} {
+}
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 2.0 {
+ CREATE TABLE t1(a PRIMARY KEY, b, c);
+ INSERT INTO t1 VALUES(1, 1, 1);
+ INSERT INTO t1 VALUES(2, 2, 2);
+ INSERT INTO t1 VALUES(3, 3, 3);
+}
+
+proc do_rebase_test {tn sql_local sql_remote conflict_res expected} {
+ proc xConflict {args} [list return $conflict_res]
+
+ uplevel [list \
+ do_test $tn [subst -nocommands {
+ execsql BEGIN
+ set c_remote [changeset_from_sql {$sql_remote}]
+ execsql ROLLBACK
+
+ execsql BEGIN
+ set c_local [changeset_from_sql {$sql_local}]
+ set base [sqlite3changeset_apply_v2 db [set c_remote] xConflict]
+ execsql ROLLBACK
+
+ sqlite3rebaser_create R
+ R config [set base]
+ set res [list]
+ sqlite3session_foreach elem [R rebase [set c_local]] {
+ lappend res [set elem]
+ }
+ R delete
+ set res
+ }] [list {*}$expected]
+ ]
+}
+
+do_rebase_test 2.1 {
+ UPDATE t1 SET c=2 WHERE a=1; -- local
+} {
+ UPDATE t1 SET c=3 WHERE a=1; -- remote
+} OMIT {
+ {UPDATE t1 0 X.. {i 1 {} {} i 3} {{} {} {} {} i 2}}
+}
+
+do_rebase_test 2.2 {
+ UPDATE t1 SET c=2 WHERE a=1; -- local
+} {
+ UPDATE t1 SET c=3 WHERE a=1; -- remote
+} REPLACE {
+}
+
+do_rebase_test 2.3.1 {
+ UPDATE t1 SET c=4 WHERE a=1; -- local
+} {
+ UPDATE t1 SET c=4 WHERE a=1 -- remote
+} OMIT {
+ {UPDATE t1 0 X.. {i 1 {} {} i 4} {{} {} {} {} i 4}}
+}
+
+do_rebase_test 2.3.2 {
+ UPDATE t1 SET c=5 WHERE a=1; -- local
+} {
+ UPDATE t1 SET c=5 WHERE a=1 -- remote
+} REPLACE {
+}
+
+#-------------------------------------------------------------------------
+#
+reset_db
+do_execsql_test 3.0 {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c);
+ INSERT INTO t1 VALUES(1, 1, 1);
+ INSERT INTO t1 VALUES(2, 2, 2);
+ INSERT INTO t1 VALUES(3, 3, 3);
+ INSERT INTO t1 VALUES(4, 4, 4);
+}
+
+# Arg $pkstr contains one character for each column in the table. An
+# "X" for PK column, or a "." for a non-PK.
+#
+proc mk_tbl_header {name pkstr} {
+ set ret [binary format H2c 54 [string length $pkstr]]
+ foreach i [split $pkstr {}] {
+ if {$i=="X"} {
+ append ret [binary format H2 01]
+ } else {
+ if {$i!="."} {error "bad pkstr: $pkstr ($i)"}
+ append ret [binary format H2 00]
+ }
+ }
+ append ret $name
+ append ret [binary format H2 00]
+ set ret
+}
+
+proc mk_update_change {args} {
+ set ret [binary format H2H2 17 00]
+ foreach a $args {
+ if {$a==""} {
+ append ret [binary format H2 00]
+ } else {
+ append ret [binary format H2W 01 $a]
+ }
+ }
+ set ret
+}
+
+proc xConflict {args} { return "ABORT" }
+do_test 3.1 {
+ set C [mk_tbl_header t1 X..]
+ append C [mk_update_change 1 {} 1 {} {} 500]
+ append C [mk_update_change 2 {} {} {} {} {}]
+ append C [mk_update_change 3 3 {} {} 600 {}]
+ append C [mk_update_change 4 {} {} {} {} {}]
+
+ sqlite3changeset_apply_v2 db $C xConflict
+} {}
+do_execsql_test 3.2 {
+ SELECT * FROM t1
+} {
+ 1 1 500
+ 2 2 2
+ 3 600 3
+ 4 4 4
+}
+
+
+
+
+
+
+finish_test
+
diff --git a/ext/session/sessionrebase.test b/ext/session/sessionrebase.test
new file mode 100644
index 0000000..cdf3322
--- /dev/null
+++ b/ext/session/sessionrebase.test
@@ -0,0 +1,476 @@
+# 2018 March 14
+#
+# 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 sessionrebase
+
+set ::lConflict [list]
+proc xConflict {args} {
+ set res [lindex $::lConflict 0]
+ set ::lConflict [lrange $::lConflict 1 end]
+ return $res
+}
+
+#-------------------------------------------------------------------------
+# The following test cases - 1.* - test that the rebase blobs output by
+# sqlite3_changeset_apply_v2 look correct in some simple cases. The blob
+# is itself a changeset, containing records determined as follows:
+#
+# * For each conflict resolved with REPLACE, the rebase blob contains
+# a DELETE record. All fields other than the PK fields are undefined.
+#
+# * For each conflict resolved with OMIT, the rebase blob contains an
+# INSERT record. For an INSERT or UPDATE operation, the indirect flag
+# is clear and all updated fields are defined. For a DELETE operation,
+# the indirect flag is set and all non-PK fields left undefined.
+#
+proc do_apply_v2_test {tn sql modsql conflict_handler res} {
+
+ execsql BEGIN
+ sqlite3session S db main
+ S attach *
+ execsql $sql
+ set changeset [S changeset]
+ S delete
+ execsql ROLLBACK
+
+ execsql BEGIN
+ execsql $modsql
+ set ::lConflict $conflict_handler
+ set blob [sqlite3changeset_apply_v2 db $changeset xConflict]
+ execsql ROLLBACK
+
+ uplevel [list do_test $tn [list changeset_to_list $blob] [list {*}$res]]
+}
+
+
+set ::lConflict [list]
+proc xConflict {args} {
+ set res [lindex $::lConflict 0]
+ set ::lConflict [lrange $::lConflict 1 end]
+ return $res
+}
+
+# Take a copy of database test.db in file test.db2. Execute $sql1
+# against test.db and $sql2 against test.db2. Capture a changeset
+# for each. Then send the test.db2 changeset to test.db and apply
+# it with the conflict handlers in $conflict_handler. Patch the
+# test.db changeset and then execute it against test.db2. Test that
+# the two databases come out the same.
+#
+proc do_rebase_test {tn sql1 sql2 conflict_handler {testsql ""} {testres ""}} {
+
+ for {set i 1} {$i <= 2} {incr i} {
+ forcedelete test.db2 test.db2-journal test.db2-wal
+ forcecopy test.db test.db2
+ sqlite3 db2 test.db2
+
+ db eval BEGIN
+
+ sqlite3session S1 db main
+ S1 attach *
+ execsql $sql1 db
+ set c1 [S1 changeset]
+ S1 delete
+
+ if {$i==1} {
+ sqlite3session S2 db2 main
+ S2 attach *
+ execsql $sql2 db2
+ set c2 [S2 changeset]
+ S2 delete
+ } else {
+ set c2 [list]
+ foreach sql [split $sql2 ";"] {
+ if {[string is space $sql]} continue
+ sqlite3session S2 db2 main
+ S2 attach *
+ execsql $sql db2
+ lappend c2 [S2 changeset]
+ S2 delete
+ }
+ }
+
+ set ::lConflict $conflict_handler
+ set rebase [list]
+ if {$i==1} {
+ lappend rebase [sqlite3changeset_apply_v2 db $c2 xConflict]
+ } else {
+ foreach c $c2 {
+#puts "apply_v2: [changeset_to_list $c]"
+ lappend rebase [sqlite3changeset_apply_v2 db $c xConflict]
+ }
+ #puts "llength: [llength $rebase]"
+ }
+ #if {$tn=="2.1.4"} { puts [changeset_to_list $rebase] ; breakpoint }
+ #puts [changeset_to_list [lindex $rebase 0]] ; breakpoint
+ #puts [llength $rebase]
+
+ sqlite3rebaser_create R
+ foreach r $rebase {
+#puts [changeset_to_list $r]
+ R configure $r
+ }
+ set c1r [R rebase $c1]
+ R delete
+ #if {$tn=="2.1.4"} { puts [changeset_to_list $c1r] }
+
+ sqlite3changeset_apply_v2 db2 $c1r xConflictAbort
+
+ if {[string range $tn end end]!="*"} {
+ uplevel [list do_test $tn.$i.1 [list compare_db db db2] {}]
+ }
+ db2 close
+
+ if {$testsql!=""} {
+ uplevel [list do_execsql_test $tn.$i.2 $testsql $testres]
+ }
+
+ db eval ROLLBACK
+ }
+}
+
+do_execsql_test 1.0 {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b);
+ INSERT INTO t1 VALUES(1, 'value A');
+}
+
+do_apply_v2_test 1.1.1 {
+ UPDATE t1 SET b = 'value B' WHERE a=1;
+} {
+ UPDATE t1 SET b = 'value C' WHERE a=1;
+} {
+ OMIT
+} {
+ {INSERT t1 0 X. {} {i 1 t {value B}}}
+}
+
+do_apply_v2_test 1.1.2 {
+ UPDATE t1 SET b = 'value B' WHERE a=1;
+} {
+ UPDATE t1 SET b = 'value C' WHERE a=1;
+} {
+ REPLACE
+} {
+ {INSERT t1 1 X. {} {i 1 t {value B}}}
+}
+
+do_apply_v2_test 1.2.1 {
+ INSERT INTO t1 VALUES(2, 'first');
+} {
+ INSERT INTO t1 VALUES(2, 'second');
+} {
+ OMIT
+} {
+ {INSERT t1 0 X. {} {i 2 t first}}
+}
+do_apply_v2_test 1.2.2 {
+ INSERT INTO t1 VALUES(2, 'first');
+} {
+ INSERT INTO t1 VALUES(2, 'second');
+} {
+ REPLACE
+} {
+ {INSERT t1 1 X. {} {i 2 t first}}
+}
+
+do_apply_v2_test 1.3.1 {
+ DELETE FROM t1 WHERE a=1;
+} {
+ UPDATE t1 SET b='value D' WHERE a=1;
+} {
+ OMIT
+} {
+ {DELETE t1 0 X. {i 1 t {value A}} {}}
+}
+do_apply_v2_test 1.3.2 {
+ DELETE FROM t1 WHERE a=1;
+} {
+ UPDATE t1 SET b='value D' WHERE a=1;
+} {
+ REPLACE
+} {
+ {DELETE t1 1 X. {i 1 t {value A}} {}}
+}
+
+#-------------------------------------------------------------------------
+# Test cases 2.* - simple tests of rebasing actual changesets.
+#
+# 2.1.1 - 1u2u1r
+# 2.1.2 - 1u2u2r
+# 2.1.3 - 1d2d
+# 2.1.4 - 1d2u1r
+# 2.1.5 - 1d2u2r !!
+# 2.1.6 - 1u2d1r
+# 2.1.7 - 1u2d2r
+#
+# 2.1.8 - 1i2i2r
+# 2.1.9 - 1i2i1r
+#
+
+proc xConflictAbort {args} {
+ return "ABORT"
+}
+
+reset_db
+do_execsql_test 2.1.0 {
+ CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT);
+ INSERT INTO t1 VALUES(1, 'one');
+ INSERT INTO t1 VALUES(2, 'two');
+ INSERT INTO t1 VALUES(3, 'three');
+}
+do_rebase_test 2.1.1 {
+ UPDATE t1 SET b = 'two.1' WHERE a=2
+} {
+ UPDATE t1 SET b = 'two.2' WHERE a=2;
+} {
+ OMIT
+} { SELECT * FROM t1 } {1 one 2 two.1 3 three}
+
+do_rebase_test 2.1.2 {
+ UPDATE t1 SET b = 'two.1' WHERE a=2
+} {
+ UPDATE t1 SET b = 'two.2' WHERE a=2;
+} {
+ REPLACE
+} { SELECT * FROM t1 } {1 one 2 two.2 3 three}
+
+do_rebase_test 2.1.3 {
+ DELETE FROM t1 WHERE a=3
+} {
+ DELETE FROM t1 WHERE a=3;
+} {
+ OMIT
+} { SELECT * FROM t1 } {1 one 2 two}
+
+do_rebase_test 2.1.4 {
+ DELETE FROM t1 WHERE a=1
+} {
+ UPDATE t1 SET b='one.2' WHERE a=1
+} {
+ OMIT
+} { SELECT * FROM t1 } {2 two 3 three}
+
+#do_rebase_test 2.1.5 {
+# DELETE FROM t1 WHERE a=1;
+#} {
+# UPDATE t1 SET b='one.2' WHERE a=1
+#} {
+# REPLACE
+#} { SELECT * FROM t1 } {2 two 3 three}
+
+do_rebase_test 2.1.6 {
+ UPDATE t1 SET b='three.1' WHERE a=3
+} {
+ DELETE FROM t1 WHERE a=3;
+} {
+ OMIT
+} { SELECT * FROM t1 } {1 one 2 two 3 three.1}
+
+do_rebase_test 2.1.7 {
+ UPDATE t1 SET b='three.1' WHERE a=3
+} {
+ DELETE FROM t1 WHERE a=3;
+} {
+ REPLACE
+} { SELECT * FROM t1 } {1 one 2 two}
+
+do_rebase_test 2.1.8 {
+ INSERT INTO t1 VALUES(4, 'four.1')
+} {
+ INSERT INTO t1 VALUES(4, 'four.2');
+} {
+ REPLACE
+} { SELECT * FROM t1 } {1 one 2 two 3 three 4 four.2}
+
+do_rebase_test 2.1.9 {
+ INSERT INTO t1 VALUES(4, 'four.1')
+} {
+ INSERT INTO t1 VALUES(4, 'four.2');
+} {
+ OMIT
+} { SELECT * FROM t1 } {1 one 2 two 3 three 4 four.1}
+
+do_execsql_test 2.2.0 {
+ CREATE TABLE t2(x, y, z PRIMARY KEY);
+ INSERT INTO t2 VALUES('i', 'a', 'A');
+ INSERT INTO t2 VALUES('ii', 'b', 'B');
+ INSERT INTO t2 VALUES('iii', 'c', 'C');
+
+ CREATE TABLE t3(a INTEGER PRIMARY KEY, b, c);
+ INSERT INTO t3 VALUES(-1, 'z', 'Z');
+ INSERT INTO t3 VALUES(-2, 'y', 'Y');
+}
+
+do_rebase_test 2.2.1 {
+ UPDATE t2 SET x=1 WHERE z='A'
+} {
+ UPDATE t2 SET y='one' WHERE z='A';
+} {
+} { SELECT * FROM t2 WHERE z='A' } { 1 one A }
+
+do_rebase_test 2.2.2 {
+ UPDATE t2 SET x=1, y='one' WHERE z='B'
+} {
+ UPDATE t2 SET y='two' WHERE z='B';
+} {
+ REPLACE
+} { SELECT * FROM t2 WHERE z='B' } { 1 two B }
+
+do_rebase_test 2.2.3 {
+ UPDATE t2 SET x=1, y='one' WHERE z='B'
+} {
+ UPDATE t2 SET y='two' WHERE z='B';
+} {
+ OMIT
+} { SELECT * FROM t2 WHERE z='B' } { 1 one B }
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 3.0 {
+ CREATE TABLE t3(a, b, c, PRIMARY KEY(b, c));
+ CREATE TABLE abcdefghijkl(x PRIMARY KEY, y, z);
+
+ INSERT INTO t3 VALUES(1, 2, 3);
+ INSERT INTO t3 VALUES(4, 2, 5);
+ INSERT INTO t3 VALUES(7, 2, 9);
+
+ INSERT INTO abcdefghijkl VALUES('a', 'b', 'c');
+ INSERT INTO abcdefghijkl VALUES('d', 'e', 'f');
+ INSERT INTO abcdefghijkl VALUES('g', 'h', 'i');
+}
+
+breakpoint
+# do_rebase_test 3.6.tn {
+# UPDATE abcdefghijkl SET z='X', y='X' WHERE x='d';
+# } {
+# UPDATE abcdefghijkl SET y=1 WHERE x='d';
+# UPDATE abcdefghijkl SET z=1 WHERE x='d';
+# } [list REPLACE REPLACE REPLACE]
+
+foreach {tn p} {
+ 1 OMIT 2 REPLACE
+} {
+ do_rebase_test 3.1.$tn {
+ INSERT INTO t3 VALUES(1, 1, 1);
+ UPDATE abcdefghijkl SET y=2;
+ } {
+ INSERT INTO t3 VALUES(4, 1, 1);
+ DELETE FROM abcdefghijkl;
+ } [list $p $p $p $p $p $p $p $p]
+
+ do_rebase_test 3.2.$tn {
+ INSERT INTO abcdefghijkl SELECT * FROM t3;
+ UPDATE t3 SET b=b+1;
+ } {
+ INSERT INTO t3 VALUES(3, 3, 3);
+ INSERT INTO abcdefghijkl SELECT * FROM t3;
+ } [list $p $p $p $p $p $p $p $p]
+
+ do_rebase_test 3.3.$tn {
+ INSERT INTO abcdefghijkl VALUES(22, 23, 24);
+ } {
+ INSERT INTO abcdefghijkl VALUES(22, 25, 26);
+ UPDATE abcdefghijkl SET y=400 WHERE x=22;
+ } [list $p $p $p $p $p $p $p $p]
+
+ do_rebase_test 3.4.$tn {
+ INSERT INTO abcdefghijkl VALUES(22, 23, 24);
+ } {
+ INSERT INTO abcdefghijkl VALUES(22, 25, 26);
+ UPDATE abcdefghijkl SET y=400 WHERE x=22;
+ } [list REPLACE $p]
+
+ do_rebase_test 3.5.$tn* {
+ UPDATE abcdefghijkl SET y='X' WHERE x='d';
+ } {
+ DELETE FROM abcdefghijkl WHERE x='d';
+ INSERT INTO abcdefghijkl VALUES('d', NULL, NULL);
+ } [list $p $p $p]
+ do_rebase_test 3.5.$tn {
+ UPDATE abcdefghijkl SET y='X' WHERE x='d';
+ } {
+ DELETE FROM abcdefghijkl WHERE x='d';
+ INSERT INTO abcdefghijkl VALUES('d', NULL, NULL);
+ } [list REPLACE $p $p]
+
+ do_rebase_test 3.6.$tn {
+ UPDATE abcdefghijkl SET z='X', y='X' WHERE x='d';
+ } {
+ UPDATE abcdefghijkl SET y=1 WHERE x='d';
+ UPDATE abcdefghijkl SET z=1 WHERE x='d';
+ } [list REPLACE $p $p]
+}
+
+#-------------------------------------------------------------------------
+# Check that apply_v2() does not create a rebase buffer for a patchset.
+# And that it is not possible to rebase a patchset.
+#
+do_execsql_test 4.0 {
+ CREATE TABLE t5(o PRIMARY KEY, p, q);
+ INSERT INTO t5 VALUES(1, 2, 3);
+ INSERT INTO t5 VALUES(4, 5, 6);
+}
+foreach {tn cmd rebasable} {
+ 1 patchset 0
+ 2 changeset 1
+} {
+ proc xConflict {args} { return "OMIT" }
+ do_test 4.1.$tn {
+ execsql {
+ BEGIN;
+ DELETE FROM t5 WHERE o=4;
+ }
+
+ sqlite3session S db main
+ S attach *
+ execsql {
+ INSERT INTO t5 VALUES(4, 'five', 'six');
+ }
+ set P [S $cmd]
+ S delete
+
+ execsql ROLLBACK;
+
+ set ::rebase [sqlite3changeset_apply_v2 db $P xConflict]
+ expr [llength $::rebase]>0
+ } $rebasable
+}
+
+foreach {tn cmd rebasable} {
+ 1 patchset 0
+ 2 changeset 1
+} {
+ do_test 4.2.$tn {
+ sqlite3session S db main
+ S attach *
+ execsql {
+ INSERT INTO t5 VALUES(5+$tn, 'five', 'six');
+ }
+ set P [S $cmd]
+ S delete
+
+ sqlite3rebaser_create R
+ R configure $::rebase
+ expr [catch {R rebase $P}]==0
+ } $rebasable
+
+ catch { R delete }
+}
+finish_test
diff --git a/ext/session/sessionsize.test b/ext/session/sessionsize.test
new file mode 100644
index 0000000..04d0551
--- /dev/null
+++ b/ext/session/sessionsize.test
@@ -0,0 +1,131 @@
+# 2021 April 22
+#
+# 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 sessionsize
+
+proc do_changeset_size_test {tn sql} {
+ sqlite3session S db main
+ S attach *
+ db eval $sql
+
+ set sz [S changeset_size]
+ set C [S changeset]
+ set szC [string length $C]
+ S delete
+
+ do_test $tn "expr $sz" $szC
+}
+
+do_execsql_test 1.0 {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c);
+ INSERT INTO t1 VALUES(1, 'abc', 'def');
+ INSERT INTO t1 VALUES(2, 'ghi', 'jkl');
+}
+
+do_changeset_size_test 1.1 {
+ INSERT INTO t1 VALUES(3, 'hello', 'world');
+}
+
+do_changeset_size_test 1.2 {
+ DELETE FROM t1 WHERE a=2;
+}
+
+do_changeset_size_test 1.3 {
+ DELETE FROM t1 WHERE a=3;
+ INSERT INTO t1 VALUES(3, 1, 2);
+}
+
+do_changeset_size_test 1.4 {
+ UPDATE t1 SET c='hello world' WHERE a=3;
+}
+
+#-------------------------------------------------------------------------
+
+do_execsql_test 2.0 {
+ CREATE TABlE t2(a, b, c, d, PRIMARY KEY(a, b)) WITHOUT ROWID;
+ CREATE TABlE t3(a, b, c, d PRIMARY KEY);
+}
+
+do_changeset_size_test 2.1 {
+ WITH s(i) AS (
+ SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<50
+ )
+ INSERT INTO t2 SELECT i, i+1, i+2, i+3 FROM s;
+
+ UPDATE t2 SET c=randomblob(a) WHERE a>10
+}
+
+do_changeset_size_test 2.2 {
+ DELETE FROM t2 WHERE a=1;
+ INSERT INTO t2 VALUES(1, 4, 3, 4);
+}
+
+do_changeset_size_test 2.2 {
+ UPDATE t2 SET b=4 WHERE a=2
+}
+
+do_changeset_size_test 2.3 {
+ INSERT INTO t2 VALUES('a', 'b', 'c', 'd');
+ UPDATE t2 SET c='qwertyuiop' WHERE a='a';
+}
+
+do_changeset_size_test 2.4 {
+ DELETE FROM t2 WHERE a='a';
+ INSERT INTO t2 VALUES('a', 'b', 'c', 'd');
+}
+
+do_changeset_size_test 2.5 {
+ UPDATE t2 SET a='aa', b='bb' WHERE (a, b) = ('a', 'b');
+}
+
+do_changeset_size_test 2.6 {
+ UPDATE t2 SET a='a', b='b' WHERE (a, b) = ('aa', 'bb');
+}
+
+do_changeset_size_test 2.7 {
+ INSERT INTO t3 DEFAULT VALUES;
+ INSERT INTO t3 VALUES(1,2,3,4);
+}
+
+#-------------------------------------------------------------------------
+reset_db
+
+do_execsql_test 3.0 {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b);
+}
+
+do_test 3.1 {
+ sqlite3session S db main
+ S object_config_size -1
+} 1
+
+do_test 3.2.1 { S object_config_size 0 } 0
+do_test 3.2.2 { S object_config_size -1 } 0
+do_test 3.2.3 { S object_config_size 1 } 1
+do_test 3.2.4 { S object_config_size -1 } 1
+
+do_test 3.3 { S attach t1 } {}
+do_test 3.4 { S object_config_size 1 } {SQLITE_MISUSE}
+do_test 3.4 { S object_config_size -1 } {1}
+
+S delete
+
+finish_test
+
diff --git a/ext/session/sessionstat1.test b/ext/session/sessionstat1.test
new file mode 100644
index 0000000..774899d
--- /dev/null
+++ b/ext/session/sessionstat1.test
@@ -0,0 +1,310 @@
+# 2018 January 12
+#
+# 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 sessionstat1
+
+do_execsql_test 1.0 {
+ CREATE TABLE t1(a PRIMARY KEY, b, c);
+ CREATE INDEX t1b ON t1(b);
+ CREATE INDEX t1c ON t1(c);
+
+ WITH s(i) AS (
+ SELECT 0 UNION ALL SELECT i+1 FROM s WHERE (i+1)<32
+ )
+ INSERT INTO t1 SELECT i, i%8, i%2 FROM s;
+}
+
+do_iterator_test 1.1 {} {
+ ANALYZE
+} {
+ {INSERT sqlite_stat1 0 XX. {} {t t1 t sqlite_autoindex_t1_1 t {32 1}}}
+ {INSERT sqlite_stat1 0 XX. {} {t t1 t t1b t {32 4}}}
+ {INSERT sqlite_stat1 0 XX. {} {t t1 t t1c t {32 16}}}
+}
+
+do_execsql_test 1.2 {
+ WITH s(i) AS (
+ SELECT 32 UNION ALL SELECT i+1 FROM s WHERE (i+1)<64
+ )
+ INSERT INTO t1 SELECT i, i%8, i%2 FROM s;
+}
+
+do_iterator_test 1.3 {} {
+ ANALYZE
+} {
+ {UPDATE sqlite_stat1 0 XX. {t t1 t sqlite_autoindex_t1_1 t {32 1}} {{} {} {} {} t {64 1}}}
+ {UPDATE sqlite_stat1 0 XX. {t t1 t t1b t {32 4}} {{} {} {} {} t {64 8}}}
+ {UPDATE sqlite_stat1 0 XX. {t t1 t t1c t {32 16}} {{} {} {} {} t {64 32}}}
+}
+
+do_iterator_test 1.5 {} {
+ DROP INDEX t1b;
+} {
+ {DELETE sqlite_stat1 0 XX. {t t1 t t1b t {64 8}} {}}
+}
+
+do_iterator_test 1.6 {} {
+ DROP TABLE t1;
+} {
+ {DELETE sqlite_stat1 0 XX. {t t1 t sqlite_autoindex_t1_1 t {64 1}} {}}
+ {DELETE sqlite_stat1 0 XX. {t t1 t t1c t {64 32}} {}}
+}
+
+#-------------------------------------------------------------------------
+#
+catch { db2 close }
+forcedelete test.db2
+sqlite3 db2 test.db2
+
+do_test 2.0 {
+ do_common_sql {
+ CREATE TABLE t1(a PRIMARY KEY, b, c);
+ CREATE INDEX t1b ON t1(b);
+ CREATE INDEX t1c ON t1(c);
+ ANALYZE;
+ }
+} {}
+
+do_test 2.1 {
+ do_then_apply_sql {
+ WITH s(i) AS (
+ SELECT 0 UNION ALL SELECT i+1 FROM s WHERE (i+1)<32
+ )
+ INSERT INTO t1 SELECT i, i%8, i%2 FROM s;
+ ANALYZE;
+ }
+} {}
+
+do_execsql_test -db db2 2.2 {
+ SELECT * FROM sqlite_stat1
+} {
+ t1 sqlite_autoindex_t1_1 {32 1}
+ t1 t1b {32 4}
+ t1 t1c {32 16}
+}
+
+do_test 2.3 {
+ do_then_apply_sql { DROP INDEX t1c }
+} {}
+
+do_execsql_test -db db2 2.4 {
+ SELECT * FROM sqlite_stat1
+} {
+ t1 sqlite_autoindex_t1_1 {32 1}
+ t1 t1b {32 4}
+}
+
+do_test 2.3 {
+ do_then_apply_sql { DROP TABLE t1 }
+} {}
+
+do_execsql_test -db db2 2.4 {
+ SELECT * FROM sqlite_stat1
+} {
+}
+
+do_execsql_test -db db2 2.5 { SELECT count(*) FROM t1 } 32
+
+#-------------------------------------------------------------------------
+db2 close
+forcedelete test.db2
+reset_db
+sqlite3 db2 test.db2
+
+do_test 3.0 {
+ do_common_sql {
+ CREATE TABLE t1(a, b, c);
+ ANALYZE;
+ DELETE FROM sqlite_stat1;
+ }
+ execsql {
+ INSERT INTO t1 VALUES(1, 1, 1);
+ INSERT INTO t1 VALUES(2, 2, 2);
+ INSERT INTO t1 VALUES(3, 3, 3);
+ INSERT INTO t1 VALUES(4, 4, 4);
+ }
+} {}
+
+do_iterator_test 3.1 {} {
+ ANALYZE
+} {
+ {INSERT sqlite_stat1 0 XX. {} {t t1 b {} t 4}}
+}
+db null null
+db2 null null
+do_execsql_test 3.2 {
+ SELECT * FROM sqlite_stat1;
+} {t1 null 4}
+do_test 3.3 {
+ execsql { DELETE FROM sqlite_stat1 }
+ do_then_apply_sql { ANALYZE }
+ execsql { SELECT * FROM sqlite_stat1 } db2
+} {t1 null 4}
+do_test 3.4 {
+ execsql { INSERT INTO t1 VALUES(5,5,5) }
+ do_then_apply_sql { ANALYZE }
+ execsql { SELECT * FROM sqlite_stat1 } db2
+} {t1 null 5}
+do_test 3.5 {
+ do_then_apply_sql { DROP TABLE t1 }
+ execsql { SELECT * FROM sqlite_stat1 } db2
+} {}
+
+do_test 3.6.1 {
+ execsql {
+ CREATE TABLE t1(a, b, c);
+ CREATE TABLE t2(x, y, z);
+ INSERT INTO t1 VALUES(1,1,1), (2,2,2), (3,3,3), (4,4,4), (5,5,5);
+ INSERT INTO t2 SELECT * FROM t1;
+ DELETE FROM sqlite_stat1;
+ }
+ sqlite3session S db main
+ S attach sqlite_stat1
+ execsql { ANALYZE }
+} {}
+do_changeset_test 3.6.2 S {
+ {INSERT sqlite_stat1 0 XX. {} {t t2 b {} t 5}}
+ {INSERT sqlite_stat1 0 XX. {} {t t1 b {} t 5}}
+}
+do_changeset_invert_test 3.6.3 S {
+ {DELETE sqlite_stat1 0 XX. {t t2 b {} t 5} {}}
+ {DELETE sqlite_stat1 0 XX. {t t1 b {} t 5} {}}
+}
+do_test 3.6.4 { S delete } {}
+
+proc sql_changeset_concat {args} {
+ foreach sql $args {
+ sqlite3session S db main
+ S attach sqlite_stat1
+ execsql $sql
+ set change [S changeset]
+ S delete
+
+ if {[info vars ret]!=""} {
+ set ret [sqlite3changeset_concat $ret $change]
+ } else {
+ set ret $change
+ }
+ }
+
+ changeset_to_list $ret
+}
+
+proc do_scc_test {tn args} {
+ uplevel [list \
+ do_test $tn [concat sql_changeset_concat [lrange $args 0 end-1]] \
+ [list {*}[ lindex $args end ]]
+ ]
+}
+
+do_execsql_test 3.7.0 {
+ DELETE FROM sqlite_stat1;
+}
+do_scc_test 3.7.1 {
+ ANALYZE;
+} {
+ INSERT INTO t2 VALUES(6,6,6);
+ ANALYZE;
+} {
+ {INSERT sqlite_stat1 0 XX. {} {t t1 b {} t 5}}
+ {INSERT sqlite_stat1 0 XX. {} {t t2 b {} t 6}}
+}
+
+#-------------------------------------------------------------------------
+catch { db2 close }
+reset_db
+forcedelete test.db2
+sqlite3 db2 test.db2
+
+do_test 4.1.0 {
+ do_common_sql {
+ CREATE TABLE t1(a, b);
+ CREATE INDEX i1 ON t1(a);
+ CREATE INDEX i2 ON t1(b);
+ INSERT INTO t1 VALUES(1,1), (2,2);
+ ANALYZE;
+ }
+ execsql { DELETE FROM sqlite_stat1 }
+} {}
+
+do_test 4.1.1 {
+ execsql { INSERT INTO t1 VALUES(3,3); }
+ set C [changeset_from_sql {ANALYZE}]
+ set ::c [list]
+ proc xConflict {args} {
+ lappend ::c $args
+ return "OMIT"
+ }
+ sqlite3changeset_apply db2 $C xConflict
+ set ::c
+} [list {*}{
+ {INSERT sqlite_stat1 CONFLICT {t t1 t i1 t {3 1}} {t t1 t i1 t {2 1}}}
+ {INSERT sqlite_stat1 CONFLICT {t t1 t i2 t {3 1}} {t t1 t i2 t {2 1}}}
+}]
+
+do_execsql_test -db db2 4.1.2 {
+ SELECT * FROM sqlite_stat1 ORDER BY 1,2;
+} {t1 i1 {2 1} t1 i2 {2 1}}
+
+do_test 4.1.3 {
+ proc xConflict {args} {
+ return "REPLACE"
+ }
+ sqlite3changeset_apply db2 $C xConflict
+ execsql { SELECT * FROM sqlite_stat1 ORDER BY 1,2 } db2
+} {t1 i1 {3 1} t1 i2 {3 1}}
+
+do_test 4.2.0 {
+ do_common_sql {
+ DROP TABLE t1;
+ CREATE TABLE t3(x,y);
+ INSERT INTO t3 VALUES('a','a');
+ INSERT INTO t3 VALUES('b','b');
+ ANALYZE;
+ }
+ execsql { DELETE FROM sqlite_stat1 }
+} {}
+do_test 4.2.1 {
+ execsql { INSERT INTO t3 VALUES('c','c'); }
+ set C [changeset_from_sql {ANALYZE}]
+ set ::c [list]
+ proc xConflict {args} {
+ lappend ::c $args
+ return "OMIT"
+ }
+ sqlite3changeset_apply db2 $C xConflict
+ set ::c
+} [list {*}{
+ {INSERT sqlite_stat1 CONFLICT {t t3 b {} t 3} {t t3 b {} t 2}}
+}]
+
+db2 null null
+do_execsql_test -db db2 4.2.2 {
+ SELECT * FROM sqlite_stat1 ORDER BY 1,2;
+} {t3 null 2}
+
+do_test 4.2.3 {
+ proc xConflict {args} {
+ return "REPLACE"
+ }
+ sqlite3changeset_apply db2 $C xConflict
+ execsql { SELECT * FROM sqlite_stat1 ORDER BY 1,2 } db2
+} {t3 null 3}
+
+finish_test
diff --git a/ext/session/sessionwor.test b/ext/session/sessionwor.test
new file mode 100644
index 0000000..7d9e5c6
--- /dev/null
+++ b/ext/session/sessionwor.test
@@ -0,0 +1,123 @@
+# 2017 Jan 31
+#
+# 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. Specifically,
+# testing support for WITHOUT ROWID tables.
+#
+
+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 sessionwor
+
+proc test_reset {} {
+ catch { db close }
+ catch { db2 close }
+ forcedelete test.db test.db2
+ sqlite3 db test.db
+ sqlite3 db2 test.db2
+}
+
+foreach {tn wo} {
+ 1 ""
+ 2 "WITHOUT ROWID"
+} {
+ reset_db
+
+ do_execsql_test 1.$tn.0 "CREATE TABLE t1(a PRIMARY KEY, b) $wo ;"
+
+ do_iterator_test 1.$tn.1 t1 {
+ INSERT INTO t1 VALUES('one', 'two');
+ } {
+ {INSERT t1 0 X. {} {t one t two}}
+ }
+
+ do_iterator_test 1.$tn.2 t1 {
+ UPDATE t1 SET b='three'
+ } {
+ {UPDATE t1 0 X. {t one t two} {{} {} t three}}
+ }
+
+ do_iterator_test 1.$tn.3 t1 {
+ REPLACE INTO t1 VALUES('one', 'four');
+ } {
+ {UPDATE t1 0 X. {t one t three} {{} {} t four}}
+ }
+
+ do_iterator_test 1.$tn.4 t1 {
+ DELETE FROM t1;
+ } {
+ {DELETE t1 0 X. {t one t four} {}}
+ }
+}
+
+foreach {tn wo} {
+ 1 ""
+ 2 "WITHOUT ROWID"
+} {
+ reset_db
+
+ do_execsql_test 2.$tn.0.1 "CREATE TABLE t1(a INTEGER PRIMARY KEY, b) $wo ;"
+ do_execsql_test 2.$tn.0.2 "CREATE TABLE t2(a INTEGER PRIMARY KEY, b) $wo ;"
+ do_execsql_test 2.$tn.0.3 "CREATE TABLE t3(a INTEGER PRIMARY KEY, b) $wo ;"
+
+ do_iterator_test 1.1 t1 {
+ INSERT INTO t1 VALUES(1, 'two');
+ } {
+ {INSERT t1 0 X. {} {i 1 t two}}
+ }
+
+ do_iterator_test 2.$tn.2 t1 {
+ UPDATE t1 SET b='three'
+ } {
+ {UPDATE t1 0 X. {i 1 t two} {{} {} t three}}
+ }
+
+ do_iterator_test 2.$tn.3 t1 {
+ REPLACE INTO t1 VALUES(1, 'four');
+ } {
+ {UPDATE t1 0 X. {i 1 t three} {{} {} t four}}
+ }
+
+ do_iterator_test 2.$tn.4 t1 {
+ DELETE FROM t1;
+ } {
+ {DELETE t1 0 X. {i 1 t four} {}}
+ }
+
+ do_execsql_test 2.$tn.5 {
+ INSERT INTO t1 VALUES(1, 'one');
+ INSERT INTO t1 VALUES(2, 'two');
+ INSERT INTO t1 VALUES(3, 'three');
+ }
+
+ do_iterator_test 2.$tn.6 t2 {
+ INSERT INTO t2 SELECT a, b FROM t1
+ } {
+ {INSERT t2 0 X. {} {i 1 t one}}
+ {INSERT t2 0 X. {} {i 2 t two}}
+ {INSERT t2 0 X. {} {i 3 t three}}
+ }
+ do_iterator_test 2.$tn.7 t3 {
+ INSERT INTO t3 SELECT * FROM t1
+ } {
+ {INSERT t3 0 X. {} {i 1 t one}}
+ {INSERT t3 0 X. {} {i 2 t two}}
+ {INSERT t3 0 X. {} {i 3 t three}}
+ }
+}
+
+finish_test
+
diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c
new file mode 100644
index 0000000..fd06f3b
--- /dev/null
+++ b/ext/session/sqlite3session.c
@@ -0,0 +1,5820 @@
+
+#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
+
+static int sessions_strm_chunk_size = SESSIONS_STRM_CHUNK_SIZE;
+
+typedef struct SessionHook SessionHook;
+struct SessionHook {
+ void *pCtx;
+ int (*xOld)(void*,int,sqlite3_value**);
+ int (*xNew)(void*,int,sqlite3_value**);
+ int (*xCount)(void*);
+ int (*xDepth)(void*);
+};
+
+/*
+** Session handle structure.
+*/
+struct sqlite3_session {
+ sqlite3 *db; /* Database handle session is attached to */
+ char *zDb; /* Name of database session is attached to */
+ int bEnableSize; /* True if changeset_size() enabled */
+ 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);
+ i64 nMalloc; /* Number of bytes of data allocated */
+ i64 nMaxChangesetSize;
+ sqlite3_value *pZeroBlob; /* Value containing X'' */
+ sqlite3_session *pNext; /* Next session object on same db. */
+ SessionTable *pTable; /* List of attached tables */
+ SessionHook hook; /* APIs to grab new and old data with */
+};
+
+/*
+** 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 bNoDiscard; /* If true, do not discard in InputBuffer() */
+ int iCurrent; /* Offset in aData[] of current change */
+ 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 bInvert; /* True to invert changeset */
+ int bSkipEmpty; /* Skip noop UPDATE changes */
+ 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 */
+ int bStat1; /* True if this is sqlite_stat1 */
+ 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: 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 (0x12), UPDATE (0x17) or DELETE (0x09).
+** 1 byte: The "indirect-change" flag.
+** old.* record: (delete and update only)
+** new.* record: (insert and update only)
+**
+** The "old.*" and "new.*" records, if present, are N field records in the
+** format described above under "RECORD FORMAT", where N is the number of
+** columns in the table. The i'th field of each record is associated with
+** the i'th column of the table, counting from left to right in the order
+** in which columns were declared in the CREATE TABLE statement.
+**
+** The new.* record that is part of each INSERT change contains the values
+** that make up the new row. Similarly, the old.* record that is part of each
+** DELETE change contains the values that made up the row that was deleted
+** from the database. In the changeset format, the records that are part
+** of INSERT or DELETE changes never contain any undefined (type byte 0x00)
+** fields.
+**
+** Within the old.* record associated with an UPDATE change, all fields
+** associated with table columns that are not PRIMARY KEY columns and are
+** not modified by the UPDATE change are set to "undefined". Other fields
+** are set to the values that made up the row before the UPDATE that the
+** change records took place. Within the new.* record, fields associated
+** with table columns modified by the UPDATE change contain the new
+** values. Fields associated with table columns that are not modified
+** are set to "undefined".
+**
+** PATCHSET FORMAT:
+**
+** A patchset is also a collection of changes. It is similar to a changeset,
+** but leaves undefined 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: 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 (0x12), UPDATE (0x17) or DELETE (0x09).
+** 1 byte: The "indirect-change" flag.
+** single record: (PK fields for DELETE, PK and modified fields for UPDATE,
+** full record for INSERT).
+**
+** As in the changeset format, each field of the single record that is part
+** of a patchset change is associated with the correspondingly positioned
+** table column, counting from left to right within the CREATE TABLE
+** statement.
+**
+** For a DELETE change, all fields within the record except those associated
+** with PRIMARY KEY columns are omitted. The PRIMARY KEY fields contain the
+** values identifying the row to delete.
+**
+** For an UPDATE change, all fields except those associated with PRIMARY KEY
+** columns and columns that are modified by the UPDATE are set to "undefined".
+** PRIMARY KEY fields contain the values identifying the table row to update,
+** and fields associated with modified columns contain the new column values.
+**
+** The records associated with INSERT changes are in the same format as for
+** changesets. It is not possible for a record associated with an INSERT
+** change to contain a field set to "undefined".
+**
+** REBASE BLOB FORMAT:
+**
+** A rebase blob may be output by sqlite3changeset_apply_v2() and its
+** streaming equivalent for use with the sqlite3_rebaser APIs to rebase
+** existing changesets. A rebase blob contains one entry for each conflict
+** resolved using either the OMIT or REPLACE strategies within the apply_v2()
+** call.
+**
+** The format used for a rebase blob is very similar to that used for
+** changesets. All entries related to a single table are grouped together.
+**
+** Each group of entries begins with a table header in changeset format:
+**
+** 1 byte: Constant 0x54 (capital 'T')
+** Varint: 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 entries associated with the table.
+**
+** 1 byte: Either SQLITE_INSERT (0x12), DELETE (0x09).
+** 1 byte: Flag. 0x01 for REPLACE, 0x00 for OMIT.
+** record: (in the record format defined above).
+**
+** In a rebase blob, the first field is set to SQLITE_INSERT if the change
+** that caused the conflict was an INSERT or UPDATE, or to SQLITE_DELETE if
+** it was a DELETE. The second field is set to 0x01 if the conflict
+** resolution strategy was REPLACE, or 0x00 if it was OMIT.
+**
+** If the change that caused the conflict was a DELETE, then the single
+** record is a copy of the old.* record from the original changeset. If it
+** was an INSERT, then the single record is a copy of the new.* record. If
+** the conflicting change was an UPDATE, then the single record is a copy
+** of the new.* record with the PK fields filled in based on the original
+** old.* record.
+*/
+
+/*
+** For each row modified during a session, there exists a single instance of
+** this structure stored in a SessionTable.aChange[] hash table.
+*/
+struct SessionChange {
+ u8 op; /* One of UPDATE, DELETE, INSERT */
+ u8 bIndirect; /* True if this change is "indirect" */
+ int nMaxSize; /* Max size of eventual changeset record */
+ 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);
+}
+
+/* Load an unaligned and unsigned 32-bit integer */
+#define SESSION_UINT32(x) (((u32)(x)[0]<<24)|((x)[1]<<16)|((x)[2]<<8)|(x)[3])
+
+/*
+** Read a 64-bit big-endian integer value from buffer aRec[]. Return
+** the value read.
+*/
+static sqlite3_int64 sessionGetI64(u8 *aRec){
+ u64 x = SESSION_UINT32(aRec);
+ u32 y = SESSION_UINT32(aRec+4);
+ x = (x<<32) + y;
+ return (sqlite3_int64)x;
+}
+
+/*
+** 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 */
+ sqlite3_int64 *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);
+ }
+ n = sqlite3_value_bytes(pValue);
+ if( z==0 && (eType!=SQLITE_BLOB || n>0) ) return SQLITE_NOMEM;
+ nVarint = sessionVarintLen(n);
+
+ if( aBuf ){
+ sessionVarintPut(&aBuf[1], n);
+ if( n>0 ) memcpy(&aBuf[nVarint + 1], z, n);
+ }
+
+ nByte = 1 + nVarint + n;
+ break;
+ }
+ }
+ }else{
+ nByte = 1;
+ if( aBuf ) aBuf[0] = '\0';
+ }
+
+ if( pnWrite ) *pnWrite += nByte;
+ return SQLITE_OK;
+}
+
+/*
+** Allocate and return a pointer to a buffer nByte bytes in size. If
+** pSession is not NULL, increase the sqlite3_session.nMalloc variable
+** by the number of bytes allocated.
+*/
+static void *sessionMalloc64(sqlite3_session *pSession, i64 nByte){
+ void *pRet = sqlite3_malloc64(nByte);
+ if( pSession ) pSession->nMalloc += sqlite3_msize(pRet);
+ return pRet;
+}
+
+/*
+** Free buffer pFree, which must have been allocated by an earlier
+** call to sessionMalloc64(). If pSession is not NULL, decrease the
+** sqlite3_session.nMalloc counter by the number of bytes freed.
+*/
+static void sessionFree(sqlite3_session *pSession, void *pFree){
+ if( pSession ) pSession->nMalloc -= sqlite3_msize(pFree);
+ sqlite3_free(pFree);
+}
+
+/*
+** 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_session *pSession, /* Session object that owns pTab */
+ 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==pSession->hook.xCount(pSession->hook.pCtx) );
+ for(i=0; i<pTab->nCol; i++){
+ if( pTab->abPK[i] ){
+ int rc;
+ int eType;
+ sqlite3_value *pVal;
+
+ if( bNew ){
+ rc = pSession->hook.xNew(pSession->hook.pCtx, i, &pVal);
+ }else{
+ rc = pSession->hook.xOld(pSession->hook.pCtx, 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;
+ int n;
+ if( eType==SQLITE_TEXT ){
+ z = (const u8 *)sqlite3_value_text(pVal);
+ }else{
+ z = (const u8 *)sqlite3_value_blob(pVal);
+ }
+ n = sqlite3_value_bytes(pVal);
+ if( !z && (eType!=SQLITE_BLOB || n>0) ) return SQLITE_NOMEM;
+ h = sessionHashAppendBlob(h, n, z);
+ }else{
+ assert( eType==SQLITE_NULL );
+ assert( pTab->bStat1==0 || i!=1 );
+ *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 || e==0xFF ) 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++){
+ if( pTab->abPK[iCol] ){
+ int n1 = sessionSerialLen(a1);
+ int n2 = sessionSerialLen(a2);
+
+ if( n1!=n2 || memcmp(a1, a2, n1) ){
+ return 0;
+ }
+ a1 += n1;
+ a2 += n2;
+ }else{
+ if( bLeftPkOnly==0 ) a1 += sessionSerialLen(a1);
+ if( bRightPkOnly==0 ) a2 += sessionSerialLen(a2);
+ }
+ }
+
+ 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_session *pSession, /* Session object that owns SessionTable */
+ 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 = pSession->hook.xNew(pSession->hook.pCtx, iCol, &pVal);
+ }else{
+ /* assert( db->pPreUpdate->pUnpacked ); */
+ rc = pSession->hook.xOld(pSession->hook.pCtx, 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( n>0 && memcmp(a, z, n) ) return 0;
+ a += n;
+ }
+ }
+ }
+
+ 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(
+ sqlite3_session *pSession, /* For memory accounting. May be NULL */
+ int bPatchset,
+ SessionTable *pTab
+){
+ if( pTab->nChange==0 || pTab->nEntry>=(pTab->nChange/2) ){
+ int i;
+ SessionChange **apNew;
+ sqlite3_int64 nNew = 2*(sqlite3_int64)(pTab->nChange ? pTab->nChange : 128);
+
+ apNew = (SessionChange**)sessionMalloc64(
+ pSession, 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;
+ }
+ }
+
+ sessionFree(pSession, 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.
+**
+** 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
+*/
+static int sessionTableInfo(
+ sqlite3_session *pSession, /* For memory accounting. May be NULL */
+ 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;
+ sqlite3_int64 nByte;
+ int nDbCol = 0;
+ int nThis;
+ int i;
+ u8 *pAlloc = 0;
+ char **azCol = 0;
+ u8 *abPK = 0;
+
+ assert( pazCol && pabPK );
+
+ nThis = sqlite3Strlen30(zThis);
+ if( nThis==12 && 0==sqlite3_stricmp("sqlite_stat1", zThis) ){
+ rc = sqlite3_table_column_metadata(db, zDb, zThis, 0, 0, 0, 0, 0, 0);
+ if( rc==SQLITE_OK ){
+ /* For sqlite_stat1, pretend that (tbl,idx) is the PRIMARY KEY. */
+ zPragma = sqlite3_mprintf(
+ "SELECT 0, 'tbl', '', 0, '', 1 UNION ALL "
+ "SELECT 1, 'idx', '', 0, '', 2 UNION ALL "
+ "SELECT 2, 'stat', '', 0, '', 0"
+ );
+ }else if( rc==SQLITE_ERROR ){
+ zPragma = sqlite3_mprintf("");
+ }else{
+ *pazCol = 0;
+ *pabPK = 0;
+ *pnCol = 0;
+ if( pzTab ) *pzTab = 0;
+ return rc;
+ }
+ }else{
+ zPragma = sqlite3_mprintf("PRAGMA '%q'.table_info('%q')", zDb, zThis);
+ }
+ if( !zPragma ){
+ *pazCol = 0;
+ *pabPK = 0;
+ *pnCol = 0;
+ if( pzTab ) *pzTab = 0;
+ return SQLITE_NOMEM;
+ }
+
+ rc = sqlite3_prepare_v2(db, zPragma, -1, &pStmt, 0);
+ sqlite3_free(zPragma);
+ if( rc!=SQLITE_OK ){
+ *pazCol = 0;
+ *pabPK = 0;
+ *pnCol = 0;
+ if( pzTab ) *pzTab = 0;
+ 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 = sessionMalloc64(pSession, 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;
+ sessionFree(pSession, 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, initalize the SessionTable.nCol, azCol[] and
+** abPK[] arrays accordingly.
+**
+** If an error occurs, an error code is stored in sqlite3_session.rc and
+** non-zero returned. Or, if no error occurs but the table has no primary
+** key, sqlite3_session.rc is left set to SQLITE_OK and non-zero returned to
+** indicate that updates on this table should be ignored. SessionTable.abPK
+** is set to NULL in this case.
+*/
+static int sessionInitTable(sqlite3_session *pSession, SessionTable *pTab){
+ if( pTab->nCol==0 ){
+ u8 *abPK;
+ assert( pTab->azCol==0 || pTab->abPK==0 );
+ pSession->rc = sessionTableInfo(pSession, pSession->db, pSession->zDb,
+ pTab->zName, &pTab->nCol, 0, &pTab->azCol, &abPK
+ );
+ if( pSession->rc==SQLITE_OK ){
+ int i;
+ for(i=0; i<pTab->nCol; i++){
+ if( abPK[i] ){
+ pTab->abPK = abPK;
+ break;
+ }
+ }
+ if( 0==sqlite3_stricmp("sqlite_stat1", pTab->zName) ){
+ pTab->bStat1 = 1;
+ }
+
+ if( pSession->bEnableSize ){
+ pSession->nMaxChangesetSize += (
+ 1 + sessionVarintLen(pTab->nCol) + pTab->nCol + strlen(pTab->zName)+1
+ );
+ }
+ }
+ }
+ return (pSession->rc || pTab->abPK==0);
+}
+
+/*
+** Versions of the four methods in object SessionHook for use with the
+** sqlite_stat1 table. The purpose of this is to substitute a zero-length
+** blob each time a NULL value is read from the "idx" column of the
+** sqlite_stat1 table.
+*/
+typedef struct SessionStat1Ctx SessionStat1Ctx;
+struct SessionStat1Ctx {
+ SessionHook hook;
+ sqlite3_session *pSession;
+};
+static int sessionStat1Old(void *pCtx, int iCol, sqlite3_value **ppVal){
+ SessionStat1Ctx *p = (SessionStat1Ctx*)pCtx;
+ sqlite3_value *pVal = 0;
+ int rc = p->hook.xOld(p->hook.pCtx, iCol, &pVal);
+ if( rc==SQLITE_OK && iCol==1 && sqlite3_value_type(pVal)==SQLITE_NULL ){
+ pVal = p->pSession->pZeroBlob;
+ }
+ *ppVal = pVal;
+ return rc;
+}
+static int sessionStat1New(void *pCtx, int iCol, sqlite3_value **ppVal){
+ SessionStat1Ctx *p = (SessionStat1Ctx*)pCtx;
+ sqlite3_value *pVal = 0;
+ int rc = p->hook.xNew(p->hook.pCtx, iCol, &pVal);
+ if( rc==SQLITE_OK && iCol==1 && sqlite3_value_type(pVal)==SQLITE_NULL ){
+ pVal = p->pSession->pZeroBlob;
+ }
+ *ppVal = pVal;
+ return rc;
+}
+static int sessionStat1Count(void *pCtx){
+ SessionStat1Ctx *p = (SessionStat1Ctx*)pCtx;
+ return p->hook.xCount(p->hook.pCtx);
+}
+static int sessionStat1Depth(void *pCtx){
+ SessionStat1Ctx *p = (SessionStat1Ctx*)pCtx;
+ return p->hook.xDepth(p->hook.pCtx);
+}
+
+static int sessionUpdateMaxSize(
+ int op,
+ sqlite3_session *pSession, /* Session object pTab is attached to */
+ SessionTable *pTab, /* Table that change applies to */
+ SessionChange *pC /* Update pC->nMaxSize */
+){
+ i64 nNew = 2;
+ if( pC->op==SQLITE_INSERT ){
+ if( op!=SQLITE_DELETE ){
+ int ii;
+ for(ii=0; ii<pTab->nCol; ii++){
+ sqlite3_value *p = 0;
+ pSession->hook.xNew(pSession->hook.pCtx, ii, &p);
+ sessionSerializeValue(0, p, &nNew);
+ }
+ }
+ }else if( op==SQLITE_DELETE ){
+ nNew += pC->nRecord;
+ if( sqlite3_preupdate_blobwrite(pSession->db)>=0 ){
+ nNew += pC->nRecord;
+ }
+ }else{
+ int ii;
+ u8 *pCsr = pC->aRecord;
+ for(ii=0; ii<pTab->nCol; ii++){
+ int bChanged = 1;
+ int nOld = 0;
+ int eType;
+ sqlite3_value *p = 0;
+ pSession->hook.xNew(pSession->hook.pCtx, ii, &p);
+ if( p==0 ){
+ return SQLITE_NOMEM;
+ }
+
+ eType = *pCsr++;
+ switch( eType ){
+ case SQLITE_NULL:
+ bChanged = sqlite3_value_type(p)!=SQLITE_NULL;
+ break;
+
+ case SQLITE_FLOAT:
+ case SQLITE_INTEGER: {
+ if( eType==sqlite3_value_type(p) ){
+ sqlite3_int64 iVal = sessionGetI64(pCsr);
+ if( eType==SQLITE_INTEGER ){
+ bChanged = (iVal!=sqlite3_value_int64(p));
+ }else{
+ double dVal;
+ memcpy(&dVal, &iVal, 8);
+ bChanged = (dVal!=sqlite3_value_double(p));
+ }
+ }
+ nOld = 8;
+ pCsr += 8;
+ break;
+ }
+
+ default: {
+ int nByte;
+ nOld = sessionVarintGet(pCsr, &nByte);
+ pCsr += nOld;
+ nOld += nByte;
+ assert( eType==SQLITE_TEXT || eType==SQLITE_BLOB );
+ if( eType==sqlite3_value_type(p)
+ && nByte==sqlite3_value_bytes(p)
+ && (nByte==0 || 0==memcmp(pCsr, sqlite3_value_blob(p), nByte))
+ ){
+ bChanged = 0;
+ }
+ pCsr += nByte;
+ break;
+ }
+ }
+
+ if( bChanged && pTab->abPK[ii] ){
+ nNew = pC->nRecord + 2;
+ break;
+ }
+
+ if( bChanged ){
+ nNew += 1 + nOld;
+ sessionSerializeValue(0, p, &nNew);
+ }else if( pTab->abPK[ii] ){
+ nNew += 2 + nOld;
+ }else{
+ nNew += 2;
+ }
+ }
+ }
+
+ if( nNew>pC->nMaxSize ){
+ int nIncr = nNew - pC->nMaxSize;
+ pC->nMaxSize = nNew;
+ pSession->nMaxChangesetSize += nIncr;
+ }
+ return SQLITE_OK;
+}
+
+/*
+** 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 */
+){
+ int iHash;
+ int bNull = 0;
+ int rc = SQLITE_OK;
+ SessionStat1Ctx stat1 = {{0,0,0,0,0},0};
+
+ if( pSession->rc ) return;
+
+ /* Load table details if required */
+ if( sessionInitTable(pSession, pTab) ) return;
+
+ /* Check the number of columns in this xPreUpdate call matches the
+ ** number of columns in the table. */
+ if( pTab->nCol!=pSession->hook.xCount(pSession->hook.pCtx) ){
+ pSession->rc = SQLITE_SCHEMA;
+ return;
+ }
+
+ /* Grow the hash table if required */
+ if( sessionGrowHash(pSession, 0, pTab) ){
+ pSession->rc = SQLITE_NOMEM;
+ return;
+ }
+
+ if( pTab->bStat1 ){
+ stat1.hook = pSession->hook;
+ stat1.pSession = pSession;
+ pSession->hook.pCtx = (void*)&stat1;
+ pSession->hook.xNew = sessionStat1New;
+ pSession->hook.xOld = sessionStat1Old;
+ pSession->hook.xCount = sessionStat1Count;
+ pSession->hook.xDepth = sessionStat1Depth;
+ if( pSession->pZeroBlob==0 ){
+ sqlite3_value *p = sqlite3ValueNew(0);
+ if( p==0 ){
+ rc = SQLITE_NOMEM;
+ goto error_out;
+ }
+ sqlite3ValueSetStr(p, 0, "", 0, SQLITE_STATIC);
+ pSession->pZeroBlob = p;
+ }
+ }
+
+ /* 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(pSession, pTab, op==SQLITE_INSERT, &iHash, &bNull);
+ if( rc!=SQLITE_OK ) goto error_out;
+
+ if( bNull==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(pSession, 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). */
+ sqlite3_int64 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 = ) pSession->hook.xOld(pSession->hook.pCtx, i, &p);
+ assert( trc==SQLITE_OK );
+ }else if( pTab->abPK[i] ){
+ TESTONLY(int trc = ) pSession->hook.xNew(pSession->hook.pCtx, 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 */
+ pC = (SessionChange *)sessionMalloc64(pSession, nByte);
+ if( !pC ){
+ rc = SQLITE_NOMEM;
+ goto error_out;
+ }else{
+ memset(pC, 0, sizeof(SessionChange));
+ pC->aRecord = (u8 *)&pC[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 ){
+ pSession->hook.xOld(pSession->hook.pCtx, i, &p);
+ }else if( pTab->abPK[i] ){
+ pSession->hook.xNew(pSession->hook.pCtx, i, &p);
+ }
+ sessionSerializeValue(&pC->aRecord[nByte], p, &nByte);
+ }
+
+ /* Add the change to the hash-table */
+ if( pSession->bIndirect || pSession->hook.xDepth(pSession->hook.pCtx) ){
+ pC->bIndirect = 1;
+ }
+ pC->nRecord = nByte;
+ pC->op = op;
+ pC->pNext = pTab->apChange[iHash];
+ pTab->apChange[iHash] = pC;
+
+ }else if( pC->bIndirect ){
+ /* If the existing change is considered "indirect", but this current
+ ** change is "direct", mark the change object as direct. */
+ if( pSession->hook.xDepth(pSession->hook.pCtx)==0
+ && pSession->bIndirect==0
+ ){
+ pC->bIndirect = 0;
+ }
+ }
+
+ assert( rc==SQLITE_OK );
+ if( pSession->bEnableSize ){
+ rc = sessionUpdateMaxSize(op, pSession, pTab, pC);
+ }
+ }
+
+
+ /* If an error has occurred, mark the session object as failed. */
+ error_out:
+ if( pTab->bStat1 ){
+ pSession->hook = stat1.hook;
+ }
+ if( rc!=SQLITE_OK ){
+ pSession->rc = rc;
+ }
+}
+
+static int sessionFindTable(
+ sqlite3_session *pSession,
+ const char *zName,
+ SessionTable **ppTab
+){
+ int rc = SQLITE_OK;
+ int nName = sqlite3Strlen30(zName);
+ SessionTable *pRet;
+
+ /* Search for an existing table */
+ for(pRet=pSession->pTable; pRet; pRet=pRet->pNext){
+ if( 0==sqlite3_strnicmp(pRet->zName, zName, nName+1) ) break;
+ }
+
+ if( pRet==0 && pSession->bAutoAttach ){
+ /* If there is a table-filter configured, invoke it. If it returns 0,
+ ** do not automatically add the new table. */
+ if( pSession->xTableFilter==0
+ || pSession->xTableFilter(pSession->pFilterCtx, zName)
+ ){
+ rc = sqlite3session_attach(pSession, zName);
+ if( rc==SQLITE_OK ){
+ pRet = pSession->pTable;
+ while( ALWAYS(pRet) && pRet->pNext ){
+ pRet = pRet->pNext;
+ }
+ assert( pRet!=0 );
+ assert( 0==sqlite3_strnicmp(pRet->zName, zName, nName+1) );
+ }
+ }
+ }
+
+ assert( rc==SQLITE_OK || pRet==0 );
+ *ppTab = pRet;
+ return 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);
+
+ 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;
+
+ pSession->rc = sessionFindTable(pSession, zName, &pTab);
+ if( pTab ){
+ assert( pSession->rc==SQLITE_OK );
+ sessionPreupdateOneChange(op, pSession, pTab);
+ if( op==SQLITE_UPDATE ){
+ sessionPreupdateOneChange(SQLITE_INSERT, pSession, pTab);
+ }
+ }
+ }
+}
+
+/*
+** The pre-update hook implementations.
+*/
+static int sessionPreupdateOld(void *pCtx, int iVal, sqlite3_value **ppVal){
+ return sqlite3_preupdate_old((sqlite3*)pCtx, iVal, ppVal);
+}
+static int sessionPreupdateNew(void *pCtx, int iVal, sqlite3_value **ppVal){
+ return sqlite3_preupdate_new((sqlite3*)pCtx, iVal, ppVal);
+}
+static int sessionPreupdateCount(void *pCtx){
+ return sqlite3_preupdate_count((sqlite3*)pCtx);
+}
+static int sessionPreupdateDepth(void *pCtx){
+ return sqlite3_preupdate_depth((sqlite3*)pCtx);
+}
+
+/*
+** Install the pre-update hooks on the session object passed as the only
+** argument.
+*/
+static void sessionPreupdateHooks(
+ sqlite3_session *pSession
+){
+ pSession->hook.pCtx = (void*)pSession->db;
+ pSession->hook.xOld = sessionPreupdateOld;
+ pSession->hook.xNew = sessionPreupdateNew;
+ pSession->hook.xCount = sessionPreupdateCount;
+ pSession->hook.xDepth = sessionPreupdateDepth;
+}
+
+typedef struct SessionDiffCtx SessionDiffCtx;
+struct SessionDiffCtx {
+ sqlite3_stmt *pStmt;
+ int nOldOff;
+};
+
+/*
+** The diff hook implementations.
+*/
+static int sessionDiffOld(void *pCtx, int iVal, sqlite3_value **ppVal){
+ SessionDiffCtx *p = (SessionDiffCtx*)pCtx;
+ *ppVal = sqlite3_column_value(p->pStmt, iVal+p->nOldOff);
+ return SQLITE_OK;
+}
+static int sessionDiffNew(void *pCtx, int iVal, sqlite3_value **ppVal){
+ SessionDiffCtx *p = (SessionDiffCtx*)pCtx;
+ *ppVal = sqlite3_column_value(p->pStmt, iVal);
+ return SQLITE_OK;
+}
+static int sessionDiffCount(void *pCtx){
+ SessionDiffCtx *p = (SessionDiffCtx*)pCtx;
+ return p->nOldOff ? p->nOldOff : sqlite3_column_count(p->pStmt);
+}
+static int sessionDiffDepth(void *pCtx){
+ return 0;
+}
+
+/*
+** Install the diff hooks on the session object passed as the only
+** argument.
+*/
+static void sessionDiffHooks(
+ sqlite3_session *pSession,
+ SessionDiffCtx *pDiffCtx
+){
+ pSession->hook.pCtx = (void*)pDiffCtx;
+ pSession->hook.xOld = sessionDiffOld;
+ pSession->hook.xNew = sessionDiffNew;
+ pSession->hook.xCount = sessionDiffCount;
+ pSession->hook.xDepth = sessionDiffDepth;
+}
+
+static char *sessionExprComparePK(
+ int nCol,
+ const char *zDb1, const char *zDb2,
+ const char *zTab,
+ const char **azCol, u8 *abPK
+){
+ int i;
+ const char *zSep = "";
+ char *zRet = 0;
+
+ for(i=0; i<nCol; i++){
+ if( abPK[i] ){
+ zRet = sqlite3_mprintf("%z%s\"%w\".\"%w\".\"%w\"=\"%w\".\"%w\".\"%w\"",
+ zRet, zSep, zDb1, zTab, azCol[i], zDb2, zTab, azCol[i]
+ );
+ zSep = " AND ";
+ if( zRet==0 ) break;
+ }
+ }
+
+ return zRet;
+}
+
+static char *sessionExprCompareOther(
+ int nCol,
+ const char *zDb1, const char *zDb2,
+ const char *zTab,
+ const char **azCol, u8 *abPK
+){
+ int i;
+ const char *zSep = "";
+ char *zRet = 0;
+ int bHave = 0;
+
+ for(i=0; i<nCol; i++){
+ if( abPK[i]==0 ){
+ bHave = 1;
+ zRet = sqlite3_mprintf(
+ "%z%s\"%w\".\"%w\".\"%w\" IS NOT \"%w\".\"%w\".\"%w\"",
+ zRet, zSep, zDb1, zTab, azCol[i], zDb2, zTab, azCol[i]
+ );
+ zSep = " OR ";
+ if( zRet==0 ) break;
+ }
+ }
+
+ if( bHave==0 ){
+ assert( zRet==0 );
+ zRet = sqlite3_mprintf("0");
+ }
+
+ return zRet;
+}
+
+static char *sessionSelectFindNew(
+ int nCol,
+ const char *zDb1, /* Pick rows in this db only */
+ const char *zDb2, /* But not in this one */
+ const char *zTbl, /* Table name */
+ const char *zExpr
+){
+ char *zRet = sqlite3_mprintf(
+ "SELECT * FROM \"%w\".\"%w\" WHERE NOT EXISTS ("
+ " SELECT 1 FROM \"%w\".\"%w\" WHERE %s"
+ ")",
+ zDb1, zTbl, zDb2, zTbl, zExpr
+ );
+ return zRet;
+}
+
+static int sessionDiffFindNew(
+ int op,
+ sqlite3_session *pSession,
+ SessionTable *pTab,
+ const char *zDb1,
+ const char *zDb2,
+ char *zExpr
+){
+ int rc = SQLITE_OK;
+ char *zStmt = sessionSelectFindNew(pTab->nCol, zDb1, zDb2, pTab->zName,zExpr);
+
+ if( zStmt==0 ){
+ rc = SQLITE_NOMEM;
+ }else{
+ sqlite3_stmt *pStmt;
+ rc = sqlite3_prepare(pSession->db, zStmt, -1, &pStmt, 0);
+ if( rc==SQLITE_OK ){
+ SessionDiffCtx *pDiffCtx = (SessionDiffCtx*)pSession->hook.pCtx;
+ pDiffCtx->pStmt = pStmt;
+ pDiffCtx->nOldOff = 0;
+ while( SQLITE_ROW==sqlite3_step(pStmt) ){
+ sessionPreupdateOneChange(op, pSession, pTab);
+ }
+ rc = sqlite3_finalize(pStmt);
+ }
+ sqlite3_free(zStmt);
+ }
+
+ return rc;
+}
+
+static int sessionDiffFindModified(
+ sqlite3_session *pSession,
+ SessionTable *pTab,
+ const char *zFrom,
+ const char *zExpr
+){
+ int rc = SQLITE_OK;
+
+ char *zExpr2 = sessionExprCompareOther(pTab->nCol,
+ pSession->zDb, zFrom, pTab->zName, pTab->azCol, pTab->abPK
+ );
+ if( zExpr2==0 ){
+ rc = SQLITE_NOMEM;
+ }else{
+ char *zStmt = sqlite3_mprintf(
+ "SELECT * FROM \"%w\".\"%w\", \"%w\".\"%w\" WHERE %s AND (%z)",
+ pSession->zDb, pTab->zName, zFrom, pTab->zName, zExpr, zExpr2
+ );
+ if( zStmt==0 ){
+ rc = SQLITE_NOMEM;
+ }else{
+ sqlite3_stmt *pStmt;
+ rc = sqlite3_prepare(pSession->db, zStmt, -1, &pStmt, 0);
+
+ if( rc==SQLITE_OK ){
+ SessionDiffCtx *pDiffCtx = (SessionDiffCtx*)pSession->hook.pCtx;
+ pDiffCtx->pStmt = pStmt;
+ pDiffCtx->nOldOff = pTab->nCol;
+ while( SQLITE_ROW==sqlite3_step(pStmt) ){
+ sessionPreupdateOneChange(SQLITE_UPDATE, pSession, pTab);
+ }
+ rc = sqlite3_finalize(pStmt);
+ }
+ sqlite3_free(zStmt);
+ }
+ }
+
+ return rc;
+}
+
+int sqlite3session_diff(
+ sqlite3_session *pSession,
+ const char *zFrom,
+ const char *zTbl,
+ char **pzErrMsg
+){
+ const char *zDb = pSession->zDb;
+ int rc = pSession->rc;
+ SessionDiffCtx d;
+
+ memset(&d, 0, sizeof(d));
+ sessionDiffHooks(pSession, &d);
+
+ sqlite3_mutex_enter(sqlite3_db_mutex(pSession->db));
+ if( pzErrMsg ) *pzErrMsg = 0;
+ if( rc==SQLITE_OK ){
+ char *zExpr = 0;
+ sqlite3 *db = pSession->db;
+ SessionTable *pTo; /* Table zTbl */
+
+ /* Locate and if necessary initialize the target table object */
+ rc = sessionFindTable(pSession, zTbl, &pTo);
+ if( pTo==0 ) goto diff_out;
+ if( sessionInitTable(pSession, pTo) ){
+ rc = pSession->rc;
+ goto diff_out;
+ }
+
+ /* Check the table schemas match */
+ if( rc==SQLITE_OK ){
+ int bHasPk = 0;
+ int bMismatch = 0;
+ int nCol; /* Columns in zFrom.zTbl */
+ u8 *abPK;
+ const char **azCol = 0;
+ rc = sessionTableInfo(0, db, zFrom, zTbl, &nCol, 0, &azCol, &abPK);
+ if( rc==SQLITE_OK ){
+ if( pTo->nCol!=nCol ){
+ bMismatch = 1;
+ }else{
+ int i;
+ for(i=0; i<nCol; i++){
+ if( pTo->abPK[i]!=abPK[i] ) bMismatch = 1;
+ if( sqlite3_stricmp(azCol[i], pTo->azCol[i]) ) bMismatch = 1;
+ if( abPK[i] ) bHasPk = 1;
+ }
+ }
+ }
+ sqlite3_free((char*)azCol);
+ if( bMismatch ){
+ if( pzErrMsg ){
+ *pzErrMsg = sqlite3_mprintf("table schemas do not match");
+ }
+ rc = SQLITE_SCHEMA;
+ }
+ if( bHasPk==0 ){
+ /* Ignore tables with no primary keys */
+ goto diff_out;
+ }
+ }
+
+ if( rc==SQLITE_OK ){
+ zExpr = sessionExprComparePK(pTo->nCol,
+ zDb, zFrom, pTo->zName, pTo->azCol, pTo->abPK
+ );
+ }
+
+ /* Find new rows */
+ if( rc==SQLITE_OK ){
+ rc = sessionDiffFindNew(SQLITE_INSERT, pSession, pTo, zDb, zFrom, zExpr);
+ }
+
+ /* Find old rows */
+ if( rc==SQLITE_OK ){
+ rc = sessionDiffFindNew(SQLITE_DELETE, pSession, pTo, zFrom, zDb, zExpr);
+ }
+
+ /* Find modified rows */
+ if( rc==SQLITE_OK ){
+ rc = sessionDiffFindModified(pSession, pTo, zFrom, zExpr);
+ }
+
+ sqlite3_free(zExpr);
+ }
+
+ diff_out:
+ sessionPreupdateHooks(pSession);
+ sqlite3_mutex_leave(sqlite3_db_mutex(pSession->db));
+ return rc;
+}
+
+/*
+** 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_malloc64(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);
+ sessionPreupdateHooks(pNew);
+
+ /* 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(sqlite3_session *pSession, 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 *pNextChange;
+ for(p=pTab->apChange[i]; p; p=pNextChange){
+ pNextChange = p->pNext;
+ sessionFree(pSession, p);
+ }
+ }
+ sessionFree(pSession, (char*)pTab->azCol); /* cast works around VC++ bug */
+ sessionFree(pSession, pTab->apChange);
+ sessionFree(pSession, 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; ALWAYS((*pp)!=0); pp=&((*pp)->pNext)){
+ if( (*pp)==pSession ){
+ *pp = (*pp)->pNext;
+ if( pHead ) sqlite3_preupdate_hook(db, xPreUpdate, (void*)pHead);
+ break;
+ }
+ }
+ sqlite3_mutex_leave(sqlite3_db_mutex(db));
+ sqlite3ValueFree(pSession->pZeroBlob);
+
+ /* Delete all attached table objects. And the contents of their
+ ** associated hash-tables. */
+ sessionDeleteTable(pSession, pSession->pTable);
+
+ /* Assert that all allocations have been freed and then free the
+ ** session object itself. */
+ assert( pSession->nMalloc==0 );
+ 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. */
+ int nByte = sizeof(SessionTable) + nName + 1;
+ pTab = (SessionTable*)sessionMalloc64(pSession, nByte);
+ if( !pTab ){
+ rc = SQLITE_NOMEM;
+ }else{
+ /* Populate the new SessionTable object and link it into the list.
+ ** The new object must be linked onto the end of the list, not
+ ** simply added to the start of it in order to ensure that tables
+ ** appear in the correct order when a changeset or patchset is
+ ** eventually generated. */
+ SessionTable **ppTab;
+ memset(pTab, 0, sizeof(SessionTable));
+ pTab->zName = (char *)&pTab[1];
+ memcpy(pTab->zName, zName, nName+1);
+ for(ppTab=&pSession->pTable; *ppTab; ppTab=&(*ppTab)->pNext);
+ *ppTab = 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, i64 nByte, int *pRc){
+#define SESSION_MAX_BUFFER_SZ (0x7FFFFF00 - 1)
+ i64 nReq = p->nBuf + nByte;
+ if( *pRc==SQLITE_OK && nReq>p->nAlloc ){
+ u8 *aNew;
+ i64 nNew = p->nAlloc ? p->nAlloc : 128;
+
+ do {
+ nNew = nNew*2;
+ }while( nNew<nReq );
+
+ /* The value of SESSION_MAX_BUFFER_SZ is copied from the implementation
+ ** of sqlite3_realloc64(). Allocations greater than this size in bytes
+ ** always fail. It is used here to ensure that this routine can always
+ ** allocate up to this limit - instead of up to the largest power of
+ ** two smaller than the limit. */
+ if( nNew>SESSION_MAX_BUFFER_SZ ){
+ nNew = SESSION_MAX_BUFFER_SZ;
+ if( nNew<nReq ){
+ *pRc = SQLITE_NOMEM;
+ return 1;
+ }
+ }
+
+ aNew = (u8 *)sqlite3_realloc64(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 ){
+ sqlite3_int64 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( nBlob>0 && 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;
+ int nByte;
+ if( eType==SQLITE_BLOB ){
+ z = (u8 *)sqlite3_column_blob(pStmt, iCol);
+ }else{
+ z = (u8 *)sqlite3_column_text(pStmt, iCol);
+ }
+ nByte = sqlite3_column_bytes(pStmt, iCol);
+ if( z || (eType==SQLITE_BLOB && nByte==0) ){
+ 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 */
+
+ assert( abPK!=0 );
+ 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 n;
+ int nHdr = 1 + sessionVarintGet(&pCsr[1], &n);
+ assert( eType==SQLITE_TEXT || eType==SQLITE_BLOB );
+ nAdvance = nHdr + n;
+ if( eType==sqlite3_column_type(pStmt, i)
+ && n==sqlite3_column_bytes(pStmt, i)
+ && (n==0 || 0==memcmp(&pCsr[nHdr], sqlite3_column_blob(pStmt, i), n))
+ ){
+ 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;
+ char *zSql = 0;
+ int nSql = -1;
+
+ if( 0==sqlite3_stricmp("sqlite_stat1", zTab) ){
+ zSql = sqlite3_mprintf(
+ "SELECT tbl, ?2, stat FROM %Q.sqlite_stat1 WHERE tbl IS ?1 AND "
+ "idx IS (CASE WHEN ?2=X'' THEN NULL ELSE ?2 END)", zDb
+ );
+ if( zSql==0 ) rc = SQLITE_NOMEM;
+ }else{
+ 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, " IS ?", &rc);
+ sessionAppendInteger(&buf, i+1, &rc);
+ zSep = " AND ";
+ }
+ }
+ zSql = (char*)buf.aBuf;
+ nSql = buf.nBuf;
+ }
+
+ if( rc==SQLITE_OK ){
+ rc = sqlite3_prepare_v2(db, zSql, nSql, ppStmt, 0);
+ }
+ sqlite3_free(zSql);
+ 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.
+*/
+static 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) );
+ 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 ){
+ assert( pnChangeset!=0 && ppChangeset!=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 = 0; /* Number of columns in table */
+ u8 *abPK = 0; /* 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(0, 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{
+ assert( abPK!=0 ); /* Because sessionSelectStmt() returned ok */
+ 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 */
+){
+ int rc;
+
+ if( pnChangeset==0 || ppChangeset==0 ) return SQLITE_MISUSE;
+ rc = sessionGenerateChangeset(pSession, 0, 0, 0, pnChangeset,ppChangeset);
+ assert( rc || pnChangeset==0
+ || pSession->bEnableSize==0 || *pnChangeset<=pSession->nMaxChangesetSize
+ );
+ return rc;
+}
+
+/*
+** Streaming version of sqlite3session_changeset().
+*/
+int sqlite3session_changeset_strm(
+ sqlite3_session *pSession,
+ int (*xOutput)(void *pOut, const void *pData, int nData),
+ void *pOut
+){
+ if( xOutput==0 ) return SQLITE_MISUSE;
+ 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
+){
+ if( xOutput==0 ) return SQLITE_MISUSE;
+ 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 */
+){
+ if( pnPatchset==0 || ppPatchset==0 ) return SQLITE_MISUSE;
+ 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);
+}
+
+/*
+** Return the amount of heap memory in use.
+*/
+sqlite3_int64 sqlite3session_memory_used(sqlite3_session *pSession){
+ return pSession->nMalloc;
+}
+
+/*
+** Configure the session object passed as the first argument.
+*/
+int sqlite3session_object_config(sqlite3_session *pSession, int op, void *pArg){
+ int rc = SQLITE_OK;
+ switch( op ){
+ case SQLITE_SESSION_OBJCONFIG_SIZE: {
+ int iArg = *(int*)pArg;
+ if( iArg>=0 ){
+ if( pSession->pTable ){
+ rc = SQLITE_MISUSE;
+ }else{
+ pSession->bEnableSize = (iArg!=0);
+ }
+ }
+ *(int*)pArg = pSession->bEnableSize;
+ break;
+ }
+
+ default:
+ rc = SQLITE_MISUSE;
+ }
+
+ return rc;
+}
+
+/*
+** Return the maximum size of sqlite3session_changeset() output.
+*/
+sqlite3_int64 sqlite3session_changeset_size(sqlite3_session *pSession){
+ return pSession->nMaxChangesetSize;
+}
+
+/*
+** Do the work for either sqlite3changeset_start() or start_strm().
+*/
+static 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 */
+ int bInvert, /* True to invert changeset */
+ int bSkipEmpty /* True to skip empty UPDATE changes */
+){
+ 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.bEof = (xInput ? 0 : 1);
+ pRet->bInvert = bInvert;
+ pRet->bSkipEmpty = bSkipEmpty;
+
+ /* 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, 0, 0);
+}
+int sqlite3changeset_start_v2(
+ sqlite3_changeset_iter **pp, /* OUT: Changeset iterator handle */
+ int nChangeset, /* Size of buffer pChangeset in bytes */
+ void *pChangeset, /* Pointer to buffer containing changeset */
+ int flags
+){
+ int bInvert = !!(flags & SQLITE_CHANGESETSTART_INVERT);
+ return sessionChangesetStart(pp, 0, 0, nChangeset, pChangeset, bInvert, 0);
+}
+
+/*
+** 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, 0, 0);
+}
+int sqlite3changeset_start_v2_strm(
+ sqlite3_changeset_iter **pp, /* OUT: Changeset iterator handle */
+ int (*xInput)(void *pIn, void *pData, int *pnData),
+ void *pIn,
+ int flags
+){
+ int bInvert = !!(flags & SQLITE_CHANGESETSTART_INVERT);
+ return sessionChangesetStart(pp, xInput, pIn, 0, 0, bInvert, 0);
+}
+
+/*
+** If the SessionInput object passed as the only argument is a streaming
+** object and the buffer is full, discard some data to free up space.
+*/
+static void sessionDiscardData(SessionInput *pIn){
+ if( pIn->xInput && pIn->iNext>=sessions_strm_chunk_size ){
+ int nMove = pIn->buf.nBuf - pIn->iNext;
+ assert( nMove>=0 );
+ if( nMove>0 ){
+ memmove(pIn->buf.aBuf, &pIn->buf.aBuf[pIn->iNext], nMove);
+ }
+ pIn->buf.nBuf -= pIn->iNext;
+ pIn->iNext = 0;
+ pIn->nData = pIn->buf.nBuf;
+ }
+}
+
+/*
+** 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->bNoDiscard==0 ) sessionDiscardData(pIn);
+ 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_malloc64((sqlite3_int64)nData+1);
+ 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 *pbEmpty
+){
+ int i; /* Used to iterate through columns */
+ int rc = SQLITE_OK;
+
+ assert( pbEmpty==0 || *pbEmpty==0 );
+ if( pbEmpty ) *pbEmpty = 1;
+ 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 ){
+ if( pIn->iNext>=pIn->nData ){
+ rc = SQLITE_CORRUPT_BKPT;
+ }else{
+ eType = pIn->aData[pIn->iNext++];
+ assert( apOut[i]==0 );
+ if( eType ){
+ if( pbEmpty ) *pbEmpty = 0;
+ 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 ){
+ if( nByte<0 || nByte>pIn->nData-pIn->iNext ){
+ rc = SQLITE_CORRUPT_BKPT;
+ }else{
+ 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);
+ /* The hard upper limit for the number of columns in an SQLite
+ ** database table is, according to sqliteLimit.h, 32676. So
+ ** consider any table-header that purports to have more than 65536
+ ** columns to be corrupt. This is convenient because otherwise,
+ ** if the (nCol>65536) condition below were omitted, a sufficiently
+ ** large value for nCol may cause nRead to wrap around and become
+ ** negative. Leading to a crash. */
+ if( nCol<0 || nCol>65536 ){
+ rc = SQLITE_CORRUPT_BKPT;
+ }else{
+ 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);
+ if( p->nCol>0 ){
+ nCopy -= nVarint;
+ p->in.iNext += nVarint;
+ nByte = p->nCol * sizeof(sqlite3_value*) * 2 + nCopy;
+ p->tblhdr.nBuf = 0;
+ sessionBufferGrow(&p->tblhdr, nByte, &rc);
+ }else{
+ rc = SQLITE_CORRUPT_BKPT;
+ }
+ }
+
+ if( rc==SQLITE_OK ){
+ size_t 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;
+ if( p->apValue==0 ){
+ p->abPK = 0;
+ p->zTab = 0;
+ }else{
+ p->abPK = (u8*)&p->apValue[p->nCol*2];
+ p->zTab = p->abPK ? (char*)&p->abPK[p->nCol] : 0;
+ }
+ return (p->rc = rc);
+}
+
+/*
+** Advance the changeset iterator to the next change. The differences between
+** this function and sessionChangesetNext() are that
+**
+** * If pbEmpty is not NULL and the change is a no-op UPDATE (an UPDATE
+** that modifies no columns), this function sets (*pbEmpty) to 1.
+**
+** * If the iterator is configured to skip no-op UPDATEs,
+** sessionChangesetNext() does that. This function does not.
+*/
+static int sessionChangesetNextOne(
+ 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 *pbNew, /* If non-NULL, true if new table */
+ int *pbEmpty
+){
+ int i;
+ u8 op;
+
+ assert( (paRec==0 && pnRec==0) || (paRec && pnRec) );
+ assert( pbEmpty==0 || *pbEmpty==0 );
+
+ /* 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;
+ }
+
+ sessionDiscardData(&p->in);
+ p->in.iCurrent = p->in.iNext;
+
+ op = p->in.aData[p->in.iNext++];
+ while( op=='T' || op=='P' ){
+ if( pbNew ) *pbNew = 1;
+ p->bPatchset = (op=='P');
+ if( sessionChangesetReadTblhdr(p) ) return p->rc;
+ if( (p->rc = sessionInputBuffer(&p->in, 2)) ) return p->rc;
+ p->in.iCurrent = p->in.iNext;
+ if( p->in.iNext>=p->in.nData ) return SQLITE_DONE;
+ op = p->in.aData[p->in.iNext++];
+ }
+
+ if( p->zTab==0 || (p->bPatchset && p->bInvert) ){
+ /* The first record in the changeset is not a table header. Must be a
+ ** corrupt changeset. */
+ assert( p->in.iNext==1 || p->zTab );
+ return (p->rc = SQLITE_CORRUPT_BKPT);
+ }
+
+ 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{
+ sqlite3_value **apOld = (p->bInvert ? &p->apValue[p->nCol] : p->apValue);
+ sqlite3_value **apNew = (p->bInvert ? p->apValue : &p->apValue[p->nCol]);
+
+ /* 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, apOld, 0);
+ 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, apNew, pbEmpty);
+ if( p->rc!=SQLITE_OK ) return p->rc;
+ }
+
+ if( (p->bPatchset || p->bInvert) && 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. */
+ for(i=0; i<p->nCol; i++){
+ assert( p->bPatchset==0 || p->apValue[i]==0 );
+ if( p->abPK[i] ){
+ assert( p->apValue[i]==0 );
+ p->apValue[i] = p->apValue[i+p->nCol];
+ if( p->apValue[i]==0 ) return (p->rc = SQLITE_CORRUPT_BKPT);
+ p->apValue[i+p->nCol] = 0;
+ }
+ }
+ }else if( p->bInvert ){
+ if( p->op==SQLITE_INSERT ) p->op = SQLITE_DELETE;
+ else if( p->op==SQLITE_DELETE ) p->op = SQLITE_INSERT;
+ }
+
+ /* If this is an UPDATE that is part of a changeset, then check that
+ ** there are no fields in the old.* record that are not (a) PK fields,
+ ** or (b) also present in the new.* record.
+ **
+ ** Such records are technically corrupt, but the rebaser was at one
+ ** point generating them. Under most circumstances this is benign, but
+ ** can cause spurious SQLITE_RANGE errors when applying the changeset. */
+ if( p->bPatchset==0 && p->op==SQLITE_UPDATE){
+ for(i=0; i<p->nCol; i++){
+ if( p->abPK[i]==0 && p->apValue[i+p->nCol]==0 ){
+ sqlite3ValueFree(p->apValue[i]);
+ p->apValue[i] = 0;
+ }
+ }
+ }
+ }
+
+ return SQLITE_ROW;
+}
+
+/*
+** 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 *pbNew /* If non-NULL, true if new table */
+){
+ int bEmpty;
+ int rc;
+ do {
+ bEmpty = 0;
+ rc = sessionChangesetNextOne(p, paRec, pnRec, pbNew, &bEmpty);
+ }while( rc==SQLITE_ROW && p->bSkipEmpty && bEmpty);
+ return rc;
+}
+
+/*
+** 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, 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>=pIter->nCol ){
+ 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_malloc64(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], 0);
+ if( rc==SQLITE_OK ){
+ rc = sessionReadRecord(pInput, nCol, 0, &apVal[nCol], 0);
+ }
+
+ /* 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 && ALWAYS(ppInverted) ){
+ *pnInverted = sOut.nBuf;
+ *ppInverted = sOut.aBuf;
+ sOut.aBuf = 0;
+ }else if( sOut.nBuf>0 && ALWAYS(xOutput!=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 SessionUpdate SessionUpdate;
+struct SessionUpdate {
+ sqlite3_stmt *pStmt;
+ u32 *aMask;
+ SessionUpdate *pNext;
+};
+
+typedef struct SessionApplyCtx SessionApplyCtx;
+struct SessionApplyCtx {
+ sqlite3 *db;
+ sqlite3_stmt *pDelete; /* DELETE 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 */
+ u32 *aUpdateMask; /* Used by sessionUpdateFind */
+ SessionUpdate *pUp;
+ int bStat1; /* True if table is sqlite_stat1 */
+ int bDeferConstraints; /* True to defer constraints */
+ int bInvertConstraints; /* Invert when iterating constraints buffer */
+ SessionBuffer constraints; /* Deferred constraints are stored here */
+ SessionBuffer rebase; /* Rebase information (if any) here */
+ u8 bRebaseStarted; /* If table header is already in rebase */
+ u8 bRebase; /* True to collect rebase information */
+};
+
+/* Number of prepared UPDATE statements to cache. */
+#define SESSION_UPDATE_CACHE_SZ 12
+
+/*
+** Find a prepared UPDATE statement suitable for the UPDATE step currently
+** being visited by the iterator. The UPDATE is of the form:
+**
+** UPDATE tbl SET col = ?, col2 = ? WHERE pk1 IS ? AND pk2 IS ?
+*/
+static int sessionUpdateFind(
+ sqlite3_changeset_iter *pIter,
+ SessionApplyCtx *p,
+ int bPatchset,
+ sqlite3_stmt **ppStmt
+){
+ int rc = SQLITE_OK;
+ SessionUpdate *pUp = 0;
+ int nCol = pIter->nCol;
+ int nU32 = (pIter->nCol+33)/32;
+ int ii;
+
+ if( p->aUpdateMask==0 ){
+ p->aUpdateMask = sqlite3_malloc(nU32*sizeof(u32));
+ if( p->aUpdateMask==0 ){
+ rc = SQLITE_NOMEM;
+ }
+ }
+
+ if( rc==SQLITE_OK ){
+ memset(p->aUpdateMask, 0, nU32*sizeof(u32));
+ rc = SQLITE_CORRUPT;
+ for(ii=0; ii<pIter->nCol; ii++){
+ if( sessionChangesetNew(pIter, ii) ){
+ p->aUpdateMask[ii/32] |= (1<<(ii%32));
+ rc = SQLITE_OK;
+ }
+ }
+ }
+
+ if( rc==SQLITE_OK ){
+ if( bPatchset ) p->aUpdateMask[nCol/32] |= (1<<(nCol%32));
+
+ if( p->pUp ){
+ int nUp = 0;
+ SessionUpdate **pp = &p->pUp;
+ while( 1 ){
+ nUp++;
+ if( 0==memcmp(p->aUpdateMask, (*pp)->aMask, nU32*sizeof(u32)) ){
+ pUp = *pp;
+ *pp = pUp->pNext;
+ pUp->pNext = p->pUp;
+ p->pUp = pUp;
+ break;
+ }
+
+ if( (*pp)->pNext ){
+ pp = &(*pp)->pNext;
+ }else{
+ if( nUp>=SESSION_UPDATE_CACHE_SZ ){
+ sqlite3_finalize((*pp)->pStmt);
+ sqlite3_free(*pp);
+ *pp = 0;
+ }
+ break;
+ }
+ }
+ }
+
+ if( pUp==0 ){
+ int nByte = sizeof(SessionUpdate) * nU32*sizeof(u32);
+ int bStat1 = (sqlite3_stricmp(pIter->zTab, "sqlite_stat1")==0);
+ pUp = (SessionUpdate*)sqlite3_malloc(nByte);
+ if( pUp==0 ){
+ rc = SQLITE_NOMEM;
+ }else{
+ const char *zSep = "";
+ SessionBuffer buf;
+
+ memset(&buf, 0, sizeof(buf));
+ pUp->aMask = (u32*)&pUp[1];
+ memcpy(pUp->aMask, p->aUpdateMask, nU32*sizeof(u32));
+
+ sessionAppendStr(&buf, "UPDATE main.", &rc);
+ sessionAppendIdent(&buf, pIter->zTab, &rc);
+ sessionAppendStr(&buf, " SET ", &rc);
+
+ /* Create the assignments part of the UPDATE */
+ for(ii=0; ii<pIter->nCol; ii++){
+ if( p->abPK[ii]==0 && sessionChangesetNew(pIter, ii) ){
+ sessionAppendStr(&buf, zSep, &rc);
+ sessionAppendIdent(&buf, p->azCol[ii], &rc);
+ sessionAppendStr(&buf, " = ?", &rc);
+ sessionAppendInteger(&buf, ii*2+1, &rc);
+ zSep = ", ";
+ }
+ }
+
+ /* Create the WHERE clause part of the UPDATE */
+ zSep = "";
+ sessionAppendStr(&buf, " WHERE ", &rc);
+ for(ii=0; ii<pIter->nCol; ii++){
+ if( p->abPK[ii] || (bPatchset==0 && sessionChangesetOld(pIter, ii)) ){
+ sessionAppendStr(&buf, zSep, &rc);
+ if( bStat1 && ii==1 ){
+ assert( sqlite3_stricmp(p->azCol[ii], "idx")==0 );
+ sessionAppendStr(&buf,
+ "idx IS CASE "
+ "WHEN length(?4)=0 AND typeof(?4)='blob' THEN NULL "
+ "ELSE ?4 END ", &rc
+ );
+ }else{
+ sessionAppendIdent(&buf, p->azCol[ii], &rc);
+ sessionAppendStr(&buf, " IS ?", &rc);
+ sessionAppendInteger(&buf, ii*2+2, &rc);
+ }
+ zSep = " AND ";
+ }
+ }
+
+ if( rc==SQLITE_OK ){
+ char *zSql = (char*)buf.aBuf;
+ rc = sqlite3_prepare_v2(p->db, zSql, buf.nBuf, &pUp->pStmt, 0);
+ }
+
+ if( rc!=SQLITE_OK ){
+ sqlite3_free(pUp);
+ pUp = 0;
+ }else{
+ pUp->pNext = p->pUp;
+ p->pUp = pUp;
+ }
+ sqlite3_free(buf.aBuf);
+ }
+ }
+ }
+
+ assert( (rc==SQLITE_OK)==(pUp!=0) );
+ if( pUp ){
+ *ppStmt = pUp->pStmt;
+ }else{
+ *ppStmt = 0;
+ }
+ return rc;
+}
+
+/*
+** Free all cached UPDATE statements.
+*/
+static void sessionUpdateFree(SessionApplyCtx *p){
+ SessionUpdate *pUp;
+ SessionUpdate *pNext;
+ for(pUp=p->pUp; pUp; pUp=pNext){
+ pNext = pUp->pNext;
+ sqlite3_finalize(pUp->pStmt);
+ sqlite3_free(pUp);
+ }
+ p->pUp = 0;
+ sqlite3_free(p->aUpdateMask);
+ p->aUpdateMask = 0;
+}
+
+/*
+** 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 main.", &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 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, "(", &rc);
+ for(i=0; i<p->nCol; i++){
+ if( i!=0 ) sessionAppendStr(&buf, ", ", &rc);
+ sessionAppendIdent(&buf, p->azCol[i], &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;
+}
+
+static int sessionPrepare(sqlite3 *db, sqlite3_stmt **pp, const char *zSql){
+ return sqlite3_prepare_v2(db, zSql, -1, pp, 0);
+}
+
+/*
+** Prepare statements for applying changes to the sqlite_stat1 table.
+** These are similar to those created by sessionSelectRow(),
+** sessionInsertRow(), sessionUpdateRow() and sessionDeleteRow() for
+** other tables.
+*/
+static int sessionStat1Sql(sqlite3 *db, SessionApplyCtx *p){
+ int rc = sessionSelectRow(db, "sqlite_stat1", p);
+ if( rc==SQLITE_OK ){
+ rc = sessionPrepare(db, &p->pInsert,
+ "INSERT INTO main.sqlite_stat1 VALUES(?1, "
+ "CASE WHEN length(?2)=0 AND typeof(?2)='blob' THEN NULL ELSE ?2 END, "
+ "?3)"
+ );
+ }
+ if( rc==SQLITE_OK ){
+ rc = sessionPrepare(db, &p->pDelete,
+ "DELETE FROM main.sqlite_stat1 WHERE tbl=?1 AND idx IS "
+ "CASE WHEN length(?2)=0 AND typeof(?2)='blob' THEN NULL ELSE ?2 END "
+ "AND (?4 OR stat IS ?3)"
+ );
+ }
+ 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 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 = 0;
+ (void)xValue(pIter, i, &pVal);
+ if( pVal==0 ){
+ /* The value in the changeset was "undefined". This indicates a
+ ** corrupt changeset blob. */
+ rc = SQLITE_CORRUPT_BKPT;
+ }else{
+ 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;
+}
+
+/*
+** This function is called from within sqlite3changeset_apply_v2() when
+** a conflict is encountered and resolved using conflict resolution
+** mode eType (either SQLITE_CHANGESET_OMIT or SQLITE_CHANGESET_REPLACE)..
+** It adds a conflict resolution record to the buffer in
+** SessionApplyCtx.rebase, which will eventually be returned to the caller
+** of apply_v2() as the "rebase" buffer.
+**
+** Return SQLITE_OK if successful, or an SQLite error code otherwise.
+*/
+static int sessionRebaseAdd(
+ SessionApplyCtx *p, /* Apply context */
+ int eType, /* Conflict resolution (OMIT or REPLACE) */
+ sqlite3_changeset_iter *pIter /* Iterator pointing at current change */
+){
+ int rc = SQLITE_OK;
+ if( p->bRebase ){
+ int i;
+ int eOp = pIter->op;
+ if( p->bRebaseStarted==0 ){
+ /* Append a table-header to the rebase buffer */
+ const char *zTab = pIter->zTab;
+ sessionAppendByte(&p->rebase, 'T', &rc);
+ sessionAppendVarint(&p->rebase, p->nCol, &rc);
+ sessionAppendBlob(&p->rebase, p->abPK, p->nCol, &rc);
+ sessionAppendBlob(&p->rebase, (u8*)zTab, (int)strlen(zTab)+1, &rc);
+ p->bRebaseStarted = 1;
+ }
+
+ assert( eType==SQLITE_CHANGESET_REPLACE||eType==SQLITE_CHANGESET_OMIT );
+ assert( eOp==SQLITE_DELETE || eOp==SQLITE_INSERT || eOp==SQLITE_UPDATE );
+
+ sessionAppendByte(&p->rebase,
+ (eOp==SQLITE_DELETE ? SQLITE_DELETE : SQLITE_INSERT), &rc
+ );
+ sessionAppendByte(&p->rebase, (eType==SQLITE_CHANGESET_REPLACE), &rc);
+ for(i=0; i<p->nCol; i++){
+ sqlite3_value *pVal = 0;
+ if( eOp==SQLITE_DELETE || (eOp==SQLITE_UPDATE && p->abPK[i]) ){
+ sqlite3changeset_old(pIter, i, &pVal);
+ }else{
+ sqlite3changeset_new(pIter, i, &pVal);
+ }
+ sessionAppendValue(&p->rebase, pVal, &rc);
+ }
+ }
+ 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 = 0; /* 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 ){
+ if( p->bDeferConstraints && eType==SQLITE_CHANGESET_CONFLICT ){
+ /* Instead of invoking the conflict handler, append the change blob
+ ** to the SessionApplyCtx.constraints buffer. */
+ u8 *aBlob = &pIter->in.aData[pIter->in.iCurrent];
+ int nBlob = pIter->in.iNext - pIter->in.iCurrent;
+ sessionAppendBlob(&p->constraints, aBlob, nBlob, &rc);
+ return SQLITE_OK;
+ }else{
+ /* 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;
+ }
+ if( rc==SQLITE_OK ){
+ rc = sessionRebaseAdd(p, res, pIter);
+ }
+ }
+
+ 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->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;
+ sqlite3_stmt *pUp = 0;
+ int bPatchset = (pbRetry==0 || pIter->bPatchset);
+
+ rc = sessionUpdateFind(pIter, p, bPatchset, &pUp);
+
+ /* 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);
+ if( p->abPK[i] || (bPatchset==0 && pOld) ){
+ rc = sessionBindValue(pUp, i*2+2, pOld);
+ }
+ if( rc==SQLITE_OK && pNew ){
+ rc = sessionBindValue(pUp, i*2+1, pNew);
+ }
+ }
+ 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(pUp);
+ rc = sqlite3_reset(pUp);
+
+ 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 );
+ if( p->bStat1 ){
+ /* Check if there is a conflicting row. For sqlite_stat1, this needs
+ ** to be done using a SELECT, as there is no PRIMARY KEY in the
+ ** database schema to throw an exception if a duplicate is inserted. */
+ rc = sessionSeekToRow(p->db, pIter, p->abPK, p->pSelect);
+ if( rc==SQLITE_ROW ){
+ rc = SQLITE_CONSTRAINT;
+ sqlite3_reset(p->pSelect);
+ }
+ }
+
+ if( rc==SQLITE_OK ){
+ 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;
+}
+
+/*
+** 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.
+**
+** The difference between this function and sessionApplyOne() is that this
+** function handles the case where the conflict-handler is invoked and
+** returns SQLITE_CHANGESET_REPLACE - indicating that the change should be
+** retried in some manner.
+*/
+static int sessionApplyOneWithRetry(
+ sqlite3 *db, /* Apply change to "main" db of this handle */
+ sqlite3_changeset_iter *pIter, /* Changeset iterator to read change from */
+ SessionApplyCtx *pApply, /* Apply context */
+ int(*xConflict)(void*, int, sqlite3_changeset_iter*),
+ void *pCtx /* First argument passed to xConflict */
+){
+ int bReplace = 0;
+ int bRetry = 0;
+ int rc;
+
+ rc = sessionApplyOneOp(pIter, pApply, xConflict, pCtx, &bReplace, &bRetry);
+ if( rc==SQLITE_OK ){
+ /* If the bRetry flag is set, the change has not been applied due to an
+ ** SQLITE_CHANGESET_DATA problem (i.e. this is an UPDATE or DELETE and
+ ** a row with the correct PK is present in the db, but one or more other
+ ** fields do not contain the expected values) and the conflict handler
+ ** returned SQLITE_CHANGESET_REPLACE. In this case retry the operation,
+ ** but pass NULL as the final argument so that sessionApplyOneOp() ignores
+ ** the SQLITE_CHANGESET_DATA problem. */
+ if( bRetry ){
+ assert( pIter->op==SQLITE_UPDATE || pIter->op==SQLITE_DELETE );
+ rc = sessionApplyOneOp(pIter, pApply, xConflict, pCtx, 0, 0);
+ }
+
+ /* If the bReplace flag is set, the change is an INSERT that has not
+ ** been performed because the database already contains a row with the
+ ** specified primary key and the conflict handler returned
+ ** SQLITE_CHANGESET_REPLACE. In this case remove the conflicting row
+ ** before reattempting the INSERT. */
+ else 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, pApply->nCol, pApply->abPK, pApply->pDelete);
+ sqlite3_bind_int(pApply->pDelete, pApply->nCol+1, 1);
+ }
+ if( rc==SQLITE_OK ){
+ sqlite3_step(pApply->pDelete);
+ rc = sqlite3_reset(pApply->pDelete);
+ }
+ if( rc==SQLITE_OK ){
+ rc = sessionApplyOneOp(pIter, pApply, xConflict, pCtx, 0, 0);
+ }
+ if( rc==SQLITE_OK ){
+ rc = sqlite3_exec(db, "RELEASE replace_op", 0, 0, 0);
+ }
+ }
+ }
+
+ return rc;
+}
+
+/*
+** Retry the changes accumulated in the pApply->constraints buffer.
+*/
+static int sessionRetryConstraints(
+ sqlite3 *db,
+ int bPatchset,
+ const char *zTab,
+ SessionApplyCtx *pApply,
+ int(*xConflict)(void*, int, sqlite3_changeset_iter*),
+ void *pCtx /* First argument passed to xConflict */
+){
+ int rc = SQLITE_OK;
+
+ while( pApply->constraints.nBuf ){
+ sqlite3_changeset_iter *pIter2 = 0;
+ SessionBuffer cons = pApply->constraints;
+ memset(&pApply->constraints, 0, sizeof(SessionBuffer));
+
+ rc = sessionChangesetStart(
+ &pIter2, 0, 0, cons.nBuf, cons.aBuf, pApply->bInvertConstraints, 1
+ );
+ if( rc==SQLITE_OK ){
+ size_t nByte = 2*pApply->nCol*sizeof(sqlite3_value*);
+ int rc2;
+ pIter2->bPatchset = bPatchset;
+ pIter2->zTab = (char*)zTab;
+ pIter2->nCol = pApply->nCol;
+ pIter2->abPK = pApply->abPK;
+ sessionBufferGrow(&pIter2->tblhdr, nByte, &rc);
+ pIter2->apValue = (sqlite3_value**)pIter2->tblhdr.aBuf;
+ if( rc==SQLITE_OK ) memset(pIter2->apValue, 0, nByte);
+
+ while( rc==SQLITE_OK && SQLITE_ROW==sqlite3changeset_next(pIter2) ){
+ rc = sessionApplyOneWithRetry(db, pIter2, pApply, xConflict, pCtx);
+ }
+
+ rc2 = sqlite3changeset_finalize(pIter2);
+ if( rc==SQLITE_OK ) rc = rc2;
+ }
+ assert( pApply->bDeferConstraints || pApply->constraints.nBuf==0 );
+
+ sqlite3_free(cons.aBuf);
+ if( rc!=SQLITE_OK ) break;
+ if( pApply->constraints.nBuf>=cons.nBuf ){
+ /* No progress was made on the last round. */
+ pApply->bDeferConstraints = 0;
+ }
+ }
+
+ 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 */
+ void **ppRebase, int *pnRebase, /* OUT: Rebase information */
+ int flags /* SESSION_APPLY_XXX flags */
+){
+ int schemaMismatch = 0;
+ int rc = SQLITE_OK; /* Return code */
+ const char *zTab = 0; /* Name of current table */
+ int nTab = 0; /* Result of sqlite3Strlen30(zTab) */
+ SessionApplyCtx sApply; /* changeset_apply() context object */
+ int bPatchset;
+
+ assert( xConflict!=0 );
+
+ pIter->in.bNoDiscard = 1;
+ memset(&sApply, 0, sizeof(sApply));
+ sApply.bRebase = (ppRebase && pnRebase);
+ sApply.bInvertConstraints = !!(flags & SQLITE_CHANGESETAPPLY_INVERT);
+ sqlite3_mutex_enter(sqlite3_db_mutex(db));
+ if( (flags & SQLITE_CHANGESETAPPLY_NOSAVEPOINT)==0 ){
+ 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;
+ const char *zNew;
+
+ sqlite3changeset_op(pIter, &zNew, &nCol, &op, 0);
+
+ if( zTab==0 || sqlite3_strnicmp(zNew, zTab, nTab+1) ){
+ u8 *abPK;
+
+ rc = sessionRetryConstraints(
+ db, pIter->bPatchset, zTab, &sApply, xConflict, pCtx
+ );
+ if( rc!=SQLITE_OK ) break;
+
+ sessionUpdateFree(&sApply);
+ sqlite3_free((char*)sApply.azCol); /* cast works around VC++ bug */
+ sqlite3_finalize(sApply.pDelete);
+ sqlite3_finalize(sApply.pInsert);
+ sqlite3_finalize(sApply.pSelect);
+ sApply.db = db;
+ sApply.pDelete = 0;
+ sApply.pInsert = 0;
+ sApply.pSelect = 0;
+ sApply.nCol = 0;
+ sApply.azCol = 0;
+ sApply.abPK = 0;
+ sApply.bStat1 = 0;
+ sApply.bDeferConstraints = 1;
+ sApply.bRebaseStarted = 0;
+ memset(&sApply.constraints, 0, sizeof(SessionBuffer));
+
+ /* 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{
+ int nMinCol = 0;
+ int i;
+
+ sqlite3changeset_pk(pIter, &abPK, 0);
+ rc = sessionTableInfo(0,
+ db, "main", zNew, &sApply.nCol, &zTab, &sApply.azCol, &sApply.abPK
+ );
+ if( rc!=SQLITE_OK ) break;
+ for(i=0; i<sApply.nCol; i++){
+ if( sApply.abPK[i] ) nMinCol = i+1;
+ }
+
+ 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 or more",
+ zTab, sApply.nCol, nCol
+ );
+ }
+ else if( nCol<nMinCol || memcmp(sApply.abPK, abPK, nCol)!=0 ){
+ schemaMismatch = 1;
+ sqlite3_log(SQLITE_SCHEMA, "sqlite3changeset_apply(): "
+ "primary key mismatch for table %s", zTab
+ );
+ }
+ else{
+ sApply.nCol = nCol;
+ if( 0==sqlite3_stricmp(zTab, "sqlite_stat1") ){
+ if( (rc = sessionStat1Sql(db, &sApply) ) ){
+ break;
+ }
+ sApply.bStat1 = 1;
+ }else{
+ if( (rc = sessionSelectRow(db, zTab, &sApply))
+ || (rc = sessionDeleteRow(db, zTab, &sApply))
+ || (rc = sessionInsertRow(db, zTab, &sApply))
+ ){
+ break;
+ }
+ sApply.bStat1 = 0;
+ }
+ }
+ 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 = sessionApplyOneWithRetry(db, pIter, &sApply, xConflict, pCtx);
+ }
+
+ bPatchset = pIter->bPatchset;
+ if( rc==SQLITE_OK ){
+ rc = sqlite3changeset_finalize(pIter);
+ }else{
+ sqlite3changeset_finalize(pIter);
+ }
+
+ if( rc==SQLITE_OK ){
+ rc = sessionRetryConstraints(db, bPatchset, zTab, &sApply, xConflict, pCtx);
+ }
+
+ if( rc==SQLITE_OK ){
+ int nFk, notUsed;
+ sqlite3_db_status(db, SQLITE_DBSTATUS_DEFERRED_FKS, &nFk, &notUsed, 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( (flags & SQLITE_CHANGESETAPPLY_NOSAVEPOINT)==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);
+ }
+ }
+
+ assert( sApply.bRebase || sApply.rebase.nBuf==0 );
+ if( rc==SQLITE_OK && bPatchset==0 && sApply.bRebase ){
+ *ppRebase = (void*)sApply.rebase.aBuf;
+ *pnRebase = sApply.rebase.nBuf;
+ sApply.rebase.aBuf = 0;
+ }
+ sessionUpdateFree(&sApply);
+ sqlite3_finalize(sApply.pInsert);
+ sqlite3_finalize(sApply.pDelete);
+ sqlite3_finalize(sApply.pSelect);
+ sqlite3_free((char*)sApply.azCol); /* cast works around VC++ bug */
+ sqlite3_free((char*)sApply.constraints.aBuf);
+ sqlite3_free((char*)sApply.rebase.aBuf);
+ sqlite3_mutex_leave(sqlite3_db_mutex(db));
+ return rc;
+}
+
+/*
+** Apply the changeset passed via pChangeset/nChangeset to the main
+** database attached to handle "db".
+*/
+int sqlite3changeset_apply_v2(
+ 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 */
+ void **ppRebase, int *pnRebase,
+ int flags
+){
+ sqlite3_changeset_iter *pIter; /* Iterator to skip through changeset */
+ int bInv = !!(flags & SQLITE_CHANGESETAPPLY_INVERT);
+ int rc = sessionChangesetStart(&pIter, 0, 0, nChangeset, pChangeset, bInv, 1);
+ if( rc==SQLITE_OK ){
+ rc = sessionChangesetApply(
+ db, pIter, xFilter, xConflict, pCtx, ppRebase, pnRebase, flags
+ );
+ }
+ 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 */
+){
+ return sqlite3changeset_apply_v2(
+ db, nChangeset, pChangeset, xFilter, xConflict, pCtx, 0, 0, 0
+ );
+}
+
+/*
+** 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_v2_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 */
+ void **ppRebase, int *pnRebase,
+ int flags
+){
+ sqlite3_changeset_iter *pIter; /* Iterator to skip through changeset */
+ int bInverse = !!(flags & SQLITE_CHANGESETAPPLY_INVERT);
+ int rc = sessionChangesetStart(&pIter, xInput, pIn, 0, 0, bInverse, 1);
+ if( rc==SQLITE_OK ){
+ rc = sessionChangesetApply(
+ db, pIter, xFilter, xConflict, pCtx, ppRebase, pnRebase, flags
+ );
+ }
+ return rc;
+}
+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 */
+){
+ return sqlite3changeset_apply_v2_strm(
+ db, xInput, pIn, xFilter, xConflict, pCtx, 0, 0, 0
+ );
+}
+
+/*
+** sqlite3_changegroup handle.
+*/
+struct sqlite3_changegroup {
+ int rc; /* Error code */
+ int bPatch; /* True to accumulate patchsets */
+ SessionTable *pList; /* List of tables in current patch */
+};
+
+/*
+** 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 bRebase, /* True for a rebase hash-table */
+ 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;
+ int rc = SQLITE_OK;
+
+ if( !pExist ){
+ pNew = (SessionChange *)sqlite3_malloc64(sizeof(SessionChange) + nRec);
+ if( !pNew ){
+ return SQLITE_NOMEM;
+ }
+ memset(pNew, 0, sizeof(SessionChange));
+ pNew->op = op2;
+ pNew->bIndirect = bIndirect;
+ pNew->aRecord = (u8*)&pNew[1];
+ if( bIndirect==0 || bRebase==0 ){
+ pNew->nRecord = nRec;
+ memcpy(pNew->aRecord, aRec, nRec);
+ }else{
+ int i;
+ u8 *pIn = aRec;
+ u8 *pOut = pNew->aRecord;
+ for(i=0; i<pTab->nCol; i++){
+ int nIn = sessionSerialLen(pIn);
+ if( *pIn==0 ){
+ *pOut++ = 0;
+ }else if( pTab->abPK[i]==0 ){
+ *pOut++ = 0xFF;
+ }else{
+ memcpy(pOut, pIn, nIn);
+ pOut += nIn;
+ }
+ pIn += nIn;
+ }
+ pNew->nRecord = pOut - pNew->aRecord;
+ }
+ }else if( bRebase ){
+ if( pExist->op==SQLITE_DELETE && pExist->bIndirect ){
+ *ppNew = pExist;
+ }else{
+ sqlite3_int64 nByte = nRec + pExist->nRecord + sizeof(SessionChange);
+ pNew = (SessionChange*)sqlite3_malloc64(nByte);
+ if( pNew==0 ){
+ rc = SQLITE_NOMEM;
+ }else{
+ int i;
+ u8 *a1 = pExist->aRecord;
+ u8 *a2 = aRec;
+ u8 *pOut;
+
+ memset(pNew, 0, nByte);
+ pNew->bIndirect = bIndirect || pExist->bIndirect;
+ pNew->op = op2;
+ pOut = pNew->aRecord = (u8*)&pNew[1];
+
+ for(i=0; i<pTab->nCol; i++){
+ int n1 = sessionSerialLen(a1);
+ int n2 = sessionSerialLen(a2);
+ if( *a1==0xFF || (pTab->abPK[i]==0 && bIndirect) ){
+ *pOut++ = 0xFF;
+ }else if( *a2==0 ){
+ memcpy(pOut, a1, n1);
+ pOut += n1;
+ }else{
+ memcpy(pOut, a2, n2);
+ pOut += n2;
+ }
+ a1 += n1;
+ a2 += n2;
+ }
+ pNew->nRecord = pOut - pNew->aRecord;
+ }
+ sqlite3_free(pExist);
+ }
+ }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;
+ sqlite3_int64 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_malloc64(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 rc;
+}
+
+/*
+** Add all changes in the changeset traversed by the iterator passed as
+** the first argument to the changegroup hash tables.
+*/
+static int sessionChangesetToHash(
+ sqlite3_changeset_iter *pIter, /* Iterator to read from */
+ sqlite3_changegroup *pGrp, /* Changegroup object to add changeset to */
+ int bRebase /* True if hash table is for rebasing */
+){
+ u8 *aRec;
+ int nRec;
+ int rc = SQLITE_OK;
+ SessionTable *pTab = 0;
+
+ while( SQLITE_ROW==sessionChangesetNext(pIter, &aRec, &nRec, 0) ){
+ const char *zNew;
+ int nCol;
+ int op;
+ int iHash;
+ int bIndirect;
+ SessionChange *pChange;
+ SessionChange *pExist = 0;
+ SessionChange **pp;
+
+ if( pGrp->pList==0 ){
+ pGrp->bPatch = pIter->bPatchset;
+ }else if( pIter->bPatchset!=pGrp->bPatch ){
+ rc = SQLITE_ERROR;
+ break;
+ }
+
+ 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 = pGrp->pList; pTab; pTab=pTab->pNext){
+ if( 0==sqlite3_strnicmp(pTab->zName, zNew, nNew+1) ) break;
+ }
+ if( !pTab ){
+ SessionTable **ppTab;
+
+ pTab = sqlite3_malloc64(sizeof(SessionTable) + nCol + nNew+1);
+ if( !pTab ){
+ rc = SQLITE_NOMEM;
+ break;
+ }
+ memset(pTab, 0, sizeof(SessionTable));
+ 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);
+
+ /* The new object must be linked on to the end of the list, not
+ ** simply added to the start of it. This is to ensure that the
+ ** tables within the output of sqlite3changegroup_output() are in
+ ** the right order. */
+ for(ppTab=&pGrp->pList; *ppTab; ppTab=&(*ppTab)->pNext);
+ *ppTab = pTab;
+ }else if( pTab->nCol!=nCol || memcmp(pTab->abPK, abPK, nCol) ){
+ rc = SQLITE_SCHEMA;
+ break;
+ }
+ }
+
+ if( sessionGrowHash(0, 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, bRebase,
+ 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;
+}
+
+/*
+** Serialize a changeset (or patchset) based on all changesets (or patchsets)
+** added to the changegroup object passed as the first argument.
+**
+** If xOutput is not NULL, then the changeset/patchset is returned to the
+** user via one or more calls to xOutput, as with the other streaming
+** interfaces.
+**
+** Or, if xOutput is NULL, then (*ppOut) is populated with a pointer to a
+** buffer containing the output changeset before this function returns. In
+** this case (*pnOut) is set to the size of the output buffer in bytes. It
+** is the responsibility of the caller to free the output buffer using
+** sqlite3_free() when it is no longer required.
+**
+** If successful, SQLITE_OK is returned. Or, if an error occurs, an SQLite
+** error code. If an error occurs and xOutput is NULL, (*ppOut) and (*pnOut)
+** are both set to 0 before returning.
+*/
+static int sessionChangegroupOutput(
+ sqlite3_changegroup *pGrp,
+ int (*xOutput)(void *pOut, const void *pData, int nData),
+ void *pOut,
+ int *pnOut,
+ void **ppOut
+){
+ int rc = SQLITE_OK;
+ SessionBuffer buf = {0, 0, 0};
+ SessionTable *pTab;
+ assert( xOutput==0 || (ppOut==0 && pnOut==0) );
+
+ /* Create the serialized output changeset based on the contents of the
+ ** hash tables attached to the SessionTable objects in list p->pList.
+ */
+ for(pTab=pGrp->pList; rc==SQLITE_OK && pTab; pTab=pTab->pNext){
+ int i;
+ if( pTab->nEntry==0 ) continue;
+
+ sessionAppendTableHdr(&buf, pGrp->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 if( ppOut ){
+ *ppOut = buf.aBuf;
+ if( pnOut ) *pnOut = buf.nBuf;
+ buf.aBuf = 0;
+ }
+ }
+ sqlite3_free(buf.aBuf);
+
+ return rc;
+}
+
+/*
+** Allocate a new, empty, sqlite3_changegroup.
+*/
+int sqlite3changegroup_new(sqlite3_changegroup **pp){
+ int rc = SQLITE_OK; /* Return code */
+ sqlite3_changegroup *p; /* New object */
+ p = (sqlite3_changegroup*)sqlite3_malloc(sizeof(sqlite3_changegroup));
+ if( p==0 ){
+ rc = SQLITE_NOMEM;
+ }else{
+ memset(p, 0, sizeof(sqlite3_changegroup));
+ }
+ *pp = p;
+ return rc;
+}
+
+/*
+** Add the changeset currently stored in buffer pData, size nData bytes,
+** to changeset-group p.
+*/
+int sqlite3changegroup_add(sqlite3_changegroup *pGrp, int nData, void *pData){
+ sqlite3_changeset_iter *pIter; /* Iterator opened on pData/nData */
+ int rc; /* Return code */
+
+ rc = sqlite3changeset_start(&pIter, nData, pData);
+ if( rc==SQLITE_OK ){
+ rc = sessionChangesetToHash(pIter, pGrp, 0);
+ }
+ sqlite3changeset_finalize(pIter);
+ return rc;
+}
+
+/*
+** Obtain a buffer containing a changeset representing the concatenation
+** of all changesets added to the group so far.
+*/
+int sqlite3changegroup_output(
+ sqlite3_changegroup *pGrp,
+ int *pnData,
+ void **ppData
+){
+ return sessionChangegroupOutput(pGrp, 0, 0, pnData, ppData);
+}
+
+/*
+** Streaming versions of changegroup_add().
+*/
+int sqlite3changegroup_add_strm(
+ sqlite3_changegroup *pGrp,
+ int (*xInput)(void *pIn, void *pData, int *pnData),
+ void *pIn
+){
+ sqlite3_changeset_iter *pIter; /* Iterator opened on pData/nData */
+ int rc; /* Return code */
+
+ rc = sqlite3changeset_start_strm(&pIter, xInput, pIn);
+ if( rc==SQLITE_OK ){
+ rc = sessionChangesetToHash(pIter, pGrp, 0);
+ }
+ sqlite3changeset_finalize(pIter);
+ return rc;
+}
+
+/*
+** Streaming versions of changegroup_output().
+*/
+int sqlite3changegroup_output_strm(
+ sqlite3_changegroup *pGrp,
+ int (*xOutput)(void *pOut, const void *pData, int nData),
+ void *pOut
+){
+ return sessionChangegroupOutput(pGrp, xOutput, pOut, 0, 0);
+}
+
+/*
+** Delete a changegroup object.
+*/
+void sqlite3changegroup_delete(sqlite3_changegroup *pGrp){
+ if( pGrp ){
+ sessionDeleteTable(0, pGrp->pList);
+ sqlite3_free(pGrp);
+ }
+}
+
+/*
+** 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_changegroup *pGrp;
+ int rc;
+
+ rc = sqlite3changegroup_new(&pGrp);
+ if( rc==SQLITE_OK ){
+ rc = sqlite3changegroup_add(pGrp, nLeft, pLeft);
+ }
+ if( rc==SQLITE_OK ){
+ rc = sqlite3changegroup_add(pGrp, nRight, pRight);
+ }
+ if( rc==SQLITE_OK ){
+ rc = sqlite3changegroup_output(pGrp, pnOut, ppOut);
+ }
+ sqlite3changegroup_delete(pGrp);
+
+ 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_changegroup *pGrp;
+ int rc;
+
+ rc = sqlite3changegroup_new(&pGrp);
+ if( rc==SQLITE_OK ){
+ rc = sqlite3changegroup_add_strm(pGrp, xInputA, pInA);
+ }
+ if( rc==SQLITE_OK ){
+ rc = sqlite3changegroup_add_strm(pGrp, xInputB, pInB);
+ }
+ if( rc==SQLITE_OK ){
+ rc = sqlite3changegroup_output_strm(pGrp, xOutput, pOut);
+ }
+ sqlite3changegroup_delete(pGrp);
+
+ return rc;
+}
+
+/*
+** Changeset rebaser handle.
+*/
+struct sqlite3_rebaser {
+ sqlite3_changegroup grp; /* Hash table */
+};
+
+/*
+** Buffers a1 and a2 must both contain a sessions module record nCol
+** fields in size. This function appends an nCol sessions module
+** record to buffer pBuf that is a copy of a1, except that for
+** each field that is undefined in a1[], swap in the field from a2[].
+*/
+static void sessionAppendRecordMerge(
+ SessionBuffer *pBuf, /* Buffer to append to */
+ int nCol, /* Number of columns in each record */
+ u8 *a1, int n1, /* Record 1 */
+ u8 *a2, int n2, /* Record 2 */
+ int *pRc /* IN/OUT: error code */
+){
+ sessionBufferGrow(pBuf, n1+n2, pRc);
+ if( *pRc==SQLITE_OK ){
+ int i;
+ u8 *pOut = &pBuf->aBuf[pBuf->nBuf];
+ for(i=0; i<nCol; i++){
+ int nn1 = sessionSerialLen(a1);
+ int nn2 = sessionSerialLen(a2);
+ if( *a1==0 || *a1==0xFF ){
+ memcpy(pOut, a2, nn2);
+ pOut += nn2;
+ }else{
+ memcpy(pOut, a1, nn1);
+ pOut += nn1;
+ }
+ a1 += nn1;
+ a2 += nn2;
+ }
+
+ pBuf->nBuf = pOut-pBuf->aBuf;
+ assert( pBuf->nBuf<=pBuf->nAlloc );
+ }
+}
+
+/*
+** This function is called when rebasing a local UPDATE change against one
+** or more remote UPDATE changes. The aRec/nRec buffer contains the current
+** old.* and new.* records for the change. The rebase buffer (a single
+** record) is in aChange/nChange. The rebased change is appended to buffer
+** pBuf.
+**
+** Rebasing the UPDATE involves:
+**
+** * Removing any changes to fields for which the corresponding field
+** in the rebase buffer is set to "replaced" (type 0xFF). If this
+** means the UPDATE change updates no fields, nothing is appended
+** to the output buffer.
+**
+** * For each field modified by the local change for which the
+** corresponding field in the rebase buffer is not "undefined" (0x00)
+** or "replaced" (0xFF), the old.* value is replaced by the value
+** in the rebase buffer.
+*/
+static void sessionAppendPartialUpdate(
+ SessionBuffer *pBuf, /* Append record here */
+ sqlite3_changeset_iter *pIter, /* Iterator pointed at local change */
+ u8 *aRec, int nRec, /* Local change */
+ u8 *aChange, int nChange, /* Record to rebase against */
+ int *pRc /* IN/OUT: Return Code */
+){
+ sessionBufferGrow(pBuf, 2+nRec+nChange, pRc);
+ if( *pRc==SQLITE_OK ){
+ int bData = 0;
+ u8 *pOut = &pBuf->aBuf[pBuf->nBuf];
+ int i;
+ u8 *a1 = aRec;
+ u8 *a2 = aChange;
+
+ *pOut++ = SQLITE_UPDATE;
+ *pOut++ = pIter->bIndirect;
+ for(i=0; i<pIter->nCol; i++){
+ int n1 = sessionSerialLen(a1);
+ int n2 = sessionSerialLen(a2);
+ if( pIter->abPK[i] || a2[0]==0 ){
+ if( !pIter->abPK[i] && a1[0] ) bData = 1;
+ memcpy(pOut, a1, n1);
+ pOut += n1;
+ }else if( a2[0]!=0xFF && a1[0] ){
+ bData = 1;
+ memcpy(pOut, a2, n2);
+ pOut += n2;
+ }else{
+ *pOut++ = '\0';
+ }
+ a1 += n1;
+ a2 += n2;
+ }
+ if( bData ){
+ a2 = aChange;
+ for(i=0; i<pIter->nCol; i++){
+ int n1 = sessionSerialLen(a1);
+ int n2 = sessionSerialLen(a2);
+ if( pIter->abPK[i] || a2[0]!=0xFF ){
+ memcpy(pOut, a1, n1);
+ pOut += n1;
+ }else{
+ *pOut++ = '\0';
+ }
+ a1 += n1;
+ a2 += n2;
+ }
+ pBuf->nBuf = (pOut - pBuf->aBuf);
+ }
+ }
+}
+
+/*
+** pIter is configured to iterate through a changeset. This function rebases
+** that changeset according to the current configuration of the rebaser
+** object passed as the first argument. If no error occurs and argument xOutput
+** is not NULL, then the changeset is returned to the caller by invoking
+** xOutput zero or more times and SQLITE_OK returned. Or, if xOutput is NULL,
+** then (*ppOut) is set to point to a buffer containing the rebased changeset
+** before this function returns. In this case (*pnOut) is set to the size of
+** the buffer in bytes. It is the responsibility of the caller to eventually
+** free the (*ppOut) buffer using sqlite3_free().
+**
+** If an error occurs, an SQLite error code is returned. If ppOut and
+** pnOut are not NULL, then the two output parameters are set to 0 before
+** returning.
+*/
+static int sessionRebase(
+ sqlite3_rebaser *p, /* Rebaser hash table */
+ sqlite3_changeset_iter *pIter, /* Input data */
+ int (*xOutput)(void *pOut, const void *pData, int nData),
+ void *pOut, /* Context for xOutput callback */
+ int *pnOut, /* OUT: Number of bytes in output changeset */
+ void **ppOut /* OUT: Inverse of pChangeset */
+){
+ int rc = SQLITE_OK;
+ u8 *aRec = 0;
+ int nRec = 0;
+ int bNew = 0;
+ SessionTable *pTab = 0;
+ SessionBuffer sOut = {0,0,0};
+
+ while( SQLITE_ROW==sessionChangesetNext(pIter, &aRec, &nRec, &bNew) ){
+ SessionChange *pChange = 0;
+ int bDone = 0;
+
+ if( bNew ){
+ const char *zTab = pIter->zTab;
+ for(pTab=p->grp.pList; pTab; pTab=pTab->pNext){
+ if( 0==sqlite3_stricmp(pTab->zName, zTab) ) break;
+ }
+ bNew = 0;
+
+ /* A patchset may not be rebased */
+ if( pIter->bPatchset ){
+ rc = SQLITE_ERROR;
+ }
+
+ /* Append a table header to the output for this new table */
+ sessionAppendByte(&sOut, pIter->bPatchset ? 'P' : 'T', &rc);
+ sessionAppendVarint(&sOut, pIter->nCol, &rc);
+ sessionAppendBlob(&sOut, pIter->abPK, pIter->nCol, &rc);
+ sessionAppendBlob(&sOut,(u8*)pIter->zTab,(int)strlen(pIter->zTab)+1,&rc);
+ }
+
+ if( pTab && rc==SQLITE_OK ){
+ int iHash = sessionChangeHash(pTab, 0, aRec, pTab->nChange);
+
+ for(pChange=pTab->apChange[iHash]; pChange; pChange=pChange->pNext){
+ if( sessionChangeEqual(pTab, 0, aRec, 0, pChange->aRecord) ){
+ break;
+ }
+ }
+ }
+
+ if( pChange ){
+ assert( pChange->op==SQLITE_DELETE || pChange->op==SQLITE_INSERT );
+ switch( pIter->op ){
+ case SQLITE_INSERT:
+ if( pChange->op==SQLITE_INSERT ){
+ bDone = 1;
+ if( pChange->bIndirect==0 ){
+ sessionAppendByte(&sOut, SQLITE_UPDATE, &rc);
+ sessionAppendByte(&sOut, pIter->bIndirect, &rc);
+ sessionAppendBlob(&sOut, pChange->aRecord, pChange->nRecord, &rc);
+ sessionAppendBlob(&sOut, aRec, nRec, &rc);
+ }
+ }
+ break;
+
+ case SQLITE_UPDATE:
+ bDone = 1;
+ if( pChange->op==SQLITE_DELETE ){
+ if( pChange->bIndirect==0 ){
+ u8 *pCsr = aRec;
+ sessionSkipRecord(&pCsr, pIter->nCol);
+ sessionAppendByte(&sOut, SQLITE_INSERT, &rc);
+ sessionAppendByte(&sOut, pIter->bIndirect, &rc);
+ sessionAppendRecordMerge(&sOut, pIter->nCol,
+ pCsr, nRec-(pCsr-aRec),
+ pChange->aRecord, pChange->nRecord, &rc
+ );
+ }
+ }else{
+ sessionAppendPartialUpdate(&sOut, pIter,
+ aRec, nRec, pChange->aRecord, pChange->nRecord, &rc
+ );
+ }
+ break;
+
+ default:
+ assert( pIter->op==SQLITE_DELETE );
+ bDone = 1;
+ if( pChange->op==SQLITE_INSERT ){
+ sessionAppendByte(&sOut, SQLITE_DELETE, &rc);
+ sessionAppendByte(&sOut, pIter->bIndirect, &rc);
+ sessionAppendRecordMerge(&sOut, pIter->nCol,
+ pChange->aRecord, pChange->nRecord, aRec, nRec, &rc
+ );
+ }
+ break;
+ }
+ }
+
+ if( bDone==0 ){
+ sessionAppendByte(&sOut, pIter->op, &rc);
+ sessionAppendByte(&sOut, pIter->bIndirect, &rc);
+ sessionAppendBlob(&sOut, aRec, nRec, &rc);
+ }
+ if( rc==SQLITE_OK && xOutput && sOut.nBuf>sessions_strm_chunk_size ){
+ rc = xOutput(pOut, sOut.aBuf, sOut.nBuf);
+ sOut.nBuf = 0;
+ }
+ if( rc ) break;
+ }
+
+ if( rc!=SQLITE_OK ){
+ sqlite3_free(sOut.aBuf);
+ memset(&sOut, 0, sizeof(sOut));
+ }
+
+ if( rc==SQLITE_OK ){
+ if( xOutput ){
+ if( sOut.nBuf>0 ){
+ rc = xOutput(pOut, sOut.aBuf, sOut.nBuf);
+ }
+ }else if( ppOut ){
+ *ppOut = (void*)sOut.aBuf;
+ *pnOut = sOut.nBuf;
+ sOut.aBuf = 0;
+ }
+ }
+ sqlite3_free(sOut.aBuf);
+ return rc;
+}
+
+/*
+** Create a new rebaser object.
+*/
+int sqlite3rebaser_create(sqlite3_rebaser **ppNew){
+ int rc = SQLITE_OK;
+ sqlite3_rebaser *pNew;
+
+ pNew = sqlite3_malloc(sizeof(sqlite3_rebaser));
+ if( pNew==0 ){
+ rc = SQLITE_NOMEM;
+ }else{
+ memset(pNew, 0, sizeof(sqlite3_rebaser));
+ }
+ *ppNew = pNew;
+ return rc;
+}
+
+/*
+** Call this one or more times to configure a rebaser.
+*/
+int sqlite3rebaser_configure(
+ sqlite3_rebaser *p,
+ int nRebase, const void *pRebase
+){
+ sqlite3_changeset_iter *pIter = 0; /* Iterator opened on pData/nData */
+ int rc; /* Return code */
+ rc = sqlite3changeset_start(&pIter, nRebase, (void*)pRebase);
+ if( rc==SQLITE_OK ){
+ rc = sessionChangesetToHash(pIter, &p->grp, 1);
+ }
+ sqlite3changeset_finalize(pIter);
+ return rc;
+}
+
+/*
+** Rebase a changeset according to current rebaser configuration
+*/
+int sqlite3rebaser_rebase(
+ sqlite3_rebaser *p,
+ int nIn, const void *pIn,
+ int *pnOut, void **ppOut
+){
+ sqlite3_changeset_iter *pIter = 0; /* Iterator to skip through input */
+ int rc = sqlite3changeset_start(&pIter, nIn, (void*)pIn);
+
+ if( rc==SQLITE_OK ){
+ rc = sessionRebase(p, pIter, 0, 0, pnOut, ppOut);
+ sqlite3changeset_finalize(pIter);
+ }
+
+ return rc;
+}
+
+/*
+** Rebase a changeset according to current rebaser configuration
+*/
+int sqlite3rebaser_rebase_strm(
+ sqlite3_rebaser *p,
+ int (*xInput)(void *pIn, void *pData, int *pnData),
+ void *pIn,
+ int (*xOutput)(void *pOut, const void *pData, int nData),
+ void *pOut
+){
+ sqlite3_changeset_iter *pIter = 0; /* Iterator to skip through input */
+ int rc = sqlite3changeset_start_strm(&pIter, xInput, pIn);
+
+ if( rc==SQLITE_OK ){
+ rc = sessionRebase(p, pIter, xOutput, pOut, 0, 0);
+ sqlite3changeset_finalize(pIter);
+ }
+
+ return rc;
+}
+
+/*
+** Destroy a rebaser object
+*/
+void sqlite3rebaser_delete(sqlite3_rebaser *p){
+ if( p ){
+ sessionDeleteTable(0, p->grp.pList);
+ sqlite3_free(p);
+ }
+}
+
+/*
+** Global configuration
+*/
+int sqlite3session_config(int op, void *pArg){
+ int rc = SQLITE_OK;
+ switch( op ){
+ case SQLITE_SESSION_CONFIG_STRMSIZE: {
+ int *pInt = (int*)pArg;
+ if( *pInt>0 ){
+ sessions_strm_chunk_size = *pInt;
+ }
+ *pInt = sessions_strm_chunk_size;
+ break;
+ }
+ default:
+ rc = SQLITE_MISUSE;
+ break;
+ }
+ 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 0000000..10d0133
--- /dev/null
+++ b/ext/session/sqlite3session.h
@@ -0,0 +1,1721 @@
+
+#if !defined(__SQLITESESSION_H_) && defined(SQLITE_ENABLE_SESSION)
+#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
+**
+** An instance of this object is a [session] that can be used to
+** record changes to a database.
+*/
+typedef struct sqlite3_session sqlite3_session;
+
+/*
+** CAPI3REF: Changeset Iterator Handle
+**
+** An instance of this object acts as a cursor for iterating
+** over the elements of a [changeset] or [patchset].
+*/
+typedef struct sqlite3_changeset_iter sqlite3_changeset_iter;
+
+/*
+** CAPI3REF: Create A New Session Object
+** CONSTRUCTOR: sqlite3_session
+**
+** 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
+** DESTRUCTOR: sqlite3_session
+**
+** 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);
+
+/*
+** CAPIREF: Conigure a Session Object
+** METHOD: sqlite3_session
+**
+** This method is used to configure a session object after it has been
+** created. At present the only valid value for the second parameter is
+** [SQLITE_SESSION_OBJCONFIG_SIZE].
+**
+** Arguments for sqlite3session_object_config()
+**
+** The following values may passed as the the 4th parameter to
+** sqlite3session_object_config().
+**
+** <dt>SQLITE_SESSION_OBJCONFIG_SIZE <dd>
+** This option is used to set, clear or query the flag that enables
+** the [sqlite3session_changeset_size()] API. Because it imposes some
+** computational overhead, this API is disabled by default. Argument
+** pArg must point to a value of type (int). If the value is initially
+** 0, then the sqlite3session_changeset_size() API is disabled. If it
+** is greater than 0, then the same API is enabled. Or, if the initial
+** value is less than zero, no change is made. In all cases the (int)
+** variable is set to 1 if the sqlite3session_changeset_size() API is
+** enabled following the current call, or 0 otherwise.
+**
+** It is an error (SQLITE_MISUSE) to attempt to modify this setting after
+** the first table has been attached to the session object.
+*/
+int sqlite3session_object_config(sqlite3_session*, int op, void *pArg);
+
+/*
+*/
+#define SQLITE_SESSION_OBJCONFIG_SIZE 1
+
+/*
+** CAPI3REF: Enable Or Disable A Session Object
+** METHOD: sqlite3_session
+**
+** 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
+** METHOD: sqlite3_session
+**
+** 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
+** METHOD: sqlite3_session
+**
+** 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.
+**
+** <h3>Special sqlite_stat1 Handling</h3>
+**
+** As of SQLite version 3.22.0, the "sqlite_stat1" table is an exception to
+** some of the rules above. In SQLite, the schema of sqlite_stat1 is:
+** <pre>
+** &nbsp; CREATE TABLE sqlite_stat1(tbl,idx,stat)
+** </pre>
+**
+** Even though sqlite_stat1 does not have a PRIMARY KEY, changes are
+** recorded for it as if the PRIMARY KEY is (tbl,idx). Additionally, changes
+** are recorded for rows for which (idx IS NULL) is true. However, for such
+** rows a zero-length blob (SQL value X'') is stored in the changeset or
+** patchset instead of a NULL value. This allows such changesets to be
+** manipulated by legacy implementations of sqlite3changeset_invert(),
+** concat() and similar.
+**
+** The sqlite3changeset_apply() function automatically converts the
+** zero-length blob back to a NULL value when updating the sqlite_stat1
+** table. However, if the application calls sqlite3changeset_new(),
+** sqlite3changeset_old() or sqlite3changeset_conflict on a changeset
+** iterator directly (including on a changeset iterator passed to a
+** conflict-handler callback) then the X'' value is returned. The application
+** must translate X'' to NULL itself if required.
+**
+** Legacy (older than 3.22.0) versions of the sessions module cannot capture
+** changes made to the sqlite_stat1 table. Legacy versions of the
+** sqlite3changeset_apply() function silently ignore any modifications to the
+** sqlite_stat1 table that are part of a changeset or patchset.
+*/
+int sqlite3session_attach(
+ sqlite3_session *pSession, /* Session object */
+ const char *zTab /* Table name */
+);
+
+/*
+** CAPI3REF: Set a table filter on a Session Object.
+** METHOD: sqlite3_session
+**
+** The second argument (xFilter) is the "filter callback". For changes to rows
+** in tables that are not attached to the Session object, the filter is called
+** to determine whether changes to the table's rows should be tracked or not.
+** If xFilter returns 0, changes are 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
+** METHOD: sqlite3_session
+**
+** 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.
+**
+** Within a changeset generated by this function, all changes related to a
+** single table are grouped together. In other words, when iterating through
+** a changeset or when applying a changeset to a database, all changes related
+** to a single table are processed before moving on to the next table. Tables
+** are sorted in the same order in which they were attached (or auto-attached)
+** to the sqlite3_session object. The order in which the changes related to
+** a single table are stored is undefined.
+**
+** 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: Return An Upper-limit For The Size Of The Changeset
+** METHOD: sqlite3_session
+**
+** By default, this function always returns 0. For it to return
+** a useful result, the sqlite3_session object must have been configured
+** to enable this API using sqlite3session_object_config() with the
+** SQLITE_SESSION_OBJCONFIG_SIZE verb.
+**
+** When enabled, this function returns an upper limit, in bytes, for the size
+** of the changeset that might be produced if sqlite3session_changeset() were
+** called. The final changeset size might be equal to or smaller than the
+** size in bytes returned by this function.
+*/
+sqlite3_int64 sqlite3session_changeset_size(sqlite3_session *pSession);
+
+/*
+** CAPI3REF: Load The Difference Between Tables Into A Session
+** METHOD: sqlite3_session
+**
+** If it is not already attached to the session object passed as the first
+** argument, this function attaches table zTbl in the same manner as the
+** [sqlite3session_attach()] function. If zTbl does not exist, or if it
+** does not have a primary key, this function is a no-op (but does not return
+** an error).
+**
+** Argument zFromDb must be the name of a database ("main", "temp" etc.)
+** attached to the same database handle as the session object that contains
+** a table compatible with the table attached to the session by this function.
+** A table is considered compatible if it:
+**
+** <ul>
+** <li> Has the same name,
+** <li> Has the same set of columns declared in the same order, and
+** <li> Has the same PRIMARY KEY definition.
+** </ul>
+**
+** If the tables are not compatible, SQLITE_SCHEMA is returned. If the tables
+** are compatible but do not have any PRIMARY KEY columns, it is not an error
+** but no changes are added to the session object. As with other session
+** APIs, tables without PRIMARY KEYs are simply ignored.
+**
+** This function adds a set of changes to the session object that could be
+** used to update the table in database zFrom (call this the "from-table")
+** so that its content is the same as the table attached to the session
+** object (call this the "to-table"). Specifically:
+**
+** <ul>
+** <li> For each row (primary key) that exists in the to-table but not in
+** the from-table, an INSERT record is added to the session object.
+**
+** <li> For each row (primary key) that exists in the to-table but not in
+** the from-table, a DELETE record is added to the session object.
+**
+** <li> For each row (primary key) that exists in both tables, but features
+** different non-PK values in each, an UPDATE record is added to the
+** session.
+** </ul>
+**
+** To clarify, if this function is called and then a changeset constructed
+** using [sqlite3session_changeset()], then after applying that changeset to
+** database zFrom the contents of the two compatible tables would be
+** identical.
+**
+** It an error if database zFrom does not exist or does not contain the
+** required compatible table.
+**
+** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite
+** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg
+** may be set to point to a buffer containing an English language error
+** message. It is the responsibility of the caller to free this buffer using
+** sqlite3_free().
+*/
+int sqlite3session_diff(
+ sqlite3_session *pSession,
+ const char *zFromDb,
+ const char *zTbl,
+ char **pzErrMsg
+);
+
+
+/*
+** CAPI3REF: Generate A Patchset From A Session Object
+** METHOD: sqlite3_session
+**
+** 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.
+**
+** Changes within a patchset are ordered in the same way as for changesets
+** generated by the sqlite3session_changeset() function (i.e. all changes for
+** a single table are grouped together, tables appear in the order in which
+** they were attached to the session object).
+*/
+int sqlite3session_patchset(
+ sqlite3_session *pSession, /* Session object */
+ int *pnPatchset, /* OUT: Size of buffer at *ppPatchset */
+ void **ppPatchset /* OUT: Buffer containing patchset */
+);
+
+/*
+** 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: Query for the amount of heap memory used by a session object.
+**
+** This API returns the total amount of heap memory in bytes currently
+** used by the session object passed as the only argument.
+*/
+sqlite3_int64 sqlite3session_memory_used(sqlite3_session *pSession);
+
+/*
+** CAPI3REF: Create An Iterator To Traverse A Changeset
+** CONSTRUCTOR: sqlite3_changeset_iter
+**
+** 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 visited
+** 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.
+**
+** The behavior of sqlite3changeset_start_v2() and its streaming equivalent
+** may be modified by passing a combination of
+** [SQLITE_CHANGESETSTART_INVERT | supported flags] as the 4th parameter.
+**
+** Note that the sqlite3changeset_start_v2() API is still <b>experimental</b>
+** and therefore subject to change.
+*/
+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 */
+);
+int sqlite3changeset_start_v2(
+ sqlite3_changeset_iter **pp, /* OUT: New changeset iterator handle */
+ int nChangeset, /* Size of changeset blob in bytes */
+ void *pChangeset, /* Pointer to blob containing changeset */
+ int flags /* SESSION_CHANGESETSTART_* flags */
+);
+
+/*
+** CAPI3REF: Flags for sqlite3changeset_start_v2
+**
+** The following flags may passed via the 4th parameter to
+** [sqlite3changeset_start_v2] and [sqlite3changeset_start_v2_strm]:
+**
+** <dt>SQLITE_CHANGESETAPPLY_INVERT <dd>
+** Invert the changeset while iterating through it. This is equivalent to
+** inverting a changeset using sqlite3changeset_invert() before applying it.
+** It is an error to specify this flag with a patchset.
+*/
+#define SQLITE_CHANGESETSTART_INVERT 0x0002
+
+
+/*
+** CAPI3REF: Advance A Changeset Iterator
+** METHOD: sqlite3_changeset_iter
+**
+** This function may only be used with iterators created by the 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
+** METHOD: sqlite3_changeset_iter
+**
+** 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].
+**
+** Arguments pOp, pnCol and pzTab may not be NULL. Upon return, three
+** outputs are set through these pointers:
+**
+** *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;
+**
+** *pnCol is set to the number of columns in the table affected by the change; and
+**
+** *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 pbIndirect 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.
+**
+** 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
+** METHOD: sqlite3_changeset_iter
+**
+** 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 argument 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
+** METHOD: sqlite3_changeset_iter
+**
+** 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
+** METHOD: sqlite3_changeset_iter
+**
+** 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
+** METHOD: sqlite3_changeset_iter
+**
+** 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
+** METHOD: sqlite3_changeset_iter
+**
+** 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
+** METHOD: sqlite3_changeset_iter
+**
+** 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):
+**
+** <pre>
+** sqlite3changeset_start();
+** while( SQLITE_ROW==sqlite3changeset_next() ){
+** // Do something with change.
+** }
+** rc = sqlite3changeset_finalize();
+** if( rc!=SQLITE_OK ){
+** // An error has occurred
+** }
+** </pre>
+*/
+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>
+**
+** This function does not change the order in which changes appear within
+** the changeset. It merely reverses the sense of each individual change.
+**
+** 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.
+**
+** This function combines the two input changesets using an
+** sqlite3_changegroup object. Calling it produces similar results as the
+** following code fragment:
+**
+** <pre>
+** sqlite3_changegroup *pGrp;
+** rc = sqlite3_changegroup_new(&pGrp);
+** if( rc==SQLITE_OK ) rc = sqlite3changegroup_add(pGrp, nA, pA);
+** if( rc==SQLITE_OK ) rc = sqlite3changegroup_add(pGrp, nB, pB);
+** if( rc==SQLITE_OK ){
+** rc = sqlite3changegroup_output(pGrp, pnOut, ppOut);
+** }else{
+** *ppOut = 0;
+** *pnOut = 0;
+** }
+** </pre>
+**
+** Refer to the sqlite3_changegroup documentation below for details.
+*/
+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: Changegroup Handle
+**
+** A changegroup is an object used to combine two or more
+** [changesets] or [patchsets]
+*/
+typedef struct sqlite3_changegroup sqlite3_changegroup;
+
+/*
+** CAPI3REF: Create A New Changegroup Object
+** CONSTRUCTOR: sqlite3_changegroup
+**
+** An sqlite3_changegroup object is used to combine two or more changesets
+** (or patchsets) into a single changeset (or patchset). A single changegroup
+** object may combine changesets or patchsets, but not both. The output is
+** always in the same format as the input.
+**
+** If successful, this function returns SQLITE_OK and populates (*pp) with
+** a pointer to a new sqlite3_changegroup object before returning. The caller
+** should eventually free the returned object using a call to
+** sqlite3changegroup_delete(). If an error occurs, an SQLite error code
+** (i.e. SQLITE_NOMEM) is returned and *pp is set to NULL.
+**
+** The usual usage pattern for an sqlite3_changegroup object is as follows:
+**
+** <ul>
+** <li> It is created using a call to sqlite3changegroup_new().
+**
+** <li> Zero or more changesets (or patchsets) are added to the object
+** by calling sqlite3changegroup_add().
+**
+** <li> The result of combining all input changesets together is obtained
+** by the application via a call to sqlite3changegroup_output().
+**
+** <li> The object is deleted using a call to sqlite3changegroup_delete().
+** </ul>
+**
+** Any number of calls to add() and output() may be made between the calls to
+** new() and delete(), and in any order.
+**
+** As well as the regular sqlite3changegroup_add() and
+** sqlite3changegroup_output() functions, also available are the streaming
+** versions sqlite3changegroup_add_strm() and sqlite3changegroup_output_strm().
+*/
+int sqlite3changegroup_new(sqlite3_changegroup **pp);
+
+/*
+** CAPI3REF: Add A Changeset To A Changegroup
+** METHOD: sqlite3_changegroup
+**
+** Add all changes within the changeset (or patchset) in buffer pData (size
+** nData bytes) to the changegroup.
+**
+** If the buffer contains a patchset, then all prior calls to this function
+** on the same changegroup object must also have specified patchsets. Or, if
+** the buffer contains a changeset, so must have the earlier calls to this
+** function. Otherwise, SQLITE_ERROR is returned and no changes are added
+** to the changegroup.
+**
+** Rows within the changeset and changegroup are identified by the values in
+** their PRIMARY KEY columns. A change in the changeset is considered to
+** apply to the same row as a change already present in the changegroup if
+** the two rows have the same primary key.
+**
+** Changes to rows that do not already appear in the changegroup are
+** simply copied into it. Or, if both the new changeset and the changegroup
+** contain changes that apply to a single row, the final contents of the
+** changegroup 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">Existing Change </th>
+** <th style="white-space:pre">New Change </th>
+** <th>Output Change
+** <tr><td>INSERT <td>INSERT <td>
+** The new change is ignored. This case does not occur if the new
+** changeset was recorded immediately after the changesets already
+** added to the changegroup.
+** <tr><td>INSERT <td>UPDATE <td>
+** The INSERT change remains in the changegroup. The values in the
+** INSERT change are modified as if the row was inserted by the
+** existing change and then updated according to the new change.
+** <tr><td>INSERT <td>DELETE <td>
+** The existing INSERT is removed from the changegroup. The DELETE is
+** not added.
+** <tr><td>UPDATE <td>INSERT <td>
+** The new change is ignored. This case does not occur if the new
+** changeset was recorded immediately after the changesets already
+** added to the changegroup.
+** <tr><td>UPDATE <td>UPDATE <td>
+** The existing UPDATE remains within the changegroup. It is amended
+** so that the accompanying values are as if the row was updated once
+** by the existing change and then again by the new change.
+** <tr><td>UPDATE <td>DELETE <td>
+** The existing UPDATE is replaced by the new DELETE within the
+** changegroup.
+** <tr><td>DELETE <td>INSERT <td>
+** If one or more of the column values in the row inserted by the
+** new change differ from those in the row deleted by the existing
+** change, the existing DELETE is replaced by an UPDATE within the
+** changegroup. Otherwise, if the inserted row is exactly the same
+** as the deleted row, the existing DELETE is simply discarded.
+** <tr><td>DELETE <td>UPDATE <td>
+** The new change is ignored. This case does not occur if the new
+** changeset was recorded immediately after the changesets already
+** added to the changegroup.
+** <tr><td>DELETE <td>DELETE <td>
+** The new change is ignored. This case does not occur if the new
+** changeset was recorded immediately after the changesets already
+** added to the changegroup.
+** </table>
+**
+** If the new changeset contains changes to a table that is already present
+** in the changegroup, then the number of columns and the position of the
+** primary key columns for the table must be consistent. If this is not the
+** case, this function fails with SQLITE_SCHEMA. If the input changeset
+** appears 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. In all cases, if an error occurs the state
+** of the final contents of the changegroup is undefined.
+**
+** If no error occurs, SQLITE_OK is returned.
+*/
+int sqlite3changegroup_add(sqlite3_changegroup*, int nData, void *pData);
+
+/*
+** CAPI3REF: Obtain A Composite Changeset From A Changegroup
+** METHOD: sqlite3_changegroup
+**
+** Obtain a buffer containing a changeset (or patchset) representing the
+** current contents of the changegroup. If the inputs to the changegroup
+** were themselves changesets, the output is a changeset. Or, if the
+** inputs were patchsets, the output is also a patchset.
+**
+** As with the output of the sqlite3session_changeset() and
+** sqlite3session_patchset() functions, all changes related to a single
+** table are grouped together in the output of this function. Tables appear
+** in the same order as for the very first changeset added to the changegroup.
+** If the second or subsequent changesets added to the changegroup contain
+** changes for tables that do not appear in the first changeset, they are
+** appended onto the end of the output changeset, again in the order in
+** which they are first encountered.
+**
+** If an error occurs, an SQLite error code is returned and the output
+** variables (*pnData) and (*ppData) are set to 0. Otherwise, SQLITE_OK
+** is returned and the output variables are set to the size of and a
+** pointer to the output buffer, respectively. In this case it is the
+** responsibility of the caller to eventually free the buffer using a
+** call to sqlite3_free().
+*/
+int sqlite3changegroup_output(
+ sqlite3_changegroup*,
+ int *pnData, /* OUT: Size of output buffer in bytes */
+ void **ppData /* OUT: Pointer to output buffer */
+);
+
+/*
+** CAPI3REF: Delete A Changegroup Object
+** DESTRUCTOR: sqlite3_changegroup
+*/
+void sqlite3changegroup_delete(sqlite3_changegroup*);
+
+/*
+** CAPI3REF: Apply A Changeset To A Database
+**
+** Apply a changeset or patchset to a database. These functions attempt 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 these functions 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 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
+** 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 at least as many 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, the 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 the
+** database table has more columns than are recorded in the changeset,
+** only the values of those non-primary key fields are compared against
+** the current database contents - any trailing database table columns
+** are ignored.
+**
+** 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 changeset row contains fewer fields than the
+** database table, the trailing fields are populated with their default
+** values.
+**
+** 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, the 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 modified 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 modified 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 application's conflict
+** resolution strategy.
+**
+** All changes made by these functions 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.
+**
+** If the output parameters (ppRebase) and (pnRebase) are non-NULL and
+** the input is a changeset (not a patchset), then sqlite3changeset_apply_v2()
+** may set (*ppRebase) to point to a "rebase" that may be used with the
+** sqlite3_rebaser APIs buffer before returning. In this case (*pnRebase)
+** is set to the size of the buffer in bytes. It is the responsibility of the
+** caller to eventually free any such buffer using sqlite3_free(). The buffer
+** is only allocated and populated if one or more conflicts were encountered
+** while applying the patchset. See comments surrounding the sqlite3_rebaser
+** APIs for further details.
+**
+** The behavior of sqlite3changeset_apply_v2() and its streaming equivalent
+** may be modified by passing a combination of
+** [SQLITE_CHANGESETAPPLY_NOSAVEPOINT | supported flags] as the 9th parameter.
+**
+** Note that the sqlite3changeset_apply_v2() API is still <b>experimental</b>
+** and therefore subject to 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 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_apply_v2(
+ 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 */
+ void **ppRebase, int *pnRebase, /* OUT: Rebase data */
+ int flags /* SESSION_CHANGESETAPPLY_* flags */
+);
+
+/*
+** CAPI3REF: Flags for sqlite3changeset_apply_v2
+**
+** The following flags may passed via the 9th parameter to
+** [sqlite3changeset_apply_v2] and [sqlite3changeset_apply_v2_strm]:
+**
+** <dl>
+** <dt>SQLITE_CHANGESETAPPLY_NOSAVEPOINT <dd>
+** Usually, the sessions module encloses all operations performed by
+** a single call to apply_v2() or apply_v2_strm() in a [SAVEPOINT]. The
+** SAVEPOINT is committed if the changeset or patchset is successfully
+** applied, or rolled back if an error occurs. Specifying this flag
+** causes the sessions module to omit this savepoint. In this case, if the
+** caller has an open transaction or savepoint when apply_v2() is called,
+** it may revert the partially applied changeset by rolling it back.
+**
+** <dt>SQLITE_CHANGESETAPPLY_INVERT <dd>
+** Invert the changeset before applying it. This is equivalent to inverting
+** a changeset using sqlite3changeset_invert() before applying it. It is
+** an error to specify this flag with a patchset.
+*/
+#define SQLITE_CHANGESETAPPLY_NOSAVEPOINT 0x0001
+#define SQLITE_CHANGESETAPPLY_INVERT 0x0002
+
+/*
+** 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: Rebasing changesets
+** EXPERIMENTAL
+**
+** Suppose there is a site hosting a database in state S0. And that
+** modifications are made that move that database to state S1 and a
+** changeset recorded (the "local" changeset). Then, a changeset based
+** on S0 is received from another site (the "remote" changeset) and
+** applied to the database. The database is then in state
+** (S1+"remote"), where the exact state depends on any conflict
+** resolution decisions (OMIT or REPLACE) made while applying "remote".
+** Rebasing a changeset is to update it to take those conflict
+** resolution decisions into account, so that the same conflicts
+** do not have to be resolved elsewhere in the network.
+**
+** For example, if both the local and remote changesets contain an
+** INSERT of the same key on "CREATE TABLE t1(a PRIMARY KEY, b)":
+**
+** local: INSERT INTO t1 VALUES(1, 'v1');
+** remote: INSERT INTO t1 VALUES(1, 'v2');
+**
+** and the conflict resolution is REPLACE, then the INSERT change is
+** removed from the local changeset (it was overridden). Or, if the
+** conflict resolution was "OMIT", then the local changeset is modified
+** to instead contain:
+**
+** UPDATE t1 SET b = 'v2' WHERE a=1;
+**
+** Changes within the local changeset are rebased as follows:
+**
+** <dl>
+** <dt>Local INSERT<dd>
+** This may only conflict with a remote INSERT. If the conflict
+** resolution was OMIT, then add an UPDATE change to the rebased
+** changeset. Or, if the conflict resolution was REPLACE, add
+** nothing to the rebased changeset.
+**
+** <dt>Local DELETE<dd>
+** This may conflict with a remote UPDATE or DELETE. In both cases the
+** only possible resolution is OMIT. If the remote operation was a
+** DELETE, then add no change to the rebased changeset. If the remote
+** operation was an UPDATE, then the old.* fields of change are updated
+** to reflect the new.* values in the UPDATE.
+**
+** <dt>Local UPDATE<dd>
+** This may conflict with a remote UPDATE or DELETE. If it conflicts
+** with a DELETE, and the conflict resolution was OMIT, then the update
+** is changed into an INSERT. Any undefined values in the new.* record
+** from the update change are filled in using the old.* values from
+** the conflicting DELETE. Or, if the conflict resolution was REPLACE,
+** the UPDATE change is simply omitted from the rebased changeset.
+**
+** If conflict is with a remote UPDATE and the resolution is OMIT, then
+** the old.* values are rebased using the new.* values in the remote
+** change. Or, if the resolution is REPLACE, then the change is copied
+** into the rebased changeset with updates to columns also updated by
+** the conflicting remote UPDATE removed. If this means no columns would
+** be updated, the change is omitted.
+** </dl>
+**
+** A local change may be rebased against multiple remote changes
+** simultaneously. If a single key is modified by multiple remote
+** changesets, they are combined as follows before the local changeset
+** is rebased:
+**
+** <ul>
+** <li> If there has been one or more REPLACE resolutions on a
+** key, it is rebased according to a REPLACE.
+**
+** <li> If there have been no REPLACE resolutions on a key, then
+** the local changeset is rebased according to the most recent
+** of the OMIT resolutions.
+** </ul>
+**
+** Note that conflict resolutions from multiple remote changesets are
+** combined on a per-field basis, not per-row. This means that in the
+** case of multiple remote UPDATE operations, some fields of a single
+** local change may be rebased for REPLACE while others are rebased for
+** OMIT.
+**
+** In order to rebase a local changeset, the remote changeset must first
+** be applied to the local database using sqlite3changeset_apply_v2() and
+** the buffer of rebase information captured. Then:
+**
+** <ol>
+** <li> An sqlite3_rebaser object is created by calling
+** sqlite3rebaser_create().
+** <li> The new object is configured with the rebase buffer obtained from
+** sqlite3changeset_apply_v2() by calling sqlite3rebaser_configure().
+** If the local changeset is to be rebased against multiple remote
+** changesets, then sqlite3rebaser_configure() should be called
+** multiple times, in the same order that the multiple
+** sqlite3changeset_apply_v2() calls were made.
+** <li> Each local changeset is rebased by calling sqlite3rebaser_rebase().
+** <li> The sqlite3_rebaser object is deleted by calling
+** sqlite3rebaser_delete().
+** </ol>
+*/
+typedef struct sqlite3_rebaser sqlite3_rebaser;
+
+/*
+** CAPI3REF: Create a changeset rebaser object.
+** EXPERIMENTAL
+**
+** Allocate a new changeset rebaser object. If successful, set (*ppNew) to
+** point to the new object and return SQLITE_OK. Otherwise, if an error
+** occurs, return an SQLite error code (e.g. SQLITE_NOMEM) and set (*ppNew)
+** to NULL.
+*/
+int sqlite3rebaser_create(sqlite3_rebaser **ppNew);
+
+/*
+** CAPI3REF: Configure a changeset rebaser object.
+** EXPERIMENTAL
+**
+** Configure the changeset rebaser object to rebase changesets according
+** to the conflict resolutions described by buffer pRebase (size nRebase
+** bytes), which must have been obtained from a previous call to
+** sqlite3changeset_apply_v2().
+*/
+int sqlite3rebaser_configure(
+ sqlite3_rebaser*,
+ int nRebase, const void *pRebase
+);
+
+/*
+** CAPI3REF: Rebase a changeset
+** EXPERIMENTAL
+**
+** Argument pIn must point to a buffer containing a changeset nIn bytes
+** in size. This function allocates and populates a buffer with a copy
+** of the changeset rebased according to the configuration of the
+** rebaser object passed as the first argument. If successful, (*ppOut)
+** is set to point to the new buffer containing the rebased changeset and
+** (*pnOut) to its size in bytes and SQLITE_OK returned. It is the
+** responsibility of the caller to eventually free the new buffer using
+** sqlite3_free(). Otherwise, if an error occurs, (*ppOut) and (*pnOut)
+** are set to zero and an SQLite error code returned.
+*/
+int sqlite3rebaser_rebase(
+ sqlite3_rebaser*,
+ int nIn, const void *pIn,
+ int *pnOut, void **ppOut
+);
+
+/*
+** CAPI3REF: Delete a changeset rebaser object.
+** EXPERIMENTAL
+**
+** Delete the changeset rebaser object and all associated resources. There
+** should be one call to this function for each successful invocation
+** of sqlite3rebaser_create().
+*/
+void sqlite3rebaser_delete(sqlite3_rebaser *p);
+
+/*
+** 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_strm<td>[sqlite3changeset_apply]
+** <tr><td>sqlite3changeset_apply_strm_v2<td>[sqlite3changeset_apply_v2]
+** <tr><td>sqlite3changeset_concat_strm<td>[sqlite3changeset_concat]
+** <tr><td>sqlite3changeset_invert_strm<td>[sqlite3changeset_invert]
+** <tr><td>sqlite3changeset_start_strm<td>[sqlite3changeset_start]
+** <tr><td>sqlite3session_changeset_strm<td>[sqlite3session_changeset]
+** <tr><td>sqlite3session_patchset_strm<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>
+** &nbsp; int nChangeset,
+** &nbsp; void *pChangeset,
+** </pre>
+**
+** Is replaced by:
+**
+** <pre>
+** &nbsp; int (*xInput)(void *pIn, void *pData, int *pnData),
+** &nbsp; 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>
+** &nbsp; int *pnChangeset,
+** &nbsp; void **ppChangeset,
+** </pre>
+**
+** Is replaced by:
+**
+** <pre>
+** &nbsp; int (*xOutput)(void *pOut, const void *pData, int nData),
+** &nbsp; 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_apply_v2_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 */
+ void **ppRebase, int *pnRebase,
+ int flags
+);
+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 sqlite3changeset_start_v2_strm(
+ sqlite3_changeset_iter **pp,
+ int (*xInput)(void *pIn, void *pData, int *pnData),
+ void *pIn,
+ int flags
+);
+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
+);
+int sqlite3changegroup_add_strm(sqlite3_changegroup*,
+ int (*xInput)(void *pIn, void *pData, int *pnData),
+ void *pIn
+);
+int sqlite3changegroup_output_strm(sqlite3_changegroup*,
+ int (*xOutput)(void *pOut, const void *pData, int nData),
+ void *pOut
+);
+int sqlite3rebaser_rebase_strm(
+ sqlite3_rebaser *pRebaser,
+ int (*xInput)(void *pIn, void *pData, int *pnData),
+ void *pIn,
+ int (*xOutput)(void *pOut, const void *pData, int nData),
+ void *pOut
+);
+
+/*
+** CAPI3REF: Configure global parameters
+**
+** The sqlite3session_config() interface is used to make global configuration
+** changes to the sessions module in order to tune it to the specific needs
+** of the application.
+**
+** The sqlite3session_config() interface is not threadsafe. If it is invoked
+** while any other thread is inside any other sessions method then the
+** results are undefined. Furthermore, if it is invoked after any sessions
+** related objects have been created, the results are also undefined.
+**
+** The first argument to the sqlite3session_config() function must be one
+** of the SQLITE_SESSION_CONFIG_XXX constants defined below. The
+** interpretation of the (void*) value passed as the second parameter and
+** the effect of calling this function depends on the value of the first
+** parameter.
+**
+** <dl>
+** <dt>SQLITE_SESSION_CONFIG_STRMSIZE<dd>
+** By default, the sessions module streaming interfaces attempt to input
+** and output data in approximately 1 KiB chunks. This operand may be used
+** to set and query the value of this configuration setting. The pointer
+** passed as the second argument must point to a value of type (int).
+** If this value is greater than 0, it is used as the new streaming data
+** chunk size for both input and output. Before returning, the (int) value
+** pointed to by pArg is set to the final value of the streaming interface
+** chunk size.
+** </dl>
+**
+** This function returns SQLITE_OK if successful, or an SQLite error code
+** otherwise.
+*/
+int sqlite3session_config(int op, void *pArg);
+
+/*
+** CAPI3REF: Values for sqlite3session_config().
+*/
+#define SQLITE_SESSION_CONFIG_STRMSIZE 1
+
+/*
+** Make sure we can call this stuff from C++.
+*/
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* !defined(__SQLITESESSION_H_) && defined(SQLITE_ENABLE_SESSION) */
diff --git a/ext/session/test_session.c b/ext/session/test_session.c
new file mode 100644
index 0000000..c1feb78
--- /dev/null
+++ b/ext/session/test_session.c
@@ -0,0 +1,1463 @@
+
+#if defined(SQLITE_TEST) && defined(SQLITE_ENABLE_SESSION) \
+ && defined(SQLITE_ENABLE_PREUPDATE_HOOK)
+
+#include "sqlite3session.h"
+#include <assert.h>
+#include <string.h>
+#if defined(INCLUDE_SQLITE_TCL_H)
+# include "sqlite_tcl.h"
+#else
+# include "tcl.h"
+# ifndef SQLITE_TCLAPI
+# define SQLITE_TCLAPI
+# endif
+#endif
+
+#ifndef SQLITE_AMALGAMATION
+ typedef unsigned char u8;
+#endif
+
+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 */
+};
+
+/*
+** Extract an sqlite3* db handle from the object passed as the second
+** argument. If successful, set *pDb to point to the db handle and return
+** TCL_OK. Otherwise, return TCL_ERROR.
+*/
+static int dbHandleFromObj(Tcl_Interp *interp, Tcl_Obj *pObj, sqlite3 **pDb){
+ Tcl_CmdInfo info;
+ if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(pObj), &info) ){
+ Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(pObj), 0);
+ return TCL_ERROR;
+ }
+
+ *pDb = *(sqlite3 **)info.objClientData;
+ return TCL_OK;
+}
+
+/*************************************************************************
+** The following code is copied byte-for-byte from the sessions module
+** documentation. It is used by some of the sessions modules tests to
+** ensure that the example in the documentation does actually work.
+*/
+/*
+** Argument zSql points to a buffer containing an SQL script to execute
+** against the database handle passed as the first argument. As well as
+** executing the SQL script, this function collects a changeset recording
+** all changes made to the "main" database file. Assuming no error occurs,
+** output variables (*ppChangeset) and (*pnChangeset) are set to point
+** to a buffer containing the changeset and the size of the changeset in
+** bytes before returning SQLITE_OK. In this case it is the responsibility
+** of the caller to eventually free the changeset blob by passing it to
+** the sqlite3_free function.
+**
+** Or, if an error does occur, return an SQLite error code. The final
+** value of (*pChangeset) and (*pnChangeset) are undefined in this case.
+*/
+int sql_exec_changeset(
+ sqlite3 *db, /* Database handle */
+ const char *zSql, /* SQL script to execute */
+ int *pnChangeset, /* OUT: Size of changeset blob in bytes */
+ void **ppChangeset /* OUT: Pointer to changeset blob */
+){
+ sqlite3_session *pSession = 0;
+ int rc;
+
+ /* Create a new session object */
+ rc = sqlite3session_create(db, "main", &pSession);
+
+ /* Configure the session object to record changes to all tables */
+ if( rc==SQLITE_OK ) rc = sqlite3session_attach(pSession, NULL);
+
+ /* Execute the SQL script */
+ if( rc==SQLITE_OK ) rc = sqlite3_exec(db, zSql, 0, 0, 0);
+
+ /* Collect the changeset */
+ if( rc==SQLITE_OK ){
+ rc = sqlite3session_changeset(pSession, pnChangeset, ppChangeset);
+ }
+
+ /* Delete the session object */
+ sqlite3session_delete(pSession);
+
+ return rc;
+}
+/************************************************************************/
+
+
+#ifdef SQLITE_DEBUG
+static int sqlite3_test_changeset(int, void *, char **);
+static void assert_changeset_is_ok(int n, void *p){
+ int rc = 0;
+ char *z = 0;
+ rc = sqlite3_test_changeset(n, p, &z);
+ assert( z==0 );
+}
+#else
+# define assert_changeset_is_ok(n,p)
+#endif
+
+/*
+** Tclcmd: sql_exec_changeset DB SQL
+*/
+static int SQLITE_TCLAPI test_sql_exec_changeset(
+ void * clientData,
+ Tcl_Interp *interp,
+ int objc,
+ Tcl_Obj *CONST objv[]
+){
+ const char *zSql;
+ sqlite3 *db;
+ void *pChangeset;
+ int nChangeset;
+ int rc;
+
+ if( objc!=3 ){
+ Tcl_WrongNumArgs(interp, 1, objv, "DB SQL");
+ return TCL_ERROR;
+ }
+ if( dbHandleFromObj(interp, objv[1], &db) ) return TCL_ERROR;
+ zSql = (const char*)Tcl_GetString(objv[2]);
+
+ rc = sql_exec_changeset(db, zSql, &nChangeset, &pChangeset);
+ if( rc!=SQLITE_OK ){
+ Tcl_ResetResult(interp);
+ Tcl_AppendResult(interp, "error in sql_exec_changeset()", 0);
+ return TCL_ERROR;
+ }
+
+ assert_changeset_is_ok(nChangeset, pChangeset);
+ Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(pChangeset, nChangeset));
+ sqlite3_free(pChangeset);
+ return TCL_OK;
+}
+
+
+
+#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;
+ Tcl_Obj *pName = Tcl_NewStringObj(zVar, -1);
+ Tcl_IncrRefCount(pName);
+ pObj = Tcl_ObjGetVar2(interp, pName, 0, TCL_GLOBAL_ONLY);
+ Tcl_DecrRefCount(pName);
+ if( pObj ) Tcl_GetIntFromObj(0, pObj, &iVal);
+ return iVal;
+}
+
+static int test_session_error(Tcl_Interp *interp, int rc, char *zErr){
+ extern const char *sqlite3ErrName(int);
+ Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1));
+ if( zErr ){
+ Tcl_AppendResult(interp, " - ", zErr, 0);
+ sqlite3_free(zErr);
+ }
+ 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 SQLITE_TCLAPI test_session_cmd(
+ void *clientData,
+ Tcl_Interp *interp,
+ int objc,
+ Tcl_Obj *CONST objv[]
+){
+ TestSession *p = (TestSession*)clientData;
+ sqlite3_session *pSession = p->pSession;
+ static 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 */
+ { "diff", 2, "FROMDB TBL", }, /* 8 */
+ { "memory_used", 0, "", }, /* 9 */
+ { "changeset_size", 0, "", }, /* 10 */
+ { "object_config_size", 1, "INTEGER", }, /* 11 */
+ { 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, 0);
+ }
+ 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 ){
+ assert_changeset_is_ok(o.n, o.p);
+ Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(o.p, o.n));
+ }
+ sqlite3_free(o.p);
+ if( rc!=SQLITE_OK ){
+ return test_session_error(interp, rc, 0);
+ }
+ 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;
+ }
+
+ case 8: { /* diff */
+ char *zErr = 0;
+ rc = sqlite3session_diff(pSession,
+ Tcl_GetString(objv[2]),
+ Tcl_GetString(objv[3]),
+ &zErr
+ );
+ assert( rc!=SQLITE_OK || zErr==0 );
+ if( rc ){
+ return test_session_error(interp, rc, zErr);
+ }
+ break;
+ }
+
+ case 9: { /* memory_used */
+ sqlite3_int64 nMalloc = sqlite3session_memory_used(pSession);
+ Tcl_SetObjResult(interp, Tcl_NewWideIntObj(nMalloc));
+ break;
+ }
+
+ case 10: {
+ sqlite3_int64 nSize = sqlite3session_changeset_size(pSession);
+ Tcl_SetObjResult(interp, Tcl_NewWideIntObj(nSize));
+ break;
+ }
+ case 11: {
+ int rc;
+ int iArg;
+ if( Tcl_GetIntFromObj(interp, objv[2], &iArg) ){
+ return TCL_ERROR;
+ }
+ rc = sqlite3session_object_config(
+ pSession, SQLITE_SESSION_OBJCONFIG_SIZE, &iArg
+ );
+ if( rc!=SQLITE_OK ){
+ extern const char *sqlite3ErrName(int);
+ Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1));
+ }else{
+ Tcl_SetObjResult(interp, Tcl_NewIntObj(iArg));
+ }
+ break;
+ }
+ }
+
+ return TCL_OK;
+}
+
+static void SQLITE_TCLAPI 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 SQLITE_TCLAPI 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 */
+ int iArg = -1;
+
+ 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, 0);
+ }
+
+ /* Query the SQLITE_SESSION_OBJCONFIG_SIZE option to ensure that it
+ ** is clear by default. Then set it. */
+ sqlite3session_object_config(p->pSession,SQLITE_SESSION_OBJCONFIG_SIZE,&iArg);
+ assert( iArg==0 );
+ iArg = 1;
+ sqlite3session_object_config(p->pSession,SQLITE_SESSION_OBJCONFIG_SIZE,&iArg);
+
+ 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;
+ }
+ default:
+ assert( sqlite3_value_type(pVal)==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;
+}
+
+
+static int SQLITE_TCLAPI testSqlite3changesetApply(
+ int bV2,
+ 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;
+ void *pRebase = 0;
+ int nRebase = 0;
+ int flags = 0; /* Flags for apply_v2() */
+
+ memset(&sStr, 0, sizeof(sStr));
+ sStr.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR);
+
+ /* Check for the -nosavepoint flag */
+ if( bV2 ){
+ if( objc>1 ){
+ const char *z1 = Tcl_GetString(objv[1]);
+ int n = strlen(z1);
+ if( n>1 && n<=12 && 0==sqlite3_strnicmp("-nosavepoint", z1, n) ){
+ flags |= SQLITE_CHANGESETAPPLY_NOSAVEPOINT;
+ objc--;
+ objv++;
+ }
+ }
+ if( objc>1 ){
+ const char *z1 = Tcl_GetString(objv[1]);
+ int n = strlen(z1);
+ if( n>1 && n<=7 && 0==sqlite3_strnicmp("-invert", z1, n) ){
+ flags |= SQLITE_CHANGESETAPPLY_INVERT;
+ objc--;
+ objv++;
+ }
+ }
+ }
+
+ if( objc!=4 && objc!=5 ){
+ const char *zMsg;
+ if( bV2 ){
+ zMsg = "?-nosavepoint? ?-inverse? "
+ "DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT?";
+ }else{
+ zMsg = "DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT?";
+ }
+ Tcl_WrongNumArgs(interp, 1, objv, zMsg);
+ return TCL_ERROR;
+ }
+ if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(objv[1]), &info) ){
+ Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(objv[1]), 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 ){
+ if( bV2==0 ){
+ rc = sqlite3changeset_apply(db, nChangeset, pChangeset,
+ (objc==5)?test_filter_handler:0, test_conflict_handler, (void *)&ctx
+ );
+ }else{
+ rc = sqlite3changeset_apply_v2(db, nChangeset, pChangeset,
+ (objc==5)?test_filter_handler:0, test_conflict_handler, (void *)&ctx,
+ &pRebase, &nRebase, flags
+ );
+ }
+ }else{
+ sStr.aData = (unsigned char*)pChangeset;
+ sStr.nData = nChangeset;
+ if( bV2==0 ){
+ rc = sqlite3changeset_apply_strm(db, testStreamInput, (void*)&sStr,
+ (objc==5) ? test_filter_handler : 0,
+ test_conflict_handler, (void *)&ctx
+ );
+ }else{
+ rc = sqlite3changeset_apply_v2_strm(db, testStreamInput, (void*)&sStr,
+ (objc==5) ? test_filter_handler : 0,
+ test_conflict_handler, (void *)&ctx,
+ &pRebase, &nRebase, flags
+ );
+ }
+ }
+
+ if( rc!=SQLITE_OK ){
+ return test_session_error(interp, rc, 0);
+ }else{
+ Tcl_ResetResult(interp);
+ if( bV2 && pRebase ){
+ Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(pRebase, nRebase));
+ }
+ }
+ sqlite3_free(pRebase);
+ return TCL_OK;
+}
+
+/*
+** sqlite3changeset_apply DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT?
+*/
+static int SQLITE_TCLAPI test_sqlite3changeset_apply(
+ void * clientData,
+ Tcl_Interp *interp,
+ int objc,
+ Tcl_Obj *CONST objv[]
+){
+ return testSqlite3changesetApply(0, clientData, interp, objc, objv);
+}
+/*
+** sqlite3changeset_apply_v2 DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT?
+*/
+static int SQLITE_TCLAPI test_sqlite3changeset_apply_v2(
+ void * clientData,
+ Tcl_Interp *interp,
+ int objc,
+ Tcl_Obj *CONST objv[]
+){
+ return testSqlite3changesetApply(1, clientData, interp, objc, objv);
+}
+
+/*
+** sqlite3changeset_apply_replace_all DB CHANGESET
+*/
+static int SQLITE_TCLAPI 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, 0);
+ }
+ Tcl_ResetResult(interp);
+ return TCL_OK;
+}
+
+
+/*
+** sqlite3changeset_invert CHANGESET
+*/
+static int SQLITE_TCLAPI 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, 0);
+ }else{
+ assert_changeset_is_ok(sOut.n, sOut.p);
+ Tcl_SetObjResult(interp,Tcl_NewByteArrayObj((unsigned char*)sOut.p,sOut.n));
+ }
+ sqlite3_free(sOut.p);
+ return rc;
+}
+
+/*
+** sqlite3changeset_concat LEFT RIGHT
+*/
+static int SQLITE_TCLAPI 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, 0);
+ }else{
+ assert_changeset_is_ok(sOut.n, sOut.p);
+ Tcl_SetObjResult(interp,Tcl_NewByteArrayObj((unsigned char*)sOut.p,sOut.n));
+ }
+ sqlite3_free(sOut.p);
+ return rc;
+}
+
+/*
+** sqlite3session_foreach VARNAME CHANGESET SCRIPT
+*/
+static int SQLITE_TCLAPI 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;
+ int isInvert = 0;
+
+ TestStreamInput sStr;
+ memset(&sStr, 0, sizeof(sStr));
+
+ while( objc>1 ){
+ char *zOpt = Tcl_GetString(objv[1]);
+ int nOpt = strlen(zOpt);
+ if( zOpt[0]!='-' ) break;
+ if( nOpt<=7 && 0==sqlite3_strnicmp(zOpt, "-invert", nOpt) ){
+ isInvert = 1;
+ }else
+ if( nOpt<=5 && 0==sqlite3_strnicmp(zOpt, "-next", nOpt) ){
+ isCheckNext = 1;
+ }else{
+ break;
+ }
+ objv++;
+ objc--;
+ }
+ if( objc!=4 ){
+ Tcl_WrongNumArgs(
+ interp, 1, objv, "?-next? ?-invert? VARNAME CHANGESET SCRIPT");
+ return TCL_ERROR;
+ }
+
+ pVarname = objv[1];
+ pCS = objv[2];
+ pScript = objv[3];
+
+ pChangeset = (void *)Tcl_GetByteArrayFromObj(pCS, &nChangeset);
+ sStr.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR);
+ if( isInvert ){
+ int f = SQLITE_CHANGESETSTART_INVERT;
+ if( sStr.nStream==0 ){
+ rc = sqlite3changeset_start_v2(&pIter, nChangeset, pChangeset, f);
+ }else{
+ void *pCtx = (void*)&sStr;
+ sStr.aData = (unsigned char*)pChangeset;
+ sStr.nData = nChangeset;
+ rc = sqlite3changeset_start_v2_strm(&pIter, testStreamInput, pCtx, f);
+ }
+ }else{
+ 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, 0);
+ }
+
+ 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 ){
+ 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 ){
+ 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, 0);
+ }
+
+ return TCL_OK;
+}
+
+/*
+** tclcmd: CMD configure REBASE-BLOB
+** tclcmd: CMD rebase CHANGESET
+** tclcmd: CMD delete
+*/
+static int SQLITE_TCLAPI test_rebaser_cmd(
+ void * clientData,
+ Tcl_Interp *interp,
+ int objc,
+ Tcl_Obj *CONST objv[]
+){
+ static struct RebaseSubcmd {
+ const char *zSub;
+ int nArg;
+ const char *zMsg;
+ int iSub;
+ } aSub[] = {
+ { "configure", 1, "REBASE-BLOB" }, /* 0 */
+ { "delete", 0, "" }, /* 1 */
+ { "rebase", 1, "CHANGESET" }, /* 2 */
+ { 0 }
+ };
+
+ sqlite3_rebaser *p = (sqlite3_rebaser*)clientData;
+ 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;
+ }
+
+ assert( iSub==0 || iSub==1 || iSub==2 );
+ assert( rc==SQLITE_OK );
+ switch( iSub ){
+ case 0: { /* configure */
+ int nRebase = 0;
+ unsigned char *pRebase = Tcl_GetByteArrayFromObj(objv[2], &nRebase);
+ rc = sqlite3rebaser_configure(p, nRebase, pRebase);
+ break;
+ }
+
+ case 1: /* delete */
+ Tcl_DeleteCommand(interp, Tcl_GetString(objv[0]));
+ break;
+
+ default: { /* rebase */
+ TestStreamInput sStr; /* Input stream */
+ TestSessionsBlob sOut; /* Output blob */
+
+ memset(&sStr, 0, sizeof(sStr));
+ memset(&sOut, 0, sizeof(sOut));
+ sStr.aData = Tcl_GetByteArrayFromObj(objv[2], &sStr.nData);
+ sStr.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR);
+
+ if( sStr.nStream ){
+ rc = sqlite3rebaser_rebase_strm(p,
+ testStreamInput, (void*)&sStr,
+ testStreamOutput, (void*)&sOut
+ );
+ }else{
+ rc = sqlite3rebaser_rebase(p, sStr.nData, sStr.aData, &sOut.n, &sOut.p);
+ }
+
+ if( rc==SQLITE_OK ){
+ assert_changeset_is_ok(sOut.n, sOut.p);
+ Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(sOut.p, sOut.n));
+ }
+ sqlite3_free(sOut.p);
+ break;
+ }
+ }
+
+ if( rc!=SQLITE_OK ){
+ return test_session_error(interp, rc, 0);
+ }
+ return TCL_OK;
+}
+
+static void SQLITE_TCLAPI test_rebaser_del(void *clientData){
+ sqlite3_rebaser *p = (sqlite3_rebaser*)clientData;
+ sqlite3rebaser_delete(p);
+}
+
+/*
+** tclcmd: sqlite3rebaser_create NAME
+*/
+static int SQLITE_TCLAPI test_sqlite3rebaser_create(
+ void * clientData,
+ Tcl_Interp *interp,
+ int objc,
+ Tcl_Obj *CONST objv[]
+){
+ int rc;
+ sqlite3_rebaser *pNew = 0;
+ if( objc!=2 ){
+ Tcl_WrongNumArgs(interp, 1, objv, "NAME");
+ return SQLITE_ERROR;
+ }
+
+ rc = sqlite3rebaser_create(&pNew);
+ if( rc!=SQLITE_OK ){
+ return test_session_error(interp, rc, 0);
+ }
+
+ Tcl_CreateObjCommand(interp, Tcl_GetString(objv[1]), test_rebaser_cmd,
+ (ClientData)pNew, test_rebaser_del
+ );
+ Tcl_SetObjResult(interp, objv[1]);
+ return TCL_OK;
+}
+
+/*
+**
+*/
+static int sqlite3_test_changeset(
+ int nChangeset,
+ void *pChangeset,
+ char **pzErr
+){
+ sqlite3_changeset_iter *pIter = 0;
+ char *zErr = 0;
+ int rc = SQLITE_OK;
+ int bPatch = (nChangeset>0 && ((char*)pChangeset)[0]=='P');
+
+ rc = sqlite3changeset_start(&pIter, nChangeset, pChangeset);
+ if( rc==SQLITE_OK ){
+ int rc2;
+ while( rc==SQLITE_OK && SQLITE_ROW==sqlite3changeset_next(pIter) ){
+ unsigned char *aPk = 0;
+ int nCol = 0;
+ int op = 0;
+ const char *zTab = 0;
+
+ sqlite3changeset_pk(pIter, &aPk, &nCol);
+ sqlite3changeset_op(pIter, &zTab, &nCol, &op, 0);
+
+ if( op==SQLITE_UPDATE ){
+ int iCol;
+ for(iCol=0; iCol<nCol; iCol++){
+ sqlite3_value *pNew = 0;
+ sqlite3_value *pOld = 0;
+ sqlite3changeset_new(pIter, iCol, &pNew);
+ sqlite3changeset_old(pIter, iCol, &pOld);
+
+ if( aPk[iCol] ){
+ if( pOld==0 ) rc = SQLITE_ERROR;
+ }else if( bPatch ){
+ if( pOld ) rc = SQLITE_ERROR;
+ }else{
+ if( (pOld==0)!=(pNew==0) ) rc = SQLITE_ERROR;
+ }
+
+ if( rc!=SQLITE_OK ){
+ zErr = sqlite3_mprintf(
+ "unexpected SQLITE_UPDATE (bPatch=%d pk=%d pOld=%d pNew=%d)",
+ bPatch, (int)aPk[iCol], pOld!=0, pNew!=0
+ );
+ break;
+ }
+ }
+ }
+ }
+ rc2 = sqlite3changeset_finalize(pIter);
+ if( rc==SQLITE_OK ){
+ rc = rc2;
+ }
+ }
+
+ *pzErr = zErr;
+ return rc;
+}
+
+/*
+** test_changeset CHANGESET
+*/
+static int SQLITE_TCLAPI test_changeset(
+ void * clientData,
+ Tcl_Interp *interp,
+ int objc,
+ Tcl_Obj *CONST objv[]
+){
+ void *pChangeset = 0; /* Buffer containing changeset */
+ int nChangeset = 0; /* Size of buffer aChangeset in bytes */
+ int rc = SQLITE_OK;
+ char *z = 0;
+
+ if( objc!=2 ){
+ Tcl_WrongNumArgs(interp, 1, objv, "CHANGESET");
+ return TCL_ERROR;
+ }
+ pChangeset = (void *)Tcl_GetByteArrayFromObj(objv[1], &nChangeset);
+
+ Tcl_ResetResult(interp);
+ rc = sqlite3_test_changeset(nChangeset, pChangeset, &z);
+ if( rc!=SQLITE_OK ){
+ char *zErr = sqlite3_mprintf("(%d) - \"%s\"", rc, z);
+ Tcl_SetObjResult(interp, Tcl_NewStringObj(zErr, -1));
+ sqlite3_free(zErr);
+ }
+ sqlite3_free(z);
+
+ return rc ? TCL_ERROR : TCL_OK;
+}
+
+/*
+** tclcmd: sqlite3rebaser_configure OP VALUE
+*/
+static int SQLITE_TCLAPI test_sqlite3session_config(
+ void * clientData,
+ Tcl_Interp *interp,
+ int objc,
+ Tcl_Obj *CONST objv[]
+){
+ static struct ConfigOpt {
+ const char *zSub;
+ int op;
+ } aSub[] = {
+ { "strm_size", SQLITE_SESSION_CONFIG_STRMSIZE },
+ { "invalid", 0 },
+ { 0 }
+ };
+ int rc;
+ int iSub;
+ int iVal;
+
+ if( objc!=3 ){
+ Tcl_WrongNumArgs(interp, 1, objv, "OP VALUE");
+ return SQLITE_ERROR;
+ }
+ rc = Tcl_GetIndexFromObjStruct(interp,
+ objv[1], aSub, sizeof(aSub[0]), "sub-command", 0, &iSub
+ );
+ if( rc!=TCL_OK ) return rc;
+ if( Tcl_GetIntFromObj(interp, objv[2], &iVal) ) return TCL_ERROR;
+
+ rc = sqlite3session_config(aSub[iSub].op, (void*)&iVal);
+ if( rc!=SQLITE_OK ){
+ return test_session_error(interp, rc, 0);
+ }
+ Tcl_SetObjResult(interp, Tcl_NewIntObj(iVal));
+ return TCL_OK;
+}
+
+int TestSession_Init(Tcl_Interp *interp){
+ struct Cmd {
+ const char *zCmd;
+ Tcl_ObjCmdProc *xProc;
+ } aCmd[] = {
+ { "sqlite3session", test_sqlite3session },
+ { "sqlite3session_foreach", test_sqlite3session_foreach },
+ { "sqlite3changeset_invert", test_sqlite3changeset_invert },
+ { "sqlite3changeset_concat", test_sqlite3changeset_concat },
+ { "sqlite3changeset_apply", test_sqlite3changeset_apply },
+ { "sqlite3changeset_apply_v2", test_sqlite3changeset_apply_v2 },
+ { "sqlite3changeset_apply_replace_all",
+ test_sqlite3changeset_apply_replace_all },
+ { "sql_exec_changeset", test_sql_exec_changeset },
+ { "sqlite3rebaser_create", test_sqlite3rebaser_create },
+ { "sqlite3session_config", test_sqlite3session_config },
+ { "test_changeset", test_changeset },
+ };
+ int i;
+
+ for(i=0; i<sizeof(aCmd)/sizeof(struct Cmd); i++){
+ struct Cmd *p = &aCmd[i];
+ Tcl_CreateObjCommand(interp, p->zCmd, p->xProc, 0, 0);
+ }
+
+ return TCL_OK;
+}
+
+#endif /* SQLITE_TEST && SQLITE_SESSION && SQLITE_PREUPDATE_HOOK */