aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ext/session/session1.test447
-rw-r--r--ext/session/session2.test564
-rw-r--r--ext/session/session3.test180
-rw-r--r--ext/session/session4.test68
-rw-r--r--ext/session/session_common.tcl132
-rw-r--r--ext/session/sessionfault.test254
-rw-r--r--ext/session/sqlite3session.c2538
-rw-r--r--ext/session/sqlite3session.h741
-rw-r--r--ext/session/test_session.c523
-rw-r--r--main.mk5
-rw-r--r--manifest54
-rw-r--r--manifest.uuid2
-rw-r--r--src/delete.c26
-rw-r--r--src/insert.c20
-rw-r--r--src/main.c21
-rw-r--r--src/sqlite.h.in101
-rw-r--r--src/sqliteInt.h9
-rw-r--r--src/tclsqlite.c233
-rw-r--r--src/test_config.c12
-rw-r--r--src/update.c21
-rw-r--r--src/vdbe.c116
-rw-r--r--src/vdbe.h2
-rw-r--r--src/vdbeInt.h21
-rw-r--r--src/vdbeapi.c203
-rw-r--r--src/vdbeaux.c64
-rw-r--r--test/hook.test408
-rw-r--r--test/permutations.test9
-rw-r--r--test/session.test18
-rw-r--r--test/tclsqlite.test2
29 files changed, 6658 insertions, 136 deletions
diff --git a/ext/session/session1.test b/ext/session/session1.test
new file mode 100644
index 000000000..067432aff
--- /dev/null
+++ b/ext/session/session1.test
@@ -0,0 +1,447 @@
+# 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. {{} {} t Surin} {i 2 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 CONSTRAINT {i 1 t one}}
+ {DELETE t2 NOTFOUND {i 3 t three}}
+ {DELETE t2 DATA {i 4 t four} {i 4 t five}}
+}
+do_execsql_test 3.2.4 "SELECT * FROM t2" {}
+do_db2_test 3.2.5 "SELECT * FROM t2" {1 one 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}
+
+catch { db2 close }
+finish_test
diff --git a/ext/session/session2.test b/ext/session/session2.test
new file mode 100644
index 000000000..5c7ea47d5
--- /dev/null
+++ b/ext/session/session2.test
@@ -0,0 +1,564 @@
+# 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}}
+}
+
+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..c0fcebff6
--- /dev/null
+++ b/ext/session/session3.test
@@ -0,0 +1,180 @@
+# 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.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/session_common.tcl b/ext/session/session_common.tcl
new file mode 100644
index 000000000..d5c74fba5
--- /dev/null
+++ b/ext/session/session_common.tcl
@@ -0,0 +1,132 @@
+
+
+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 ""
+}
+
+
+
diff --git a/ext/session/sessionfault.test b/ext/session/sessionfault.test
new file mode 100644
index 000000000..cf155f4f3
--- /dev/null
+++ b/ext/session/sessionfault.test
@@ -0,0 +1,254 @@
+# 2011 Mar 21
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+# The focus of this file is testing the session module.
+#
+
+if {![info exists testdir]} {
+ set testdir [file join [file dirname [info script]] .. .. test]
+}
+source [file join [file dirname [info script]] session_common.tcl]
+source $testdir/tester.tcl
+
+set testprefix sessionfault
+
+forcedelete test.db2
+sqlite3 db2 test.db2
+do_common_sql {
+ CREATE TABLE t1(a, b, c, PRIMARY KEY(a, b));
+ INSERT INTO t1 VALUES(1, 2, 3);
+ INSERT INTO t1 VALUES(4, 5, 6);
+}
+faultsim_save_and_close
+db2 close
+
+
+#-------------------------------------------------------------------------
+# Test OOM error handling when collecting and applying a simple changeset.
+#
+# Test 1.1 attaches tables individually by name to the session object.
+# Whereas test 1.2 passes NULL to sqlite3session_attach() to attach all
+# tables.
+#
+do_faultsim_test 1.1 -faults oom-* -prep {
+ catch {db2 close}
+ catch {db close}
+ faultsim_restore_and_reopen
+ sqlite3 db2 test.db2
+} -body {
+ do_then_apply_sql {
+ INSERT INTO t1 VALUES('a string value', 8, 9);
+ UPDATE t1 SET c = 10 WHERE a = 1;
+ DELETE FROM t1 WHERE a = 4;
+ }
+} -test {
+ faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
+ faultsim_integrity_check
+ if {$testrc==0} { compare_db db db2 }
+}
+
+do_faultsim_test 1.2 -faults oom-* -prep {
+ catch {db2 close}
+ catch {db close}
+ faultsim_restore_and_reopen
+} -body {
+ sqlite3session S db main
+ S attach *
+ execsql {
+ INSERT INTO t1 VALUES('a string value', 8, 9);
+ UPDATE t1 SET c = 10 WHERE a = 1;
+ DELETE FROM t1 WHERE a = 4;
+ }
+ set ::changeset [S changeset]
+ set {} {}
+} -test {
+ catch { S delete }
+ faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
+ faultsim_integrity_check
+ if {$testrc==0} {
+ proc xConflict {args} { return "OMIT" }
+ sqlite3 db2 test.db2
+ sqlite3changeset_apply db2 $::changeset xConflict
+ compare_db db db2
+ }
+}
+
+#-------------------------------------------------------------------------
+# The following block of tests - 2.* - are designed to check
+# the handling of faults in the sqlite3changeset_apply() function.
+#
+catch {db close}
+catch {db2 close}
+forcedelete test.db2 test.db
+sqlite3 db2 test.db2
+sqlite3 db test.db
+do_common_sql {
+ CREATE TABLE t1(a, b, c, PRIMARY KEY(a, b));
+ INSERT INTO t1 VALUES('apple', 'orange', 'pear');
+
+ CREATE TABLE t2(x PRIMARY KEY, y);
+}
+db2 close
+faultsim_save_and_close
+
+
+foreach {tn conflict_policy sql sql2} {
+ 1 OMIT { INSERT INTO t1 VALUES('one text', 'two text', X'00ff00') } {}
+ 2 OMIT { DELETE FROM t1 WHERE a = 'apple' } {}
+ 3 OMIT { UPDATE t1 SET c = 'banana' WHERE b = 'orange' } {}
+ 4 REPLACE { INSERT INTO t2 VALUES('keyvalue', 'value 1') } {
+ INSERT INTO t2 VALUES('keyvalue', 'value 2');
+ }
+} {
+ proc xConflict args [list return $conflict_policy]
+
+ do_faultsim_test 2.$tn -faults oom-transient -prep {
+ catch {db2 close}
+ catch {db close}
+ faultsim_restore_and_reopen
+ set ::changeset [changeset_from_sql $::sql]
+ sqlite3 db2 test.db2
+ sqlite3_db_config_lookaside db2 0 0 0
+ execsql $::sql2 db2
+ } -body {
+ sqlite3changeset_apply db2 $::changeset xConflict
+ } -test {
+ faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
+ faultsim_integrity_check
+ if {$testrc==0} { compare_db db db2 }
+ }
+}
+
+#-------------------------------------------------------------------------
+# This test case is designed so that a malloc() failure occurs while
+# resizing the session object hash-table from 256 to 512 buckets. This
+# is not an error, just a sub-optimal condition.
+#
+do_faultsim_test 3 -faults oom-* -prep {
+ catch {db2 close}
+ catch {db close}
+ faultsim_restore_and_reopen
+ sqlite3 db2 test.db2
+
+ sqlite3session S db main
+ S attach t1
+ execsql { BEGIN }
+ for {set i 0} {$i < 125} {incr i} {
+ execsql {INSERT INTO t1 VALUES(10+$i, 10+$i, 10+$i)}
+ }
+} -body {
+ for {set i 125} {$i < 133} {incr i} {
+ execsql {INSERT INTO t1 VALUES(10+$i, 10+$i, 1-+$i)}
+ }
+ S changeset
+ set {} {}
+} -test {
+ faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
+ if {$testrc==0} {
+ sqlite3changeset_apply db2 [S changeset] xConflict
+ compare_db db db2
+ }
+ catch { S delete }
+ faultsim_integrity_check
+}
+
+catch { db close }
+catch { db2 close }
+forcedelete test.db2 test.db
+sqlite3 db2 test.db2
+sqlite3 db test.db
+
+proc xConflict {op tbl type args} {
+ if { $type=="CONFLICT" || $type=="DATA" } {
+ return "REPLACE"
+ }
+ return "OMIT"
+}
+
+do_test 4.0 {
+ execsql {
+ PRAGMA encoding = 'utf16';
+ CREATE TABLE t1(a PRIMARY KEY, b);
+ INSERT INTO t1 VALUES(5, 32);
+ }
+ execsql {
+ PRAGMA encoding = 'utf16';
+ CREATE TABLE t1(a PRIMARY KEY, b NOT NULL);
+ INSERT INTO t1 VALUES(1, 2);
+ INSERT INTO t1 VALUES(2, 4);
+ INSERT INTO t1 VALUES(4, 16);
+ } db2
+} {}
+
+faultsim_save_and_close
+db2 close
+
+do_faultsim_test 4 -faults oom-* -prep {
+ catch {db2 close}
+ catch {db close}
+ faultsim_restore_and_reopen
+ sqlite3 db2 test.db2
+ sqlite3session S db main
+ S attach t1
+ execsql {
+ INSERT INTO t1 VALUES(1, 45);
+ INSERT INTO t1 VALUES(2, 55);
+ INSERT INTO t1 VALUES(3, 55);
+ UPDATE t1 SET a = 4 WHERE a = 5;
+ }
+} -body {
+ sqlite3changeset_apply db2 [S changeset] xConflict
+} -test {
+ catch { S delete }
+ faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
+ if {$testrc==0} { compare_db db db2 }
+}
+
+#-------------------------------------------------------------------------
+# This block of tests verifies that OOM faults in the
+# sqlite3changeset_invert() function are handled correctly.
+#
+catch {db close}
+catch {db2 close}
+forcedelete test.db
+sqlite3 db test.db
+execsql {
+ CREATE TABLE t1(a, b, PRIMARY KEY(b));
+ CREATE TABLE t2(a PRIMARY KEY, b);
+ INSERT INTO t1 VALUES('string', 1);
+ INSERT INTO t1 VALUES(4, 2);
+ INSERT INTO t1 VALUES(X'FFAAFFAAFFAA', 3);
+}
+set changeset [changeset_from_sql {
+ INSERT INTO t1 VALUES('xxx', 'yyy');
+ DELETE FROM t1 WHERE a = 'string';
+ UPDATE t1 SET a = 20 WHERE b = 2;
+}]
+db close
+
+do_faultsim_test 5 -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 4 i 2}}
+ } { lappend y $c }
+ if {$x != $y} { error "changeset no good" }
+ }
+}
+
+finish_test
diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c
new file mode 100644
index 000000000..8c1b8f64a
--- /dev/null
+++ b/ext/session/sqlite3session.c
@@ -0,0 +1,2538 @@
+
+#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 */
+ 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.
+** 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 bInsert; /* True if row was inserted this session */
+ 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 eType; /* Value type (SQLITE_NULL, TEXT etc.) */
+ int nByte; /* Size of serialized value in bytes */
+
+ 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: {
+ int n = sqlite3_value_bytes(pValue);
+ int nVarint = sessionVarintLen(n);
+ assert( eType==SQLITE_TEXT || eType==SQLITE_BLOB );
+ 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;
+ }
+ }
+
+ *pnWrite += nByte;
+ return SQLITE_OK;
+}
+
+#define HASH_APPEND(hash, add) ((hash) << 3) ^ (hash) ^ (unsigned int)(add)
+static unsigned int sessionHashAppendI64(unsigned int h, i64 i){
+ h = HASH_APPEND(h, i & 0xFFFFFFFF);
+ return HASH_APPEND(h, (i>>32)&0xFFFFFFFF);
+}
+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;
+}
+
+/*
+** 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. The value returned is guaranteed to
+** be less than pTab->nBucket.
+*/
+static unsigned 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
+){
+ 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 = HASH_APPEND(h, eType);
+ switch( eType ){
+ case SQLITE_INTEGER:
+ case 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);
+ break;
+ }
+
+ case SQLITE_TEXT:
+ case SQLITE_BLOB: {
+ int n = sqlite3_value_bytes(pVal);
+ const u8 *z = eType==SQLITE_TEXT ?
+ sqlite3_value_text(pVal) : sqlite3_value_blob(pVal);
+ h = sessionHashAppendBlob(h, n, z);
+ break;
+ }
+
+ default:
+ assert( eType==SQLITE_NULL );
+ *pbNullPK = 1;
+ return SQLITE_OK;
+ }
+ }
+ }
+
+ *piHash = (h % pTab->nChange);
+ return SQLITE_OK;
+}
+
+/*
+** Based on the primary key values stored in change pChange, calculate a
+** hash key, assuming the has table has nBucket buckets. The hash keys
+** calculated by this function are compatible with those calculated by
+** sessionPreupdateHash().
+*/
+static unsigned int sessionChangeHash(
+ sqlite3 *db, /* Database handle */
+ SessionTable *pTab, /* Table handle */
+ SessionChange *pChange, /* Change handle */
+ 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 = pChange->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
+ );
+
+ if( isPK ) h = HASH_APPEND(h, eType);
+ if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){
+ if( isPK ) h = sessionHashAppendI64(h, sessionGetI64(a));
+ a += 8;
+ }else{
+ int n;
+ a += sessionVarintGet(a, &n);
+ if( isPK ) h = sessionHashAppendBlob(h, n, a);
+ a += n;
+ }
+ }
+ return (h % nBucket);
+}
+
+static int sessionPreupdateEqual(
+ sqlite3 *db,
+ SessionTable *pTab,
+ SessionChange *pChange,
+ int bNew,
+ int *pbEqual
+){
+ int i;
+ u8 *a = pChange->aRecord;
+
+ *pbEqual = 0;
+
+ for(i=0; i<pTab->nCol; i++){
+ int eType = *a++;
+ if( !pTab->abPK[i] ){
+ switch( eType ){
+ case SQLITE_INTEGER:
+ case SQLITE_FLOAT:
+ a += 8;
+ break;
+
+ case SQLITE_TEXT:
+ case SQLITE_BLOB: {
+ int n;
+ a += sessionVarintGet(a, &n);
+ a += n;
+ break;
+ }
+ }
+ }else{
+ sqlite3_value *pVal;
+ int rc;
+ if( bNew ){
+ rc = sqlite3_preupdate_new(db, i, &pVal);
+ }else{
+ rc = sqlite3_preupdate_old(db, i, &pVal);
+ }
+ if( rc!=SQLITE_OK || sqlite3_value_type(pVal)!=eType ) return rc;
+
+ /* 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 SQLITE_OK;
+ }else{
+ double rVal;
+ assert( sizeof(iVal)==8 && sizeof(rVal)==8 );
+ memcpy(&rVal, &iVal, 8);
+ if( sqlite3_value_double(pVal)!=rVal ) return SQLITE_OK;
+ }
+ }else{
+ int n;
+ const u8 *z;
+ a += sessionVarintGet(a, &n);
+ if( sqlite3_value_bytes(pVal)!=n ) return SQLITE_OK;
+ if( eType==SQLITE_TEXT ){
+ z = sqlite3_value_text(pVal);
+ }else{
+ z = sqlite3_value_blob(pVal);
+ }
+ if( memcmp(a, z, n) ) return SQLITE_OK;
+ a += n;
+ break;
+ }
+ }
+ }
+
+ *pbEqual = 1;
+ return SQLITE_OK;
+}
+
+/*
+** If required, grow the hash table used to store changes on table pTab
+** (part of the session pSession). If a fatal OOM error occurs, set the
+** session object to failed and return SQLITE_ERROR. Otherwise, return
+** SQLITE_OK.
+**
+** It is possible that a non-fatal OOM error occurs in this function. In
+** that case the hash-table does not grow, but SQLITE_OK is returned anyway.
+** Growing the hash table in this case is a performance optimization only,
+** it is not required for correct operation.
+*/
+static int sessionGrowHash(sqlite3_session *pSession, 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 ){
+ pSession->rc = SQLITE_NOMEM;
+ 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(pSession->db, pTab, p, 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 it is not NULL, 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 three output variables are populated as follows:
+**
+** *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 = strlen(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;
+}
+
+static void sessionPreupdateOneChange(
+ int op,
+ sqlite3_session *pSession,
+ SessionTable *pTab
+){
+ 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(pSession, pTab) ) return;
+
+ /* Search the hash table for an existing entry for rowid=iKey2. If
+ ** one is found, store a pointer to it in pChange and unlink it from
+ ** the hash table. Otherwise, set pChange to NULL.
+ */
+ rc = sessionPreupdateHash(db, pTab, op==SQLITE_INSERT, &iHash, &bNullPk);
+ if( rc==SQLITE_OK && bNullPk==0 ){
+ SessionChange *pC;
+ for(pC=pTab->apChange[iHash]; rc==SQLITE_OK && pC; pC=pC->pNext){
+ int bEqual;
+ rc = sessionPreupdateEqual(db, pTab, pC, op==SQLITE_INSERT, &bEqual);
+ if( bEqual ) 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 && rc==SQLITE_OK; i++){
+ sqlite3_value *p = 0;
+ if( op!=SQLITE_INSERT ){
+ rc = sqlite3_preupdate_old(pSession->db, i, &p);
+ }else if( 1 || pTab->abPK[i] ){
+ rc = sqlite3_preupdate_new(pSession->db, i, &p);
+ }
+ if( p && rc==SQLITE_OK ){
+ rc = sessionSerializeValue(0, p, &nByte);
+ }
+ }
+
+ /* Allocate the change object */
+ pChange = (SessionChange *)sqlite3_malloc(nByte);
+ if( !pChange ){
+ rc = SQLITE_NOMEM;
+ }else{
+ memset(pChange, 0, sizeof(SessionChange));
+ pChange->aRecord = (u8 *)&pChange[1];
+ }
+
+ /* Populate the change object */
+ nByte = 0;
+ for(i=0; i<pTab->nCol && rc==SQLITE_OK; i++){
+ sqlite3_value *p = 0;
+ if( op!=SQLITE_INSERT ){
+ rc = sqlite3_preupdate_old(pSession->db, i, &p);
+ }else if( 1 || pTab->abPK[i] ){
+ rc = sqlite3_preupdate_new(pSession->db, i, &p);
+ }
+ if( p && rc==SQLITE_OK ){
+ rc = sessionSerializeValue(&pChange->aRecord[nByte], p, &nByte);
+ }
+ }
+ if( rc==SQLITE_OK ){
+ /* Add the change back to the hash-table */
+ if( pSession->bIndirect || sqlite3_preupdate_depth(pSession->db) ){
+ pChange->bIndirect = 1;
+ }
+ pChange->nRecord = nByte;
+ pChange->bInsert = (op==SQLITE_INSERT);
+ pChange->pNext = pTab->apChange[iHash];
+ pTab->apChange[iHash] = pChange;
+ }else{
+ sqlite3_free(pChange);
+ }
+ }else if( rc==SQLITE_OK && 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. */
+ 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 = strlen(zDb);
+ int nName = strlen(zDb);
+
+ assert( sqlite3_mutex_held(db->mutex) );
+
+ for(pSession=(sqlite3_session *)pCtx; pSession; pSession=pSession->pNext){
+ SessionTable *pTab;
+
+ /* If this session is attached to a different database ("main", "temp"
+ ** etc.), or if it is not currently enabled, there is nothing to do. Skip
+ ** to the next session object attached to this database. */
+ if( pSession->bEnable==0 ) continue;
+ if( pSession->rc ) continue;
+ if( sqlite3_strnicmp(zDb, pSession->zDb, nDb+1) ) continue;
+
+ 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. */
+ 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 = strlen(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;
+}
+
+/*
+** 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. */
+ while( pSession->pTable ){
+ int i;
+ SessionTable *pTab = pSession->pTable;
+ pSession->pTable = 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(pTab->azCol);
+ sqlite3_free(pTab->apChange);
+ sqlite3_free(pTab);
+ }
+
+ /* Free the session object itself. */
+ sqlite3_free(pSession);
+}
+
+/*
+** 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 = strlen(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( 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;
+ return 1;
+ }
+ p->aBuf = aNew;
+ p->nAlloc = nNew;
+ }
+ return 0;
+}
+
+/*
+** 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( *pRc==SQLITE_OK && 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, sqlite3_int64 v, int *pRc){
+ if( *pRc==SQLITE_OK && 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( *pRc==SQLITE_OK && 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 = strlen(zStr);
+ if( *pRc==SQLITE_OK && 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 = strlen(zStr)*2 + 2 + 1;
+ if( *pRc==SQLITE_OK && 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 = ((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 ){
+ int nByte = sqlite3_column_bytes(pStmt, iCol);
+ sessionAppendVarint(p, nByte, pRc);
+ sessionAppendBlob(p, eType==SQLITE_BLOB ?
+ sqlite3_column_blob(pStmt, iCol) : sqlite3_column_text(pStmt, iCol),
+ nByte, pRc
+ );
+ }
+ }
+}
+
+/*
+** This function is a no-op if *pRc is other than SQLITE_OK when it is
+** called.
+**
+** Otherwse, if *pRc is SQLITE_OK, then it 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 void 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 *pRc /* IN/OUT: Error code */
+){
+ if( *pRc==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, pRc);
+ sessionAppendByte(pBuf, p->bIndirect, pRc);
+ 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, pRc);
+ }else{
+ sessionAppendByte(pBuf, 0, pRc);
+ }
+
+ if( bChanged ){
+ sessionAppendCol(&buf2, pStmt, i, pRc);
+ bNoop = 0;
+ }else{
+ sessionAppendByte(&buf2, 0, pRc);
+ }
+
+ pCsr += nAdvance;
+ }
+
+ if( bNoop ){
+ pBuf->nBuf = nRewind;
+ }else{
+ sessionAppendBlob(pBuf, buf2.aBuf, buf2.nBuf, pRc);
+ }
+ sqlite3_free(buf2.aBuf);
+ }
+}
+
+static int sessionSelectStmt(
+ sqlite3 *db, /* Database handle */
+ const char *zDb, /* Database name */
+ const char *zTab, /* Table name */
+ int nCol,
+ const char **azCol,
+ u8 *abPK,
+ sqlite3_stmt **ppStmt
+){
+ 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;
+}
+
+static int sessionSelectBind(
+ sqlite3_stmt *pSelect,
+ int nCol,
+ u8 *abPK,
+ SessionChange *pChange
+){
+ 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 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;
+}
+
+/*
+** 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 */
+ sessionAppendByte(&buf, 'T', &rc);
+ sessionAppendVarint(&buf, nCol, &rc);
+ sessionAppendBlob(&buf, pTab->abPK, nCol, &rc);
+ sessionAppendBlob(&buf, (u8 *)zName, strlen(zName)+1, &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( sqlite3_step(pSel)==SQLITE_ROW ){
+ int iCol;
+ if( p->bInsert ){
+ sessionAppendByte(&buf, SQLITE_INSERT, &rc);
+ sessionAppendByte(&buf, p->bIndirect, &rc);
+ for(iCol=0; iCol<nCol; iCol++){
+ sessionAppendCol(&buf, pSel, iCol, &rc);
+ }
+ }else{
+ sessionAppendUpdate(&buf, pSel, p, abPK, &rc);
+ }
+ }else if( !p->bInsert ){
+ /* 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(azCol);
+ }
+ }
+
+ 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;
+}
+
+/*
+** 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;
+ int enc = (eType==SQLITE_TEXT ? SQLITE_UTF8 : 0);
+ aRec += sessionVarintGet(aRec, &nByte);
+ if( apOut ){
+ sqlite3ValueSetStr(apOut[i], nByte, 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 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){
+ u8 *aChange;
+ int i;
+ u8 c;
+
+ /* 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( 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;
+
+ c = *(aChange++);
+ if( c=='T' ){
+ int nByte; /* Bytes to allocate for apValue */
+ aChange += sessionVarintGet(aChange, &p->nCol);
+ p->abPK = (u8 *)aChange;
+ aChange += p->nCol;
+ p->zTab = (char *)aChange;
+ aChange += (strlen((char *)aChange) + 1);
+ p->op = *(aChange++);
+ p->bIndirect = *(aChange++);
+ 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);
+ }else{
+ p->op = c;
+ p->bIndirect = *(aChange++);
+ }
+ if( p->op!=SQLITE_UPDATE && p->op!=SQLITE_DELETE && p->op!=SQLITE_INSERT ){
+ return (p->rc = SQLITE_CORRUPT);
+ }
+
+ /* If this is an UPDATE or DELETE, read the old.* record. */
+ if( p->op!=SQLITE_INSERT ){
+ p->rc = sessionReadRecord(&aChange, p->nCol, 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, &p->apValue[p->nCol]);
+ if( p->rc!=SQLITE_OK ) return p->rc;
+ }
+
+ p->pNext = aChange;
+ return SQLITE_ROW;
+}
+
+/*
+** The following function extracts information on the current change
+** from a changeset iterator. They 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;
+}
+
+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;
+}
+
+/*
+** 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 */
+ void *pChangeset, /* Input changeset */
+ int *pnInverted, /* OUT: Number of bytes in output changeset */
+ void **ppInverted /* OUT: Inverse of pChangeset */
+){
+ u8 *aOut;
+ u8 *aIn;
+ int i;
+ int nCol = 0;
+
+ /* 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);
+ nByte += nCol;
+ nByte += 1 + strlen((char *)&aIn[i+nByte]);
+ memcpy(&aOut[i], &aIn[i], nByte);
+ i += nByte;
+ 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 = aEnd - &aIn[i+2];
+ memcpy(&aOut[i+2], &aIn[i+2], nByte);
+ i += 2 + nByte;
+ break;
+ }
+
+ case SQLITE_UPDATE: {
+ int nByte1; /* Size of old.* record in bytes */
+ int nByte2; /* Size of new.* record in bytes */
+ u8 *aEnd = &aIn[i+2];
+
+ sessionReadRecord(&aEnd, nCol, 0);
+ nByte1 = aEnd - &aIn[i+2];
+ sessionReadRecord(&aEnd, nCol, 0);
+ nByte2 = aEnd - &aIn[i+2] - nByte1;
+
+ aOut[i] = SQLITE_UPDATE;
+ aOut[i+1] = aIn[i+1];
+ memcpy(&aOut[i+2], &aIn[i+2+nByte1], nByte2);
+ memcpy(&aOut[i+2+nByte2], &aIn[i+2], nByte1);
+
+ i += 2 + nByte1 + nByte2;
+ break;
+ }
+
+ default:
+ sqlite3_free(aOut);
+ return SQLITE_CORRUPT;
+ }
+ }
+
+ *pnInverted = nChangeset;
+ *ppInverted = (void *)aOut;
+ return SQLITE_OK;
+}
+
+typedef struct SessionApplyCtx SessionApplyCtx;
+struct SessionApplyCtx {
+ sqlite3 *db;
+ sqlite3_stmt *pDelete; /* DELETE statement */
+ sqlite3_stmt *pUpdate; /* DELETE statement */
+ sqlite3_stmt *pInsert; /* INSERT statement */
+ sqlite3_stmt *pSelect; /* SELECT statement */
+ int nCol; /* Size of azCol[] and abPK[] arrays */
+ const char **azCol; /* Array of column names */
+ u8 *abPK; /* Boolean array - true if column is in PK */
+};
+
+/*
+** 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 a END,
+** c = CASE WHEN ?8 THEN ?9 ELSE a END,
+** d = CASE WHEN ?11 THEN ?12 ELSE a END
+** WHERE a = ?1 AND c = ?7 AND (?13 OR
+** (?5==0 OR b IS ?4) AND (?11==0 OR b 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 */
+){
+ if( (pVal->type==SQLITE_TEXT || pVal->type==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==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==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==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(*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 strlen(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);
+ 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;
+
+ schemaMismatch = 0;
+ sqlite3_free(sApply.azCol);
+ sqlite3_finalize(sApply.pDelete);
+ sqlite3_finalize(sApply.pUpdate);
+ sqlite3_finalize(sApply.pInsert);
+ sqlite3_finalize(sApply.pSelect);
+ memset(&sApply, 0, sizeof(sApply));
+ sApply.db = db;
+
+ 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 = strlen(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 ){
+ 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(sApply.azCol);
+ sqlite3_mutex_leave(sqlite3_db_mutex(db));
+ 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..bcb901667
--- /dev/null
+++ b/ext/session/sqlite3session.h
@@ -0,0 +1,741 @@
+
+#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: 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: 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.
+*/
+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: 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, void *pIn, /* Input changeset */
+ int *pnOut, void **ppOut /* OUT: Inverse of input */
+);
+
+/*
+** 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.
+**
+** For each change in the changeset, 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 the change is
+** not 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.
+**
+** Otherwise, if 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 fourth 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(*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 */
+);
+
+/*
+** 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_CONSTRAINT<dd>
+** If any other constraint violation occurs while applying a change (i.e.
+** a FOREIGN KEY, 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
+
+/*
+** 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
+
+#endif
+
diff --git a/ext/session/test_session.c b/ext/session/test_session.c
new file mode 100644
index 000000000..6af189215
--- /dev/null
+++ b/ext/session/test_session.c
@@ -0,0 +1,523 @@
+
+#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>
+
+static int test_session_error(Tcl_Interp *interp, int rc){
+ extern const char *sqlite3TestErrorName(int);
+ Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3TestErrorName(rc), -1));
+ return TCL_ERROR;
+}
+
+/*
+** Tclcmd: $session attach TABLE
+** $session changeset
+** $session delete
+** $session enable BOOL
+** $session indirect INTEGER
+*/
+static int test_session_cmd(
+ void *clientData,
+ Tcl_Interp *interp,
+ int objc,
+ Tcl_Obj *CONST objv[]
+){
+ sqlite3_session *pSession = (sqlite3_session *)clientData;
+ 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 */
+ { 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;
+ }
+ }
+
+ return TCL_OK;
+}
+
+static void test_session_del(void *clientData){
+ sqlite3_session *pSession = (sqlite3_session *)clientData;
+ sqlite3session_delete(pSession);
+}
+
+/*
+** 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 */
+ sqlite3_session *pSession; /* New session 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;
+
+ rc = sqlite3session_create(db, Tcl_GetString(objv[3]), &pSession);
+ if( rc!=SQLITE_OK ){
+ return test_session_error(interp, rc);
+ }
+
+ Tcl_CreateObjCommand(
+ interp, Tcl_GetString(objv[1]), test_session_cmd, (ClientData)pSession,
+ 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:
+ Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("t", 1));
+ pObj = Tcl_NewStringObj((char *)sqlite3_value_text(pVal), -1);
+ 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 *pScript;
+};
+
+static int test_obj_eq_string(Tcl_Obj *p, const char *z){
+ int n;
+ int nObj;
+ char *zObj;
+
+ n = strlen(z);
+ zObj = Tcl_GetStringFromObj(p, &nObj);
+
+ return (nObj==n && (n==0 || 0==memcmp(zObj, z, n)));
+}
+
+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->pScript);
+ Tcl_IncrRefCount(pEval);
+
+ sqlite3changeset_op(pIter, &zTab, &nCol, &op, 0);
+
+ /* 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 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 ){
+ Tcl_WrongNumArgs(interp, 1, objv, "DB CHANGESET 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.pScript = objv[3];
+ ctx.interp = interp;
+
+ rc = sqlite3changeset_apply(
+ db, nChangeset, pChangeset, 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;
+}
+
+/*
+** 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;
+
+ if( objc!=4 ){
+ Tcl_WrongNumArgs(interp, 1, objv, "VARNAME CHANGESET SCRIPT");
+ return TCL_ERROR;
+ }
+
+ pChangeSet = (void *)Tcl_GetByteArrayFromObj(objv[2], &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, objv[1], 0, pVar, 0);
+ rc = Tcl_EvalObjEx(interp, objv[3], 0);
+ if( rc!=TCL_OK && rc!=TCL_CONTINUE ){
+ sqlite3changeset_finalize(pIter);
+ return rc==TCL_BREAK ? TCL_OK : rc;
+ }
+ }
+ 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_apply", test_sqlite3changeset_apply, 0, 0
+ );
+ return TCL_OK;
+}
+
+#endif /* SQLITE_TEST && SQLITE_SESSION && SQLITE_PREUPDATE_HOOK */
diff --git a/main.mk b/main.mk
index e1df7a77f..ba63ad0d6 100644
--- a/main.mk
+++ b/main.mk
@@ -47,6 +47,7 @@
TCCX = $(TCC) $(OPTS) -I. -I$(TOP)/src -I$(TOP)
TCCX += -I$(TOP)/ext/rtree -I$(TOP)/ext/icu -I$(TOP)/ext/fts3
TCCX += -I$(TOP)/ext/async
+TCCX += -I$(TOP)/ext/session
# Object files for the SQLite library.
#
@@ -301,7 +302,9 @@ TESTSRC2 = \
$(TOP)/ext/fts3/fts3_expr.c \
$(TOP)/ext/fts3/fts3_tokenizer.c \
$(TOP)/ext/fts3/fts3_write.c \
- $(TOP)/ext/async/sqlite3async.c
+ $(TOP)/ext/async/sqlite3async.c \
+ $(TOP)/ext/session/sqlite3session.c \
+ $(TOP)/ext/session/test_session.c
# Header files used by all library source files.
#
diff --git a/manifest b/manifest
index 65800fd98..b8cb6b4d4 100644
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Move\sthe\sexpired-statement\stest\sfor\sOP_Function\suntil\safter\sall\smemory\shas\nbeen\sfreed.\s\sThe\stest\sis\sstill\scommented\sout,\showever.
-D 2011-04-04T12:29:20.265
+C Merge\sthe\slatest\schanges\sof\strunk\sinto\sthe\ssession\sbranch.
+D 2011-04-04T13:19:36.696
F Makefile.arm-wince-mingw32ce-gcc d6df77f1f48d690bd73162294bbba7f59507c72f
F Makefile.in 7a4d9524721d40ef9ee26f93f9bd6a51dba106f2
F Makefile.linux-gcc 91d710bdc4998cb015f39edf3cb314ec4f4d7e23
@@ -99,9 +99,18 @@ F ext/rtree/rtree_util.tcl 06aab2ed5b826545bf215fff90ecb9255a8647ea
F ext/rtree/sqlite3rtree.h 1af0899c63a688e272d69d8e746f24e76f10a3f0
F ext/rtree/tkt3363.test 142ab96eded44a3615ec79fba98c7bde7d0f96de
F ext/rtree/viewrtree.tcl eea6224b3553599ae665b239bd827e182b466024
+F ext/session/session1.test 7a92a2a6f531aef1e9764ffb7f983fb8b315376d
+F ext/session/session2.test c3e5f78d5eb988e35cc2ba9ce3678f706283cfdb
+F ext/session/session3.test bfa2376db7cbb2ac69496f84d93a8d81b13110d3
+F ext/session/session4.test a6ed685da7a5293c5d6f99855bcf41dbc352ca84
+F ext/session/session_common.tcl fb91560b6dbd086010df8b3a137a452f1ac21a28
+F ext/session/sessionfault.test 2544a2e2ecad56e3c07a32c09799871d243c114c
+F ext/session/sqlite3session.c 2b2936b5188776b33bba1263f93267f3ec9d0d84
+F ext/session/sqlite3session.h f284bac51c12de0e0096fc986e61f5ae6b9e5be5
+F ext/session/test_session.c 82e3fd7d94f485ea63bcfb15d636c95a01db97a9
F install-sh 9d4de14ab9fb0facae2f48780b874848cbf2f895 x
F ltmain.sh 3ff0879076df340d2e23ae905484d8c15d5fdea8
-F main.mk bd4e376deea4704b2bd9c77a4e6f0fa3de25c495
+F main.mk f942406cb7df55d1aec40a88a7ae399b730cd94f
F mkdll.sh 7d09b23c05d56532e9d44a50868eb4b12ff4f74a
F mkextu.sh 416f9b7089d80e5590a29692c9d9280a10dbad9f
F mkextw.sh 4123480947681d9b434a5e7b1ee08135abe409ac
@@ -129,7 +138,7 @@ F src/callback.c 5069f224882cbdccd559f591271d28d7f37745bc
F src/complete.c dc1d136c0feee03c2f7550bafc0d29075e36deac
F src/ctime.c 52ff72f966cee3087e0138a3ec69371c22be3c01
F src/date.c 1548fdac51377e4e7833251de878b4058c148e1b
-F src/delete.c 7ed8a8c8b5f748ece92df173d7e0f7810c899ebd
+F src/delete.c 44800af91a90f9d9445a266129313f27487614d8
F src/expr.c 00817c672af554321fd67c44325afd7cef0e4648
F src/fault.c 160a0c015b6c2629d3899ed2daf63d75754a32bb
F src/fkey.c 418b840007c873975fd0d071746d952f8bca20ce
@@ -138,12 +147,12 @@ F src/global.c 02335177cf6946fe5525c6f0755cf181140debf3
F src/hash.c 458488dcc159c301b8e7686280ab209f1fb915af
F src/hash.h 2894c932d84d9f892d4b4023a75e501f83050970
F src/hwtime.h d32741c8f4df852c7d959236615444e2b1063b08
-F src/insert.c 8ffb544ff516669aa84a6f13d05dbf5c93bdb1ea
+F src/insert.c e354eadb2ca3e8d939ba928f229e2268974f380c
F src/journal.c 552839e54d1bf76fb8f7abe51868b66acacf6a0e
F src/legacy.c a199d7683d60cef73089e892409113e69c23a99f
F src/lempar.c 7f026423f4d71d989e719a743f98a1cbd4e6d99e
F src/loadext.c 8af9fcc75708d60b88636ccba38b4a7b3c155c3e
-F src/main.c c23c62e53d120c5eee086c0e2cc96cc9a691c50e
+F src/main.c bed41512fabb19492b61d37fd988d79f6225e624
F src/malloc.c 92d59a007d7a42857d4e9454aa25b6b703286be1
F src/mem0.c 6a55ebe57c46ca1a7d98da93aaa07f99f1059645
F src/mem1.c 00bd8265c81abb665c48fea1e0c234eb3b922206
@@ -178,13 +187,13 @@ F src/resolve.c 1c0f32b64f8e3f555fe1f732f9d6f501a7f05706
F src/rowset.c 69afa95a97c524ba6faf3805e717b5b7ae85a697
F src/select.c d24406c45dd2442eb2eeaac413439066b149c944
F src/shell.c 9dc0b4bb59290c0a35256d278cab0f314987ad6a
-F src/sqlite.h.in e047f69a61d604d4f8be6cf1d1bdfc68be9ba7e5
+F src/sqlite.h.in 235300cdca517ce148385d3ab816e7e8cf9e1ff3
F src/sqlite3ext.h c90bd5507099f62043832d73f6425d8d5c5da754
-F src/sqliteInt.h d45b0b1db417d5308e3bf9c8983aaf2d06e7cd36
+F src/sqliteInt.h bb9e764799e8c48ca547fbd1ddc3978aeb035bc5
F src/sqliteLimit.h a17dcd3fb775d63b64a43a55c54cb282f9726f44
F src/status.c 4997380fbb915426fef9e500b4872e79c99267fc
F src/table.c 2cd62736f845d82200acfa1287e33feb3c15d62e
-F src/tclsqlite.c 501c9a200fd998a268be475be5858febc90b725b
+F src/tclsqlite.c fe0da0eb0ebd8d21eec90683b779456e64351de6
F src/test1.c 9ca440e80e16e53920904a0a5ac7feffb9b2c9a1
F src/test2.c 80d323d11e909cf0eb1b6fbb4ac22276483bcf31
F src/test3.c 056093cfef69ff4227a6bdb9108564dc7f45e4bc
@@ -198,7 +207,7 @@ F src/test_async.c 0612a752896fad42d55c3999a5122af10dcf22ad
F src/test_autoext.c 30e7bd98ab6d70a62bb9ba572e4c7df347fe645e
F src/test_backup.c c129c91127e9b46e335715ae2e75756e25ba27de
F src/test_btree.c 47cd771250f09cdc6e12dda5bc71bc0b3abc96e2
-F src/test_config.c 62f0f8f934b1d5c7e4cd4f506ae453a1117b47d7
+F src/test_config.c 3050df9faf023fb52937f7e9998004c2415d4122
F src/test_demovfs.c 0aed671636735116fc872c5b03706fd5612488b5
F src/test_devsym.c e7498904e72ba7491d142d5c83b476c4e76993bc
F src/test_func.c cbdec5cededa0761daedde5baf06004a9bf416b5
@@ -231,15 +240,15 @@ F src/test_wholenumber.c 6129adfbe7c7444f2e60cc785927f3aa74e12290
F src/test_wsd.c 41cadfd9d97fe8e3e4e44f61a4a8ccd6f7ca8fe9
F src/tokenize.c 604607d6813e9551cf5189d899e0a25c12681080
F src/trigger.c ec4813709e990a169b6923293e839fa5dfd64282
-F src/update.c 81911be16ece3c3e7716aa18565b4814ec41f8b9
+F src/update.c 3f3f3bb734a0da1dffd0ed33e504642b35ed3605
F src/utf.c d83650c3ea08f7407bd9d0839d9885241c209c60
F src/util.c cd997077bad039efc0597eb027c929658f93c018
F src/vacuum.c 924bd1bcee2dfb05376f79845bd3b4cec7b54b2f
-F src/vdbe.c 6488f759929602cbc3acea5e9c7c10e2d2cd00ed
-F src/vdbe.h 4de0efb4b0fdaaa900cf419b35c458933ef1c6d2
-F src/vdbeInt.h 7e2f028ecc1a9faa6f253e7aa8d89cae03662bae
-F src/vdbeapi.c a09ad9164cafc505250d5dd6b69660c960f1308c
-F src/vdbeaux.c 010e4e67a5a577451b0220ea8903c9e279a43248
+F src/vdbe.c e4afe80d602eb2e59d384e7e8e7cb19b06858bde
+F src/vdbe.h edef9c4f0be83e1f1dccd049da37b40e021b63d9
+F src/vdbeInt.h 36dddc4cc333867d27f00d326e29af30062a102a
+F src/vdbeapi.c 7ef519083e3420bf7793d8b4eae2961d894d98ab
+F src/vdbeaux.c 15eff6a3202145a54e7a6e65374652045ece3545
F src/vdbeblob.c c3ccb7c8732858c680f442932e66ad06bb036562
F src/vdbemem.c 0498796b6ffbe45e32960d6a1f5adfb6e419883b
F src/vdbetrace.c 3ba13bc32bdf16d2bdea523245fd16736bed67b5
@@ -481,7 +490,7 @@ F test/fuzz3.test aec64345184d1662bd30e6a17851ff659d596dc5
F test/fuzz_common.tcl a87dfbb88c2a6b08a38e9a070dabd129e617b45b
F test/fuzz_malloc.test dd7001ac86d09c154a7dff064f4739c60e2b312c
F test/fuzzer1.test 3105b5a89a6cb0d475f0877debec942fe4143462
-F test/hook.test f04c3412463f8ec117c1c704c74ca0f627ce733a
+F test/hook.test 040cf2ca263f192c66b358e095138dad0a9d75bb
F test/icu.test 70df4faca133254c042d02ae342c0a141f2663f4
F test/in.test 19b642bb134308980a92249750ea4ce3f6c75c2d
F test/in2.test 5d4c61d17493c832f7d2d32bef785119e87bde75
@@ -605,7 +614,7 @@ F test/pageropt.test 8146bf448cf09e87bb1867c2217b921fb5857806
F test/pagesize.test 76aa9f23ecb0741a4ed9d2e16c5fa82671f28efb
F test/pcache.test 065aa286e722ab24f2e51792c1f093bf60656b16
F test/pcache2.test 0d85f2ab6963aee28c671d4c71bec038c00a1d16
-F test/permutations.test 5b2a4cb756ffb2407cb4743163668d1d769febb6
+F test/permutations.test 1e35edce72e6d9e2e392420caed18652a97b1a95
F test/pragma.test fdfc09067ea104a0c247a1a79d8093b56656f850
F test/pragma2.test 5364893491b9231dd170e3459bfc2e2342658b47
F test/printf.test 05970cde31b1a9f54bd75af60597be75a5c54fea
@@ -648,6 +657,7 @@ F test/selectA.test 06d1032fa9009314c95394f2ca2e60d9f7ae8532
F test/selectB.test f305cc6660804cb239aab4e2f26b0e288b59958b
F test/selectC.test f9bf1bc4581b5b8158caa6e4e4f682acb379fb25
F test/server1.test f5b790d4c0498179151ca8a7715a65a7802c859c
+F test/session.test c1a17c11ef7d01c24fe2b9f7871190d949a8e718
F test/shared.test b9114eaea7e748a3a4c8ff7b9ca806c8f95cef3e
F test/shared2.test 7f6ad2d857d0f4e5d6a0b9a897b5e56a6b6ea18c
F test/shared3.test d69bdd5f156580876c5345652d21dc2092e85962
@@ -681,7 +691,7 @@ F test/syscall.test 02e5359bf4f5e6f716696318fde4e0b5c2c70d21
F test/sysfault.test c79441d88d23696fbec7b147dba98d42a04f523f
F test/table.test 04ba066432430657712d167ebf28080fe878d305
F test/tableapi.test 2674633fa95d80da917571ebdd759a14d9819126
-F test/tclsqlite.test 8c154101e704170c2be10f137a5499ac2c6da8d3
+F test/tclsqlite.test 1ce9b6340d6d412420634e129a2e3722c651056a
F test/tempdb.test 19d0f66e2e3eeffd68661a11c83ba5e6ace9128c
F test/temptable.test f42121a0d29a62f00f93274464164177ab1cc24a
F test/temptrigger.test b0273db072ce5f37cf19140ceb1f0d524bbe9f05
@@ -926,7 +936,7 @@ F tool/speedtest8.c 2902c46588c40b55661e471d7a86e4dd71a18224
F tool/speedtest8inst1.c 293327bc76823f473684d589a8160bde1f52c14e
F tool/split-sqlite3c.tcl d9be87f1c340285a3e081eb19b4a247981ed290c
F tool/vdbe-compress.tcl d70ea6d8a19e3571d7ab8c9b75cba86d1173ff0f
-P 4e996f36c73c5e768a954394fcbbf5e17576c807
-R 4c64876ffe2c0a670add1f1b62152ed8
+P d184cf00574b52b93f1f089025d6a0addca57558 425e3edb146c497817855dd741878709a25c8b98
+R ffd652e7771374b0d07d334e1f6024f2
U drh
-Z badc0ef9dd3fcc800ebaf4f50ec0d36c
+Z 333cd8aa259ea963e7224b1b34eb509c
diff --git a/manifest.uuid b/manifest.uuid
index 3a4413c91..5524b4073 100644
--- a/manifest.uuid
+++ b/manifest.uuid
@@ -1 +1 @@
-425e3edb146c497817855dd741878709a25c8b98 \ No newline at end of file
+95d53c44320b9639f2623aa9cc88d0d3e1a3be8f \ No newline at end of file
diff --git a/src/delete.c b/src/delete.c
index bd7ac3d1f..8a44d1224 100644
--- a/src/delete.c
+++ b/src/delete.c
@@ -338,8 +338,15 @@ void sqlite3DeleteFrom(
/* Special case: A DELETE without a WHERE clause deletes everything.
** It is easier just to erase the whole table. Prior to version 3.6.5,
** this optimization caused the row change count (the value returned by
- ** API function sqlite3_count_changes) to be set incorrectly. */
- if( rcauth==SQLITE_OK && pWhere==0 && !pTrigger && !IsVirtual(pTab)
+ ** API function sqlite3_count_changes) to be set incorrectly.
+ */
+ if( rcauth==SQLITE_OK
+ && pWhere==0
+ && !pTrigger
+ && !IsVirtual(pTab)
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+ && db->xPreUpdateCallback==0
+#endif
&& 0==sqlite3FkRequired(pParse, pTab, 0, 0)
){
assert( !isView );
@@ -391,7 +398,7 @@ void sqlite3DeleteFrom(
if( IsVirtual(pTab) ){
const char *pVTab = (const char *)sqlite3GetVTable(db, pTab);
sqlite3VtabMakeWritable(pParse, pTab);
- sqlite3VdbeAddOp4(v, OP_VUpdate, 0, 1, iRowid, pVTab, P4_VTAB);
+ sqlite3VdbeAddOp4(v, OP_VUpdate, 0, 1, iRowid, (char*)pVTab, P4_VTAB);
sqlite3MayAbort(pParse);
}else
#endif
@@ -531,13 +538,18 @@ void sqlite3GenerateRowDelete(
/* Delete the index and table entries. Skip this step if pTab is really
** a view (in which case the only effect of the DELETE statement is to
- ** fire the INSTEAD OF triggers). */
+ ** fire the INSTEAD OF triggers).
+ **
+ ** If variable 'count' is non-zero, then this OP_Delete instruction should
+ ** invoke the update-hook. The pre-update-hook, on the other hand should
+ ** be invoked unless table pTab is a system table. The difference is that
+ ** the update-hook is not invoked for rows removed by REPLACE, but the
+ ** pre-update-hook is.
+ */
if( pTab->pSelect==0 ){
sqlite3GenerateRowIndexDelete(pParse, pTab, iCur, 0);
sqlite3VdbeAddOp2(v, OP_Delete, iCur, (count?OPFLAG_NCHANGE:0));
- if( count ){
- sqlite3VdbeChangeP4(v, -1, pTab->zName, P4_STATIC);
- }
+ sqlite3VdbeChangeP4(v, -1, (char*)pTab, P4_TABLE);
}
/* Do any ON CASCADE, SET NULL or SET DEFAULT operations required to
diff --git a/src/insert.c b/src/insert.c
index a4efcf2bf..364e68460 100644
--- a/src/insert.c
+++ b/src/insert.c
@@ -1285,9 +1285,19 @@ void sqlite3GenerateConstraintChecks(
sqlite3GenerateRowDelete(
pParse, pTab, baseCur, regRowid, 0, pTrigger, OE_Replace
);
- }else if( pTab->pIndex ){
- sqlite3MultiWrite(pParse);
- sqlite3GenerateRowIndexDelete(pParse, pTab, baseCur, 0);
+ }else{
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+ /* This OP_Delete opcode fires the pre-update-hook only. It does
+ ** not modify the b-tree. It is more efficient to let the coming
+ ** OP_Insert replace the existing entry than it is to delete the
+ ** existing entry and then insert a new one. */
+ sqlite3VdbeAddOp2(v, OP_Delete, baseCur, OPFLAG_ISNOOP);
+ sqlite3VdbeChangeP4(v, -1, (char *)pTab, P4_TABLE);
+#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */
+ if( pTab->pIndex ){
+ sqlite3MultiWrite(pParse);
+ sqlite3GenerateRowIndexDelete(pParse, pTab, baseCur, 0);
+ }
}
seenReplace = 1;
break;
@@ -1473,7 +1483,7 @@ void sqlite3CompleteInsertion(
}
sqlite3VdbeAddOp3(v, OP_Insert, baseCur, regRec, regRowid);
if( !pParse->nested ){
- sqlite3VdbeChangeP4(v, -1, pTab->zName, P4_STATIC);
+ sqlite3VdbeChangeP4(v, -1, (char *)pTab, P4_TABLE);
}
sqlite3VdbeChangeP5(v, pik_flags);
}
@@ -1795,7 +1805,7 @@ static int xferOptimization(
sqlite3VdbeAddOp2(v, OP_RowData, iSrc, regData);
sqlite3VdbeAddOp3(v, OP_Insert, iDest, regData, regRowid);
sqlite3VdbeChangeP5(v, OPFLAG_NCHANGE|OPFLAG_LASTROWID|OPFLAG_APPEND);
- sqlite3VdbeChangeP4(v, -1, pDest->zName, 0);
+ sqlite3VdbeChangeP4(v, -1, (char *)pDest, P4_TABLE);
sqlite3VdbeAddOp2(v, OP_Next, iSrc, addr1);
for(pDestIdx=pDest->pIndex; pDestIdx; pDestIdx=pDestIdx->pNext){
for(pSrcIdx=pSrc->pIndex; ALWAYS(pSrcIdx); pSrcIdx=pSrcIdx->pNext){
diff --git a/src/main.c b/src/main.c
index 03097498c..21991c558 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1306,6 +1306,27 @@ void *sqlite3_rollback_hook(
return pRet;
}
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+/*
+** Register a callback to be invoked each time a row is updated,
+** inserted or deleted using this database connection.
+*/
+void *sqlite3_preupdate_hook(
+ sqlite3 *db, /* Attach the hook to this database */
+ void(*xCallback)( /* Callback function */
+ void*,sqlite3*,int,char const*,char const*,sqlite3_int64,sqlite3_int64),
+ void *pArg /* First callback argument */
+){
+ void *pRet;
+ sqlite3_mutex_enter(db->mutex);
+ pRet = db->pPreUpdateArg;
+ db->xPreUpdateCallback = xCallback;
+ db->pPreUpdateArg = pArg;
+ sqlite3_mutex_leave(db->mutex);
+ return pRet;
+}
+#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */
+
#ifndef SQLITE_OMIT_WAL
/*
** The sqlite3_wal_hook() callback registered by sqlite3_wal_autocheckpoint().
diff --git a/src/sqlite.h.in b/src/sqlite.h.in
index c4dc4a6ef..3daf72cd3 100644
--- a/src/sqlite.h.in
+++ b/src/sqlite.h.in
@@ -1129,7 +1129,7 @@ int sqlite3_config(int, ...);
** [database connection] (specified in the first argument).
**
** The second argument to sqlite3_db_config(D,V,...) is the
-** [SQLITE_DBCONIG_LOOKASIDE | configuration verb] - an integer code
+** [SQLITE_DBCONFIG_LOOKASIDE | configuration verb] - an integer code
** that indicates what aspect of the [database connection] is being configured.
** Subsequent arguments vary depending on the configuration verb.
**
@@ -4263,8 +4263,8 @@ void *sqlite3_rollback_hook(sqlite3*, void(*)(void *), void*);
** on the same [database connection] D, or NULL for
** the first call on D.
**
-** See also the [sqlite3_commit_hook()] and [sqlite3_rollback_hook()]
-** interfaces.
+** See also the [sqlite3_commit_hook()], [sqlite3_rollback_hook()],
+** and [sqlite3_preupdate_hook()] interfaces.
*/
void *sqlite3_update_hook(
sqlite3*,
@@ -6389,6 +6389,101 @@ int sqlite3_wal_checkpoint_v2(
/*
+** CAPI3REF: The pre-update hook.
+** EXPERIMENTAL
+**
+** ^These interfaces are only available if SQLite is compiled using the
+** [SQLITE_ENABLE_UPDATE_HOOK] compile-time option.
+**
+** ^The [sqlite3_preupdate_hook()] interface registers a callback function
+** that is invoked prior to each [INSERT], [UPDATE], and [DELETE] operation.
+** ^At most one preupdate hook may be registered at a time on a single
+** [database connection]; each call to [sqlite3_preupdate_hook()] overrides
+** the previous setting.
+** ^The preupdate hook is disabled by invoking [sqlite3_preupdate_hook()]
+** with a NULL pointer as the second parameter.
+** ^The third parameter to [sqlite3_preupdate_hook()] is passed through as
+** the first parameter to callbacks.
+**
+** ^The preupdate hook only fires for changes to real tables; the preupdate
+** hook is not invoked for changes to virtual tables.
+**
+** ^The second parameter to the preupdate callback is a pointer to
+** the [database connection] that registered the preupdate hook.
+** ^The third parameter to the preupdate callback is one of the constants
+** [SQLITE_INSERT], [SQLITE_DELETE], or [SQLITE_UPDATE] to indentify the
+** kind of update operation that is about to occur.
+** ^(The fourth parameter to the preupdate callback is the name of the
+** database within the database connection that is being modified. This
+** will be "main" for the main database or "temp" for TEMP tables or
+** the name given after the AS keyword in the [ATTACH] statement for attached
+** databases.)^
+** ^The fifth parameter to the preupdate callback is the name of the
+** table that is being modified.
+** ^The sixth parameter to the preupdate callback is the initial [rowid] of the
+** row being changes for SQLITE_UPDATE and SQLITE_DELETE changes and is
+** undefined for SQLITE_INSERT changes.
+** ^The seventh parameter to the preupdate callback is the final [rowid] of
+** the row being changed for SQLITE_UPDATE and SQLITE_INSERT changes and is
+** undefined for SQLITE_DELETE changes.
+**
+** The [sqlite3_preupdate_old()], [sqlite3_preupdate_new()],
+** [sqlite3_preupdate_count()], and [sqlite3_preupdate_depth()] interfaces
+** provide additional information about a preupdate event. These routines
+** may only be called from within a preupdate callback. Invoking any of
+** these routines from outside of a preupdate callback or with a
+** [database connection] pointer that is different from the one supplied
+** to the preupdate callback results in undefined and probably undesirable
+** behavior.
+**
+** ^The [sqlite3_preupdate_count(D)] interface returns the number of columns
+** in the row that is being inserted, updated, or deleted.
+**
+** ^The [sqlite3_preupdate_old(D,N,P)] interface writes into P a pointer to
+** a [protected sqlite3_value] that contains the value of the Nth column of
+** the table row before it is updated. The N parameter must be between 0
+** and one less than the number of columns or the behavior will be
+** undefined. This must only be used within SQLITE_UPDATE and SQLITE_DELETE
+** preupdate callbacks; if it is used by an SQLITE_INSERT callback then the
+** behavior is undefined. The [sqlite3_value] that P points to
+** will be destroyed when the preupdate callback returns.
+**
+** ^The [sqlite3_preupdate_new(D,N,P)] interface writes into P a pointer to
+** a [protected sqlite3_value] that contains the value of the Nth column of
+** the table row after it is updated. The N parameter must be between 0
+** and one less than the number of columns or the behavior will be
+** undefined. This must only be used within SQLITE_INSERT and SQLITE_UPDATE
+** preupdate callbacks; if it is used by an SQLITE_DELETE callback then the
+** behavior is undefined. The [sqlite3_value] that P points to
+** will be destroyed when the preupdate callback returns.
+**
+** ^The [sqlite3_preupdate_depth(D)] interface returns 0 if the preupdate
+** callback was invoked as a result of a direct insert, update, or delete
+** operation; or 1 for inserts, updates, or deletes invoked by top-level
+** triggers; or 2 for changes resulting from triggers called by top-level
+** triggers; and so forth.
+**
+** See also: [sqlite3_update_hook()]
+*/
+SQLITE_EXPERIMENTAL void *sqlite3_preupdate_hook(
+ sqlite3 *db,
+ void(*xPreUpdate)(
+ void *pCtx, /* Copy of third arg to preupdate_hook() */
+ sqlite3 *db, /* Database handle */
+ int op, /* SQLITE_UPDATE, DELETE or INSERT */
+ char const *zDb, /* Database name */
+ char const *zName, /* Table name */
+ sqlite3_int64 iKey1, /* Rowid of row about to be deleted/updated */
+ sqlite3_int64 iKey2 /* New rowid value (for a rowid UPDATE) */
+ ),
+ void*
+);
+SQLITE_EXPERIMENTAL int sqlite3_preupdate_old(sqlite3 *, int, sqlite3_value **);
+SQLITE_EXPERIMENTAL int sqlite3_preupdate_count(sqlite3 *);
+SQLITE_EXPERIMENTAL int sqlite3_preupdate_depth(sqlite3 *);
+SQLITE_EXPERIMENTAL int sqlite3_preupdate_new(sqlite3 *, int, sqlite3_value **);
+
+/*
** Undo the hack that converts floating point types to integer for
** builds on processors without floating point support.
*/
diff --git a/src/sqliteInt.h b/src/sqliteInt.h
index ba5552dba..ce1e106df 100644
--- a/src/sqliteInt.h
+++ b/src/sqliteInt.h
@@ -619,6 +619,7 @@ typedef struct LookasideSlot LookasideSlot;
typedef struct Module Module;
typedef struct NameContext NameContext;
typedef struct Parse Parse;
+typedef struct PreUpdate PreUpdate;
typedef struct RowSet RowSet;
typedef struct Savepoint Savepoint;
typedef struct Select Select;
@@ -828,6 +829,13 @@ struct sqlite3 {
void (*xRollbackCallback)(void*); /* Invoked at every commit. */
void *pUpdateArg;
void (*xUpdateCallback)(void*,int, const char*,const char*,sqlite_int64);
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+ void *pPreUpdateArg; /* First argument to xPreUpdateCallback */
+ void (*xPreUpdateCallback)( /* Registered using sqlite3_preupdate_hook() */
+ void*,sqlite3*,int,char const*,char const*,sqlite3_int64,sqlite3_int64
+ );
+ PreUpdate *pPreUpdate; /* Context for active pre-update callback */
+#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */
#ifndef SQLITE_OMIT_WAL
int (*xWalCallback)(void *, sqlite3 *, const char *, int);
void *pWalArg;
@@ -2265,6 +2273,7 @@ struct AuthContext {
#define OPFLAG_APPEND 0x08 /* This is likely to be an append */
#define OPFLAG_USESEEKRESULT 0x10 /* Try to avoid a seek in BtreeInsert() */
#define OPFLAG_CLEARCACHE 0x20 /* Clear pseudo-table cache in OP_Column */
+#define OPFLAG_ISNOOP 0x40 /* OP_Delete does pre-update-hook only */
/*
* Each trigger present in the database schema is stored as an instance of
diff --git a/src/tclsqlite.c b/src/tclsqlite.c
index 575651d7e..e1f451541 100644
--- a/src/tclsqlite.c
+++ b/src/tclsqlite.c
@@ -122,6 +122,7 @@ struct SqliteDb {
char *zNull; /* Text to substitute for an SQL NULL value */
SqlFunc *pFunc; /* List of SQL functions */
Tcl_Obj *pUpdateHook; /* Update hook script (if any) */
+ Tcl_Obj *pPreUpdateHook; /* Pre-update hook script (if any) */
Tcl_Obj *pRollbackHook; /* Rollback hook script (if any) */
Tcl_Obj *pWalHook; /* WAL hook script (if any) */
Tcl_Obj *pUnlockNotify; /* Unlock notify script (if any) */
@@ -483,6 +484,9 @@ static void DbDeleteCmd(void *db){
if( pDb->pUpdateHook ){
Tcl_DecrRefCount(pDb->pUpdateHook);
}
+ if( pDb->pPreUpdateHook ){
+ Tcl_DecrRefCount(pDb->pPreUpdateHook);
+ }
if( pDb->pRollbackHook ){
Tcl_DecrRefCount(pDb->pRollbackHook);
}
@@ -649,6 +653,42 @@ static void DbUnlockNotify(void **apArg, int nArg){
}
#endif
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+/*
+** Pre-update hook callback.
+*/
+static void DbPreUpdateHandler(
+ void *p,
+ sqlite3 *db,
+ int op,
+ const char *zDb,
+ const char *zTbl,
+ sqlite_int64 iKey1,
+ sqlite_int64 iKey2
+){
+ SqliteDb *pDb = (SqliteDb *)p;
+ Tcl_Obj *pCmd;
+ static const char *azStr[] = {"DELETE", "INSERT", "UPDATE"};
+
+ assert( (SQLITE_DELETE-1)/9 == 0 );
+ assert( (SQLITE_INSERT-1)/9 == 1 );
+ assert( (SQLITE_UPDATE-1)/9 == 2 );
+ assert( pDb->pPreUpdateHook );
+ assert( db==pDb->db );
+ assert( op==SQLITE_INSERT || op==SQLITE_UPDATE || op==SQLITE_DELETE );
+
+ pCmd = Tcl_DuplicateObj(pDb->pPreUpdateHook);
+ Tcl_IncrRefCount(pCmd);
+ Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(azStr[(op-1)/9], -1));
+ Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(zDb, -1));
+ Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(zTbl, -1));
+ Tcl_ListObjAppendElement(0, pCmd, Tcl_NewWideIntObj(iKey1));
+ Tcl_ListObjAppendElement(0, pCmd, Tcl_NewWideIntObj(iKey2));
+ Tcl_EvalObjEx(pDb->interp, pCmd, TCL_EVAL_DIRECT);
+ Tcl_DecrRefCount(pCmd);
+}
+#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */
+
static void DbUpdateHandler(
void *p,
int op,
@@ -658,14 +698,18 @@ static void DbUpdateHandler(
){
SqliteDb *pDb = (SqliteDb *)p;
Tcl_Obj *pCmd;
+ static const char *azStr[] = {"DELETE", "INSERT", "UPDATE"};
+
+ assert( (SQLITE_DELETE-1)/9 == 0 );
+ assert( (SQLITE_INSERT-1)/9 == 1 );
+ assert( (SQLITE_UPDATE-1)/9 == 2 );
assert( pDb->pUpdateHook );
assert( op==SQLITE_INSERT || op==SQLITE_UPDATE || op==SQLITE_DELETE );
pCmd = Tcl_DuplicateObj(pDb->pUpdateHook);
Tcl_IncrRefCount(pCmd);
- Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(
- ( (op==SQLITE_INSERT)?"INSERT":(op==SQLITE_UPDATE)?"UPDATE":"DELETE"), -1));
+ Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(azStr[(op-1)/9], -1));
Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(zDb, -1));
Tcl_ListObjAppendElement(0, pCmd, Tcl_NewStringObj(zTbl, -1));
Tcl_ListObjAppendElement(0, pCmd, Tcl_NewWideIntObj(rowid));
@@ -1551,6 +1595,46 @@ static int DbEvalNextCmd(
}
/*
+** This function is used by the implementations of the following database
+** handle sub-commands:
+**
+** $db update_hook ?SCRIPT?
+** $db wal_hook ?SCRIPT?
+** $db commit_hook ?SCRIPT?
+** $db preupdate hook ?SCRIPT?
+*/
+static void DbHookCmd(
+ Tcl_Interp *interp, /* Tcl interpreter */
+ SqliteDb *pDb, /* Database handle */
+ Tcl_Obj *pArg, /* SCRIPT argument (or NULL) */
+ Tcl_Obj **ppHook /* Pointer to member of SqliteDb */
+){
+ sqlite3 *db = pDb->db;
+
+ if( *ppHook ){
+ Tcl_SetObjResult(interp, *ppHook);
+ if( pArg ){
+ Tcl_DecrRefCount(*ppHook);
+ *ppHook = 0;
+ }
+ }
+ if( pArg ){
+ assert( !(*ppHook) );
+ if( Tcl_GetCharLength(pArg)>0 ){
+ *ppHook = pArg;
+ Tcl_IncrRefCount(*ppHook);
+ }
+ }
+
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+ sqlite3_preupdate_hook(db, (pDb->pPreUpdateHook?DbPreUpdateHandler:0), pDb);
+#endif
+ sqlite3_update_hook(db, (pDb->pUpdateHook?DbUpdateHandler:0), pDb);
+ sqlite3_rollback_hook(db, (pDb->pRollbackHook?DbRollbackHandler:0), pDb);
+ sqlite3_wal_hook(db, (pDb->pWalHook?DbWalHandler:0), pDb);
+}
+
+/*
** The "sqlite" command below creates a new Tcl command for each
** connection it opens to an SQLite database. This routine is invoked
** whenever one of those connection-specific commands is executed
@@ -1575,11 +1659,12 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){
"errorcode", "eval", "exists",
"function", "incrblob", "interrupt",
"last_insert_rowid", "nullvalue", "onecolumn",
- "profile", "progress", "rekey",
- "restore", "rollback_hook", "status",
- "timeout", "total_changes", "trace",
- "transaction", "unlock_notify", "update_hook",
- "version", "wal_hook", 0
+ "preupdate", "profile", "progress",
+ "rekey", "restore", "rollback_hook",
+ "status", "timeout", "total_changes",
+ "trace", "transaction", "unlock_notify",
+ "update_hook", "version", "wal_hook",
+ 0
};
enum DB_enum {
DB_AUTHORIZER, DB_BACKUP, DB_BUSY,
@@ -1589,11 +1674,11 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){
DB_ERRORCODE, DB_EVAL, DB_EXISTS,
DB_FUNCTION, DB_INCRBLOB, DB_INTERRUPT,
DB_LAST_INSERT_ROWID, DB_NULLVALUE, DB_ONECOLUMN,
- DB_PROFILE, DB_PROGRESS, DB_REKEY,
- DB_RESTORE, DB_ROLLBACK_HOOK, DB_STATUS,
- DB_TIMEOUT, DB_TOTAL_CHANGES, DB_TRACE,
- DB_TRANSACTION, DB_UNLOCK_NOTIFY, DB_UPDATE_HOOK,
- DB_VERSION, DB_WAL_HOOK
+ DB_PREUPDATE, DB_PROFILE, DB_PROGRESS,
+ DB_REKEY, DB_RESTORE, DB_ROLLBACK_HOOK,
+ DB_STATUS, DB_TIMEOUT, DB_TOTAL_CHANGES,
+ DB_TRACE, DB_TRANSACTION, DB_UNLOCK_NOTIFY,
+ DB_UPDATE_HOOK, DB_VERSION, DB_WAL_HOOK,
};
/* don't leave trailing commas on DB_enum, it confuses the AIX xlc compiler */
@@ -2764,6 +2849,90 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){
}
/*
+ ** $db preupdate_hook count
+ ** $db preupdate_hook hook ?SCRIPT?
+ ** $db preupdate_hook new INDEX
+ ** $db preupdate_hook old INDEX
+ */
+ case DB_PREUPDATE: {
+#ifndef SQLITE_ENABLE_PREUPDATE_HOOK
+ Tcl_AppendResult(interp, "preupdate_hook was omitted at compile-time");
+ rc = TCL_ERROR;
+#else
+ static const char *azSub[] = {"count", "depth", "hook", "new", "old", 0};
+ enum DbPreupdateSubCmd {
+ PRE_COUNT, PRE_DEPTH, PRE_HOOK, PRE_NEW, PRE_OLD
+ };
+ int iSub;
+
+ if( objc<3 ){
+ Tcl_WrongNumArgs(interp, 2, objv, "SUB-COMMAND ?ARGS?");
+ }
+ if( Tcl_GetIndexFromObj(interp, objv[2], azSub, "sub-command", 0, &iSub) ){
+ return TCL_ERROR;
+ }
+
+ switch( (enum DbPreupdateSubCmd)iSub ){
+ case PRE_COUNT: {
+ int nCol = sqlite3_preupdate_count(pDb->db);
+ Tcl_SetObjResult(interp, Tcl_NewIntObj(nCol));
+ break;
+ }
+
+ case PRE_HOOK: {
+ if( objc>4 ){
+ Tcl_WrongNumArgs(interp, 2, objv, "hook ?SCRIPT?");
+ return TCL_ERROR;
+ }
+ DbHookCmd(interp, pDb, (objc==4 ? objv[3] : 0), &pDb->pPreUpdateHook);
+ break;
+ }
+
+ case PRE_DEPTH: {
+ Tcl_Obj *pRet;
+ if( objc!=3 ){
+ Tcl_WrongNumArgs(interp, 3, objv, "");
+ return TCL_ERROR;
+ }
+ pRet = Tcl_NewIntObj(sqlite3_preupdate_depth(pDb->db));
+ Tcl_SetObjResult(interp, pRet);
+ break;
+ }
+
+ case PRE_NEW:
+ case PRE_OLD: {
+ int iIdx;
+ sqlite3_value *pValue;
+ if( objc!=4 ){
+ Tcl_WrongNumArgs(interp, 3, objv, "INDEX");
+ return TCL_ERROR;
+ }
+ if( Tcl_GetIntFromObj(interp, objv[3], &iIdx) ){
+ return TCL_ERROR;
+ }
+
+ if( iSub==PRE_OLD ){
+ rc = sqlite3_preupdate_old(pDb->db, iIdx, &pValue);
+ }else{
+ assert( iSub==PRE_NEW );
+ rc = sqlite3_preupdate_new(pDb->db, iIdx, &pValue);
+ }
+
+ if( rc==SQLITE_OK ){
+ Tcl_Obj *pObj;
+ pObj = Tcl_NewStringObj((char*)sqlite3_value_text(pValue), -1);
+ Tcl_SetObjResult(interp, pObj);
+ }else{
+ Tcl_AppendResult(interp, sqlite3_errmsg(pDb->db), 0);
+ return TCL_ERROR;
+ }
+ }
+ }
+#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */
+ break;
+ }
+
+ /*
** $db wal_hook ?script?
** $db update_hook ?script?
** $db rollback_hook ?script?
@@ -2771,42 +2940,19 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){
case DB_WAL_HOOK:
case DB_UPDATE_HOOK:
case DB_ROLLBACK_HOOK: {
-
/* set ppHook to point at pUpdateHook or pRollbackHook, depending on
** whether [$db update_hook] or [$db rollback_hook] was invoked.
*/
Tcl_Obj **ppHook;
- if( choice==DB_UPDATE_HOOK ){
- ppHook = &pDb->pUpdateHook;
- }else if( choice==DB_WAL_HOOK ){
- ppHook = &pDb->pWalHook;
- }else{
- ppHook = &pDb->pRollbackHook;
- }
-
- if( objc!=2 && objc!=3 ){
+ if( choice==DB_WAL_HOOK ) ppHook = &pDb->pWalHook;
+ if( choice==DB_UPDATE_HOOK ) ppHook = &pDb->pUpdateHook;
+ if( choice==DB_ROLLBACK_HOOK ) ppHook = &pDb->pRollbackHook;
+ if( objc>3 ){
Tcl_WrongNumArgs(interp, 2, objv, "?SCRIPT?");
return TCL_ERROR;
}
- if( *ppHook ){
- Tcl_SetObjResult(interp, *ppHook);
- if( objc==3 ){
- Tcl_DecrRefCount(*ppHook);
- *ppHook = 0;
- }
- }
- if( objc==3 ){
- assert( !(*ppHook) );
- if( Tcl_GetCharLength(objv[2])>0 ){
- *ppHook = objv[2];
- Tcl_IncrRefCount(*ppHook);
- }
- }
-
- sqlite3_update_hook(pDb->db, (pDb->pUpdateHook?DbUpdateHandler:0), pDb);
- sqlite3_rollback_hook(pDb->db,(pDb->pRollbackHook?DbRollbackHandler:0),pDb);
- sqlite3_wal_hook(pDb->db,(pDb->pWalHook?DbWalHandler:0),pDb);
+ DbHookCmd(interp, pDb, (objc==3 ? objv[2] : 0), ppHook);
break;
}
@@ -3584,7 +3730,9 @@ static void init_all(Tcl_Interp *interp){
extern int SqlitetestSyscall_Init(Tcl_Interp*);
extern int Sqlitetestfuzzer_Init(Tcl_Interp*);
extern int Sqlitetestwholenumber_Init(Tcl_Interp*);
-
+#if defined(SQLITE_ENABLE_SESSION) && defined(SQLITE_ENABLE_PREUPDATE_HOOK)
+ extern int TestSession_Init(Tcl_Interp*);
+#endif
#ifdef SQLITE_ENABLE_ZIPVFS
extern int Zipvfs_Init(Tcl_Interp*);
Zipvfs_Init(interp);
@@ -3624,6 +3772,9 @@ static void init_all(Tcl_Interp *interp){
SqlitetestSyscall_Init(interp);
Sqlitetestfuzzer_Init(interp);
Sqlitetestwholenumber_Init(interp);
+#if defined(SQLITE_ENABLE_SESSION) && defined(SQLITE_ENABLE_PREUPDATE_HOOK)
+ TestSession_Init(interp);
+#endif
Tcl_CreateObjCommand(interp,"load_testfixture_extensions",init_all_cmd,0,0);
diff --git a/src/test_config.c b/src/test_config.c
index 6eee524c0..2bf7af572 100644
--- a/src/test_config.c
+++ b/src/test_config.c
@@ -85,6 +85,12 @@ static void set_options(Tcl_Interp *interp){
Tcl_SetVar2(interp, "sqlite_options", "mem5", "0", TCL_GLOBAL_ONLY);
#endif
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+ Tcl_SetVar2(interp, "sqlite_options", "preupdate", "1", TCL_GLOBAL_ONLY);
+#else
+ Tcl_SetVar2(interp, "sqlite_options", "preupdate", "0", TCL_GLOBAL_ONLY);
+#endif
+
#ifdef SQLITE_MUTEX_OMIT
Tcl_SetVar2(interp, "sqlite_options", "mutex", "0", TCL_GLOBAL_ONLY);
#else
@@ -404,6 +410,12 @@ Tcl_SetVar2(interp, "sqlite_options", "long_double",
Tcl_SetVar2(interp, "sqlite_options", "schema_version", "1", TCL_GLOBAL_ONLY);
#endif
+#ifdef SQLITE_ENABLE_SESSION
+ Tcl_SetVar2(interp, "sqlite_options", "session", "1", TCL_GLOBAL_ONLY);
+#else
+ Tcl_SetVar2(interp, "sqlite_options", "session", "0", TCL_GLOBAL_ONLY);
+#endif
+
#ifdef SQLITE_ENABLE_STAT2
Tcl_SetVar2(interp, "sqlite_options", "stat2", "1", TCL_GLOBAL_ONLY);
#else
diff --git a/src/update.c b/src/update.c
index 315034d86..d702cba57 100644
--- a/src/update.c
+++ b/src/update.c
@@ -487,10 +487,23 @@ void sqlite3Update(
/* Delete the index entries associated with the current record. */
j1 = sqlite3VdbeAddOp3(v, OP_NotExists, iCur, 0, regOldRowid);
sqlite3GenerateRowIndexDelete(pParse, pTab, iCur, aRegIdx);
-
- /* If changing the record number, delete the old record. */
- if( hasFK || chngRowid ){
- sqlite3VdbeAddOp2(v, OP_Delete, iCur, 0);
+
+ /* If changing the rowid value, or if there are foreign key constraints
+ ** to process, delete the old record. Otherwise, add a noop OP_Delete
+ ** to invoke the pre-update hook.
+ **
+ ** That (regNew==regnewRowid+1) is true is also important for the
+ ** pre-update hook. If the caller invokes preupdate_new(), the returned
+ ** value is copied from memory cell (regNewRowid+1+iCol), where iCol
+ ** is the column index supplied by the user.
+ */
+ assert( regNew==regNewRowid+1 );
+ sqlite3VdbeAddOp3(v, OP_Delete, iCur,
+ OPFLAG_ISUPDATE | ((hasFK || chngRowid) ? 0 : OPFLAG_ISNOOP),
+ regNewRowid
+ );
+ if( !pParse->nested ){
+ sqlite3VdbeChangeP4(v, -1, (char*)pTab, P4_TABLE);
}
sqlite3VdbeJumpHere(v, j1);
diff --git a/src/vdbe.c b/src/vdbe.c
index 4bfe518a9..b0e66317c 100644
--- a/src/vdbe.c
+++ b/src/vdbe.c
@@ -108,6 +108,16 @@ static void updateMaxBlobsize(Mem *p){
#endif
/*
+** This macro evaluates to true if either the update hook or the preupdate
+** hook are enabled for database connect DB.
+*/
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+# define HAS_UPDATE_HOOK(DB) ((DB)->xPreUpdateCallback||(DB)->xUpdateCallback)
+#else
+# define HAS_UPDATE_HOOK(DB) ((DB)->xUpdateCallback)
+#endif
+
+/*
** The next global variable is incremented each type the OP_Found opcode
** is executed. This is used to test whether or not the foreign key
** operation implemented using OP_FkIsZero is working. This variable
@@ -2595,7 +2605,7 @@ case OP_Savepoint: {
}else{
db->nSavepoint++;
}
-
+
/* Link the new savepoint into the database handle's list. */
pNew->pNext = db->pSavepoint;
db->pSavepoint = pNew;
@@ -3850,9 +3860,9 @@ case OP_NewRowid: { /* out2-prerelease */
** is part of an INSERT operation. The difference is only important to
** the update hook.
**
-** Parameter P4 may point to a string containing the table-name, or
-** may be NULL. If it is not NULL, then the update-hook
-** (sqlite3.xUpdateCallback) is invoked following a successful insert.
+** Parameter P4 may point to a Table structure, or may be NULL. If it is
+** not NULL, then the update-hook (sqlite3.xUpdateCallback) is invoked
+** following a successful insert.
**
** (WARNING/TODO: If P1 is a pseudo-cursor and P2 is dynamically
** allocated, then ownership of P2 is transferred to the pseudo-cursor
@@ -3877,7 +3887,7 @@ case OP_InsertInt: {
int nZero; /* Number of zero-bytes to append */
int seekResult; /* Result of prior seek or 0 if no USESEEKRESULT flag */
const char *zDb; /* database name - used by the update hook */
- const char *zTbl; /* Table name - used by the opdate hook */
+ Table *pTab; /* Table structure - used by update and pre-update hooks */
int op; /* Opcode for update hook: SQLITE_UPDATE or SQLITE_INSERT */
pData = &aMem[pOp->p2];
@@ -3888,6 +3898,7 @@ case OP_InsertInt: {
assert( pC->pCursor!=0 );
assert( pC->pseudoTableReg==0 );
assert( pC->isTable );
+ assert( pOp->p4type==P4_TABLE || pOp->p4type==P4_NOTUSED );
REGISTER_TRACE(pOp->p2, pData);
if( pOp->opcode==OP_Insert ){
@@ -3901,6 +3912,24 @@ case OP_InsertInt: {
iKey = pOp->p3;
}
+ if( pOp->p4type==P4_TABLE && HAS_UPDATE_HOOK(db) ){
+ assert( pC->isTable );
+ assert( pC->iDb>=0 );
+ zDb = db->aDb[pC->iDb].zName;
+ pTab = pOp->p4.pTab;
+ op = ((pOp->p5 & OPFLAG_ISUPDATE) ? SQLITE_UPDATE : SQLITE_INSERT);
+ }
+
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+ /* Invoke the pre-update hook, if any */
+ if( db->xPreUpdateCallback
+ && pOp->p4type==P4_TABLE
+ && (!(pOp->p5 & OPFLAG_ISUPDATE) || pC->rowidIsValid==0)
+ ){
+ sqlite3VdbePreUpdateHook(p, pC, SQLITE_INSERT, zDb, pTab, iKey, pOp->p2);
+ }
+#endif
+
if( pOp->p5 & OPFLAG_NCHANGE ) p->nChange++;
if( pOp->p5 & OPFLAG_LASTROWID ) db->lastRowid = iKey;
if( pData->flags & MEM_Null ){
@@ -3926,17 +3955,12 @@ case OP_InsertInt: {
/* Invoke the update-hook if required. */
if( rc==SQLITE_OK && db->xUpdateCallback && pOp->p4.z ){
- zDb = db->aDb[pC->iDb].zName;
- zTbl = pOp->p4.z;
- op = ((pOp->p5 & OPFLAG_ISUPDATE) ? SQLITE_UPDATE : SQLITE_INSERT);
- assert( pC->isTable );
- db->xUpdateCallback(db->pUpdateArg, op, zDb, zTbl, iKey);
- assert( pC->iDb>=0 );
+ db->xUpdateCallback(db->pUpdateArg, op, zDb, pTab->zName, iKey);
}
break;
}
-/* Opcode: Delete P1 P2 * P4 *
+/* Opcode: Delete P1 P2 P3 P4 *
**
** Delete the record at which the P1 cursor is currently pointing.
**
@@ -3951,29 +3975,31 @@ case OP_InsertInt: {
** P1 must not be pseudo-table. It has to be a real table with
** multiple rows.
**
-** If P4 is not NULL, then it is the name of the table that P1 is
-** pointing to. The update hook will be invoked, if it exists.
-** If P4 is not NULL then the P1 cursor must have been positioned
-** using OP_NotFound prior to invoking this opcode.
+** If P4 is not NULL then it points to a Table struture. In this case either
+** the update or pre-update hook, or both, may be invoked. The P1 cursor must
+** have been positioned using OP_NotFound prior to invoking this opcode in
+** this case. Specifically, if one is configured, the pre-update hook is
+** invoked if P4 is not NULL. The update-hook is invoked if one is configured,
+** P4 is not NULL, and the OPFLAG_NCHANGE flag is set in P2.
+**
+** If the OPFLAG_ISUPDATE flag is set in P2, then P3 contains the address
+** of the memory cell that contains the value that the rowid of the row will
+** be set to by the update.
*/
case OP_Delete: {
i64 iKey;
VdbeCursor *pC;
+ const char *zDb;
+ Table *pTab;
+ int opflags;
+ opflags = pOp->p2;
iKey = 0;
assert( pOp->p1>=0 && pOp->p1<p->nCursor );
pC = p->apCsr[pOp->p1];
assert( pC!=0 );
assert( pC->pCursor!=0 ); /* Only valid for real tables, no pseudotables */
-
- /* If the update-hook will be invoked, set iKey to the rowid of the
- ** row being deleted.
- */
- if( db->xUpdateCallback && pOp->p4.z ){
- assert( pC->isTable );
- assert( pC->rowidIsValid ); /* lastRowid set by previous OP_NotFound */
- iKey = pC->lastRowid;
- }
+ assert( pOp->p4type==P4_TABLE || pOp->p4type==P4_NOTUSED );
/* The OP_Delete opcode always follows an OP_NotExists or OP_Last or
** OP_Column on the same table without any intervening operations that
@@ -3986,18 +4012,44 @@ case OP_Delete: {
rc = sqlite3VdbeCursorMoveto(pC);
if( NEVER(rc!=SQLITE_OK) ) goto abort_due_to_error;
+ /* If the update-hook or pre-update-hook will be invoked, set iKey to
+ ** the rowid of the row being deleted. Set zDb and zTab as well.
+ */
+ if( pOp->p4.z && HAS_UPDATE_HOOK(db) ){
+ assert( pC->iDb>=0 );
+ assert( pC->isTable );
+ assert( pC->rowidIsValid ); /* lastRowid set by previous OP_NotFound */
+ iKey = pC->lastRowid;
+ zDb = db->aDb[pC->iDb].zName;
+ pTab = pOp->p4.pTab;
+ }
+
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+ /* Invoke the pre-update-hook if required. */
+ if( db->xPreUpdateCallback && pOp->p4.z ){
+ assert( !(opflags & OPFLAG_ISUPDATE) || (aMem[pOp->p3].flags & MEM_Int) );
+ sqlite3VdbePreUpdateHook(p, pC,
+ (opflags & OPFLAG_ISUPDATE) ? SQLITE_UPDATE : SQLITE_DELETE,
+ zDb, pTab, iKey,
+ pOp->p3
+ );
+ }
+#endif
+
+ if( opflags & OPFLAG_ISNOOP ) break;
+
sqlite3BtreeSetCachedRowid(pC->pCursor, 0);
rc = sqlite3BtreeDelete(pC->pCursor);
pC->cacheStatus = CACHE_STALE;
- /* Invoke the update-hook if required. */
- if( rc==SQLITE_OK && db->xUpdateCallback && pOp->p4.z ){
- const char *zDb = db->aDb[pC->iDb].zName;
- const char *zTbl = pOp->p4.z;
- db->xUpdateCallback(db->pUpdateArg, SQLITE_DELETE, zDb, zTbl, iKey);
- assert( pC->iDb>=0 );
+ /* Update the change-counter and invoke the update-hook if required. */
+ if( opflags & OPFLAG_NCHANGE ){
+ p->nChange++;
+ assert( pOp->p4.z );
+ if( rc==SQLITE_OK && db->xUpdateCallback ){
+ db->xUpdateCallback(db->pUpdateArg, SQLITE_DELETE, zDb, pTab->zName,iKey);
+ }
}
- if( pOp->p2 & OPFLAG_NCHANGE ) p->nChange++;
break;
}
/* Opcode: ResetCount * * * * *
diff --git a/src/vdbe.h b/src/vdbe.h
index 71c871d52..2a6fe95fd 100644
--- a/src/vdbe.h
+++ b/src/vdbe.h
@@ -61,6 +61,7 @@ struct VdbeOp {
KeyInfo *pKeyInfo; /* Used when p4type is P4_KEYINFO */
int *ai; /* Used when p4type is P4_INTARRAY */
SubProgram *pProgram; /* Used when p4type is P4_SUBPROGRAM */
+ Table *pTab; /* Used when p4type is P4_TABLE */
} p4;
#ifdef SQLITE_DEBUG
char *zComment; /* Comment to improve readability */
@@ -116,6 +117,7 @@ typedef struct VdbeOpList VdbeOpList;
#define P4_INT32 (-14) /* P4 is a 32-bit signed integer */
#define P4_INTARRAY (-15) /* P4 is a vector of 32-bit integers */
#define P4_SUBPROGRAM (-18) /* P4 is a pointer to a SubProgram structure */
+#define P4_TABLE (-19) /* P4 is a pointer to a Table structure */
/* When adding a P4 argument using P4_KEYINFO, a copy of the KeyInfo structure
** is made. That copy is freed when the Vdbe is finalized. But if the
diff --git a/src/vdbeInt.h b/src/vdbeInt.h
index 15e4a812a..05136cbeb 100644
--- a/src/vdbeInt.h
+++ b/src/vdbeInt.h
@@ -332,6 +332,25 @@ struct Vdbe {
#define VDBE_MAGIC_DEAD 0xb606c3c8 /* The VDBE has been deallocated */
/*
+** Structure used to store the context required by the
+** sqlite3_preupdate_*() API functions.
+*/
+struct PreUpdate {
+ Vdbe *v;
+ VdbeCursor *pCsr; /* Cursor to read old values from */
+ int op; /* One of SQLITE_INSERT, UPDATE, DELETE */
+ u8 *aRecord; /* old.* database record */
+ KeyInfo keyinfo;
+ UnpackedRecord *pUnpacked; /* Unpacked version of aRecord[] */
+ UnpackedRecord *pNewUnpacked; /* Unpacked version of new.* record */
+ int iNewReg; /* Register for new.* values */
+ i64 iKey1; /* First key value passed to hook */
+ i64 iKey2; /* Second key value passed to hook */
+ int iPKey; /* If not negative index of IPK column */
+ Mem *aNew; /* Array of new.* values */
+};
+
+/*
** Function prototypes
*/
void sqlite3VdbeFreeCursor(Vdbe *, VdbeCursor*);
@@ -387,6 +406,8 @@ int sqlite3VdbeCloseStatement(Vdbe *, int);
void sqlite3VdbeFrameDelete(VdbeFrame*);
int sqlite3VdbeFrameRestore(VdbeFrame *);
void sqlite3VdbeMemStoreType(Mem *pMem);
+void sqlite3VdbePreUpdateHook(
+ Vdbe *, VdbeCursor *, int, const char*, Table *, i64, int);
void sqlite3VdbeEnter(Vdbe*);
void sqlite3VdbeLeave(Vdbe*);
void sqlite3VdbeMutexResync(Vdbe*);
diff --git a/src/vdbeapi.c b/src/vdbeapi.c
index 76cd9215a..8af16e9ee 100644
--- a/src/vdbeapi.c
+++ b/src/vdbeapi.c
@@ -673,6 +673,26 @@ int sqlite3_data_count(sqlite3_stmt *pStmt){
return pVm->nResColumn;
}
+/*
+** Return a pointer to static memory containing an SQL NULL value.
+*/
+static const Mem *columnNullValue(void){
+ /* Even though the Mem structure contains an element
+ ** of type i64, on certain architecture (x86) with certain compiler
+ ** switches (-Os), gcc may align this Mem object on a 4-byte boundary
+ ** instead of an 8-byte one. This all works fine, except that when
+ ** running with SQLITE_DEBUG defined the SQLite code sometimes assert()s
+ ** that a Mem structure is located on an 8-byte boundary. To prevent
+ ** this assert() from failing, when building with SQLITE_DEBUG defined
+ ** using gcc, force nullMem to be 8-byte aligned using the magical
+ ** __attribute__((aligned(8))) macro. */
+ static const Mem nullMem
+#if defined(SQLITE_DEBUG) && defined(__GNUC__)
+ __attribute__((aligned(8)))
+#endif
+ = {0, "", (double)0, {0}, 0, MEM_Null, SQLITE_NULL, 0, 0, 0 };
+ return &nullMem;
+}
/*
** Check to see if column iCol of the given statement is valid. If
@@ -690,27 +710,13 @@ static Mem *columnMem(sqlite3_stmt *pStmt, int i){
pOut = &pVm->pResultSet[i];
}else{
/* If the value passed as the second argument is out of range, return
- ** a pointer to the following static Mem object which contains the
- ** value SQL NULL. Even though the Mem structure contains an element
- ** of type i64, on certain architecture (x86) with certain compiler
- ** switches (-Os), gcc may align this Mem object on a 4-byte boundary
- ** instead of an 8-byte one. This all works fine, except that when
- ** running with SQLITE_DEBUG defined the SQLite code sometimes assert()s
- ** that a Mem structure is located on an 8-byte boundary. To prevent
- ** this assert() from failing, when building with SQLITE_DEBUG defined
- ** using gcc, force nullMem to be 8-byte aligned using the magical
- ** __attribute__((aligned(8))) macro. */
- static const Mem nullMem
-#if defined(SQLITE_DEBUG) && defined(__GNUC__)
- __attribute__((aligned(8)))
-#endif
- = {0, "", (double)0, {0}, 0, MEM_Null, SQLITE_NULL, 0, 0, 0 };
-
+ ** a pointer to a static Mem object that contains the value SQL NULL.
+ */
if( pVm && ALWAYS(pVm->db) ){
sqlite3_mutex_enter(pVm->db->mutex);
sqlite3Error(pVm->db, SQLITE_RANGE, 0);
}
- pOut = (Mem*)&nullMem;
+ pOut = (Mem*)columnNullValue();
}
return pOut;
}
@@ -1320,3 +1326,166 @@ int sqlite3_stmt_status(sqlite3_stmt *pStmt, int op, int resetFlag){
if( resetFlag ) pVdbe->aCounter[op-1] = 0;
return v;
}
+
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+/*
+** This function is called from within a pre-update callback to retrieve
+** a field of the row currently being updated or deleted.
+*/
+int sqlite3_preupdate_old(sqlite3 *db, int iIdx, sqlite3_value **ppValue){
+ PreUpdate *p = db->pPreUpdate;
+ int rc = SQLITE_OK;
+
+ /* Test that this call is being made from within an SQLITE_DELETE or
+ ** SQLITE_UPDATE pre-update callback, and that iIdx is within range. */
+ if( !p || p->op==SQLITE_INSERT ){
+ rc = SQLITE_MISUSE_BKPT;
+ goto preupdate_old_out;
+ }
+ if( iIdx>=p->pCsr->nField || iIdx<0 ){
+ rc = SQLITE_RANGE;
+ goto preupdate_old_out;
+ }
+
+ /* If the old.* record has not yet been loaded into memory, do so now. */
+ if( p->pUnpacked==0 ){
+ u32 nRec;
+ u8 *aRec;
+
+ rc = sqlite3BtreeDataSize(p->pCsr->pCursor, &nRec);
+ if( rc!=SQLITE_OK ) goto preupdate_old_out;
+ aRec = sqlite3DbMallocRaw(db, nRec);
+ if( !aRec ) goto preupdate_old_out;
+ rc = sqlite3BtreeData(p->pCsr->pCursor, 0, nRec, aRec);
+ if( rc==SQLITE_OK ){
+ p->pUnpacked = sqlite3VdbeRecordUnpack(&p->keyinfo, nRec, aRec, 0, 0);
+ if( !p->pUnpacked ) rc = SQLITE_NOMEM;
+ }
+ if( rc!=SQLITE_OK ){
+ sqlite3DbFree(db, aRec);
+ goto preupdate_old_out;
+ }
+ p->aRecord = aRec;
+ }
+
+ if( iIdx>=p->pUnpacked->nField ){
+ *ppValue = (sqlite3_value *)columnNullValue();
+ }else{
+ *ppValue = &p->pUnpacked->aMem[iIdx];
+ if( iIdx==p->iPKey ){
+ sqlite3VdbeMemSetInt64(*ppValue, p->iKey1);
+ }
+ sqlite3VdbeMemStoreType(*ppValue);
+ }
+
+ preupdate_old_out:
+ sqlite3Error(db, rc, 0);
+ return sqlite3ApiExit(db, rc);
+}
+#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */
+
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+/*
+** This function is called from within a pre-update callback to retrieve
+** the number of columns in the row being updated, deleted or inserted.
+*/
+int sqlite3_preupdate_count(sqlite3 *db){
+ PreUpdate *p = db->pPreUpdate;
+ return (p ? p->pCsr->nField : 0);
+}
+#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */
+
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+/*
+** This function is designed to be called from within a pre-update callback
+** only. It returns zero if the change that caused the callback was made
+** immediately by a user SQL statement. Or, if the change was made by a
+** trigger program, it returns the number of trigger programs currently
+** on the stack (1 for a top-level trigger, 2 for a trigger fired by a
+** top-level trigger etc.).
+**
+** For the purposes of the previous paragraph, a foreign key CASCADE, SET NULL
+** or SET DEFAULT action is considered a trigger.
+*/
+int sqlite3_preupdate_depth(sqlite3 *db){
+ PreUpdate *p = db->pPreUpdate;
+ return (p ? p->v->nFrame : 0);
+}
+#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */
+
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+/*
+** This function is called from within a pre-update callback to retrieve
+** a field of the row currently being updated or inserted.
+*/
+int sqlite3_preupdate_new(sqlite3 *db, int iIdx, sqlite3_value **ppValue){
+ PreUpdate *p = db->pPreUpdate;
+ int rc = SQLITE_OK;
+ Mem *pMem;
+
+ if( !p || p->op==SQLITE_DELETE ){
+ rc = SQLITE_MISUSE_BKPT;
+ goto preupdate_new_out;
+ }
+ if( iIdx>=p->pCsr->nField || iIdx<0 ){
+ rc = SQLITE_RANGE;
+ goto preupdate_new_out;
+ }
+
+ if( p->op==SQLITE_INSERT ){
+ /* For an INSERT, memory cell p->iNewReg contains the serialized record
+ ** that is being inserted. Deserialize it. */
+ UnpackedRecord *pUnpack = p->pNewUnpacked;
+ if( !pUnpack ){
+ Mem *pData = &p->v->aMem[p->iNewReg];
+ rc = sqlite3VdbeMemExpandBlob(pData);
+ if( rc!=SQLITE_OK ) goto preupdate_new_out;
+ pUnpack = sqlite3VdbeRecordUnpack(&p->keyinfo, pData->n, pData->z, 0, 0);
+ if( !pUnpack ){
+ rc = SQLITE_NOMEM;
+ goto preupdate_new_out;
+ }
+ p->pNewUnpacked = pUnpack;
+ }
+ if( iIdx>=pUnpack->nField ){
+ pMem = (sqlite3_value *)columnNullValue();
+ }else{
+ pMem = &pUnpack->aMem[iIdx];
+ if( iIdx==p->iPKey ){
+ sqlite3VdbeMemSetInt64(pMem, p->iKey2);
+ }
+ sqlite3VdbeMemStoreType(pMem);
+ }
+ }else{
+ /* For an UPDATE, memory cell (p->iNewReg+1+iIdx) contains the required
+ ** value. Make a copy of the cell contents and return a pointer to it.
+ ** It is not safe to return a pointer to the memory cell itself as the
+ ** caller may modify the value text encoding.
+ */
+ assert( p->op==SQLITE_UPDATE );
+ if( !p->aNew ){
+ p->aNew = (Mem *)sqlite3DbMallocZero(db, sizeof(Mem) * p->pCsr->nField);
+ if( !p->aNew ){
+ rc = SQLITE_NOMEM;
+ goto preupdate_new_out;
+ }
+ }
+ assert( iIdx>=0 && iIdx<p->pCsr->nField );
+ pMem = &p->aNew[iIdx];
+ if( pMem->flags==0 ){
+ if( iIdx==p->iPKey ){
+ sqlite3VdbeMemSetInt64(pMem, p->iKey2);
+ }else{
+ rc = sqlite3VdbeMemCopy(pMem, &p->v->aMem[p->iNewReg+1+iIdx]);
+ if( rc!=SQLITE_OK ) goto preupdate_new_out;
+ }
+ sqlite3VdbeMemStoreType(pMem);
+ }
+ }
+ *ppValue = pMem;
+
+ preupdate_new_out:
+ sqlite3Error(db, rc, 0);
+ return sqlite3ApiExit(db, rc);
+}
+#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */
diff --git a/src/vdbeaux.c b/src/vdbeaux.c
index b28bac4f3..16eeb5ce1 100644
--- a/src/vdbeaux.c
+++ b/src/vdbeaux.c
@@ -75,6 +75,7 @@ const char *sqlite3_sql(sqlite3_stmt *pStmt){
void sqlite3VdbeSwap(Vdbe *pA, Vdbe *pB){
Vdbe tmp, *pTmp;
char *zTmp;
+ assert( pA->db==pB->db );
tmp = *pA;
*pA = *pB;
*pB = tmp;
@@ -2901,6 +2902,7 @@ UnpackedRecord *sqlite3VdbeRecordUnpack(
pMem->db = pKeyInfo->db;
pMem->flags = 0;
pMem->zMalloc = 0;
+ pMem->z = 0;
d += sqlite3VdbeSerialGet(&aKey[d], serial_type, pMem);
pMem++;
u++;
@@ -2925,7 +2927,7 @@ void sqlite3VdbeDeleteUnpackedRecord(UnpackedRecord *p){
** strings and blobs static. And none of the elements are
** ever transformed, so there is never anything to delete.
*/
- if( NEVER(pMem->zMalloc) ) sqlite3VdbeMemRelease(pMem);
+ if( pMem->zMalloc ) sqlite3VdbeMemRelease(pMem);
}
if( p->flags & UNPACKED_NEED_FREE ){
sqlite3DbFree(p->pKeyInfo->db, p);
@@ -3257,3 +3259,63 @@ void sqlite3VdbeSetVarmask(Vdbe *v, int iVar){
v->expmask |= ((u32)1 << (iVar-1));
}
}
+
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+/*
+** Invoke the pre-update hook. If this is an UPDATE or DELETE pre-update call,
+** then cursor passed as the second argument should point to the row about
+** to be update or deleted. If the application calls sqlite3_preupdate_old(),
+** the required value will be read from the row the cursor points to.
+*/
+void sqlite3VdbePreUpdateHook(
+ Vdbe *v, /* Vdbe pre-update hook is invoked by */
+ VdbeCursor *pCsr, /* Cursor to grab old.* values from */
+ int op, /* SQLITE_INSERT, UPDATE or DELETE */
+ const char *zDb, /* Database name */
+ Table *pTab, /* Modified table */
+ i64 iKey1, /* Initial key value */
+ int iReg /* Register for new.* record */
+){
+ sqlite3 *db = v->db;
+ i64 iKey2;
+ PreUpdate preupdate;
+ const char *zTbl = pTab->zName;
+
+ assert( db->pPreUpdate==0 );
+ memset(&preupdate, 0, sizeof(PreUpdate));
+ if( op==SQLITE_UPDATE ){
+ iKey2 = v->aMem[iReg].u.i;
+ }else{
+ iKey2 = iKey1;
+ }
+
+ preupdate.v = v;
+ preupdate.pCsr = pCsr;
+ preupdate.op = op;
+ preupdate.iNewReg = iReg;
+ preupdate.keyinfo.db = db;
+ preupdate.keyinfo.enc = ENC(db);
+ preupdate.keyinfo.nField = pCsr->nField;
+ preupdate.iKey1 = iKey1;
+ preupdate.iKey2 = iKey2;
+ preupdate.iPKey = pTab->iPKey;
+
+ db->pPreUpdate = &preupdate;
+ db->xPreUpdateCallback(db->pPreUpdateArg, db, op, zDb, zTbl, iKey1, iKey2);
+ db->pPreUpdate = 0;
+ sqlite3DbFree(db, preupdate.aRecord);
+ if( preupdate.pUnpacked ){
+ sqlite3VdbeDeleteUnpackedRecord(preupdate.pUnpacked);
+ }
+ if( preupdate.pNewUnpacked ){
+ sqlite3VdbeDeleteUnpackedRecord(preupdate.pNewUnpacked);
+ }
+ if( preupdate.aNew ){
+ int i;
+ for(i=0; i<pCsr->nField; i++){
+ sqlite3VdbeMemRelease(&preupdate.aNew[i]);
+ }
+ sqlite3DbFree(db, preupdate.aNew);
+ }
+}
+#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */
diff --git a/test/hook.test b/test/hook.test
index 6496d41e1..58a6fed8b 100644
--- a/test/hook.test
+++ b/test/hook.test
@@ -21,6 +21,7 @@
set testdir [file dirname $argv0]
source $testdir/tester.tcl
+set ::testprefix hook
do_test hook-1.2 {
db commit_hook
@@ -361,4 +362,411 @@ do_test hook-6.2 {
} {COMMIT ROLLBACK}
unset ::hooks
+#----------------------------------------------------------------------------
+# The following tests - hook-7.* - test the pre-update hook.
+#
+ifcapable !preupdate {
+ finish_test
+ return
+}
+#
+# 7.1.1 - INSERT statement.
+# 7.1.2 - INSERT INTO ... SELECT statement.
+# 7.1.3 - REPLACE INTO ... (rowid conflict)
+# 7.1.4 - REPLACE INTO ... (other index conflicts)
+# 7.1.5 - REPLACE INTO ... (both rowid and other index conflicts)
+#
+# 7.2.1 - DELETE statement.
+# 7.2.2 - DELETE statement that uses the truncate optimization.
+#
+# 7.3.1 - UPDATE statement.
+# 7.3.2 - UPDATE statement that modifies the rowid.
+# 7.3.3 - UPDATE OR REPLACE ... (rowid conflict).
+# 7.3.4 - UPDATE OR REPLACE ... (other index conflicts)
+# 7.3.4 - UPDATE OR REPLACE ... (both rowid and other index conflicts)
+#
+# 7.4.1 - Test that the pre-update-hook is invoked only once if a row being
+# deleted is removed by a BEFORE trigger.
+#
+# 7.4.2 - Test that the pre-update-hook is invoked if a BEFORE trigger
+# removes a row being updated. In this case the update hook should
+# be invoked with SQLITE_INSERT as the opcode when inserting the
+# new version of the row.
+#
+# TODO: Short records (those created before a column is added to a table
+# using ALTER TABLE)
+#
+
+proc do_preupdate_test {tn sql x} {
+ set X [list]
+ foreach elem $x {lappend X $elem}
+ uplevel do_test $tn [list "
+ set ::preupdate \[list\]
+ execsql { $sql }
+ set ::preupdate
+ "] [list $X]
+}
+
+proc preupdate_hook {args} {
+ set type [lindex $args 0]
+ eval lappend ::preupdate $args
+ if {$type != "INSERT"} {
+ for {set i 0} {$i < [db preupdate count]} {incr i} {
+ lappend ::preupdate [db preupdate old $i]
+ }
+ }
+ if {$type != "DELETE"} {
+ for {set i 0} {$i < [db preupdate count]} {incr i} {
+ set rc [catch { db preupdate new $i } v]
+ lappend ::preupdate $v
+ }
+ }
+}
+
+db close
+forcedelete test.db
+sqlite3 db test.db
+db preupdate hook preupdate_hook
+
+# Set up a schema to use for tests 7.1.* to 7.3.*.
+do_execsql_test 7.0 {
+ CREATE TABLE t1(a, b);
+ CREATE TABLE t2(x, y);
+ CREATE TABLE t3(i, j, UNIQUE(i));
+
+ INSERT INTO t2 VALUES('a', 'b');
+ INSERT INTO t2 VALUES('c', 'd');
+
+ INSERT INTO t3 VALUES(4, 16);
+ INSERT INTO t3 VALUES(5, 25);
+ INSERT INTO t3 VALUES(6, 36);
+}
+
+do_preupdate_test 7.1.1 {
+ INSERT INTO t1 VALUES('x', 'y')
+} {INSERT main t1 1 1 x y}
+
+# 7.1.2.1 does not use the xfer optimization. 7.1.2.2 does.
+do_preupdate_test 7.1.2.1 {
+ INSERT INTO t1 SELECT y, x FROM t2;
+} {INSERT main t1 2 2 b a INSERT main t1 3 3 d c}
+do_preupdate_test 7.1.2.2 {
+ INSERT INTO t1 SELECT * FROM t2;
+} {INSERT main t1 4 4 a b INSERT main t1 5 5 c d}
+
+do_preupdate_test 7.1.3 {
+ REPLACE INTO t1(rowid, a, b) VALUES(1, 1, 1);
+} {
+ DELETE main t1 1 1 x y
+ INSERT main t1 1 1 1 1
+}
+
+do_preupdate_test 7.1.4 {
+ REPLACE INTO t3 VALUES(4, NULL);
+} {
+ DELETE main t3 1 1 4 16
+ INSERT main t3 4 4 4 {}
+}
+
+do_preupdate_test 7.1.5 {
+ REPLACE INTO t3(rowid, i, j) VALUES(2, 6, NULL);
+} {
+ DELETE main t3 2 2 5 25
+ DELETE main t3 3 3 6 36
+ INSERT main t3 2 2 6 {}
+}
+
+do_execsql_test 7.2.0 { SELECT rowid FROM t1 } {1 2 3 4 5}
+
+do_preupdate_test 7.2.1 {
+ DELETE FROM t1 WHERE rowid = 3
+} {
+ DELETE main t1 3 3 d c
+}
+do_preupdate_test 7.2.2 {
+ DELETE FROM t1
+} {
+ DELETE main t1 1 1 1 1
+ DELETE main t1 2 2 b a
+ DELETE main t1 4 4 a b
+ DELETE main t1 5 5 c d
+}
+
+do_execsql_test 7.3.0 {
+ DELETE FROM t1;
+ DELETE FROM t2;
+ DELETE FROM t3;
+
+ INSERT INTO t2 VALUES('a', 'b');
+ INSERT INTO t2 VALUES('c', 'd');
+
+ INSERT INTO t3 VALUES(4, 16);
+ INSERT INTO t3 VALUES(5, 25);
+ INSERT INTO t3 VALUES(6, 36);
+}
+
+do_preupdate_test 7.3.1 {
+ UPDATE t2 SET y = y||y;
+} {
+ UPDATE main t2 1 1 a b a bb
+ UPDATE main t2 2 2 c d c dd
+}
+
+do_preupdate_test 7.3.2 {
+ UPDATE t2 SET rowid = rowid-1;
+} {
+ UPDATE main t2 1 0 a bb a bb
+ UPDATE main t2 2 1 c dd c dd
+}
+
+do_preupdate_test 7.3.3 {
+ UPDATE OR REPLACE t2 SET rowid = 1 WHERE x = 'a'
+} {
+ DELETE main t2 1 1 c dd
+ UPDATE main t2 0 1 a bb a bb
+}
+
+do_preupdate_test 7.3.4.1 {
+ UPDATE OR REPLACE t3 SET i = 5 WHERE i = 6
+} {
+ DELETE main t3 2 2 5 25
+ UPDATE main t3 3 3 6 36 5 36
+}
+
+do_execsql_test 7.3.4.2 {
+ INSERT INTO t3 VALUES(10, 100);
+ SELECT rowid, * FROM t3;
+} {1 4 16 3 5 36 4 10 100}
+
+do_preupdate_test 7.3.5 {
+ UPDATE OR REPLACE t3 SET rowid = 1, i = 5 WHERE j = 100;
+} {
+ DELETE main t3 1 1 4 16
+ DELETE main t3 3 3 5 36
+ UPDATE main t3 4 1 10 100 5 100
+}
+
+do_execsql_test 7.4.1.0 {
+ CREATE TABLE t4(a, b);
+ INSERT INTO t4 VALUES('a', 1);
+ INSERT INTO t4 VALUES('b', 2);
+ INSERT INTO t4 VALUES('c', 3);
+
+ CREATE TRIGGER t4t BEFORE DELETE ON t4 BEGIN
+ DELETE FROM t4 WHERE b = 1;
+ END;
+}
+
+do_preupdate_test 7.4.1.1 {
+ DELETE FROM t4 WHERE b = 3
+} {
+ DELETE main t4 1 1 a 1
+ DELETE main t4 3 3 c 3
+}
+
+do_execsql_test 7.4.1.2 {
+ INSERT INTO t4(rowid, a, b) VALUES(1, 'a', 1);
+ INSERT INTO t4(rowid, a, b) VALUES(3, 'c', 3);
+}
+do_preupdate_test 7.4.1.3 {
+ DELETE FROM t4 WHERE b = 1
+} {
+ DELETE main t4 1 1 a 1
+}
+
+do_execsql_test 7.4.2.0 {
+ CREATE TABLE t5(a, b);
+ INSERT INTO t5 VALUES('a', 1);
+ INSERT INTO t5 VALUES('b', 2);
+ INSERT INTO t5 VALUES('c', 3);
+
+ CREATE TRIGGER t5t BEFORE UPDATE ON t5 BEGIN
+ DELETE FROM t5 WHERE b = 1;
+ END;
+}
+do_preupdate_test 7.4.2.1 {
+ UPDATE t5 SET b = 4 WHERE a = 'c'
+} {
+ DELETE main t5 1 1 a 1
+ UPDATE main t5 3 3 c 3 c 4
+}
+
+do_execsql_test 7.4.2.2 {
+ INSERT INTO t5(rowid, a, b) VALUES(1, 'a', 1);
+}
+
+do_preupdate_test 7.4.2.3 {
+ UPDATE t5 SET b = 5 WHERE a = 'a'
+} {
+ DELETE main t5 1 1 a 1
+}
+
+do_execsql_test 7.5.1.0 {
+ CREATE TABLE t7(a, b);
+ INSERT INTO t7 VALUES('one', 'two');
+ INSERT INTO t7 VALUES('three', 'four');
+ ALTER TABLE t7 ADD COLUMN c DEFAULT NULL;
+}
+
+do_preupdate_test 7.5.1.1 {
+ DELETE FROM t7 WHERE a = 'one'
+} {
+ DELETE main t7 1 1 one two {}
+}
+
+do_preupdate_test 7.5.1.2 {
+ UPDATE t7 SET b = 'five'
+} {
+ UPDATE main t7 2 2 three four {} three five {}
+}
+
+do_execsql_test 7.5.2.0 {
+ CREATE TABLE t8(a, b);
+ INSERT INTO t8 VALUES('one', 'two');
+ INSERT INTO t8 VALUES('three', 'four');
+ ALTER TABLE t8 ADD COLUMN c DEFAULT 'xxx';
+}
+
+# At time of writing, these two are broken. They demonstrate that the
+# sqlite3_preupdate_old() method does not handle the case where ALTER TABLE
+# has been used to add a column with a default value other than NULL.
+#
+do_preupdate_test 7.5.2.1 {
+ DELETE FROM t8 WHERE a = 'one'
+} {
+ DELETE main t8 1 1 one two xxx
+}
+do_preupdate_test 7.5.2.2 {
+ UPDATE t8 SET b = 'five'
+} {
+ UPDATE main t8 2 2 three four xxx three five xxx
+}
+
+# This block of tests verifies that IPK values are correctly reported
+# by the sqlite3_preupdate_old() and sqlite3_preupdate_new() functions.
+#
+do_execsql_test 7.6.1 { CREATE TABLE t9(a, b INTEGER PRIMARY KEY, c) }
+do_preupdate_test 7.6.2 {
+ INSERT INTO t9 VALUES(1, 2, 3);
+ UPDATE t9 SET b = b+1, c = c+1;
+ DELETE FROM t9 WHERE a = 1;
+} {
+ INSERT main t9 2 2 1 2 3
+ UPDATE main t9 2 3 1 2 3 1 3 4
+ DELETE main t9 3 3 1 3 4
+}
+
+#--------------------------------------------------------------------------
+# Test that the sqlite3_preupdate_depth() API seems to work.
+#
+proc preupdate_hook {args} {
+ set type [lindex $args 0]
+ eval lappend ::preupdate $args
+ eval lappend ::preupdate [db preupdate depth]
+
+ if {$type != "INSERT"} {
+ for {set i 0} {$i < [db preupdate count]} {incr i} {
+ lappend ::preupdate [db preupdate old $i]
+ }
+ }
+ if {$type != "DELETE"} {
+ for {set i 0} {$i < [db preupdate count]} {incr i} {
+ set rc [catch { db preupdate new $i } v]
+ lappend ::preupdate $v
+ }
+ }
+}
+
+db close
+forcedelete test.db
+sqlite3 db test.db
+db preupdate hook preupdate_hook
+
+do_execsql_test 7.6.1 {
+ CREATE TABLE t1(x PRIMARY KEY);
+ CREATE TABLE t2(x PRIMARY KEY);
+ CREATE TABLE t3(x PRIMARY KEY);
+ CREATE TABLE t4(x PRIMARY KEY);
+
+ CREATE TRIGGER a AFTER INSERT ON t1 BEGIN INSERT INTO t2 VALUES(new.x); END;
+ CREATE TRIGGER b AFTER INSERT ON t2 BEGIN INSERT INTO t3 VALUES(new.x); END;
+ CREATE TRIGGER c AFTER INSERT ON t3 BEGIN INSERT INTO t4 VALUES(new.x); END;
+
+ CREATE TRIGGER d AFTER UPDATE ON t1 BEGIN UPDATE t2 SET x = new.x; END;
+ CREATE TRIGGER e AFTER UPDATE ON t2 BEGIN UPDATE t3 SET x = new.x; END;
+ CREATE TRIGGER f AFTER UPDATE ON t3 BEGIN UPDATE t4 SET x = new.x; END;
+
+ CREATE TRIGGER g AFTER DELETE ON t1 BEGIN DELETE FROM t2 WHERE 1; END;
+ CREATE TRIGGER h AFTER DELETE ON t2 BEGIN DELETE FROM t3 WHERE 1; END;
+ CREATE TRIGGER i AFTER DELETE ON t3 BEGIN DELETE FROM t4 WHERE 1; END;
+}
+
+do_preupdate_test 7.6.2 {
+ INSERT INTO t1 VALUES('xyz');
+} {
+ INSERT main t1 1 1 0 xyz
+ INSERT main t2 1 1 1 xyz
+ INSERT main t3 1 1 2 xyz
+ INSERT main t4 1 1 3 xyz
+}
+do_preupdate_test 7.6.3 {
+ UPDATE t1 SET x = 'abc';
+} {
+ UPDATE main t1 1 1 0 xyz abc
+ UPDATE main t2 1 1 1 xyz abc
+ UPDATE main t3 1 1 2 xyz abc
+ UPDATE main t4 1 1 3 xyz abc
+}
+do_preupdate_test 7.6.4 {
+ DELETE FROM t1 WHERE 1;
+} {
+ DELETE main t1 1 1 0 abc
+ DELETE main t2 1 1 1 abc
+ DELETE main t3 1 1 2 abc
+ DELETE main t4 1 1 3 abc
+}
+
+do_execsql_test 7.6.5 {
+ DROP TRIGGER a; DROP TRIGGER b; DROP TRIGGER c;
+ DROP TRIGGER d; DROP TRIGGER e; DROP TRIGGER f;
+ DROP TRIGGER g; DROP TRIGGER h; DROP TRIGGER i;
+
+ CREATE TRIGGER a BEFORE INSERT ON t1 BEGIN INSERT INTO t2 VALUES(new.x); END;
+ CREATE TRIGGER b BEFORE INSERT ON t2 BEGIN INSERT INTO t3 VALUES(new.x); END;
+ CREATE TRIGGER c BEFORE INSERT ON t3 BEGIN INSERT INTO t4 VALUES(new.x); END;
+
+ CREATE TRIGGER d BEFORE UPDATE ON t1 BEGIN UPDATE t2 SET x = new.x; END;
+ CREATE TRIGGER e BEFORE UPDATE ON t2 BEGIN UPDATE t3 SET x = new.x; END;
+ CREATE TRIGGER f BEFORE UPDATE ON t3 BEGIN UPDATE t4 SET x = new.x; END;
+
+ CREATE TRIGGER g BEFORE DELETE ON t1 BEGIN DELETE FROM t2 WHERE 1; END;
+ CREATE TRIGGER h BEFORE DELETE ON t2 BEGIN DELETE FROM t3 WHERE 1; END;
+ CREATE TRIGGER i BEFORE DELETE ON t3 BEGIN DELETE FROM t4 WHERE 1; END;
+}
+
+do_preupdate_test 7.6.6 {
+ INSERT INTO t1 VALUES('xyz');
+} {
+ INSERT main t4 1 1 3 xyz
+ INSERT main t3 1 1 2 xyz
+ INSERT main t2 1 1 1 xyz
+ INSERT main t1 1 1 0 xyz
+}
+do_preupdate_test 7.6.3 {
+ UPDATE t1 SET x = 'abc';
+} {
+ UPDATE main t4 1 1 3 xyz abc
+ UPDATE main t3 1 1 2 xyz abc
+ UPDATE main t2 1 1 1 xyz abc
+ UPDATE main t1 1 1 0 xyz abc
+}
+do_preupdate_test 7.6.4 {
+ DELETE FROM t1 WHERE 1;
+} {
+ DELETE main t4 1 1 3 abc
+ DELETE main t3 1 1 2 abc
+ DELETE main t2 1 1 1 abc
+ DELETE main t1 1 1 0 abc
+}
+
finish_test
diff --git a/test/permutations.test b/test/permutations.test
index 9c48d9aa2..7e336b16a 100644
--- a/test/permutations.test
+++ b/test/permutations.test
@@ -89,6 +89,9 @@ foreach f [glob $testdir/*.test] { lappend alltests [file tail $f] }
foreach f [glob -nocomplain $testdir/../ext/rtree/*.test] {
lappend alltests $f
}
+foreach f [glob -nocomplain $testdir/../ext/session/*.test] {
+ lappend alltests $f
+}
if {$::tcl_platform(platform)!="unix"} {
set alltests [test_set $alltests -exclude crash.test crash2.test]
@@ -96,7 +99,7 @@ if {$::tcl_platform(platform)!="unix"} {
set alltests [test_set $alltests -exclude {
all.test async.test quick.test veryquick.test
memleak.test permutations.test soak.test fts3.test
- mallocAll.test rtree.test
+ mallocAll.test rtree.test session.test
}]
set allquicktests [test_set $alltests -exclude {
@@ -762,6 +765,10 @@ test_suite "rtree" -description {
All R-tree related tests. Provides coverage of source file rtree.c.
} -files [glob -nocomplain $::testdir/../ext/rtree/*.test]
+test_suite "session" -description {
+ All session module related tests.
+} -files [glob -nocomplain $::testdir/../ext/session/*.test]
+
test_suite "no_optimization" -description {
Run test scripts with optimizations disabled using the
sqlite3_test_control(SQLITE_TESTCTRL_OPTIMIZATIONS) interface.
diff --git a/test/session.test b/test/session.test
new file mode 100644
index 000000000..bf159c3b2
--- /dev/null
+++ b/test/session.test
@@ -0,0 +1,18 @@
+# 2008 June 23
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+# This file runs all rtree related tests.
+#
+
+set testdir [file dirname $argv0]
+source $testdir/permutations.test
+
+ifcapable session {
+ run_test_suite session
+}
+
+finish_test
diff --git a/test/tclsqlite.test b/test/tclsqlite.test
index 8db04ebfa..c9d562beb 100644
--- a/test/tclsqlite.test
+++ b/test/tclsqlite.test
@@ -35,7 +35,7 @@ do_test tcl-1.1 {
do_test tcl-1.2 {
set v [catch {db bogus} msg]
lappend v $msg
-} {1 {bad option "bogus": must be authorizer, backup, busy, cache, changes, close, collate, collation_needed, commit_hook, complete, copy, enable_load_extension, errorcode, eval, exists, function, incrblob, interrupt, last_insert_rowid, nullvalue, onecolumn, profile, progress, rekey, restore, rollback_hook, status, timeout, total_changes, trace, transaction, unlock_notify, update_hook, version, or wal_hook}}
+} {1 {bad option "bogus": must be authorizer, backup, busy, cache, changes, close, collate, collation_needed, commit_hook, complete, copy, enable_load_extension, errorcode, eval, exists, function, incrblob, interrupt, last_insert_rowid, nullvalue, onecolumn, preupdate, profile, progress, rekey, restore, rollback_hook, status, timeout, total_changes, trace, transaction, unlock_notify, update_hook, version, or wal_hook}}
do_test tcl-1.2.1 {
set v [catch {db cache bogus} msg]
lappend v $msg