aboutsummaryrefslogtreecommitdiff
path: root/ext/session
diff options
context:
space:
mode:
Diffstat (limited to 'ext/session')
-rw-r--r--ext/session/session1.test509
-rw-r--r--ext/session/session2.test581
-rw-r--r--ext/session/session3.test211
-rw-r--r--ext/session/session4.test68
-rw-r--r--ext/session/session5.test409
-rw-r--r--ext/session/session6.test90
-rw-r--r--ext/session/session8.test92
-rw-r--r--ext/session/session9.test254
-rw-r--r--ext/session/sessionA.test69
-rw-r--r--ext/session/session_common.tcl136
-rw-r--r--ext/session/sessionfault.test443
-rw-r--r--ext/session/sqlite3session.c3300
-rw-r--r--ext/session/sqlite3session.h918
-rw-r--r--ext/session/test_session.c674
14 files changed, 7754 insertions, 0 deletions
diff --git a/ext/session/session1.test b/ext/session/session1.test
new file mode 100644
index 000000000..e47fb3e84
--- /dev/null
+++ b/ext/session/session1.test
@@ -0,0 +1,509 @@
+# 2011 March 07
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+# This file implements regression tests for SQLite library.
+#
+
+if {![info exists testdir]} {
+ set testdir [file join [file dirname [info script]] .. .. test]
+}
+source [file join [file dirname [info script]] session_common.tcl]
+source $testdir/tester.tcl
+ifcapable !session {finish_test; return}
+
+set testprefix session1
+
+proc do_changeset_test {tn session res} {
+ set r [list]
+ foreach x $res {lappend r $x}
+ uplevel do_test $tn [list [subst -nocommands {
+ set x [list]
+ sqlite3session_foreach c [$session changeset] { lappend x [set c] }
+ set x
+ }]] [list $r]
+}
+
+proc do_changeset_invert_test {tn session res} {
+ set r [list]
+ foreach x $res {lappend r $x}
+ uplevel do_test $tn [list [subst -nocommands {
+ set x [list]
+ set changeset [sqlite3changeset_invert [$session changeset]]
+ sqlite3session_foreach c [set changeset] { lappend x [set c] }
+ set x
+ }]] [list $r]
+}
+
+do_execsql_test 1.0 {
+ CREATE TABLE t1(x PRIMARY KEY, y);
+ INSERT INTO t1 VALUES('abc', 'def');
+}
+
+#-------------------------------------------------------------------------
+# Test creating, attaching tables to and deleting session objects.
+#
+do_test 1.1 { sqlite3session S db main } {S}
+do_test 1.2 { S delete } {}
+do_test 1.3 { sqlite3session S db main } {S}
+do_test 1.4 { S attach t1 } {}
+do_test 1.5 { S delete } {}
+do_test 1.6 { sqlite3session S db main } {S}
+do_test 1.7 { S attach t1 ; S attach t2 ; S attach t3 } {}
+do_test 1.8 { S attach t1 ; S attach t2 ; S attach t3 } {}
+do_test 1.9 { S delete } {}
+do_test 1.10 {
+ sqlite3session S db main
+ S attach t1
+ execsql { INSERT INTO t1 VALUES('ghi', 'jkl') }
+} {}
+do_test 1.11 { S delete } {}
+do_test 1.12 {
+ sqlite3session S db main
+ S attach t1
+ execsql { INSERT INTO t1 VALUES('mno', 'pqr') }
+ execsql { UPDATE t1 SET x = 111 WHERE rowid = 1 }
+ execsql { DELETE FROM t1 WHERE rowid = 2 }
+} {}
+do_test 1.13 {
+ S changeset
+ S delete
+} {}
+
+#-------------------------------------------------------------------------
+# Simple changeset tests. Also test the sqlite3changeset_invert()
+# function.
+#
+do_test 2.1.1 {
+ execsql { DELETE FROM t1 }
+ sqlite3session S db main
+ S attach t1
+ execsql { INSERT INTO t1 VALUES(1, 'Sukhothai') }
+ execsql { INSERT INTO t1 VALUES(2, 'Ayutthaya') }
+ execsql { INSERT INTO t1 VALUES(3, 'Thonburi') }
+} {}
+do_changeset_test 2.1.2 S {
+ {INSERT t1 0 X. {} {i 1 t Sukhothai}}
+ {INSERT t1 0 X. {} {i 2 t Ayutthaya}}
+ {INSERT t1 0 X. {} {i 3 t Thonburi}}
+}
+do_changeset_invert_test 2.1.3 S {
+ {DELETE t1 0 X. {i 1 t Sukhothai} {}}
+ {DELETE t1 0 X. {i 2 t Ayutthaya} {}}
+ {DELETE t1 0 X. {i 3 t Thonburi} {}}
+}
+do_test 2.1.4 { S delete } {}
+
+do_test 2.2.1 {
+ sqlite3session S db main
+ S attach t1
+ execsql { DELETE FROM t1 WHERE 1 }
+} {}
+do_changeset_test 2.2.2 S {
+ {DELETE t1 0 X. {i 1 t Sukhothai} {}}
+ {DELETE t1 0 X. {i 2 t Ayutthaya} {}}
+ {DELETE t1 0 X. {i 3 t Thonburi} {}}
+}
+do_changeset_invert_test 2.2.3 S {
+ {INSERT t1 0 X. {} {i 1 t Sukhothai}}
+ {INSERT t1 0 X. {} {i 2 t Ayutthaya}}
+ {INSERT t1 0 X. {} {i 3 t Thonburi}}
+}
+do_test 2.2.4 { S delete } {}
+
+do_test 2.3.1 {
+ execsql { DELETE FROM t1 }
+ sqlite3session S db main
+ execsql { INSERT INTO t1 VALUES(1, 'Sukhothai') }
+ execsql { INSERT INTO t1 VALUES(2, 'Ayutthaya') }
+ execsql { INSERT INTO t1 VALUES(3, 'Thonburi') }
+ S attach t1
+ execsql {
+ UPDATE t1 SET x = 10 WHERE x = 1;
+ UPDATE t1 SET y = 'Surin' WHERE x = 2;
+ UPDATE t1 SET x = 20, y = 'Thapae' WHERE x = 3;
+ }
+} {}
+
+do_changeset_test 2.3.2 S {
+ {INSERT t1 0 X. {} {i 10 t Sukhothai}}
+ {DELETE t1 0 X. {i 1 t Sukhothai} {}}
+ {UPDATE t1 0 X. {i 2 t Ayutthaya} {{} {} t Surin}}
+ {DELETE t1 0 X. {i 3 t Thonburi} {}}
+ {INSERT t1 0 X. {} {i 20 t Thapae}}
+}
+
+do_changeset_invert_test 2.3.3 S {
+ {DELETE t1 0 X. {i 10 t Sukhothai} {}}
+ {INSERT t1 0 X. {} {i 1 t Sukhothai}}
+ {UPDATE t1 0 X. {i 2 t Surin} {{} {} t Ayutthaya}}
+ {INSERT t1 0 X. {} {i 3 t Thonburi}}
+ {DELETE t1 0 X. {i 20 t Thapae} {}}
+}
+do_test 2.3.4 { S delete } {}
+
+do_test 2.4.1 {
+ sqlite3session S db main
+ S attach t1
+ execsql { INSERT INTO t1 VALUES(100, 'Bangkok') }
+ execsql { DELETE FROM t1 WHERE x = 100 }
+} {}
+do_changeset_test 2.4.2 S {}
+do_changeset_invert_test 2.4.3 S {}
+do_test 2.4.4 { S delete } {}
+
+#-------------------------------------------------------------------------
+# Test the application of simple changesets. These tests also test that
+# the conflict callback is invoked correctly. For these tests, the
+# conflict callback always returns OMIT.
+#
+db close
+forcedelete test.db test.db2
+sqlite3 db test.db
+sqlite3 db2 test.db2
+
+proc xConflict {args} {
+ lappend ::xConflict $args
+ return ""
+}
+
+proc bgerror {args} { set ::background_error $args }
+
+proc do_conflict_test {tn args} {
+ set O(-tables) [list]
+ set O(-sql) [list]
+ set O(-conflicts) [list]
+
+ array set V $args
+ foreach key [array names V] {
+ if {![info exists O($key)]} {error "no such option: $key"}
+ }
+ array set O $args
+
+ sqlite3session S db main
+ foreach t $O(-tables) { S attach $t }
+ execsql $O(-sql)
+ set ::xConflict [list]
+ sqlite3changeset_apply db2 [S changeset] xConflict
+
+ set conflicts [list]
+ foreach c $O(-conflicts) {
+ lappend conflicts $c
+ }
+
+ after 1 {set go 1}
+ vwait go
+
+ uplevel do_test $tn [list { set ::xConflict }] [list $conflicts]
+ S delete
+}
+
+proc do_db2_test {testname sql {result {}}} {
+ uplevel do_test $testname [list "execsql {$sql} db2"] [list [list {*}$result]]
+}
+
+# Test INSERT changesets.
+#
+do_test 3.1.0 {
+ execsql { CREATE TABLE t1(a PRIMARY KEY, b NOT NULL) } db2
+ execsql {
+ CREATE TABLE t1(a PRIMARY KEY, b);
+ INSERT INTO t1 VALUES(1, 'one');
+ INSERT INTO t1 VALUES(2, 'two');
+ } db
+} {}
+do_db2_test 3.1.1 "INSERT INTO t1 VALUES(6, 'VI')"
+do_conflict_test 3.1.2 -tables t1 -sql {
+ INSERT INTO t1 VALUES(3, 'three');
+ INSERT INTO t1 VALUES(4, 'four');
+ INSERT INTO t1 VALUES(5, 'five');
+ INSERT INTO t1 VALUES(6, 'six');
+ INSERT INTO t1 VALUES(7, 'seven');
+ INSERT INTO t1 VALUES(8, NULL);
+} -conflicts {
+ {INSERT t1 CONSTRAINT {i 8 n {}}}
+ {INSERT t1 CONFLICT {i 6 t six} {i 6 t VI}}
+}
+
+do_db2_test 3.1.3 "SELECT * FROM t1" {
+ 6 VI 3 three 4 four 5 five 7 seven
+}
+do_execsql_test 3.1.4 "SELECT * FROM t1" {
+ 1 one 2 two 3 three 4 four 5 five 6 six 7 seven 8 {}
+}
+
+# Test DELETE changesets.
+#
+do_execsql_test 3.2.1 {
+ PRAGMA foreign_keys = on;
+ CREATE TABLE t2(a PRIMARY KEY, b);
+ CREATE TABLE t3(c, d REFERENCES t2);
+ INSERT INTO t2 VALUES(1, 'one');
+ INSERT INTO t2 VALUES(2, 'two');
+ INSERT INTO t2 VALUES(3, 'three');
+ INSERT INTO t2 VALUES(4, 'four');
+}
+do_db2_test 3.2.2 {
+ PRAGMA foreign_keys = on;
+ CREATE TABLE t2(a PRIMARY KEY, b);
+ CREATE TABLE t3(c, d REFERENCES t2);
+ INSERT INTO t2 VALUES(1, 'one');
+ INSERT INTO t2 VALUES(2, 'two');
+ INSERT INTO t2 VALUES(4, 'five');
+ INSERT INTO t3 VALUES('i', 1);
+}
+do_conflict_test 3.2.3 -tables t2 -sql {
+ DELETE FROM t2 WHERE a = 1;
+ DELETE FROM t2 WHERE a = 2;
+ DELETE FROM t2 WHERE a = 3;
+ DELETE FROM t2 WHERE a = 4;
+} -conflicts {
+ {DELETE t2 NOTFOUND {i 3 t three}}
+ {DELETE t2 DATA {i 4 t four} {i 4 t five}}
+ {FOREIGN_KEY 1}
+}
+do_execsql_test 3.2.4 "SELECT * FROM t2" {}
+do_db2_test 3.2.5 "SELECT * FROM t2" {4 five}
+
+# Test UPDATE changesets.
+#
+do_execsql_test 3.3.1 {
+ CREATE TABLE t4(a, b, c, PRIMARY KEY(b, c));
+ INSERT INTO t4 VALUES(1, 2, 3);
+ INSERT INTO t4 VALUES(4, 5, 6);
+ INSERT INTO t4 VALUES(7, 8, 9);
+ INSERT INTO t4 VALUES(10, 11, 12);
+}
+do_db2_test 3.3.2 {
+ CREATE TABLE t4(a NOT NULL, b, c, PRIMARY KEY(b, c));
+ INSERT INTO t4 VALUES(0, 2, 3);
+ INSERT INTO t4 VALUES(4, 5, 7);
+ INSERT INTO t4 VALUES(7, 8, 9);
+ INSERT INTO t4 VALUES(10, 11, 12);
+}
+do_conflict_test 3.3.3 -tables t4 -sql {
+ UPDATE t4 SET a = -1 WHERE b = 2;
+ UPDATE t4 SET a = -1 WHERE b = 5;
+ UPDATE t4 SET a = NULL WHERE c = 9;
+ UPDATE t4 SET a = 'x' WHERE b = 11;
+} -conflicts {
+ {UPDATE t4 CONSTRAINT {i 7 i 8 i 9} {n {} {} {} {} {}}}
+ {UPDATE t4 DATA {i 1 i 2 i 3} {i -1 {} {} {} {}} {i 0 i 2 i 3}}
+ {UPDATE t4 NOTFOUND {i 4 i 5 i 6} {i -1 {} {} {} {}}}
+}
+do_db2_test 3.3.4 { SELECT * FROM t4 } {0 2 3 4 5 7 7 8 9 x 11 12}
+do_execsql_test 3.3.5 { SELECT * FROM t4 } {-1 2 3 -1 5 6 {} 8 9 x 11 12}
+
+#-------------------------------------------------------------------------
+# This next block of tests verifies that values returned by the conflict
+# handler are intepreted correctly.
+#
+
+proc test_reset {} {
+ db close
+ db2 close
+ forcedelete test.db test.db2
+ sqlite3 db test.db
+ sqlite3 db2 test.db2
+}
+
+proc xConflict {args} {
+ lappend ::xConflict $args
+ return $::conflict_return
+}
+
+foreach {tn conflict_return after} {
+ 1 OMIT {1 2 value1 4 5 7 10 x x}
+ 2 REPLACE {1 2 value1 4 5 value2 10 8 9}
+} {
+ test_reset
+
+ do_test 4.$tn.1 {
+ foreach db {db db2} {
+ execsql {
+ CREATE TABLE t1(a, b, c, PRIMARY KEY(a));
+ INSERT INTO t1 VALUES(1, 2, 3);
+ INSERT INTO t1 VALUES(4, 5, 6);
+ INSERT INTO t1 VALUES(7, 8, 9);
+ } $db
+ }
+ execsql {
+ REPLACE INTO t1 VALUES(4, 5, 7);
+ REPLACE INTO t1 VALUES(10, 'x', 'x');
+ } db2
+ } {}
+
+ do_conflict_test 4.$tn.2 -tables t1 -sql {
+ UPDATE t1 SET c = 'value1' WHERE a = 1; -- no conflict
+ UPDATE t1 SET c = 'value2' WHERE a = 4; -- DATA conflict
+ UPDATE t1 SET a = 10 WHERE a = 7; -- CONFLICT conflict
+ } -conflicts {
+ {INSERT t1 CONFLICT {i 10 i 8 i 9} {i 10 t x t x}}
+ {UPDATE t1 DATA {i 4 {} {} i 6} {{} {} {} {} t value2} {i 4 i 5 i 7}}
+ }
+
+ do_db2_test 4.$tn.3 "SELECT * FROM t1 ORDER BY a" $after
+}
+
+foreach {tn conflict_return} {
+ 1 OMIT
+ 2 REPLACE
+} {
+ test_reset
+
+ do_test 5.$tn.1 {
+ # Create an identical schema in both databases.
+ set schema {
+ CREATE TABLE "'foolish name'"(x, y, z, PRIMARY KEY(x, y));
+ }
+ execsql $schema db
+ execsql $schema db2
+
+ # Add some rows to [db2]. These rows will cause conflicts later
+ # on when the changeset from [db] is applied to it.
+ execsql {
+ INSERT INTO "'foolish name'" VALUES('one', 'one', 'ii');
+ INSERT INTO "'foolish name'" VALUES('one', 'two', 'i');
+ INSERT INTO "'foolish name'" VALUES('two', 'two', 'ii');
+ } db2
+
+ } {}
+
+ do_conflict_test 5.$tn.2 -tables {{'foolish name'}} -sql {
+ INSERT INTO "'foolish name'" VALUES('one', 'two', 2);
+ } -conflicts {
+ {INSERT {'foolish name'} CONFLICT {t one t two i 2} {t one t two t i}}
+ }
+
+ set res(REPLACE) {one one ii one two 2 two two ii}
+ set res(OMIT) {one one ii one two i two two ii}
+ do_db2_test 5.$tn.3 {
+ SELECT * FROM "'foolish name'" ORDER BY x, y
+ } $res($conflict_return)
+
+
+ do_test 5.$tn.1 {
+ set schema {
+ CREATE TABLE d1("z""z" PRIMARY KEY, y);
+ INSERT INTO d1 VALUES(1, 'one');
+ INSERT INTO d1 VALUES(2, 'two');
+ }
+ execsql $schema db
+ execsql $schema db2
+
+ execsql {
+ UPDATE d1 SET y = 'TWO' WHERE "z""z" = 2;
+ } db2
+
+ } {}
+
+ do_conflict_test 5.$tn.2 -tables d1 -sql {
+ DELETE FROM d1 WHERE "z""z" = 2;
+ } -conflicts {
+ {DELETE d1 DATA {i 2 t two} {i 2 t TWO}}
+ }
+
+ set res(REPLACE) {1 one}
+ set res(OMIT) {1 one 2 TWO}
+ do_db2_test 5.$tn.3 "SELECT * FROM d1" $res($conflict_return)
+}
+
+#-------------------------------------------------------------------------
+# Test that two tables can be monitored by a single session object.
+#
+test_reset
+set schema {
+ CREATE TABLE t1(a COLLATE nocase PRIMARY KEY, b);
+ CREATE TABLE t2(a, b PRIMARY KEY);
+}
+do_test 6.0 {
+ execsql $schema db
+ execsql $schema db2
+ execsql {
+ INSERT INTO t1 VALUES('a', 'b');
+ INSERT INTO t2 VALUES('a', 'b');
+ } db2
+} {}
+
+set conflict_return ""
+do_conflict_test 6.1 -tables {t1 t2} -sql {
+ INSERT INTO t1 VALUES('1', '2');
+ INSERT INTO t1 VALUES('A', 'B');
+ INSERT INTO t2 VALUES('A', 'B');
+} -conflicts {
+ {INSERT t1 CONFLICT {t A t B} {t a t b}}
+}
+
+do_db2_test 6.2 "SELECT * FROM t1" {a b 1 2}
+do_db2_test 6.3 "SELECT * FROM t2" {a b A B}
+
+#-------------------------------------------------------------------------
+# Test that session objects are not confused by changes to table in
+# other databases.
+#
+catch { db2 close }
+drop_all_tables
+forcedelete test.db2
+do_iterator_test 7.1 * {
+ ATTACH 'test.db2' AS aux;
+ CREATE TABLE main.t1(x PRIMARY KEY, y);
+ CREATE TABLE aux.t1(x PRIMARY KEY, y);
+
+ INSERT INTO main.t1 VALUES('one', 1);
+ INSERT INTO main.t1 VALUES('two', 2);
+ INSERT INTO aux.t1 VALUES('three', 3);
+ INSERT INTO aux.t1 VALUES('four', 4);
+} {
+ {INSERT t1 0 X. {} {t two i 2}}
+ {INSERT t1 0 X. {} {t one i 1}}
+}
+
+#-------------------------------------------------------------------------
+# Test the sqlite3session_isempty() function.
+#
+do_test 8.1 {
+ execsql {
+ CREATE TABLE t5(x PRIMARY KEY, y);
+ CREATE TABLE t6(x PRIMARY KEY, y);
+ INSERT INTO t5 VALUES('a', 'b');
+ INSERT INTO t6 VALUES('a', 'b');
+ }
+ sqlite3session S db main
+ S attach *
+
+ S isempty
+} {1}
+do_test 8.2 {
+ execsql { DELETE FROM t5 }
+ S isempty
+} {0}
+do_test 8.3 {
+ S delete
+ sqlite3session S db main
+ S attach t5
+ execsql { DELETE FROM t5 }
+ S isempty
+} {1}
+do_test 8.4 { S delete } {}
+
+#-------------------------------------------------------------------------
+#
+do_execsql_test 9.1 {
+ CREATE TABLE t7(a, b, c, d, e PRIMARY KEY, f, g);
+ INSERT INTO t7 VALUES(1, 1, 1, 1, 1, 1, 1);
+}
+do_test 9.2 {
+ sqlite3session S db main
+ S attach *
+ execsql { UPDATE t7 SET b=2, d=2 }
+} {}
+do_changeset_test 9.2 S {{UPDATE t7 0 ....X.. {{} {} i 1 {} {} i 1 i 1 {} {} {} {}} {{} {} i 2 {} {} i 2 {} {} {} {} {} {}}}}
+S delete
+catch { db2 close }
+finish_test
diff --git a/ext/session/session2.test b/ext/session/session2.test
new file mode 100644
index 000000000..04be5f091
--- /dev/null
+++ b/ext/session/session2.test
@@ -0,0 +1,581 @@
+# 2011 Mar 16
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+# The focus of this file is testing the session module.
+#
+
+if {![info exists testdir]} {
+ set testdir [file join [file dirname [info script]] .. .. test]
+}
+source [file join [file dirname [info script]] session_common.tcl]
+source $testdir/tester.tcl
+ifcapable !session {finish_test; return}
+
+set testprefix session2
+
+proc test_reset {} {
+ catch { db close }
+ catch { db2 close }
+ forcedelete test.db test.db2
+ sqlite3 db test.db
+ sqlite3 db2 test.db2
+}
+
+##########################################################################
+# End of proc definitions. Start of tests.
+##########################################################################
+
+test_reset
+do_execsql_test 1.0 {
+ CREATE TABLE t1(a PRIMARY KEY, b);
+ INSERT INTO t1 VALUES('i', 'one');
+}
+do_iterator_test 1.1 t1 {
+ DELETE FROM t1 WHERE a = 'i';
+ INSERT INTO t1 VALUES('ii', 'two');
+} {
+ {DELETE t1 0 X. {t i t one} {}}
+ {INSERT t1 0 X. {} {t ii t two}}
+}
+
+do_iterator_test 1.2 t1 {
+ INSERT INTO t1 VALUES(1.5, 99.9)
+} {
+ {INSERT t1 0 X. {} {f 1.5 f 99.9}}
+}
+
+do_iterator_test 1.3 t1 {
+ UPDATE t1 SET b = 100.1 WHERE a = 1.5;
+ UPDATE t1 SET b = 99.9 WHERE a = 1.5;
+} { }
+
+do_iterator_test 1.4 t1 {
+ UPDATE t1 SET b = 100.1 WHERE a = 1.5;
+} {
+ {UPDATE t1 0 X. {f 1.5 f 99.9} {{} {} f 100.1}}
+}
+
+
+# Execute each of the following blocks of SQL on database [db1]. Collect
+# changes using a session object. Apply the resulting changeset to
+# database [db2]. Then check that the contents of the two databases are
+# identical.
+#
+
+set set_of_tests {
+ 1 { INSERT INTO %T1% VALUES(1, 2) }
+
+ 2 {
+ INSERT INTO %T2% VALUES(1, NULL);
+ INSERT INTO %T2% VALUES(2, NULL);
+ INSERT INTO %T2% VALUES(3, NULL);
+ DELETE FROM %T2% WHERE a = 2;
+ INSERT INTO %T2% VALUES(4, NULL);
+ UPDATE %T2% SET b=0 WHERE b=1;
+ }
+
+ 3 { INSERT INTO %T3% SELECT *, NULL FROM %T2% }
+
+ 4 {
+ INSERT INTO %T3% SELECT a||a, b||b, NULL FROM %T3%;
+ DELETE FROM %T3% WHERE rowid%2;
+ }
+
+ 5 { UPDATE %T3% SET c = a||b }
+
+ 6 { UPDATE %T1% SET a = 32 }
+
+ 7 {
+ INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
+ INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
+ INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
+ INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
+ INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
+ INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
+ INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
+ INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
+ INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
+ INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
+ INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
+ DELETE FROM %T1% WHERE (rowid%3)==0;
+ }
+
+ 8 {
+ BEGIN;
+ INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
+ ROLLBACK;
+ }
+ 9 {
+ BEGIN;
+ UPDATE %T1% SET b = 'xxx';
+ ROLLBACK;
+ }
+ 10 {
+ BEGIN;
+ DELETE FROM %T1% WHERE 1;
+ ROLLBACK;
+ }
+ 11 {
+ INSERT INTO %T1% VALUES(randomblob(21000), randomblob(0));
+ INSERT INTO %T1% VALUES(1.5, 1.5);
+ INSERT INTO %T1% VALUES(4.56, -99.999999999999999999999);
+ }
+ 12 {
+ INSERT INTO %T2% VALUES(NULL, NULL);
+ }
+
+ 13 {
+ DELETE FROM %T1% WHERE 1;
+
+ -- Insert many rows with real primary keys. Enough to force the session
+ -- objects hash table to resize.
+ INSERT INTO %T1% VALUES(0.1, 0.1);
+ INSERT INTO %T1% SELECT a+0.1, b+0.1 FROM %T1%;
+ INSERT INTO %T1% SELECT a+0.2, b+0.2 FROM %T1%;
+ INSERT INTO %T1% SELECT a+0.4, b+0.4 FROM %T1%;
+ INSERT INTO %T1% SELECT a+0.8, b+0.8 FROM %T1%;
+ INSERT INTO %T1% SELECT a+1.6, b+1.6 FROM %T1%;
+ INSERT INTO %T1% SELECT a+3.2, b+3.2 FROM %T1%;
+ INSERT INTO %T1% SELECT a+6.4, b+6.4 FROM %T1%;
+ INSERT INTO %T1% SELECT a+12.8, b+12.8 FROM %T1%;
+ INSERT INTO %T1% SELECT a+25.6, b+25.6 FROM %T1%;
+ INSERT INTO %T1% SELECT a+51.2, b+51.2 FROM %T1%;
+ INSERT INTO %T1% SELECT a+102.4, b+102.4 FROM %T1%;
+ INSERT INTO %T1% SELECT a+204.8, b+204.8 FROM %T1%;
+ }
+
+ 14 {
+ DELETE FROM %T1% WHERE 1;
+ }
+
+ 15 {
+ INSERT INTO %T1% VALUES(1, 1);
+ INSERT INTO %T1% SELECT a+2, b+2 FROM %T1%;
+ INSERT INTO %T1% SELECT a+4, b+4 FROM %T1%;
+ INSERT INTO %T1% SELECT a+8, b+8 FROM %T1%;
+ INSERT INTO %T1% SELECT a+256, b+256 FROM %T1%;
+ }
+
+ 16 {
+ INSERT INTO %T4% VALUES('abc', 'def');
+ INSERT INTO %T4% VALUES('def', 'abc');
+ }
+ 17 { UPDATE %T4% SET b = 1 }
+ 18 { DELETE FROM %T4% WHERE 1 }
+}
+
+test_reset
+do_common_sql {
+ CREATE TABLE t1(a PRIMARY KEY, b);
+ CREATE TABLE t2(a, b INTEGER PRIMARY KEY);
+ CREATE TABLE t3(a, b, c, PRIMARY KEY(a, b));
+ CREATE TABLE t4(a, b, PRIMARY KEY(b, a));
+}
+
+foreach {tn sql} [string map {%T1% t1 %T2% t2 %T3% t3 %T4% t4} $set_of_tests] {
+ do_then_apply_sql $sql
+ do_test 2.$tn { compare_db db db2 } {}
+}
+
+# The following block of tests is similar to the last, except that the
+# session object is recording changes made to an attached database. The
+# main database contains a table of the same name as the table being
+# modified within the attached db.
+#
+test_reset
+forcedelete test.db3
+sqlite3 db3 test.db3
+do_test 3.0 {
+ execsql {
+ ATTACH 'test.db3' AS 'aux';
+ CREATE TABLE t1(a, b PRIMARY KEY);
+ CREATE TABLE t2(x, y, z);
+ CREATE TABLE t3(a);
+
+ CREATE TABLE aux.t1(a PRIMARY KEY, b);
+ CREATE TABLE aux.t2(a, b INTEGER PRIMARY KEY);
+ CREATE TABLE aux.t3(a, b, c, PRIMARY KEY(a, b));
+ CREATE TABLE aux.t4(a, b, PRIMARY KEY(b, a));
+ }
+ execsql {
+ CREATE TABLE t1(a PRIMARY KEY, b);
+ CREATE TABLE t2(a, b INTEGER PRIMARY KEY);
+ CREATE TABLE t3(a, b, c, PRIMARY KEY(a, b));
+ CREATE TABLE t4(a, b, PRIMARY KEY(b, a));
+ } db2
+} {}
+
+proc xTrace {args} { puts $args }
+
+foreach {tn sql} [
+ string map {%T1% aux.t1 %T2% aux.t2 %T3% aux.t3 %T4% aux.t4} $set_of_tests
+] {
+ do_then_apply_sql $sql aux
+ do_test 3.$tn { compare_db db2 db3 } {}
+}
+catch {db3 close}
+
+
+#-------------------------------------------------------------------------
+# The following tests verify that NULL values in primary key columns are
+# handled correctly by the session module.
+#
+test_reset
+do_execsql_test 4.0 {
+ CREATE TABLE t1(a PRIMARY KEY);
+ CREATE TABLE t2(a, b, c, PRIMARY KEY(c, b));
+ CREATE TABLE t3(a, b INTEGER PRIMARY KEY);
+}
+
+foreach {tn sql changeset} {
+ 1 {
+ INSERT INTO t1 VALUES(123);
+ INSERT INTO t1 VALUES(NULL);
+ INSERT INTO t1 VALUES(456);
+ } {
+ {INSERT t1 0 X {} {i 456}}
+ {INSERT t1 0 X {} {i 123}}
+ }
+
+ 2 {
+ UPDATE t1 SET a = NULL;
+ } {
+ {DELETE t1 0 X {i 456} {}}
+ {DELETE t1 0 X {i 123} {}}
+ }
+
+ 3 { DELETE FROM t1 } { }
+
+ 4 {
+ INSERT INTO t3 VALUES(NULL, NULL)
+ } {
+ {INSERT t3 0 .X {} {n {} i 1}}
+ }
+
+ 5 { INSERT INTO t2 VALUES(1, 2, NULL) } { }
+ 6 { INSERT INTO t2 VALUES(1, NULL, 3) } { }
+ 7 { INSERT INTO t2 VALUES(1, NULL, NULL) } { }
+ 8 { INSERT INTO t2 VALUES(1, 2, 3) } { {INSERT t2 0 .XX {} {i 1 i 2 i 3}} }
+ 9 { DELETE FROM t2 WHERE 1 } { {DELETE t2 0 .XX {i 1 i 2 i 3} {}} }
+
+} {
+ do_iterator_test 4.$tn {t1 t2 t3} $sql $changeset
+}
+
+
+#-------------------------------------------------------------------------
+# Test that if NULL is passed to sqlite3session_attach(), all database
+# tables are attached to the session object.
+#
+test_reset
+do_execsql_test 5.0 {
+ CREATE TABLE t1(a PRIMARY KEY);
+ CREATE TABLE t2(x, y PRIMARY KEY);
+}
+
+foreach {tn sql changeset} {
+ 1 { INSERT INTO t1 VALUES(35) } { {INSERT t1 0 X {} {i 35}} }
+ 2 { INSERT INTO t2 VALUES(36, 37) } { {INSERT t2 0 .X {} {i 36 i 37}} }
+ 3 {
+ DELETE FROM t1 WHERE 1;
+ UPDATE t2 SET x = 34;
+ } {
+ {UPDATE t2 0 .X {i 36 i 37} {i 34 {} {}}}
+ {DELETE t1 0 X {i 35} {}}
+ }
+} {
+ do_iterator_test 5.$tn * $sql $changeset
+}
+
+#-------------------------------------------------------------------------
+# The next block of tests verify that the "indirect" flag is set
+# correctly within changesets. The indirect flag is set for a change
+# if either of the following are true:
+#
+# * The sqlite3session_indirect() API has been used to set the session
+# indirect flag to true, or
+# * The change was made by a trigger.
+#
+# If the same row is updated more than once during a session, then the
+# change is considered indirect only if all changes meet the criteria
+# above.
+#
+test_reset
+db function indirect [list S indirect]
+
+do_execsql_test 6.0 {
+ CREATE TABLE t1(a PRIMARY KEY, b, c);
+
+ CREATE TABLE t2(x PRIMARY KEY, y);
+ CREATE TRIGGER AFTER INSERT ON t2 WHEN new.x%2 BEGIN
+ INSERT INTO t2 VALUES(new.x+1, NULL);
+ END;
+}
+
+do_iterator_test 6.1.1 * {
+ INSERT INTO t1 VALUES(1, 'one', 'i');
+ SELECT indirect(1);
+ INSERT INTO t1 VALUES(2, 'two', 'ii');
+ SELECT indirect(0);
+ INSERT INTO t1 VALUES(3, 'three', 'iii');
+} {
+ {INSERT t1 0 X.. {} {i 1 t one t i}}
+ {INSERT t1 1 X.. {} {i 2 t two t ii}}
+ {INSERT t1 0 X.. {} {i 3 t three t iii}}
+}
+
+do_iterator_test 6.1.2 * {
+ SELECT indirect(1);
+ UPDATE t1 SET c = 'I' WHERE a = 1;
+ SELECT indirect(0);
+} {
+ {UPDATE t1 1 X.. {i 1 {} {} t i} {{} {} {} {} t I}}
+}
+do_iterator_test 6.1.3 * {
+ SELECT indirect(1);
+ UPDATE t1 SET c = '.' WHERE a = 1;
+ SELECT indirect(0);
+ UPDATE t1 SET c = 'o' WHERE a = 1;
+} {
+ {UPDATE t1 0 X.. {i 1 {} {} t I} {{} {} {} {} t o}}
+}
+do_iterator_test 6.1.4 * {
+ SELECT indirect(0);
+ UPDATE t1 SET c = 'x' WHERE a = 1;
+ SELECT indirect(1);
+ UPDATE t1 SET c = 'i' WHERE a = 1;
+} {
+ {UPDATE t1 0 X.. {i 1 {} {} t o} {{} {} {} {} t i}}
+}
+do_iterator_test 6.1.4 * {
+ SELECT indirect(1);
+ UPDATE t1 SET c = 'y' WHERE a = 1;
+ SELECT indirect(1);
+ UPDATE t1 SET c = 'I' WHERE a = 1;
+} {
+ {UPDATE t1 1 X.. {i 1 {} {} t i} {{} {} {} {} t I}}
+}
+
+do_iterator_test 6.1.5 * {
+ INSERT INTO t2 VALUES(1, 'x');
+} {
+ {INSERT t2 0 X. {} {i 1 t x}}
+ {INSERT t2 1 X. {} {i 2 n {}}}
+}
+
+do_iterator_test 6.1.6 * {
+ SELECT indirect(1);
+ INSERT INTO t2 VALUES(3, 'x');
+ SELECT indirect(0);
+ UPDATE t2 SET y = 'y' WHERE x>2;
+} {
+ {INSERT t2 0 X. {} {i 3 t y}}
+ {INSERT t2 0 X. {} {i 4 t y}}
+}
+
+do_iterator_test 6.1.7 * {
+ SELECT indirect(1);
+ DELETE FROM t2 WHERE x = 4;
+ SELECT indirect(0);
+ INSERT INTO t2 VALUES(4, 'new');
+} {
+ {UPDATE t2 0 X. {i 4 t y} {{} {} t new}}
+}
+
+do_iterator_test 6.1.8 * {
+ CREATE TABLE t3(a, b PRIMARY KEY);
+ CREATE TABLE t4(a, b PRIMARY KEY);
+ CREATE TRIGGER t4t AFTER UPDATE ON t4 BEGIN
+ UPDATE t3 SET a = new.a WHERE b = new.b;
+ END;
+
+ SELECT indirect(1);
+ INSERT INTO t3 VALUES('one', 1);
+ INSERT INTO t4 VALUES('one', 1);
+ SELECT indirect(0);
+ UPDATE t4 SET a = 'two' WHERE b = 1;
+} {
+ {INSERT t4 0 .X {} {t two i 1}}
+ {INSERT t3 1 .X {} {t two i 1}}
+}
+
+sqlite3session S db main
+do_execsql_test 6.2.1 {
+ SELECT indirect(0);
+ SELECT indirect(-1);
+ SELECT indirect(45);
+ SELECT indirect(-100);
+} {0 0 1 1}
+S delete
+
+#-------------------------------------------------------------------------
+# Test that if a conflict-handler that has been passed either NOTFOUND or
+# CONSTRAINT returns REPLACE - the sqlite3changeset_apply() call returns
+# MISUSE and rolls back any changes made so far.
+#
+# 7.1.*: NOTFOUND conflict-callback.
+# 7.2.*: CONSTRAINT conflict-callback.
+#
+proc xConflict {args} {return REPLACE}
+test_reset
+
+do_execsql_test 7.1.1 {
+ CREATE TABLE t1(a PRIMARY KEY, b);
+ INSERT INTO t1 VALUES(1, 'one');
+ INSERT INTO t1 VALUES(2, 'two');
+}
+do_test 7.1.2 {
+ execsql {
+ CREATE TABLE t1(a PRIMARY KEY, b NOT NULL);
+ INSERT INTO t1 VALUES(1, 'one');
+ } db2
+} {}
+do_test 7.1.3 {
+ set changeset [changeset_from_sql {
+ UPDATE t1 SET b = 'five' WHERE a = 1;
+ UPDATE t1 SET b = 'six' WHERE a = 2;
+ }]
+ set x [list]
+ sqlite3session_foreach c $changeset { lappend x $c }
+ set x
+} [list \
+ {UPDATE t1 0 X. {i 1 t one} {{} {} t five}} \
+ {UPDATE t1 0 X. {i 2 t two} {{} {} t six}} \
+]
+do_test 7.1.4 {
+ list [catch {sqlite3changeset_apply db2 $changeset xConflict} msg] $msg
+} {1 SQLITE_MISUSE}
+do_test 7.1.5 { execsql { SELECT * FROM t1 } db2 } {1 one}
+
+do_test 7.2.1 {
+ set changeset [changeset_from_sql { UPDATE t1 SET b = NULL WHERE a = 1 }]
+
+ set x [list]
+ sqlite3session_foreach c $changeset { lappend x $c }
+ set x
+} [list \
+ {UPDATE t1 0 X. {i 1 t five} {{} {} n {}}} \
+]
+do_test 7.2.2 {
+ list [catch {sqlite3changeset_apply db2 $changeset xConflict} msg] $msg
+} {1 SQLITE_MISUSE}
+do_test 7.2.3 { execsql { SELECT * FROM t1 } db2 } {1 one}
+
+#-------------------------------------------------------------------------
+# Test that if a conflict-handler returns ABORT, application of the
+# changeset is rolled back and the sqlite3changeset_apply() method returns
+# SQLITE_ABORT.
+#
+# Also test that the same thing happens if a conflict handler returns an
+# unrecognized integer value. Except, in this case SQLITE_MISUSE is returned
+# instead of SQLITE_ABORT.
+#
+foreach {tn conflict_return apply_return} {
+ 1 ABORT SQLITE_ABORT
+ 2 567 SQLITE_MISUSE
+} {
+ test_reset
+ proc xConflict {args} [list return $conflict_return]
+
+ do_test 8.$tn.0 {
+ do_common_sql {
+ CREATE TABLE t1(x, y, PRIMARY KEY(x, y));
+ INSERT INTO t1 VALUES('x', 'y');
+ }
+ execsql { INSERT INTO t1 VALUES('w', 'w') }
+
+ set changeset [changeset_from_sql { DELETE FROM t1 WHERE 1 }]
+
+ set x [list]
+ sqlite3session_foreach c $changeset { lappend x $c }
+ set x
+ } [list \
+ {DELETE t1 0 XX {t w t w} {}} \
+ {DELETE t1 0 XX {t x t y} {}} \
+ ]
+
+ do_test 8.$tn.1 {
+ list [catch {sqlite3changeset_apply db2 $changeset xConflict} msg] $msg
+ } [list 1 $apply_return]
+
+ do_test 8.$tn.2 {
+ execsql {SELECT * FROM t1} db2
+ } {x y}
+}
+
+
+#-------------------------------------------------------------------------
+# Try to cause an infinite loop as follows:
+#
+# 1. Have a changeset insert a row that causes a CONFLICT callback,
+# 2. Have the conflict handler return REPLACE,
+# 3. After the session module deletes the conflicting row, have a trigger
+# re-insert it.
+# 4. Goto step 1...
+#
+# This doesn't work, as the second invocation of the conflict handler is a
+# CONSTRAINT, not a CONFLICT. There is at most one CONFLICT callback for
+# each change in the changeset.
+#
+test_reset
+proc xConflict {type args} {
+ if {$type == "CONFLICT"} { return REPLACE }
+ return OMIT
+}
+do_test 9.1 {
+ execsql {
+ CREATE TABLE t1(a PRIMARY KEY, b);
+ }
+ execsql {
+ CREATE TABLE t1(a PRIMARY KEY, b);
+ INSERT INTO t1 VALUES('x', 2);
+ CREATE TRIGGER tr1 AFTER DELETE ON t1 BEGIN
+ INSERT INTO t1 VALUES(old.a, old.b);
+ END;
+ } db2
+} {}
+do_test 9.2 {
+ set changeset [changeset_from_sql { INSERT INTO t1 VALUES('x', 1) }]
+ sqlite3changeset_apply db2 $changeset xConflict
+} {}
+do_test 9.3 {
+ execsql { SELECT * FROM t1 } db2
+} {x 2}
+
+#-------------------------------------------------------------------------
+#
+test_reset
+db function enable [list S enable]
+
+do_common_sql {
+ CREATE TABLE t1(a PRIMARY KEY, b);
+ INSERT INTO t1 VALUES('x', 'X');
+}
+
+do_iterator_test 10.1 t1 {
+ INSERT INTO t1 VALUES('y', 'Y');
+ SELECT enable(0);
+ INSERT INTO t1 VALUES('z', 'Z');
+ SELECT enable(1);
+} {
+ {INSERT t1 0 X. {} {t y t Y}}
+}
+
+sqlite3session S db main
+do_execsql_test 10.2 {
+ SELECT enable(0);
+ SELECT enable(-1);
+ SELECT enable(1);
+ SELECT enable(-1);
+} {0 0 1 1}
+S delete
+
+finish_test
diff --git a/ext/session/session3.test b/ext/session/session3.test
new file mode 100644
index 000000000..e15407c2e
--- /dev/null
+++ b/ext/session/session3.test
@@ -0,0 +1,211 @@
+# 2011 March 24
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+# This file implements regression tests for the session module. More
+# specifically, it focuses on testing the session modules response to
+# database schema modifications and mismatches.
+#
+
+if {![info exists testdir]} {
+ set testdir [file join [file dirname [info script]] .. .. test]
+}
+source [file join [file dirname [info script]] session_common.tcl]
+source $testdir/tester.tcl
+ifcapable !session {finish_test; return}
+
+set testprefix session3
+
+#-------------------------------------------------------------------------
+# These tests - session3-1.* - verify that the session module behaves
+# correctly when confronted with a schema mismatch when applying a
+# changeset (in function sqlite3changeset_apply()).
+#
+# session3-1.1.*: Table does not exist in target db.
+# session3-1.2.*: Table has wrong number of columns in target db.
+# session3-1.3.*: Table has wrong PK columns in target db.
+#
+db close
+sqlite3_shutdown
+test_sqlite3_log log
+sqlite3 db test.db
+
+proc log {code msg} { lappend ::log $code $msg }
+
+forcedelete test.db2
+sqlite3 db2 test.db2
+
+do_execsql_test 1.0 {
+ CREATE TABLE t1(a PRIMARY KEY, b);
+}
+do_test 1.1 {
+ set ::log {}
+ do_then_apply_sql {
+ INSERT INTO t1 VALUES(1, 2);
+ INSERT INTO t1 VALUES(3, 4);
+ }
+ set ::log
+} {SQLITE_SCHEMA {sqlite3changeset_apply(): no such table: t1}}
+
+do_test 1.2.0 {
+ execsql { CREATE TABLE t1(a PRIMARY KEY, b, c) } db2
+} {}
+do_test 1.2.1 {
+ set ::log {}
+ do_then_apply_sql {
+ INSERT INTO t1 VALUES(5, 6);
+ INSERT INTO t1 VALUES(7, 8);
+ }
+ set ::log
+} {SQLITE_SCHEMA {sqlite3changeset_apply(): table t1 has 3 columns, expected 2}}
+
+do_test 1.3.0 {
+ execsql {
+ DROP TABLE t1;
+ CREATE TABLE t1(a, b PRIMARY KEY);
+ } db2
+} {}
+do_test 1.3.1 {
+ set ::log {}
+ do_then_apply_sql {
+ INSERT INTO t1 VALUES(9, 10);
+ INSERT INTO t1 VALUES(11, 12);
+ }
+ set ::log
+} {SQLITE_SCHEMA {sqlite3changeset_apply(): primary key mismatch for table t1}}
+
+#-------------------------------------------------------------------------
+# These tests - session3-2.* - verify that the session module behaves
+# correctly when the schema of an attached table is modified during the
+# session.
+#
+# session3-2.1.*: Table is dropped midway through the session.
+# session3-2.2.*: Table is dropped and recreated with a different # cols.
+# session3-2.3.*: Table is dropped and recreated with a different PK.
+#
+# In all of these scenarios, the call to sqlite3session_changeset() will
+# return SQLITE_SCHEMA. Also:
+#
+# session3-2.4.*: Table is dropped and recreated with an identical schema.
+# In this case sqlite3session_changeset() returns SQLITE_OK.
+#
+
+do_test 2.1 {
+ execsql { CREATE TABLE t2(a, b PRIMARY KEY) }
+ sqlite3session S db main
+ S attach t2
+ execsql {
+ INSERT INTO t2 VALUES(1, 2);
+ DROP TABLE t2;
+ }
+ list [catch { S changeset } msg] $msg
+} {1 SQLITE_SCHEMA}
+
+do_test 2.2.1 {
+ S delete
+ sqlite3session S db main
+ execsql { CREATE TABLE t2(a, b PRIMARY KEY, c) }
+ S attach t2
+ execsql {
+ INSERT INTO t2 VALUES(1, 2, 3);
+ DROP TABLE t2;
+ CREATE TABLE t2(a, b PRIMARY KEY);
+ }
+ list [catch { S changeset } msg] $msg
+} {1 SQLITE_SCHEMA}
+do_test 2.2.2 {
+ S delete
+ sqlite3session S db main
+ execsql {
+ DROP TABLE t2;
+ CREATE TABLE t2(a, b PRIMARY KEY, c);
+ }
+ S attach t2
+ execsql {
+ INSERT INTO t2 VALUES(1, 2, 3);
+ DROP TABLE t2;
+ CREATE TABLE t2(a, b PRIMARY KEY, c, d);
+ }
+ list [catch { S changeset } msg] $msg
+} {1 SQLITE_SCHEMA}
+do_test 2.2.3 {
+ S delete
+ sqlite3session S db main
+ execsql {
+ DROP TABLE t2;
+ CREATE TABLE t2(a, b PRIMARY KEY, c);
+ }
+ S attach t2
+ execsql {
+ INSERT INTO t2 VALUES(1, 2, 3);
+ DROP TABLE t2;
+ CREATE TABLE t2(a, b PRIMARY KEY);
+ INSERT INTO t2 VALUES(4, 5);
+ }
+ list [catch { S changeset } msg] $msg
+} {1 SQLITE_SCHEMA}
+do_test 2.2.4 {
+ S delete
+ sqlite3session S db main
+ execsql {
+ DROP TABLE t2;
+ CREATE TABLE t2(a, b PRIMARY KEY, c);
+ }
+ S attach t2
+ execsql {
+ INSERT INTO t2 VALUES(1, 2, 3);
+ DROP TABLE t2;
+ CREATE TABLE t2(a, b PRIMARY KEY, c, d);
+ INSERT INTO t2 VALUES(4, 5, 6, 7);
+ }
+ list [catch { S changeset } msg] $msg
+} {1 SQLITE_SCHEMA}
+
+do_test 2.3 {
+ S delete
+ sqlite3session S db main
+ execsql {
+ DROP TABLE t2;
+ CREATE TABLE t2(a, b PRIMARY KEY);
+ }
+ S attach t2
+ execsql {
+ INSERT INTO t2 VALUES(1, 2);
+ DROP TABLE t2;
+ CREATE TABLE t2(a PRIMARY KEY, b);
+ }
+ list [catch { S changeset } msg] $msg
+} {1 SQLITE_SCHEMA}
+
+do_test 2.4 {
+ S delete
+ sqlite3session S db main
+ execsql {
+ DROP TABLE t2;
+ CREATE TABLE t2(a, b PRIMARY KEY);
+ }
+ S attach t2
+ execsql {
+ INSERT INTO t2 VALUES(1, 2);
+ DROP TABLE t2;
+ CREATE TABLE t2(a, b PRIMARY KEY);
+ }
+ list [catch { S changeset } msg] $msg
+} {0 {}}
+
+S delete
+
+
+catch { db close }
+catch { db2 close }
+sqlite3_shutdown
+test_sqlite3_log
+sqlite3_initialize
+
+finish_test
diff --git a/ext/session/session4.test b/ext/session/session4.test
new file mode 100644
index 000000000..8e179baf6
--- /dev/null
+++ b/ext/session/session4.test
@@ -0,0 +1,68 @@
+# 2011 March 25
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+# This file implements regression tests for the session module.
+#
+
+if {![info exists testdir]} {
+ set testdir [file join [file dirname [info script]] .. .. test]
+}
+source [file join [file dirname [info script]] session_common.tcl]
+source $testdir/tester.tcl
+ifcapable !session {finish_test; return}
+
+set testprefix session4
+
+do_test 1.0 {
+ execsql {
+ CREATE TABLE x(a, b, c, d, e, PRIMARY KEY(c, e));
+ INSERT INTO x VALUES(65.21, X'28B0', 16.35, NULL, 'doers');
+ INSERT INTO x VALUES(NULL, 78.49, 2, X'60', -66);
+ INSERT INTO x VALUES('cathedral', NULL, 35, NULL, X'B220937E80A2D8');
+ INSERT INTO x VALUES(NULL, 'masking', -91.37, NULL, X'596D');
+ INSERT INTO x VALUES(19, 'domains', 'espouse', -94, 'throw');
+ }
+
+ sqlite3session S db main
+ set changeset [changeset_from_sql {
+ DELETE FROM x WHERE e = -66;
+ UPDATE x SET a = 'parameterizable', b = 31.8 WHERE c = 35;
+ INSERT INTO x VALUES(-75.61, -17, 16.85, NULL, X'D73DB02678');
+ }]
+ set {} {}
+} {}
+
+
+# This currently causes crashes. sqlite3changeset_invert() does not handle
+# corrupt changesets well.
+if 0 {
+ do_test 1.1 {
+ for {set i 0} {$i < [string length $changeset]} {incr i} {
+ set before [string range $changeset 0 [expr $i-1]]
+ set after [string range $changeset [expr $i+1] end]
+ for {set j 10} {$j < 260} {incr j} {
+ set x [binary format "a*ca*" $before $j $after]
+ catch { sqlite3changeset_invert $x }
+ }
+ }
+ } {}
+}
+
+do_test 1.2 {
+ set x [binary format "ca*" 0 [string range $changeset 1 end]]
+ list [catch { sqlite3changeset_invert $x } msg] $msg
+} {1 SQLITE_CORRUPT}
+
+do_test 1.3 {
+ set x [binary format "ca*" 0 [string range $changeset 1 end]]
+ list [catch { sqlite3changeset_apply db $x xConflict } msg] $msg
+} {1 SQLITE_CORRUPT}
+
+finish_test
diff --git a/ext/session/session5.test b/ext/session/session5.test
new file mode 100644
index 000000000..88430057d
--- /dev/null
+++ b/ext/session/session5.test
@@ -0,0 +1,409 @@
+# 2011 April 13
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+# This file implements regression tests for the session module.
+# Specifically, for the sqlite3changeset_concat() command.
+#
+
+if {![info exists testdir]} {
+ set testdir [file join [file dirname [info script]] .. .. test]
+}
+source [file join [file dirname [info script]] session_common.tcl]
+source $testdir/tester.tcl
+ifcapable !session {finish_test; return}
+
+set testprefix session5
+
+# Organization of tests:
+#
+# session5-1.*: Simple tests to check the concat() function produces
+# correct results.
+#
+# session5-2.*: More complicated tests.
+#
+# session5-3.*: Schema mismatch errors.
+#
+# session5-4.*: Test the concat cases that indicate that the database
+# was modified in between recording of the two changesets
+# being concatenated (i.e. two changesets that INSERT rows
+# with the same PK values).
+#
+
+proc do_concat_test {tn args} {
+
+ set subtest 0
+ foreach sql $args {
+ incr subtest
+ sqlite3session S db main ; S attach *
+ execsql $sql
+
+ set c [S changeset]
+ if {[info commands s_prev] != ""} {
+ set c_concat [sqlite3changeset_concat $c_prev $c]
+ set c_two [s_prev changeset]
+ s_prev delete
+
+ set h_concat [changeset_to_list $c_concat]
+ set h_two [changeset_to_list $c_two]
+
+ do_test $tn.$subtest [list set {} $h_concat] $h_two
+ }
+ set c_prev $c
+ rename S s_prev
+ }
+
+ catch { s_prev delete }
+}
+
+#-------------------------------------------------------------------------
+# Test cases session5-1.* - simple tests.
+#
+do_execsql_test 1.0 {
+ CREATE TABLE t1(a PRIMARY KEY, b);
+}
+
+do_concat_test 1.1.1 {
+ INSERT INTO t1 VALUES(1, 'one');
+} {
+ INSERT INTO t1 VALUES(2, 'two');
+}
+
+do_concat_test 1.1.2 {
+ UPDATE t1 SET b = 'five' WHERE a = 1;
+} {
+ UPDATE t1 SET b = 'six' WHERE a = 2;
+}
+
+do_concat_test 1.1.3 {
+ DELETE FROM t1 WHERE a = 1;
+} {
+ DELETE FROM t1 WHERE a = 2;
+}
+
+
+# 1.2.1: INSERT + DELETE -> (none)
+# 1.2.2: INSERT + UPDATE -> INSERT
+#
+# 1.2.3: DELETE + INSERT (matching data) -> (none)
+# 1.2.4: DELETE + INSERT (non-matching data) -> UPDATE
+#
+# 1.2.5: UPDATE + UPDATE (matching data) -> (none)
+# 1.2.6: UPDATE + UPDATE (non-matching data) -> UPDATE
+# 1.2.7: UPDATE + DELETE -> DELETE
+#
+do_concat_test 1.2.1 {
+ INSERT INTO t1 VALUES('x', 'y');
+} {
+ DELETE FROM t1 WHERE a = 'x';
+}
+do_concat_test 1.2.2 {
+ INSERT INTO t1 VALUES(5.0, 'five');
+} {
+ UPDATE t1 SET b = 'six' WHERE a = 5.0;
+}
+
+do_execsql_test 1.2.3.1 "INSERT INTO t1 VALUES('I', 'one')"
+do_concat_test 1.2.3.2 {
+ DELETE FROM t1 WHERE a = 'I';
+} {
+ INSERT INTO t1 VALUES('I', 'one');
+}
+do_concat_test 1.2.4 {
+ DELETE FROM t1 WHERE a = 'I';
+} {
+ INSERT INTO t1 VALUES('I', 'two');
+}
+do_concat_test 1.2.5 {
+ UPDATE t1 SET b = 'five' WHERE a = 'I';
+} {
+ UPDATE t1 SET b = 'two' WHERE a = 'I';
+}
+do_concat_test 1.2.6 {
+ UPDATE t1 SET b = 'six' WHERE a = 'I';
+} {
+ UPDATE t1 SET b = 'seven' WHERE a = 'I';
+}
+do_concat_test 1.2.7 {
+ UPDATE t1 SET b = 'eight' WHERE a = 'I';
+} {
+ DELETE FROM t1 WHERE a = 'I';
+}
+
+
+#-------------------------------------------------------------------------
+# Test cases session5-2.* - more complex tests.
+#
+db function indirect indirect
+proc indirect {{x -1}} {
+ S indirect $x
+ s_prev indirect $x
+}
+do_concat_test 2.1 {
+ CREATE TABLE abc(a, b, c PRIMARY KEY);
+ INSERT INTO abc VALUES(NULL, NULL, 1);
+ INSERT INTO abc VALUES('abcdefghijkl', NULL, 2);
+} {
+ DELETE FROM abc WHERE c = 1;
+ UPDATE abc SET c = 1 WHERE c = 2;
+} {
+ INSERT INTO abc VALUES('abcdefghijkl', NULL, 2);
+ INSERT INTO abc VALUES(1.0, 2.0, 3);
+} {
+ UPDATE abc SET a = a-1;
+} {
+ CREATE TABLE def(d, e, f, PRIMARY KEY(e, f));
+ INSERT INTO def VALUES('x', randomblob(11000), 67);
+ INSERT INTO def SELECT d, e, f+1 FROM def;
+ INSERT INTO def SELECT d, e, f+2 FROM def;
+ INSERT INTO def SELECT d, e, f+4 FROM def;
+} {
+ DELETE FROM def WHERE rowid>4;
+} {
+ INSERT INTO def SELECT d, e, f+4 FROM def;
+} {
+ INSERT INTO abc VALUES(22, 44, -1);
+} {
+ UPDATE abc SET c=-2 WHERE c=-1;
+ UPDATE abc SET c=-3 WHERE c=-2;
+} {
+ UPDATE abc SET c=-4 WHERE c=-3;
+} {
+ UPDATE abc SET a=a+1 WHERE c=-3;
+ UPDATE abc SET a=a+1 WHERE c=-3;
+} {
+ UPDATE abc SET a=a+1 WHERE c=-3;
+ UPDATE abc SET a=a+1 WHERE c=-3;
+} {
+ INSERT INTO abc VALUES('one', 'two', 'three');
+} {
+ SELECT indirect(1);
+ UPDATE abc SET a='one point five' WHERE c = 'three';
+} {
+ SELECT indirect(0);
+ UPDATE abc SET a='one point six' WHERE c = 'three';
+} {
+ CREATE TABLE x1(a, b, PRIMARY KEY(a));
+ SELECT indirect(1);
+ INSERT INTO x1 VALUES(1, 2);
+} {
+ SELECT indirect(1);
+ UPDATE x1 SET b = 3 WHERE a = 1;
+}
+
+catch {db close}
+forcedelete test.db
+sqlite3 db test.db
+do_concat_test 2.2 {
+ CREATE TABLE t1(a, b, PRIMARY KEY(b));
+ CREATE TABLE t2(a PRIMARY KEY, b);
+ INSERT INTO t1 VALUES('string', 1);
+ INSERT INTO t1 VALUES(4, 2);
+ INSERT INTO t1 VALUES(X'FFAAFFAAFFAA', 3);
+} {
+ INSERT INTO t2 VALUES('one', 'two');
+ INSERT INTO t2 VALUES(1, NULL);
+ UPDATE t1 SET a = 5 WHERE a = 2;
+} {
+ DELETE FROM t2 WHERE a = 1;
+ UPDATE t1 SET a = 4 WHERE a = 2;
+ INSERT INTO t2 VALUES('x', 'y');
+}
+
+do_test 2.3.0 {
+ catch {db close}
+ forcedelete test.db
+ sqlite3 db test.db
+
+ set sql1 ""
+ set sql2 ""
+ for {set i 1} {$i < 120} {incr i} {
+ append sql1 "INSERT INTO x1 VALUES($i*4, $i);"
+ }
+ for {set i 1} {$i < 120} {incr i} {
+ append sql2 "DELETE FROM x1 WHERE a = $i*4;"
+ }
+ set {} {}
+} {}
+do_concat_test 2.3 {
+ CREATE TABLE x1(a PRIMARY KEY, b)
+} $sql1 $sql2 $sql1 $sql2
+
+do_concat_test 2.4 {
+ CREATE TABLE x2(a PRIMARY KEY, b);
+ CREATE TABLE x3(a PRIMARY KEY, b);
+
+ INSERT INTO x2 VALUES('a', 'b');
+ INSERT INTO x2 VALUES('x', 'y');
+ INSERT INTO x3 VALUES('a', 'b');
+} {
+ INSERT INTO x2 VALUES('c', 'd');
+ INSERT INTO x3 VALUES('e', 'f');
+ INSERT INTO x3 VALUES('x', 'y');
+}
+
+do_concat_test 2.5 {
+ UPDATE x3 SET b = 'Y' WHERE a = 'x'
+} {
+ DELETE FROM x3 WHERE a = 'x'
+} {
+ DELETE FROM x2 WHERE a = 'a'
+} {
+ INSERT INTO x2 VALUES('a', 'B');
+}
+
+for {set k 1} {$k <=10} {incr k} {
+ do_test 2.6.$k.1 {
+ drop_all_tables
+ set sql1 ""
+ set sql2 ""
+ for {set i 1} {$i < 120} {incr i} {
+ append sql1 "INSERT INTO x1 VALUES(randomblob(20+(random()%10)), $i);"
+ }
+ for {set i 1} {$i < 120} {incr i} {
+ append sql2 "DELETE FROM x1 WHERE rowid = $i;"
+ }
+ set {} {}
+ } {}
+ do_concat_test 2.6.$k {
+ CREATE TABLE x1(a PRIMARY KEY, b)
+ } $sql1 $sql2 $sql1 $sql2
+}
+
+for {set k 1} {$k <=10} {incr k} {
+ do_test 2.7.$k.1 {
+ drop_all_tables
+ set sql1 ""
+ set sql2 ""
+ for {set i 1} {$i < 120} {incr i} {
+ append sql1 {
+ INSERT INTO x1 VALUES(
+ CASE WHEN random()%2 THEN random() ELSE randomblob(20+random()%10) END,
+ CASE WHEN random()%2 THEN random() ELSE randomblob(20+random()%10) END
+ );
+ }
+ }
+ for {set i 1} {$i < 120} {incr i} {
+ append sql2 "DELETE FROM x1 WHERE rowid = $i;"
+ }
+ set {} {}
+ } {}
+ do_concat_test 2.7.$k {
+ CREATE TABLE x1(a PRIMARY KEY, b)
+ } $sql1 $sql2 $sql1 $sql2
+}
+
+
+#-------------------------------------------------------------------------
+# Test that schema incompatibilities are detected correctly.
+#
+# session5-3.1: Incompatible number of columns.
+# session5-3.2: Incompatible PK definition.
+#
+
+do_test 3.1 {
+ db close
+ forcedelete test.db
+ sqlite3 db test.db
+
+ execsql { CREATE TABLE t1(a PRIMARY KEY, b) }
+ set c1 [changeset_from_sql { INSERT INTO t1 VALUES(1, 2) }]
+ execsql {
+ DROP TABLE t1;
+ CREATE TABLE t1(a PRIMARY KEY, b, c);
+ }
+ set c2 [changeset_from_sql { INSERT INTO t1 VALUES(2, 3, 4) }]
+
+ list [catch { sqlite3changeset_concat $c1 $c2 } msg] $msg
+} {1 SQLITE_SCHEMA}
+
+do_test 3.2 {
+ db close
+ forcedelete test.db
+ sqlite3 db test.db
+
+ execsql { CREATE TABLE t1(a PRIMARY KEY, b) }
+ set c1 [changeset_from_sql { INSERT INTO t1 VALUES(1, 2) }]
+ execsql {
+ DROP TABLE t1;
+ CREATE TABLE t1(a, b PRIMARY KEY);
+ }
+ set c2 [changeset_from_sql { INSERT INTO t1 VALUES(2, 3) }]
+
+ list [catch { sqlite3changeset_concat $c1 $c2 } msg] $msg
+} {1 SQLITE_SCHEMA}
+
+#-------------------------------------------------------------------------
+# Test that concat() handles these properly:
+#
+# session5-4.1: INSERT + INSERT
+# session5-4.2: UPDATE + INSERT
+# session5-4.3: DELETE + UPDATE
+# session5-4.4: DELETE + DELETE
+#
+
+proc do_concat_test2 {tn sql1 sqlX sql2 expected} {
+ sqlite3session S db main ; S attach *
+ execsql $sql1
+ set ::c1 [S changeset]
+ S delete
+
+ execsql $sqlX
+
+ sqlite3session S db main ; S attach *
+ execsql $sql2
+ set ::c2 [S changeset]
+ S delete
+
+ uplevel do_test $tn [list {
+ changeset_to_list [sqlite3changeset_concat $::c1 $::c2]
+ }] [list [normalize_list $expected]]
+}
+
+drop_all_tables db
+do_concat_test2 4.1 {
+ CREATE TABLE t1(a PRIMARY KEY, b);
+ INSERT INTO t1 VALUES('key', 'value');
+} {
+ DELETE FROM t1 WHERE a = 'key';
+} {
+ INSERT INTO t1 VALUES('key', 'xxx');
+} {
+ {INSERT t1 0 X. {} {t key t value}}
+}
+do_concat_test2 4.2 {
+ UPDATE t1 SET b = 'yyy';
+} {
+ DELETE FROM t1 WHERE a = 'key';
+} {
+ INSERT INTO t1 VALUES('key', 'value');
+} {
+ {UPDATE t1 0 X. {t key t xxx} {{} {} t yyy}}
+}
+do_concat_test2 4.3 {
+ DELETE FROM t1 WHERE a = 'key';
+} {
+ INSERT INTO t1 VALUES('key', 'www');
+} {
+ UPDATE t1 SET b = 'valueX' WHERE a = 'key';
+} {
+ {DELETE t1 0 X. {t key t value} {}}
+}
+do_concat_test2 4.4 {
+ DELETE FROM t1 WHERE a = 'key';
+} {
+ INSERT INTO t1 VALUES('key', 'ttt');
+} {
+ DELETE FROM t1 WHERE a = 'key';
+} {
+ {DELETE t1 0 X. {t key t valueX} {}}
+}
+
+finish_test
+
diff --git a/ext/session/session6.test b/ext/session/session6.test
new file mode 100644
index 000000000..8a1f172cd
--- /dev/null
+++ b/ext/session/session6.test
@@ -0,0 +1,90 @@
+# 2011 July 11
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+# This file implements regression tests for SQLite sessions extension.
+# Specifically, it tests that sessions work when the database is modified
+# using incremental blob handles.
+#
+
+if {![info exists testdir]} {
+ set testdir [file join [file dirname [info script]] .. .. test]
+}
+source [file join [file dirname [info script]] session_common.tcl]
+source $testdir/tester.tcl
+ifcapable !session {finish_test; return}
+
+set testprefix session6
+
+proc do_then_apply_tcl {tcl {dbname main}} {
+ proc xConflict args { return "OMIT" }
+ set rc [catch {
+ sqlite3session S db $dbname
+ db eval "SELECT name FROM $dbname.sqlite_master WHERE type = 'table'" {
+ S attach $name
+ }
+ eval $tcl
+ sqlite3changeset_apply db2 [S changeset] xConflict
+ } msg]
+
+ catch { S delete }
+ if {$rc} {error $msg}
+}
+
+test_sqlite3_log x
+proc x {args} {puts $args}
+
+forcedelete test.db2
+sqlite3 db2 test.db2
+
+do_common_sql {
+ CREATE TABLE t1(a PRIMARY KEY, b);
+ CREATE TABLE t2(c PRIMARY KEY, d);
+}
+
+# Test a blob update.
+#
+do_test 1.1 {
+ do_then_apply_tcl {
+ db eval { INSERT INTO t1 VALUES(1, 'helloworld') }
+ db eval { INSERT INTO t2 VALUES(2, 'onetwothree') }
+ }
+ compare_db db db2
+} {}
+do_test 1.2 {
+ do_then_apply_tcl {
+ set fd [db incrblob t1 b 1]
+ puts -nonewline $fd 1234567890
+ close $fd
+ }
+ compare_db db db2
+} {}
+
+# Test an attached database.
+#
+do_test 2.1 {
+ forcedelete test.db3
+ file copy test.db2 test.db3
+ execsql { ATTACH 'test.db3' AS aux; }
+
+ do_then_apply_tcl {
+ set fd [db incrblob aux t2 d 1]
+ puts -nonewline $fd fourfivesix
+ close $fd
+ } aux
+
+ sqlite3 db3 test.db3
+ compare_db db2 db3
+} {}
+
+
+db3 close
+db2 close
+
+finish_test
diff --git a/ext/session/session8.test b/ext/session/session8.test
new file mode 100644
index 000000000..93bd4e06c
--- /dev/null
+++ b/ext/session/session8.test
@@ -0,0 +1,92 @@
+# 2011 July 13
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+# This file implements regression tests for SQLite library.
+#
+
+if {![info exists testdir]} {
+ set testdir [file join [file dirname [info script]] .. .. test]
+}
+source [file join [file dirname [info script]] session_common.tcl]
+source $testdir/tester.tcl
+ifcapable !session {finish_test; return}
+
+set testprefix session8
+
+proc noop {args} {}
+
+# Like [dbcksum] in tester.tcl. Except this version is not sensitive
+# to changes in the value of implicit IPK columns.
+#
+proc udbcksum {db dbname} {
+ if {$dbname=="temp"} {
+ set master sqlite_temp_master
+ } else {
+ set master $dbname.sqlite_master
+ }
+ set alltab [$db eval "SELECT name FROM $master WHERE type='table'"]
+ set txt [$db eval "SELECT * FROM $master"]\n
+ foreach tab $alltab {
+ append txt [lsort [$db eval "SELECT * FROM $dbname.$tab"]]\n
+ }
+ return [md5 $txt]
+}
+
+proc do_then_undo {tn sql} {
+ set ck1 [udbcksum db main]
+
+ sqlite3session S db main
+ S attach *
+ db eval $sql
+
+ set ck2 [udbcksum db main]
+
+ set invert [sqlite3changeset_invert [S changeset]]
+ S delete
+ sqlite3changeset_apply db $invert noop
+
+ set ck3 [udbcksum db main]
+
+ set a [expr {$ck1==$ck2}]
+ set b [expr {$ck1==$ck3}]
+ uplevel [list do_test $tn.1 "set {} $a" 0]
+ uplevel [list do_test $tn.2 "set {} $b" 1]
+}
+
+do_execsql_test 1.1 {
+ CREATE TABLE t1(a PRIMARY KEY, b);
+ INSERT INTO t1 VALUES(1, 2);
+ INSERT INTO t1 VALUES("abc", "xyz");
+}
+do_then_undo 1.2 { INSERT INTO t1 VALUES(3, 4); }
+do_then_undo 1.3 { DELETE FROM t1 WHERE b=2; }
+do_then_undo 1.4 { UPDATE t1 SET b = 3 WHERE a = 1; }
+
+do_execsql_test 2.1 {
+ CREATE TABLE t2(a, b PRIMARY KEY);
+ INSERT INTO t2 VALUES(1, 2);
+ INSERT INTO t2 VALUES('abc', 'xyz');
+}
+do_then_undo 1.2 { INSERT INTO t2 VALUES(3, 4); }
+do_then_undo 1.3 { DELETE FROM t2 WHERE b=2; }
+do_then_undo 1.4 { UPDATE t1 SET a = '123' WHERE b = 'xyz'; }
+
+do_execsql_test 3.1 {
+ CREATE TABLE t3(a, b, c, d, e, PRIMARY KEY(c, e));
+ INSERT INTO t3 VALUES('x', 45, 0.0, 'abcdef', 12);
+ INSERT INTO t3 VALUES(45, 0.0, 'abcdef', 12, 'x');
+ INSERT INTO t3 VALUES(0.0, 'abcdef', 12, 'x', 45);
+}
+
+do_then_undo 3.2 { UPDATE t3 SET b=b||b WHERE e!='x' }
+do_then_undo 3.3 { UPDATE t3 SET a = 46 }
+
+finish_test
+
diff --git a/ext/session/session9.test b/ext/session/session9.test
new file mode 100644
index 000000000..3b42c3106
--- /dev/null
+++ b/ext/session/session9.test
@@ -0,0 +1,254 @@
+# 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 {}}
+
+finish_test
diff --git a/ext/session/sessionA.test b/ext/session/sessionA.test
new file mode 100644
index 000000000..1ca0f1370
--- /dev/null
+++ b/ext/session/sessionA.test
@@ -0,0 +1,69 @@
+# 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 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}
+
+
+finish_test
+
+
diff --git a/ext/session/session_common.tcl b/ext/session/session_common.tcl
new file mode 100644
index 000000000..53870c402
--- /dev/null
+++ b/ext/session/session_common.tcl
@@ -0,0 +1,136 @@
+
+
+proc do_conflict_test {tn args} {
+ proc xConflict {args} {
+ lappend ::xConflict $args
+ return ""
+ }
+ proc bgerror {args} { set ::background_error $args }
+
+
+ set O(-tables) [list]
+ set O(-sql) [list]
+ set O(-conflicts) [list]
+
+ array set V $args
+ foreach key [array names V] {
+ if {![info exists O($key)]} {error "no such option: $key"}
+ }
+ array set O $args
+
+ sqlite3session S db main
+ foreach t $O(-tables) { S attach $t }
+ execsql $O(-sql)
+
+ set ::xConflict [list]
+ sqlite3changeset_apply db2 [S changeset] xConflict
+
+ set conflicts [list]
+ foreach c $O(-conflicts) {
+ lappend conflicts $c
+ }
+
+ after 1 {set go 1}
+ vwait go
+
+ uplevel do_test $tn [list { set ::xConflict }] [list $conflicts]
+ S delete
+}
+
+proc do_common_sql {sql} {
+ execsql $sql db
+ execsql $sql db2
+}
+
+proc changeset_from_sql {sql {dbname main}} {
+ set rc [catch {
+ sqlite3session S db $dbname
+ db eval "SELECT name FROM $dbname.sqlite_master WHERE type = 'table'" {
+ S attach $name
+ }
+ db eval $sql
+ S changeset
+ } changeset]
+ catch { S delete }
+
+ if {$rc} {
+ error $changeset
+ }
+ return $changeset
+}
+
+proc do_then_apply_sql {sql {dbname main}} {
+ proc xConflict args { return "OMIT" }
+ set rc [catch {
+ sqlite3session S db $dbname
+ db eval "SELECT name FROM $dbname.sqlite_master WHERE type = 'table'" {
+ S attach $name
+ }
+ db eval $sql
+ sqlite3changeset_apply db2 [S changeset] xConflict
+ } msg]
+
+ catch { S delete }
+
+ if {$rc} {error $msg}
+}
+
+proc do_iterator_test {tn tbl_list sql res} {
+ sqlite3session S db main
+ if {[llength $tbl_list]==0} { S attach * }
+ foreach t $tbl_list {S attach $t}
+
+ execsql $sql
+
+ set r [list]
+ foreach v $res { lappend r $v }
+
+ set x [list]
+ sqlite3session_foreach c [S changeset] { lappend x $c }
+ uplevel do_test $tn [list [list set {} $x]] [list $r]
+
+ S delete
+}
+
+# Compare the contents of all tables in [db1] and [db2]. Throw an error if
+# they are not identical, or return an empty string if they are.
+#
+proc compare_db {db1 db2} {
+
+ set sql {SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name}
+ set lot1 [$db1 eval $sql]
+ set lot2 [$db2 eval $sql]
+
+ if {$lot1 != $lot2} {
+ puts $lot1
+ puts $lot2
+ error "databases contain different tables"
+ }
+
+ foreach tbl $lot1 {
+ set col1 [list]
+ set col2 [list]
+
+ $db1 eval "PRAGMA table_info = $tbl" { lappend col1 $name }
+ $db2 eval "PRAGMA table_info = $tbl" { lappend col2 $name }
+ if {$col1 != $col2} { error "table $tbl schema mismatch" }
+
+ set sql "SELECT * FROM $tbl ORDER BY [join $col1 ,]"
+ set data1 [$db1 eval $sql]
+ set data2 [$db2 eval $sql]
+ if {$data1 != $data2} {
+ puts "$data1"
+ puts "$data2"
+ error "table $tbl data mismatch"
+ }
+ }
+
+ return ""
+}
+
+proc changeset_to_list {c} {
+ set list [list]
+ sqlite3session_foreach elem $c { lappend list $elem }
+ lsort $list
+}
+
diff --git a/ext/session/sessionfault.test b/ext/session/sessionfault.test
new file mode 100644
index 000000000..f17daccfc
--- /dev/null
+++ b/ext/session/sessionfault.test
@@ -0,0 +1,443 @@
+# 2011 Mar 21
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+# The focus of this file is testing the session module.
+#
+
+if {![info exists testdir]} {
+ set testdir [file join [file dirname [info script]] .. .. test]
+}
+source [file join [file dirname [info script]] session_common.tcl]
+source $testdir/tester.tcl
+
+set testprefix sessionfault
+
+if 1 {
+
+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 -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" }
+ }
+}
+
+#-------------------------------------------------------------------------
+# Test that OOM errors in sqlite3changeset_concat() are handled correctly.
+#
+catch {db close}
+forcedelete test.db
+sqlite3 db test.db
+do_execsql_test 5.prep1 {
+ CREATE TABLE t1(a, b, PRIMARY KEY(b));
+ CREATE TABLE t2(a PRIMARY KEY, b);
+ INSERT INTO t1 VALUES('string', 1);
+ INSERT INTO t1 VALUES(4, 2);
+ INSERT INTO t1 VALUES(X'FFAAFFAAFFAA', 3);
+}
+
+do_test 6.prep2 {
+ sqlite3session M db main
+ M attach *
+ set ::c2 [changeset_from_sql {
+ INSERT INTO t2 VALUES(randomblob(1000), randomblob(1000));
+ INSERT INTO t2 VALUES('one', 'two');
+ INSERT INTO t2 VALUES(1, NULL);
+ UPDATE t1 SET a = 5 WHERE a = 2;
+ }]
+ set ::c1 [changeset_from_sql {
+ DELETE FROM t2 WHERE a = 1;
+ UPDATE t1 SET a = 4 WHERE a = 2;
+ INSERT INTO t2 VALUES('x', 'y');
+ }]
+ set ::total [changeset_to_list [M changeset]]
+ M delete
+} {}
+
+do_faultsim_test 6 -faults oom-* -body {
+ set ::result [sqlite3changeset_concat $::c1 $::c2]
+ set {} {}
+} -test {
+ faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
+ if {$testrc==0} {
+ set v [changeset_to_list $::result]
+ if {$v != $::total} { error "result no good" }
+ }
+}
+
+faultsim_delete_and_reopen
+do_execsql_test 5.prep1 {
+ CREATE TABLE t1(a, b, PRIMARY KEY(a));
+}
+faultsim_save_and_close
+
+set res [list]
+for {set ::i 0} {$::i < 480} {incr ::i 4} {
+ lappend res "INSERT t1 0 X. {} {i $::i i $::i}"
+}
+set res [lsort $res]
+do_faultsim_test 7 -faults oom-transient -prep {
+ faultsim_restore_and_reopen
+ sqlite3session S db main
+ S attach *
+} -body {
+ for {set ::i 0} {$::i < 480} {incr ::i 4} {
+ execsql {INSERT INTO t1 VALUES($::i, $::i)}
+ }
+} -test {
+ faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
+ if {$testrc==0} {
+ set cres [list [catch {changeset_to_list [S changeset]} msg] $msg]
+ S delete
+ if {$cres != "1 SQLITE_NOMEM" && $cres != "0 {$::res}"} {
+ error "Expected {0 $::res} Got {$cres}"
+ }
+ } else {
+ S changeset
+ S delete
+ }
+}
+
+faultsim_delete_and_reopen
+do_test 8.prep {
+ sqlite3session S db main
+ S attach *
+ execsql {
+ CREATE TABLE t1(a, b, PRIMARY KEY(a));
+ INSERT INTO t1 VALUES(1, 2);
+ INSERT INTO t1 VALUES(3, 4);
+ INSERT INTO t1 VALUES(5, 6);
+ }
+ set ::changeset [S changeset]
+ S delete
+} {}
+
+set expected [normalize_list {
+ {INSERT t1 0 X. {} {i 1 i 2}}
+ {INSERT t1 0 X. {} {i 3 i 4}}
+ {INSERT t1 0 X. {} {i 5 i 6}}
+}]
+do_faultsim_test 8.1 -faults oom* -body {
+ set ::res [list]
+ sqlite3session_foreach -next v $::changeset { lappend ::res $v }
+ normalize_list $::res
+} -test {
+ faultsim_test_result [list 0 $::expected] {1 SQLITE_NOMEM}
+}
+do_faultsim_test 8.2 -faults oom* -body {
+ set ::res [list]
+ sqlite3session_foreach v $::changeset { lappend ::res $v }
+ normalize_list $::res
+} -test {
+ faultsim_test_result [list 0 $::expected] {1 SQLITE_NOMEM}
+}
+
+faultsim_delete_and_reopen
+do_test 9.1.prep {
+ execsql {
+ PRAGMA encoding = 'utf16';
+ CREATE TABLE t1(a PRIMARY KEY, b);
+ }
+} {}
+faultsim_save_and_close
+
+set answers [list {0 {}} {1 SQLITE_NOMEM} \
+ {1 {callback requested query abort}} \
+ {1 {abort due to ROLLBACK}}]
+do_faultsim_test 9.1 -faults oom-transient -prep {
+ catch { unset ::c }
+ faultsim_restore_and_reopen
+ sqlite3session S db main
+ S attach *
+} -body {
+ execsql {
+ INSERT INTO t1 VALUES('abcdefghijklmnopqrstuv', 'ABCDEFGHIJKLMNOPQRSTUV');
+ }
+ set ::c [S changeset]
+ set {} {}
+} -test {
+ S delete
+ eval faultsim_test_result $::answers
+ if {[info exists ::c]} {
+ set expected [normalize_list {
+ {INSERT t1 0 X. {} {t abcdefghijklmnopqrstuv t ABCDEFGHIJKLMNOPQRSTUV}}
+ }]
+ if { [changeset_to_list $::c] != $expected } {
+ error "changeset mismatch"
+ }
+ }
+}
+
+}
+
+faultsim_delete_and_reopen
+do_test 9.2.prep {
+ execsql {
+ PRAGMA encoding = 'utf16';
+ CREATE TABLE t1(a PRIMARY KEY, b);
+ INSERT INTO t1 VALUES('abcdefghij', 'ABCDEFGHIJKLMNOPQRSTUV');
+ }
+} {}
+faultsim_save_and_close
+
+set answers [list {0 {}} {1 SQLITE_NOMEM} \
+ {1 {callback requested query abort}} \
+ {1 {abort due to ROLLBACK}}]
+do_faultsim_test 9.2 -faults oom-transient -prep {
+ catch { unset ::c }
+ faultsim_restore_and_reopen
+ sqlite3session S db main
+ S attach *
+} -body {
+ execsql {
+ UPDATE t1 SET b = 'xyz';
+ }
+ set ::c [S changeset]
+ set {} {}
+} -test {
+ S delete
+ eval faultsim_test_result $::answers
+ if {[info exists ::c]} {
+ set expected [normalize_list {
+ {UPDATE t1 0 X. {t abcdefghij t ABCDEFGHIJKLMNOPQRSTUV} {{} {} t xyz}}
+ }]
+ if { [changeset_to_list $::c] != $expected } {
+ error "changeset mismatch"
+ }
+ }
+}
+
+
+
+finish_test
diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c
new file mode 100644
index 000000000..256d5f089
--- /dev/null
+++ b/ext/session/sqlite3session.c
@@ -0,0 +1,3300 @@
+
+#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;
+
+/*
+** Session handle structure.
+*/
+struct sqlite3_session {
+ sqlite3 *db; /* Database handle session is attached to */
+ char *zDb; /* Name of database session is attached to */
+ int bEnable; /* True if currently recording */
+ int bIndirect; /* True if all changes are indirect */
+ int bAutoAttach; /* True to auto-attach tables */
+ int rc; /* Non-zero if an error has occurred */
+ void *pFilterCtx; /* First argument to pass to xTableFilter */
+ int (*xTableFilter)(void *pCtx, const char *zTab);
+ sqlite3_session *pNext; /* Next session object on same db. */
+ SessionTable *pTable; /* List of attached tables */
+};
+
+/*
+** Structure for changeset iterators.
+*/
+struct sqlite3_changeset_iter {
+ u8 *aChangeset; /* Pointer to buffer containing changeset */
+ int nChangeset; /* Number of bytes in aChangeset */
+ u8 *pNext; /* Pointer to next change within aChangeset */
+ int rc; /* Iterator error code */
+ sqlite3_stmt *pConflict; /* Points to conflicting row, if any */
+ char *zTab; /* Current table */
+ int nCol; /* Number of columns in zTab */
+ int op; /* Current operation */
+ int bIndirect; /* True if current change was indirect */
+ u8 *abPK; /* Primary key array */
+ sqlite3_value **apValue; /* old.* and new.* values */
+};
+
+/*
+** Each session object maintains a set of the following structures, one
+** for each table the session object is monitoring. The structures are
+** stored in a linked list starting at sqlite3_session.pTable.
+**
+** The keys of the SessionTable.aChange[] hash table are all rows that have
+** been modified in any way since the session object was attached to the
+** table.
+**
+** The data associated with each hash-table entry is a structure containing
+** a subset of the initial values that the modified row contained at the
+** start of the session. Or no initial values if the row was inserted.
+*/
+struct SessionTable {
+ SessionTable *pNext;
+ char *zName; /* Local name of table */
+ int nCol; /* Number of columns in table zName */
+ const char **azCol; /* Column names */
+ u8 *abPK; /* Array of primary key flags */
+ int nEntry; /* Total number of entries in hash table */
+ int nChange; /* Size of apChange[] array */
+ SessionChange **apChange; /* Hash table buckets */
+};
+
+/*
+** RECORD FORMAT:
+**
+** The following record format is similar to (but not compatible with) that
+** used in SQLite database files. This format is used as part of the
+** change-set binary format, and so must be architecture independent.
+**
+** Unlike the SQLite database record format, each field is self-contained -
+** there is no separation of header and data. Each field begins with a
+** single byte describing its type, as follows:
+**
+** 0x00: Undefined value.
+** 0x01: Integer value.
+** 0x02: Real value.
+** 0x03: Text value.
+** 0x04: Blob value.
+** 0x05: SQL NULL value.
+**
+** Note that the above match the definitions of SQLITE_INTEGER, SQLITE_TEXT
+** and so on in sqlite3.h. For undefined and NULL values, the field consists
+** only of the single type byte. For other types of values, the type byte
+** is followed by:
+**
+** Text values:
+** A varint containing the number of bytes in the value (encoded using
+** UTF-8). Followed by a buffer containing the UTF-8 representation
+** of the text value. There is no nul terminator.
+**
+** Blob values:
+** A varint containing the number of bytes in the value, followed by
+** a buffer containing the value itself.
+**
+** Integer values:
+** An 8-byte big-endian integer value.
+**
+** Real values:
+** An 8-byte big-endian IEEE 754-2008 real value.
+**
+** Varint values are encoded in the same way as varints in the SQLite
+** record format.
+**
+** CHANGESET FORMAT:
+**
+** A changeset is a collection of DELETE, UPDATE and INSERT operations on
+** one or more tables. Operations on a single table are grouped together,
+** but may occur in any order (i.e. deletes, updates and inserts are all
+** mixed together).
+**
+** Each group of changes begins with a table header:
+**
+** 1 byte: Constant 0x54 (capital 'T')
+** Varint: Big-endian integer set to the number of columns in the table.
+** N bytes: Unqualified table name (encoded using UTF-8). Nul-terminated.
+**
+** Followed by one or more changes to the table.
+**
+** 1 byte: Either SQLITE_INSERT, UPDATE or DELETE.
+** 1 byte: The "indirect-change" flag.
+** old.* record: (delete and update only)
+** new.* record: (insert and update only)
+*/
+
+/*
+** For each row modified during a session, there exists a single instance of
+** this structure stored in a SessionTable.aChange[] hash table.
+*/
+struct SessionChange {
+ int op; /* One of UPDATE, DELETE, INSERT */
+ int bIndirect; /* True if this change is "indirect" */
+ int nRecord; /* Number of bytes in buffer aRecord[] */
+ u8 *aRecord; /* Buffer containing old.* record */
+ SessionChange *pNext; /* For hash-table collisions */
+};
+
+/*
+** 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 */
+};
+
+/*
+** Write a varint with value iVal into the buffer at aBuf. Return the
+** number of bytes written.
+*/
+static int sessionVarintPut(u8 *aBuf, int iVal){
+ return putVarint32(aBuf, iVal);
+}
+
+/*
+** Return the number of bytes required to store value iVal as a varint.
+*/
+static int sessionVarintLen(int iVal){
+ return sqlite3VarintLen(iVal);
+}
+
+/*
+** Read a varint value from aBuf[] into *piVal. Return the number of
+** bytes read.
+*/
+static int sessionVarintGet(u8 *aBuf, int *piVal){
+ return getVarint32(aBuf, *piVal);
+}
+
+/*
+** Read a 64-bit big-endian integer value from buffer aRec[]. Return
+** the value read.
+*/
+static sqlite3_int64 sessionGetI64(u8 *aRec){
+ return (((sqlite3_int64)aRec[0]) << 56)
+ + (((sqlite3_int64)aRec[1]) << 48)
+ + (((sqlite3_int64)aRec[2]) << 40)
+ + (((sqlite3_int64)aRec[3]) << 32)
+ + (((sqlite3_int64)aRec[4]) << 24)
+ + (((sqlite3_int64)aRec[5]) << 16)
+ + (((sqlite3_int64)aRec[6]) << 8)
+ + (((sqlite3_int64)aRec[7]) << 0);
+}
+
+/*
+** Write a 64-bit big-endian integer value to the buffer aBuf[].
+*/
+static void sessionPutI64(u8 *aBuf, sqlite3_int64 i){
+ aBuf[0] = (i>>56) & 0xFF;
+ aBuf[1] = (i>>48) & 0xFF;
+ aBuf[2] = (i>>40) & 0xFF;
+ aBuf[3] = (i>>32) & 0xFF;
+ aBuf[4] = (i>>24) & 0xFF;
+ aBuf[5] = (i>>16) & 0xFF;
+ aBuf[6] = (i>> 8) & 0xFF;
+ aBuf[7] = (i>> 0) & 0xFF;
+}
+
+/*
+** This function is used to serialize the contents of value pValue (see
+** comment titled "RECORD FORMAT" above).
+**
+** If it is non-NULL, the serialized form of the value is written to
+** buffer aBuf. *pnWrite is set to the number of bytes written before
+** returning. Or, if aBuf is NULL, the only thing this function does is
+** set *pnWrite.
+**
+** If no error occurs, SQLITE_OK is returned. Or, if an OOM error occurs
+** within a call to sqlite3_value_text() (may fail if the db is utf-16))
+** SQLITE_NOMEM is returned.
+*/
+static int sessionSerializeValue(
+ u8 *aBuf, /* If non-NULL, write serialized value here */
+ sqlite3_value *pValue, /* Value to serialize */
+ int *pnWrite /* IN/OUT: Increment by bytes written */
+){
+ int nByte; /* Size of serialized value in bytes */
+
+ if( pValue ){
+ int eType; /* Value type (SQLITE_NULL, TEXT etc.) */
+
+ eType = sqlite3_value_type(pValue);
+ if( aBuf ) aBuf[0] = eType;
+
+ switch( eType ){
+ case SQLITE_NULL:
+ nByte = 1;
+ break;
+
+ case SQLITE_INTEGER:
+ case SQLITE_FLOAT:
+ if( aBuf ){
+ /* TODO: SQLite does something special to deal with mixed-endian
+ ** floating point values (e.g. ARM7). This code probably should
+ ** too. */
+ u64 i;
+ if( eType==SQLITE_INTEGER ){
+ i = (u64)sqlite3_value_int64(pValue);
+ }else{
+ double r;
+ assert( sizeof(double)==8 && sizeof(u64)==8 );
+ r = sqlite3_value_double(pValue);
+ memcpy(&i, &r, 8);
+ }
+ sessionPutI64(&aBuf[1], i);
+ }
+ nByte = 9;
+ break;
+
+ default: {
+ u8 *z;
+ int n;
+ int nVarint;
+
+ assert( eType==SQLITE_TEXT || eType==SQLITE_BLOB );
+ if( eType==SQLITE_TEXT ){
+ z = (u8 *)sqlite3_value_text(pValue);
+ }else{
+ z = (u8 *)sqlite3_value_blob(pValue);
+ }
+ if( z==0 ) return SQLITE_NOMEM;
+ n = sqlite3_value_bytes(pValue);
+ nVarint = sessionVarintLen(n);
+
+ if( aBuf ){
+ sessionVarintPut(&aBuf[1], n);
+ memcpy(&aBuf[nVarint + 1], eType==SQLITE_TEXT ?
+ sqlite3_value_text(pValue) : sqlite3_value_blob(pValue), n
+ );
+ }
+
+ nByte = 1 + nVarint + n;
+ break;
+ }
+ }
+ }else{
+ nByte = 1;
+ if( aBuf ) aBuf[0] = '\0';
+ }
+
+ *pnWrite += nByte;
+ return SQLITE_OK;
+}
+
+/*
+** This macro is used to calculate hash key values for data structures. In
+** order to use this macro, the entire data structure must be represented
+** as a series of unsigned integers. In order to calculate a hash-key value
+** for a data structure represented as three such integers, the macro may
+** then be used as follows:
+**
+** int hash_key_value;
+** hash_key_value = HASH_APPEND(0, <value 1>);
+** hash_key_value = HASH_APPEND(hash_key_value, <value 2>);
+** hash_key_value = HASH_APPEND(hash_key_value, <value 3>);
+**
+** In practice, the data structures this macro is used for are the primary
+** key values of modified rows.
+*/
+#define HASH_APPEND(hash, add) ((hash) << 3) ^ (hash) ^ (unsigned int)(add)
+
+/*
+** Append the hash of the 64-bit integer passed as the second argument to the
+** hash-key value passed as the first. Return the new hash-key value.
+*/
+static unsigned int sessionHashAppendI64(unsigned int h, i64 i){
+ h = HASH_APPEND(h, i & 0xFFFFFFFF);
+ return HASH_APPEND(h, (i>>32)&0xFFFFFFFF);
+}
+
+/*
+** Append the hash of the blob passed via the second and third arguments to
+** the hash-key value passed as the first. Return the new hash-key value.
+*/
+static unsigned int sessionHashAppendBlob(unsigned int h, int n, const u8 *z){
+ int i;
+ for(i=0; i<n; i++) h = HASH_APPEND(h, z[i]);
+ return h;
+}
+
+/*
+** Append the hash of the data type passed as the second argument to the
+** hash-key value passed as the first. Return the new hash-key value.
+*/
+static unsigned int sessionHashAppendType(unsigned int h, int eType){
+ return HASH_APPEND(h, eType);
+}
+
+/*
+** This function may only be called from within a pre-update callback.
+** It calculates a hash based on the primary key values of the old.* or
+** new.* row currently available and, assuming no error occurs, writes it to
+** *piHash before returning. If the primary key contains one or more NULL
+** values, *pbNullPK is set to true before returning.
+**
+** If an error occurs, an SQLite error code is returned and the final values
+** of *piHash asn *pbNullPK are undefined. Otherwise, SQLITE_OK is returned
+** and the output variables are set as described above.
+*/
+static int sessionPreupdateHash(
+ sqlite3 *db, /* Database handle */
+ SessionTable *pTab, /* Session table handle */
+ int bNew, /* True to hash the new.* PK */
+ int *piHash, /* OUT: Hash value */
+ int *pbNullPK /* OUT: True if there are NULL values in PK */
+){
+ unsigned int h = 0; /* Hash value to return */
+ int i; /* Used to iterate through columns */
+
+ assert( *pbNullPK==0 );
+ assert( pTab->nCol==sqlite3_preupdate_count(db) );
+ for(i=0; i<pTab->nCol; i++){
+ if( pTab->abPK[i] ){
+ int rc;
+ int eType;
+ sqlite3_value *pVal;
+
+ if( bNew ){
+ rc = sqlite3_preupdate_new(db, i, &pVal);
+ }else{
+ rc = sqlite3_preupdate_old(db, i, &pVal);
+ }
+ if( rc!=SQLITE_OK ) return rc;
+
+ eType = sqlite3_value_type(pVal);
+ h = sessionHashAppendType(h, eType);
+ if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){
+ i64 iVal;
+ if( eType==SQLITE_INTEGER ){
+ iVal = sqlite3_value_int64(pVal);
+ }else{
+ double rVal = sqlite3_value_double(pVal);
+ assert( sizeof(iVal)==8 && sizeof(rVal)==8 );
+ memcpy(&iVal, &rVal, 8);
+ }
+ h = sessionHashAppendI64(h, iVal);
+ }else if( eType==SQLITE_TEXT || eType==SQLITE_BLOB ){
+ const u8 *z;
+ if( eType==SQLITE_TEXT ){
+ z = (const u8 *)sqlite3_value_text(pVal);
+ }else{
+ z = (const u8 *)sqlite3_value_blob(pVal);
+ }
+ if( !z ) return SQLITE_NOMEM;
+ h = sessionHashAppendBlob(h, sqlite3_value_bytes(pVal), z);
+ }else{
+ assert( eType==SQLITE_NULL );
+ *pbNullPK = 1;
+ }
+ }
+ }
+
+ *piHash = (h % pTab->nChange);
+ return SQLITE_OK;
+}
+
+/*
+** The buffer that the argument points to contains a serialized SQL value.
+** Return the number of bytes of space occupied by the value (including
+** the type byte).
+*/
+static int sessionSerialLen(u8 *a){
+ int e = *a;
+ int n;
+ if( e==0 ) return 1;
+ if( e==SQLITE_NULL ) return 1;
+ if( e==SQLITE_INTEGER || e==SQLITE_FLOAT ) return 9;
+ return sessionVarintGet(&a[1], &n) + 1 + n;
+}
+
+/*
+** Based on the primary key values stored in change aRecord, calculate a
+** hash key. Assume the has table has nBucket buckets. The hash keys
+** calculated by this function are compatible with those calculated by
+** sessionPreupdateHash().
+*/
+static unsigned int sessionChangeHash(
+ SessionTable *pTab, /* Table handle */
+ 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];
+
+ /* 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 */
+ u8 *aLeft, /* Change record */
+ u8 *aRight /* Change record */
+){
+ u8 *a1 = aLeft; /* Cursor to iterate through aLeft */
+ u8 *a2 = aRight; /* Cursor to iterate through aRight */
+ int iCol; /* Used to iterate through table columns */
+
+ for(iCol=0; iCol<pTab->nCol; iCol++){
+ int n1 = sessionSerialLen(a1);
+ int n2 = sessionSerialLen(a2);
+
+ if( pTab->abPK[iCol] && (n1!=n2 || memcmp(a1, a2, n1)) ){
+ return 0;
+ }
+ a1 += n1;
+ a2 += n2;
+ }
+
+ return 1;
+}
+
+/*
+** Arguments aLeft and aRight both point to buffers containing change
+** records with nCol columns. This function "merges" the two records into
+** a single records which is written to the buffer at *paOut. *paOut is
+** then set to point to one byte after the last byte written before
+** returning.
+**
+** The merging of records is done as follows: For each column, if the
+** aRight record contains a value for the column, copy the value from
+** their. Otherwise, if aLeft contains a value, copy it. If neither
+** record contains a value for a given column, then neither does the
+** output record.
+*/
+static void sessionMergeRecord(
+ u8 **paOut,
+ int nCol,
+ u8 *aLeft,
+ u8 *aRight
+){
+ u8 *a1 = aLeft; /* Cursor used to iterate through aLeft */
+ u8 *a2 = aRight; /* Cursor used to iterate through aRight */
+ u8 *aOut = *paOut; /* Output cursor */
+ int iCol; /* Used to iterate from 0 to nCol */
+
+ for(iCol=0; iCol<nCol; iCol++){
+ int n1 = sessionSerialLen(a1);
+ int n2 = sessionSerialLen(a2);
+ if( *a2 ){
+ memcpy(aOut, a2, n2);
+ aOut += n2;
+ }else{
+ memcpy(aOut, a1, n1);
+ aOut += n1;
+ }
+ a1 += n1;
+ a2 += n2;
+ }
+
+ *paOut = aOut;
+}
+
+/*
+** This is a helper function used by sessionMergeUpdate().
+**
+** When this function is called, both *paOne and *paTwo point to a value
+** within a change record. Before it returns, both have been advanced so
+** as to point to the next value in the record.
+**
+** If, when this function is called, *paTwo points to a valid value (i.e.
+** *paTwo[0] is not 0x00 - the "no value" placeholder), a copy of the *paOne
+** 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.
+*/
+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 */
+ 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;
+ 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( pTab->abPK[i] || (nOld==nNew && 0==memcmp(aOld, aNew, nNew)) ){
+ *(aOut++) = '\0';
+ }else{
+ memcpy(aOut, aNew, nNew);
+ aOut += nNew;
+ }
+ }
+
+ *paOut = aOut;
+ return 1;
+}
+
+/*
+** This function is only called from within a pre-update-hook callback.
+** It determines if the current pre-update-hook change affects the same row
+** as the change stored in argument pChange. If so, it returns true. Otherwise
+** if the pre-update-hook does not affect the same row as pChange, it returns
+** false.
+*/
+static int sessionPreupdateEqual(
+ sqlite3 *db, /* Database handle */
+ SessionTable *pTab, /* Table associated with change */
+ SessionChange *pChange, /* Change to compare to */
+ int op /* Current pre-update operation */
+){
+ int iCol; /* Used to iterate through columns */
+ u8 *a = pChange->aRecord; /* Cursor used to scan change record */
+
+ assert( op==SQLITE_INSERT || op==SQLITE_UPDATE || op==SQLITE_DELETE );
+ for(iCol=0; iCol<pTab->nCol; iCol++){
+ if( !pTab->abPK[iCol] ){
+ a += sessionSerialLen(a);
+ }else{
+ sqlite3_value *pVal; /* Value returned by preupdate_new/old */
+ int rc; /* Error code from preupdate_new/old */
+ int eType = *a++; /* Type of value from change record */
+
+ /* The following calls to preupdate_new() and preupdate_old() can not
+ ** fail. This is because they cache their return values, and by the
+ ** time control flows to here they have already been called once from
+ ** within sessionPreupdateHash(). The first two asserts below verify
+ ** this (that the method has already been called). */
+ if( op==SQLITE_INSERT ){
+ assert( db->pPreUpdate->pNewUnpacked || db->pPreUpdate->aNew );
+ rc = sqlite3_preupdate_new(db, iCol, &pVal);
+ }else{
+ assert( db->pPreUpdate->pUnpacked );
+ rc = sqlite3_preupdate_old(db, iCol, &pVal);
+ }
+ assert( rc==SQLITE_OK );
+ if( sqlite3_value_type(pVal)!=eType ) return 0;
+
+ /* A SessionChange object never has a NULL value in a PK column */
+ assert( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT
+ || eType==SQLITE_BLOB || eType==SQLITE_TEXT
+ );
+
+ if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){
+ i64 iVal = sessionGetI64(a);
+ a += 8;
+ if( eType==SQLITE_INTEGER ){
+ if( sqlite3_value_int64(pVal)!=iVal ) return 0;
+ }else{
+ double rVal;
+ assert( sizeof(iVal)==8 && sizeof(rVal)==8 );
+ memcpy(&rVal, &iVal, 8);
+ if( sqlite3_value_double(pVal)!=rVal ) return 0;
+ }
+ }else{
+ int n;
+ const u8 *z;
+ a += sessionVarintGet(a, &n);
+ if( sqlite3_value_bytes(pVal)!=n ) return 0;
+ if( eType==SQLITE_TEXT ){
+ z = sqlite3_value_text(pVal);
+ }else{
+ z = sqlite3_value_blob(pVal);
+ }
+ if( memcmp(a, z, n) ) return 0;
+ a += n;
+ break;
+ }
+ }
+ }
+
+ return 1;
+}
+
+/*
+** If required, grow the hash table used to store changes on table pTab
+** (part of the session pSession). If a fatal OOM error occurs, set the
+** session object to failed and return SQLITE_ERROR. Otherwise, return
+** SQLITE_OK.
+**
+** It is possible that a non-fatal OOM error occurs in this function. In
+** that case the hash-table does not grow, but SQLITE_OK is returned anyway.
+** Growing the hash table in this case is a performance optimization only,
+** it is not required for correct operation.
+*/
+static int sessionGrowHash(SessionTable *pTab){
+ if( pTab->nChange==0 || pTab->nEntry>=(pTab->nChange/2) ){
+ int i;
+ SessionChange **apNew;
+ int nNew = (pTab->nChange ? pTab->nChange : 128) * 2;
+
+ apNew = (SessionChange **)sqlite3_malloc(sizeof(SessionChange *) * nNew);
+ if( apNew==0 ){
+ if( pTab->nChange==0 ){
+ return SQLITE_ERROR;
+ }
+ return SQLITE_OK;
+ }
+ memset(apNew, 0, sizeof(SessionChange *) * nNew);
+
+ for(i=0; i<pTab->nChange; i++){
+ SessionChange *p;
+ SessionChange *pNext;
+ for(p=pTab->apChange[i]; p; p=pNext){
+ int iHash = sessionChangeHash(pTab, p->aRecord, nNew);
+ pNext = p->pNext;
+ p->pNext = apNew[iHash];
+ apNew[iHash] = p;
+ }
+ }
+
+ sqlite3_free(pTab->apChange);
+ pTab->nChange = nNew;
+ pTab->apChange = apNew;
+ }
+
+ return SQLITE_OK;
+}
+
+/*
+** This function queries the database for the names of the columns of table
+** zThis, in schema zDb. It is expected that the table has nCol columns. If
+** not, SQLITE_SCHEMA is returned and none of the output variables are
+** populated.
+**
+** Otherwise, if they are not NULL, variable *pnCol is set to the number
+** of columns in the database table and variable *pzTab is set to point to a
+** nul-terminated copy of the table name. *pazCol (if not NULL) is set to
+** point to an array of pointers to column names. And *pabPK (again, if not
+** NULL) is set to point to an array of booleans - true if the corresponding
+** column is part of the primary key.
+**
+** For example, if the table is declared as:
+**
+** CREATE TABLE tbl1(w, x, y, z, PRIMARY KEY(w, z));
+**
+** Then the four output variables are populated as follows:
+**
+** *pnCol = 4
+** *pzTab = "tbl1"
+** *pazCol = {"w", "x", "y", "z"}
+** *pabPK = {1, 0, 0, 1}
+**
+** All returned buffers are part of the same single allocation, which must
+** be freed using sqlite3_free() by the caller. If pazCol was not NULL, then
+** pointer *pazCol should be freed to release all memory. Otherwise, pointer
+** *pabPK. It is illegal for both pazCol and pabPK to be NULL.
+*/
+static int sessionTableInfo(
+ sqlite3 *db, /* Database connection */
+ const char *zDb, /* Name of attached database (e.g. "main") */
+ const char *zThis, /* Table name */
+ int *pnCol, /* OUT: number of columns */
+ const char **pzTab, /* OUT: Copy of zThis */
+ const char ***pazCol, /* OUT: Array of column names for table */
+ u8 **pabPK /* OUT: Array of booleans - true for PK col */
+){
+ char *zPragma;
+ sqlite3_stmt *pStmt;
+ int rc;
+ int nByte;
+ int nDbCol = 0;
+ int nThis;
+ int i;
+ u8 *pAlloc;
+ char **azCol = 0;
+ u8 *abPK;
+
+ assert( pazCol && pabPK );
+
+ nThis = sqlite3Strlen30(zThis);
+ zPragma = sqlite3_mprintf("PRAGMA '%q'.table_info('%q')", zDb, zThis);
+ if( !zPragma ) return SQLITE_NOMEM;
+
+ rc = sqlite3_prepare_v2(db, zPragma, -1, &pStmt, 0);
+ sqlite3_free(zPragma);
+ if( rc!=SQLITE_OK ) return rc;
+
+ nByte = nThis + 1;
+ while( SQLITE_ROW==sqlite3_step(pStmt) ){
+ nByte += sqlite3_column_bytes(pStmt, 1);
+ nDbCol++;
+ }
+ rc = sqlite3_reset(pStmt);
+
+ if( rc==SQLITE_OK ){
+ nByte += nDbCol * (sizeof(const char *) + sizeof(u8) + 1);
+ pAlloc = sqlite3_malloc(nByte);
+ if( pAlloc==0 ){
+ rc = SQLITE_NOMEM;
+ }
+ }
+ if( rc==SQLITE_OK ){
+ azCol = (char **)pAlloc;
+ pAlloc = (u8 *)&azCol[nDbCol];
+ abPK = (u8 *)pAlloc;
+ pAlloc = &abPK[nDbCol];
+ if( pzTab ){
+ memcpy(pAlloc, zThis, nThis+1);
+ *pzTab = (char *)pAlloc;
+ pAlloc += nThis+1;
+ }
+
+ i = 0;
+ while( SQLITE_ROW==sqlite3_step(pStmt) ){
+ int nName = sqlite3_column_bytes(pStmt, 1);
+ const unsigned char *zName = sqlite3_column_text(pStmt, 1);
+ if( zName==0 ) break;
+ memcpy(pAlloc, zName, nName+1);
+ azCol[i] = (char *)pAlloc;
+ pAlloc += nName+1;
+ abPK[i] = sqlite3_column_int(pStmt, 5);
+ i++;
+ }
+ rc = sqlite3_reset(pStmt);
+
+ }
+
+ /* If successful, populate the output variables. Otherwise, zero them and
+ ** free any allocation made. An error code will be returned in this case.
+ */
+ if( rc==SQLITE_OK ){
+ *pazCol = (const char **)azCol;
+ *pabPK = abPK;
+ *pnCol = nDbCol;
+ }else{
+ *pazCol = 0;
+ *pabPK = 0;
+ *pnCol = 0;
+ if( pzTab ) *pzTab = 0;
+ sqlite3_free(azCol);
+ }
+ sqlite3_finalize(pStmt);
+ return rc;
+}
+
+/*
+** This function is only called from within a pre-update handler for a
+** write to table pTab, part of session pSession. If this is the first
+** write to this table, set the SessionTable.nCol variable to the number
+** of columns in the table.
+**
+** Otherwise, if this is not the first time this table has been written
+** to, check that the number of columns in the table has not changed. If
+** it has not, return zero.
+**
+** If the number of columns in the table has changed since the last write
+** was recorded, set the session error-code to SQLITE_SCHEMA and return
+** non-zero. Users are not allowed to change the number of columns in a table
+** for which changes are being recorded by the session module. If they do so,
+** it is an error.
+*/
+static int sessionInitTable(sqlite3_session *pSession, SessionTable *pTab){
+ if( pTab->nCol==0 ){
+ assert( pTab->azCol==0 || pTab->abPK==0 );
+ pSession->rc = sessionTableInfo(pSession->db, pSession->zDb,
+ pTab->zName, &pTab->nCol, 0, &pTab->azCol, &pTab->abPK
+ );
+ }
+ if( pSession->rc==SQLITE_OK
+ && pTab->nCol!=sqlite3_preupdate_count(pSession->db)
+ ){
+ pSession->rc = SQLITE_SCHEMA;
+ }
+ return pSession->rc;
+}
+
+/*
+** This function is only called from with a pre-update-hook reporting a
+** change on table pTab (attached to session pSession). The type of change
+** (UPDATE, INSERT, DELETE) is specified by the first argument.
+**
+** Unless one is already present or an error occurs, an entry is added
+** to the changed-rows hash table associated with table pTab.
+*/
+static void sessionPreupdateOneChange(
+ int op, /* One of SQLITE_UPDATE, INSERT, DELETE */
+ sqlite3_session *pSession, /* Session object pTab is attached to */
+ SessionTable *pTab /* Table that change applies to */
+){
+ sqlite3 *db = pSession->db;
+ int iHash;
+ int bNullPk = 0;
+ int rc = SQLITE_OK;
+
+ if( pSession->rc ) return;
+
+ /* Load table details if required */
+ if( sessionInitTable(pSession, pTab) ) return;
+
+ /* Grow the hash table if required */
+ if( sessionGrowHash(pTab) ){
+ pSession->rc = SQLITE_NOMEM;
+ return;
+ }
+
+ /* Calculate the hash-key for this change. If the primary key of the row
+ ** includes a NULL value, exit early. Such changes are ignored by the
+ ** session module. */
+ rc = sessionPreupdateHash(db, pTab, op==SQLITE_INSERT, &iHash, &bNullPk);
+ if( rc!=SQLITE_OK ) goto error_out;
+
+ if( bNullPk==0 ){
+ /* Search the hash table for an existing record for this row. */
+ SessionChange *pC;
+ for(pC=pTab->apChange[iHash]; pC; pC=pC->pNext){
+ if( sessionPreupdateEqual(db, pTab, pC, op) ) break;
+ }
+
+ if( pC==0 ){
+ /* Create a new change object containing all the old values (if
+ ** this is an SQLITE_UPDATE or SQLITE_DELETE), or just the PK
+ ** values (if this is an INSERT). */
+ SessionChange *pChange; /* New change object */
+ int nByte; /* Number of bytes to allocate */
+ int i; /* Used to iterate through columns */
+
+ assert( rc==SQLITE_OK );
+ pTab->nEntry++;
+
+ /* Figure out how large an allocation is required */
+ nByte = sizeof(SessionChange);
+ for(i=0; i<pTab->nCol; i++){
+ sqlite3_value *p = 0;
+ if( op!=SQLITE_INSERT ){
+ TESTONLY(int trc = ) sqlite3_preupdate_old(pSession->db, i, &p);
+ assert( trc==SQLITE_OK );
+ }else if( pTab->abPK[i] ){
+ TESTONLY(int trc = ) sqlite3_preupdate_new(pSession->db, i, &p);
+ assert( trc==SQLITE_OK );
+ }
+
+ /* This may fail if SQLite value p contains a utf-16 string that must
+ ** be converted to utf-8 and an OOM error occurs while doing so. */
+ rc = sessionSerializeValue(0, p, &nByte);
+ if( rc!=SQLITE_OK ) goto error_out;
+ }
+
+ /* Allocate the change object */
+ pChange = (SessionChange *)sqlite3_malloc(nByte);
+ if( !pChange ){
+ rc = SQLITE_NOMEM;
+ goto error_out;
+ }else{
+ memset(pChange, 0, sizeof(SessionChange));
+ pChange->aRecord = (u8 *)&pChange[1];
+ }
+
+ /* Populate the change object. None of the preupdate_old(),
+ ** preupdate_new() or SerializeValue() calls below may fail as all
+ ** required values and encodings have already been cached in memory.
+ ** It is not possible for an OOM to occur in this block. */
+ nByte = 0;
+ for(i=0; i<pTab->nCol; i++){
+ sqlite3_value *p = 0;
+ if( op!=SQLITE_INSERT ){
+ sqlite3_preupdate_old(pSession->db, i, &p);
+ }else if( pTab->abPK[i] ){
+ sqlite3_preupdate_new(pSession->db, i, &p);
+ }
+ sessionSerializeValue(&pChange->aRecord[nByte], p, &nByte);
+ }
+
+ /* Add the change to the hash-table */
+ if( pSession->bIndirect || sqlite3_preupdate_depth(pSession->db) ){
+ pChange->bIndirect = 1;
+ }
+ pChange->nRecord = nByte;
+ pChange->op = op;
+ pChange->pNext = pTab->apChange[iHash];
+ pTab->apChange[iHash] = pChange;
+
+ }else if( pC->bIndirect ){
+ /* If the existing change is considered "indirect", but this current
+ ** change is "direct", mark the change object as direct. */
+ if( sqlite3_preupdate_depth(pSession->db)==0 && pSession->bIndirect==0 ){
+ pC->bIndirect = 0;
+ }
+ }
+ }
+
+ /* If an error has occurred, mark the session object as failed. */
+ error_out:
+ if( rc!=SQLITE_OK ){
+ pSession->rc = rc;
+ }
+}
+
+/*
+** The 'pre-update' hook registered by this module with SQLite databases.
+*/
+static void xPreUpdate(
+ void *pCtx, /* Copy of third arg to preupdate_hook() */
+ sqlite3 *db, /* Database handle */
+ int op, /* SQLITE_UPDATE, DELETE or INSERT */
+ char const *zDb, /* Database name */
+ char const *zName, /* Table name */
+ sqlite3_int64 iKey1, /* Rowid of row about to be deleted/updated */
+ sqlite3_int64 iKey2 /* New rowid value (for a rowid UPDATE) */
+){
+ sqlite3_session *pSession;
+ int nDb = sqlite3Strlen30(zDb);
+ int nName = sqlite3Strlen30(zName);
+
+ assert( sqlite3_mutex_held(db->mutex) );
+
+ for(pSession=(sqlite3_session *)pCtx; pSession; pSession=pSession->pNext){
+ SessionTable *pTab;
+
+ /* If this session is attached to a different database ("main", "temp"
+ ** etc.), or if it is not currently enabled, there is nothing to do. Skip
+ ** to the next session object attached to this database. */
+ if( pSession->bEnable==0 ) continue;
+ if( pSession->rc ) continue;
+ if( sqlite3_strnicmp(zDb, pSession->zDb, nDb+1) ) continue;
+
+ for(pTab=pSession->pTable; pTab || pSession->bAutoAttach; pTab=pTab->pNext){
+ if( !pTab ){
+ /* This branch is taken if table zName has not yet been attached to
+ ** this session and the auto-attach flag is set. */
+
+ /* If there is a table-filter configured, invoke it. If it returns 0,
+ ** this change will not be recorded. Break out of the loop early in
+ ** this case. */
+ if( pSession->xTableFilter
+ && pSession->xTableFilter(pSession->pFilterCtx, zName)==0
+ ){
+ break;
+ }
+
+ pSession->rc = sqlite3session_attach(pSession,zName);
+ if( pSession->rc ) break;
+ pTab = pSession->pTable;
+ assert( 0==sqlite3_strnicmp(pTab->zName, zName, nName+1) );
+ }
+
+ if( 0==sqlite3_strnicmp(pTab->zName, zName, nName+1) ){
+ sessionPreupdateOneChange(op, pSession, pTab);
+ if( op==SQLITE_UPDATE ){
+ sessionPreupdateOneChange(SQLITE_INSERT, pSession, pTab);
+ }
+ break;
+ }
+ }
+ }
+}
+
+/*
+** Create a session object. This session object will record changes to
+** database zDb attached to connection db.
+*/
+int sqlite3session_create(
+ sqlite3 *db, /* Database handle */
+ const char *zDb, /* Name of db (e.g. "main") */
+ sqlite3_session **ppSession /* OUT: New session object */
+){
+ sqlite3_session *pNew; /* Newly allocated session object */
+ sqlite3_session *pOld; /* Session object already attached to db */
+ int nDb = sqlite3Strlen30(zDb); /* Length of zDb in bytes */
+
+ /* Zero the output value in case an error occurs. */
+ *ppSession = 0;
+
+ /* Allocate and populate the new session object. */
+ pNew = (sqlite3_session *)sqlite3_malloc(sizeof(sqlite3_session) + nDb + 1);
+ if( !pNew ) return SQLITE_NOMEM;
+ memset(pNew, 0, sizeof(sqlite3_session));
+ pNew->db = db;
+ pNew->zDb = (char *)&pNew[1];
+ pNew->bEnable = 1;
+ memcpy(pNew->zDb, zDb, nDb+1);
+
+ /* Add the new session object to the linked list of session objects
+ ** attached to database handle $db. Do this under the cover of the db
+ ** handle mutex. */
+ sqlite3_mutex_enter(sqlite3_db_mutex(db));
+ pOld = (sqlite3_session*)sqlite3_preupdate_hook(db, xPreUpdate, (void*)pNew);
+ pNew->pNext = pOld;
+ sqlite3_mutex_leave(sqlite3_db_mutex(db));
+
+ *ppSession = pNew;
+ return SQLITE_OK;
+}
+
+/*
+** Free the list of table objects passed as the first argument. The contents
+** of the changed-rows hash tables are also deleted.
+*/
+static void sessionDeleteTable(SessionTable *pList){
+ SessionTable *pNext;
+ SessionTable *pTab;
+
+ for(pTab=pList; pTab; pTab=pNext){
+ int i;
+ pNext = pTab->pNext;
+ for(i=0; i<pTab->nChange; i++){
+ SessionChange *p;
+ SessionChange *pNext;
+ for(p=pTab->apChange[i]; p; p=pNext){
+ pNext = p->pNext;
+ sqlite3_free(p);
+ }
+ }
+ sqlite3_free((char*)pTab->azCol); /* cast works around VC++ bug */
+ sqlite3_free(pTab->apChange);
+ sqlite3_free(pTab);
+ }
+}
+
+/*
+** Delete a session object previously allocated using sqlite3session_create().
+*/
+void sqlite3session_delete(sqlite3_session *pSession){
+ sqlite3 *db = pSession->db;
+ sqlite3_session *pHead;
+ sqlite3_session **pp;
+
+ /* Unlink the session from the linked list of sessions attached to the
+ ** database handle. Hold the db mutex while doing so. */
+ sqlite3_mutex_enter(sqlite3_db_mutex(db));
+ pHead = (sqlite3_session*)sqlite3_preupdate_hook(db, 0, 0);
+ for(pp=&pHead; (*pp)!=pSession; pp=&((*pp)->pNext));
+ *pp = (*pp)->pNext;
+ if( pHead ) sqlite3_preupdate_hook(db, xPreUpdate, (void *)pHead);
+ sqlite3_mutex_leave(sqlite3_db_mutex(db));
+
+ /* Delete all attached table objects. And the contents of their
+ ** associated hash-tables. */
+ sessionDeleteTable(pSession->pTable);
+
+ /* Free the session object itself. */
+ sqlite3_free(pSession);
+}
+
+/*
+** Set a table filter on a Session Object.
+*/
+void sqlite3session_table_filter(
+ sqlite3_session *pSession,
+ int(*xFilter)(void*, const char*),
+ void *pCtx /* First argument passed to xFilter */
+){
+ pSession->bAutoAttach = 1;
+ pSession->pFilterCtx = pCtx;
+ pSession->xTableFilter = xFilter;
+}
+
+/*
+** Attach a table to a session. All subsequent changes made to the table
+** while the session object is enabled will be recorded.
+**
+** Only tables that have a PRIMARY KEY defined may be attached. It does
+** not matter if the PRIMARY KEY is an "INTEGER PRIMARY KEY" (rowid alias)
+** or not.
+*/
+int sqlite3session_attach(
+ sqlite3_session *pSession, /* Session object */
+ const char *zName /* Table name */
+){
+ int rc = SQLITE_OK;
+ sqlite3_mutex_enter(sqlite3_db_mutex(pSession->db));
+
+ if( !zName ){
+ pSession->bAutoAttach = 1;
+ }else{
+ SessionTable *pTab; /* New table object (if required) */
+ int nName; /* Number of bytes in string zName */
+
+ /* First search for an existing entry. If one is found, this call is
+ ** a no-op. Return early. */
+ nName = sqlite3Strlen30(zName);
+ for(pTab=pSession->pTable; pTab; pTab=pTab->pNext){
+ if( 0==sqlite3_strnicmp(pTab->zName, zName, nName+1) ) break;
+ }
+
+ if( !pTab ){
+ /* Allocate new SessionTable object. */
+ pTab = (SessionTable *)sqlite3_malloc(sizeof(SessionTable) + nName + 1);
+ if( !pTab ){
+ rc = SQLITE_NOMEM;
+ }else{
+ /* Populate the new SessionTable object and link it into the list. */
+ memset(pTab, 0, sizeof(SessionTable));
+ pTab->zName = (char *)&pTab[1];
+ memcpy(pTab->zName, zName, nName+1);
+ pTab->pNext = pSession->pTable;
+ pSession->pTable = pTab;
+ }
+ }
+ }
+
+ sqlite3_mutex_leave(sqlite3_db_mutex(pSession->db));
+ return rc;
+}
+
+/*
+** Ensure that there is room in the buffer to append nByte bytes of data.
+** If not, use sqlite3_realloc() to grow the buffer so that there is.
+**
+** If successful, return zero. Otherwise, if an OOM condition is encountered,
+** set *pRc to SQLITE_NOMEM and return non-zero.
+*/
+static int sessionBufferGrow(SessionBuffer *p, int nByte, int *pRc){
+ if( *pRc==SQLITE_OK && p->nAlloc-p->nBuf<nByte ){
+ u8 *aNew;
+ int nNew = p->nAlloc ? p->nAlloc : 128;
+ do {
+ nNew = nNew*2;
+ }while( nNew<(p->nAlloc+nByte) );
+
+ aNew = (u8 *)sqlite3_realloc(p->aBuf, nNew);
+ if( 0==aNew ){
+ *pRc = SQLITE_NOMEM;
+ }else{
+ p->aBuf = aNew;
+ p->nAlloc = nNew;
+ }
+ }
+ return (*pRc!=SQLITE_OK);
+}
+
+/*
+** This function is a no-op if *pRc is other than SQLITE_OK when it is
+** called. Otherwise, append a single byte to the buffer.
+**
+** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before
+** returning.
+*/
+static void sessionAppendByte(SessionBuffer *p, u8 v, int *pRc){
+ if( 0==sessionBufferGrow(p, 1, pRc) ){
+ p->aBuf[p->nBuf++] = v;
+ }
+}
+
+/*
+** This function is a no-op if *pRc is other than SQLITE_OK when it is
+** called. Otherwise, append a single varint to the buffer.
+**
+** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before
+** returning.
+*/
+static void sessionAppendVarint(SessionBuffer *p, int v, int *pRc){
+ if( 0==sessionBufferGrow(p, 9, pRc) ){
+ p->nBuf += sessionVarintPut(&p->aBuf[p->nBuf], v);
+ }
+}
+
+/*
+** This function is a no-op if *pRc is other than SQLITE_OK when it is
+** called. Otherwise, append a blob of data to the buffer.
+**
+** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before
+** returning.
+*/
+static void sessionAppendBlob(
+ SessionBuffer *p,
+ const u8 *aBlob,
+ int nBlob,
+ int *pRc
+){
+ if( 0==sessionBufferGrow(p, nBlob, pRc) ){
+ memcpy(&p->aBuf[p->nBuf], aBlob, nBlob);
+ p->nBuf += nBlob;
+ }
+}
+
+/*
+** This function is a no-op if *pRc is other than SQLITE_OK when it is
+** called. Otherwise, append a string to the buffer. All bytes in the string
+** up to (but not including) the nul-terminator are written to the buffer.
+**
+** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before
+** returning.
+*/
+static void sessionAppendStr(
+ SessionBuffer *p,
+ const char *zStr,
+ int *pRc
+){
+ int nStr = sqlite3Strlen30(zStr);
+ if( 0==sessionBufferGrow(p, nStr, pRc) ){
+ memcpy(&p->aBuf[p->nBuf], zStr, nStr);
+ p->nBuf += nStr;
+ }
+}
+
+/*
+** This function is a no-op if *pRc is other than SQLITE_OK when it is
+** called. Otherwise, append the string representation of integer iVal
+** to the buffer. No nul-terminator is written.
+**
+** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before
+** returning.
+*/
+static void sessionAppendInteger(
+ SessionBuffer *p, /* Buffer to append to */
+ int iVal, /* Value to write the string rep. of */
+ int *pRc /* IN/OUT: Error code */
+){
+ char aBuf[24];
+ sqlite3_snprintf(sizeof(aBuf)-1, aBuf, "%d", iVal);
+ sessionAppendStr(p, aBuf, pRc);
+}
+
+/*
+** This function is a no-op if *pRc is other than SQLITE_OK when it is
+** called. Otherwise, append the string zStr enclosed in quotes (") and
+** with any embedded quote characters escaped to the buffer. No
+** nul-terminator byte is written.
+**
+** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before
+** returning.
+*/
+static void sessionAppendIdent(
+ SessionBuffer *p, /* Buffer to a append to */
+ const char *zStr, /* String to quote, escape and append */
+ int *pRc /* IN/OUT: Error code */
+){
+ int nStr = sqlite3Strlen30(zStr)*2 + 2 + 1;
+ if( 0==sessionBufferGrow(p, nStr, pRc) ){
+ char *zOut = (char *)&p->aBuf[p->nBuf];
+ const char *zIn = zStr;
+ *zOut++ = '"';
+ while( *zIn ){
+ if( *zIn=='"' ) *zOut++ = '"';
+ *zOut++ = *(zIn++);
+ }
+ *zOut++ = '"';
+ p->nBuf = (int)((u8 *)zOut - p->aBuf);
+ }
+}
+
+/*
+** This function is a no-op if *pRc is other than SQLITE_OK when it is
+** called. Otherwse, it appends the serialized version of the value stored
+** in column iCol of the row that SQL statement pStmt currently points
+** to to the buffer.
+*/
+static void sessionAppendCol(
+ SessionBuffer *p, /* Buffer to append to */
+ sqlite3_stmt *pStmt, /* Handle pointing to row containing value */
+ int iCol, /* Column to read value from */
+ int *pRc /* IN/OUT: Error code */
+){
+ if( *pRc==SQLITE_OK ){
+ int eType = sqlite3_column_type(pStmt, iCol);
+ sessionAppendByte(p, (u8)eType, pRc);
+ if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){
+ sqlite3_int64 i;
+ u8 aBuf[8];
+ if( eType==SQLITE_INTEGER ){
+ i = sqlite3_column_int64(pStmt, iCol);
+ }else{
+ double r = sqlite3_column_double(pStmt, iCol);
+ memcpy(&i, &r, 8);
+ }
+ sessionPutI64(aBuf, i);
+ sessionAppendBlob(p, aBuf, 8, pRc);
+ }
+ if( eType==SQLITE_BLOB || eType==SQLITE_TEXT ){
+ u8 *z;
+ if( eType==SQLITE_BLOB ){
+ z = (u8 *)sqlite3_column_blob(pStmt, iCol);
+ }else{
+ z = (u8 *)sqlite3_column_text(pStmt, iCol);
+ }
+ if( z ){
+ int nByte = sqlite3_column_bytes(pStmt, iCol);
+ sessionAppendVarint(p, nByte, pRc);
+ sessionAppendBlob(p, z, nByte, pRc);
+ }else{
+ *pRc = SQLITE_NOMEM;
+ }
+ }
+ }
+}
+
+/*
+**
+** This function appends an update change to the buffer (see the comments
+** under "CHANGESET FORMAT" at the top of the file). An update change
+** consists of:
+**
+** 1 byte: SQLITE_UPDATE (0x17)
+** n bytes: old.* record (see RECORD FORMAT)
+** m bytes: new.* record (see RECORD FORMAT)
+**
+** The SessionChange object passed as the third argument contains the
+** values that were stored in the row when the session began (the old.*
+** values). The statement handle passed as the second argument points
+** at the current version of the row (the new.* values).
+**
+** If all of the old.* values are equal to their corresponding new.* value
+** (i.e. nothing has changed), then no data at all is appended to the buffer.
+**
+** Otherwise, the old.* record contains all primary key values and the
+** original values of any fields that have been modified. The new.* record
+** contains the new values of only those fields that have been modified.
+*/
+static int sessionAppendUpdate(
+ SessionBuffer *pBuf, /* Buffer to append to */
+ sqlite3_stmt *pStmt, /* Statement handle pointing at new row */
+ SessionChange *p, /* Object containing old values */
+ u8 *abPK /* Boolean array - true for PK columns */
+){
+ int rc = SQLITE_OK;
+ SessionBuffer buf2 = {0,0,0}; /* Buffer to accumulate new.* record in */
+ int bNoop = 1; /* Set to zero if any values are modified */
+ int nRewind = pBuf->nBuf; /* Set to zero if any values are modified */
+ int i; /* Used to iterate through columns */
+ u8 *pCsr = p->aRecord; /* Used to iterate through old.* values */
+
+ sessionAppendByte(pBuf, SQLITE_UPDATE, &rc);
+ sessionAppendByte(pBuf, p->bIndirect, &rc);
+ for(i=0; i<sqlite3_column_count(pStmt); i++){
+ int bChanged = 0;
+ int nAdvance;
+ int eType = *pCsr;
+ switch( eType ){
+ case SQLITE_NULL:
+ nAdvance = 1;
+ if( sqlite3_column_type(pStmt, i)!=SQLITE_NULL ){
+ bChanged = 1;
+ }
+ break;
+
+ case SQLITE_FLOAT:
+ case SQLITE_INTEGER: {
+ nAdvance = 9;
+ if( eType==sqlite3_column_type(pStmt, i) ){
+ sqlite3_int64 iVal = sessionGetI64(&pCsr[1]);
+ if( eType==SQLITE_INTEGER ){
+ if( iVal==sqlite3_column_int64(pStmt, i) ) break;
+ }else{
+ double dVal;
+ memcpy(&dVal, &iVal, 8);
+ if( dVal==sqlite3_column_double(pStmt, i) ) break;
+ }
+ }
+ bChanged = 1;
+ break;
+ }
+
+ default: {
+ int nByte;
+ int nHdr = 1 + sessionVarintGet(&pCsr[1], &nByte);
+ assert( eType==SQLITE_TEXT || eType==SQLITE_BLOB );
+ nAdvance = nHdr + nByte;
+ if( eType==sqlite3_column_type(pStmt, i)
+ && nByte==sqlite3_column_bytes(pStmt, i)
+ && 0==memcmp(&pCsr[nHdr], sqlite3_column_blob(pStmt, i), nByte)
+ ){
+ break;
+ }
+ bChanged = 1;
+ }
+ }
+
+ if( bChanged || abPK[i] ){
+ sessionAppendBlob(pBuf, pCsr, nAdvance, &rc);
+ }else{
+ sessionAppendByte(pBuf, 0, &rc);
+ }
+
+ if( bChanged ){
+ sessionAppendCol(&buf2, pStmt, i, &rc);
+ bNoop = 0;
+ }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;
+}
+
+/*
+** Formulate and prepare a SELECT statement to retrieve a row from table
+** zTab in database zDb based on its primary key. i.e.
+**
+** SELECT * FROM zDb.zTab WHERE pk1 = ? AND pk2 = ? AND ...
+*/
+static int sessionSelectStmt(
+ sqlite3 *db, /* Database handle */
+ const char *zDb, /* Database name */
+ const char *zTab, /* Table name */
+ int nCol, /* Number of columns in table */
+ const char **azCol, /* Names of table columns */
+ u8 *abPK, /* PRIMARY KEY array */
+ sqlite3_stmt **ppStmt /* OUT: Prepared SELECT statement */
+){
+ int rc = SQLITE_OK;
+ int i;
+ const char *zSep = "";
+ SessionBuffer buf = {0, 0, 0};
+
+ sessionAppendStr(&buf, "SELECT * FROM ", &rc);
+ sessionAppendIdent(&buf, zDb, &rc);
+ sessionAppendStr(&buf, ".", &rc);
+ sessionAppendIdent(&buf, zTab, &rc);
+ sessionAppendStr(&buf, " WHERE ", &rc);
+ for(i=0; i<nCol; i++){
+ if( abPK[i] ){
+ sessionAppendStr(&buf, zSep, &rc);
+ sessionAppendIdent(&buf, azCol[i], &rc);
+ sessionAppendStr(&buf, " = ?", &rc);
+ sessionAppendInteger(&buf, i+1, &rc);
+ zSep = " AND ";
+ }
+ }
+ if( rc==SQLITE_OK ){
+ rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, ppStmt, 0);
+ }
+ sqlite3_free(buf.aBuf);
+ return rc;
+}
+
+/*
+** Bind the PRIMARY KEY values from the change passed in argument pChange
+** to the SELECT statement passed as the first argument. The SELECT statement
+** is as prepared by function sessionSelectStmt().
+**
+** Return SQLITE_OK if all PK values are successfully bound, or an SQLite
+** error code (e.g. SQLITE_NOMEM) otherwise.
+*/
+static int sessionSelectBind(
+ sqlite3_stmt *pSelect, /* SELECT from sessionSelectStmt() */
+ int nCol, /* Number of columns in table */
+ u8 *abPK, /* PRIMARY KEY array */
+ SessionChange *pChange /* Change structure */
+){
+ int i;
+ int rc = SQLITE_OK;
+ u8 *a = pChange->aRecord;
+
+ for(i=0; i<nCol && rc==SQLITE_OK; i++){
+ int eType = *a++;
+
+ switch( eType ){
+ case 0:
+ case SQLITE_NULL:
+ assert( abPK[i]==0 );
+ break;
+
+ case SQLITE_INTEGER: {
+ if( abPK[i] ){
+ i64 iVal = sessionGetI64(a);
+ rc = sqlite3_bind_int64(pSelect, i+1, iVal);
+ }
+ a += 8;
+ break;
+ }
+
+ case SQLITE_FLOAT: {
+ if( abPK[i] ){
+ double rVal;
+ i64 iVal = sessionGetI64(a);
+ memcpy(&rVal, &iVal, 8);
+ rc = sqlite3_bind_double(pSelect, i+1, rVal);
+ }
+ a += 8;
+ break;
+ }
+
+ case SQLITE_TEXT: {
+ int n;
+ a += sessionVarintGet(a, &n);
+ if( abPK[i] ){
+ rc = sqlite3_bind_text(pSelect, i+1, (char *)a, n, SQLITE_TRANSIENT);
+ }
+ a += n;
+ break;
+ }
+
+ default: {
+ int n;
+ assert( eType==SQLITE_BLOB );
+ a += sessionVarintGet(a, &n);
+ if( abPK[i] ){
+ rc = sqlite3_bind_blob(pSelect, i+1, a, n, SQLITE_TRANSIENT);
+ }
+ a += n;
+ break;
+ }
+ }
+ }
+
+ return rc;
+}
+
+/*
+** This function is a no-op if *pRc is set to other than SQLITE_OK when it
+** is called. Otherwise, append a serialized table header (part of the binary
+** changeset format) to buffer *pBuf. If an error occurs, set *pRc to an
+** SQLite error code before returning.
+*/
+static void sessionAppendTableHdr(
+ SessionBuffer *pBuf,
+ SessionTable *pTab,
+ int *pRc
+){
+ /* Write a table header */
+ sessionAppendByte(pBuf, '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);
+}
+
+/*
+** 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 */
+){
+ 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 */
+
+ /* 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. */
+ *pnChangeset = 0;
+ *ppChangeset = 0;
+
+ if( pSession->rc ) return pSession->rc;
+ rc = sqlite3_exec(pSession->db, "SAVEPOINT changeset", 0, 0, 0);
+ if( rc!=SQLITE_OK ) return rc;
+
+ sqlite3_mutex_enter(sqlite3_db_mutex(db));
+
+ for(pTab=pSession->pTable; rc==SQLITE_OK && pTab; pTab=pTab->pNext){
+ if( pTab->nEntry ){
+ const char *zName = pTab->zName;
+ int nCol; /* Number of columns in table */
+ u8 *abPK; /* Primary key array */
+ const char **azCol = 0; /* Table columns */
+ int i; /* Used to iterate through hash buckets */
+ sqlite3_stmt *pSel = 0; /* SELECT statement to query table pTab */
+ int nRewind = buf.nBuf; /* Initial size of write buffer */
+ int nNoop; /* Size of buffer after writing tbl header */
+
+ /* Check the table schema is still Ok. */
+ rc = sessionTableInfo(db, pSession->zDb, zName, &nCol, 0, &azCol, &abPK);
+ if( !rc && (pTab->nCol!=nCol || memcmp(abPK, pTab->abPK, nCol)) ){
+ rc = SQLITE_SCHEMA;
+ }
+
+ /* Write a table header */
+ sessionAppendTableHdr(&buf, pTab, &rc);
+
+ /* Build and compile a statement to execute: */
+ if( rc==SQLITE_OK ){
+ rc = sessionSelectStmt(
+ db, pSession->zDb, zName, nCol, azCol, abPK, &pSel);
+ }
+
+ nNoop = buf.nBuf;
+ for(i=0; i<pTab->nChange && rc==SQLITE_OK; i++){
+ SessionChange *p; /* Used to iterate through changes */
+
+ for(p=pTab->apChange[i]; rc==SQLITE_OK && p; p=p->pNext){
+ rc = sessionSelectBind(pSel, nCol, abPK, p);
+ if( rc!=SQLITE_OK ) continue;
+ if( sqlite3_step(pSel)==SQLITE_ROW ){
+ if( p->op==SQLITE_INSERT ){
+ int iCol;
+ sessionAppendByte(&buf, SQLITE_INSERT, &rc);
+ sessionAppendByte(&buf, p->bIndirect, &rc);
+ for(iCol=0; iCol<nCol; iCol++){
+ sessionAppendCol(&buf, pSel, iCol, &rc);
+ }
+ }else{
+ rc = sessionAppendUpdate(&buf, pSel, p, abPK);
+ }
+ }else if( p->op!=SQLITE_INSERT ){
+ /* A DELETE change */
+ sessionAppendByte(&buf, SQLITE_DELETE, &rc);
+ sessionAppendByte(&buf, p->bIndirect, &rc);
+ sessionAppendBlob(&buf, p->aRecord, p->nRecord, &rc);
+ }
+ if( rc==SQLITE_OK ){
+ rc = sqlite3_reset(pSel);
+ }
+ }
+ }
+
+ sqlite3_finalize(pSel);
+ if( buf.nBuf==nNoop ){
+ buf.nBuf = nRewind;
+ }
+ sqlite3_free((char*)azCol); /* cast works around VC++ bug */
+ }
+ }
+
+ if( rc==SQLITE_OK ){
+ *pnChangeset = buf.nBuf;
+ *ppChangeset = buf.aBuf;
+ }else{
+ sqlite3_free(buf.aBuf);
+ }
+
+ sqlite3_exec(db, "RELEASE changeset", 0, 0, 0);
+ sqlite3_mutex_leave(sqlite3_db_mutex(db));
+ return rc;
+}
+
+/*
+** 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);
+}
+
+/*
+** 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 */
+){
+ sqlite3_changeset_iter *pRet; /* Iterator to return */
+ int nByte; /* Number of bytes to allocate for iterator */
+
+ /* 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->aChangeset = (u8 *)pChangeset;
+ pRet->nChangeset = nChangeset;
+ pRet->pNext = pRet->aChangeset;
+
+ /* Populate the output variable and return success. */
+ *pp = pRet;
+ 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 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(
+ u8 **paChange, /* IN/OUT: Pointer to binary record */
+ int nCol, /* Number of values in record */
+ sqlite3_value **apOut /* Write values to this array */
+){
+ int i; /* Used to iterate through columns */
+ u8 *aRec = *paChange; /* Cursor for the serialized record */
+
+ for(i=0; i<nCol; i++){
+ int eType = *aRec++; /* Type of value (SQLITE_NULL, TEXT etc.) */
+ assert( !apOut || apOut[i]==0 );
+ if( eType ){
+ if( apOut ){
+ apOut[i] = sqlite3ValueNew(0);
+ if( !apOut[i] ) return SQLITE_NOMEM;
+ }
+
+ if( eType==SQLITE_TEXT || eType==SQLITE_BLOB ){
+ int nByte;
+ aRec += sessionVarintGet(aRec, &nByte);
+ if( apOut ){
+ u8 enc = (eType==SQLITE_TEXT ? SQLITE_UTF8 : 0);
+ sqlite3ValueSetStr(apOut[i], nByte, (char *)aRec, enc, SQLITE_STATIC);
+ }
+ aRec += nByte;
+ }
+ if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){
+ if( apOut ){
+ sqlite3_int64 v = sessionGetI64(aRec);
+ if( eType==SQLITE_INTEGER ){
+ sqlite3VdbeMemSetInt64(apOut[i], v);
+ }else{
+ double d;
+ memcpy(&d, &v, 8);
+ sqlite3VdbeMemSetDouble(apOut[i], d);
+ }
+ }
+ aRec += 8;
+ }
+ }
+ }
+
+ *paChange = aRec;
+ return SQLITE_OK;
+}
+
+/*
+** 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 */
+){
+ u8 *aChange;
+ int i;
+
+ assert( (paRec==0 && pnRec==0) || (paRec && pnRec) );
+
+ /* If the iterator is in the error-state, return immediately. */
+ if( p->rc!=SQLITE_OK ) return p->rc;
+
+ /* Free the current contents of p->apValue[], if any. */
+ if( p->apValue ){
+ for(i=0; i<p->nCol*2; i++){
+ sqlite3ValueFree(p->apValue[i]);
+ }
+ memset(p->apValue, 0, sizeof(sqlite3_value*)*p->nCol*2);
+ }
+
+ /* If the iterator is already at the end of the changeset, return DONE. */
+ if( p->pNext>=&p->aChangeset[p->nChangeset] ){
+ return SQLITE_DONE;
+ }
+ aChange = p->pNext;
+
+ if( aChange[0]=='T' ){
+ int nByte; /* Bytes to allocate for apValue */
+ aChange++;
+ aChange += sessionVarintGet(aChange, &p->nCol);
+ p->abPK = (u8 *)aChange;
+ aChange += p->nCol;
+ p->zTab = (char *)aChange;
+ aChange += (sqlite3Strlen30((char *)aChange) + 1);
+
+ if( paRec==0 ){
+ sqlite3_free(p->apValue);
+ nByte = sizeof(sqlite3_value *) * p->nCol * 2;
+ p->apValue = (sqlite3_value **)sqlite3_malloc(nByte);
+ if( !p->apValue ){
+ return (p->rc = SQLITE_NOMEM);
+ }
+ memset(p->apValue, 0, sizeof(sqlite3_value*)*p->nCol*2);
+ }
+ }
+
+ p->op = *(aChange++);
+ p->bIndirect = *(aChange++);
+ if( p->op!=SQLITE_UPDATE && p->op!=SQLITE_DELETE && p->op!=SQLITE_INSERT ){
+ return (p->rc = SQLITE_CORRUPT);
+ }
+
+ if( paRec ){ *paRec = aChange; }
+
+ /* If this is an UPDATE or DELETE, read the old.* record. */
+ if( p->op!=SQLITE_INSERT ){
+ p->rc = sessionReadRecord(&aChange, p->nCol, paRec?0:p->apValue);
+ if( p->rc!=SQLITE_OK ) return p->rc;
+ }
+
+ /* If this is an INSERT or UPDATE, read the new.* record. */
+ if( p->op!=SQLITE_DELETE ){
+ p->rc = sessionReadRecord(&aChange, p->nCol, paRec?0:&p->apValue[p->nCol]);
+ if( p->rc!=SQLITE_OK ) return p->rc;
+ }
+
+ if( pnRec ){ *pnRec = (int)(aChange - *paRec); }
+ p->pNext = aChange;
+ return SQLITE_ROW;
+}
+
+/*
+** Advance an iterator created by sqlite3changeset_start() to the next
+** change in the changeset. This function may return SQLITE_ROW, SQLITE_DONE
+** or SQLITE_CORRUPT.
+**
+** This function may not be called on iterators passed to a conflict handler
+** callback by changeset_apply().
+*/
+int sqlite3changeset_next(sqlite3_changeset_iter *p){
+ return sessionChangesetNext(p, 0, 0);
+}
+
+/*
+** The following function extracts information on the current change
+** from a changeset iterator. It may only be called after changeset_next()
+** has returned SQLITE_ROW.
+*/
+int sqlite3changeset_op(
+ sqlite3_changeset_iter *pIter, /* Iterator handle */
+ const char **pzTab, /* OUT: Pointer to table name */
+ int *pnCol, /* OUT: Number of columns in table */
+ int *pOp, /* OUT: SQLITE_INSERT, DELETE or UPDATE */
+ int *pbIndirect /* OUT: True if change is indirect */
+){
+ *pOp = pIter->op;
+ *pnCol = pIter->nCol;
+ *pzTab = pIter->zTab;
+ if( pbIndirect ) *pbIndirect = pIter->bIndirect;
+ return SQLITE_OK;
+}
+
+/*
+** Return information regarding the PRIMARY KEY and number of columns in
+** the database table affected by the change that pIter currently points
+** to. This function may only be called after changeset_next() returns
+** SQLITE_ROW.
+*/
+int sqlite3changeset_pk(
+ sqlite3_changeset_iter *pIter, /* Iterator object */
+ unsigned char **pabPK, /* OUT: Array of boolean - true for PK cols */
+ int *pnCol /* OUT: Number of entries in output array */
+){
+ *pabPK = pIter->abPK;
+ if( pnCol ) *pnCol = pIter->nCol;
+ return SQLITE_OK;
+}
+
+/*
+** This function may only be called while the iterator is pointing to an
+** SQLITE_UPDATE or SQLITE_DELETE change (see sqlite3changeset_op()).
+** Otherwise, SQLITE_MISUSE is returned.
+**
+** It sets *ppValue to point to an sqlite3_value structure containing the
+** iVal'th value in the old.* record. Or, if that particular value is not
+** included in the record (because the change is an UPDATE and the field
+** was not modified and is not a PK column), set *ppValue to NULL.
+**
+** If value iVal is out-of-range, SQLITE_RANGE is returned and *ppValue is
+** not modified. Otherwise, SQLITE_OK.
+*/
+int sqlite3changeset_old(
+ sqlite3_changeset_iter *pIter, /* Changeset iterator */
+ int iVal, /* Index of old.* value to retrieve */
+ sqlite3_value **ppValue /* OUT: Old value (or NULL pointer) */
+){
+ if( pIter->op!=SQLITE_UPDATE && pIter->op!=SQLITE_DELETE ){
+ return SQLITE_MISUSE;
+ }
+ if( iVal<0 || iVal>=pIter->nCol ){
+ return SQLITE_RANGE;
+ }
+ *ppValue = pIter->apValue[iVal];
+ return SQLITE_OK;
+}
+
+/*
+** This function may only be called while the iterator is pointing to an
+** SQLITE_UPDATE or SQLITE_INSERT change (see sqlite3changeset_op()).
+** Otherwise, SQLITE_MISUSE is returned.
+**
+** It sets *ppValue to point to an sqlite3_value structure containing the
+** iVal'th value in the new.* record. Or, if that particular value is not
+** included in the record (because the change is an UPDATE and the field
+** was not modified), set *ppValue to NULL.
+**
+** If value iVal is out-of-range, SQLITE_RANGE is returned and *ppValue is
+** not modified. Otherwise, SQLITE_OK.
+*/
+int sqlite3changeset_new(
+ sqlite3_changeset_iter *pIter, /* Changeset iterator */
+ int iVal, /* Index of new.* value to retrieve */
+ sqlite3_value **ppValue /* OUT: New value (or NULL pointer) */
+){
+ if( pIter->op!=SQLITE_UPDATE && pIter->op!=SQLITE_INSERT ){
+ return SQLITE_MISUSE;
+ }
+ if( iVal<0 || iVal>=pIter->nCol ){
+ return SQLITE_RANGE;
+ }
+ *ppValue = pIter->apValue[pIter->nCol+iVal];
+ return SQLITE_OK;
+}
+
+/*
+** The following two macros are used internally. They are similar to the
+** sqlite3changeset_new() and sqlite3changeset_old() functions, except that
+** they omit all error checking and return a pointer to the requested value.
+*/
+#define sessionChangesetNew(pIter, iVal) (pIter)->apValue[(pIter)->nCol+(iVal)]
+#define sessionChangesetOld(pIter, iVal) (pIter)->apValue[(iVal)]
+
+/*
+** This function may only be called with a changeset iterator that has been
+** passed to an SQLITE_CHANGESET_DATA or SQLITE_CHANGESET_CONFLICT
+** conflict-handler function. Otherwise, SQLITE_MISUSE is returned.
+**
+** If successful, *ppValue is set to point to an sqlite3_value structure
+** containing the iVal'th value of the conflicting record.
+**
+** If value iVal is out-of-range or some other error occurs, an SQLite error
+** code is returned. Otherwise, SQLITE_OK.
+*/
+int sqlite3changeset_conflict(
+ sqlite3_changeset_iter *pIter, /* Changeset iterator */
+ int iVal, /* Index of conflict record value to fetch */
+ sqlite3_value **ppValue /* OUT: Value from conflicting row */
+){
+ if( !pIter->pConflict ){
+ return SQLITE_MISUSE;
+ }
+ if( iVal<0 || iVal>=sqlite3_column_count(pIter->pConflict) ){
+ return SQLITE_RANGE;
+ }
+ *ppValue = sqlite3_column_value(pIter->pConflict, iVal);
+ return SQLITE_OK;
+}
+
+/*
+** This function may only be called with an iterator passed to an
+** SQLITE_CHANGESET_FOREIGN_KEY conflict handler callback. In this case
+** it sets the output variable to the total number of known foreign key
+** violations in the destination database and returns SQLITE_OK.
+**
+** In all other cases this function returns SQLITE_MISUSE.
+*/
+int sqlite3changeset_fk_conflicts(
+ sqlite3_changeset_iter *pIter, /* Changeset iterator */
+ int *pnOut /* OUT: Number of FK violations */
+){
+ if( pIter->pConflict || pIter->apValue ){
+ return SQLITE_MISUSE;
+ }
+ *pnOut = pIter->nCol;
+ return SQLITE_OK;
+}
+
+
+/*
+** Finalize an iterator allocated with sqlite3changeset_start().
+**
+** This function may not be called on iterators passed to a conflict handler
+** callback by changeset_apply().
+*/
+int sqlite3changeset_finalize(sqlite3_changeset_iter *p){
+ int i; /* Used to iterate through p->apValue[] */
+ int rc = p->rc; /* Return code */
+ if( p->apValue ){
+ for(i=0; i<p->nCol*2; i++) sqlite3ValueFree(p->apValue[i]);
+ }
+ sqlite3_free(p->apValue);
+ sqlite3_free(p);
+ 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 */
+){
+ int rc = SQLITE_OK; /* Return value */
+ u8 *aOut;
+ u8 *aIn;
+ int i;
+ 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 */
+
+ /* Zero the output variables in case an error occurs. */
+ *ppInverted = 0;
+ *pnInverted = 0;
+ if( nChangeset==0 ) return SQLITE_OK;
+
+ aOut = (u8 *)sqlite3_malloc(nChangeset);
+ if( !aOut ) return SQLITE_NOMEM;
+ aIn = (u8 *)pChangeset;
+
+ i = 0;
+ while( i<nChangeset ){
+ u8 eType = aIn[i];
+ 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 (abPK),
+ ** * A nul-terminated table name.
+ */
+ int nByte = 1 + sessionVarintGet(&aIn[i+1], &nCol);
+ abPK = &aIn[i+nByte];
+ nByte += nCol;
+ nByte += 1 + sqlite3Strlen30((char *)&aIn[i+nByte]);
+ memcpy(&aOut[i], &aIn[i], nByte);
+ i += nByte;
+ sqlite3_free(apVal);
+ apVal = 0;
+ break;
+ }
+
+ case SQLITE_INSERT:
+ case SQLITE_DELETE: {
+ int nByte;
+ u8 *aEnd = &aIn[i+2];
+
+ sessionReadRecord(&aEnd, nCol, 0);
+ aOut[i] = (eType==SQLITE_DELETE ? SQLITE_INSERT : SQLITE_DELETE);
+ aOut[i+1] = aIn[i+1];
+ nByte = (int)(aEnd - &aIn[i+2]);
+ memcpy(&aOut[i+2], &aIn[i+2], nByte);
+ i += 2 + nByte;
+ break;
+ }
+
+ case SQLITE_UPDATE: {
+ int iCol;
+ int nWrite = 0;
+ u8 *aEnd = &aIn[i+2];
+
+ if( 0==apVal ){
+ apVal = (sqlite3_value **)sqlite3_malloc(sizeof(apVal[0])*nCol*2);
+ if( 0==apVal ){
+ rc = SQLITE_NOMEM;
+ goto finished_invert;
+ }
+ memset(apVal, 0, sizeof(apVal[0])*nCol*2);
+ }
+
+ /* Read the old.* and new.* records for the update change. */
+ rc = sessionReadRecord(&aEnd, nCol, &apVal[0]);
+ if( rc==SQLITE_OK ){
+ rc = sessionReadRecord(&aEnd, nCol, &apVal[nCol]);
+ }
+
+ /* Write the header for the new UPDATE change. Same as the original. */
+ aOut[i] = SQLITE_UPDATE;
+ aOut[i+1] = aIn[i+1];
+ nWrite = 2;
+
+ /* 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; rc==SQLITE_OK && iCol<nCol; iCol++){
+ sqlite3_value *pVal = apVal[iCol + (abPK[iCol] ? 0 : nCol)];
+ rc = sessionSerializeValue(&aOut[i+nWrite], pVal, &nWrite);
+ }
+
+ /* 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; rc==SQLITE_OK && iCol<nCol; iCol++){
+ sqlite3_value *pVal = (abPK[iCol] ? 0 : apVal[iCol]);
+ rc = sessionSerializeValue(&aOut[i+nWrite], pVal, &nWrite);
+ }
+
+ 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;
+ }
+
+ i += nWrite;
+ assert( &aIn[i]==aEnd );
+ break;
+ }
+
+ default:
+ rc = SQLITE_CORRUPT;
+ goto finished_invert;
+ }
+ }
+
+ assert( rc==SQLITE_OK );
+ *pnInverted = nChangeset;
+ *ppInverted = (void *)aOut;
+
+ finished_invert:
+ if( rc!=SQLITE_OK ){
+ sqlite3_free(aOut);
+ }
+ sqlite3_free(apVal);
+ return rc;
+}
+
+typedef struct SessionApplyCtx SessionApplyCtx;
+struct SessionApplyCtx {
+ sqlite3 *db;
+ sqlite3_stmt *pDelete; /* DELETE statement */
+ sqlite3_stmt *pUpdate; /* UPDATE statement */
+ sqlite3_stmt *pInsert; /* INSERT statement */
+ sqlite3_stmt *pSelect; /* SELECT statement */
+ int nCol; /* Size of azCol[] and abPK[] arrays */
+ const char **azCol; /* Array of column names */
+ u8 *abPK; /* Boolean array - true if column is in PK */
+};
+
+/*
+** Formulate a statement to DELETE a row from database db. Assuming a table
+** structure like this:
+**
+** CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c));
+**
+** The DELETE statement looks like this:
+**
+** DELETE FROM x WHERE a = :1 AND c = :3 AND (:5 OR b IS :2 AND d IS :4)
+**
+** Variable :5 (nCol+1) is a boolean. It should be set to 0 if we require
+** matching b and d values, or 1 otherwise. The second case comes up if the
+** conflict handler is invoked with NOTFOUND and returns CHANGESET_REPLACE.
+**
+** If successful, SQLITE_OK is returned and SessionApplyCtx.pDelete is left
+** pointing to the prepared version of the SQL statement.
+*/
+static int sessionDeleteRow(
+ sqlite3 *db, /* Database handle */
+ const char *zTab, /* Table name */
+ SessionApplyCtx *p /* Session changeset-apply context */
+){
+ int i;
+ const char *zSep = "";
+ int rc = SQLITE_OK;
+ SessionBuffer buf = {0, 0, 0};
+ int nPk = 0;
+
+ sessionAppendStr(&buf, "DELETE FROM ", &rc);
+ sessionAppendIdent(&buf, zTab, &rc);
+ sessionAppendStr(&buf, " WHERE ", &rc);
+
+ for(i=0; i<p->nCol; i++){
+ if( p->abPK[i] ){
+ nPk++;
+ sessionAppendStr(&buf, zSep, &rc);
+ sessionAppendIdent(&buf, p->azCol[i], &rc);
+ sessionAppendStr(&buf, " = ?", &rc);
+ sessionAppendInteger(&buf, i+1, &rc);
+ zSep = " AND ";
+ }
+ }
+
+ if( nPk<p->nCol ){
+ sessionAppendStr(&buf, " AND (?", &rc);
+ sessionAppendInteger(&buf, p->nCol+1, &rc);
+ sessionAppendStr(&buf, " OR ", &rc);
+
+ zSep = "";
+ for(i=0; i<p->nCol; i++){
+ if( !p->abPK[i] ){
+ sessionAppendStr(&buf, zSep, &rc);
+ sessionAppendIdent(&buf, p->azCol[i], &rc);
+ sessionAppendStr(&buf, " IS ?", &rc);
+ sessionAppendInteger(&buf, i+1, &rc);
+ zSep = "AND ";
+ }
+ }
+ sessionAppendStr(&buf, ")", &rc);
+ }
+
+ if( rc==SQLITE_OK ){
+ rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, &p->pDelete, 0);
+ }
+ sqlite3_free(buf.aBuf);
+
+ return rc;
+}
+
+/*
+** Formulate and prepare a statement to UPDATE a row from database db.
+** Assuming a table structure like this:
+**
+** CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c));
+**
+** The UPDATE statement looks like this:
+**
+** UPDATE x SET
+** a = CASE WHEN ?2 THEN ?3 ELSE a END,
+** b = CASE WHEN ?5 THEN ?6 ELSE b END,
+** c = CASE WHEN ?8 THEN ?9 ELSE c END,
+** d = CASE WHEN ?11 THEN ?12 ELSE d END
+** WHERE a = ?1 AND c = ?7 AND (?13 OR
+** (?5==0 OR b IS ?4) AND (?11==0 OR d IS ?10) AND
+** )
+**
+** For each column in the table, there are three variables to bind:
+**
+** ?(i*3+1) The old.* value of the column, if any.
+** ?(i*3+2) A boolean flag indicating that the value is being modified.
+** ?(i*3+3) The new.* value of the column, if any.
+**
+** Also, a boolean flag that, if set to true, causes the statement to update
+** a row even if the non-PK values do not match. This is required if the
+** conflict-handler is invoked with CHANGESET_DATA and returns
+** CHANGESET_REPLACE. This is variable "?(nCol*3+1)".
+**
+** If successful, SQLITE_OK is returned and SessionApplyCtx.pUpdate is left
+** pointing to the prepared version of the SQL statement.
+*/
+static int sessionUpdateRow(
+ sqlite3 *db, /* Database handle */
+ const char *zTab, /* Table name */
+ SessionApplyCtx *p /* Session changeset-apply context */
+){
+ int rc = SQLITE_OK;
+ int i;
+ const char *zSep = "";
+ SessionBuffer buf = {0, 0, 0};
+
+ /* Append "UPDATE tbl SET " */
+ sessionAppendStr(&buf, "UPDATE ", &rc);
+ sessionAppendIdent(&buf, zTab, &rc);
+ sessionAppendStr(&buf, " SET ", &rc);
+
+ /* Append the assignments */
+ for(i=0; i<p->nCol; i++){
+ sessionAppendStr(&buf, zSep, &rc);
+ sessionAppendIdent(&buf, p->azCol[i], &rc);
+ sessionAppendStr(&buf, " = CASE WHEN ?", &rc);
+ sessionAppendInteger(&buf, i*3+2, &rc);
+ sessionAppendStr(&buf, " THEN ?", &rc);
+ sessionAppendInteger(&buf, i*3+3, &rc);
+ sessionAppendStr(&buf, " ELSE ", &rc);
+ sessionAppendIdent(&buf, p->azCol[i], &rc);
+ sessionAppendStr(&buf, " END", &rc);
+ zSep = ", ";
+ }
+
+ /* Append the PK part of the WHERE clause */
+ sessionAppendStr(&buf, " WHERE ", &rc);
+ for(i=0; i<p->nCol; i++){
+ if( p->abPK[i] ){
+ sessionAppendIdent(&buf, p->azCol[i], &rc);
+ sessionAppendStr(&buf, " = ?", &rc);
+ sessionAppendInteger(&buf, i*3+1, &rc);
+ sessionAppendStr(&buf, " AND ", &rc);
+ }
+ }
+
+ /* Append the non-PK part of the WHERE clause */
+ sessionAppendStr(&buf, " (?", &rc);
+ sessionAppendInteger(&buf, p->nCol*3+1, &rc);
+ sessionAppendStr(&buf, " OR 1", &rc);
+ for(i=0; i<p->nCol; i++){
+ if( !p->abPK[i] ){
+ sessionAppendStr(&buf, " AND (?", &rc);
+ sessionAppendInteger(&buf, i*3+2, &rc);
+ sessionAppendStr(&buf, "=0 OR ", &rc);
+ sessionAppendIdent(&buf, p->azCol[i], &rc);
+ sessionAppendStr(&buf, " IS ?", &rc);
+ sessionAppendInteger(&buf, i*3+1, &rc);
+ sessionAppendStr(&buf, ")", &rc);
+ }
+ }
+ sessionAppendStr(&buf, ")", &rc);
+
+ if( rc==SQLITE_OK ){
+ rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, &p->pUpdate, 0);
+ }
+ sqlite3_free(buf.aBuf);
+
+ return rc;
+}
+
+/*
+** Formulate and prepare an SQL statement to query table zTab by primary
+** key. Assuming the following table structure:
+**
+** CREATE TABLE x(a, b, c, d, PRIMARY KEY(a, c));
+**
+** The SELECT statement looks like this:
+**
+** SELECT * FROM x WHERE a = ?1 AND c = ?3
+**
+** If successful, SQLITE_OK is returned and SessionApplyCtx.pSelect is left
+** pointing to the prepared version of the SQL statement.
+*/
+static int sessionSelectRow(
+ sqlite3 *db, /* Database handle */
+ const char *zTab, /* Table name */
+ SessionApplyCtx *p /* Session changeset-apply context */
+){
+ return sessionSelectStmt(
+ db, "main", zTab, p->nCol, p->azCol, p->abPK, &p->pSelect);
+}
+
+/*
+** Formulate and prepare an INSERT statement to add a record to table zTab.
+** For example:
+**
+** INSERT INTO main."zTab" VALUES(?1, ?2, ?3 ...);
+**
+** If successful, SQLITE_OK is returned and SessionApplyCtx.pInsert is left
+** pointing to the prepared version of the SQL statement.
+*/
+static int sessionInsertRow(
+ sqlite3 *db, /* Database handle */
+ const char *zTab, /* Table name */
+ SessionApplyCtx *p /* Session changeset-apply context */
+){
+ int rc = SQLITE_OK;
+ int i;
+ SessionBuffer buf = {0, 0, 0};
+
+ sessionAppendStr(&buf, "INSERT INTO main.", &rc);
+ sessionAppendIdent(&buf, zTab, &rc);
+ sessionAppendStr(&buf, " VALUES(?", &rc);
+ for(i=1; i<p->nCol; i++){
+ sessionAppendStr(&buf, ", ?", &rc);
+ }
+ sessionAppendStr(&buf, ")", &rc);
+
+ if( rc==SQLITE_OK ){
+ rc = sqlite3_prepare_v2(db, (char *)buf.aBuf, buf.nBuf, &p->pInsert, 0);
+ }
+ sqlite3_free(buf.aBuf);
+ return rc;
+}
+
+/*
+** A wrapper around sqlite3_bind_value() that detects an extra problem.
+** See comments in the body of this function for details.
+*/
+static int sessionBindValue(
+ sqlite3_stmt *pStmt, /* Statement to bind value to */
+ int i, /* Parameter number to bind to */
+ sqlite3_value *pVal /* Value to bind */
+){
+ int eType = sqlite3_value_type(pVal);
+ 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-hanler) has zeroed the pVal->z pointer. Return NOMEM. */
+ return SQLITE_NOMEM;
+ }
+ return sqlite3_bind_value(pStmt, i, pVal);
+}
+
+/*
+** Iterator pIter must point to an SQLITE_INSERT entry. This function
+** transfers new.* values from the current iterator entry to statement
+** pStmt. The table being inserted into has nCol columns.
+**
+** New.* value $i 0 from the iterator is bound to variable ($i+1) of
+** statement pStmt. If parameter abPK is NULL, all values from 0 to (nCol-1)
+** are transfered to the statement. Otherwise, if abPK is not NULL, it points
+** to an array nCol elements in size. In this case only those values for
+** which abPK[$i] is true are read from the iterator and bound to the
+** statement.
+**
+** An SQLite error code is returned if an error occurs. Otherwise, SQLITE_OK.
+*/
+static int sessionBindRow(
+ sqlite3_changeset_iter *pIter, /* Iterator to read values from */
+ int(*xValue)(sqlite3_changeset_iter *, int, sqlite3_value **),
+ int nCol, /* Number of columns */
+ u8 *abPK, /* If not NULL, bind only if true */
+ sqlite3_stmt *pStmt /* Bind values to this statement */
+){
+ int i;
+ int rc = SQLITE_OK;
+
+ /* Neither sqlite3changeset_old or sqlite3changeset_new can fail if the
+ ** argument iterator points to a suitable entry. Make sure that xValue
+ ** is one of these to guarantee that it is safe to ignore the return
+ ** in the code below. */
+ assert( xValue==sqlite3changeset_old || xValue==sqlite3changeset_new );
+
+ for(i=0; rc==SQLITE_OK && i<nCol; i++){
+ if( !abPK || abPK[i] ){
+ sqlite3_value *pVal;
+ (void)xValue(pIter, i, &pVal);
+ rc = sessionBindValue(pStmt, i+1, pVal);
+ }
+ }
+ return rc;
+}
+
+/*
+** SQL statement pSelect is as generated by the sessionSelectRow() function.
+** This function binds the primary key values from the change that changeset
+** iterator pIter points to to the SELECT and attempts to seek to the table
+** entry. If a row is found, the SELECT statement left pointing at the row
+** and SQLITE_ROW is returned. Otherwise, if no row is found and no error
+** has occured, the statement is reset and SQLITE_OK is returned. If an
+** error occurs, the statement is reset and an SQLite error code is returned.
+**
+** If this function returns SQLITE_ROW, the caller must eventually reset()
+** statement pSelect. If any other value is returned, the statement does
+** not require a reset().
+**
+** If the iterator currently points to an INSERT record, bind values from the
+** new.* record to the SELECT statement. Or, if it points to a DELETE or
+** UPDATE, bind values from the old.* record.
+*/
+static int sessionSeekToRow(
+ sqlite3 *db, /* Database handle */
+ sqlite3_changeset_iter *pIter, /* Changeset iterator */
+ u8 *abPK, /* Primary key flags array */
+ sqlite3_stmt *pSelect /* SELECT statement from sessionSelectRow() */
+){
+ int rc; /* Return code */
+ int nCol; /* Number of columns in table */
+ int op; /* Changset operation (SQLITE_UPDATE etc.) */
+ const char *zDummy; /* Unused */
+
+ sqlite3changeset_op(pIter, &zDummy, &nCol, &op, 0);
+ rc = sessionBindRow(pIter,
+ op==SQLITE_INSERT ? sqlite3changeset_new : sqlite3changeset_old,
+ nCol, abPK, pSelect
+ );
+
+ if( rc==SQLITE_OK ){
+ rc = sqlite3_step(pSelect);
+ if( rc!=SQLITE_ROW ) rc = sqlite3_reset(pSelect);
+ }
+
+ return rc;
+}
+
+/*
+** Invoke the conflict handler for the change that the changeset iterator
+** currently points to.
+**
+** Argument eType must be either CHANGESET_DATA or CHANGESET_CONFLICT.
+** If argument pbReplace is NULL, then the type of conflict handler invoked
+** depends solely on eType, as follows:
+**
+** eType value Value passed to xConflict
+** -------------------------------------------------
+** CHANGESET_DATA CHANGESET_NOTFOUND
+** CHANGESET_CONFLICT CHANGESET_CONSTRAINT
+**
+** Or, if pbReplace is not NULL, then an attempt is made to find an existing
+** record with the same primary key as the record about to be deleted, updated
+** or inserted. If such a record can be found, it is available to the conflict
+** handler as the "conflicting" record. In this case the type of conflict
+** handler invoked is as follows:
+**
+** eType value PK Record found? Value passed to xConflict
+** ----------------------------------------------------------------
+** CHANGESET_DATA Yes CHANGESET_DATA
+** CHANGESET_DATA No CHANGESET_NOTFOUND
+** CHANGESET_CONFLICT Yes CHANGESET_CONFLICT
+** CHANGESET_CONFLICT No CHANGESET_CONSTRAINT
+**
+** If pbReplace is not NULL, and a record with a matching PK is found, and
+** the conflict handler function returns SQLITE_CHANGESET_REPLACE, *pbReplace
+** is set to non-zero before returning SQLITE_OK.
+**
+** If the conflict handler returns SQLITE_CHANGESET_ABORT, SQLITE_ABORT is
+** returned. Or, if the conflict handler returns an invalid value,
+** SQLITE_MISUSE. If the conflict handler returns SQLITE_CHANGESET_OMIT,
+** this function returns SQLITE_OK.
+*/
+static int sessionConflictHandler(
+ int eType, /* Either CHANGESET_DATA or CONFLICT */
+ SessionApplyCtx *p, /* changeset_apply() context */
+ sqlite3_changeset_iter *pIter, /* Changeset iterator */
+ int(*xConflict)(void *, int, sqlite3_changeset_iter*),
+ void *pCtx, /* First argument for conflict handler */
+ int *pbReplace /* OUT: Set to true if PK row is found */
+){
+ int res; /* Value returned by conflict handler */
+ int rc;
+ int nCol;
+ int op;
+ const char *zDummy;
+
+ sqlite3changeset_op(pIter, &zDummy, &nCol, &op, 0);
+
+ assert( eType==SQLITE_CHANGESET_CONFLICT || eType==SQLITE_CHANGESET_DATA );
+ assert( SQLITE_CHANGESET_CONFLICT+1==SQLITE_CHANGESET_CONSTRAINT );
+ assert( SQLITE_CHANGESET_DATA+1==SQLITE_CHANGESET_NOTFOUND );
+
+ /* Bind the new.* PRIMARY KEY values to the SELECT statement. */
+ if( pbReplace ){
+ rc = sessionSeekToRow(p->db, pIter, p->abPK, p->pSelect);
+ }else{
+ rc = SQLITE_OK;
+ }
+
+ if( rc==SQLITE_ROW ){
+ /* There exists another row with the new.* primary key. */
+ pIter->pConflict = p->pSelect;
+ res = xConflict(pCtx, eType, pIter);
+ pIter->pConflict = 0;
+ rc = sqlite3_reset(p->pSelect);
+ }else if( rc==SQLITE_OK ){
+ /* No other row with the new.* primary key. */
+ res = xConflict(pCtx, eType+1, pIter);
+ if( res==SQLITE_CHANGESET_REPLACE ) rc = SQLITE_MISUSE;
+ }
+
+ if( rc==SQLITE_OK ){
+ switch( res ){
+ case SQLITE_CHANGESET_REPLACE:
+ assert( pbReplace );
+ *pbReplace = 1;
+ break;
+
+ case SQLITE_CHANGESET_OMIT:
+ break;
+
+ case SQLITE_CHANGESET_ABORT:
+ rc = SQLITE_ABORT;
+ break;
+
+ default:
+ rc = SQLITE_MISUSE;
+ break;
+ }
+ }
+
+ return rc;
+}
+
+/*
+** Attempt to apply the change that the iterator passed as the first argument
+** currently points to to the database. If a conflict is encountered, invoke
+** the conflict handler callback.
+**
+** If argument pbRetry is NULL, then ignore any CHANGESET_DATA conflict. If
+** one is encountered, update or delete the row with the matching primary key
+** instead. Or, if pbRetry is not NULL and a CHANGESET_DATA conflict occurs,
+** invoke the conflict handler. If it returns CHANGESET_REPLACE, set *pbRetry
+** to true before returning. In this case the caller will invoke this function
+** again, this time with pbRetry set to NULL.
+**
+** If argument pbReplace is NULL and a CHANGESET_CONFLICT conflict is
+** encountered invoke the conflict handler with CHANGESET_CONSTRAINT instead.
+** Or, if pbReplace is not NULL, invoke it with CHANGESET_CONFLICT. If such
+** an invocation returns SQLITE_CHANGESET_REPLACE, set *pbReplace to true
+** before retrying. In this case the caller attempts to remove the conflicting
+** row before invoking this function again, this time with pbReplace set
+** to NULL.
+**
+** If any conflict handler returns SQLITE_CHANGESET_ABORT, this function
+** returns SQLITE_ABORT. Otherwise, if no error occurs, SQLITE_OK is
+** returned.
+*/
+static int sessionApplyOneOp(
+ sqlite3_changeset_iter *pIter, /* Changeset iterator */
+ SessionApplyCtx *p, /* changeset_apply() context */
+ int(*xConflict)(void *, int, sqlite3_changeset_iter *),
+ void *pCtx, /* First argument for the conflict handler */
+ int *pbReplace, /* OUT: True to remove PK row and retry */
+ int *pbRetry /* OUT: True to retry. */
+){
+ const char *zDummy;
+ int op;
+ int nCol;
+ int rc = SQLITE_OK;
+
+ assert( p->pDelete && p->pUpdate && p->pInsert && p->pSelect );
+ assert( p->azCol && p->abPK );
+ assert( !pbReplace || *pbReplace==0 );
+
+ sqlite3changeset_op(pIter, &zDummy, &nCol, &op, 0);
+
+ if( op==SQLITE_DELETE ){
+
+ /* Bind values to the DELETE statement. */
+ rc = sessionBindRow(pIter, sqlite3changeset_old, nCol, 0, p->pDelete);
+ if( rc==SQLITE_OK && sqlite3_bind_parameter_count(p->pDelete)>nCol ){
+ rc = sqlite3_bind_int(p->pDelete, nCol+1, pbRetry==0);
+ }
+ if( rc!=SQLITE_OK ) return rc;
+
+ sqlite3_step(p->pDelete);
+ rc = sqlite3_reset(p->pDelete);
+ if( rc==SQLITE_OK && sqlite3_changes(p->db)==0 ){
+ rc = sessionConflictHandler(
+ SQLITE_CHANGESET_DATA, p, pIter, xConflict, pCtx, pbRetry
+ );
+ }else if( (rc&0xff)==SQLITE_CONSTRAINT ){
+ rc = sessionConflictHandler(
+ SQLITE_CHANGESET_CONFLICT, p, pIter, xConflict, pCtx, 0
+ );
+ }
+
+ }else if( op==SQLITE_UPDATE ){
+ int i;
+
+ /* Bind values to the UPDATE statement. */
+ for(i=0; rc==SQLITE_OK && i<nCol; i++){
+ sqlite3_value *pOld = sessionChangesetOld(pIter, i);
+ sqlite3_value *pNew = sessionChangesetNew(pIter, i);
+
+ sqlite3_bind_int(p->pUpdate, i*3+2, !!pNew);
+ if( pOld ){
+ rc = sessionBindValue(p->pUpdate, i*3+1, pOld);
+ }
+ if( rc==SQLITE_OK && pNew ){
+ rc = sessionBindValue(p->pUpdate, i*3+3, pNew);
+ }
+ }
+ if( rc==SQLITE_OK ) sqlite3_bind_int(p->pUpdate, nCol*3+1, pbRetry==0);
+ if( rc!=SQLITE_OK ) return rc;
+
+ /* Attempt the UPDATE. In the case of a NOTFOUND or DATA conflict,
+ ** the result will be SQLITE_OK with 0 rows modified. */
+ sqlite3_step(p->pUpdate);
+ rc = sqlite3_reset(p->pUpdate);
+
+ if( rc==SQLITE_OK && sqlite3_changes(p->db)==0 ){
+ /* A NOTFOUND or DATA error. Search the table to see if it contains
+ ** a row with a matching primary key. If so, this is a DATA conflict.
+ ** Otherwise, if there is no primary key match, it is a NOTFOUND. */
+
+ rc = sessionConflictHandler(
+ SQLITE_CHANGESET_DATA, p, pIter, xConflict, pCtx, pbRetry
+ );
+
+ }else if( (rc&0xff)==SQLITE_CONSTRAINT ){
+ /* This is always a CONSTRAINT conflict. */
+ rc = sessionConflictHandler(
+ SQLITE_CHANGESET_CONFLICT, p, pIter, xConflict, pCtx, 0
+ );
+ }
+
+ }else{
+ assert( op==SQLITE_INSERT );
+ rc = sessionBindRow(pIter, sqlite3changeset_new, nCol, 0, p->pInsert);
+ if( rc!=SQLITE_OK ) return rc;
+
+ sqlite3_step(p->pInsert);
+ rc = sqlite3_reset(p->pInsert);
+ if( (rc&0xff)==SQLITE_CONSTRAINT ){
+ rc = sessionConflictHandler(
+ SQLITE_CHANGESET_CONFLICT, p, pIter, xConflict, pCtx, pbReplace
+ );
+ }
+ }
+
+ return rc;
+}
+
+/*
+** 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 */
+){
+ int schemaMismatch = 0;
+ sqlite3_changeset_iter *pIter; /* Iterator to skip through changeset */
+ int rc; /* Return code */
+ const char *zTab = 0; /* Name of current table */
+ int nTab = 0; /* Result of sqlite3Strlen30(zTab) */
+ SessionApplyCtx sApply; /* changeset_apply() context object */
+
+ memset(&sApply, 0, sizeof(sApply));
+ rc = sqlite3changeset_start(&pIter, nChangeset, pChangeset);
+ if( rc!=SQLITE_OK ) return rc;
+
+ sqlite3_mutex_enter(sqlite3_db_mutex(db));
+ rc = sqlite3_exec(db, "SAVEPOINT changeset_apply", 0, 0, 0);
+ if( rc==SQLITE_OK ){
+ rc = sqlite3_exec(db, "PRAGMA defer_foreign_keys = 1", 0, 0, 0);
+ }
+ while( rc==SQLITE_OK && SQLITE_ROW==sqlite3changeset_next(pIter) ){
+ int nCol;
+ int op;
+ int bReplace = 0;
+ int bRetry = 0;
+ const char *zNew;
+
+ sqlite3changeset_op(pIter, &zNew, &nCol, &op, 0);
+
+ if( zTab==0 || sqlite3_strnicmp(zNew, zTab, nTab+1) ){
+ u8 *abPK;
+
+ sqlite3_free((char*)sApply.azCol); /* cast works around VC++ bug */
+ sqlite3_finalize(sApply.pDelete);
+ sqlite3_finalize(sApply.pUpdate);
+ sqlite3_finalize(sApply.pInsert);
+ sqlite3_finalize(sApply.pSelect);
+ memset(&sApply, 0, sizeof(sApply));
+ sApply.db = db;
+
+ /* If an xFilter() callback was specified, invoke it now. If the
+ ** xFilter callback returns zero, skip this table. If it returns
+ ** non-zero, proceed. */
+ schemaMismatch = (xFilter && (0==xFilter(pCtx, zNew)));
+ if( schemaMismatch ){
+ zTab = sqlite3_mprintf("%s", zNew);
+ nTab = (int)strlen(zTab);
+ sApply.azCol = (const char **)zTab;
+ }else{
+ sqlite3changeset_pk(pIter, &abPK, 0);
+ rc = sessionTableInfo(
+ db, "main", zNew, &sApply.nCol, &zTab, &sApply.azCol, &sApply.abPK
+ );
+ if( rc!=SQLITE_OK ) break;
+
+ if( sApply.nCol==0 ){
+ schemaMismatch = 1;
+ sqlite3_log(SQLITE_SCHEMA,
+ "sqlite3changeset_apply(): no such table: %s", zTab
+ );
+ }
+ else if( sApply.nCol!=nCol ){
+ schemaMismatch = 1;
+ sqlite3_log(SQLITE_SCHEMA,
+ "sqlite3changeset_apply(): table %s has %d columns, expected %d",
+ zTab, sApply.nCol, nCol
+ );
+ }
+ else if( memcmp(sApply.abPK, abPK, nCol)!=0 ){
+ schemaMismatch = 1;
+ sqlite3_log(SQLITE_SCHEMA, "sqlite3changeset_apply(): "
+ "primary key mismatch for table %s", zTab
+ );
+ }
+ else if(
+ (rc = sessionSelectRow(db, zTab, &sApply))
+ || (rc = sessionUpdateRow(db, zTab, &sApply))
+ || (rc = sessionDeleteRow(db, zTab, &sApply))
+ || (rc = sessionInsertRow(db, zTab, &sApply))
+ ){
+ break;
+ }
+ nTab = sqlite3Strlen30(zTab);
+ }
+ }
+
+ /* If there is a schema mismatch on the current table, proceed to the
+ ** next change. A log message has already been issued. */
+ if( schemaMismatch ) continue;
+
+ rc = sessionApplyOneOp(pIter, &sApply, xConflict, pCtx, &bReplace, &bRetry);
+
+ if( rc==SQLITE_OK && bRetry ){
+ rc = sessionApplyOneOp(pIter, &sApply, xConflict, pCtx, &bReplace, 0);
+ }
+
+ if( bReplace ){
+ assert( pIter->op==SQLITE_INSERT );
+ rc = sqlite3_exec(db, "SAVEPOINT replace_op", 0, 0, 0);
+ if( rc==SQLITE_OK ){
+ rc = sessionBindRow(pIter,
+ sqlite3changeset_new, sApply.nCol, sApply.abPK, sApply.pDelete);
+ sqlite3_bind_int(sApply.pDelete, sApply.nCol+1, 1);
+ }
+ if( rc==SQLITE_OK ){
+ sqlite3_step(sApply.pDelete);
+ rc = sqlite3_reset(sApply.pDelete);
+ }
+ if( rc==SQLITE_OK ){
+ rc = sessionApplyOneOp(pIter, &sApply, xConflict, pCtx, 0, 0);
+ }
+ if( rc==SQLITE_OK ){
+ rc = sqlite3_exec(db, "RELEASE replace_op", 0, 0, 0);
+ }
+ }
+ }
+
+ if( rc==SQLITE_OK ){
+ rc = sqlite3changeset_finalize(pIter);
+ }else{
+ sqlite3changeset_finalize(pIter);
+ }
+
+ if( rc==SQLITE_OK ){
+ int nFk, notUsed;
+ sqlite3_db_status(db, SQLITE_DBSTATUS_DEFERRED_FKS, &nFk, &notUsed, 0);
+ if( nFk!=0 ){
+ int res = SQLITE_CHANGESET_ABORT;
+ if( xConflict ){
+ sqlite3_changeset_iter sIter;
+ memset(&sIter, 0, sizeof(sIter));
+ sIter.nCol = nFk;
+ res = xConflict(pCtx, SQLITE_CHANGESET_FOREIGN_KEY, &sIter);
+ }
+ if( res!=SQLITE_CHANGESET_OMIT ){
+ rc = SQLITE_CONSTRAINT;
+ }
+ }
+ }
+ sqlite3_exec(db, "PRAGMA defer_foreign_keys = 0", 0, 0, 0);
+
+ if( rc==SQLITE_OK ){
+ rc = sqlite3_exec(db, "RELEASE changeset_apply", 0, 0, 0);
+ }else{
+ sqlite3_exec(db, "ROLLBACK TO changeset_apply", 0, 0, 0);
+ sqlite3_exec(db, "RELEASE changeset_apply", 0, 0, 0);
+ }
+
+ sqlite3_finalize(sApply.pInsert);
+ sqlite3_finalize(sApply.pDelete);
+ sqlite3_finalize(sApply.pUpdate);
+ sqlite3_finalize(sApply.pSelect);
+ sqlite3_free((char*)sApply.azCol); /* cast works around VC++ bug */
+ sqlite3_mutex_leave(sqlite3_db_mutex(db));
+ return rc;
+}
+
+/*
+** 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 */
+ SessionChange *pExist, /* Existing change */
+ int op2, /* Second change operation */
+ int bIndirect, /* True if second change is indirect */
+ u8 *aRec, /* Second change record */
+ int nRec, /* Number of bytes in aRec */
+ SessionChange **ppNew /* OUT: Merged change */
+){
+ SessionChange *pNew = 0;
+
+ if( !pExist ){
+ pNew = (SessionChange *)sqlite3_malloc(sizeof(SessionChange));
+ if( !pNew ){
+ return SQLITE_NOMEM;
+ }
+ memset(pNew, 0, sizeof(SessionChange));
+ pNew->op = op2;
+ pNew->bIndirect = bIndirect;
+ pNew->nRecord = nRec;
+ pNew->aRecord = aRec;
+ }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{
+ int nByte;
+ u8 *aCsr;
+
+ nByte = sizeof(SessionChange) + pExist->nRecord + nRec;
+ pNew = (SessionChange *)sqlite3_malloc(nByte);
+ if( !pNew ){
+ sqlite3_free(pExist);
+ return SQLITE_NOMEM;
+ }
+ memset(pNew, 0, sizeof(SessionChange));
+ pNew->bIndirect = (bIndirect && pExist->bIndirect);
+ aCsr = pNew->aRecord = (u8 *)&pNew[1];
+
+ if( op1==SQLITE_INSERT ){ /* INSERT + UPDATE */
+ u8 *a1 = aRec;
+ assert( op2==SQLITE_UPDATE );
+ pNew->op = SQLITE_INSERT;
+ sessionReadRecord(&a1, pTab->nCol, 0);
+ sessionMergeRecord(&aCsr, pTab->nCol, pExist->aRecord, a1);
+ }else if( op1==SQLITE_DELETE ){ /* DELETE + INSERT */
+ assert( op2==SQLITE_INSERT );
+ pNew->op = SQLITE_UPDATE;
+ if( 0==sessionMergeUpdate(&aCsr, pTab, pExist->aRecord, 0, aRec, 0) ){
+ sqlite3_free(pNew);
+ pNew = 0;
+ }
+ }else if( op2==SQLITE_UPDATE ){ /* UPDATE + UPDATE */
+ u8 *a1 = pExist->aRecord;
+ u8 *a2 = aRec;
+ assert( op1==SQLITE_UPDATE );
+ sessionReadRecord(&a1, pTab->nCol, 0);
+ sessionReadRecord(&a2, pTab->nCol, 0);
+ pNew->op = SQLITE_UPDATE;
+ if( 0==sessionMergeUpdate(&aCsr, pTab, aRec, pExist->aRecord, a1, a2) ){
+ sqlite3_free(pNew);
+ pNew = 0;
+ }
+ }else{ /* UPDATE + DELETE */
+ assert( op1==SQLITE_UPDATE && op2==SQLITE_DELETE );
+ pNew->op = SQLITE_DELETE;
+ sessionMergeRecord(&aCsr, pTab->nCol, aRec, pExist->aRecord);
+ }
+
+ if( pNew ){
+ pNew->nRecord = (int)(aCsr - pNew->aRecord);
+ }
+ sqlite3_free(pExist);
+ }
+ }
+
+ *ppNew = pNew;
+ return SQLITE_OK;
+}
+
+/*
+** Add all changes in the changeset passed via the first two arguments to
+** hash tables.
+*/
+static int sessionConcatChangeset(
+ int nChangeset, /* Number of bytes in pChangeset */
+ void *pChangeset, /* Changeset buffer */
+ SessionTable **ppTabList /* IN/OUT: List of table objects */
+){
+ u8 *aRec;
+ int nRec;
+ sqlite3_changeset_iter *pIter;
+ int rc;
+ SessionTable *pTab = 0;
+
+ rc = sqlite3changeset_start(&pIter, nChangeset, pChangeset);
+ if( rc!=SQLITE_OK ) return rc;
+
+ while( SQLITE_ROW==sessionChangesetNext(pIter, &aRec, &nRec) ){
+ const char *zNew;
+ int nCol;
+ int op;
+ int iHash;
+ int bIndirect;
+ SessionChange *pChange;
+ SessionChange *pExist = 0;
+ SessionChange **pp;
+
+ assert( pIter->apValue==0 );
+ sqlite3changeset_op(pIter, &zNew, &nCol, &op, &bIndirect);
+
+ assert( zNew>=(char *)pChangeset && zNew-nChangeset<((char *)pChangeset) );
+ assert( !pTab || pTab->zName-nChangeset<(char *)pChangeset );
+ assert( !pTab || zNew>=pTab->zName );
+
+ if( !pTab || zNew!=pTab->zName ){
+ /* Search the list for a matching table */
+ int nNew = (int)strlen(zNew);
+ u8 *abPK;
+
+ sqlite3changeset_pk(pIter, &abPK, 0);
+ for(pTab = *ppTabList; pTab; pTab=pTab->pNext){
+ if( 0==sqlite3_strnicmp(pTab->zName, zNew, nNew+1) ) break;
+ }
+ if( !pTab ){
+ pTab = sqlite3_malloc(sizeof(SessionTable));
+ if( !pTab ){
+ rc = SQLITE_NOMEM;
+ break;
+ }
+ memset(pTab, 0, sizeof(SessionTable));
+ pTab->pNext = *ppTabList;
+ pTab->abPK = abPK;
+ pTab->nCol = nCol;
+ *ppTabList = pTab;
+ }else if( pTab->nCol!=nCol || memcmp(pTab->abPK, abPK, nCol) ){
+ rc = SQLITE_SCHEMA;
+ break;
+ }
+ pTab->zName = (char *)zNew;
+ }
+
+ if( sessionGrowHash(pTab) ){
+ rc = SQLITE_NOMEM;
+ break;
+ }
+ iHash = sessionChangeHash(pTab, 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){
+ if( sessionChangeEqual(pTab, (*pp)->aRecord, aRec) ){
+ pExist = *pp;
+ *pp = (*pp)->pNext;
+ pTab->nEntry--;
+ break;
+ }
+ }
+
+ rc = sessionChangeMerge(pTab, 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 = sqlite3changeset_finalize(pIter);
+ }else{
+ sqlite3changeset_finalize(pIter);
+ }
+ return rc;
+}
+
+
+/*
+** 1. Iterate through the left-hand changeset. Add an entry to a table
+** specific hash table for each change in the changeset. The hash table
+** key is the PK of the row affected by the change.
+**
+** 2. Then interate through the right-hand changeset. Attempt to add an
+** entry to a hash table for each component change. If a change already
+** exists with the same PK values, combine the two into a single change.
+**
+** 3. Write an output changeset based on the contents of the hash table.
+*/
+int 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) */
+){
+ SessionTable *pList = 0; /* List of SessionTable objects */
+ int rc; /* Return code */
+
+ *pnOut = 0;
+ *ppOut = 0;
+
+ rc = sessionConcatChangeset(nLeft, pLeft, &pList);
+ if( rc==SQLITE_OK ){
+ rc = sessionConcatChangeset(nRight, pRight, &pList);
+ }
+
+ /* Create the serialized output changeset based on the contents of the
+ ** hash tables attached to the SessionTable objects in list pList.
+ */
+ if( rc==SQLITE_OK ){
+ SessionTable *pTab;
+ SessionBuffer buf = {0, 0, 0};
+ for(pTab=pList; pTab; pTab=pTab->pNext){
+ int i;
+ if( pTab->nEntry==0 ) continue;
+
+ sessionAppendTableHdr(&buf, 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 ){
+ *ppOut = buf.aBuf;
+ *pnOut = buf.nBuf;
+ }else{
+ sqlite3_free(buf.aBuf);
+ }
+ }
+
+ sessionDeleteTable(pList);
+ return rc;
+}
+
+#endif /* SQLITE_ENABLE_SESSION && SQLITE_ENABLE_PREUPDATE_HOOK */
diff --git a/ext/session/sqlite3session.h b/ext/session/sqlite3session.h
new file mode 100644
index 000000000..974a770c7
--- /dev/null
+++ b/ext/session/sqlite3session.h
@@ -0,0 +1,918 @@
+
+#ifndef __SQLITESESSION_H_
+#define __SQLITESESSION_H_ 1
+
+/*
+** Make sure we can call this stuff from C++.
+*/
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "sqlite3.h"
+
+/*
+** CAPI3REF: Session Object Handle
+*/
+typedef struct sqlite3_session sqlite3_session;
+
+/*
+** CAPI3REF: Changeset Iterator Handle
+*/
+typedef struct sqlite3_changeset_iter sqlite3_changeset_iter;
+
+/*
+** CAPI3REF: Create A New Session Object
+**
+** Create a new session object attached to database handle db. If successful,
+** a pointer to the new object is written to *ppSession and SQLITE_OK is
+** returned. If an error occurs, *ppSession is set to NULL and an SQLite
+** error code (e.g. SQLITE_NOMEM) is returned.
+**
+** It is possible to create multiple session objects attached to a single
+** database handle.
+**
+** Session objects created using this function should be deleted using the
+** [sqlite3session_delete()] function before the database handle that they
+** are attached to is itself closed. If the database handle is closed before
+** the session object is deleted, then the results of calling any session
+** module function, including [sqlite3session_delete()] on the session object
+** are undefined.
+**
+** Because the session module uses the [sqlite3_preupdate_hook()] API, it
+** is not possible for an application to register a pre-update hook on a
+** database handle that has one or more session objects attached. Nor is
+** it possible to create a session object attached to a database handle for
+** which a pre-update hook is already defined. The results of attempting
+** either of these things are undefined.
+**
+** The session object will be used to create changesets for tables in
+** database zDb, where zDb is either "main", or "temp", or the name of an
+** attached database. It is not an error if database zDb is not attached
+** to the database when the session object is created.
+*/
+int sqlite3session_create(
+ sqlite3 *db, /* Database handle */
+ const char *zDb, /* Name of db (e.g. "main") */
+ sqlite3_session **ppSession /* OUT: New session object */
+);
+
+/*
+** CAPI3REF: Delete A Session Object
+**
+** Delete a session object previously allocated using
+** [sqlite3session_create()]. Once a session object has been deleted, the
+** results of attempting to use pSession with any other session module
+** function are undefined.
+**
+** Session objects must be deleted before the database handle to which they
+** are attached is closed. Refer to the documentation for
+** [sqlite3session_create()] for details.
+*/
+void sqlite3session_delete(sqlite3_session *pSession);
+
+
+/*
+** CAPI3REF: Enable Or Disable A Session Object
+**
+** Enable or disable the recording of changes by a session object. When
+** enabled, a session object records changes made to the database. When
+** disabled - it does not. A newly created session object is enabled.
+** Refer to the documentation for [sqlite3session_changeset()] for further
+** details regarding how enabling and disabling a session object affects
+** the eventual changesets.
+**
+** Passing zero to this function disables the session. Passing a value
+** greater than zero enables it. Passing a value less than zero is a
+** no-op, and may be used to query the current state of the session.
+**
+** The return value indicates the final state of the session object: 0 if
+** the session is disabled, or 1 if it is enabled.
+*/
+int sqlite3session_enable(sqlite3_session *pSession, int bEnable);
+
+/*
+** CAPI3REF: Set Or Clear the Indirect Change Flag
+**
+** Each change recorded by a session object is marked as either direct or
+** indirect. A change is marked as indirect if either:
+**
+** <ul>
+** <li> The session object "indirect" flag is set when the change is
+** made, or
+** <li> The change is made by an SQL trigger or foreign key action
+** instead of directly as a result of a users SQL statement.
+** </ul>
+**
+** If a single row is affected by more than one operation within a session,
+** then the change is considered indirect if all operations meet the criteria
+** for an indirect change above, or direct otherwise.
+**
+** This function is used to set, clear or query the session object indirect
+** flag. If the second argument passed to this function is zero, then the
+** indirect flag is cleared. If it is greater than zero, the indirect flag
+** is set. Passing a value less than zero does not modify the current value
+** of the indirect flag, and may be used to query the current state of the
+** indirect flag for the specified session object.
+**
+** The return value indicates the final state of the indirect flag: 0 if
+** it is clear, or 1 if it is set.
+*/
+int sqlite3session_indirect(sqlite3_session *pSession, int bIndirect);
+
+/*
+** CAPI3REF: Attach A Table To A Session Object
+**
+** If argument zTab is not NULL, then it is the name of a table to attach
+** to the session object passed as the first argument. All subsequent changes
+** made to the table while the session object is enabled will be recorded. See
+** documentation for [sqlite3session_changeset()] for further details.
+**
+** Or, if argument zTab is NULL, then changes are recorded for all tables
+** in the database. If additional tables are added to the database (by
+** executing "CREATE TABLE" statements) after this call is made, changes for
+** the new tables are also recorded.
+**
+** Changes can only be recorded for tables that have a PRIMARY KEY explicitly
+** defined as part of their CREATE TABLE statement. It does not matter if the
+** PRIMARY KEY is an "INTEGER PRIMARY KEY" (rowid alias) or not. The PRIMARY
+** KEY may consist of a single column, or may be a composite key.
+**
+** It is not an error if the named table does not exist in the database. Nor
+** is it an error if the named table does not have a PRIMARY KEY. However,
+** no changes will be recorded in either of these scenarios.
+**
+** Changes are not recorded for individual rows that have NULL values stored
+** in one or more of their PRIMARY KEY columns.
+**
+** SQLITE_OK is returned if the call completes without error. Or, if an error
+** occurs, an SQLite error code (e.g. SQLITE_NOMEM) is returned.
+*/
+int sqlite3session_attach(
+ sqlite3_session *pSession, /* Session object */
+ const char *zTab /* Table name */
+);
+
+/*
+** CAPI3REF: Set a table filter on a Session Object.
+**
+** The second argument (xFilter) is the "filter callback". For changes to rows
+** in tables that are not attached to the Session oject, the filter is called
+** to determine whether changes to the table's rows should be tracked or not.
+** If xFilter returns 0, changes is not tracked. Note that once a table is
+** attached, xFilter will not be called again.
+*/
+void sqlite3session_table_filter(
+ sqlite3_session *pSession, /* Session object */
+ int(*xFilter)(
+ void *pCtx, /* Copy of third arg to _filter_table() */
+ const char *zTab /* Table name */
+ ),
+ void *pCtx /* First argument passed to xFilter */
+);
+
+/*
+** CAPI3REF: Generate A Changeset From A Session Object
+**
+** Obtain a changeset containing changes to the tables attached to the
+** session object passed as the first argument. If successful,
+** set *ppChangeset to point to a buffer containing the changeset
+** and *pnChangeset to the size of the changeset in bytes before returning
+** SQLITE_OK. If an error occurs, set both *ppChangeset and *pnChangeset to
+** zero and return an SQLite error code.
+**
+** A changeset consists of zero or more INSERT, UPDATE and/or DELETE changes,
+** each representing a change to a single row of an attached table. An INSERT
+** change contains the values of each field of a new database row. A DELETE
+** contains the original values of each field of a deleted database row. An
+** UPDATE change contains the original values of each field of an updated
+** database row along with the updated values for each updated non-primary-key
+** column. It is not possible for an UPDATE change to represent a change that
+** modifies the values of primary key columns. If such a change is made, it
+** is represented in a changeset as a DELETE followed by an INSERT.
+**
+** Changes are not recorded for rows that have NULL values stored in one or
+** more of their PRIMARY KEY columns. If such a row is inserted or deleted,
+** no corresponding change is present in the changesets returned by this
+** function. If an existing row with one or more NULL values stored in
+** PRIMARY KEY columns is updated so that all PRIMARY KEY columns are non-NULL,
+** only an INSERT is appears in the changeset. Similarly, if an existing row
+** with non-NULL PRIMARY KEY values is updated so that one or more of its
+** PRIMARY KEY columns are set to NULL, the resulting changeset contains a
+** DELETE change only.
+**
+** The contents of a changeset may be traversed using an iterator created
+** using the [sqlite3changeset_start()] API. A changeset may be applied to
+** a database with a compatible schema using the [sqlite3changeset_apply()]
+** API.
+**
+** Following a successful call to this function, it is the responsibility of
+** the caller to eventually free the buffer that *ppChangeset points to using
+** [sqlite3_free()].
+**
+** <h3>Changeset Generation</h3>
+**
+** Once a table has been attached to a session object, the session object
+** records the primary key values of all new rows inserted into the table.
+** It also records the original primary key and other column values of any
+** deleted or updated rows. For each unique primary key value, data is only
+** recorded once - the first time a row with said primary key is inserted,
+** updated or deleted in the lifetime of the session.
+**
+** There is one exception to the previous paragraph: when a row is inserted,
+** updated or deleted, if one or more of its primary key columns contain a
+** NULL value, no record of the change is made.
+**
+** The session object therefore accumulates two types of records - those
+** that consist of primary key values only (created when the user inserts
+** a new record) and those that consist of the primary key values and the
+** original values of other table columns (created when the users deletes
+** or updates a record).
+**
+** When this function is called, the requested changeset is created using
+** both the accumulated records and the current contents of the database
+** file. Specifically:
+**
+** <ul>
+** <li> For each record generated by an insert, the database is queried
+** for a row with a matching primary key. If one is found, an INSERT
+** change is added to the changeset. If no such row is found, no change
+** is added to the changeset.
+**
+** <li> For each record generated by an update or delete, the database is
+** queried for a row with a matching primary key. If such a row is
+** found and one or more of the non-primary key fields have been
+** modified from their original values, an UPDATE change is added to
+** the changeset. Or, if no such row is found in the table, a DELETE
+** change is added to the changeset. If there is a row with a matching
+** primary key in the database, but all fields contain their original
+** values, no change is added to the changeset.
+** </ul>
+**
+** This means, amongst other things, that if a row is inserted and then later
+** deleted while a session object is active, neither the insert nor the delete
+** will be present in the changeset. Or if a row is deleted and then later a
+** row with the same primary key values inserted while a session object is
+** active, the resulting changeset will contain an UPDATE change instead of
+** a DELETE and an INSERT.
+**
+** When a session object is disabled (see the [sqlite3session_enable()] API),
+** it does not accumulate records when rows are inserted, updated or deleted.
+** This may appear to have some counter-intuitive effects if a single row
+** is written to more than once during a session. For example, if a row
+** is inserted while a session object is enabled, then later deleted while
+** the same session object is disabled, no INSERT record will appear in the
+** changeset, even though the delete took place while the session was disabled.
+** Or, if one field of a row is updated while a session is disabled, and
+** another field of the same row is updated while the session is enabled, the
+** resulting changeset will contain an UPDATE change that updates both fields.
+*/
+int sqlite3session_changeset(
+ sqlite3_session *pSession, /* Session object */
+ int *pnChangeset, /* OUT: Size of buffer at *ppChangeset */
+ void **ppChangeset /* OUT: Buffer containing changeset */
+);
+
+/*
+** CAPI3REF: Test if a changeset has recorded any changes.
+**
+** Return non-zero if no changes to attached tables have been recorded by
+** the session object passed as the first argument. Otherwise, if one or
+** more changes have been recorded, return zero.
+**
+** Even if this function returns zero, it is possible that calling
+** [sqlite3session_changeset()] on the session handle may still return a
+** changeset that contains no changes. This can happen when a row in
+** an attached table is modified and then later on the original values
+** are restored. However, if this function returns non-zero, then it is
+** guaranteed that a call to sqlite3session_changeset() will return a
+** changeset containing zero changes.
+*/
+int sqlite3session_isempty(sqlite3_session *pSession);
+
+/*
+** CAPI3REF: Create An Iterator To Traverse A Changeset
+**
+** Create an iterator used to iterate through the contents of a changeset.
+** If successful, *pp is set to point to the iterator handle and SQLITE_OK
+** is returned. Otherwise, if an error occurs, *pp is set to zero and an
+** SQLite error code is returned.
+**
+** The following functions can be used to advance and query a changeset
+** iterator created by this function:
+**
+** <ul>
+** <li> [sqlite3changeset_next()]
+** <li> [sqlite3changeset_op()]
+** <li> [sqlite3changeset_new()]
+** <li> [sqlite3changeset_old()]
+** </ul>
+**
+** It is the responsibility of the caller to eventually destroy the iterator
+** by passing it to [sqlite3changeset_finalize()]. The buffer containing the
+** changeset (pChangeset) must remain valid until after the iterator is
+** destroyed.
+**
+** Assuming the changeset blob was created by one of the
+** [sqlite3session_changeset()], [sqlite3changeset_concat()] or
+** [sqlite3changeset_invert()] functions, all changes within the changeset
+** that apply to a single table are grouped together. This means that when
+** an application iterates through a changeset using an iterator created by
+** this function, all changes that relate to a single table are visted
+** consecutively. There is no chance that the iterator will visit a change
+** the applies to table X, then one for table Y, and then later on visit
+** another change for table X.
+*/
+int sqlite3changeset_start(
+ sqlite3_changeset_iter **pp, /* OUT: New changeset iterator handle */
+ int nChangeset, /* Size of changeset blob in bytes */
+ void *pChangeset /* Pointer to blob containing changeset */
+);
+
+/*
+** CAPI3REF: Advance A Changeset Iterator
+**
+** This function may only be used with iterators created by function
+** [sqlite3changeset_start()]. If it is called on an iterator passed to
+** a conflict-handler callback by [sqlite3changeset_apply()], SQLITE_MISUSE
+** is returned and the call has no effect.
+**
+** Immediately after an iterator is created by sqlite3changeset_start(), it
+** does not point to any change in the changeset. Assuming the changeset
+** is not empty, the first call to this function advances the iterator to
+** point to the first change in the changeset. Each subsequent call advances
+** the iterator to point to the next change in the changeset (if any). If
+** no error occurs and the iterator points to a valid change after a call
+** to sqlite3changeset_next() has advanced it, SQLITE_ROW is returned.
+** Otherwise, if all changes in the changeset have already been visited,
+** SQLITE_DONE is returned.
+**
+** If an error occurs, an SQLite error code is returned. Possible error
+** codes include SQLITE_CORRUPT (if the changeset buffer is corrupt) or
+** SQLITE_NOMEM.
+*/
+int sqlite3changeset_next(sqlite3_changeset_iter *pIter);
+
+/*
+** CAPI3REF: Obtain The Current Operation From A Changeset Iterator
+**
+** The pIter argument passed to this function may either be an iterator
+** passed to a conflict-handler by [sqlite3changeset_apply()], or an iterator
+** created by [sqlite3changeset_start()]. In the latter case, the most recent
+** call to [sqlite3changeset_next()] must have returned [SQLITE_ROW]. If this
+** is not the case, this function returns [SQLITE_MISUSE].
+**
+** If argument pzTab is not NULL, then *pzTab is set to point to a
+** nul-terminated utf-8 encoded string containing the name of the table
+** affected by the current change. The buffer remains valid until either
+** sqlite3changeset_next() is called on the iterator or until the
+** conflict-handler function returns. If pnCol is not NULL, then *pnCol is
+** set to the number of columns in the table affected by the change. If
+** pbIncorrect is not NULL, then *pbIndirect is set to true (1) if the change
+** is an indirect change, or false (0) otherwise. See the documentation for
+** [sqlite3session_indirect()] for a description of direct and indirect
+** changes. Finally, if pOp is not NULL, then *pOp is set to one of
+** [SQLITE_INSERT], [SQLITE_DELETE] or [SQLITE_UPDATE], depending on the
+** type of change that the iterator currently points to.
+**
+** If no error occurs, SQLITE_OK is returned. If an error does occur, an
+** SQLite error code is returned. The values of the output variables may not
+** be trusted in this case.
+*/
+int sqlite3changeset_op(
+ sqlite3_changeset_iter *pIter, /* Iterator object */
+ const char **pzTab, /* OUT: Pointer to table name */
+ int *pnCol, /* OUT: Number of columns in table */
+ int *pOp, /* OUT: SQLITE_INSERT, DELETE or UPDATE */
+ int *pbIndirect /* OUT: True for an 'indirect' change */
+);
+
+/*
+** CAPI3REF: Obtain The Primary Key Definition Of A Table
+**
+** For each modified table, a changeset includes the following:
+**
+** <ul>
+** <li> The number of columns in the table, and
+** <li> Which of those columns make up the tables PRIMARY KEY.
+** </ul>
+**
+** This function is used to find which columns comprise the PRIMARY KEY of
+** the table modified by the change that iterator pIter currently points to.
+** If successful, *pabPK is set to point to an array of nCol entries, where
+** nCol is the number of columns in the table. Elements of *pabPK are set to
+** 0x01 if the corresponding column is part of the tables primary key, or
+** 0x00 if it is not.
+**
+** If argumet pnCol is not NULL, then *pnCol is set to the number of columns
+** in the table.
+**
+** If this function is called when the iterator does not point to a valid
+** entry, SQLITE_MISUSE is returned and the output variables zeroed. Otherwise,
+** SQLITE_OK is returned and the output variables populated as described
+** above.
+*/
+int sqlite3changeset_pk(
+ sqlite3_changeset_iter *pIter, /* Iterator object */
+ unsigned char **pabPK, /* OUT: Array of boolean - true for PK cols */
+ int *pnCol /* OUT: Number of entries in output array */
+);
+
+/*
+** CAPI3REF: Obtain old.* Values From A Changeset Iterator
+**
+** The pIter argument passed to this function may either be an iterator
+** passed to a conflict-handler by [sqlite3changeset_apply()], or an iterator
+** created by [sqlite3changeset_start()]. In the latter case, the most recent
+** call to [sqlite3changeset_next()] must have returned SQLITE_ROW.
+** Furthermore, it may only be called if the type of change that the iterator
+** currently points to is either [SQLITE_DELETE] or [SQLITE_UPDATE]. Otherwise,
+** this function returns [SQLITE_MISUSE] and sets *ppValue to NULL.
+**
+** Argument iVal must be greater than or equal to 0, and less than the number
+** of columns in the table affected by the current change. Otherwise,
+** [SQLITE_RANGE] is returned and *ppValue is set to NULL.
+**
+** If successful, this function sets *ppValue to point to a protected
+** sqlite3_value object containing the iVal'th value from the vector of
+** original row values stored as part of the UPDATE or DELETE change and
+** returns SQLITE_OK. The name of the function comes from the fact that this
+** is similar to the "old.*" columns available to update or delete triggers.
+**
+** If some other error occurs (e.g. an OOM condition), an SQLite error code
+** is returned and *ppValue is set to NULL.
+*/
+int sqlite3changeset_old(
+ sqlite3_changeset_iter *pIter, /* Changeset iterator */
+ int iVal, /* Column number */
+ sqlite3_value **ppValue /* OUT: Old value (or NULL pointer) */
+);
+
+/*
+** CAPI3REF: Obtain new.* Values From A Changeset Iterator
+**
+** The pIter argument passed to this function may either be an iterator
+** passed to a conflict-handler by [sqlite3changeset_apply()], or an iterator
+** created by [sqlite3changeset_start()]. In the latter case, the most recent
+** call to [sqlite3changeset_next()] must have returned SQLITE_ROW.
+** Furthermore, it may only be called if the type of change that the iterator
+** currently points to is either [SQLITE_UPDATE] or [SQLITE_INSERT]. Otherwise,
+** this function returns [SQLITE_MISUSE] and sets *ppValue to NULL.
+**
+** Argument iVal must be greater than or equal to 0, and less than the number
+** of columns in the table affected by the current change. Otherwise,
+** [SQLITE_RANGE] is returned and *ppValue is set to NULL.
+**
+** If successful, this function sets *ppValue to point to a protected
+** sqlite3_value object containing the iVal'th value from the vector of
+** new row values stored as part of the UPDATE or INSERT change and
+** returns SQLITE_OK. If the change is an UPDATE and does not include
+** a new value for the requested column, *ppValue is set to NULL and
+** SQLITE_OK returned. The name of the function comes from the fact that
+** this is similar to the "new.*" columns available to update or delete
+** triggers.
+**
+** If some other error occurs (e.g. an OOM condition), an SQLite error code
+** is returned and *ppValue is set to NULL.
+*/
+int sqlite3changeset_new(
+ sqlite3_changeset_iter *pIter, /* Changeset iterator */
+ int iVal, /* Column number */
+ sqlite3_value **ppValue /* OUT: New value (or NULL pointer) */
+);
+
+/*
+** CAPI3REF: Obtain Conflicting Row Values From A Changeset Iterator
+**
+** This function should only be used with iterator objects passed to a
+** conflict-handler callback by [sqlite3changeset_apply()] with either
+** [SQLITE_CHANGESET_DATA] or [SQLITE_CHANGESET_CONFLICT]. If this function
+** is called on any other iterator, [SQLITE_MISUSE] is returned and *ppValue
+** is set to NULL.
+**
+** Argument iVal must be greater than or equal to 0, and less than the number
+** of columns in the table affected by the current change. Otherwise,
+** [SQLITE_RANGE] is returned and *ppValue is set to NULL.
+**
+** If successful, this function sets *ppValue to point to a protected
+** sqlite3_value object containing the iVal'th value from the
+** "conflicting row" associated with the current conflict-handler callback
+** and returns SQLITE_OK.
+**
+** If some other error occurs (e.g. an OOM condition), an SQLite error code
+** is returned and *ppValue is set to NULL.
+*/
+int sqlite3changeset_conflict(
+ sqlite3_changeset_iter *pIter, /* Changeset iterator */
+ int iVal, /* Column number */
+ sqlite3_value **ppValue /* OUT: Value from conflicting row */
+);
+
+/*
+** CAPI3REF: Determine The Number Of Foreign Key Constraint Violations
+**
+** This function may only be called with an iterator passed to an
+** SQLITE_CHANGESET_FOREIGN_KEY conflict handler callback. In this case
+** it sets the output variable to the total number of known foreign key
+** violations in the destination database and returns SQLITE_OK.
+**
+** In all other cases this function returns SQLITE_MISUSE.
+*/
+int sqlite3changeset_fk_conflicts(
+ sqlite3_changeset_iter *pIter, /* Changeset iterator */
+ int *pnOut /* OUT: Number of FK violations */
+);
+
+
+/*
+** CAPI3REF: Finalize A Changeset Iterator
+**
+** This function is used to finalize an iterator allocated with
+** [sqlite3changeset_start()].
+**
+** This function should only be called on iterators created using the
+** [sqlite3changeset_start()] function. If an application calls this
+** function with an iterator passed to a conflict-handler by
+** [sqlite3changeset_apply()], [SQLITE_MISUSE] is immediately returned and the
+** call has no effect.
+**
+** If an error was encountered within a call to an sqlite3changeset_xxx()
+** function (for example an [SQLITE_CORRUPT] in [sqlite3changeset_next()] or an
+** [SQLITE_NOMEM] in [sqlite3changeset_new()]) then an error code corresponding
+** to that error is returned by this function. Otherwise, SQLITE_OK is
+** returned. This is to allow the following pattern (pseudo-code):
+**
+** sqlite3changeset_start();
+** while( SQLITE_ROW==sqlite3changeset_next() ){
+** // Do something with change.
+** }
+** rc = sqlite3changeset_finalize();
+** if( rc!=SQLITE_OK ){
+** // An error has occurred
+** }
+*/
+int sqlite3changeset_finalize(sqlite3_changeset_iter *pIter);
+
+/*
+** CAPI3REF: Invert A Changeset
+**
+** This function is used to "invert" a changeset object. Applying an inverted
+** changeset to a database reverses the effects of applying the uninverted
+** changeset. Specifically:
+**
+** <ul>
+** <li> Each DELETE change is changed to an INSERT, and
+** <li> Each INSERT change is changed to a DELETE, and
+** <li> For each UPDATE change, the old.* and new.* values are exchanged.
+** </ul>
+**
+** If successful, a pointer to a buffer containing the inverted changeset
+** is stored in *ppOut, the size of the same buffer is stored in *pnOut, and
+** SQLITE_OK is returned. If an error occurs, both *pnOut and *ppOut are
+** zeroed and an SQLite error code returned.
+**
+** It is the responsibility of the caller to eventually call sqlite3_free()
+** on the *ppOut pointer to free the buffer allocation following a successful
+** call to this function.
+**
+** WARNING/TODO: This function currently assumes that the input is a valid
+** changeset. If it is not, the results are undefined.
+*/
+int sqlite3changeset_invert(
+ int nIn, const void *pIn, /* Input changeset */
+ int *pnOut, void **ppOut /* OUT: Inverse of input */
+);
+
+/*
+** CAPI3REF: Concatenate Two Changeset Objects
+**
+** This function is used to concatenate two changesets, A and B, into a
+** single changeset. The result is a changeset equivalent to applying
+** changeset A followed by changeset B.
+**
+** Rows are identified by the values in their PRIMARY KEY columns. A change
+** in changeset A is considered to apply to the same row as a change in
+** changeset B if the two rows have the same primary key.
+**
+** Changes to rows that appear only in changeset A or B are copied into the
+** output changeset. Or, if both changeset A and B contain a change that
+** applies to a single row, the output depends on the type of each change,
+** as follows:
+**
+** <table border=1 style="margin-left:8ex;margin-right:8ex">
+** <tr><th style="white-space:pre">Change A </th>
+** <th style="white-space:pre">Change B </th>
+** <th>Output Change
+** <tr><td>INSERT <td>INSERT <td>
+** Change A is copied into the output changeset. Change B is discarded.
+** This case does not occur if changeset B is recorded immediately after
+** changeset A.
+** <tr><td>INSERT <td>UPDATE <td>
+** An INSERT change is copied into the output changeset. The values in
+** the INSERT change are as if the row was inserted by change A and then
+** updated according to change B.
+** <tr><td>INSERT <td>DELETE <td>
+** No change at all is copied into the output changeset.
+** <tr><td>UPDATE <td>INSERT <td>
+** Change A is copied into the output changeset. Change B is discarded.
+** This case does not occur if changeset B is recorded immediately after
+** changeset A.
+** <tr><td>UPDATE <td>UPDATE <td>
+** A single UPDATE is copied into the output changeset. The accompanying
+** values are as if the row was updated once by change A and then again
+** by change B.
+** <tr><td>UPDATE <td>DELETE <td>
+** A single DELETE is copied into the output changeset.
+** <tr><td>DELETE <td>INSERT <td>
+** If one or more of the column values in the row inserted by change
+** B differ from those in the row deleted by change A, an UPDATE
+** change is added to the output changeset. Otherwise, if the inserted
+** row is exactly the same as the deleted row, no change is added to
+** the output changeset.
+** <tr><td>DELETE <td>UPDATE <td>
+** Change A is copied into the output changeset. Change B is discarded.
+** This case does not occur if changeset B is recorded immediately after
+** changeset A.
+** <tr><td>DELETE <td>DELETE <td>
+** Change A is copied into the output changeset. Change B is discarded.
+** This case does not occur if changeset B is recorded immediately after
+** changeset A.
+** </table>
+**
+** If the two changesets contain changes to the same table, then the number
+** of columns and the position of the primary key columns for the table must
+** be the same in each changeset. If this is not the case, attempting to
+** concatenate the two changesets together fails and this function returns
+** SQLITE_SCHEMA. If either of the two input changesets appear to be corrupt,
+** and the corruption is detected, SQLITE_CORRUPT is returned. Or, if an
+** out-of-memory condition occurs during processing, this function returns
+** SQLITE_NOMEM.
+**
+** If none of the above errors occur, SQLITE_OK is returned and *ppOut set
+** to point to a buffer containing the output changeset. It is the
+** responsibility of the caller to eventually call sqlite3_free() on *ppOut
+** to release memory allocated for the buffer. *pnOut is set to the number
+** of bytes in the output changeset. If an error does occur, both *ppOut and
+** *pnOut are set to zero before returning.
+*/
+int sqlite3changeset_concat(
+ int nA, /* Number of bytes in buffer pA */
+ void *pA, /* Pointer to buffer containing changeset A */
+ int nB, /* Number of bytes in buffer pB */
+ void *pB, /* Pointer to buffer containing changeset B */
+ int *pnOut, /* OUT: Number of bytes in output changeset */
+ void **ppOut /* OUT: Buffer containing output changeset */
+);
+
+/*
+** CAPI3REF: Apply A Changeset To A Database
+**
+** Apply a changeset to a database. This function attempts to update the
+** "main" database attached to handle db with the changes found in the
+** changeset passed via the second and third arguments.
+**
+** The fourth argument (xFilter) passed to this function is the "filter
+** callback". If it is not NULL, then for each table affected by at least one
+** change in the changeset, the filter callback is invoked with
+** the table name as the second argument, and a copy of the context pointer
+** passed as the sixth argument to this function as the first. If the "filter
+** callback" returns zero, then no attempt is made to apply any changes to
+** the table. Otherwise, if the return value is non-zero or the xFilter
+** argument to this function is NULL, all changes related to the table are
+** attempted.
+**
+** For each table that is not excluded by the filter callback, this function
+** tests that the target database contains a compatible table. A table is
+** considered compatible if all of the following are true:
+**
+** <ul>
+** <li> The table has the same name as the name recorded in the
+** changeset, and
+** <li> The table has the same number of columns as recorded in the
+** changeset, and
+** <li> The table has primary key columns in the same position as
+** recorded in the changeset.
+** </ul>
+**
+** If there is no compatible table, it is not an error, but none of the
+** changes associated with the table are applied. A warning message is issued
+** via the sqlite3_log() mechanism with the error code SQLITE_SCHEMA. At most
+** one such warning is issued for each table in the changeset.
+**
+** For each change for which there is a compatible table, an attempt is made
+** to modify the table contents according to the UPDATE, INSERT or DELETE
+** change. If a change cannot be applied cleanly, the conflict handler
+** function passed as the fifth argument to sqlite3changeset_apply() may be
+** invoked. A description of exactly when the conflict handler is invoked for
+** each type of change is below.
+**
+** Each time the conflict handler function is invoked, it must return one
+** of [SQLITE_CHANGESET_OMIT], [SQLITE_CHANGESET_ABORT] or
+** [SQLITE_CHANGESET_REPLACE]. SQLITE_CHANGESET_REPLACE may only be returned
+** if the second argument passed to the conflict handler is either
+** SQLITE_CHANGESET_DATA or SQLITE_CHANGESET_CONFLICT. If the conflict-handler
+** returns an illegal value, any changes already made are rolled back and
+** the call to sqlite3changeset_apply() returns SQLITE_MISUSE. Different
+** actions are taken by sqlite3changeset_apply() depending on the value
+** returned by each invocation of the conflict-handler function. Refer to
+** the documentation for the three
+** [SQLITE_CHANGESET_OMIT|available return values] for details.
+**
+** <dl>
+** <dt>DELETE Changes<dd>
+** For each DELETE change, this function checks if the target database
+** contains a row with the same primary key value (or values) as the
+** original row values stored in the changeset. If it does, and the values
+** stored in all non-primary key columns also match the values stored in
+** the changeset the row is deleted from the target database.
+**
+** If a row with matching primary key values is found, but one or more of
+** the non-primary key fields contains a value different from the original
+** row value stored in the changeset, the conflict-handler function is
+** invoked with [SQLITE_CHANGESET_DATA] as the second argument.
+**
+** If no row with matching primary key values is found in the database,
+** the conflict-handler function is invoked with [SQLITE_CHANGESET_NOTFOUND]
+** passed as the second argument.
+**
+** If the DELETE operation is attempted, but SQLite returns SQLITE_CONSTRAINT
+** (which can only happen if a foreign key constraint is violated), the
+** conflict-handler function is invoked with [SQLITE_CHANGESET_CONSTRAINT]
+** passed as the second argument. This includes the case where the DELETE
+** operation is attempted because an earlier call to the conflict handler
+** function returned [SQLITE_CHANGESET_REPLACE].
+**
+** <dt>INSERT Changes<dd>
+** For each INSERT change, an attempt is made to insert the new row into
+** the database.
+**
+** If the attempt to insert the row fails because the database already
+** contains a row with the same primary key values, the conflict handler
+** function is invoked with the second argument set to
+** [SQLITE_CHANGESET_CONFLICT].
+**
+** If the attempt to insert the row fails because of some other constraint
+** violation (e.g. NOT NULL or UNIQUE), the conflict handler function is
+** invoked with the second argument set to [SQLITE_CHANGESET_CONSTRAINT].
+** This includes the case where the INSERT operation is re-attempted because
+** an earlier call to the conflict handler function returned
+** [SQLITE_CHANGESET_REPLACE].
+**
+** <dt>UPDATE Changes<dd>
+** For each UPDATE change, this function checks if the target database
+** contains a row with the same primary key value (or values) as the
+** original row values stored in the changeset. If it does, and the values
+** stored in all non-primary key columns also match the values stored in
+** the changeset the row is updated within the target database.
+**
+** If a row with matching primary key values is found, but one or more of
+** the non-primary key fields contains a value different from an original
+** row value stored in the changeset, the conflict-handler function is
+** invoked with [SQLITE_CHANGESET_DATA] as the second argument. Since
+** UPDATE changes only contain values for non-primary key fields that are
+** to be modified, only those fields need to match the original values to
+** avoid the SQLITE_CHANGESET_DATA conflict-handler callback.
+**
+** If no row with matching primary key values is found in the database,
+** the conflict-handler function is invoked with [SQLITE_CHANGESET_NOTFOUND]
+** passed as the second argument.
+**
+** If the UPDATE operation is attempted, but SQLite returns
+** SQLITE_CONSTRAINT, the conflict-handler function is invoked with
+** [SQLITE_CHANGESET_CONSTRAINT] passed as the second argument.
+** This includes the case where the UPDATE operation is attempted after
+** an earlier call to the conflict handler function returned
+** [SQLITE_CHANGESET_REPLACE].
+** </dl>
+**
+** It is safe to execute SQL statements, including those that write to the
+** table that the callback related to, from within the xConflict callback.
+** This can be used to further customize the applications conflict
+** resolution strategy.
+**
+** All changes made by this function are enclosed in a savepoint transaction.
+** If any other error (aside from a constraint failure when attempting to
+** write to the target database) occurs, then the savepoint transaction is
+** rolled back, restoring the target database to its original state, and an
+** SQLite error code returned.
+*/
+int sqlite3changeset_apply(
+ sqlite3 *db, /* Apply change to "main" db of this handle */
+ int nChangeset, /* Size of changeset in bytes */
+ void *pChangeset, /* Changeset blob */
+ int(*xFilter)(
+ void *pCtx, /* Copy of sixth arg to _apply() */
+ const char *zTab /* Table name */
+ ),
+ int(*xConflict)(
+ void *pCtx, /* Copy of sixth arg to _apply() */
+ int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */
+ sqlite3_changeset_iter *p /* Handle describing change and conflict */
+ ),
+ void *pCtx /* First argument passed to xConflict */
+);
+
+/*
+** CAPI3REF: Constants Passed To The Conflict Handler
+**
+** Values that may be passed as the second argument to a conflict-handler.
+**
+** <dl>
+** <dt>SQLITE_CHANGESET_DATA<dd>
+** The conflict handler is invoked with CHANGESET_DATA as the second argument
+** when processing a DELETE or UPDATE change if a row with the required
+** PRIMARY KEY fields is present in the database, but one or more other
+** (non primary-key) fields modified by the update do not contain the
+** expected "before" values.
+**
+** The conflicting row, in this case, is the database row with the matching
+** primary key.
+**
+** <dt>SQLITE_CHANGESET_NOTFOUND<dd>
+** The conflict handler is invoked with CHANGESET_NOTFOUND as the second
+** argument when processing a DELETE or UPDATE change if a row with the
+** required PRIMARY KEY fields is not present in the database.
+**
+** There is no conflicting row in this case. The results of invoking the
+** sqlite3changeset_conflict() API are undefined.
+**
+** <dt>SQLITE_CHANGESET_CONFLICT<dd>
+** CHANGESET_CONFLICT is passed as the second argument to the conflict
+** handler while processing an INSERT change if the operation would result
+** in duplicate primary key values.
+**
+** The conflicting row in this case is the database row with the matching
+** primary key.
+**
+** <dt>SQLITE_CHANGESET_FOREIGN_KEY<dd>
+** If foreign key handling is enabled, and applying a changeset leaves the
+** database in a state containing foreign key violations, the conflict
+** handler is invoked with CHANGESET_FOREIGN_KEY as the second argument
+** exactly once before the changeset is committed. If the conflict handler
+** returns CHANGESET_OMIT, the changes, including those that caused the
+** foreign key constraint violation, are committed. Or, if it returns
+** CHANGESET_ABORT, the changeset is rolled back.
+**
+** No current or conflicting row information is provided. The only function
+** it is possible to call on the supplied sqlite3_changeset_iter handle
+** is sqlite3changeset_fk_conflicts().
+**
+** <dt>SQLITE_CHANGESET_CONSTRAINT<dd>
+** If any other constraint violation occurs while applying a change (i.e.
+** a UNIQUE, CHECK or NOT NULL constraint), the conflict handler is
+** invoked with CHANGESET_CONSTRAINT as the second argument.
+**
+** There is no conflicting row in this case. The results of invoking the
+** sqlite3changeset_conflict() API are undefined.
+**
+** </dl>
+*/
+#define SQLITE_CHANGESET_DATA 1
+#define SQLITE_CHANGESET_NOTFOUND 2
+#define SQLITE_CHANGESET_CONFLICT 3
+#define SQLITE_CHANGESET_CONSTRAINT 4
+#define SQLITE_CHANGESET_FOREIGN_KEY 5
+
+/*
+** CAPI3REF: Constants Returned By The Conflict Handler
+**
+** A conflict handler callback must return one of the following three values.
+**
+** <dl>
+** <dt>SQLITE_CHANGESET_OMIT<dd>
+** If a conflict handler returns this value no special action is taken. The
+** change that caused the conflict is not applied. The session module
+** continues to the next change in the changeset.
+**
+** <dt>SQLITE_CHANGESET_REPLACE<dd>
+** This value may only be returned if the second argument to the conflict
+** handler was SQLITE_CHANGESET_DATA or SQLITE_CHANGESET_CONFLICT. If this
+** is not the case, any changes applied so far are rolled back and the
+** call to sqlite3changeset_apply() returns SQLITE_MISUSE.
+**
+** If CHANGESET_REPLACE is returned by an SQLITE_CHANGESET_DATA conflict
+** handler, then the conflicting row is either updated or deleted, depending
+** on the type of change.
+**
+** If CHANGESET_REPLACE is returned by an SQLITE_CHANGESET_CONFLICT conflict
+** handler, then the conflicting row is removed from the database and a
+** second attempt to apply the change is made. If this second attempt fails,
+** the original row is restored to the database before continuing.
+**
+** <dt>SQLITE_CHANGESET_ABORT<dd>
+** If this value is returned, any changes applied so far are rolled back
+** and the call to sqlite3changeset_apply() returns SQLITE_ABORT.
+** </dl>
+*/
+#define SQLITE_CHANGESET_OMIT 0
+#define SQLITE_CHANGESET_REPLACE 1
+#define SQLITE_CHANGESET_ABORT 2
+
+/*
+** Make sure we can call this stuff from C++.
+*/
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SQLITE_ENABLE_SESSION && SQLITE_ENABLE_PREUPDATE_HOOK */
diff --git a/ext/session/test_session.c b/ext/session/test_session.c
new file mode 100644
index 000000000..f1c2fbe9a
--- /dev/null
+++ b/ext/session/test_session.c
@@ -0,0 +1,674 @@
+
+#if defined(SQLITE_TEST) && defined(SQLITE_ENABLE_SESSION) \
+ && defined(SQLITE_ENABLE_PREUPDATE_HOOK)
+
+#include "sqlite3session.h"
+#include <assert.h>
+#include <string.h>
+#include <tcl.h>
+
+typedef struct TestSession TestSession;
+struct TestSession {
+ sqlite3_session *pSession;
+ Tcl_Interp *interp;
+ Tcl_Obj *pFilterScript;
+};
+
+static int test_session_error(Tcl_Interp *interp, int rc){
+ extern const char *sqlite3ErrName(int);
+ Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1));
+ return TCL_ERROR;
+}
+
+static int test_table_filter(void *pCtx, const char *zTbl){
+ TestSession *p = (TestSession*)pCtx;
+ Tcl_Obj *pEval;
+ int rc;
+ int bRes = 0;
+
+ pEval = Tcl_DuplicateObj(p->pFilterScript);
+ Tcl_IncrRefCount(pEval);
+ rc = Tcl_ListObjAppendElement(p->interp, pEval, Tcl_NewStringObj(zTbl, -1));
+ if( rc==TCL_OK ){
+ rc = Tcl_EvalObjEx(p->interp, pEval, TCL_EVAL_GLOBAL);
+ }
+ if( rc==TCL_OK ){
+ rc = Tcl_GetBooleanFromObj(p->interp, Tcl_GetObjResult(p->interp), &bRes);
+ }
+ if( rc!=TCL_OK ){
+ /* printf("error: %s\n", Tcl_GetStringResult(p->interp)); */
+ Tcl_BackgroundError(p->interp);
+ }
+ Tcl_DecrRefCount(pEval);
+
+ return bRes;
+}
+
+/*
+** Tclcmd: $session attach TABLE
+** $session changeset
+** $session delete
+** $session enable BOOL
+** $session indirect INTEGER
+** $session table_filter SCRIPT
+*/
+static int test_session_cmd(
+ void *clientData,
+ Tcl_Interp *interp,
+ int objc,
+ Tcl_Obj *CONST objv[]
+){
+ TestSession *p = (TestSession*)clientData;
+ sqlite3_session *pSession = p->pSession;
+ struct SessionSubcmd {
+ const char *zSub;
+ int nArg;
+ const char *zMsg;
+ int iSub;
+ } aSub[] = {
+ { "attach", 1, "TABLE", }, /* 0 */
+ { "changeset", 0, "", }, /* 1 */
+ { "delete", 0, "", }, /* 2 */
+ { "enable", 1, "BOOL", }, /* 3 */
+ { "indirect", 1, "BOOL", }, /* 4 */
+ { "isempty", 0, "", }, /* 5 */
+ { "table_filter", 1, "SCRIPT", }, /* 6 */
+ { 0 }
+ };
+ int iSub;
+ int rc;
+
+ if( objc<2 ){
+ Tcl_WrongNumArgs(interp, 1, objv, "SUBCOMMAND ...");
+ return TCL_ERROR;
+ }
+ rc = Tcl_GetIndexFromObjStruct(interp,
+ objv[1], aSub, sizeof(aSub[0]), "sub-command", 0, &iSub
+ );
+ if( rc!=TCL_OK ) return rc;
+ if( objc!=2+aSub[iSub].nArg ){
+ Tcl_WrongNumArgs(interp, 2, objv, aSub[iSub].zMsg);
+ return TCL_ERROR;
+ }
+
+ switch( iSub ){
+ case 0: { /* attach */
+ char *zArg = Tcl_GetString(objv[2]);
+ if( zArg[0]=='*' && zArg[1]=='\0' ) zArg = 0;
+ rc = sqlite3session_attach(pSession, zArg);
+ if( rc!=SQLITE_OK ){
+ return test_session_error(interp, rc);
+ }
+ break;
+ }
+
+ case 1: { /* changeset */
+ int nChange;
+ void *pChange;
+ rc = sqlite3session_changeset(pSession, &nChange, &pChange);
+ if( rc==SQLITE_OK ){
+ Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(pChange, nChange));
+ sqlite3_free(pChange);
+ }else{
+ return test_session_error(interp, rc);
+ }
+ break;
+ }
+
+ case 2: /* delete */
+ Tcl_DeleteCommand(interp, Tcl_GetString(objv[0]));
+ break;
+
+ case 3: { /* enable */
+ int val;
+ if( Tcl_GetIntFromObj(interp, objv[2], &val) ) return TCL_ERROR;
+ val = sqlite3session_enable(pSession, val);
+ Tcl_SetObjResult(interp, Tcl_NewBooleanObj(val));
+ break;
+ }
+
+ case 4: { /* indirect */
+ int val;
+ if( Tcl_GetIntFromObj(interp, objv[2], &val) ) return TCL_ERROR;
+ val = sqlite3session_indirect(pSession, val);
+ Tcl_SetObjResult(interp, Tcl_NewBooleanObj(val));
+ break;
+ }
+
+ case 5: { /* isempty */
+ int val;
+ val = sqlite3session_isempty(pSession);
+ Tcl_SetObjResult(interp, Tcl_NewBooleanObj(val));
+ break;
+ }
+
+ case 6: { /* table_filter */
+ if( p->pFilterScript ) Tcl_DecrRefCount(p->pFilterScript);
+ p->interp = interp;
+ p->pFilterScript = Tcl_DuplicateObj(objv[2]);
+ Tcl_IncrRefCount(p->pFilterScript);
+ sqlite3session_table_filter(pSession, test_table_filter, clientData);
+ break;
+ }
+ }
+
+ return TCL_OK;
+}
+
+static void test_session_del(void *clientData){
+ TestSession *p = (TestSession*)clientData;
+ if( p->pFilterScript ) Tcl_DecrRefCount(p->pFilterScript);
+ sqlite3session_delete(p->pSession);
+ ckfree((char*)p);
+}
+
+/*
+** Tclcmd: sqlite3session CMD DB-HANDLE DB-NAME
+*/
+static int test_sqlite3session(
+ void * clientData,
+ Tcl_Interp *interp,
+ int objc,
+ Tcl_Obj *CONST objv[]
+){
+ sqlite3 *db;
+ Tcl_CmdInfo info;
+ int rc; /* sqlite3session_create() return code */
+ TestSession *p; /* New wrapper object */
+
+ if( objc!=4 ){
+ Tcl_WrongNumArgs(interp, 1, objv, "CMD DB-HANDLE DB-NAME");
+ return TCL_ERROR;
+ }
+
+ if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(objv[2]), &info) ){
+ Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(objv[2]), 0);
+ return TCL_ERROR;
+ }
+ db = *(sqlite3 **)info.objClientData;
+
+ p = (TestSession*)ckalloc(sizeof(TestSession));
+ memset(p, 0, sizeof(TestSession));
+ rc = sqlite3session_create(db, Tcl_GetString(objv[3]), &p->pSession);
+ if( rc!=SQLITE_OK ){
+ ckfree((char*)p);
+ return test_session_error(interp, rc);
+ }
+
+ Tcl_CreateObjCommand(
+ interp, Tcl_GetString(objv[1]), test_session_cmd, (ClientData)p,
+ test_session_del
+ );
+ Tcl_SetObjResult(interp, objv[1]);
+ return TCL_OK;
+}
+
+static void test_append_value(Tcl_Obj *pList, sqlite3_value *pVal){
+ if( pVal==0 ){
+ Tcl_ListObjAppendElement(0, pList, Tcl_NewObj());
+ Tcl_ListObjAppendElement(0, pList, Tcl_NewObj());
+ }else{
+ Tcl_Obj *pObj;
+ switch( sqlite3_value_type(pVal) ){
+ case SQLITE_NULL:
+ Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("n", 1));
+ pObj = Tcl_NewObj();
+ break;
+ case SQLITE_INTEGER:
+ Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("i", 1));
+ pObj = Tcl_NewWideIntObj(sqlite3_value_int64(pVal));
+ break;
+ case SQLITE_FLOAT:
+ Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("f", 1));
+ pObj = Tcl_NewDoubleObj(sqlite3_value_double(pVal));
+ break;
+ case SQLITE_TEXT: {
+ const char *z = (char*)sqlite3_value_blob(pVal);
+ int n = sqlite3_value_bytes(pVal);
+ Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("t", 1));
+ pObj = Tcl_NewStringObj(z, n);
+ break;
+ }
+ case SQLITE_BLOB:
+ Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("b", 1));
+ pObj = Tcl_NewByteArrayObj(
+ sqlite3_value_blob(pVal),
+ sqlite3_value_bytes(pVal)
+ );
+ break;
+ }
+ Tcl_ListObjAppendElement(0, pList, pObj);
+ }
+}
+
+typedef struct TestConflictHandler TestConflictHandler;
+struct TestConflictHandler {
+ Tcl_Interp *interp;
+ Tcl_Obj *pConflictScript;
+ Tcl_Obj *pFilterScript;
+};
+
+static int test_obj_eq_string(Tcl_Obj *p, const char *z){
+ int n;
+ int nObj;
+ char *zObj;
+
+ n = (int)strlen(z);
+ zObj = Tcl_GetStringFromObj(p, &nObj);
+
+ return (nObj==n && (n==0 || 0==memcmp(zObj, z, n)));
+}
+
+static int test_filter_handler(
+ void *pCtx, /* Pointer to TestConflictHandler structure */
+ const char *zTab /* Table name */
+){
+ TestConflictHandler *p = (TestConflictHandler *)pCtx;
+ int res = 1;
+ Tcl_Obj *pEval;
+ Tcl_Interp *interp = p->interp;
+
+ pEval = Tcl_DuplicateObj(p->pFilterScript);
+ Tcl_IncrRefCount(pEval);
+
+ if( TCL_OK!=Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj(zTab, -1))
+ || TCL_OK!=Tcl_EvalObjEx(interp, pEval, TCL_EVAL_GLOBAL)
+ || TCL_OK!=Tcl_GetIntFromObj(interp, Tcl_GetObjResult(interp), &res)
+ ){
+ Tcl_BackgroundError(interp);
+ }
+
+ Tcl_DecrRefCount(pEval);
+ return res;
+}
+
+static int test_conflict_handler(
+ void *pCtx, /* Pointer to TestConflictHandler structure */
+ int eConf, /* DATA, MISSING, CONFLICT, CONSTRAINT */
+ sqlite3_changeset_iter *pIter /* Handle describing change and conflict */
+){
+ TestConflictHandler *p = (TestConflictHandler *)pCtx;
+ Tcl_Obj *pEval;
+ Tcl_Interp *interp = p->interp;
+ int ret = 0; /* Return value */
+
+ int op; /* SQLITE_UPDATE, DELETE or INSERT */
+ const char *zTab; /* Name of table conflict is on */
+ int nCol; /* Number of columns in table zTab */
+
+ pEval = Tcl_DuplicateObj(p->pConflictScript);
+ Tcl_IncrRefCount(pEval);
+
+ sqlite3changeset_op(pIter, &zTab, &nCol, &op, 0);
+
+ if( eConf==SQLITE_CHANGESET_FOREIGN_KEY ){
+ int nFk;
+ sqlite3changeset_fk_conflicts(pIter, &nFk);
+ Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj("FOREIGN_KEY", -1));
+ Tcl_ListObjAppendElement(0, pEval, Tcl_NewIntObj(nFk));
+ }else{
+
+ /* Append the operation type. */
+ Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj(
+ op==SQLITE_INSERT ? "INSERT" :
+ op==SQLITE_UPDATE ? "UPDATE" :
+ "DELETE", -1
+ ));
+
+ /* Append the table name. */
+ Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj(zTab, -1));
+
+ /* Append the conflict type. */
+ switch( eConf ){
+ case SQLITE_CHANGESET_DATA:
+ Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("DATA",-1));
+ break;
+ case SQLITE_CHANGESET_NOTFOUND:
+ Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("NOTFOUND",-1));
+ break;
+ case SQLITE_CHANGESET_CONFLICT:
+ Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("CONFLICT",-1));
+ break;
+ case SQLITE_CHANGESET_CONSTRAINT:
+ Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("CONSTRAINT",-1));
+ break;
+ }
+
+ /* If this is not an INSERT, append the old row */
+ if( op!=SQLITE_INSERT ){
+ int i;
+ Tcl_Obj *pOld = Tcl_NewObj();
+ for(i=0; i<nCol; i++){
+ sqlite3_value *pVal;
+ sqlite3changeset_old(pIter, i, &pVal);
+ test_append_value(pOld, pVal);
+ }
+ Tcl_ListObjAppendElement(0, pEval, pOld);
+ }
+
+ /* If this is not a DELETE, append the new row */
+ if( op!=SQLITE_DELETE ){
+ int i;
+ Tcl_Obj *pNew = Tcl_NewObj();
+ for(i=0; i<nCol; i++){
+ sqlite3_value *pVal;
+ sqlite3changeset_new(pIter, i, &pVal);
+ test_append_value(pNew, pVal);
+ }
+ Tcl_ListObjAppendElement(0, pEval, pNew);
+ }
+
+ /* If this is a CHANGESET_DATA or CHANGESET_CONFLICT conflict, append
+ ** the conflicting row. */
+ if( eConf==SQLITE_CHANGESET_DATA || eConf==SQLITE_CHANGESET_CONFLICT ){
+ int i;
+ Tcl_Obj *pConflict = Tcl_NewObj();
+ for(i=0; i<nCol; i++){
+ int rc;
+ sqlite3_value *pVal;
+ rc = sqlite3changeset_conflict(pIter, i, &pVal);
+ assert( rc==SQLITE_OK );
+ test_append_value(pConflict, pVal);
+ }
+ Tcl_ListObjAppendElement(0, pEval, pConflict);
+ }
+
+ /***********************************************************************
+ ** This block is purely for testing some error conditions.
+ */
+ if( eConf==SQLITE_CHANGESET_CONSTRAINT
+ || eConf==SQLITE_CHANGESET_NOTFOUND
+ ){
+ sqlite3_value *pVal;
+ int rc = sqlite3changeset_conflict(pIter, 0, &pVal);
+ assert( rc==SQLITE_MISUSE );
+ }else{
+ sqlite3_value *pVal;
+ int rc = sqlite3changeset_conflict(pIter, -1, &pVal);
+ assert( rc==SQLITE_RANGE );
+ rc = sqlite3changeset_conflict(pIter, nCol, &pVal);
+ assert( rc==SQLITE_RANGE );
+ }
+ if( op==SQLITE_DELETE ){
+ sqlite3_value *pVal;
+ int rc = sqlite3changeset_new(pIter, 0, &pVal);
+ assert( rc==SQLITE_MISUSE );
+ }else{
+ sqlite3_value *pVal;
+ int rc = sqlite3changeset_new(pIter, -1, &pVal);
+ assert( rc==SQLITE_RANGE );
+ rc = sqlite3changeset_new(pIter, nCol, &pVal);
+ assert( rc==SQLITE_RANGE );
+ }
+ if( op==SQLITE_INSERT ){
+ sqlite3_value *pVal;
+ int rc = sqlite3changeset_old(pIter, 0, &pVal);
+ assert( rc==SQLITE_MISUSE );
+ }else{
+ sqlite3_value *pVal;
+ int rc = sqlite3changeset_old(pIter, -1, &pVal);
+ assert( rc==SQLITE_RANGE );
+ rc = sqlite3changeset_old(pIter, nCol, &pVal);
+ assert( rc==SQLITE_RANGE );
+ }
+ /* 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;
+}
+
+/*
+** sqlite3changeset_apply DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT?
+*/
+static int test_sqlite3changeset_apply(
+ void * clientData,
+ Tcl_Interp *interp,
+ int objc,
+ Tcl_Obj *CONST objv[]
+){
+ sqlite3 *db; /* Database handle */
+ Tcl_CmdInfo info; /* Database Tcl command (objv[1]) info */
+ int rc; /* Return code from changeset_invert() */
+ void *pChangeset; /* Buffer containing changeset */
+ int nChangeset; /* Size of buffer aChangeset in bytes */
+ TestConflictHandler ctx;
+
+ if( objc!=4 && objc!=5 ){
+ Tcl_WrongNumArgs(interp, 1, objv,
+ "DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT?"
+ );
+ return TCL_ERROR;
+ }
+ if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(objv[1]), &info) ){
+ Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(objv[2]), 0);
+ return TCL_ERROR;
+ }
+ db = *(sqlite3 **)info.objClientData;
+ pChangeset = (void *)Tcl_GetByteArrayFromObj(objv[2], &nChangeset);
+ ctx.pConflictScript = objv[3];
+ ctx.pFilterScript = objc==5 ? objv[4] : 0;
+ ctx.interp = interp;
+
+ rc = sqlite3changeset_apply(db, nChangeset, pChangeset,
+ (objc==5) ? test_filter_handler : 0, test_conflict_handler, (void *)&ctx
+ );
+ if( rc!=SQLITE_OK ){
+ return test_session_error(interp, rc);
+ }
+ Tcl_ResetResult(interp);
+ return TCL_OK;
+}
+
+/*
+** sqlite3changeset_invert CHANGESET
+*/
+static int test_sqlite3changeset_invert(
+ void * clientData,
+ Tcl_Interp *interp,
+ int objc,
+ Tcl_Obj *CONST objv[]
+){
+ int rc; /* Return code from changeset_invert() */
+ void *aChangeset; /* Input changeset */
+ int nChangeSet; /* Size of buffer aChangeset in bytes */
+ void *aOut; /* Output changeset */
+ int nOut; /* Size of buffer aOut in bytes */
+
+ if( objc!=2 ){
+ Tcl_WrongNumArgs(interp, 1, objv, "CHANGESET");
+ return TCL_ERROR;
+ }
+ aChangeset = (void *)Tcl_GetByteArrayFromObj(objv[1], &nChangeSet);
+
+ rc = sqlite3changeset_invert(nChangeSet, aChangeset, &nOut, &aOut);
+ if( rc!=SQLITE_OK ){
+ return test_session_error(interp, rc);
+ }
+ Tcl_SetObjResult(interp, Tcl_NewByteArrayObj((unsigned char *)aOut, nOut));
+ sqlite3_free(aOut);
+ return TCL_OK;
+}
+
+/*
+** sqlite3changeset_concat LEFT RIGHT
+*/
+static int test_sqlite3changeset_concat(
+ void * clientData,
+ Tcl_Interp *interp,
+ int objc,
+ Tcl_Obj *CONST objv[]
+){
+ int rc; /* Return code from changeset_invert() */
+ void *aLeft; /* Input changeset */
+ int nLeft; /* Size of buffer aChangeset in bytes */
+ void *aRight; /* Input changeset */
+ int nRight; /* Size of buffer aChangeset in bytes */
+ void *aOut; /* Output changeset */
+ int nOut; /* Size of buffer aOut in bytes */
+
+ if( objc!=3 ){
+ Tcl_WrongNumArgs(interp, 1, objv, "LEFT RIGHT");
+ return TCL_ERROR;
+ }
+ aLeft = (void *)Tcl_GetByteArrayFromObj(objv[1], &nLeft);
+ aRight = (void *)Tcl_GetByteArrayFromObj(objv[2], &nRight);
+
+ rc = sqlite3changeset_concat(nLeft, aLeft, nRight, aRight, &nOut, &aOut);
+ if( rc!=SQLITE_OK ){
+ return test_session_error(interp, rc);
+ }
+ Tcl_SetObjResult(interp, Tcl_NewByteArrayObj((unsigned char *)aOut, nOut));
+ sqlite3_free(aOut);
+ return TCL_OK;
+}
+
+/*
+** sqlite3session_foreach VARNAME CHANGESET SCRIPT
+*/
+static int test_sqlite3session_foreach(
+ void * clientData,
+ Tcl_Interp *interp,
+ int objc,
+ Tcl_Obj *CONST objv[]
+){
+ void *pChangeSet;
+ int nChangeSet;
+ sqlite3_changeset_iter *pIter;
+ int rc;
+ Tcl_Obj *pVarname;
+ Tcl_Obj *pCS;
+ Tcl_Obj *pScript;
+ int isCheckNext = 0;
+
+ if( objc>1 ){
+ char *zOpt = Tcl_GetString(objv[1]);
+ isCheckNext = (strcmp(zOpt, "-next")==0);
+ }
+ if( objc!=4+isCheckNext ){
+ Tcl_WrongNumArgs(interp, 1, objv, "?-next? VARNAME CHANGESET SCRIPT");
+ return TCL_ERROR;
+ }
+
+ pVarname = objv[1+isCheckNext];
+ pCS = objv[2+isCheckNext];
+ pScript = objv[3+isCheckNext];
+
+ pChangeSet = (void *)Tcl_GetByteArrayFromObj(pCS, &nChangeSet);
+ rc = sqlite3changeset_start(&pIter, nChangeSet, pChangeSet);
+ if( rc!=SQLITE_OK ){
+ return test_session_error(interp, rc);
+ }
+
+ while( SQLITE_ROW==sqlite3changeset_next(pIter) ){
+ int nCol; /* Number of columns in table */
+ int nCol2; /* Number of columns in table */
+ int op; /* SQLITE_INSERT, UPDATE or DELETE */
+ const char *zTab; /* Name of table change applies to */
+ Tcl_Obj *pVar; /* Tcl value to set $VARNAME to */
+ Tcl_Obj *pOld; /* Vector of old.* values */
+ Tcl_Obj *pNew; /* Vector of new.* values */
+ int bIndirect;
+
+ char *zPK;
+ unsigned char *abPK;
+ int i;
+
+ sqlite3changeset_op(pIter, &zTab, &nCol, &op, &bIndirect);
+ pVar = Tcl_NewObj();
+ Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj(
+ op==SQLITE_INSERT ? "INSERT" :
+ op==SQLITE_UPDATE ? "UPDATE" :
+ "DELETE", -1
+ ));
+
+ Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj(zTab, -1));
+ Tcl_ListObjAppendElement(0, pVar, Tcl_NewBooleanObj(bIndirect));
+
+ zPK = ckalloc(nCol+1);
+ memset(zPK, 0, nCol+1);
+ sqlite3changeset_pk(pIter, &abPK, &nCol2);
+ assert( nCol==nCol2 );
+ for(i=0; i<nCol; i++){
+ zPK[i] = (abPK[i] ? 'X' : '.');
+ }
+ Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj(zPK, -1));
+ ckfree(zPK);
+
+ pOld = Tcl_NewObj();
+ if( op!=SQLITE_INSERT ){
+ int i;
+ for(i=0; i<nCol; i++){
+ sqlite3_value *pVal;
+ sqlite3changeset_old(pIter, i, &pVal);
+ test_append_value(pOld, pVal);
+ }
+ }
+ pNew = Tcl_NewObj();
+ if( op!=SQLITE_DELETE ){
+ int i;
+ for(i=0; i<nCol; i++){
+ sqlite3_value *pVal;
+ sqlite3changeset_new(pIter, i, &pVal);
+ test_append_value(pNew, pVal);
+ }
+ }
+ Tcl_ListObjAppendElement(0, pVar, pOld);
+ Tcl_ListObjAppendElement(0, pVar, pNew);
+
+ Tcl_ObjSetVar2(interp, pVarname, 0, pVar, 0);
+ rc = Tcl_EvalObjEx(interp, pScript, 0);
+ if( rc!=TCL_OK && rc!=TCL_CONTINUE ){
+ sqlite3changeset_finalize(pIter);
+ return rc==TCL_BREAK ? TCL_OK : rc;
+ }
+ }
+
+ if( isCheckNext ){
+ int rc2 = sqlite3changeset_next(pIter);
+ rc = sqlite3changeset_finalize(pIter);
+ assert( (rc2==SQLITE_DONE && rc==SQLITE_OK) || rc2==rc );
+ }else{
+ rc = sqlite3changeset_finalize(pIter);
+ }
+ if( rc!=SQLITE_OK ){
+ return test_session_error(interp, rc);
+ }
+
+ return TCL_OK;
+}
+
+int TestSession_Init(Tcl_Interp *interp){
+ Tcl_CreateObjCommand(interp, "sqlite3session", test_sqlite3session, 0, 0);
+ Tcl_CreateObjCommand(
+ interp, "sqlite3session_foreach", test_sqlite3session_foreach, 0, 0
+ );
+ Tcl_CreateObjCommand(
+ interp, "sqlite3changeset_invert", test_sqlite3changeset_invert, 0, 0
+ );
+ Tcl_CreateObjCommand(
+ interp, "sqlite3changeset_concat", test_sqlite3changeset_concat, 0, 0
+ );
+ Tcl_CreateObjCommand(
+ interp, "sqlite3changeset_apply", test_sqlite3changeset_apply, 0, 0
+ );
+ return TCL_OK;
+}
+
+#endif /* SQLITE_TEST && SQLITE_SESSION && SQLITE_PREUPDATE_HOOK */