Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a9ef8d81c8 | |||
| c2a9c57a33 | |||
| 4d4f3e5d85 | |||
| 05c093e1b3 | |||
| ff7bc49645 | |||
| 68379484d0 | |||
| 6c1d7897e2 | |||
| 659f76245f | |||
| 7417cefec6 | |||
| 310874ca00 | |||
| 16de7f4d32 | |||
| 618fea4830 | |||
| 8973a04e3b | |||
| c706edac44 | |||
| 14bcd7d25e | |||
| e56467fc9f | |||
| 7b9370a445 | |||
| 3581020e53 | |||
| caee673b0d | |||
| 0d7706f41a | |||
| efa50bdac1 | |||
| bd91913dc0 | |||
| 108606837e | |||
| 6d972c1b98 | |||
| ddf01aeb16 | |||
| fdf1795618 | |||
| adb96a62e5 | |||
| af49af7f9e | |||
| 54cced185c | |||
| f004e1d69e | |||
| efb1ca3738 | |||
| 29c21c77af | |||
| 946e610a84 | |||
| 063d96fc18 | |||
| 93bca69578 | |||
| 7cca2e6259 | |||
| 0e9a916c36 | |||
| 7f197bb7b0 | |||
| 964cbd469b | |||
| 1e556c3fe5 | |||
| cfec7eee20 | |||
| 8a3bb3fddd | |||
| ff53032641 | |||
| a93166e030 | |||
| b69ec3482a | |||
| 4f52804206 | |||
| 40368988da | |||
| e437ca5ec0 | |||
| 88ab69f614 | |||
| 893ab7ccba | |||
| 53ae686bfb | |||
| 34ffcfff02 | |||
| 467eb01caa | |||
| 8863f35f5c | |||
| bf0ed47d82 | |||
| 4ec233f12a | |||
| 0576a30dad | |||
| 23328bedf0 | |||
| 77fc1d5bb4 | |||
| 798693b2b1 | |||
| 80fe2d9310 | |||
| 6734007d75 | |||
| b08a1efae5 | |||
| 1756ae10e1 | |||
| f29123b572 | |||
| 29e03e977c | |||
| 6cda207f5f | |||
| 5d607a6e06 | |||
| 9e3fbc0157 | |||
| 36828bd9bb | |||
| cfdbde214b | |||
| 22e9c155ae | |||
| d7162fa64e | |||
| c94f31236e | |||
| b92fce1eae | |||
| 3d06237770 | |||
| 5d8a298495 | |||
| 9b1c62d416 | |||
| 67c495ac20 | |||
| 930e1b6a5d | |||
| 5847b5f9c3 | |||
| e5754eec26 | |||
| f51e5f6c99 | |||
| a9605b9125 | |||
| ca62ad57f0 | |||
| 244593c846 | |||
| 9a48bf528d | |||
| b4480e942f | |||
| 1e7a2d4315 | |||
| 245b49b203 | |||
| ff4d0f41bb | |||
| 7aa469cdd3 | |||
| 157546f4df | |||
| db04571c79 | |||
| 8cc9c1aa15 | |||
| 12ca0b5695 | |||
| 27453faef8 | |||
| 4e895da185 | |||
| 77e6500473 | |||
| a2df3d9f95 | |||
| d7fb7d24c6 | |||
| 1f34f8cc71 | |||
| 4565faa990 | |||
| 4131639c63 | |||
| 7cf7df7d35 | |||
| 319eeb7b33 | |||
| 516ae18005 | |||
| 4c22025225 | |||
| 304637c026 | |||
| 2635a3be20 | |||
| ea022cffdd | |||
| c21111daff | |||
| 0639c34ecd | |||
| e8d5648e16 | |||
| 37db03bf73 | |||
| 6566ebe1b6 | |||
| 296c76589f | |||
| 0c698471d1 | |||
| 37f133ec31 | |||
| d5f0767c9c | |||
| 91ddd5595b | |||
| 4fccf43aae | |||
| 21e8d0126d | |||
| 46c47d4677 |
+19
-3
@@ -351,6 +351,8 @@ RCC = $(RCC) -DSQLITE_TEMP_STORE=1
|
||||
OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_FTS3=1
|
||||
OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_RTREE=1
|
||||
OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_COLUMN_METADATA=1
|
||||
OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_SESSION=1
|
||||
OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_PREUPDATE_HOOK=1
|
||||
# END standard options
|
||||
|
||||
# BEGIN required Windows option
|
||||
@@ -607,6 +609,9 @@ SRC = $(SRC) \
|
||||
SRC = $(SRC) \
|
||||
$(TOP)\ext\rtree\rtree.h \
|
||||
$(TOP)\ext\rtree\rtree.c
|
||||
SRC = $(SRC) \
|
||||
$(TOP)\ext\session\sqlite3session.h \
|
||||
$(TOP)\ext\session\sqlite3session.c
|
||||
|
||||
|
||||
# Generated source code files
|
||||
@@ -663,9 +668,11 @@ TESTSRC = \
|
||||
$(TOP)\src\test_wholenumber.c \
|
||||
$(TOP)\src\test_wsd.c \
|
||||
$(TOP)\ext\fts3\fts3_term.c \
|
||||
$(TOP)\ext\fts3\fts3_test.c
|
||||
$(TOP)\ext\fts3\fts3_test.c \
|
||||
$(TOP)\ext\session\test_session.c
|
||||
|
||||
# Source code to the library files needed by the test fixture
|
||||
# (non-amalgamation)
|
||||
#
|
||||
TESTSRC2 = \
|
||||
$(TOP)\src\attach.c \
|
||||
@@ -709,7 +716,14 @@ TESTSRC2 = \
|
||||
$(TOP)\ext\fts3\fts3_unicode.c \
|
||||
$(TOP)\ext\fts3\fts3_unicode2.c \
|
||||
$(TOP)\ext\fts3\fts3_write.c \
|
||||
$(TOP)\ext\async\sqlite3async.c
|
||||
$(TOP)\ext\async\sqlite3async.c \
|
||||
$(TOP)\ext\session\sqlite3session.c
|
||||
|
||||
# Source code to the library files needed by the test fixture
|
||||
# (amalgamation)
|
||||
#
|
||||
TESTSRC3 = \
|
||||
$(TOP)\ext\session\sqlite3session.c
|
||||
|
||||
# Header files used by all library source files.
|
||||
#
|
||||
@@ -754,6 +768,8 @@ EXTHDR = $(EXTHDR) \
|
||||
$(TOP)\ext\icu\sqliteicu.h
|
||||
EXTHDR = $(EXTHDR) \
|
||||
$(TOP)\ext\rtree\sqlite3rtree.h
|
||||
EXTHDR = $(EXTHDR) \
|
||||
$(TOP)\ext\session\sqlite3session.h
|
||||
|
||||
# This is the default Makefile target. The objects listed here
|
||||
# are what get build when you type just "make" with no arguments.
|
||||
@@ -1150,7 +1166,7 @@ TESTFIXTURE_FLAGS = -DTCLSH=1 -DSQLITE_TEST=1 -DSQLITE_CRASH_TEST=1
|
||||
TESTFIXTURE_FLAGS = $(TESTFIXTURE_FLAGS) -DSQLITE_SERVER=1 -DSQLITE_PRIVATE="" -DSQLITE_CORE
|
||||
|
||||
TESTFIXTURE_SRC0 = $(TESTSRC2) libsqlite3.lib
|
||||
TESTFIXTURE_SRC1 = sqlite3.c
|
||||
TESTFIXTURE_SRC1 = $(TESTSRC3) sqlite3.c
|
||||
!IF $(USE_AMALGAMATION)==0
|
||||
TESTFIXTURE_SRC = $(TESTSRC) $(TOP)\src\tclsqlite.c $(TESTFIXTURE_SRC0)
|
||||
!ELSE
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
# 2011 March 07
|
||||
#
|
||||
# The author disclaims copyright to this source code. In place of
|
||||
# a legal notice, here is a blessing:
|
||||
#
|
||||
# May you do good and not evil.
|
||||
# May you find forgiveness for yourself and forgive others.
|
||||
# May you share freely, never taking more than you give.
|
||||
#
|
||||
#***********************************************************************
|
||||
# This file implements regression tests for SQLite library.
|
||||
#
|
||||
|
||||
if {![info exists testdir]} {
|
||||
set testdir [file join [file dirname [info script]] .. .. test]
|
||||
}
|
||||
source [file join [file dirname [info script]] session_common.tcl]
|
||||
source $testdir/tester.tcl
|
||||
ifcapable !session {finish_test; return}
|
||||
|
||||
set testprefix session1
|
||||
|
||||
proc do_changeset_test {tn session res} {
|
||||
set r [list]
|
||||
foreach x $res {lappend r $x}
|
||||
uplevel do_test $tn [list [subst -nocommands {
|
||||
set x [list]
|
||||
sqlite3session_foreach c [$session changeset] { lappend x [set c] }
|
||||
set x
|
||||
}]] [list $r]
|
||||
}
|
||||
|
||||
proc do_changeset_invert_test {tn session res} {
|
||||
set r [list]
|
||||
foreach x $res {lappend r $x}
|
||||
uplevel do_test $tn [list [subst -nocommands {
|
||||
set x [list]
|
||||
set changeset [sqlite3changeset_invert [$session changeset]]
|
||||
sqlite3session_foreach c [set changeset] { lappend x [set c] }
|
||||
set x
|
||||
}]] [list $r]
|
||||
}
|
||||
|
||||
do_execsql_test 1.0 {
|
||||
CREATE TABLE t1(x PRIMARY KEY, y);
|
||||
INSERT INTO t1 VALUES('abc', 'def');
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# Test creating, attaching tables to and deleting session objects.
|
||||
#
|
||||
do_test 1.1 { sqlite3session S db main } {S}
|
||||
do_test 1.2 { S delete } {}
|
||||
do_test 1.3 { sqlite3session S db main } {S}
|
||||
do_test 1.4 { S attach t1 } {}
|
||||
do_test 1.5 { S delete } {}
|
||||
do_test 1.6 { sqlite3session S db main } {S}
|
||||
do_test 1.7 { S attach t1 ; S attach t2 ; S attach t3 } {}
|
||||
do_test 1.8 { S attach t1 ; S attach t2 ; S attach t3 } {}
|
||||
do_test 1.9 { S delete } {}
|
||||
do_test 1.10 {
|
||||
sqlite3session S db main
|
||||
S attach t1
|
||||
execsql { INSERT INTO t1 VALUES('ghi', 'jkl') }
|
||||
} {}
|
||||
do_test 1.11 { S delete } {}
|
||||
do_test 1.12 {
|
||||
sqlite3session S db main
|
||||
S attach t1
|
||||
execsql { INSERT INTO t1 VALUES('mno', 'pqr') }
|
||||
execsql { UPDATE t1 SET x = 111 WHERE rowid = 1 }
|
||||
execsql { DELETE FROM t1 WHERE rowid = 2 }
|
||||
} {}
|
||||
do_test 1.13 {
|
||||
S changeset
|
||||
S delete
|
||||
} {}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# Simple changeset tests. Also test the sqlite3changeset_invert()
|
||||
# function.
|
||||
#
|
||||
do_test 2.1.1 {
|
||||
execsql { DELETE FROM t1 }
|
||||
sqlite3session S db main
|
||||
S attach t1
|
||||
execsql { INSERT INTO t1 VALUES(1, 'Sukhothai') }
|
||||
execsql { INSERT INTO t1 VALUES(2, 'Ayutthaya') }
|
||||
execsql { INSERT INTO t1 VALUES(3, 'Thonburi') }
|
||||
} {}
|
||||
do_changeset_test 2.1.2 S {
|
||||
{INSERT t1 0 X. {} {i 1 t Sukhothai}}
|
||||
{INSERT t1 0 X. {} {i 2 t Ayutthaya}}
|
||||
{INSERT t1 0 X. {} {i 3 t Thonburi}}
|
||||
}
|
||||
do_changeset_invert_test 2.1.3 S {
|
||||
{DELETE t1 0 X. {i 1 t Sukhothai} {}}
|
||||
{DELETE t1 0 X. {i 2 t Ayutthaya} {}}
|
||||
{DELETE t1 0 X. {i 3 t Thonburi} {}}
|
||||
}
|
||||
do_test 2.1.4 { S delete } {}
|
||||
|
||||
do_test 2.2.1 {
|
||||
sqlite3session S db main
|
||||
S attach t1
|
||||
execsql { DELETE FROM t1 WHERE 1 }
|
||||
} {}
|
||||
do_changeset_test 2.2.2 S {
|
||||
{DELETE t1 0 X. {i 1 t Sukhothai} {}}
|
||||
{DELETE t1 0 X. {i 2 t Ayutthaya} {}}
|
||||
{DELETE t1 0 X. {i 3 t Thonburi} {}}
|
||||
}
|
||||
do_changeset_invert_test 2.2.3 S {
|
||||
{INSERT t1 0 X. {} {i 1 t Sukhothai}}
|
||||
{INSERT t1 0 X. {} {i 2 t Ayutthaya}}
|
||||
{INSERT t1 0 X. {} {i 3 t Thonburi}}
|
||||
}
|
||||
do_test 2.2.4 { S delete } {}
|
||||
|
||||
do_test 2.3.1 {
|
||||
execsql { DELETE FROM t1 }
|
||||
sqlite3session S db main
|
||||
execsql { INSERT INTO t1 VALUES(1, 'Sukhothai') }
|
||||
execsql { INSERT INTO t1 VALUES(2, 'Ayutthaya') }
|
||||
execsql { INSERT INTO t1 VALUES(3, 'Thonburi') }
|
||||
S attach t1
|
||||
execsql {
|
||||
UPDATE t1 SET x = 10 WHERE x = 1;
|
||||
UPDATE t1 SET y = 'Surin' WHERE x = 2;
|
||||
UPDATE t1 SET x = 20, y = 'Thapae' WHERE x = 3;
|
||||
}
|
||||
} {}
|
||||
|
||||
do_changeset_test 2.3.2 S {
|
||||
{INSERT t1 0 X. {} {i 10 t Sukhothai}}
|
||||
{DELETE t1 0 X. {i 1 t Sukhothai} {}}
|
||||
{UPDATE t1 0 X. {i 2 t Ayutthaya} {{} {} t Surin}}
|
||||
{DELETE t1 0 X. {i 3 t Thonburi} {}}
|
||||
{INSERT t1 0 X. {} {i 20 t Thapae}}
|
||||
}
|
||||
|
||||
do_changeset_invert_test 2.3.3 S {
|
||||
{DELETE t1 0 X. {i 10 t Sukhothai} {}}
|
||||
{INSERT t1 0 X. {} {i 1 t Sukhothai}}
|
||||
{UPDATE t1 0 X. {i 2 t Surin} {{} {} t Ayutthaya}}
|
||||
{INSERT t1 0 X. {} {i 3 t Thonburi}}
|
||||
{DELETE t1 0 X. {i 20 t Thapae} {}}
|
||||
}
|
||||
do_test 2.3.4 { S delete } {}
|
||||
|
||||
do_test 2.4.1 {
|
||||
sqlite3session S db main
|
||||
S attach t1
|
||||
execsql { INSERT INTO t1 VALUES(100, 'Bangkok') }
|
||||
execsql { DELETE FROM t1 WHERE x = 100 }
|
||||
} {}
|
||||
do_changeset_test 2.4.2 S {}
|
||||
do_changeset_invert_test 2.4.3 S {}
|
||||
do_test 2.4.4 { S delete } {}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# Test the application of simple changesets. These tests also test that
|
||||
# the conflict callback is invoked correctly. For these tests, the
|
||||
# conflict callback always returns OMIT.
|
||||
#
|
||||
db close
|
||||
forcedelete test.db test.db2
|
||||
sqlite3 db test.db
|
||||
sqlite3 db2 test.db2
|
||||
|
||||
proc xConflict {args} {
|
||||
lappend ::xConflict $args
|
||||
return ""
|
||||
}
|
||||
|
||||
proc bgerror {args} { set ::background_error $args }
|
||||
|
||||
proc do_conflict_test {tn args} {
|
||||
set O(-tables) [list]
|
||||
set O(-sql) [list]
|
||||
set O(-conflicts) [list]
|
||||
|
||||
array set V $args
|
||||
foreach key [array names V] {
|
||||
if {![info exists O($key)]} {error "no such option: $key"}
|
||||
}
|
||||
array set O $args
|
||||
|
||||
sqlite3session S db main
|
||||
foreach t $O(-tables) { S attach $t }
|
||||
execsql $O(-sql)
|
||||
set ::xConflict [list]
|
||||
sqlite3changeset_apply db2 [S changeset] xConflict
|
||||
|
||||
set conflicts [list]
|
||||
foreach c $O(-conflicts) {
|
||||
lappend conflicts $c
|
||||
}
|
||||
|
||||
after 1 {set go 1}
|
||||
vwait go
|
||||
|
||||
uplevel do_test $tn [list { set ::xConflict }] [list $conflicts]
|
||||
S delete
|
||||
}
|
||||
|
||||
proc do_db2_test {testname sql {result {}}} {
|
||||
uplevel do_test $testname [list "execsql {$sql} db2"] [list [list {*}$result]]
|
||||
}
|
||||
|
||||
# Test INSERT changesets.
|
||||
#
|
||||
do_test 3.1.0 {
|
||||
execsql { CREATE TABLE t1(a PRIMARY KEY, b NOT NULL) } db2
|
||||
execsql {
|
||||
CREATE TABLE t1(a PRIMARY KEY, b);
|
||||
INSERT INTO t1 VALUES(1, 'one');
|
||||
INSERT INTO t1 VALUES(2, 'two');
|
||||
} db
|
||||
} {}
|
||||
do_db2_test 3.1.1 "INSERT INTO t1 VALUES(6, 'VI')"
|
||||
do_conflict_test 3.1.2 -tables t1 -sql {
|
||||
INSERT INTO t1 VALUES(3, 'three');
|
||||
INSERT INTO t1 VALUES(4, 'four');
|
||||
INSERT INTO t1 VALUES(5, 'five');
|
||||
INSERT INTO t1 VALUES(6, 'six');
|
||||
INSERT INTO t1 VALUES(7, 'seven');
|
||||
INSERT INTO t1 VALUES(8, NULL);
|
||||
} -conflicts {
|
||||
{INSERT t1 CONSTRAINT {i 8 n {}}}
|
||||
{INSERT t1 CONFLICT {i 6 t six} {i 6 t VI}}
|
||||
}
|
||||
|
||||
do_db2_test 3.1.3 "SELECT * FROM t1" {
|
||||
6 VI 3 three 4 four 5 five 7 seven
|
||||
}
|
||||
do_execsql_test 3.1.4 "SELECT * FROM t1" {
|
||||
1 one 2 two 3 three 4 four 5 five 6 six 7 seven 8 {}
|
||||
}
|
||||
|
||||
# Test DELETE changesets.
|
||||
#
|
||||
do_execsql_test 3.2.1 {
|
||||
PRAGMA foreign_keys = on;
|
||||
CREATE TABLE t2(a PRIMARY KEY, b);
|
||||
CREATE TABLE t3(c, d REFERENCES t2);
|
||||
INSERT INTO t2 VALUES(1, 'one');
|
||||
INSERT INTO t2 VALUES(2, 'two');
|
||||
INSERT INTO t2 VALUES(3, 'three');
|
||||
INSERT INTO t2 VALUES(4, 'four');
|
||||
}
|
||||
do_db2_test 3.2.2 {
|
||||
PRAGMA foreign_keys = on;
|
||||
CREATE TABLE t2(a PRIMARY KEY, b);
|
||||
CREATE TABLE t3(c, d REFERENCES t2);
|
||||
INSERT INTO t2 VALUES(1, 'one');
|
||||
INSERT INTO t2 VALUES(2, 'two');
|
||||
INSERT INTO t2 VALUES(4, 'five');
|
||||
INSERT INTO t3 VALUES('i', 1);
|
||||
}
|
||||
do_conflict_test 3.2.3 -tables t2 -sql {
|
||||
DELETE FROM t2 WHERE a = 1;
|
||||
DELETE FROM t2 WHERE a = 2;
|
||||
DELETE FROM t2 WHERE a = 3;
|
||||
DELETE FROM t2 WHERE a = 4;
|
||||
} -conflicts {
|
||||
{DELETE t2 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}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# Test that session objects are not confused by changes to table in
|
||||
# other databases.
|
||||
#
|
||||
catch { db2 close }
|
||||
drop_all_tables
|
||||
forcedelete test.db2
|
||||
do_iterator_test 7.1 * {
|
||||
ATTACH 'test.db2' AS aux;
|
||||
CREATE TABLE main.t1(x PRIMARY KEY, y);
|
||||
CREATE TABLE aux.t1(x PRIMARY KEY, y);
|
||||
|
||||
INSERT INTO main.t1 VALUES('one', 1);
|
||||
INSERT INTO main.t1 VALUES('two', 2);
|
||||
INSERT INTO aux.t1 VALUES('three', 3);
|
||||
INSERT INTO aux.t1 VALUES('four', 4);
|
||||
} {
|
||||
{INSERT t1 0 X. {} {t two i 2}}
|
||||
{INSERT t1 0 X. {} {t one i 1}}
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# Test the sqlite3session_isempty() function.
|
||||
#
|
||||
do_test 8.1 {
|
||||
execsql {
|
||||
CREATE TABLE t5(x PRIMARY KEY, y);
|
||||
CREATE TABLE t6(x PRIMARY KEY, y);
|
||||
INSERT INTO t5 VALUES('a', 'b');
|
||||
INSERT INTO t6 VALUES('a', 'b');
|
||||
}
|
||||
sqlite3session S db main
|
||||
S attach *
|
||||
|
||||
S isempty
|
||||
} {1}
|
||||
do_test 8.2 {
|
||||
execsql { DELETE FROM t5 }
|
||||
S isempty
|
||||
} {0}
|
||||
do_test 8.3 {
|
||||
S delete
|
||||
sqlite3session S db main
|
||||
S attach t5
|
||||
execsql { DELETE FROM t5 }
|
||||
S isempty
|
||||
} {1}
|
||||
do_test 8.4 { S delete } {}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
do_execsql_test 9.1 {
|
||||
CREATE TABLE t7(a, b, c, d, e PRIMARY KEY, f, g);
|
||||
INSERT INTO t7 VALUES(1, 1, 1, 1, 1, 1, 1);
|
||||
}
|
||||
do_test 9.2 {
|
||||
sqlite3session S db main
|
||||
S attach *
|
||||
execsql { UPDATE t7 SET b=2, d=2 }
|
||||
} {}
|
||||
do_changeset_test 9.2 S {{UPDATE t7 0 ....X.. {{} {} i 1 {} {} i 1 i 1 {} {} {} {}} {{} {} i 2 {} {} i 2 {} {} {} {} {} {}}}}
|
||||
S delete
|
||||
catch { db2 close }
|
||||
finish_test
|
||||
@@ -0,0 +1,581 @@
|
||||
# 2011 Mar 16
|
||||
#
|
||||
# The author disclaims copyright to this source code. In place of
|
||||
# a legal notice, here is a blessing:
|
||||
#
|
||||
# May you do good and not evil.
|
||||
# May you find forgiveness for yourself and forgive others.
|
||||
# May you share freely, never taking more than you give.
|
||||
#
|
||||
#***********************************************************************
|
||||
#
|
||||
# The focus of this file is testing the session module.
|
||||
#
|
||||
|
||||
if {![info exists testdir]} {
|
||||
set testdir [file join [file dirname [info script]] .. .. test]
|
||||
}
|
||||
source [file join [file dirname [info script]] session_common.tcl]
|
||||
source $testdir/tester.tcl
|
||||
ifcapable !session {finish_test; return}
|
||||
|
||||
set testprefix session2
|
||||
|
||||
proc test_reset {} {
|
||||
catch { db close }
|
||||
catch { db2 close }
|
||||
forcedelete test.db test.db2
|
||||
sqlite3 db test.db
|
||||
sqlite3 db2 test.db2
|
||||
}
|
||||
|
||||
##########################################################################
|
||||
# End of proc definitions. Start of tests.
|
||||
##########################################################################
|
||||
|
||||
test_reset
|
||||
do_execsql_test 1.0 {
|
||||
CREATE TABLE t1(a PRIMARY KEY, b);
|
||||
INSERT INTO t1 VALUES('i', 'one');
|
||||
}
|
||||
do_iterator_test 1.1 t1 {
|
||||
DELETE FROM t1 WHERE a = 'i';
|
||||
INSERT INTO t1 VALUES('ii', 'two');
|
||||
} {
|
||||
{DELETE t1 0 X. {t i t one} {}}
|
||||
{INSERT t1 0 X. {} {t ii t two}}
|
||||
}
|
||||
|
||||
do_iterator_test 1.2 t1 {
|
||||
INSERT INTO t1 VALUES(1.5, 99.9)
|
||||
} {
|
||||
{INSERT t1 0 X. {} {f 1.5 f 99.9}}
|
||||
}
|
||||
|
||||
do_iterator_test 1.3 t1 {
|
||||
UPDATE t1 SET b = 100.1 WHERE a = 1.5;
|
||||
UPDATE t1 SET b = 99.9 WHERE a = 1.5;
|
||||
} { }
|
||||
|
||||
do_iterator_test 1.4 t1 {
|
||||
UPDATE t1 SET b = 100.1 WHERE a = 1.5;
|
||||
} {
|
||||
{UPDATE t1 0 X. {f 1.5 f 99.9} {{} {} f 100.1}}
|
||||
}
|
||||
|
||||
|
||||
# Execute each of the following blocks of SQL on database [db1]. Collect
|
||||
# changes using a session object. Apply the resulting changeset to
|
||||
# database [db2]. Then check that the contents of the two databases are
|
||||
# identical.
|
||||
#
|
||||
|
||||
set set_of_tests {
|
||||
1 { INSERT INTO %T1% VALUES(1, 2) }
|
||||
|
||||
2 {
|
||||
INSERT INTO %T2% VALUES(1, NULL);
|
||||
INSERT INTO %T2% VALUES(2, NULL);
|
||||
INSERT INTO %T2% VALUES(3, NULL);
|
||||
DELETE FROM %T2% WHERE a = 2;
|
||||
INSERT INTO %T2% VALUES(4, NULL);
|
||||
UPDATE %T2% SET b=0 WHERE b=1;
|
||||
}
|
||||
|
||||
3 { INSERT INTO %T3% SELECT *, NULL FROM %T2% }
|
||||
|
||||
4 {
|
||||
INSERT INTO %T3% SELECT a||a, b||b, NULL FROM %T3%;
|
||||
DELETE FROM %T3% WHERE rowid%2;
|
||||
}
|
||||
|
||||
5 { UPDATE %T3% SET c = a||b }
|
||||
|
||||
6 { UPDATE %T1% SET a = 32 }
|
||||
|
||||
7 {
|
||||
INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
|
||||
INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
|
||||
INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
|
||||
INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
|
||||
INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
|
||||
INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
|
||||
INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
|
||||
INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
|
||||
INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
|
||||
INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
|
||||
INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
|
||||
DELETE FROM %T1% WHERE (rowid%3)==0;
|
||||
}
|
||||
|
||||
8 {
|
||||
BEGIN;
|
||||
INSERT INTO %T1% SELECT randomblob(32), randomblob(32) FROM %T1%;
|
||||
ROLLBACK;
|
||||
}
|
||||
9 {
|
||||
BEGIN;
|
||||
UPDATE %T1% SET b = 'xxx';
|
||||
ROLLBACK;
|
||||
}
|
||||
10 {
|
||||
BEGIN;
|
||||
DELETE FROM %T1% WHERE 1;
|
||||
ROLLBACK;
|
||||
}
|
||||
11 {
|
||||
INSERT INTO %T1% VALUES(randomblob(21000), randomblob(0));
|
||||
INSERT INTO %T1% VALUES(1.5, 1.5);
|
||||
INSERT INTO %T1% VALUES(4.56, -99.999999999999999999999);
|
||||
}
|
||||
12 {
|
||||
INSERT INTO %T2% VALUES(NULL, NULL);
|
||||
}
|
||||
|
||||
13 {
|
||||
DELETE FROM %T1% WHERE 1;
|
||||
|
||||
-- Insert many rows with real primary keys. Enough to force the session
|
||||
-- objects hash table to resize.
|
||||
INSERT INTO %T1% VALUES(0.1, 0.1);
|
||||
INSERT INTO %T1% SELECT a+0.1, b+0.1 FROM %T1%;
|
||||
INSERT INTO %T1% SELECT a+0.2, b+0.2 FROM %T1%;
|
||||
INSERT INTO %T1% SELECT a+0.4, b+0.4 FROM %T1%;
|
||||
INSERT INTO %T1% SELECT a+0.8, b+0.8 FROM %T1%;
|
||||
INSERT INTO %T1% SELECT a+1.6, b+1.6 FROM %T1%;
|
||||
INSERT INTO %T1% SELECT a+3.2, b+3.2 FROM %T1%;
|
||||
INSERT INTO %T1% SELECT a+6.4, b+6.4 FROM %T1%;
|
||||
INSERT INTO %T1% SELECT a+12.8, b+12.8 FROM %T1%;
|
||||
INSERT INTO %T1% SELECT a+25.6, b+25.6 FROM %T1%;
|
||||
INSERT INTO %T1% SELECT a+51.2, b+51.2 FROM %T1%;
|
||||
INSERT INTO %T1% SELECT a+102.4, b+102.4 FROM %T1%;
|
||||
INSERT INTO %T1% SELECT a+204.8, b+204.8 FROM %T1%;
|
||||
}
|
||||
|
||||
14 {
|
||||
DELETE FROM %T1% WHERE 1;
|
||||
}
|
||||
|
||||
15 {
|
||||
INSERT INTO %T1% VALUES(1, 1);
|
||||
INSERT INTO %T1% SELECT a+2, b+2 FROM %T1%;
|
||||
INSERT INTO %T1% SELECT a+4, b+4 FROM %T1%;
|
||||
INSERT INTO %T1% SELECT a+8, b+8 FROM %T1%;
|
||||
INSERT INTO %T1% SELECT a+256, b+256 FROM %T1%;
|
||||
}
|
||||
|
||||
16 {
|
||||
INSERT INTO %T4% VALUES('abc', 'def');
|
||||
INSERT INTO %T4% VALUES('def', 'abc');
|
||||
}
|
||||
17 { UPDATE %T4% SET b = 1 }
|
||||
18 { DELETE FROM %T4% WHERE 1 }
|
||||
}
|
||||
|
||||
test_reset
|
||||
do_common_sql {
|
||||
CREATE TABLE t1(a PRIMARY KEY, b);
|
||||
CREATE TABLE t2(a, b INTEGER PRIMARY KEY);
|
||||
CREATE TABLE t3(a, b, c, PRIMARY KEY(a, b));
|
||||
CREATE TABLE t4(a, b, PRIMARY KEY(b, a));
|
||||
}
|
||||
|
||||
foreach {tn sql} [string map {%T1% t1 %T2% t2 %T3% t3 %T4% t4} $set_of_tests] {
|
||||
do_then_apply_sql $sql
|
||||
do_test 2.$tn { compare_db db db2 } {}
|
||||
}
|
||||
|
||||
# The following block of tests is similar to the last, except that the
|
||||
# session object is recording changes made to an attached database. The
|
||||
# main database contains a table of the same name as the table being
|
||||
# modified within the attached db.
|
||||
#
|
||||
test_reset
|
||||
forcedelete test.db3
|
||||
sqlite3 db3 test.db3
|
||||
do_test 3.0 {
|
||||
execsql {
|
||||
ATTACH 'test.db3' AS 'aux';
|
||||
CREATE TABLE t1(a, b PRIMARY KEY);
|
||||
CREATE TABLE t2(x, y, z);
|
||||
CREATE TABLE t3(a);
|
||||
|
||||
CREATE TABLE aux.t1(a PRIMARY KEY, b);
|
||||
CREATE TABLE aux.t2(a, b INTEGER PRIMARY KEY);
|
||||
CREATE TABLE aux.t3(a, b, c, PRIMARY KEY(a, b));
|
||||
CREATE TABLE aux.t4(a, b, PRIMARY KEY(b, a));
|
||||
}
|
||||
execsql {
|
||||
CREATE TABLE t1(a PRIMARY KEY, b);
|
||||
CREATE TABLE t2(a, b INTEGER PRIMARY KEY);
|
||||
CREATE TABLE t3(a, b, c, PRIMARY KEY(a, b));
|
||||
CREATE TABLE t4(a, b, PRIMARY KEY(b, a));
|
||||
} db2
|
||||
} {}
|
||||
|
||||
proc xTrace {args} { puts $args }
|
||||
|
||||
foreach {tn sql} [
|
||||
string map {%T1% aux.t1 %T2% aux.t2 %T3% aux.t3 %T4% aux.t4} $set_of_tests
|
||||
] {
|
||||
do_then_apply_sql $sql aux
|
||||
do_test 3.$tn { compare_db db2 db3 } {}
|
||||
}
|
||||
catch {db3 close}
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# The following tests verify that NULL values in primary key columns are
|
||||
# handled correctly by the session module.
|
||||
#
|
||||
test_reset
|
||||
do_execsql_test 4.0 {
|
||||
CREATE TABLE t1(a PRIMARY KEY);
|
||||
CREATE TABLE t2(a, b, c, PRIMARY KEY(c, b));
|
||||
CREATE TABLE t3(a, b INTEGER PRIMARY KEY);
|
||||
}
|
||||
|
||||
foreach {tn sql changeset} {
|
||||
1 {
|
||||
INSERT INTO t1 VALUES(123);
|
||||
INSERT INTO t1 VALUES(NULL);
|
||||
INSERT INTO t1 VALUES(456);
|
||||
} {
|
||||
{INSERT t1 0 X {} {i 456}}
|
||||
{INSERT t1 0 X {} {i 123}}
|
||||
}
|
||||
|
||||
2 {
|
||||
UPDATE t1 SET a = NULL;
|
||||
} {
|
||||
{DELETE t1 0 X {i 456} {}}
|
||||
{DELETE t1 0 X {i 123} {}}
|
||||
}
|
||||
|
||||
3 { DELETE FROM t1 } { }
|
||||
|
||||
4 {
|
||||
INSERT INTO t3 VALUES(NULL, NULL)
|
||||
} {
|
||||
{INSERT t3 0 .X {} {n {} i 1}}
|
||||
}
|
||||
|
||||
5 { INSERT INTO t2 VALUES(1, 2, NULL) } { }
|
||||
6 { INSERT INTO t2 VALUES(1, NULL, 3) } { }
|
||||
7 { INSERT INTO t2 VALUES(1, NULL, NULL) } { }
|
||||
8 { INSERT INTO t2 VALUES(1, 2, 3) } { {INSERT t2 0 .XX {} {i 1 i 2 i 3}} }
|
||||
9 { DELETE FROM t2 WHERE 1 } { {DELETE t2 0 .XX {i 1 i 2 i 3} {}} }
|
||||
|
||||
} {
|
||||
do_iterator_test 4.$tn {t1 t2 t3} $sql $changeset
|
||||
}
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# Test that if NULL is passed to sqlite3session_attach(), all database
|
||||
# tables are attached to the session object.
|
||||
#
|
||||
test_reset
|
||||
do_execsql_test 5.0 {
|
||||
CREATE TABLE t1(a PRIMARY KEY);
|
||||
CREATE TABLE t2(x, y PRIMARY KEY);
|
||||
}
|
||||
|
||||
foreach {tn sql changeset} {
|
||||
1 { INSERT INTO t1 VALUES(35) } { {INSERT t1 0 X {} {i 35}} }
|
||||
2 { INSERT INTO t2 VALUES(36, 37) } { {INSERT t2 0 .X {} {i 36 i 37}} }
|
||||
3 {
|
||||
DELETE FROM t1 WHERE 1;
|
||||
UPDATE t2 SET x = 34;
|
||||
} {
|
||||
{UPDATE t2 0 .X {i 36 i 37} {i 34 {} {}}}
|
||||
{DELETE t1 0 X {i 35} {}}
|
||||
}
|
||||
} {
|
||||
do_iterator_test 5.$tn * $sql $changeset
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# The next block of tests verify that the "indirect" flag is set
|
||||
# correctly within changesets. The indirect flag is set for a change
|
||||
# if either of the following are true:
|
||||
#
|
||||
# * The sqlite3session_indirect() API has been used to set the session
|
||||
# indirect flag to true, or
|
||||
# * The change was made by a trigger.
|
||||
#
|
||||
# If the same row is updated more than once during a session, then the
|
||||
# change is considered indirect only if all changes meet the criteria
|
||||
# above.
|
||||
#
|
||||
test_reset
|
||||
db function indirect [list S indirect]
|
||||
|
||||
do_execsql_test 6.0 {
|
||||
CREATE TABLE t1(a PRIMARY KEY, b, c);
|
||||
|
||||
CREATE TABLE t2(x PRIMARY KEY, y);
|
||||
CREATE TRIGGER AFTER INSERT ON t2 WHEN new.x%2 BEGIN
|
||||
INSERT INTO t2 VALUES(new.x+1, NULL);
|
||||
END;
|
||||
}
|
||||
|
||||
do_iterator_test 6.1.1 * {
|
||||
INSERT INTO t1 VALUES(1, 'one', 'i');
|
||||
SELECT indirect(1);
|
||||
INSERT INTO t1 VALUES(2, 'two', 'ii');
|
||||
SELECT indirect(0);
|
||||
INSERT INTO t1 VALUES(3, 'three', 'iii');
|
||||
} {
|
||||
{INSERT t1 0 X.. {} {i 1 t one t i}}
|
||||
{INSERT t1 1 X.. {} {i 2 t two t ii}}
|
||||
{INSERT t1 0 X.. {} {i 3 t three t iii}}
|
||||
}
|
||||
|
||||
do_iterator_test 6.1.2 * {
|
||||
SELECT indirect(1);
|
||||
UPDATE t1 SET c = 'I' WHERE a = 1;
|
||||
SELECT indirect(0);
|
||||
} {
|
||||
{UPDATE t1 1 X.. {i 1 {} {} t i} {{} {} {} {} t I}}
|
||||
}
|
||||
do_iterator_test 6.1.3 * {
|
||||
SELECT indirect(1);
|
||||
UPDATE t1 SET c = '.' WHERE a = 1;
|
||||
SELECT indirect(0);
|
||||
UPDATE t1 SET c = 'o' WHERE a = 1;
|
||||
} {
|
||||
{UPDATE t1 0 X.. {i 1 {} {} t I} {{} {} {} {} t o}}
|
||||
}
|
||||
do_iterator_test 6.1.4 * {
|
||||
SELECT indirect(0);
|
||||
UPDATE t1 SET c = 'x' WHERE a = 1;
|
||||
SELECT indirect(1);
|
||||
UPDATE t1 SET c = 'i' WHERE a = 1;
|
||||
} {
|
||||
{UPDATE t1 0 X.. {i 1 {} {} t o} {{} {} {} {} t i}}
|
||||
}
|
||||
do_iterator_test 6.1.4 * {
|
||||
SELECT indirect(1);
|
||||
UPDATE t1 SET c = 'y' WHERE a = 1;
|
||||
SELECT indirect(1);
|
||||
UPDATE t1 SET c = 'I' WHERE a = 1;
|
||||
} {
|
||||
{UPDATE t1 1 X.. {i 1 {} {} t i} {{} {} {} {} t I}}
|
||||
}
|
||||
|
||||
do_iterator_test 6.1.5 * {
|
||||
INSERT INTO t2 VALUES(1, 'x');
|
||||
} {
|
||||
{INSERT t2 0 X. {} {i 1 t x}}
|
||||
{INSERT t2 1 X. {} {i 2 n {}}}
|
||||
}
|
||||
|
||||
do_iterator_test 6.1.6 * {
|
||||
SELECT indirect(1);
|
||||
INSERT INTO t2 VALUES(3, 'x');
|
||||
SELECT indirect(0);
|
||||
UPDATE t2 SET y = 'y' WHERE x>2;
|
||||
} {
|
||||
{INSERT t2 0 X. {} {i 3 t y}}
|
||||
{INSERT t2 0 X. {} {i 4 t y}}
|
||||
}
|
||||
|
||||
do_iterator_test 6.1.7 * {
|
||||
SELECT indirect(1);
|
||||
DELETE FROM t2 WHERE x = 4;
|
||||
SELECT indirect(0);
|
||||
INSERT INTO t2 VALUES(4, 'new');
|
||||
} {
|
||||
{UPDATE t2 0 X. {i 4 t y} {{} {} t new}}
|
||||
}
|
||||
|
||||
do_iterator_test 6.1.8 * {
|
||||
CREATE TABLE t3(a, b PRIMARY KEY);
|
||||
CREATE TABLE t4(a, b PRIMARY KEY);
|
||||
CREATE TRIGGER t4t AFTER UPDATE ON t4 BEGIN
|
||||
UPDATE t3 SET a = new.a WHERE b = new.b;
|
||||
END;
|
||||
|
||||
SELECT indirect(1);
|
||||
INSERT INTO t3 VALUES('one', 1);
|
||||
INSERT INTO t4 VALUES('one', 1);
|
||||
SELECT indirect(0);
|
||||
UPDATE t4 SET a = 'two' WHERE b = 1;
|
||||
} {
|
||||
{INSERT t4 0 .X {} {t two i 1}}
|
||||
{INSERT t3 1 .X {} {t two i 1}}
|
||||
}
|
||||
|
||||
sqlite3session S db main
|
||||
do_execsql_test 6.2.1 {
|
||||
SELECT indirect(0);
|
||||
SELECT indirect(-1);
|
||||
SELECT indirect(45);
|
||||
SELECT indirect(-100);
|
||||
} {0 0 1 1}
|
||||
S delete
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# Test that if a conflict-handler that has been passed either NOTFOUND or
|
||||
# CONSTRAINT returns REPLACE - the sqlite3changeset_apply() call returns
|
||||
# MISUSE and rolls back any changes made so far.
|
||||
#
|
||||
# 7.1.*: NOTFOUND conflict-callback.
|
||||
# 7.2.*: CONSTRAINT conflict-callback.
|
||||
#
|
||||
proc xConflict {args} {return REPLACE}
|
||||
test_reset
|
||||
|
||||
do_execsql_test 7.1.1 {
|
||||
CREATE TABLE t1(a PRIMARY KEY, b);
|
||||
INSERT INTO t1 VALUES(1, 'one');
|
||||
INSERT INTO t1 VALUES(2, 'two');
|
||||
}
|
||||
do_test 7.1.2 {
|
||||
execsql {
|
||||
CREATE TABLE t1(a PRIMARY KEY, b NOT NULL);
|
||||
INSERT INTO t1 VALUES(1, 'one');
|
||||
} db2
|
||||
} {}
|
||||
do_test 7.1.3 {
|
||||
set changeset [changeset_from_sql {
|
||||
UPDATE t1 SET b = 'five' WHERE a = 1;
|
||||
UPDATE t1 SET b = 'six' WHERE a = 2;
|
||||
}]
|
||||
set x [list]
|
||||
sqlite3session_foreach c $changeset { lappend x $c }
|
||||
set x
|
||||
} [list \
|
||||
{UPDATE t1 0 X. {i 1 t one} {{} {} t five}} \
|
||||
{UPDATE t1 0 X. {i 2 t two} {{} {} t six}} \
|
||||
]
|
||||
do_test 7.1.4 {
|
||||
list [catch {sqlite3changeset_apply db2 $changeset xConflict} msg] $msg
|
||||
} {1 SQLITE_MISUSE}
|
||||
do_test 7.1.5 { execsql { SELECT * FROM t1 } db2 } {1 one}
|
||||
|
||||
do_test 7.2.1 {
|
||||
set changeset [changeset_from_sql { UPDATE t1 SET b = NULL WHERE a = 1 }]
|
||||
|
||||
set x [list]
|
||||
sqlite3session_foreach c $changeset { lappend x $c }
|
||||
set x
|
||||
} [list \
|
||||
{UPDATE t1 0 X. {i 1 t five} {{} {} n {}}} \
|
||||
]
|
||||
do_test 7.2.2 {
|
||||
list [catch {sqlite3changeset_apply db2 $changeset xConflict} msg] $msg
|
||||
} {1 SQLITE_MISUSE}
|
||||
do_test 7.2.3 { execsql { SELECT * FROM t1 } db2 } {1 one}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# Test that if a conflict-handler returns ABORT, application of the
|
||||
# changeset is rolled back and the sqlite3changeset_apply() method returns
|
||||
# SQLITE_ABORT.
|
||||
#
|
||||
# Also test that the same thing happens if a conflict handler returns an
|
||||
# unrecognized integer value. Except, in this case SQLITE_MISUSE is returned
|
||||
# instead of SQLITE_ABORT.
|
||||
#
|
||||
foreach {tn conflict_return apply_return} {
|
||||
1 ABORT SQLITE_ABORT
|
||||
2 567 SQLITE_MISUSE
|
||||
} {
|
||||
test_reset
|
||||
proc xConflict {args} [list return $conflict_return]
|
||||
|
||||
do_test 8.$tn.0 {
|
||||
do_common_sql {
|
||||
CREATE TABLE t1(x, y, PRIMARY KEY(x, y));
|
||||
INSERT INTO t1 VALUES('x', 'y');
|
||||
}
|
||||
execsql { INSERT INTO t1 VALUES('w', 'w') }
|
||||
|
||||
set changeset [changeset_from_sql { DELETE FROM t1 WHERE 1 }]
|
||||
|
||||
set x [list]
|
||||
sqlite3session_foreach c $changeset { lappend x $c }
|
||||
set x
|
||||
} [list \
|
||||
{DELETE t1 0 XX {t w t w} {}} \
|
||||
{DELETE t1 0 XX {t x t y} {}} \
|
||||
]
|
||||
|
||||
do_test 8.$tn.1 {
|
||||
list [catch {sqlite3changeset_apply db2 $changeset xConflict} msg] $msg
|
||||
} [list 1 $apply_return]
|
||||
|
||||
do_test 8.$tn.2 {
|
||||
execsql {SELECT * FROM t1} db2
|
||||
} {x y}
|
||||
}
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# Try to cause an infinite loop as follows:
|
||||
#
|
||||
# 1. Have a changeset insert a row that causes a CONFLICT callback,
|
||||
# 2. Have the conflict handler return REPLACE,
|
||||
# 3. After the session module deletes the conflicting row, have a trigger
|
||||
# re-insert it.
|
||||
# 4. Goto step 1...
|
||||
#
|
||||
# This doesn't work, as the second invocation of the conflict handler is a
|
||||
# CONSTRAINT, not a CONFLICT. There is at most one CONFLICT callback for
|
||||
# each change in the changeset.
|
||||
#
|
||||
test_reset
|
||||
proc xConflict {type args} {
|
||||
if {$type == "CONFLICT"} { return REPLACE }
|
||||
return OMIT
|
||||
}
|
||||
do_test 9.1 {
|
||||
execsql {
|
||||
CREATE TABLE t1(a PRIMARY KEY, b);
|
||||
}
|
||||
execsql {
|
||||
CREATE TABLE t1(a PRIMARY KEY, b);
|
||||
INSERT INTO t1 VALUES('x', 2);
|
||||
CREATE TRIGGER tr1 AFTER DELETE ON t1 BEGIN
|
||||
INSERT INTO t1 VALUES(old.a, old.b);
|
||||
END;
|
||||
} db2
|
||||
} {}
|
||||
do_test 9.2 {
|
||||
set changeset [changeset_from_sql { INSERT INTO t1 VALUES('x', 1) }]
|
||||
sqlite3changeset_apply db2 $changeset xConflict
|
||||
} {}
|
||||
do_test 9.3 {
|
||||
execsql { SELECT * FROM t1 } db2
|
||||
} {x 2}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
test_reset
|
||||
db function enable [list S enable]
|
||||
|
||||
do_common_sql {
|
||||
CREATE TABLE t1(a PRIMARY KEY, b);
|
||||
INSERT INTO t1 VALUES('x', 'X');
|
||||
}
|
||||
|
||||
do_iterator_test 10.1 t1 {
|
||||
INSERT INTO t1 VALUES('y', 'Y');
|
||||
SELECT enable(0);
|
||||
INSERT INTO t1 VALUES('z', 'Z');
|
||||
SELECT enable(1);
|
||||
} {
|
||||
{INSERT t1 0 X. {} {t y t Y}}
|
||||
}
|
||||
|
||||
sqlite3session S db main
|
||||
do_execsql_test 10.2 {
|
||||
SELECT enable(0);
|
||||
SELECT enable(-1);
|
||||
SELECT enable(1);
|
||||
SELECT enable(-1);
|
||||
} {0 0 1 1}
|
||||
S delete
|
||||
|
||||
finish_test
|
||||
@@ -0,0 +1,211 @@
|
||||
# 2011 March 24
|
||||
#
|
||||
# The author disclaims copyright to this source code. In place of
|
||||
# a legal notice, here is a blessing:
|
||||
#
|
||||
# May you do good and not evil.
|
||||
# May you find forgiveness for yourself and forgive others.
|
||||
# May you share freely, never taking more than you give.
|
||||
#
|
||||
#***********************************************************************
|
||||
# This file implements regression tests for the session module. More
|
||||
# specifically, it focuses on testing the session modules response to
|
||||
# database schema modifications and mismatches.
|
||||
#
|
||||
|
||||
if {![info exists testdir]} {
|
||||
set testdir [file join [file dirname [info script]] .. .. test]
|
||||
}
|
||||
source [file join [file dirname [info script]] session_common.tcl]
|
||||
source $testdir/tester.tcl
|
||||
ifcapable !session {finish_test; return}
|
||||
|
||||
set testprefix session3
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# These tests - session3-1.* - verify that the session module behaves
|
||||
# correctly when confronted with a schema mismatch when applying a
|
||||
# changeset (in function sqlite3changeset_apply()).
|
||||
#
|
||||
# session3-1.1.*: Table does not exist in target db.
|
||||
# session3-1.2.*: Table has wrong number of columns in target db.
|
||||
# session3-1.3.*: Table has wrong PK columns in target db.
|
||||
#
|
||||
db close
|
||||
sqlite3_shutdown
|
||||
test_sqlite3_log log
|
||||
sqlite3 db test.db
|
||||
|
||||
proc log {code msg} { lappend ::log $code $msg }
|
||||
|
||||
forcedelete test.db2
|
||||
sqlite3 db2 test.db2
|
||||
|
||||
do_execsql_test 1.0 {
|
||||
CREATE TABLE t1(a PRIMARY KEY, b);
|
||||
}
|
||||
do_test 1.1 {
|
||||
set ::log {}
|
||||
do_then_apply_sql {
|
||||
INSERT INTO t1 VALUES(1, 2);
|
||||
INSERT INTO t1 VALUES(3, 4);
|
||||
}
|
||||
set ::log
|
||||
} {SQLITE_SCHEMA {sqlite3changeset_apply(): no such table: t1}}
|
||||
|
||||
do_test 1.2.0 {
|
||||
execsql { CREATE TABLE t1(a PRIMARY KEY, b, c) } db2
|
||||
} {}
|
||||
do_test 1.2.1 {
|
||||
set ::log {}
|
||||
do_then_apply_sql {
|
||||
INSERT INTO t1 VALUES(5, 6);
|
||||
INSERT INTO t1 VALUES(7, 8);
|
||||
}
|
||||
set ::log
|
||||
} {SQLITE_SCHEMA {sqlite3changeset_apply(): table t1 has 3 columns, expected 2}}
|
||||
|
||||
do_test 1.3.0 {
|
||||
execsql {
|
||||
DROP TABLE t1;
|
||||
CREATE TABLE t1(a, b PRIMARY KEY);
|
||||
} db2
|
||||
} {}
|
||||
do_test 1.3.1 {
|
||||
set ::log {}
|
||||
do_then_apply_sql {
|
||||
INSERT INTO t1 VALUES(9, 10);
|
||||
INSERT INTO t1 VALUES(11, 12);
|
||||
}
|
||||
set ::log
|
||||
} {SQLITE_SCHEMA {sqlite3changeset_apply(): primary key mismatch for table t1}}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# These tests - session3-2.* - verify that the session module behaves
|
||||
# correctly when the schema of an attached table is modified during the
|
||||
# session.
|
||||
#
|
||||
# session3-2.1.*: Table is dropped midway through the session.
|
||||
# session3-2.2.*: Table is dropped and recreated with a different # cols.
|
||||
# session3-2.3.*: Table is dropped and recreated with a different PK.
|
||||
#
|
||||
# In all of these scenarios, the call to sqlite3session_changeset() will
|
||||
# return SQLITE_SCHEMA. Also:
|
||||
#
|
||||
# session3-2.4.*: Table is dropped and recreated with an identical schema.
|
||||
# In this case sqlite3session_changeset() returns SQLITE_OK.
|
||||
#
|
||||
|
||||
do_test 2.1 {
|
||||
execsql { CREATE TABLE t2(a, b PRIMARY KEY) }
|
||||
sqlite3session S db main
|
||||
S attach t2
|
||||
execsql {
|
||||
INSERT INTO t2 VALUES(1, 2);
|
||||
DROP TABLE t2;
|
||||
}
|
||||
list [catch { S changeset } msg] $msg
|
||||
} {1 SQLITE_SCHEMA}
|
||||
|
||||
do_test 2.2.1 {
|
||||
S delete
|
||||
sqlite3session S db main
|
||||
execsql { CREATE TABLE t2(a, b PRIMARY KEY, c) }
|
||||
S attach t2
|
||||
execsql {
|
||||
INSERT INTO t2 VALUES(1, 2, 3);
|
||||
DROP TABLE t2;
|
||||
CREATE TABLE t2(a, b PRIMARY KEY);
|
||||
}
|
||||
list [catch { S changeset } msg] $msg
|
||||
} {1 SQLITE_SCHEMA}
|
||||
do_test 2.2.2 {
|
||||
S delete
|
||||
sqlite3session S db main
|
||||
execsql {
|
||||
DROP TABLE t2;
|
||||
CREATE TABLE t2(a, b PRIMARY KEY, c);
|
||||
}
|
||||
S attach t2
|
||||
execsql {
|
||||
INSERT INTO t2 VALUES(1, 2, 3);
|
||||
DROP TABLE t2;
|
||||
CREATE TABLE t2(a, b PRIMARY KEY, c, d);
|
||||
}
|
||||
list [catch { S changeset } msg] $msg
|
||||
} {1 SQLITE_SCHEMA}
|
||||
do_test 2.2.3 {
|
||||
S delete
|
||||
sqlite3session S db main
|
||||
execsql {
|
||||
DROP TABLE t2;
|
||||
CREATE TABLE t2(a, b PRIMARY KEY, c);
|
||||
}
|
||||
S attach t2
|
||||
execsql {
|
||||
INSERT INTO t2 VALUES(1, 2, 3);
|
||||
DROP TABLE t2;
|
||||
CREATE TABLE t2(a, b PRIMARY KEY);
|
||||
INSERT INTO t2 VALUES(4, 5);
|
||||
}
|
||||
list [catch { S changeset } msg] $msg
|
||||
} {1 SQLITE_SCHEMA}
|
||||
do_test 2.2.4 {
|
||||
S delete
|
||||
sqlite3session S db main
|
||||
execsql {
|
||||
DROP TABLE t2;
|
||||
CREATE TABLE t2(a, b PRIMARY KEY, c);
|
||||
}
|
||||
S attach t2
|
||||
execsql {
|
||||
INSERT INTO t2 VALUES(1, 2, 3);
|
||||
DROP TABLE t2;
|
||||
CREATE TABLE t2(a, b PRIMARY KEY, c, d);
|
||||
INSERT INTO t2 VALUES(4, 5, 6, 7);
|
||||
}
|
||||
list [catch { S changeset } msg] $msg
|
||||
} {1 SQLITE_SCHEMA}
|
||||
|
||||
do_test 2.3 {
|
||||
S delete
|
||||
sqlite3session S db main
|
||||
execsql {
|
||||
DROP TABLE t2;
|
||||
CREATE TABLE t2(a, b PRIMARY KEY);
|
||||
}
|
||||
S attach t2
|
||||
execsql {
|
||||
INSERT INTO t2 VALUES(1, 2);
|
||||
DROP TABLE t2;
|
||||
CREATE TABLE t2(a PRIMARY KEY, b);
|
||||
}
|
||||
list [catch { S changeset } msg] $msg
|
||||
} {1 SQLITE_SCHEMA}
|
||||
|
||||
do_test 2.4 {
|
||||
S delete
|
||||
sqlite3session S db main
|
||||
execsql {
|
||||
DROP TABLE t2;
|
||||
CREATE TABLE t2(a, b PRIMARY KEY);
|
||||
}
|
||||
S attach t2
|
||||
execsql {
|
||||
INSERT INTO t2 VALUES(1, 2);
|
||||
DROP TABLE t2;
|
||||
CREATE TABLE t2(a, b PRIMARY KEY);
|
||||
}
|
||||
list [catch { S changeset } msg] $msg
|
||||
} {0 {}}
|
||||
|
||||
S delete
|
||||
|
||||
|
||||
catch { db close }
|
||||
catch { db2 close }
|
||||
sqlite3_shutdown
|
||||
test_sqlite3_log
|
||||
sqlite3_initialize
|
||||
|
||||
finish_test
|
||||
@@ -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
|
||||
@@ -0,0 +1,409 @@
|
||||
# 2011 April 13
|
||||
#
|
||||
# The author disclaims copyright to this source code. In place of
|
||||
# a legal notice, here is a blessing:
|
||||
#
|
||||
# May you do good and not evil.
|
||||
# May you find forgiveness for yourself and forgive others.
|
||||
# May you share freely, never taking more than you give.
|
||||
#
|
||||
#***********************************************************************
|
||||
# This file implements regression tests for the session module.
|
||||
# Specifically, for the sqlite3changeset_concat() command.
|
||||
#
|
||||
|
||||
if {![info exists testdir]} {
|
||||
set testdir [file join [file dirname [info script]] .. .. test]
|
||||
}
|
||||
source [file join [file dirname [info script]] session_common.tcl]
|
||||
source $testdir/tester.tcl
|
||||
ifcapable !session {finish_test; return}
|
||||
|
||||
set testprefix session5
|
||||
|
||||
# Organization of tests:
|
||||
#
|
||||
# session5-1.*: Simple tests to check the concat() function produces
|
||||
# correct results.
|
||||
#
|
||||
# session5-2.*: More complicated tests.
|
||||
#
|
||||
# session5-3.*: Schema mismatch errors.
|
||||
#
|
||||
# session5-4.*: Test the concat cases that indicate that the database
|
||||
# was modified in between recording of the two changesets
|
||||
# being concatenated (i.e. two changesets that INSERT rows
|
||||
# with the same PK values).
|
||||
#
|
||||
|
||||
proc do_concat_test {tn args} {
|
||||
|
||||
set subtest 0
|
||||
foreach sql $args {
|
||||
incr subtest
|
||||
sqlite3session S db main ; S attach *
|
||||
execsql $sql
|
||||
|
||||
set c [S changeset]
|
||||
if {[info commands s_prev] != ""} {
|
||||
set c_concat [sqlite3changeset_concat $c_prev $c]
|
||||
set c_two [s_prev changeset]
|
||||
s_prev delete
|
||||
|
||||
set h_concat [changeset_to_list $c_concat]
|
||||
set h_two [changeset_to_list $c_two]
|
||||
|
||||
do_test $tn.$subtest [list set {} $h_concat] $h_two
|
||||
}
|
||||
set c_prev $c
|
||||
rename S s_prev
|
||||
}
|
||||
|
||||
catch { s_prev delete }
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# Test cases session5-1.* - simple tests.
|
||||
#
|
||||
do_execsql_test 1.0 {
|
||||
CREATE TABLE t1(a PRIMARY KEY, b);
|
||||
}
|
||||
|
||||
do_concat_test 1.1.1 {
|
||||
INSERT INTO t1 VALUES(1, 'one');
|
||||
} {
|
||||
INSERT INTO t1 VALUES(2, 'two');
|
||||
}
|
||||
|
||||
do_concat_test 1.1.2 {
|
||||
UPDATE t1 SET b = 'five' WHERE a = 1;
|
||||
} {
|
||||
UPDATE t1 SET b = 'six' WHERE a = 2;
|
||||
}
|
||||
|
||||
do_concat_test 1.1.3 {
|
||||
DELETE FROM t1 WHERE a = 1;
|
||||
} {
|
||||
DELETE FROM t1 WHERE a = 2;
|
||||
}
|
||||
|
||||
|
||||
# 1.2.1: INSERT + DELETE -> (none)
|
||||
# 1.2.2: INSERT + UPDATE -> INSERT
|
||||
#
|
||||
# 1.2.3: DELETE + INSERT (matching data) -> (none)
|
||||
# 1.2.4: DELETE + INSERT (non-matching data) -> UPDATE
|
||||
#
|
||||
# 1.2.5: UPDATE + UPDATE (matching data) -> (none)
|
||||
# 1.2.6: UPDATE + UPDATE (non-matching data) -> UPDATE
|
||||
# 1.2.7: UPDATE + DELETE -> DELETE
|
||||
#
|
||||
do_concat_test 1.2.1 {
|
||||
INSERT INTO t1 VALUES('x', 'y');
|
||||
} {
|
||||
DELETE FROM t1 WHERE a = 'x';
|
||||
}
|
||||
do_concat_test 1.2.2 {
|
||||
INSERT INTO t1 VALUES(5.0, 'five');
|
||||
} {
|
||||
UPDATE t1 SET b = 'six' WHERE a = 5.0;
|
||||
}
|
||||
|
||||
do_execsql_test 1.2.3.1 "INSERT INTO t1 VALUES('I', 'one')"
|
||||
do_concat_test 1.2.3.2 {
|
||||
DELETE FROM t1 WHERE a = 'I';
|
||||
} {
|
||||
INSERT INTO t1 VALUES('I', 'one');
|
||||
}
|
||||
do_concat_test 1.2.4 {
|
||||
DELETE FROM t1 WHERE a = 'I';
|
||||
} {
|
||||
INSERT INTO t1 VALUES('I', 'two');
|
||||
}
|
||||
do_concat_test 1.2.5 {
|
||||
UPDATE t1 SET b = 'five' WHERE a = 'I';
|
||||
} {
|
||||
UPDATE t1 SET b = 'two' WHERE a = 'I';
|
||||
}
|
||||
do_concat_test 1.2.6 {
|
||||
UPDATE t1 SET b = 'six' WHERE a = 'I';
|
||||
} {
|
||||
UPDATE t1 SET b = 'seven' WHERE a = 'I';
|
||||
}
|
||||
do_concat_test 1.2.7 {
|
||||
UPDATE t1 SET b = 'eight' WHERE a = 'I';
|
||||
} {
|
||||
DELETE FROM t1 WHERE a = 'I';
|
||||
}
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# Test cases session5-2.* - more complex tests.
|
||||
#
|
||||
db function indirect indirect
|
||||
proc indirect {{x -1}} {
|
||||
S indirect $x
|
||||
s_prev indirect $x
|
||||
}
|
||||
do_concat_test 2.1 {
|
||||
CREATE TABLE abc(a, b, c PRIMARY KEY);
|
||||
INSERT INTO abc VALUES(NULL, NULL, 1);
|
||||
INSERT INTO abc VALUES('abcdefghijkl', NULL, 2);
|
||||
} {
|
||||
DELETE FROM abc WHERE c = 1;
|
||||
UPDATE abc SET c = 1 WHERE c = 2;
|
||||
} {
|
||||
INSERT INTO abc VALUES('abcdefghijkl', NULL, 2);
|
||||
INSERT INTO abc VALUES(1.0, 2.0, 3);
|
||||
} {
|
||||
UPDATE abc SET a = a-1;
|
||||
} {
|
||||
CREATE TABLE def(d, e, f, PRIMARY KEY(e, f));
|
||||
INSERT INTO def VALUES('x', randomblob(11000), 67);
|
||||
INSERT INTO def SELECT d, e, f+1 FROM def;
|
||||
INSERT INTO def SELECT d, e, f+2 FROM def;
|
||||
INSERT INTO def SELECT d, e, f+4 FROM def;
|
||||
} {
|
||||
DELETE FROM def WHERE rowid>4;
|
||||
} {
|
||||
INSERT INTO def SELECT d, e, f+4 FROM def;
|
||||
} {
|
||||
INSERT INTO abc VALUES(22, 44, -1);
|
||||
} {
|
||||
UPDATE abc SET c=-2 WHERE c=-1;
|
||||
UPDATE abc SET c=-3 WHERE c=-2;
|
||||
} {
|
||||
UPDATE abc SET c=-4 WHERE c=-3;
|
||||
} {
|
||||
UPDATE abc SET a=a+1 WHERE c=-3;
|
||||
UPDATE abc SET a=a+1 WHERE c=-3;
|
||||
} {
|
||||
UPDATE abc SET a=a+1 WHERE c=-3;
|
||||
UPDATE abc SET a=a+1 WHERE c=-3;
|
||||
} {
|
||||
INSERT INTO abc VALUES('one', 'two', 'three');
|
||||
} {
|
||||
SELECT indirect(1);
|
||||
UPDATE abc SET a='one point five' WHERE c = 'three';
|
||||
} {
|
||||
SELECT indirect(0);
|
||||
UPDATE abc SET a='one point six' WHERE c = 'three';
|
||||
} {
|
||||
CREATE TABLE x1(a, b, PRIMARY KEY(a));
|
||||
SELECT indirect(1);
|
||||
INSERT INTO x1 VALUES(1, 2);
|
||||
} {
|
||||
SELECT indirect(1);
|
||||
UPDATE x1 SET b = 3 WHERE a = 1;
|
||||
}
|
||||
|
||||
catch {db close}
|
||||
forcedelete test.db
|
||||
sqlite3 db test.db
|
||||
do_concat_test 2.2 {
|
||||
CREATE TABLE t1(a, b, PRIMARY KEY(b));
|
||||
CREATE TABLE t2(a PRIMARY KEY, b);
|
||||
INSERT INTO t1 VALUES('string', 1);
|
||||
INSERT INTO t1 VALUES(4, 2);
|
||||
INSERT INTO t1 VALUES(X'FFAAFFAAFFAA', 3);
|
||||
} {
|
||||
INSERT INTO t2 VALUES('one', 'two');
|
||||
INSERT INTO t2 VALUES(1, NULL);
|
||||
UPDATE t1 SET a = 5 WHERE a = 2;
|
||||
} {
|
||||
DELETE FROM t2 WHERE a = 1;
|
||||
UPDATE t1 SET a = 4 WHERE a = 2;
|
||||
INSERT INTO t2 VALUES('x', 'y');
|
||||
}
|
||||
|
||||
do_test 2.3.0 {
|
||||
catch {db close}
|
||||
forcedelete test.db
|
||||
sqlite3 db test.db
|
||||
|
||||
set sql1 ""
|
||||
set sql2 ""
|
||||
for {set i 1} {$i < 120} {incr i} {
|
||||
append sql1 "INSERT INTO x1 VALUES($i*4, $i);"
|
||||
}
|
||||
for {set i 1} {$i < 120} {incr i} {
|
||||
append sql2 "DELETE FROM x1 WHERE a = $i*4;"
|
||||
}
|
||||
set {} {}
|
||||
} {}
|
||||
do_concat_test 2.3 {
|
||||
CREATE TABLE x1(a PRIMARY KEY, b)
|
||||
} $sql1 $sql2 $sql1 $sql2
|
||||
|
||||
do_concat_test 2.4 {
|
||||
CREATE TABLE x2(a PRIMARY KEY, b);
|
||||
CREATE TABLE x3(a PRIMARY KEY, b);
|
||||
|
||||
INSERT INTO x2 VALUES('a', 'b');
|
||||
INSERT INTO x2 VALUES('x', 'y');
|
||||
INSERT INTO x3 VALUES('a', 'b');
|
||||
} {
|
||||
INSERT INTO x2 VALUES('c', 'd');
|
||||
INSERT INTO x3 VALUES('e', 'f');
|
||||
INSERT INTO x3 VALUES('x', 'y');
|
||||
}
|
||||
|
||||
do_concat_test 2.5 {
|
||||
UPDATE x3 SET b = 'Y' WHERE a = 'x'
|
||||
} {
|
||||
DELETE FROM x3 WHERE a = 'x'
|
||||
} {
|
||||
DELETE FROM x2 WHERE a = 'a'
|
||||
} {
|
||||
INSERT INTO x2 VALUES('a', 'B');
|
||||
}
|
||||
|
||||
for {set k 1} {$k <=10} {incr k} {
|
||||
do_test 2.6.$k.1 {
|
||||
drop_all_tables
|
||||
set sql1 ""
|
||||
set sql2 ""
|
||||
for {set i 1} {$i < 120} {incr i} {
|
||||
append sql1 "INSERT INTO x1 VALUES(randomblob(20+(random()%10)), $i);"
|
||||
}
|
||||
for {set i 1} {$i < 120} {incr i} {
|
||||
append sql2 "DELETE FROM x1 WHERE rowid = $i;"
|
||||
}
|
||||
set {} {}
|
||||
} {}
|
||||
do_concat_test 2.6.$k {
|
||||
CREATE TABLE x1(a PRIMARY KEY, b)
|
||||
} $sql1 $sql2 $sql1 $sql2
|
||||
}
|
||||
|
||||
for {set k 1} {$k <=10} {incr k} {
|
||||
do_test 2.7.$k.1 {
|
||||
drop_all_tables
|
||||
set sql1 ""
|
||||
set sql2 ""
|
||||
for {set i 1} {$i < 120} {incr i} {
|
||||
append sql1 {
|
||||
INSERT INTO x1 VALUES(
|
||||
CASE WHEN random()%2 THEN random() ELSE randomblob(20+random()%10) END,
|
||||
CASE WHEN random()%2 THEN random() ELSE randomblob(20+random()%10) END
|
||||
);
|
||||
}
|
||||
}
|
||||
for {set i 1} {$i < 120} {incr i} {
|
||||
append sql2 "DELETE FROM x1 WHERE rowid = $i;"
|
||||
}
|
||||
set {} {}
|
||||
} {}
|
||||
do_concat_test 2.7.$k {
|
||||
CREATE TABLE x1(a PRIMARY KEY, b)
|
||||
} $sql1 $sql2 $sql1 $sql2
|
||||
}
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# Test that schema incompatibilities are detected correctly.
|
||||
#
|
||||
# session5-3.1: Incompatible number of columns.
|
||||
# session5-3.2: Incompatible PK definition.
|
||||
#
|
||||
|
||||
do_test 3.1 {
|
||||
db close
|
||||
forcedelete test.db
|
||||
sqlite3 db test.db
|
||||
|
||||
execsql { CREATE TABLE t1(a PRIMARY KEY, b) }
|
||||
set c1 [changeset_from_sql { INSERT INTO t1 VALUES(1, 2) }]
|
||||
execsql {
|
||||
DROP TABLE t1;
|
||||
CREATE TABLE t1(a PRIMARY KEY, b, c);
|
||||
}
|
||||
set c2 [changeset_from_sql { INSERT INTO t1 VALUES(2, 3, 4) }]
|
||||
|
||||
list [catch { sqlite3changeset_concat $c1 $c2 } msg] $msg
|
||||
} {1 SQLITE_SCHEMA}
|
||||
|
||||
do_test 3.2 {
|
||||
db close
|
||||
forcedelete test.db
|
||||
sqlite3 db test.db
|
||||
|
||||
execsql { CREATE TABLE t1(a PRIMARY KEY, b) }
|
||||
set c1 [changeset_from_sql { INSERT INTO t1 VALUES(1, 2) }]
|
||||
execsql {
|
||||
DROP TABLE t1;
|
||||
CREATE TABLE t1(a, b PRIMARY KEY);
|
||||
}
|
||||
set c2 [changeset_from_sql { INSERT INTO t1 VALUES(2, 3) }]
|
||||
|
||||
list [catch { sqlite3changeset_concat $c1 $c2 } msg] $msg
|
||||
} {1 SQLITE_SCHEMA}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# Test that concat() handles these properly:
|
||||
#
|
||||
# session5-4.1: INSERT + INSERT
|
||||
# session5-4.2: UPDATE + INSERT
|
||||
# session5-4.3: DELETE + UPDATE
|
||||
# session5-4.4: DELETE + DELETE
|
||||
#
|
||||
|
||||
proc do_concat_test2 {tn sql1 sqlX sql2 expected} {
|
||||
sqlite3session S db main ; S attach *
|
||||
execsql $sql1
|
||||
set ::c1 [S changeset]
|
||||
S delete
|
||||
|
||||
execsql $sqlX
|
||||
|
||||
sqlite3session S db main ; S attach *
|
||||
execsql $sql2
|
||||
set ::c2 [S changeset]
|
||||
S delete
|
||||
|
||||
uplevel do_test $tn [list {
|
||||
changeset_to_list [sqlite3changeset_concat $::c1 $::c2]
|
||||
}] [list [normalize_list $expected]]
|
||||
}
|
||||
|
||||
drop_all_tables db
|
||||
do_concat_test2 4.1 {
|
||||
CREATE TABLE t1(a PRIMARY KEY, b);
|
||||
INSERT INTO t1 VALUES('key', 'value');
|
||||
} {
|
||||
DELETE FROM t1 WHERE a = 'key';
|
||||
} {
|
||||
INSERT INTO t1 VALUES('key', 'xxx');
|
||||
} {
|
||||
{INSERT t1 0 X. {} {t key t value}}
|
||||
}
|
||||
do_concat_test2 4.2 {
|
||||
UPDATE t1 SET b = 'yyy';
|
||||
} {
|
||||
DELETE FROM t1 WHERE a = 'key';
|
||||
} {
|
||||
INSERT INTO t1 VALUES('key', 'value');
|
||||
} {
|
||||
{UPDATE t1 0 X. {t key t xxx} {{} {} t yyy}}
|
||||
}
|
||||
do_concat_test2 4.3 {
|
||||
DELETE FROM t1 WHERE a = 'key';
|
||||
} {
|
||||
INSERT INTO t1 VALUES('key', 'www');
|
||||
} {
|
||||
UPDATE t1 SET b = 'valueX' WHERE a = 'key';
|
||||
} {
|
||||
{DELETE t1 0 X. {t key t value} {}}
|
||||
}
|
||||
do_concat_test2 4.4 {
|
||||
DELETE FROM t1 WHERE a = 'key';
|
||||
} {
|
||||
INSERT INTO t1 VALUES('key', 'ttt');
|
||||
} {
|
||||
DELETE FROM t1 WHERE a = 'key';
|
||||
} {
|
||||
{DELETE t1 0 X. {t key t valueX} {}}
|
||||
}
|
||||
|
||||
finish_test
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# 2011 July 11
|
||||
#
|
||||
# The author disclaims copyright to this source code. In place of
|
||||
# a legal notice, here is a blessing:
|
||||
#
|
||||
# May you do good and not evil.
|
||||
# May you find forgiveness for yourself and forgive others.
|
||||
# May you share freely, never taking more than you give.
|
||||
#
|
||||
#***********************************************************************
|
||||
# This file implements regression tests for SQLite sessions extension.
|
||||
# Specifically, it tests that sessions work when the database is modified
|
||||
# using incremental blob handles.
|
||||
#
|
||||
|
||||
if {![info exists testdir]} {
|
||||
set testdir [file join [file dirname [info script]] .. .. test]
|
||||
}
|
||||
source [file join [file dirname [info script]] session_common.tcl]
|
||||
source $testdir/tester.tcl
|
||||
ifcapable !session {finish_test; return}
|
||||
|
||||
set testprefix session6
|
||||
|
||||
proc do_then_apply_tcl {tcl {dbname main}} {
|
||||
proc xConflict args { return "OMIT" }
|
||||
set rc [catch {
|
||||
sqlite3session S db $dbname
|
||||
db eval "SELECT name FROM $dbname.sqlite_master WHERE type = 'table'" {
|
||||
S attach $name
|
||||
}
|
||||
eval $tcl
|
||||
sqlite3changeset_apply db2 [S changeset] xConflict
|
||||
} msg]
|
||||
|
||||
catch { S delete }
|
||||
if {$rc} {error $msg}
|
||||
}
|
||||
|
||||
test_sqlite3_log x
|
||||
proc x {args} {puts $args}
|
||||
|
||||
forcedelete test.db2
|
||||
sqlite3 db2 test.db2
|
||||
|
||||
do_common_sql {
|
||||
CREATE TABLE t1(a PRIMARY KEY, b);
|
||||
CREATE TABLE t2(c PRIMARY KEY, d);
|
||||
}
|
||||
|
||||
# Test a blob update.
|
||||
#
|
||||
do_test 1.1 {
|
||||
do_then_apply_tcl {
|
||||
db eval { INSERT INTO t1 VALUES(1, 'helloworld') }
|
||||
db eval { INSERT INTO t2 VALUES(2, 'onetwothree') }
|
||||
}
|
||||
compare_db db db2
|
||||
} {}
|
||||
do_test 1.2 {
|
||||
do_then_apply_tcl {
|
||||
set fd [db incrblob t1 b 1]
|
||||
puts -nonewline $fd 1234567890
|
||||
close $fd
|
||||
}
|
||||
compare_db db db2
|
||||
} {}
|
||||
|
||||
# Test an attached database.
|
||||
#
|
||||
do_test 2.1 {
|
||||
forcedelete test.db3
|
||||
file copy test.db2 test.db3
|
||||
execsql { ATTACH 'test.db3' AS aux; }
|
||||
|
||||
do_then_apply_tcl {
|
||||
set fd [db incrblob aux t2 d 1]
|
||||
puts -nonewline $fd fourfivesix
|
||||
close $fd
|
||||
} aux
|
||||
|
||||
sqlite3 db3 test.db3
|
||||
compare_db db2 db3
|
||||
} {}
|
||||
|
||||
|
||||
db3 close
|
||||
db2 close
|
||||
|
||||
finish_test
|
||||
@@ -0,0 +1,92 @@
|
||||
# 2011 July 13
|
||||
#
|
||||
# The author disclaims copyright to this source code. In place of
|
||||
# a legal notice, here is a blessing:
|
||||
#
|
||||
# May you do good and not evil.
|
||||
# May you find forgiveness for yourself and forgive others.
|
||||
# May you share freely, never taking more than you give.
|
||||
#
|
||||
#***********************************************************************
|
||||
# This file implements regression tests for SQLite library.
|
||||
#
|
||||
|
||||
if {![info exists testdir]} {
|
||||
set testdir [file join [file dirname [info script]] .. .. test]
|
||||
}
|
||||
source [file join [file dirname [info script]] session_common.tcl]
|
||||
source $testdir/tester.tcl
|
||||
ifcapable !session {finish_test; return}
|
||||
|
||||
set testprefix session8
|
||||
|
||||
proc noop {args} {}
|
||||
|
||||
# Like [dbcksum] in tester.tcl. Except this version is not sensitive
|
||||
# to changes in the value of implicit IPK columns.
|
||||
#
|
||||
proc udbcksum {db dbname} {
|
||||
if {$dbname=="temp"} {
|
||||
set master sqlite_temp_master
|
||||
} else {
|
||||
set master $dbname.sqlite_master
|
||||
}
|
||||
set alltab [$db eval "SELECT name FROM $master WHERE type='table'"]
|
||||
set txt [$db eval "SELECT * FROM $master"]\n
|
||||
foreach tab $alltab {
|
||||
append txt [lsort [$db eval "SELECT * FROM $dbname.$tab"]]\n
|
||||
}
|
||||
return [md5 $txt]
|
||||
}
|
||||
|
||||
proc do_then_undo {tn sql} {
|
||||
set ck1 [udbcksum db main]
|
||||
|
||||
sqlite3session S db main
|
||||
S attach *
|
||||
db eval $sql
|
||||
|
||||
set ck2 [udbcksum db main]
|
||||
|
||||
set invert [sqlite3changeset_invert [S changeset]]
|
||||
S delete
|
||||
sqlite3changeset_apply db $invert noop
|
||||
|
||||
set ck3 [udbcksum db main]
|
||||
|
||||
set a [expr {$ck1==$ck2}]
|
||||
set b [expr {$ck1==$ck3}]
|
||||
uplevel [list do_test $tn.1 "set {} $a" 0]
|
||||
uplevel [list do_test $tn.2 "set {} $b" 1]
|
||||
}
|
||||
|
||||
do_execsql_test 1.1 {
|
||||
CREATE TABLE t1(a PRIMARY KEY, b);
|
||||
INSERT INTO t1 VALUES(1, 2);
|
||||
INSERT INTO t1 VALUES("abc", "xyz");
|
||||
}
|
||||
do_then_undo 1.2 { INSERT INTO t1 VALUES(3, 4); }
|
||||
do_then_undo 1.3 { DELETE FROM t1 WHERE b=2; }
|
||||
do_then_undo 1.4 { UPDATE t1 SET b = 3 WHERE a = 1; }
|
||||
|
||||
do_execsql_test 2.1 {
|
||||
CREATE TABLE t2(a, b PRIMARY KEY);
|
||||
INSERT INTO t2 VALUES(1, 2);
|
||||
INSERT INTO t2 VALUES('abc', 'xyz');
|
||||
}
|
||||
do_then_undo 1.2 { INSERT INTO t2 VALUES(3, 4); }
|
||||
do_then_undo 1.3 { DELETE FROM t2 WHERE b=2; }
|
||||
do_then_undo 1.4 { UPDATE t1 SET a = '123' WHERE b = 'xyz'; }
|
||||
|
||||
do_execsql_test 3.1 {
|
||||
CREATE TABLE t3(a, b, c, d, e, PRIMARY KEY(c, e));
|
||||
INSERT INTO t3 VALUES('x', 45, 0.0, 'abcdef', 12);
|
||||
INSERT INTO t3 VALUES(45, 0.0, 'abcdef', 12, 'x');
|
||||
INSERT INTO t3 VALUES(0.0, 'abcdef', 12, 'x', 45);
|
||||
}
|
||||
|
||||
do_then_undo 3.2 { UPDATE t3 SET b=b||b WHERE e!='x' }
|
||||
do_then_undo 3.3 { UPDATE t3 SET a = 46 }
|
||||
|
||||
finish_test
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
|
||||
|
||||
proc do_conflict_test {tn args} {
|
||||
proc xConflict {args} {
|
||||
lappend ::xConflict $args
|
||||
return ""
|
||||
}
|
||||
proc bgerror {args} { set ::background_error $args }
|
||||
|
||||
|
||||
set O(-tables) [list]
|
||||
set O(-sql) [list]
|
||||
set O(-conflicts) [list]
|
||||
|
||||
array set V $args
|
||||
foreach key [array names V] {
|
||||
if {![info exists O($key)]} {error "no such option: $key"}
|
||||
}
|
||||
array set O $args
|
||||
|
||||
sqlite3session S db main
|
||||
foreach t $O(-tables) { S attach $t }
|
||||
execsql $O(-sql)
|
||||
|
||||
set ::xConflict [list]
|
||||
sqlite3changeset_apply db2 [S changeset] xConflict
|
||||
|
||||
set conflicts [list]
|
||||
foreach c $O(-conflicts) {
|
||||
lappend conflicts $c
|
||||
}
|
||||
|
||||
after 1 {set go 1}
|
||||
vwait go
|
||||
|
||||
uplevel do_test $tn [list { set ::xConflict }] [list $conflicts]
|
||||
S delete
|
||||
}
|
||||
|
||||
proc do_common_sql {sql} {
|
||||
execsql $sql db
|
||||
execsql $sql db2
|
||||
}
|
||||
|
||||
proc changeset_from_sql {sql {dbname main}} {
|
||||
set rc [catch {
|
||||
sqlite3session S db $dbname
|
||||
db eval "SELECT name FROM $dbname.sqlite_master WHERE type = 'table'" {
|
||||
S attach $name
|
||||
}
|
||||
db eval $sql
|
||||
S changeset
|
||||
} changeset]
|
||||
catch { S delete }
|
||||
|
||||
if {$rc} {
|
||||
error $changeset
|
||||
}
|
||||
return $changeset
|
||||
}
|
||||
|
||||
proc do_then_apply_sql {sql {dbname main}} {
|
||||
proc xConflict args { return "OMIT" }
|
||||
set rc [catch {
|
||||
sqlite3session S db $dbname
|
||||
db eval "SELECT name FROM $dbname.sqlite_master WHERE type = 'table'" {
|
||||
S attach $name
|
||||
}
|
||||
db eval $sql
|
||||
sqlite3changeset_apply db2 [S changeset] xConflict
|
||||
} msg]
|
||||
|
||||
catch { S delete }
|
||||
|
||||
if {$rc} {error $msg}
|
||||
}
|
||||
|
||||
proc do_iterator_test {tn tbl_list sql res} {
|
||||
sqlite3session S db main
|
||||
if {[llength $tbl_list]==0} { S attach * }
|
||||
foreach t $tbl_list {S attach $t}
|
||||
|
||||
execsql $sql
|
||||
|
||||
set r [list]
|
||||
foreach v $res { lappend r $v }
|
||||
|
||||
set x [list]
|
||||
sqlite3session_foreach c [S changeset] { lappend x $c }
|
||||
uplevel do_test $tn [list [list set {} $x]] [list $r]
|
||||
|
||||
S delete
|
||||
}
|
||||
|
||||
# Compare the contents of all tables in [db1] and [db2]. Throw an error if
|
||||
# they are not identical, or return an empty string if they are.
|
||||
#
|
||||
proc compare_db {db1 db2} {
|
||||
|
||||
set sql {SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name}
|
||||
set lot1 [$db1 eval $sql]
|
||||
set lot2 [$db2 eval $sql]
|
||||
|
||||
if {$lot1 != $lot2} {
|
||||
puts $lot1
|
||||
puts $lot2
|
||||
error "databases contain different tables"
|
||||
}
|
||||
|
||||
foreach tbl $lot1 {
|
||||
set col1 [list]
|
||||
set col2 [list]
|
||||
|
||||
$db1 eval "PRAGMA table_info = $tbl" { lappend col1 $name }
|
||||
$db2 eval "PRAGMA table_info = $tbl" { lappend col2 $name }
|
||||
if {$col1 != $col2} { error "table $tbl schema mismatch" }
|
||||
|
||||
set sql "SELECT * FROM $tbl ORDER BY [join $col1 ,]"
|
||||
set data1 [$db1 eval $sql]
|
||||
set data2 [$db2 eval $sql]
|
||||
if {$data1 != $data2} {
|
||||
puts "$data1"
|
||||
puts "$data2"
|
||||
error "table $tbl data mismatch"
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
proc changeset_to_list {c} {
|
||||
set list [list]
|
||||
sqlite3session_foreach elem $c { lappend list $elem }
|
||||
lsort $list
|
||||
}
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
# 2011 Mar 21
|
||||
#
|
||||
# The author disclaims copyright to this source code. In place of
|
||||
# a legal notice, here is a blessing:
|
||||
#
|
||||
# May you do good and not evil.
|
||||
# May you find forgiveness for yourself and forgive others.
|
||||
# May you share freely, never taking more than you give.
|
||||
#
|
||||
#***********************************************************************
|
||||
#
|
||||
# The focus of this file is testing the session module.
|
||||
#
|
||||
|
||||
if {![info exists testdir]} {
|
||||
set testdir [file join [file dirname [info script]] .. .. test]
|
||||
}
|
||||
source [file join [file dirname [info script]] session_common.tcl]
|
||||
source $testdir/tester.tcl
|
||||
|
||||
set testprefix sessionfault
|
||||
|
||||
if 1 {
|
||||
|
||||
forcedelete test.db2
|
||||
sqlite3 db2 test.db2
|
||||
do_common_sql {
|
||||
CREATE TABLE t1(a, b, c, PRIMARY KEY(a, b));
|
||||
INSERT INTO t1 VALUES(1, 2, 3);
|
||||
INSERT INTO t1 VALUES(4, 5, 6);
|
||||
}
|
||||
faultsim_save_and_close
|
||||
db2 close
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# Test OOM error handling when collecting and applying a simple changeset.
|
||||
#
|
||||
# Test 1.1 attaches tables individually by name to the session object.
|
||||
# Whereas test 1.2 passes NULL to sqlite3session_attach() to attach all
|
||||
# tables.
|
||||
#
|
||||
do_faultsim_test 1.1 -faults oom-* -prep {
|
||||
catch {db2 close}
|
||||
catch {db close}
|
||||
faultsim_restore_and_reopen
|
||||
sqlite3 db2 test.db2
|
||||
} -body {
|
||||
do_then_apply_sql {
|
||||
INSERT INTO t1 VALUES('a string value', 8, 9);
|
||||
UPDATE t1 SET c = 10 WHERE a = 1;
|
||||
DELETE FROM t1 WHERE a = 4;
|
||||
}
|
||||
} -test {
|
||||
faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
|
||||
faultsim_integrity_check
|
||||
if {$testrc==0} { compare_db db db2 }
|
||||
}
|
||||
|
||||
do_faultsim_test 1.2 -faults oom-* -prep {
|
||||
catch {db2 close}
|
||||
catch {db close}
|
||||
faultsim_restore_and_reopen
|
||||
} -body {
|
||||
sqlite3session S db main
|
||||
S attach *
|
||||
execsql {
|
||||
INSERT INTO t1 VALUES('a string value', 8, 9);
|
||||
UPDATE t1 SET c = 10 WHERE a = 1;
|
||||
DELETE FROM t1 WHERE a = 4;
|
||||
}
|
||||
set ::changeset [S changeset]
|
||||
set {} {}
|
||||
} -test {
|
||||
catch { S delete }
|
||||
faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
|
||||
faultsim_integrity_check
|
||||
if {$testrc==0} {
|
||||
proc xConflict {args} { return "OMIT" }
|
||||
sqlite3 db2 test.db2
|
||||
sqlite3changeset_apply db2 $::changeset xConflict
|
||||
compare_db db db2
|
||||
}
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# The following block of tests - 2.* - are designed to check
|
||||
# the handling of faults in the sqlite3changeset_apply() function.
|
||||
#
|
||||
catch {db close}
|
||||
catch {db2 close}
|
||||
forcedelete test.db2 test.db
|
||||
sqlite3 db2 test.db2
|
||||
sqlite3 db test.db
|
||||
do_common_sql {
|
||||
CREATE TABLE t1(a, b, c, PRIMARY KEY(a, b));
|
||||
INSERT INTO t1 VALUES('apple', 'orange', 'pear');
|
||||
|
||||
CREATE TABLE t2(x PRIMARY KEY, y);
|
||||
}
|
||||
db2 close
|
||||
faultsim_save_and_close
|
||||
|
||||
|
||||
foreach {tn conflict_policy sql sql2} {
|
||||
1 OMIT { INSERT INTO t1 VALUES('one text', 'two text', X'00ff00') } {}
|
||||
2 OMIT { DELETE FROM t1 WHERE a = 'apple' } {}
|
||||
3 OMIT { UPDATE t1 SET c = 'banana' WHERE b = 'orange' } {}
|
||||
4 REPLACE { INSERT INTO t2 VALUES('keyvalue', 'value 1') } {
|
||||
INSERT INTO t2 VALUES('keyvalue', 'value 2');
|
||||
}
|
||||
} {
|
||||
proc xConflict args [list return $conflict_policy]
|
||||
|
||||
do_faultsim_test 2.$tn -faults oom-transient -prep {
|
||||
catch {db2 close}
|
||||
catch {db close}
|
||||
faultsim_restore_and_reopen
|
||||
set ::changeset [changeset_from_sql $::sql]
|
||||
sqlite3 db2 test.db2
|
||||
sqlite3_db_config_lookaside db2 0 0 0
|
||||
execsql $::sql2 db2
|
||||
} -body {
|
||||
sqlite3changeset_apply db2 $::changeset xConflict
|
||||
} -test {
|
||||
faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
|
||||
faultsim_integrity_check
|
||||
if {$testrc==0} { compare_db db db2 }
|
||||
}
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# This test case is designed so that a malloc() failure occurs while
|
||||
# resizing the session object hash-table from 256 to 512 buckets. This
|
||||
# is not an error, just a sub-optimal condition.
|
||||
#
|
||||
do_faultsim_test 3 -faults oom-* -prep {
|
||||
catch {db2 close}
|
||||
catch {db close}
|
||||
faultsim_restore_and_reopen
|
||||
sqlite3 db2 test.db2
|
||||
|
||||
sqlite3session S db main
|
||||
S attach t1
|
||||
execsql { BEGIN }
|
||||
for {set i 0} {$i < 125} {incr i} {
|
||||
execsql {INSERT INTO t1 VALUES(10+$i, 10+$i, 10+$i)}
|
||||
}
|
||||
} -body {
|
||||
for {set i 125} {$i < 133} {incr i} {
|
||||
execsql {INSERT INTO t1 VALUES(10+$i, 10+$i, 1-+$i)}
|
||||
}
|
||||
S changeset
|
||||
set {} {}
|
||||
} -test {
|
||||
faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
|
||||
if {$testrc==0} {
|
||||
sqlite3changeset_apply db2 [S changeset] xConflict
|
||||
compare_db db db2
|
||||
}
|
||||
catch { S delete }
|
||||
faultsim_integrity_check
|
||||
}
|
||||
|
||||
catch { db close }
|
||||
catch { db2 close }
|
||||
forcedelete test.db2 test.db
|
||||
sqlite3 db2 test.db2
|
||||
sqlite3 db test.db
|
||||
|
||||
proc xConflict {op tbl type args} {
|
||||
if { $type=="CONFLICT" || $type=="DATA" } {
|
||||
return "REPLACE"
|
||||
}
|
||||
return "OMIT"
|
||||
}
|
||||
|
||||
do_test 4.0 {
|
||||
execsql {
|
||||
PRAGMA encoding = 'utf16';
|
||||
CREATE TABLE t1(a PRIMARY KEY, b);
|
||||
INSERT INTO t1 VALUES(5, 32);
|
||||
}
|
||||
execsql {
|
||||
PRAGMA encoding = 'utf16';
|
||||
CREATE TABLE t1(a PRIMARY KEY, b NOT NULL);
|
||||
INSERT INTO t1 VALUES(1, 2);
|
||||
INSERT INTO t1 VALUES(2, 4);
|
||||
INSERT INTO t1 VALUES(4, 16);
|
||||
} db2
|
||||
} {}
|
||||
|
||||
faultsim_save_and_close
|
||||
db2 close
|
||||
|
||||
do_faultsim_test 4 -faults oom-* -prep {
|
||||
catch {db2 close}
|
||||
catch {db close}
|
||||
faultsim_restore_and_reopen
|
||||
sqlite3 db2 test.db2
|
||||
sqlite3session S db main
|
||||
S attach t1
|
||||
execsql {
|
||||
INSERT INTO t1 VALUES(1, 45);
|
||||
INSERT INTO t1 VALUES(2, 55);
|
||||
INSERT INTO t1 VALUES(3, 55);
|
||||
UPDATE t1 SET a = 4 WHERE a = 5;
|
||||
}
|
||||
} -body {
|
||||
sqlite3changeset_apply db2 [S changeset] xConflict
|
||||
} -test {
|
||||
catch { S delete }
|
||||
faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
|
||||
if {$testrc==0} { compare_db db db2 }
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# This block of tests verifies that OOM faults in the
|
||||
# sqlite3changeset_invert() function are handled correctly.
|
||||
#
|
||||
catch {db close}
|
||||
catch {db2 close}
|
||||
forcedelete test.db
|
||||
sqlite3 db test.db
|
||||
execsql {
|
||||
CREATE TABLE t1(a, b, PRIMARY KEY(b));
|
||||
CREATE TABLE t2(a PRIMARY KEY, b);
|
||||
INSERT INTO t1 VALUES('string', 1);
|
||||
INSERT INTO t1 VALUES(4, 2);
|
||||
INSERT INTO t1 VALUES(X'FFAAFFAAFFAA', 3);
|
||||
}
|
||||
set changeset [changeset_from_sql {
|
||||
INSERT INTO t1 VALUES('xxx', 'yyy');
|
||||
DELETE FROM t1 WHERE a = 'string';
|
||||
UPDATE t1 SET a = 20 WHERE b = 2;
|
||||
}]
|
||||
db close
|
||||
|
||||
do_faultsim_test 5 -faults oom* -body {
|
||||
set ::inverse [sqlite3changeset_invert $::changeset]
|
||||
set {} {}
|
||||
} -test {
|
||||
faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
|
||||
if {$testrc==0} {
|
||||
set x [list]
|
||||
sqlite3session_foreach c $::inverse { lappend x $c }
|
||||
foreach c {
|
||||
{DELETE t1 0 .X {t xxx t yyy} {}}
|
||||
{INSERT t1 0 .X {} {t string i 1}}
|
||||
{UPDATE t1 0 .X {i 20 i 2} {i 4 {} {}}}
|
||||
} { lappend y $c }
|
||||
if {$x != $y} { error "changeset no good" }
|
||||
}
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# Test that OOM errors in sqlite3changeset_concat() are handled correctly.
|
||||
#
|
||||
catch {db close}
|
||||
forcedelete test.db
|
||||
sqlite3 db test.db
|
||||
do_execsql_test 5.prep1 {
|
||||
CREATE TABLE t1(a, b, PRIMARY KEY(b));
|
||||
CREATE TABLE t2(a PRIMARY KEY, b);
|
||||
INSERT INTO t1 VALUES('string', 1);
|
||||
INSERT INTO t1 VALUES(4, 2);
|
||||
INSERT INTO t1 VALUES(X'FFAAFFAAFFAA', 3);
|
||||
}
|
||||
|
||||
do_test 6.prep2 {
|
||||
sqlite3session M db main
|
||||
M attach *
|
||||
set ::c2 [changeset_from_sql {
|
||||
INSERT INTO t2 VALUES(randomblob(1000), randomblob(1000));
|
||||
INSERT INTO t2 VALUES('one', 'two');
|
||||
INSERT INTO t2 VALUES(1, NULL);
|
||||
UPDATE t1 SET a = 5 WHERE a = 2;
|
||||
}]
|
||||
set ::c1 [changeset_from_sql {
|
||||
DELETE FROM t2 WHERE a = 1;
|
||||
UPDATE t1 SET a = 4 WHERE a = 2;
|
||||
INSERT INTO t2 VALUES('x', 'y');
|
||||
}]
|
||||
set ::total [changeset_to_list [M changeset]]
|
||||
M delete
|
||||
} {}
|
||||
|
||||
do_faultsim_test 6 -faults oom-* -body {
|
||||
set ::result [sqlite3changeset_concat $::c1 $::c2]
|
||||
set {} {}
|
||||
} -test {
|
||||
faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
|
||||
if {$testrc==0} {
|
||||
set v [changeset_to_list $::result]
|
||||
if {$v != $::total} { error "result no good" }
|
||||
}
|
||||
}
|
||||
|
||||
faultsim_delete_and_reopen
|
||||
do_execsql_test 5.prep1 {
|
||||
CREATE TABLE t1(a, b, PRIMARY KEY(a));
|
||||
}
|
||||
faultsim_save_and_close
|
||||
|
||||
set res [list]
|
||||
for {set ::i 0} {$::i < 480} {incr ::i 4} {
|
||||
lappend res "INSERT t1 0 X. {} {i $::i i $::i}"
|
||||
}
|
||||
set res [lsort $res]
|
||||
do_faultsim_test 7 -faults oom-transient -prep {
|
||||
faultsim_restore_and_reopen
|
||||
sqlite3session S db main
|
||||
S attach *
|
||||
} -body {
|
||||
for {set ::i 0} {$::i < 480} {incr ::i 4} {
|
||||
execsql {INSERT INTO t1 VALUES($::i, $::i)}
|
||||
}
|
||||
} -test {
|
||||
faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
|
||||
if {$testrc==0} {
|
||||
set cres [list [catch {changeset_to_list [S changeset]} msg] $msg]
|
||||
S delete
|
||||
if {$cres != "1 SQLITE_NOMEM" && $cres != "0 {$::res}"} {
|
||||
error "Expected {0 $::res} Got {$cres}"
|
||||
}
|
||||
} else {
|
||||
S changeset
|
||||
S delete
|
||||
}
|
||||
}
|
||||
|
||||
faultsim_delete_and_reopen
|
||||
do_test 8.prep {
|
||||
sqlite3session S db main
|
||||
S attach *
|
||||
execsql {
|
||||
CREATE TABLE t1(a, b, PRIMARY KEY(a));
|
||||
INSERT INTO t1 VALUES(1, 2);
|
||||
INSERT INTO t1 VALUES(3, 4);
|
||||
INSERT INTO t1 VALUES(5, 6);
|
||||
}
|
||||
set ::changeset [S changeset]
|
||||
S delete
|
||||
} {}
|
||||
|
||||
set expected [normalize_list {
|
||||
{INSERT t1 0 X. {} {i 1 i 2}}
|
||||
{INSERT t1 0 X. {} {i 3 i 4}}
|
||||
{INSERT t1 0 X. {} {i 5 i 6}}
|
||||
}]
|
||||
do_faultsim_test 8.1 -faults oom* -body {
|
||||
set ::res [list]
|
||||
sqlite3session_foreach -next v $::changeset { lappend ::res $v }
|
||||
normalize_list $::res
|
||||
} -test {
|
||||
faultsim_test_result [list 0 $::expected] {1 SQLITE_NOMEM}
|
||||
}
|
||||
do_faultsim_test 8.2 -faults oom* -body {
|
||||
set ::res [list]
|
||||
sqlite3session_foreach v $::changeset { lappend ::res $v }
|
||||
normalize_list $::res
|
||||
} -test {
|
||||
faultsim_test_result [list 0 $::expected] {1 SQLITE_NOMEM}
|
||||
}
|
||||
|
||||
faultsim_delete_and_reopen
|
||||
do_test 9.1.prep {
|
||||
execsql {
|
||||
PRAGMA encoding = 'utf16';
|
||||
CREATE TABLE t1(a PRIMARY KEY, b);
|
||||
}
|
||||
} {}
|
||||
faultsim_save_and_close
|
||||
|
||||
set answers [list {0 {}} {1 SQLITE_NOMEM} {1 {callback requested query abort}}]
|
||||
do_faultsim_test 9.1 -faults oom-transient -prep {
|
||||
catch { unset ::c }
|
||||
faultsim_restore_and_reopen
|
||||
sqlite3session S db main
|
||||
S attach *
|
||||
} -body {
|
||||
execsql {
|
||||
INSERT INTO t1 VALUES('abcdefghijklmnopqrstuv', 'ABCDEFGHIJKLMNOPQRSTUV');
|
||||
}
|
||||
set ::c [S changeset]
|
||||
set {} {}
|
||||
} -test {
|
||||
S delete
|
||||
eval faultsim_test_result $::answers
|
||||
if {[info exists ::c]} {
|
||||
set expected [normalize_list {
|
||||
{INSERT t1 0 X. {} {t abcdefghijklmnopqrstuv t ABCDEFGHIJKLMNOPQRSTUV}}
|
||||
}]
|
||||
if { [changeset_to_list $::c] != $expected } {
|
||||
error "changeset mismatch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
faultsim_delete_and_reopen
|
||||
do_test 9.2.prep {
|
||||
execsql {
|
||||
PRAGMA encoding = 'utf16';
|
||||
CREATE TABLE t1(a PRIMARY KEY, b);
|
||||
INSERT INTO t1 VALUES('abcdefghij', 'ABCDEFGHIJKLMNOPQRSTUV');
|
||||
}
|
||||
} {}
|
||||
faultsim_save_and_close
|
||||
|
||||
set answers [list {0 {}} {1 SQLITE_NOMEM} {1 {callback requested query abort}}]
|
||||
do_faultsim_test 9.2 -faults oom-transient -prep {
|
||||
catch { unset ::c }
|
||||
faultsim_restore_and_reopen
|
||||
sqlite3session S db main
|
||||
S attach *
|
||||
} -body {
|
||||
execsql {
|
||||
UPDATE t1 SET b = 'xyz';
|
||||
}
|
||||
set ::c [S changeset]
|
||||
set {} {}
|
||||
} -test {
|
||||
S delete
|
||||
eval faultsim_test_result $::answers
|
||||
if {[info exists ::c]} {
|
||||
set expected [normalize_list {
|
||||
{UPDATE t1 0 X. {t abcdefghij t ABCDEFGHIJKLMNOPQRSTUV} {{} {} t xyz}}
|
||||
}]
|
||||
if { [changeset_to_list $::c] != $expected } {
|
||||
error "changeset mismatch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
finish_test
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,869 @@
|
||||
|
||||
#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: Test if a changeset has recorded any changes.
|
||||
**
|
||||
** Return non-zero if no changes to attached tables have been recorded by
|
||||
** the session object passed as the first argument. Otherwise, if one or
|
||||
** more changes have been recorded, return zero.
|
||||
**
|
||||
** Even if this function returns zero, it is possible that calling
|
||||
** [sqlite3session_changeset()] on the session handle may still return a
|
||||
** changeset that contains no changes. This can happen when a row in
|
||||
** an attached table is modified and then later on the original values
|
||||
** are restored. However, if this function returns non-zero, then it is
|
||||
** guaranteed that a call to sqlite3session_changeset() will return a
|
||||
** changeset containing zero changes.
|
||||
*/
|
||||
int sqlite3session_isempty(sqlite3_session *pSession);
|
||||
|
||||
/*
|
||||
** CAPI3REF: Create An Iterator To Traverse A Changeset
|
||||
**
|
||||
** Create an iterator used to iterate through the contents of a changeset.
|
||||
** If successful, *pp is set to point to the iterator handle and SQLITE_OK
|
||||
** is returned. Otherwise, if an error occurs, *pp is set to zero and an
|
||||
** SQLite error code is returned.
|
||||
**
|
||||
** The following functions can be used to advance and query a changeset
|
||||
** iterator created by this function:
|
||||
**
|
||||
** <ul>
|
||||
** <li> [sqlite3changeset_next()]
|
||||
** <li> [sqlite3changeset_op()]
|
||||
** <li> [sqlite3changeset_new()]
|
||||
** <li> [sqlite3changeset_old()]
|
||||
** </ul>
|
||||
**
|
||||
** It is the responsibility of the caller to eventually destroy the iterator
|
||||
** by passing it to [sqlite3changeset_finalize()]. The buffer containing the
|
||||
** changeset (pChangeset) must remain valid until after the iterator is
|
||||
** destroyed.
|
||||
**
|
||||
** Assuming the changeset blob was created by one of the
|
||||
** [sqlite3session_changeset()], [sqlite3changeset_concat()] or
|
||||
** [sqlite3changeset_invert()] functions, all changes within the changeset
|
||||
** that apply to a single table are grouped together. This means that when
|
||||
** an application iterates through a changeset using an iterator created by
|
||||
** this function, all changes that relate to a single table are visted
|
||||
** consecutively. There is no chance that the iterator will visit a change
|
||||
** the applies to table X, then one for table Y, and then later on visit
|
||||
** another change for table X.
|
||||
*/
|
||||
int sqlite3changeset_start(
|
||||
sqlite3_changeset_iter **pp, /* OUT: New changeset iterator handle */
|
||||
int nChangeset, /* Size of changeset blob in bytes */
|
||||
void *pChangeset /* Pointer to blob containing changeset */
|
||||
);
|
||||
|
||||
/*
|
||||
** CAPI3REF: Advance A Changeset Iterator
|
||||
**
|
||||
** This function may only be used with iterators created by function
|
||||
** [sqlite3changeset_start()]. If it is called on an iterator passed to
|
||||
** a conflict-handler callback by [sqlite3changeset_apply()], SQLITE_MISUSE
|
||||
** is returned and the call has no effect.
|
||||
**
|
||||
** Immediately after an iterator is created by sqlite3changeset_start(), it
|
||||
** does not point to any change in the changeset. Assuming the changeset
|
||||
** is not empty, the first call to this function advances the iterator to
|
||||
** point to the first change in the changeset. Each subsequent call advances
|
||||
** the iterator to point to the next change in the changeset (if any). If
|
||||
** no error occurs and the iterator points to a valid change after a call
|
||||
** to sqlite3changeset_next() has advanced it, SQLITE_ROW is returned.
|
||||
** Otherwise, if all changes in the changeset have already been visited,
|
||||
** SQLITE_DONE is returned.
|
||||
**
|
||||
** If an error occurs, an SQLite error code is returned. Possible error
|
||||
** codes include SQLITE_CORRUPT (if the changeset buffer is corrupt) or
|
||||
** SQLITE_NOMEM.
|
||||
*/
|
||||
int sqlite3changeset_next(sqlite3_changeset_iter *pIter);
|
||||
|
||||
/*
|
||||
** CAPI3REF: Obtain The Current Operation From A Changeset Iterator
|
||||
**
|
||||
** The pIter argument passed to this function may either be an iterator
|
||||
** passed to a conflict-handler by [sqlite3changeset_apply()], or an iterator
|
||||
** created by [sqlite3changeset_start()]. In the latter case, the most recent
|
||||
** call to [sqlite3changeset_next()] must have returned [SQLITE_ROW]. If this
|
||||
** is not the case, this function returns [SQLITE_MISUSE].
|
||||
**
|
||||
** If argument pzTab is not NULL, then *pzTab is set to point to a
|
||||
** nul-terminated utf-8 encoded string containing the name of the table
|
||||
** affected by the current change. The buffer remains valid until either
|
||||
** sqlite3changeset_next() is called on the iterator or until the
|
||||
** conflict-handler function returns. If pnCol is not NULL, then *pnCol is
|
||||
** set to the number of columns in the table affected by the change. If
|
||||
** pbIncorrect is not NULL, then *pbIndirect is set to true (1) if the change
|
||||
** is an indirect change, or false (0) otherwise. See the documentation for
|
||||
** [sqlite3session_indirect()] for a description of direct and indirect
|
||||
** changes. Finally, if pOp is not NULL, then *pOp is set to one of
|
||||
** [SQLITE_INSERT], [SQLITE_DELETE] or [SQLITE_UPDATE], depending on the
|
||||
** type of change that the iterator currently points to.
|
||||
**
|
||||
** If no error occurs, SQLITE_OK is returned. If an error does occur, an
|
||||
** SQLite error code is returned. The values of the output variables may not
|
||||
** be trusted in this case.
|
||||
*/
|
||||
int sqlite3changeset_op(
|
||||
sqlite3_changeset_iter *pIter, /* Iterator object */
|
||||
const char **pzTab, /* OUT: Pointer to table name */
|
||||
int *pnCol, /* OUT: Number of columns in table */
|
||||
int *pOp, /* OUT: SQLITE_INSERT, DELETE or UPDATE */
|
||||
int *pbIndirect /* OUT: True for an 'indirect' change */
|
||||
);
|
||||
|
||||
/*
|
||||
** CAPI3REF: Obtain The Primary Key Definition Of A Table
|
||||
**
|
||||
** For each modified table, a changeset includes the following:
|
||||
**
|
||||
** <ul>
|
||||
** <li> The number of columns in the table, and
|
||||
** <li> Which of those columns make up the tables PRIMARY KEY.
|
||||
** </ul>
|
||||
**
|
||||
** This function is used to find which columns comprise the PRIMARY KEY of
|
||||
** the table modified by the change that iterator pIter currently points to.
|
||||
** If successful, *pabPK is set to point to an array of nCol entries, where
|
||||
** nCol is the number of columns in the table. Elements of *pabPK are set to
|
||||
** 0x01 if the corresponding column is part of the tables primary key, or
|
||||
** 0x00 if it is not.
|
||||
**
|
||||
** If argumet pnCol is not NULL, then *pnCol is set to the number of columns
|
||||
** in the table.
|
||||
**
|
||||
** If this function is called when the iterator does not point to a valid
|
||||
** entry, SQLITE_MISUSE is returned and the output variables zeroed. Otherwise,
|
||||
** SQLITE_OK is returned and the output variables populated as described
|
||||
** above.
|
||||
*/
|
||||
int sqlite3changeset_pk(
|
||||
sqlite3_changeset_iter *pIter, /* Iterator object */
|
||||
unsigned char **pabPK, /* OUT: Array of boolean - true for PK cols */
|
||||
int *pnCol /* OUT: Number of entries in output array */
|
||||
);
|
||||
|
||||
/*
|
||||
** CAPI3REF: Obtain old.* Values From A Changeset Iterator
|
||||
**
|
||||
** The pIter argument passed to this function may either be an iterator
|
||||
** passed to a conflict-handler by [sqlite3changeset_apply()], or an iterator
|
||||
** created by [sqlite3changeset_start()]. In the latter case, the most recent
|
||||
** call to [sqlite3changeset_next()] must have returned SQLITE_ROW.
|
||||
** Furthermore, it may only be called if the type of change that the iterator
|
||||
** currently points to is either [SQLITE_DELETE] or [SQLITE_UPDATE]. Otherwise,
|
||||
** this function returns [SQLITE_MISUSE] and sets *ppValue to NULL.
|
||||
**
|
||||
** Argument iVal must be greater than or equal to 0, and less than the number
|
||||
** of columns in the table affected by the current change. Otherwise,
|
||||
** [SQLITE_RANGE] is returned and *ppValue is set to NULL.
|
||||
**
|
||||
** If successful, this function sets *ppValue to point to a protected
|
||||
** sqlite3_value object containing the iVal'th value from the vector of
|
||||
** original row values stored as part of the UPDATE or DELETE change and
|
||||
** returns SQLITE_OK. The name of the function comes from the fact that this
|
||||
** is similar to the "old.*" columns available to update or delete triggers.
|
||||
**
|
||||
** If some other error occurs (e.g. an OOM condition), an SQLite error code
|
||||
** is returned and *ppValue is set to NULL.
|
||||
*/
|
||||
int sqlite3changeset_old(
|
||||
sqlite3_changeset_iter *pIter, /* Changeset iterator */
|
||||
int iVal, /* Column number */
|
||||
sqlite3_value **ppValue /* OUT: Old value (or NULL pointer) */
|
||||
);
|
||||
|
||||
/*
|
||||
** CAPI3REF: Obtain new.* Values From A Changeset Iterator
|
||||
**
|
||||
** The pIter argument passed to this function may either be an iterator
|
||||
** passed to a conflict-handler by [sqlite3changeset_apply()], or an iterator
|
||||
** created by [sqlite3changeset_start()]. In the latter case, the most recent
|
||||
** call to [sqlite3changeset_next()] must have returned SQLITE_ROW.
|
||||
** Furthermore, it may only be called if the type of change that the iterator
|
||||
** currently points to is either [SQLITE_UPDATE] or [SQLITE_INSERT]. Otherwise,
|
||||
** this function returns [SQLITE_MISUSE] and sets *ppValue to NULL.
|
||||
**
|
||||
** Argument iVal must be greater than or equal to 0, and less than the number
|
||||
** of columns in the table affected by the current change. Otherwise,
|
||||
** [SQLITE_RANGE] is returned and *ppValue is set to NULL.
|
||||
**
|
||||
** If successful, this function sets *ppValue to point to a protected
|
||||
** sqlite3_value object containing the iVal'th value from the vector of
|
||||
** new row values stored as part of the UPDATE or INSERT change and
|
||||
** returns SQLITE_OK. If the change is an UPDATE and does not include
|
||||
** a new value for the requested column, *ppValue is set to NULL and
|
||||
** SQLITE_OK returned. The name of the function comes from the fact that
|
||||
** this is similar to the "new.*" columns available to update or delete
|
||||
** triggers.
|
||||
**
|
||||
** If some other error occurs (e.g. an OOM condition), an SQLite error code
|
||||
** is returned and *ppValue is set to NULL.
|
||||
*/
|
||||
int sqlite3changeset_new(
|
||||
sqlite3_changeset_iter *pIter, /* Changeset iterator */
|
||||
int iVal, /* Column number */
|
||||
sqlite3_value **ppValue /* OUT: New value (or NULL pointer) */
|
||||
);
|
||||
|
||||
/*
|
||||
** CAPI3REF: Obtain Conflicting Row Values From A Changeset Iterator
|
||||
**
|
||||
** This function should only be used with iterator objects passed to a
|
||||
** conflict-handler callback by [sqlite3changeset_apply()] with either
|
||||
** [SQLITE_CHANGESET_DATA] or [SQLITE_CHANGESET_CONFLICT]. If this function
|
||||
** is called on any other iterator, [SQLITE_MISUSE] is returned and *ppValue
|
||||
** is set to NULL.
|
||||
**
|
||||
** Argument iVal must be greater than or equal to 0, and less than the number
|
||||
** of columns in the table affected by the current change. Otherwise,
|
||||
** [SQLITE_RANGE] is returned and *ppValue is set to NULL.
|
||||
**
|
||||
** If successful, this function sets *ppValue to point to a protected
|
||||
** sqlite3_value object containing the iVal'th value from the
|
||||
** "conflicting row" associated with the current conflict-handler callback
|
||||
** and returns SQLITE_OK.
|
||||
**
|
||||
** If some other error occurs (e.g. an OOM condition), an SQLite error code
|
||||
** is returned and *ppValue is set to NULL.
|
||||
*/
|
||||
int sqlite3changeset_conflict(
|
||||
sqlite3_changeset_iter *pIter, /* Changeset iterator */
|
||||
int iVal, /* Column number */
|
||||
sqlite3_value **ppValue /* OUT: Value from conflicting row */
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
** CAPI3REF: Finalize A Changeset Iterator
|
||||
**
|
||||
** This function is used to finalize an iterator allocated with
|
||||
** [sqlite3changeset_start()].
|
||||
**
|
||||
** This function should only be called on iterators created using the
|
||||
** [sqlite3changeset_start()] function. If an application calls this
|
||||
** function with an iterator passed to a conflict-handler by
|
||||
** [sqlite3changeset_apply()], [SQLITE_MISUSE] is immediately returned and the
|
||||
** call has no effect.
|
||||
**
|
||||
** If an error was encountered within a call to an sqlite3changeset_xxx()
|
||||
** function (for example an [SQLITE_CORRUPT] in [sqlite3changeset_next()] or an
|
||||
** [SQLITE_NOMEM] in [sqlite3changeset_new()]) then an error code corresponding
|
||||
** to that error is returned by this function. Otherwise, SQLITE_OK is
|
||||
** returned. This is to allow the following pattern (pseudo-code):
|
||||
**
|
||||
** sqlite3changeset_start();
|
||||
** while( SQLITE_ROW==sqlite3changeset_next() ){
|
||||
** // Do something with change.
|
||||
** }
|
||||
** rc = sqlite3changeset_finalize();
|
||||
** if( rc!=SQLITE_OK ){
|
||||
** // An error has occurred
|
||||
** }
|
||||
*/
|
||||
int sqlite3changeset_finalize(sqlite3_changeset_iter *pIter);
|
||||
|
||||
/*
|
||||
** CAPI3REF: Invert A Changeset
|
||||
**
|
||||
** This function is used to "invert" a changeset object. Applying an inverted
|
||||
** changeset to a database reverses the effects of applying the uninverted
|
||||
** changeset. Specifically:
|
||||
**
|
||||
** <ul>
|
||||
** <li> Each DELETE change is changed to an INSERT, and
|
||||
** <li> Each INSERT change is changed to a DELETE, and
|
||||
** <li> For each UPDATE change, the old.* and new.* values are exchanged.
|
||||
** </ul>
|
||||
**
|
||||
** If successful, a pointer to a buffer containing the inverted changeset
|
||||
** is stored in *ppOut, the size of the same buffer is stored in *pnOut, and
|
||||
** SQLITE_OK is returned. If an error occurs, both *pnOut and *ppOut are
|
||||
** zeroed and an SQLite error code returned.
|
||||
**
|
||||
** It is the responsibility of the caller to eventually call sqlite3_free()
|
||||
** on the *ppOut pointer to free the buffer allocation following a successful
|
||||
** call to this function.
|
||||
**
|
||||
** WARNING/TODO: This function currently assumes that the input is a valid
|
||||
** changeset. If it is not, the results are undefined.
|
||||
*/
|
||||
int sqlite3changeset_invert(
|
||||
int nIn, const void *pIn, /* Input changeset */
|
||||
int *pnOut, void **ppOut /* OUT: Inverse of input */
|
||||
);
|
||||
|
||||
/*
|
||||
** CAPI3REF: Concatenate Two Changeset Objects
|
||||
**
|
||||
** This function is used to concatenate two changesets, A and B, into a
|
||||
** single changeset. The result is a changeset equivalent to applying
|
||||
** changeset A followed by changeset B.
|
||||
**
|
||||
** Rows are identified by the values in their PRIMARY KEY columns. A change
|
||||
** in changeset A is considered to apply to the same row as a change in
|
||||
** changeset B if the two rows have the same primary key.
|
||||
**
|
||||
** Changes to rows that appear only in changeset A or B are copied into the
|
||||
** output changeset. Or, if both changeset A and B contain a change that
|
||||
** applies to a single row, the output depends on the type of each change,
|
||||
** as follows:
|
||||
**
|
||||
** <table border=1 style="margin-left:8ex;margin-right:8ex">
|
||||
** <tr><th style="white-space:pre">Change A </th>
|
||||
** <th style="white-space:pre">Change B </th>
|
||||
** <th>Output Change
|
||||
** <tr><td>INSERT <td>INSERT <td>
|
||||
** Change A is copied into the output changeset. Change B is discarded.
|
||||
** This case does not occur if changeset B is recorded immediately after
|
||||
** changeset A.
|
||||
** <tr><td>INSERT <td>UPDATE <td>
|
||||
** An INSERT change is copied into the output changeset. The values in
|
||||
** the INSERT change are as if the row was inserted by change A and then
|
||||
** updated according to change B.
|
||||
** <tr><td>INSERT <td>DELETE <td>
|
||||
** No change at all is copied into the output changeset.
|
||||
** <tr><td>UPDATE <td>INSERT <td>
|
||||
** Change A is copied into the output changeset. Change B is discarded.
|
||||
** This case does not occur if changeset B is recorded immediately after
|
||||
** changeset A.
|
||||
** <tr><td>UPDATE <td>UPDATE <td>
|
||||
** A single UPDATE is copied into the output changeset. The accompanying
|
||||
** values are as if the row was updated once by change A and then again
|
||||
** by change B.
|
||||
** <tr><td>UPDATE <td>DELETE <td>
|
||||
** A single DELETE is copied into the output changeset.
|
||||
** <tr><td>DELETE <td>INSERT <td>
|
||||
** If one or more of the column values in the row inserted by change
|
||||
** B differ from those in the row deleted by change A, an UPDATE
|
||||
** change is added to the output changeset. Otherwise, if the inserted
|
||||
** row is exactly the same as the deleted row, no change is added to
|
||||
** the output changeset.
|
||||
** <tr><td>DELETE <td>UPDATE <td>
|
||||
** Change A is copied into the output changeset. Change B is discarded.
|
||||
** This case does not occur if changeset B is recorded immediately after
|
||||
** changeset A.
|
||||
** <tr><td>DELETE <td>DELETE <td>
|
||||
** Change A is copied into the output changeset. Change B is discarded.
|
||||
** This case does not occur if changeset B is recorded immediately after
|
||||
** changeset A.
|
||||
** </table>
|
||||
**
|
||||
** If the two changesets contain changes to the same table, then the number
|
||||
** of columns and the position of the primary key columns for the table must
|
||||
** be the same in each changeset. If this is not the case, attempting to
|
||||
** concatenate the two changesets together fails and this function returns
|
||||
** SQLITE_SCHEMA. If either of the two input changesets appear to be corrupt,
|
||||
** and the corruption is detected, SQLITE_CORRUPT is returned. Or, if an
|
||||
** out-of-memory condition occurs during processing, this function returns
|
||||
** SQLITE_NOMEM.
|
||||
**
|
||||
** If none of the above errors occur, SQLITE_OK is returned and *ppOut set
|
||||
** to point to a buffer containing the output changeset. It is the
|
||||
** responsibility of the caller to eventually call sqlite3_free() on *ppOut
|
||||
** to release memory allocated for the buffer. *pnOut is set to the number
|
||||
** of bytes in the output changeset. If an error does occur, both *ppOut and
|
||||
** *pnOut are set to zero before returning.
|
||||
*/
|
||||
int sqlite3changeset_concat(
|
||||
int nA, /* Number of bytes in buffer pA */
|
||||
void *pA, /* Pointer to buffer containing changeset A */
|
||||
int nB, /* Number of bytes in buffer pB */
|
||||
void *pB, /* Pointer to buffer containing changeset B */
|
||||
int *pnOut, /* OUT: Number of bytes in output changeset */
|
||||
void **ppOut /* OUT: Buffer containing output changeset */
|
||||
);
|
||||
|
||||
/*
|
||||
** CAPI3REF: Apply A Changeset To A Database
|
||||
**
|
||||
** Apply a changeset to a database. This function attempts to update the
|
||||
** "main" database attached to handle db with the changes found in the
|
||||
** changeset passed via the second and third arguments.
|
||||
**
|
||||
** The fourth argument (xFilter) passed to this function is the "filter
|
||||
** callback". If it is not NULL, then for each table affected by at least one
|
||||
** change in the changeset, the filter callback is invoked with
|
||||
** the table name as the second argument, and a copy of the context pointer
|
||||
** passed as the sixth argument to this function as the first. If the "filter
|
||||
** callback" returns zero, then no attempt is made to apply any changes to
|
||||
** the table. Otherwise, if the return value is non-zero or the xFilter
|
||||
** argument to this function is NULL, all changes related to the table are
|
||||
** attempted.
|
||||
**
|
||||
** For each table that is not excluded by the filter callback, this function
|
||||
** tests that the target database contains a compatible table. A table is
|
||||
** considered compatible if all of the following are true:
|
||||
**
|
||||
** <ul>
|
||||
** <li> The table has the same name as the name recorded in the
|
||||
** changeset, and
|
||||
** <li> The table has the same number of columns as recorded in the
|
||||
** changeset, and
|
||||
** <li> The table has primary key columns in the same position as
|
||||
** recorded in the changeset.
|
||||
** </ul>
|
||||
**
|
||||
** If there is no compatible table, it is not an error, but none of the
|
||||
** changes associated with the table are applied. A warning message is issued
|
||||
** via the sqlite3_log() mechanism with the error code SQLITE_SCHEMA. At most
|
||||
** one such warning is issued for each table in the changeset.
|
||||
**
|
||||
** For each change for which there is a compatible table, an attempt is made
|
||||
** to modify the table contents according to the UPDATE, INSERT or DELETE
|
||||
** change. If a change cannot be applied cleanly, the conflict handler
|
||||
** function passed as the fifth argument to sqlite3changeset_apply() may be
|
||||
** invoked. A description of exactly when the conflict handler is invoked for
|
||||
** each type of change is below.
|
||||
**
|
||||
** Each time the conflict handler function is invoked, it must return one
|
||||
** of [SQLITE_CHANGESET_OMIT], [SQLITE_CHANGESET_ABORT] or
|
||||
** [SQLITE_CHANGESET_REPLACE]. SQLITE_CHANGESET_REPLACE may only be returned
|
||||
** if the second argument passed to the conflict handler is either
|
||||
** SQLITE_CHANGESET_DATA or SQLITE_CHANGESET_CONFLICT. If the conflict-handler
|
||||
** returns an illegal value, any changes already made are rolled back and
|
||||
** the call to sqlite3changeset_apply() returns SQLITE_MISUSE. Different
|
||||
** actions are taken by sqlite3changeset_apply() depending on the value
|
||||
** returned by each invocation of the conflict-handler function. Refer to
|
||||
** the documentation for the three
|
||||
** [SQLITE_CHANGESET_OMIT|available return values] for details.
|
||||
**
|
||||
** <dl>
|
||||
** <dt>DELETE Changes<dd>
|
||||
** For each DELETE change, this function checks if the target database
|
||||
** contains a row with the same primary key value (or values) as the
|
||||
** original row values stored in the changeset. If it does, and the values
|
||||
** stored in all non-primary key columns also match the values stored in
|
||||
** the changeset the row is deleted from the target database.
|
||||
**
|
||||
** If a row with matching primary key values is found, but one or more of
|
||||
** the non-primary key fields contains a value different from the original
|
||||
** row value stored in the changeset, the conflict-handler function is
|
||||
** invoked with [SQLITE_CHANGESET_DATA] as the second argument.
|
||||
**
|
||||
** If no row with matching primary key values is found in the database,
|
||||
** the conflict-handler function is invoked with [SQLITE_CHANGESET_NOTFOUND]
|
||||
** passed as the second argument.
|
||||
**
|
||||
** If the DELETE operation is attempted, but SQLite returns SQLITE_CONSTRAINT
|
||||
** (which can only happen if a foreign key constraint is violated), the
|
||||
** conflict-handler function is invoked with [SQLITE_CHANGESET_CONSTRAINT]
|
||||
** passed as the second argument. This includes the case where the DELETE
|
||||
** operation is attempted because an earlier call to the conflict handler
|
||||
** function returned [SQLITE_CHANGESET_REPLACE].
|
||||
**
|
||||
** <dt>INSERT Changes<dd>
|
||||
** For each INSERT change, an attempt is made to insert the new row into
|
||||
** the database.
|
||||
**
|
||||
** If the attempt to insert the row fails because the database already
|
||||
** contains a row with the same primary key values, the conflict handler
|
||||
** function is invoked with the second argument set to
|
||||
** [SQLITE_CHANGESET_CONFLICT].
|
||||
**
|
||||
** If the attempt to insert the row fails because of some other constraint
|
||||
** violation (e.g. NOT NULL or UNIQUE), the conflict handler function is
|
||||
** invoked with the second argument set to [SQLITE_CHANGESET_CONSTRAINT].
|
||||
** This includes the case where the INSERT operation is re-attempted because
|
||||
** an earlier call to the conflict handler function returned
|
||||
** [SQLITE_CHANGESET_REPLACE].
|
||||
**
|
||||
** <dt>UPDATE Changes<dd>
|
||||
** For each UPDATE change, this function checks if the target database
|
||||
** contains a row with the same primary key value (or values) as the
|
||||
** original row values stored in the changeset. If it does, and the values
|
||||
** stored in all non-primary key columns also match the values stored in
|
||||
** the changeset the row is updated within the target database.
|
||||
**
|
||||
** If a row with matching primary key values is found, but one or more of
|
||||
** the non-primary key fields contains a value different from an original
|
||||
** row value stored in the changeset, the conflict-handler function is
|
||||
** invoked with [SQLITE_CHANGESET_DATA] as the second argument. Since
|
||||
** UPDATE changes only contain values for non-primary key fields that are
|
||||
** to be modified, only those fields need to match the original values to
|
||||
** avoid the SQLITE_CHANGESET_DATA conflict-handler callback.
|
||||
**
|
||||
** If no row with matching primary key values is found in the database,
|
||||
** the conflict-handler function is invoked with [SQLITE_CHANGESET_NOTFOUND]
|
||||
** passed as the second argument.
|
||||
**
|
||||
** If the UPDATE operation is attempted, but SQLite returns
|
||||
** SQLITE_CONSTRAINT, the conflict-handler function is invoked with
|
||||
** [SQLITE_CHANGESET_CONSTRAINT] passed as the second argument.
|
||||
** This includes the case where the UPDATE operation is attempted after
|
||||
** an earlier call to the conflict handler function returned
|
||||
** [SQLITE_CHANGESET_REPLACE].
|
||||
** </dl>
|
||||
**
|
||||
** It is safe to execute SQL statements, including those that write to the
|
||||
** table that the callback related to, from within the xConflict callback.
|
||||
** This can be used to further customize the applications conflict
|
||||
** resolution strategy.
|
||||
**
|
||||
** All changes made by this function are enclosed in a savepoint transaction.
|
||||
** If any other error (aside from a constraint failure when attempting to
|
||||
** write to the target database) occurs, then the savepoint transaction is
|
||||
** rolled back, restoring the target database to its original state, and an
|
||||
** SQLite error code returned.
|
||||
*/
|
||||
int sqlite3changeset_apply(
|
||||
sqlite3 *db, /* Apply change to "main" db of this handle */
|
||||
int nChangeset, /* Size of changeset in bytes */
|
||||
void *pChangeset, /* Changeset blob */
|
||||
int(*xFilter)(
|
||||
void *pCtx, /* Copy of sixth arg to _apply() */
|
||||
const char *zTab /* Table name */
|
||||
),
|
||||
int(*xConflict)(
|
||||
void *pCtx, /* Copy of sixth arg to _apply() */
|
||||
int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */
|
||||
sqlite3_changeset_iter *p /* Handle describing change and conflict */
|
||||
),
|
||||
void *pCtx /* First argument passed to xConflict */
|
||||
);
|
||||
|
||||
/*
|
||||
** CAPI3REF: Constants Passed To The Conflict Handler
|
||||
**
|
||||
** Values that may be passed as the second argument to a conflict-handler.
|
||||
**
|
||||
** <dl>
|
||||
** <dt>SQLITE_CHANGESET_DATA<dd>
|
||||
** The conflict handler is invoked with CHANGESET_DATA as the second argument
|
||||
** when processing a DELETE or UPDATE change if a row with the required
|
||||
** PRIMARY KEY fields is present in the database, but one or more other
|
||||
** (non primary-key) fields modified by the update do not contain the
|
||||
** expected "before" values.
|
||||
**
|
||||
** The conflicting row, in this case, is the database row with the matching
|
||||
** primary key.
|
||||
**
|
||||
** <dt>SQLITE_CHANGESET_NOTFOUND<dd>
|
||||
** The conflict handler is invoked with CHANGESET_NOTFOUND as the second
|
||||
** argument when processing a DELETE or UPDATE change if a row with the
|
||||
** required PRIMARY KEY fields is not present in the database.
|
||||
**
|
||||
** There is no conflicting row in this case. The results of invoking the
|
||||
** sqlite3changeset_conflict() API are undefined.
|
||||
**
|
||||
** <dt>SQLITE_CHANGESET_CONFLICT<dd>
|
||||
** CHANGESET_CONFLICT is passed as the second argument to the conflict
|
||||
** handler while processing an INSERT change if the operation would result
|
||||
** in duplicate primary key values.
|
||||
**
|
||||
** The conflicting row in this case is the database row with the matching
|
||||
** primary key.
|
||||
**
|
||||
** <dt>SQLITE_CHANGESET_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
|
||||
|
||||
/*
|
||||
** Make sure we can call this stuff from C++.
|
||||
*/
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SQLITE_ENABLE_SESSION && SQLITE_ENABLE_PREUPDATE_HOOK */
|
||||
@@ -0,0 +1,613 @@
|
||||
|
||||
#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 */
|
||||
{ "isempty", 0, "", }, /* 5 */
|
||||
{ 0 }
|
||||
};
|
||||
int iSub;
|
||||
int rc;
|
||||
|
||||
if( objc<2 ){
|
||||
Tcl_WrongNumArgs(interp, 1, objv, "SUBCOMMAND ...");
|
||||
return TCL_ERROR;
|
||||
}
|
||||
rc = Tcl_GetIndexFromObjStruct(interp,
|
||||
objv[1], aSub, sizeof(aSub[0]), "sub-command", 0, &iSub
|
||||
);
|
||||
if( rc!=TCL_OK ) return rc;
|
||||
if( objc!=2+aSub[iSub].nArg ){
|
||||
Tcl_WrongNumArgs(interp, 2, objv, aSub[iSub].zMsg);
|
||||
return TCL_ERROR;
|
||||
}
|
||||
|
||||
switch( iSub ){
|
||||
case 0: { /* attach */
|
||||
char *zArg = Tcl_GetString(objv[2]);
|
||||
if( zArg[0]=='*' && zArg[1]=='\0' ) zArg = 0;
|
||||
rc = sqlite3session_attach(pSession, zArg);
|
||||
if( rc!=SQLITE_OK ){
|
||||
return test_session_error(interp, rc);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 1: { /* changeset */
|
||||
int nChange;
|
||||
void *pChange;
|
||||
rc = sqlite3session_changeset(pSession, &nChange, &pChange);
|
||||
if( rc==SQLITE_OK ){
|
||||
Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(pChange, nChange));
|
||||
sqlite3_free(pChange);
|
||||
}else{
|
||||
return test_session_error(interp, rc);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 2: /* delete */
|
||||
Tcl_DeleteCommand(interp, Tcl_GetString(objv[0]));
|
||||
break;
|
||||
|
||||
case 3: { /* enable */
|
||||
int val;
|
||||
if( Tcl_GetIntFromObj(interp, objv[2], &val) ) return TCL_ERROR;
|
||||
val = sqlite3session_enable(pSession, val);
|
||||
Tcl_SetObjResult(interp, Tcl_NewBooleanObj(val));
|
||||
break;
|
||||
}
|
||||
|
||||
case 4: { /* indirect */
|
||||
int val;
|
||||
if( Tcl_GetIntFromObj(interp, objv[2], &val) ) return TCL_ERROR;
|
||||
val = sqlite3session_indirect(pSession, val);
|
||||
Tcl_SetObjResult(interp, Tcl_NewBooleanObj(val));
|
||||
break;
|
||||
}
|
||||
|
||||
case 5: { /* isempty */
|
||||
int val;
|
||||
val = sqlite3session_isempty(pSession);
|
||||
Tcl_SetObjResult(interp, Tcl_NewBooleanObj(val));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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 *pConflictScript;
|
||||
Tcl_Obj *pFilterScript;
|
||||
};
|
||||
|
||||
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_filter_handler(
|
||||
void *pCtx, /* Pointer to TestConflictHandler structure */
|
||||
const char *zTab /* Table name */
|
||||
){
|
||||
TestConflictHandler *p = (TestConflictHandler *)pCtx;
|
||||
int res = 1;
|
||||
Tcl_Obj *pEval;
|
||||
Tcl_Interp *interp = p->interp;
|
||||
|
||||
pEval = Tcl_DuplicateObj(p->pFilterScript);
|
||||
Tcl_IncrRefCount(pEval);
|
||||
|
||||
if( TCL_OK!=Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj(zTab, -1))
|
||||
|| TCL_OK!=Tcl_EvalObjEx(interp, pEval, TCL_EVAL_GLOBAL)
|
||||
|| TCL_OK!=Tcl_GetIntFromObj(interp, Tcl_GetObjResult(interp), &res)
|
||||
){
|
||||
Tcl_BackgroundError(interp);
|
||||
}
|
||||
|
||||
Tcl_DecrRefCount(pEval);
|
||||
return res;
|
||||
}
|
||||
|
||||
static int test_conflict_handler(
|
||||
void *pCtx, /* Pointer to TestConflictHandler structure */
|
||||
int eConf, /* DATA, MISSING, CONFLICT, CONSTRAINT */
|
||||
sqlite3_changeset_iter *pIter /* Handle describing change and conflict */
|
||||
){
|
||||
TestConflictHandler *p = (TestConflictHandler *)pCtx;
|
||||
Tcl_Obj *pEval;
|
||||
Tcl_Interp *interp = p->interp;
|
||||
int ret = 0; /* Return value */
|
||||
|
||||
int op; /* SQLITE_UPDATE, DELETE or INSERT */
|
||||
const char *zTab; /* Name of table conflict is on */
|
||||
int nCol; /* Number of columns in table zTab */
|
||||
|
||||
pEval = Tcl_DuplicateObj(p->pConflictScript);
|
||||
Tcl_IncrRefCount(pEval);
|
||||
|
||||
sqlite3changeset_op(pIter, &zTab, &nCol, &op, 0);
|
||||
|
||||
/* Append the operation type. */
|
||||
Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj(
|
||||
op==SQLITE_INSERT ? "INSERT" :
|
||||
op==SQLITE_UPDATE ? "UPDATE" :
|
||||
"DELETE", -1
|
||||
));
|
||||
|
||||
/* Append the table name. */
|
||||
Tcl_ListObjAppendElement(0, pEval, Tcl_NewStringObj(zTab, -1));
|
||||
|
||||
/* Append the conflict type. */
|
||||
switch( eConf ){
|
||||
case SQLITE_CHANGESET_DATA:
|
||||
Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("DATA",-1));
|
||||
break;
|
||||
case SQLITE_CHANGESET_NOTFOUND:
|
||||
Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("NOTFOUND",-1));
|
||||
break;
|
||||
case SQLITE_CHANGESET_CONFLICT:
|
||||
Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("CONFLICT",-1));
|
||||
break;
|
||||
case SQLITE_CHANGESET_CONSTRAINT:
|
||||
Tcl_ListObjAppendElement(interp, pEval,Tcl_NewStringObj("CONSTRAINT",-1));
|
||||
break;
|
||||
}
|
||||
|
||||
/* If this is not an INSERT, append the old row */
|
||||
if( op!=SQLITE_INSERT ){
|
||||
int i;
|
||||
Tcl_Obj *pOld = Tcl_NewObj();
|
||||
for(i=0; i<nCol; i++){
|
||||
sqlite3_value *pVal;
|
||||
sqlite3changeset_old(pIter, i, &pVal);
|
||||
test_append_value(pOld, pVal);
|
||||
}
|
||||
Tcl_ListObjAppendElement(0, pEval, pOld);
|
||||
}
|
||||
|
||||
/* If this is not a DELETE, append the new row */
|
||||
if( op!=SQLITE_DELETE ){
|
||||
int i;
|
||||
Tcl_Obj *pNew = Tcl_NewObj();
|
||||
for(i=0; i<nCol; i++){
|
||||
sqlite3_value *pVal;
|
||||
sqlite3changeset_new(pIter, i, &pVal);
|
||||
test_append_value(pNew, pVal);
|
||||
}
|
||||
Tcl_ListObjAppendElement(0, pEval, pNew);
|
||||
}
|
||||
|
||||
/* If this is a CHANGESET_DATA or CHANGESET_CONFLICT conflict, append
|
||||
** the conflicting row. */
|
||||
if( eConf==SQLITE_CHANGESET_DATA || eConf==SQLITE_CHANGESET_CONFLICT ){
|
||||
int i;
|
||||
Tcl_Obj *pConflict = Tcl_NewObj();
|
||||
for(i=0; i<nCol; i++){
|
||||
int rc;
|
||||
sqlite3_value *pVal;
|
||||
rc = sqlite3changeset_conflict(pIter, i, &pVal);
|
||||
assert( rc==SQLITE_OK );
|
||||
test_append_value(pConflict, pVal);
|
||||
}
|
||||
Tcl_ListObjAppendElement(0, pEval, pConflict);
|
||||
}
|
||||
|
||||
/***********************************************************************
|
||||
** This block is purely for testing some error conditions.
|
||||
*/
|
||||
if( eConf==SQLITE_CHANGESET_CONSTRAINT || eConf==SQLITE_CHANGESET_NOTFOUND ){
|
||||
sqlite3_value *pVal;
|
||||
int rc = sqlite3changeset_conflict(pIter, 0, &pVal);
|
||||
assert( rc==SQLITE_MISUSE );
|
||||
}else{
|
||||
sqlite3_value *pVal;
|
||||
int rc = sqlite3changeset_conflict(pIter, -1, &pVal);
|
||||
assert( rc==SQLITE_RANGE );
|
||||
rc = sqlite3changeset_conflict(pIter, nCol, &pVal);
|
||||
assert( rc==SQLITE_RANGE );
|
||||
}
|
||||
if( op==SQLITE_DELETE ){
|
||||
sqlite3_value *pVal;
|
||||
int rc = sqlite3changeset_new(pIter, 0, &pVal);
|
||||
assert( rc==SQLITE_MISUSE );
|
||||
}else{
|
||||
sqlite3_value *pVal;
|
||||
int rc = sqlite3changeset_new(pIter, -1, &pVal);
|
||||
assert( rc==SQLITE_RANGE );
|
||||
rc = sqlite3changeset_new(pIter, nCol, &pVal);
|
||||
assert( rc==SQLITE_RANGE );
|
||||
}
|
||||
if( op==SQLITE_INSERT ){
|
||||
sqlite3_value *pVal;
|
||||
int rc = sqlite3changeset_old(pIter, 0, &pVal);
|
||||
assert( rc==SQLITE_MISUSE );
|
||||
}else{
|
||||
sqlite3_value *pVal;
|
||||
int rc = sqlite3changeset_old(pIter, -1, &pVal);
|
||||
assert( rc==SQLITE_RANGE );
|
||||
rc = sqlite3changeset_old(pIter, nCol, &pVal);
|
||||
assert( rc==SQLITE_RANGE );
|
||||
}
|
||||
/* End of testing block
|
||||
***********************************************************************/
|
||||
|
||||
if( TCL_OK!=Tcl_EvalObjEx(interp, pEval, TCL_EVAL_GLOBAL) ){
|
||||
Tcl_BackgroundError(interp);
|
||||
}else{
|
||||
Tcl_Obj *pRes = Tcl_GetObjResult(interp);
|
||||
if( test_obj_eq_string(pRes, "OMIT") || test_obj_eq_string(pRes, "") ){
|
||||
ret = SQLITE_CHANGESET_OMIT;
|
||||
}else if( test_obj_eq_string(pRes, "REPLACE") ){
|
||||
ret = SQLITE_CHANGESET_REPLACE;
|
||||
}else if( test_obj_eq_string(pRes, "ABORT") ){
|
||||
ret = SQLITE_CHANGESET_ABORT;
|
||||
}else{
|
||||
Tcl_GetIntFromObj(0, pRes, &ret);
|
||||
}
|
||||
}
|
||||
|
||||
Tcl_DecrRefCount(pEval);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
** sqlite3changeset_apply DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT?
|
||||
*/
|
||||
static int test_sqlite3changeset_apply(
|
||||
void * clientData,
|
||||
Tcl_Interp *interp,
|
||||
int objc,
|
||||
Tcl_Obj *CONST objv[]
|
||||
){
|
||||
sqlite3 *db; /* Database handle */
|
||||
Tcl_CmdInfo info; /* Database Tcl command (objv[1]) info */
|
||||
int rc; /* Return code from changeset_invert() */
|
||||
void *pChangeset; /* Buffer containing changeset */
|
||||
int nChangeset; /* Size of buffer aChangeset in bytes */
|
||||
TestConflictHandler ctx;
|
||||
|
||||
if( objc!=4 && objc!=5 ){
|
||||
Tcl_WrongNumArgs(interp, 1, objv,
|
||||
"DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT?"
|
||||
);
|
||||
return TCL_ERROR;
|
||||
}
|
||||
if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(objv[1]), &info) ){
|
||||
Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(objv[2]), 0);
|
||||
return TCL_ERROR;
|
||||
}
|
||||
db = *(sqlite3 **)info.objClientData;
|
||||
pChangeset = (void *)Tcl_GetByteArrayFromObj(objv[2], &nChangeset);
|
||||
ctx.pConflictScript = objv[3];
|
||||
ctx.pFilterScript = objc==5 ? objv[4] : 0;
|
||||
ctx.interp = interp;
|
||||
|
||||
rc = sqlite3changeset_apply(db, nChangeset, pChangeset,
|
||||
(objc==5) ? test_filter_handler : 0, test_conflict_handler, (void *)&ctx
|
||||
);
|
||||
if( rc!=SQLITE_OK ){
|
||||
return test_session_error(interp, rc);
|
||||
}
|
||||
Tcl_ResetResult(interp);
|
||||
return TCL_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
** sqlite3changeset_invert CHANGESET
|
||||
*/
|
||||
static int test_sqlite3changeset_invert(
|
||||
void * clientData,
|
||||
Tcl_Interp *interp,
|
||||
int objc,
|
||||
Tcl_Obj *CONST objv[]
|
||||
){
|
||||
int rc; /* Return code from changeset_invert() */
|
||||
void *aChangeset; /* Input changeset */
|
||||
int nChangeSet; /* Size of buffer aChangeset in bytes */
|
||||
void *aOut; /* Output changeset */
|
||||
int nOut; /* Size of buffer aOut in bytes */
|
||||
|
||||
if( objc!=2 ){
|
||||
Tcl_WrongNumArgs(interp, 1, objv, "CHANGESET");
|
||||
return TCL_ERROR;
|
||||
}
|
||||
aChangeset = (void *)Tcl_GetByteArrayFromObj(objv[1], &nChangeSet);
|
||||
|
||||
rc = sqlite3changeset_invert(nChangeSet, aChangeset, &nOut, &aOut);
|
||||
if( rc!=SQLITE_OK ){
|
||||
return test_session_error(interp, rc);
|
||||
}
|
||||
Tcl_SetObjResult(interp, Tcl_NewByteArrayObj((unsigned char *)aOut, nOut));
|
||||
sqlite3_free(aOut);
|
||||
return TCL_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
** sqlite3changeset_concat LEFT RIGHT
|
||||
*/
|
||||
static int test_sqlite3changeset_concat(
|
||||
void * clientData,
|
||||
Tcl_Interp *interp,
|
||||
int objc,
|
||||
Tcl_Obj *CONST objv[]
|
||||
){
|
||||
int rc; /* Return code from changeset_invert() */
|
||||
void *aLeft; /* Input changeset */
|
||||
int nLeft; /* Size of buffer aChangeset in bytes */
|
||||
void *aRight; /* Input changeset */
|
||||
int nRight; /* Size of buffer aChangeset in bytes */
|
||||
void *aOut; /* Output changeset */
|
||||
int nOut; /* Size of buffer aOut in bytes */
|
||||
|
||||
if( objc!=3 ){
|
||||
Tcl_WrongNumArgs(interp, 1, objv, "LEFT RIGHT");
|
||||
return TCL_ERROR;
|
||||
}
|
||||
aLeft = (void *)Tcl_GetByteArrayFromObj(objv[1], &nLeft);
|
||||
aRight = (void *)Tcl_GetByteArrayFromObj(objv[2], &nRight);
|
||||
|
||||
rc = sqlite3changeset_concat(nLeft, aLeft, nRight, aRight, &nOut, &aOut);
|
||||
if( rc!=SQLITE_OK ){
|
||||
return test_session_error(interp, rc);
|
||||
}
|
||||
Tcl_SetObjResult(interp, Tcl_NewByteArrayObj((unsigned char *)aOut, nOut));
|
||||
sqlite3_free(aOut);
|
||||
return TCL_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
** sqlite3session_foreach VARNAME CHANGESET SCRIPT
|
||||
*/
|
||||
static int test_sqlite3session_foreach(
|
||||
void * clientData,
|
||||
Tcl_Interp *interp,
|
||||
int objc,
|
||||
Tcl_Obj *CONST objv[]
|
||||
){
|
||||
void *pChangeSet;
|
||||
int nChangeSet;
|
||||
sqlite3_changeset_iter *pIter;
|
||||
int rc;
|
||||
Tcl_Obj *pVarname;
|
||||
Tcl_Obj *pCS;
|
||||
Tcl_Obj *pScript;
|
||||
int isCheckNext = 0;
|
||||
|
||||
if( objc>1 ){
|
||||
char *zOpt = Tcl_GetString(objv[1]);
|
||||
isCheckNext = (strcmp(zOpt, "-next")==0);
|
||||
}
|
||||
if( objc!=4+isCheckNext ){
|
||||
Tcl_WrongNumArgs(interp, 1, objv, "?-next? VARNAME CHANGESET SCRIPT");
|
||||
return TCL_ERROR;
|
||||
}
|
||||
|
||||
pVarname = objv[1+isCheckNext];
|
||||
pCS = objv[2+isCheckNext];
|
||||
pScript = objv[3+isCheckNext];
|
||||
|
||||
pChangeSet = (void *)Tcl_GetByteArrayFromObj(pCS, &nChangeSet);
|
||||
rc = sqlite3changeset_start(&pIter, nChangeSet, pChangeSet);
|
||||
if( rc!=SQLITE_OK ){
|
||||
return test_session_error(interp, rc);
|
||||
}
|
||||
|
||||
while( SQLITE_ROW==sqlite3changeset_next(pIter) ){
|
||||
int nCol; /* Number of columns in table */
|
||||
int nCol2; /* Number of columns in table */
|
||||
int op; /* SQLITE_INSERT, UPDATE or DELETE */
|
||||
const char *zTab; /* Name of table change applies to */
|
||||
Tcl_Obj *pVar; /* Tcl value to set $VARNAME to */
|
||||
Tcl_Obj *pOld; /* Vector of old.* values */
|
||||
Tcl_Obj *pNew; /* Vector of new.* values */
|
||||
int bIndirect;
|
||||
|
||||
char *zPK;
|
||||
unsigned char *abPK;
|
||||
int i;
|
||||
|
||||
sqlite3changeset_op(pIter, &zTab, &nCol, &op, &bIndirect);
|
||||
pVar = Tcl_NewObj();
|
||||
Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj(
|
||||
op==SQLITE_INSERT ? "INSERT" :
|
||||
op==SQLITE_UPDATE ? "UPDATE" :
|
||||
"DELETE", -1
|
||||
));
|
||||
|
||||
Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj(zTab, -1));
|
||||
Tcl_ListObjAppendElement(0, pVar, Tcl_NewBooleanObj(bIndirect));
|
||||
|
||||
zPK = ckalloc(nCol+1);
|
||||
memset(zPK, 0, nCol+1);
|
||||
sqlite3changeset_pk(pIter, &abPK, &nCol2);
|
||||
assert( nCol==nCol2 );
|
||||
for(i=0; i<nCol; i++){
|
||||
zPK[i] = (abPK[i] ? 'X' : '.');
|
||||
}
|
||||
Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj(zPK, -1));
|
||||
ckfree(zPK);
|
||||
|
||||
pOld = Tcl_NewObj();
|
||||
if( op!=SQLITE_INSERT ){
|
||||
int i;
|
||||
for(i=0; i<nCol; i++){
|
||||
sqlite3_value *pVal;
|
||||
sqlite3changeset_old(pIter, i, &pVal);
|
||||
test_append_value(pOld, pVal);
|
||||
}
|
||||
}
|
||||
pNew = Tcl_NewObj();
|
||||
if( op!=SQLITE_DELETE ){
|
||||
int i;
|
||||
for(i=0; i<nCol; i++){
|
||||
sqlite3_value *pVal;
|
||||
sqlite3changeset_new(pIter, i, &pVal);
|
||||
test_append_value(pNew, pVal);
|
||||
}
|
||||
}
|
||||
Tcl_ListObjAppendElement(0, pVar, pOld);
|
||||
Tcl_ListObjAppendElement(0, pVar, pNew);
|
||||
|
||||
Tcl_ObjSetVar2(interp, pVarname, 0, pVar, 0);
|
||||
rc = Tcl_EvalObjEx(interp, pScript, 0);
|
||||
if( rc!=TCL_OK && rc!=TCL_CONTINUE ){
|
||||
sqlite3changeset_finalize(pIter);
|
||||
return rc==TCL_BREAK ? TCL_OK : rc;
|
||||
}
|
||||
}
|
||||
|
||||
if( isCheckNext ){
|
||||
int rc2 = sqlite3changeset_next(pIter);
|
||||
rc = sqlite3changeset_finalize(pIter);
|
||||
assert( (rc2==SQLITE_DONE && rc==SQLITE_OK) || rc2==rc );
|
||||
}else{
|
||||
rc = sqlite3changeset_finalize(pIter);
|
||||
}
|
||||
if( rc!=SQLITE_OK ){
|
||||
return test_session_error(interp, rc);
|
||||
}
|
||||
|
||||
return TCL_OK;
|
||||
}
|
||||
|
||||
int TestSession_Init(Tcl_Interp *interp){
|
||||
Tcl_CreateObjCommand(interp, "sqlite3session", test_sqlite3session, 0, 0);
|
||||
Tcl_CreateObjCommand(
|
||||
interp, "sqlite3session_foreach", test_sqlite3session_foreach, 0, 0
|
||||
);
|
||||
Tcl_CreateObjCommand(
|
||||
interp, "sqlite3changeset_invert", test_sqlite3changeset_invert, 0, 0
|
||||
);
|
||||
Tcl_CreateObjCommand(
|
||||
interp, "sqlite3changeset_concat", test_sqlite3changeset_concat, 0, 0
|
||||
);
|
||||
Tcl_CreateObjCommand(
|
||||
interp, "sqlite3changeset_apply", test_sqlite3changeset_apply, 0, 0
|
||||
);
|
||||
return TCL_OK;
|
||||
}
|
||||
|
||||
#endif /* SQLITE_TEST && SQLITE_SESSION && SQLITE_PREUPDATE_HOOK */
|
||||
@@ -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.
|
||||
#
|
||||
@@ -304,7 +305,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.
|
||||
#
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
C Version\s3.7.14
|
||||
D 2012-09-03T15:42:36.217
|
||||
C Merge\sthe\s3.7.14.1\srelease\sincluding\sthe\sfix\sfor\sthe\sLEFT\sJOIN\sproblem\sinto\nthe\ssessions\sbranch.
|
||||
D 2012-10-04T22:45:58.218
|
||||
F Makefile.arm-wince-mingw32ce-gcc d6df77f1f48d690bd73162294bbba7f59507c72f
|
||||
F Makefile.in abd5c10d21d1395f140d9e50ea999df8fa4d6376
|
||||
F Makefile.linux-gcc 91d710bdc4998cb015f39edf3cb314ec4f4d7e23
|
||||
F Makefile.msc 2d696f01c228995e98b3b953a08b7bba1d48c130
|
||||
F Makefile.msc 2f3680c29a53135c2b45a5c9c5c5ee860ed1a34c
|
||||
F Makefile.vxworks 879f034a64062a364b21000266bbd5bc6e0c19b9
|
||||
F README cd04a36fbc7ea56932a4052d7d0b7f09f27c33d6
|
||||
F VERSION a71848df48082f1d6585d4b0819d530fc455485d
|
||||
F VERSION 9694dff497fc7e53037a5890f30ab069c26e8ab7
|
||||
F aclocal.m4 a5c22d164aff7ed549d53a90fa56d56955281f50
|
||||
F addopcodes.awk 17dc593f791f874d2c23a0f9360850ded0286531
|
||||
F art/sqlite370.eps aa97a671332b432a54e1d74ff5e8775be34200c2
|
||||
@@ -15,7 +15,7 @@ F art/sqlite370.jpg d512473dae7e378a67e28ff96a34da7cb331def2
|
||||
F config.guess 226d9a188c6196f3033ffc651cbc9dcee1a42977
|
||||
F config.h.in 0921066a13130082764ab4ab6456f7b5bebe56de
|
||||
F config.sub 9ebe4c3b3dab6431ece34f16828b594fb420da55
|
||||
F configure e2d0e3b67d2b1b1049d389fd671275d79bb80457 x
|
||||
F configure e2c3b28dec7f5d1f72dd7465c87bb37f6af1931f x
|
||||
F configure.ac 6e909664785b8184db2179013cd9d574f96ca3a3
|
||||
F contrib/sqlitecon.tcl 210a913ad63f9f991070821e599d600bd913e0ad
|
||||
F doc/lemon.html 334dbf6621b8fb8790297ec1abf3cfa4621709d1
|
||||
@@ -101,9 +101,21 @@ F ext/rtree/rtree_util.tcl 06aab2ed5b826545bf215fff90ecb9255a8647ea
|
||||
F ext/rtree/sqlite3rtree.h c34c1e41d1ab80bb8ad09aae402c9c956871a765
|
||||
F ext/rtree/tkt3363.test 142ab96eded44a3615ec79fba98c7bde7d0f96de
|
||||
F ext/rtree/viewrtree.tcl eea6224b3553599ae665b239bd827e182b466024
|
||||
F ext/session/session1.test 502086908e4144dfaccb1baa77bc29d75a9daace
|
||||
F ext/session/session2.test 99ca0da7ddb617d42bafd83adccf99f18ae0384b
|
||||
F ext/session/session3.test a7a9ce59b8d1e49e2cc23d81421ac485be0eea01
|
||||
F ext/session/session4.test a6ed685da7a5293c5d6f99855bcf41dbc352ca84
|
||||
F ext/session/session5.test 8fdfaf9dba28a2f1c6b89b06168bdab1fef2d478
|
||||
F ext/session/session6.test 443789bc2fca12e4f7075cf692c60b8a2bea1a26
|
||||
F ext/session/session8.test 7d35947ad329b8966f095d34f9617a9eff52dc65
|
||||
F ext/session/session_common.tcl 1539d8973b2aea0025c133eb0cc4c89fcef541a5
|
||||
F ext/session/sessionfault.test 891453e96630ee2915081487fc4b7226f0aa252f
|
||||
F ext/session/sqlite3session.c 57d04e1d6a3579e673e61dea29f214fb4e0fc505
|
||||
F ext/session/sqlite3session.h f374c9c4c96e08f67ac418871c29d423245c7673
|
||||
F ext/session/test_session.c ea4dc9b4a1895c8e6bddcbfe3838d7eb57df2d99
|
||||
F install-sh 9d4de14ab9fb0facae2f48780b874848cbf2f895 x
|
||||
F ltmain.sh 3ff0879076df340d2e23ae905484d8c15d5fdea8
|
||||
F main.mk 72026405046ed5b1f0368943b89c0aa29ad558b6
|
||||
F main.mk 1e1c4a0f975cc43e3e4221249367a40c076b34c0
|
||||
F mkdll.sh 7d09b23c05d56532e9d44a50868eb4b12ff4f74a
|
||||
F mkextu.sh 416f9b7089d80e5590a29692c9d9280a10dbad9f
|
||||
F mkextw.sh 4123480947681d9b434a5e7b1ee08135abe409ac
|
||||
@@ -123,7 +135,7 @@ F src/auth.c 523da7fb4979469955d822ff9298352d6b31de34
|
||||
F src/backup.c 5b31b24d6814b11de763debf342c8cd0a15a4910
|
||||
F src/bitvec.c 26675fe8e431dc555e6f2d0e11e651d172234aa1
|
||||
F src/btmutex.c 976f45a12e37293e32cae0281b15a21d48a8aaa7
|
||||
F src/btree.c 9cf6de113d23d47967df24b8d8ce6501c879d7e6
|
||||
F src/btree.c 97edf88abd2b66f31886ba977d2b3b2a21d81d4c
|
||||
F src/btree.h 4aee02e879211bfcfd3f551769578d2e940ab6c2
|
||||
F src/btreeInt.h 4e5c2bd0f9b36b2a815a6d84f771a61a65830621
|
||||
F src/build.c a3b700afd475e6387da59be6f2e86161e80d6d87
|
||||
@@ -131,7 +143,7 @@ F src/callback.c 0cb4228cdcd827dcc5def98fb099edcc9142dbcd
|
||||
F src/complete.c dc1d136c0feee03c2f7550bafc0d29075e36deac
|
||||
F src/ctime.c 500d019da966631ad957c37705642be87524463b
|
||||
F src/date.c 067a81c9942c497aafd2c260e13add8a7d0c7dd4
|
||||
F src/delete.c 335f36750dc6ac88d580aa36a6487459be9889de
|
||||
F src/delete.c 38f7517f76c61b971a51c91cc343e77897ef6a12
|
||||
F src/expr.c 217840a107dcc1e5dbb57cea311daad04bedbb9a
|
||||
F src/fault.c 160a0c015b6c2629d3899ed2daf63d75754a32bb
|
||||
F src/fkey.c 9c77d842dc9961d92a06a65abb80c64ef1750296
|
||||
@@ -140,12 +152,12 @@ F src/global.c 4cfdca5cb0edd33c4d021baec4ede958cb2c793b
|
||||
F src/hash.c a4031441741932da9e7a65bee2b36b5d0e81c073
|
||||
F src/hash.h 2894c932d84d9f892d4b4023a75e501f83050970
|
||||
F src/hwtime.h d32741c8f4df852c7d959236615444e2b1063b08
|
||||
F src/insert.c b090d0a9fb9ff2dbdeaf66aedccf98cd13b1af60
|
||||
F src/insert.c 8c5a3aba0e97c39679de3b33f95e7795d336fc87
|
||||
F src/journal.c 552839e54d1bf76fb8f7abe51868b66acacf6a0e
|
||||
F src/legacy.c a199d7683d60cef73089e892409113e69c23a99f
|
||||
F src/lempar.c 0ee69fca0be54cd93939df98d2aca4ca46f44416
|
||||
F src/loadext.c f20382fbaeec832438a1ba7797bee3d3c8a6d51d
|
||||
F src/main.c 02255cf1da50956c5427c469abddb15bccc4ba09
|
||||
F src/main.c 1f15146d32b183d45fd0d41e8370a326c5d647ac
|
||||
F src/malloc.c fe085aa851b666b7c375c1ff957643dc20a04bf6
|
||||
F src/mem0.c 6a55ebe57c46ca1a7d98da93aaa07f99f1059645
|
||||
F src/mem1.c 437c7c4af964895d4650f29881df63535caaa1fa
|
||||
@@ -178,14 +190,14 @@ F src/resolve.c 9e28280ec98035f31900fdd1db01f86f68ca6c32
|
||||
F src/rowset.c 64655f1a627c9c212d9ab497899e7424a34222e0
|
||||
F src/select.c f843c872a97baa1594c2cc3d4c003409a7bd03af
|
||||
F src/shell.c 87953c5d9c73d9494db97d1607e2e2280418f261
|
||||
F src/sqlite.h.in c447d35212736c4c77d86bc2d00f6cf4d4c12131
|
||||
F src/sqlite.h.in 930b719ce295058817aa58e7b65c43dc668cc098
|
||||
F src/sqlite3.rc fea433eb0a59f4c9393c8e6d76a6e2596b1fe0c0
|
||||
F src/sqlite3ext.h 6904f4aadf976f95241311fbffb00823075d9477
|
||||
F src/sqliteInt.h 053e03a532beb909ead2df0721db67cdb4c48ae8
|
||||
F src/sqliteInt.h 569cb02b6555db22dbe38566ed904f40f0ae5eb8
|
||||
F src/sqliteLimit.h 164b0e6749d31e0daa1a4589a169d31c0dec7b3d
|
||||
F src/status.c 35939e7e03abf1b7577ce311f48f682c40de3208
|
||||
F src/table.c 2cd62736f845d82200acfa1287e33feb3c15d62e
|
||||
F src/tclsqlite.c fe5406573e1527957e00dcaf51edd9d8bd31b918
|
||||
F src/tclsqlite.c 55b8c6fad0cac37a3472d7db1272ad1c9b4a244a
|
||||
F src/test1.c 3d70f7c5987f186884cfebbfa7151a7d3d67d86e
|
||||
F src/test2.c 4178056dd1e7d70f954ad8a1e3edb71a2a784daf
|
||||
F src/test3.c 3c3c2407fa6ec7a19e24ae23f7cb439d0275a60d
|
||||
@@ -199,7 +211,7 @@ F src/test_async.c 0612a752896fad42d55c3999a5122af10dcf22ad
|
||||
F src/test_autoext.c 30e7bd98ab6d70a62bb9ba572e4c7df347fe645e
|
||||
F src/test_backup.c c129c91127e9b46e335715ae2e75756e25ba27de
|
||||
F src/test_btree.c 5b89601dcb42a33ba8b820a6b763cc9cb48bac16
|
||||
F src/test_config.c 09781397ccc24268cb895be0d4c21b4aad651486
|
||||
F src/test_config.c 1ffddfdfa5a73e7fb497e8c89c22754bbc949c81
|
||||
F src/test_demovfs.c 20a4975127993f4959890016ae9ce5535a880094
|
||||
F src/test_devsym.c e7498904e72ba7491d142d5c83b476c4e76993bc
|
||||
F src/test_func.c 3a8dd37c08ab43b76d38eea2836e34a3897bf170
|
||||
@@ -234,16 +246,16 @@ F src/test_wholenumber.c 3d2b9ed1505c40ad5c5ca2ad16ae7a289d6cc251
|
||||
F src/test_wsd.c 41cadfd9d97fe8e3e4e44f61a4a8ccd6f7ca8fe9
|
||||
F src/tokenize.c 1e86210d3976717a19238ea7b047fac481fe8c12
|
||||
F src/trigger.c 3f258307040173aff383eb23fb74c44fe829078c
|
||||
F src/update.c 28d2d098b43a2c70dae399896ea8a02f622410ef
|
||||
F src/update.c abb0fcabe551dae0a133fd2a4370b5a8c23b1831
|
||||
F src/utf.c 890c67dcfcc7a74623c95baac7535aadfe265e84
|
||||
F src/util.c 0af2e515dc0dabacec931bca39525f6c3f1c5455
|
||||
F src/vacuum.c 587a52bb8833d7ac15af8916f25437e2575028bd
|
||||
F src/vdbe.c 9c524bded348fd0a53adc19f2d7cad76ba3442b2
|
||||
F src/vdbe.h 18f581cac1f4339ec3299f3e0cc6e11aec654cdb
|
||||
F src/vdbeInt.h 986b6b11a13c517337355009e5438703ba5b0a40
|
||||
F src/vdbeapi.c 88ea823bbcb4320f5a6607f39cd7c2d3cc4c26b1
|
||||
F src/vdbeaux.c 9c293fd3040211687e83d5d27bef2382933146c2
|
||||
F src/vdbeblob.c 32f2a4899d67f69634ea4dd93e3f651936d732cb
|
||||
F src/vdbe.c b01b505ced376ee6a9310300c9cb1a0980731f94
|
||||
F src/vdbe.h 87b8ff40de3f55dbcdc33029416862f517c37a2f
|
||||
F src/vdbeInt.h 3f5d994703b6d61032d48f3086329c960b1697e9
|
||||
F src/vdbeapi.c 7d23764ea26c1fff2959e6a0984dd96de0fa8f75
|
||||
F src/vdbeaux.c 797201e170a8bcc746d62aa82e2a280419b3839b
|
||||
F src/vdbeblob.c 11248c6362389569764682eb0f59ce910f3cc381
|
||||
F src/vdbemem.c cb55e84b8e2c15704968ee05f0fae25883299b74
|
||||
F src/vdbesort.c 0dc1b274dcb4d4c8e71b0b2b15261f286caba39b
|
||||
F src/vdbetrace.c 8bd5da325fc90f28464335e4cc4ad1407fe30835
|
||||
@@ -251,7 +263,7 @@ F src/vtab.c d2c54fd22aa83eb34fc6f7cd9b097f2fc2b1e9de
|
||||
F src/wal.c 5acb3e7bbd31f10ba39acad9ce6b399055337a9d
|
||||
F src/wal.h 29c197540b19044e6cd73487017e5e47a1d3dac6
|
||||
F src/walker.c 3d75ba73de15e0f8cd0737643badbeb0e002f07b
|
||||
F src/where.c 22783f4275f6fc09b663115a6091837cb5c510e0
|
||||
F src/where.c e74f9ed463b0153e7f2a0e5f291c2be16ae76b35
|
||||
F test/8_3_names.test 631ea964a3edb091cf73c3b540f6bcfdb36ce823
|
||||
F test/aggerror.test a867e273ef9e3d7919f03ef4f0e8c0d2767944f2
|
||||
F test/aggnested.test 0be144b453e0622a085fae8665c32f5676708e00
|
||||
@@ -514,7 +526,7 @@ F test/fuzz_common.tcl a87dfbb88c2a6b08a38e9a070dabd129e617b45b
|
||||
F test/fuzz_malloc.test 328f70aaca63adf29b4c6f06505ed0cf57ca7c26
|
||||
F test/fuzzer1.test 69cf1036b92fd3b8e1fd65bef4d7ee3f085c28fb
|
||||
F test/fuzzerfault.test ff2282c81797b6a355f0748d8b54c7287c5d2b25
|
||||
F test/hook.test 5f3749de6462a6b87b4209b74adf7df5ac2df639
|
||||
F test/hook.test 94b927b15883f5c1477ab09eecd16275addb08f4
|
||||
F test/icu.test 70df4faca133254c042d02ae342c0a141f2663f4
|
||||
F test/in.test 5941096407d8c133b9eff15bd3e666624b6cbde3
|
||||
F test/in2.test 5d4c61d17493c832f7d2d32bef785119e87bde75
|
||||
@@ -644,7 +656,7 @@ F test/pageropt.test 9191867ed19a2b3db6c42d1b36b6fbc657cd1ab0
|
||||
F test/pagesize.test 1dd51367e752e742f58e861e65ed7390603827a0
|
||||
F test/pcache.test 065aa286e722ab24f2e51792c1f093bf60656b16
|
||||
F test/pcache2.test a83efe2dec0d392f814bfc998def1d1833942025
|
||||
F test/permutations.test 1a8ac849b659445a0b3883caf42fa2c2a289f4a1
|
||||
F test/permutations.test 02dd9f7fd2fc874d25572b9ae431181bd69a174f
|
||||
F test/pragma.test a62f73293b0f0d79b0c87f8dd32d46fe53b0bd17
|
||||
F test/pragma2.test 3a55f82b954242c642f8342b17dffc8b47472947
|
||||
F test/printf.test ec9870c4dce8686a37818e0bf1aba6e6a1863552
|
||||
@@ -692,6 +704,7 @@ F test/selectA.test 06d1032fa9009314c95394f2ca2e60d9f7ae8532
|
||||
F test/selectB.test 954e4e49cf1f896d61794e440669e03a27ceea25
|
||||
F test/selectC.test 871fb55d884d3de5943c4057ebd22c2459e71977
|
||||
F test/server1.test 46803bd3fe8b99b30dbc5ff38ffc756f5c13a118
|
||||
F test/session.test c1a17c11ef7d01c24fe2b9f7871190d949a8e718
|
||||
F test/shared.test 1da9dbad400cee0d93f252ccf76e1ae007a63746
|
||||
F test/shared2.test 03eb4a8d372e290107d34b6ce1809919a698e879
|
||||
F test/shared3.test ebf77f023f4bdaa8f74f65822b559e86ce5c6257
|
||||
@@ -734,11 +747,11 @@ F test/syscall.test bea9bf329bff733c791310244617c2a76974e64a
|
||||
F test/sysfault.test c79441d88d23696fbec7b147dba98d42a04f523f
|
||||
F test/table.test a59d985ca366e39b17b175f387f9d5db5a18d4e2
|
||||
F test/tableapi.test 2674633fa95d80da917571ebdd759a14d9819126
|
||||
F test/tclsqlite.test 1597d353308531527583481d14d9da52ea8ed0af
|
||||
F test/tclsqlite.test 2052ca66640b14d33b8e17f3a24b946cddfb0ec2
|
||||
F test/tempdb.test 19d0f66e2e3eeffd68661a11c83ba5e6ace9128c
|
||||
F test/temptable.test 51edd31c65ed1560dd600b1796e8325df96318e2
|
||||
F test/temptrigger.test 26670ed7a39cf2296a7f0a9e0a1d7bdb7abe936d
|
||||
F test/tester.tcl 2665f64c9ce71944b4d41269114e658fb81bda05
|
||||
F test/tester.tcl 184ef244e01486ba1625f71f501b07dd483f5a54
|
||||
F test/thread001.test 7cc2ce08f9cde95964736d11e91f9ab610f82f91
|
||||
F test/thread002.test e630504f8a06c00bf8bbe68528774dd96aeb2e58
|
||||
F test/thread003.test ee4c9efc3b86a6a2767516a37bd64251272560a7
|
||||
@@ -966,7 +979,7 @@ F test/where9.test ae98dc22ef9b6f2bc81e9f164e41b38faa9bda06
|
||||
F test/whereA.test 24c234263c8fe358f079d5e57d884fb569d2da0a
|
||||
F test/whereB.test 0def95db3bdec220a731c7e4bec5930327c1d8c5
|
||||
F test/whereC.test 13ff5ec0dba407c0e0c075980c75b3275a6774e5
|
||||
F test/whereD.test 304ccbe3c77e0d0764f37c91d43b8c4792a5e02f
|
||||
F test/whereD.test 11945e79899a97958d87d1e5ac6ae5abd827356b
|
||||
F test/wherelimit.test 5e9fd41e79bb2b2d588ed999d641d9c965619b31
|
||||
F test/win32lock.test b2a539e85ae6b2d78475e016a9636b4451dc7fb9
|
||||
F test/zeroblob.test caaecfb4f908f7bc086ed238668049f96774d688
|
||||
@@ -1014,10 +1027,7 @@ F tool/vdbe-compress.tcl d70ea6d8a19e3571d7ab8c9b75cba86d1173ff0f
|
||||
F tool/warnings-clang.sh f6aa929dc20ef1f856af04a730772f59283631d4
|
||||
F tool/warnings.sh fbc018d67fd7395f440c28f33ef0f94420226381
|
||||
F tool/win/sqlite.vsix 67d8a99aceb56384a81b3f30d6c71743146d2cc9
|
||||
P ebb08d0b4d1ed674e4a23c3754206ca2df9063ff
|
||||
R 8bdf9ef517616ac5a5043f12bec4acdb
|
||||
T +bgcolor * #d0c0ff
|
||||
T +sym-release *
|
||||
T +sym-version-3.7.14 *
|
||||
P 2ce625fdbee78b2acbe3aa19b01ff92241c0d598 091570e46d04e84b67228e0bdbcd6e1fb60c6bdb
|
||||
R 05bcb07e9ac288a81b251a2d7eae2e18
|
||||
U drh
|
||||
Z 126ab1aaea0f3a8cb5c4792cf0a82989
|
||||
Z 261201057227005a81c4f41c6b14168e
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
c0d89d4a9752922f9e367362366efde4f1b06f2a
|
||||
17644c0e33f3d9e144b873806e9530cc2db274ee
|
||||
@@ -5922,6 +5922,9 @@ static void copyNodeContent(MemPage *pFrom, MemPage *pTo, int *pRC){
|
||||
** If aOvflSpace is set to a null pointer, this function returns
|
||||
** SQLITE_NOMEM.
|
||||
*/
|
||||
#if defined(_MSC_VER) && _MSC_VER >= 1700 && defined(_M_ARM)
|
||||
#pragma optimize("", off)
|
||||
#endif
|
||||
static int balance_nonroot(
|
||||
MemPage *pParent, /* Parent page of siblings being balanced */
|
||||
int iParentIdx, /* Index of "the page" in pParent */
|
||||
@@ -6552,6 +6555,9 @@ balance_cleanup:
|
||||
|
||||
return rc;
|
||||
}
|
||||
#if defined(_MSC_VER) && _MSC_VER >= 1700 && defined(_M_ARM)
|
||||
#pragma optimize("", on)
|
||||
#endif
|
||||
|
||||
|
||||
/*
|
||||
|
||||
+19
-7
@@ -346,8 +346,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 );
|
||||
@@ -401,7 +408,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);
|
||||
sqlite3VdbeChangeP5(v, OE_Abort);
|
||||
sqlite3MayAbort(pParse);
|
||||
}else
|
||||
@@ -542,13 +549,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_TRANSIENT);
|
||||
}
|
||||
sqlite3VdbeChangeP4(v, -1, (char*)pTab, P4_TABLE);
|
||||
}
|
||||
|
||||
/* Do any ON CASCADE, SET NULL or SET DEFAULT operations required to
|
||||
|
||||
+15
-5
@@ -1300,9 +1300,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;
|
||||
@@ -1481,7 +1491,7 @@ void sqlite3CompleteInsertion(
|
||||
}
|
||||
sqlite3VdbeAddOp3(v, OP_Insert, baseCur, regRec, regRowid);
|
||||
if( !pParse->nested ){
|
||||
sqlite3VdbeChangeP4(v, -1, pTab->zName, P4_TRANSIENT);
|
||||
sqlite3VdbeChangeP4(v, -1, (char *)pTab, P4_TABLE);
|
||||
}
|
||||
sqlite3VdbeChangeP5(v, pik_flags);
|
||||
}
|
||||
@@ -1813,7 +1823,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){
|
||||
|
||||
+21
@@ -1469,6 +1469,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().
|
||||
|
||||
+98
-3
@@ -4694,8 +4694,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*,
|
||||
@@ -6091,7 +6091,7 @@ int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int resetFlg);
|
||||
** database file in rollback mode databases. Any pages written as part of
|
||||
** transaction rollback or database recovery operations are not included.
|
||||
** If an IO or other error occurs while writing a page to disk, the effect
|
||||
** on subsequent SQLITE_DBSTATUS_CACHE_WRITE requests is undefined.)^ ^The
|
||||
** on subsequent SQLITE_DBSTATUS_CACHE_WRITE requests is undefined). ^The
|
||||
** highwater mark associated with SQLITE_DBSTATUS_CACHE_WRITE is always 0.
|
||||
** </dd>
|
||||
** </dl>
|
||||
@@ -7017,6 +7017,101 @@ int sqlite3_vtab_on_conflict(sqlite3 *);
|
||||
|
||||
|
||||
|
||||
/*
|
||||
** 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.
|
||||
|
||||
@@ -658,6 +658,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;
|
||||
@@ -860,6 +861,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;
|
||||
@@ -2310,6 +2318,9 @@ struct AuthContext {
|
||||
|
||||
/*
|
||||
** Bitfield flags for P5 value in various opcodes.
|
||||
**
|
||||
** Note that the values for ISNOOP and LENGTHARG are the same. But as
|
||||
** those bits are never used on the same opcode, the overlap is harmless.
|
||||
*/
|
||||
#define OPFLAG_NCHANGE 0x01 /* Set to update db->nChange */
|
||||
#define OPFLAG_LASTROWID 0x02 /* Set to update db->lastRowid */
|
||||
@@ -2317,6 +2328,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 */
|
||||
#define OPFLAG_LENGTHARG 0x40 /* OP_Column only used for length() */
|
||||
#define OPFLAG_TYPEOFARG 0x80 /* OP_Column only used for typeof() */
|
||||
#define OPFLAG_BULKCSR 0x01 /* OP_Open** used to open bulk cursor */
|
||||
|
||||
+206
-80
@@ -53,15 +53,8 @@
|
||||
#define NUM_PREPARED_STMTS 10
|
||||
#define MAX_PREPARED_STMTS 100
|
||||
|
||||
/*
|
||||
** If TCL uses UTF-8 and SQLite is configured to use iso8859, then we
|
||||
** have to do a translation when going between the two. Set the
|
||||
** UTF_TRANSLATION_NEEDED macro to indicate that we need to do
|
||||
** this translation.
|
||||
*/
|
||||
#if defined(TCL_UTF_MAX) && !defined(SQLITE_UTF8)
|
||||
# define UTF_TRANSLATION_NEEDED 1
|
||||
#endif
|
||||
/* Forward declaration */
|
||||
typedef struct SqliteDb SqliteDb;
|
||||
|
||||
/*
|
||||
** New SQL functions can be created as TCL scripts. Each such function
|
||||
@@ -71,6 +64,7 @@ typedef struct SqlFunc SqlFunc;
|
||||
struct SqlFunc {
|
||||
Tcl_Interp *interp; /* The TCL interpret to execute the function */
|
||||
Tcl_Obj *pScript; /* The Tcl_Obj representation of the script */
|
||||
SqliteDb *pDb; /* Database connection that owns this function */
|
||||
int useEvalObjv; /* True if it is safe to use Tcl_EvalObjv */
|
||||
char *zName; /* Name of this function */
|
||||
SqlFunc *pNext; /* Next function on the list of them all */
|
||||
@@ -113,7 +107,6 @@ typedef struct IncrblobChannel IncrblobChannel;
|
||||
** sqlite3_prepare_v2() or sqlite3_prepare() to prepare SQL statements.
|
||||
** If SqliteDb.bLegacyPrepare is true, sqlite3_prepare() is used.
|
||||
*/
|
||||
typedef struct SqliteDb SqliteDb;
|
||||
struct SqliteDb {
|
||||
sqlite3 *db; /* The "real" database structure. MUST BE FIRST */
|
||||
Tcl_Interp *interp; /* The interpreter used for this database */
|
||||
@@ -127,6 +120,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) */
|
||||
@@ -431,6 +425,7 @@ static SqlFunc *findSqlFunc(SqliteDb *pDb, const char *zName){
|
||||
}
|
||||
}
|
||||
pNew->interp = pDb->interp;
|
||||
pNew->pDb = pDb;
|
||||
pNew->pScript = 0;
|
||||
pNew->pNext = pDb->pFunc;
|
||||
pDb->pFunc = pNew;
|
||||
@@ -478,6 +473,7 @@ static void DbDeleteCmd(void *db){
|
||||
while( pDb->pFunc ){
|
||||
SqlFunc *pFunc = pDb->pFunc;
|
||||
pDb->pFunc = pFunc->pNext;
|
||||
assert( pFunc->pDb==pDb );
|
||||
Tcl_DecrRefCount(pFunc->pScript);
|
||||
Tcl_Free((char*)pFunc);
|
||||
}
|
||||
@@ -504,6 +500,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);
|
||||
}
|
||||
@@ -670,6 +669,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,
|
||||
@@ -679,14 +714,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));
|
||||
@@ -794,7 +833,7 @@ static void tclSqlFunc(sqlite3_context *context, int argc, sqlite3_value**argv){
|
||||
break;
|
||||
}
|
||||
case SQLITE_NULL: {
|
||||
pVal = Tcl_NewStringObj("", 0);
|
||||
pVal = Tcl_NewStringObj(p->pDb->zNull, -1);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -933,26 +972,6 @@ static int auth_callback(
|
||||
}
|
||||
#endif /* SQLITE_OMIT_AUTHORIZATION */
|
||||
|
||||
/*
|
||||
** zText is a pointer to text obtained via an sqlite3_result_text()
|
||||
** or similar interface. This routine returns a Tcl string object,
|
||||
** reference count set to 0, containing the text. If a translation
|
||||
** between iso8859 and UTF-8 is required, it is preformed.
|
||||
*/
|
||||
static Tcl_Obj *dbTextToObj(char const *zText){
|
||||
Tcl_Obj *pVal;
|
||||
#ifdef UTF_TRANSLATION_NEEDED
|
||||
Tcl_DString dCol;
|
||||
Tcl_DStringInit(&dCol);
|
||||
Tcl_ExternalToUtfDString(NULL, zText, -1, &dCol);
|
||||
pVal = Tcl_NewStringObj(Tcl_DStringValue(&dCol), -1);
|
||||
Tcl_DStringFree(&dCol);
|
||||
#else
|
||||
pVal = Tcl_NewStringObj(zText, -1);
|
||||
#endif
|
||||
return pVal;
|
||||
}
|
||||
|
||||
/*
|
||||
** This routine reads a line of text from FILE in, stores
|
||||
** the text in memory obtained from malloc() and returns a pointer
|
||||
@@ -1140,13 +1159,13 @@ static int dbPrepareAndBind(
|
||||
int nByte;
|
||||
|
||||
if( SQLITE_OK!=dbPrepare(pDb, zSql, &pStmt, pzOut) ){
|
||||
Tcl_SetObjResult(interp, dbTextToObj(sqlite3_errmsg(pDb->db)));
|
||||
Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3_errmsg(pDb->db), -1));
|
||||
return TCL_ERROR;
|
||||
}
|
||||
if( pStmt==0 ){
|
||||
if( SQLITE_OK!=sqlite3_errcode(pDb->db) ){
|
||||
/* A compile-time error in the statement. */
|
||||
Tcl_SetObjResult(interp, dbTextToObj(sqlite3_errmsg(pDb->db)));
|
||||
Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3_errmsg(pDb->db), -1));
|
||||
return TCL_ERROR;
|
||||
}else{
|
||||
/* The statement was a no-op. Continue to the next statement
|
||||
@@ -1365,7 +1384,7 @@ static void dbEvalRowInfo(
|
||||
if( nCol>0 && (papColName || p->pArray) ){
|
||||
apColName = (Tcl_Obj**)Tcl_Alloc( sizeof(Tcl_Obj*)*nCol );
|
||||
for(i=0; i<nCol; i++){
|
||||
apColName[i] = dbTextToObj(sqlite3_column_name(pStmt,i));
|
||||
apColName[i] = Tcl_NewStringObj(sqlite3_column_name(pStmt,i), -1);
|
||||
Tcl_IncrRefCount(apColName[i]);
|
||||
}
|
||||
p->apColName = apColName;
|
||||
@@ -1452,7 +1471,8 @@ static int dbEvalStep(DbEvalContext *p){
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
Tcl_SetObjResult(pDb->interp, dbTextToObj(sqlite3_errmsg(pDb->db)));
|
||||
Tcl_SetObjResult(pDb->interp,
|
||||
Tcl_NewStringObj(sqlite3_errmsg(pDb->db), -1));
|
||||
return TCL_ERROR;
|
||||
}else{
|
||||
dbReleaseStmt(pDb, pPreStmt, 0);
|
||||
@@ -1509,11 +1529,11 @@ static Tcl_Obj *dbEvalColumnValue(DbEvalContext *p, int iCol){
|
||||
return Tcl_NewDoubleObj(sqlite3_column_double(pStmt, iCol));
|
||||
}
|
||||
case SQLITE_NULL: {
|
||||
return dbTextToObj(p->pDb->zNull);
|
||||
return Tcl_NewStringObj(p->pDb->zNull, -1);
|
||||
}
|
||||
}
|
||||
|
||||
return dbTextToObj((char *)sqlite3_column_text(pStmt, iCol));
|
||||
return Tcl_NewStringObj(sqlite3_column_text(pStmt, iCol), -1);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1609,6 +1629,46 @@ static int DbEvalNextCmd(
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
** 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
|
||||
@@ -1634,11 +1694,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,
|
||||
@@ -1648,11 +1709,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 */
|
||||
|
||||
@@ -2433,7 +2494,7 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){
|
||||
pDb->zNull = 0;
|
||||
}
|
||||
}
|
||||
Tcl_SetObjResult(interp, dbTextToObj(pDb->zNull));
|
||||
Tcl_SetObjResult(interp, Tcl_NewStringObj(pDb->zNull, -1));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2825,6 +2886,90 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
** $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?
|
||||
@@ -2833,42 +2978,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;
|
||||
}
|
||||
|
||||
@@ -3703,7 +3825,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
|
||||
#if defined(SQLITE_ENABLE_FTS3) || defined(SQLITE_ENABLE_FTS4)
|
||||
extern int Sqlitetestfts3_Init(Tcl_Interp *interp);
|
||||
#endif
|
||||
@@ -3746,7 +3870,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
|
||||
#if defined(SQLITE_ENABLE_FTS3) || defined(SQLITE_ENABLE_FTS4)
|
||||
Sqlitetestfts3_Init(interp);
|
||||
#endif
|
||||
|
||||
@@ -111,6 +111,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
|
||||
@@ -456,6 +462,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_STAT3
|
||||
Tcl_SetVar2(interp, "sqlite_options", "stat3", "1", TCL_GLOBAL_ONLY);
|
||||
#else
|
||||
|
||||
+17
-4
@@ -492,10 +492,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);
|
||||
|
||||
|
||||
+84
-32
@@ -107,6 +107,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
|
||||
@@ -2682,7 +2692,7 @@ case OP_Savepoint: {
|
||||
}else{
|
||||
db->nSavepoint++;
|
||||
}
|
||||
|
||||
|
||||
/* Link the new savepoint into the database handle's list. */
|
||||
pNew->pNext = db->pSavepoint;
|
||||
db->pSavepoint = pNew;
|
||||
@@ -3978,9 +3988,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
|
||||
@@ -4005,7 +4015,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];
|
||||
@@ -4016,6 +4026,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 ){
|
||||
@@ -4029,6 +4040,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 = lastRowid = iKey;
|
||||
if( pData->flags & MEM_Null ){
|
||||
@@ -4054,17 +4083,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.
|
||||
**
|
||||
@@ -4079,29 +4103,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
|
||||
@@ -4114,18 +4140,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 * * * * *
|
||||
|
||||
@@ -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 */
|
||||
int (*xAdvance)(BtCursor *, int *);
|
||||
} p4;
|
||||
#ifdef SQLITE_DEBUG
|
||||
@@ -119,6 +120,7 @@ typedef struct VdbeOpList VdbeOpList;
|
||||
#define P4_INTARRAY (-15) /* P4 is a vector of 32-bit integers */
|
||||
#define P4_SUBPROGRAM (-18) /* P4 is a pointer to a SubProgram structure */
|
||||
#define P4_ADVANCE (-19) /* P4 is a pointer to BtreeNext() or BtreePrev() */
|
||||
#define P4_TABLE (-20) /* P4 is a pointer to a Table structure */
|
||||
|
||||
/* 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
|
||||
|
||||
@@ -359,6 +359,25 @@ struct Vdbe {
|
||||
#define VDBE_MAGIC_HALT 0x519c2973 /* VDBE has completed execution */
|
||||
#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
|
||||
*/
|
||||
@@ -418,6 +437,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);
|
||||
int sqlite3VdbeTransferError(Vdbe *p);
|
||||
|
||||
#ifdef SQLITE_OMIT_MERGE_SORT
|
||||
|
||||
+204
-1
@@ -675,6 +675,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
|
||||
@@ -716,7 +736,7 @@ static Mem *columnMem(sqlite3_stmt *pStmt, int i){
|
||||
sqlite3_mutex_enter(pVm->db->mutex);
|
||||
sqlite3Error(pVm->db, SQLITE_RANGE, 0);
|
||||
}
|
||||
pOut = (Mem*)&nullMem;
|
||||
pOut = (Mem*)columnNullValue();
|
||||
}
|
||||
return pOut;
|
||||
}
|
||||
@@ -1306,3 +1326,186 @@ 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
|
||||
/*
|
||||
** Allocate and populate an UnpackedRecord structure based on the serialized
|
||||
** record in nKey/pKey. Return a pointer to the new UnpackedRecord structure
|
||||
** if successful, or a NULL pointer if an OOM error is encountered.
|
||||
*/
|
||||
static UnpackedRecord *vdbeUnpackRecord(
|
||||
KeyInfo *pKeyInfo,
|
||||
int nKey,
|
||||
const void *pKey
|
||||
){
|
||||
char *dummy; /* Dummy argument for AllocUnpackedRecord() */
|
||||
UnpackedRecord *pRet; /* Return value */
|
||||
|
||||
pRet = sqlite3VdbeAllocUnpackedRecord(pKeyInfo, 0, 0, &dummy);
|
||||
if( pRet ){
|
||||
sqlite3VdbeRecordUnpack(pKeyInfo, nKey, pKey, pRet);
|
||||
}
|
||||
return pRet;
|
||||
}
|
||||
|
||||
/*
|
||||
** This function is called from within a pre-update callback to retrieve
|
||||
** a field of the row currently being updated or deleted.
|
||||
*/
|
||||
int sqlite3_preupdate_old(sqlite3 *db, int iIdx, sqlite3_value **ppValue){
|
||||
PreUpdate *p = db->pPreUpdate;
|
||||
int rc = SQLITE_OK;
|
||||
|
||||
/* Test that this call is being made from within an SQLITE_DELETE or
|
||||
** SQLITE_UPDATE pre-update callback, and that iIdx is within range. */
|
||||
if( !p || p->op==SQLITE_INSERT ){
|
||||
rc = SQLITE_MISUSE_BKPT;
|
||||
goto preupdate_old_out;
|
||||
}
|
||||
if( iIdx>=p->pCsr->nField || iIdx<0 ){
|
||||
rc = SQLITE_RANGE;
|
||||
goto preupdate_old_out;
|
||||
}
|
||||
|
||||
/* If the old.* record has not yet been loaded into memory, do so now. */
|
||||
if( p->pUnpacked==0 ){
|
||||
u32 nRec;
|
||||
u8 *aRec;
|
||||
|
||||
rc = sqlite3BtreeDataSize(p->pCsr->pCursor, &nRec);
|
||||
if( rc!=SQLITE_OK ) goto preupdate_old_out;
|
||||
aRec = sqlite3DbMallocRaw(db, nRec);
|
||||
if( !aRec ) goto preupdate_old_out;
|
||||
rc = sqlite3BtreeData(p->pCsr->pCursor, 0, nRec, aRec);
|
||||
if( rc==SQLITE_OK ){
|
||||
p->pUnpacked = vdbeUnpackRecord(&p->keyinfo, nRec, aRec);
|
||||
if( !p->pUnpacked ) rc = SQLITE_NOMEM;
|
||||
}
|
||||
if( rc!=SQLITE_OK ){
|
||||
sqlite3DbFree(db, aRec);
|
||||
goto preupdate_old_out;
|
||||
}
|
||||
p->aRecord = aRec;
|
||||
}
|
||||
|
||||
if( iIdx>=p->pUnpacked->nField ){
|
||||
*ppValue = (sqlite3_value *)columnNullValue();
|
||||
}else{
|
||||
*ppValue = &p->pUnpacked->aMem[iIdx];
|
||||
if( iIdx==p->iPKey ){
|
||||
sqlite3VdbeMemSetInt64(*ppValue, p->iKey1);
|
||||
}
|
||||
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->keyinfo.nField : 0);
|
||||
}
|
||||
#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */
|
||||
|
||||
#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
|
||||
/*
|
||||
** This function is designed to be called from within a pre-update callback
|
||||
** only. It returns zero if the change that caused the callback was made
|
||||
** immediately by a user SQL statement. Or, if the change was made by a
|
||||
** trigger program, it returns the number of trigger programs currently
|
||||
** on the stack (1 for a top-level trigger, 2 for a trigger fired by a
|
||||
** top-level trigger etc.).
|
||||
**
|
||||
** For the purposes of the previous paragraph, a foreign key CASCADE, SET NULL
|
||||
** or SET DEFAULT action is considered a trigger.
|
||||
*/
|
||||
int sqlite3_preupdate_depth(sqlite3 *db){
|
||||
PreUpdate *p = db->pPreUpdate;
|
||||
return (p ? p->v->nFrame : 0);
|
||||
}
|
||||
#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */
|
||||
|
||||
#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
|
||||
/*
|
||||
** This function is called from within a pre-update callback to retrieve
|
||||
** a field of the row currently being updated or inserted.
|
||||
*/
|
||||
int sqlite3_preupdate_new(sqlite3 *db, int iIdx, sqlite3_value **ppValue){
|
||||
PreUpdate *p = db->pPreUpdate;
|
||||
int rc = SQLITE_OK;
|
||||
Mem *pMem;
|
||||
|
||||
if( !p || p->op==SQLITE_DELETE ){
|
||||
rc = SQLITE_MISUSE_BKPT;
|
||||
goto preupdate_new_out;
|
||||
}
|
||||
if( iIdx>=p->pCsr->nField || iIdx<0 ){
|
||||
rc = SQLITE_RANGE;
|
||||
goto preupdate_new_out;
|
||||
}
|
||||
|
||||
if( p->op==SQLITE_INSERT ){
|
||||
/* For an INSERT, memory cell p->iNewReg contains the serialized record
|
||||
** that is being inserted. Deserialize it. */
|
||||
UnpackedRecord *pUnpack = p->pNewUnpacked;
|
||||
if( !pUnpack ){
|
||||
Mem *pData = &p->v->aMem[p->iNewReg];
|
||||
rc = sqlite3VdbeMemExpandBlob(pData);
|
||||
if( rc!=SQLITE_OK ) goto preupdate_new_out;
|
||||
pUnpack = vdbeUnpackRecord(&p->keyinfo, pData->n, pData->z);
|
||||
if( !pUnpack ){
|
||||
rc = SQLITE_NOMEM;
|
||||
goto preupdate_new_out;
|
||||
}
|
||||
p->pNewUnpacked = pUnpack;
|
||||
}
|
||||
if( iIdx>=pUnpack->nField ){
|
||||
pMem = (sqlite3_value *)columnNullValue();
|
||||
}else{
|
||||
pMem = &pUnpack->aMem[iIdx];
|
||||
if( iIdx==p->iPKey ){
|
||||
sqlite3VdbeMemSetInt64(pMem, p->iKey2);
|
||||
}
|
||||
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 */
|
||||
|
||||
@@ -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;
|
||||
@@ -2914,6 +2915,7 @@ void sqlite3VdbeRecordUnpack(
|
||||
pMem->db = pKeyInfo->db;
|
||||
/* pMem->flags = 0; // sqlite3VdbeSerialGet() will set this for us */
|
||||
pMem->zMalloc = 0;
|
||||
pMem->z = 0;
|
||||
d += sqlite3VdbeSerialGet(&aKey[d], serial_type, pMem);
|
||||
pMem++;
|
||||
u++;
|
||||
@@ -3235,3 +3237,83 @@ void sqlite3VdbeSetVarmask(Vdbe *v, int iVar){
|
||||
v->expmask |= ((u32)1 << (iVar-1));
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
|
||||
|
||||
/*
|
||||
** If the second argument is not NULL, release any allocations associated
|
||||
** with the memory cells in the p->aMem[] array. Also free the UnpackedRecord
|
||||
** structure itself, using sqlite3DbFree().
|
||||
**
|
||||
** This function is used to free UnpackedRecord structures allocated by
|
||||
** the vdbeUnpackRecord() function found in vdbeapi.c.
|
||||
*/
|
||||
static void vdbeFreeUnpacked(sqlite3 *db, UnpackedRecord *p){
|
||||
if( p ){
|
||||
int i;
|
||||
for(i=0; i<p->nField; i++){
|
||||
Mem *pMem = &p->aMem[i];
|
||||
if( pMem->zMalloc ) sqlite3VdbeMemRelease(pMem);
|
||||
}
|
||||
sqlite3DbFree(db, p);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
** Invoke the pre-update hook. If this is an UPDATE or DELETE pre-update call,
|
||||
** then cursor passed as the second argument should point to the row about
|
||||
** to be update or deleted. If the application calls sqlite3_preupdate_old(),
|
||||
** the required value will be read from the row the cursor points to.
|
||||
*/
|
||||
void sqlite3VdbePreUpdateHook(
|
||||
Vdbe *v, /* Vdbe pre-update hook is invoked by */
|
||||
VdbeCursor *pCsr, /* Cursor to grab old.* values from */
|
||||
int op, /* SQLITE_INSERT, UPDATE or DELETE */
|
||||
const char *zDb, /* Database name */
|
||||
Table *pTab, /* Modified table */
|
||||
i64 iKey1, /* Initial key value */
|
||||
int iReg /* Register for new.* record */
|
||||
){
|
||||
sqlite3 *db = v->db;
|
||||
i64 iKey2;
|
||||
PreUpdate preupdate;
|
||||
const char *zTbl = pTab->zName;
|
||||
|
||||
assert( db->pPreUpdate==0 );
|
||||
memset(&preupdate, 0, sizeof(PreUpdate));
|
||||
if( op==SQLITE_UPDATE ){
|
||||
iKey2 = v->aMem[iReg].u.i;
|
||||
}else{
|
||||
iKey2 = iKey1;
|
||||
}
|
||||
|
||||
assert( pCsr->nField==pTab->nCol
|
||||
|| (pCsr->nField==pTab->nCol+1 && op==SQLITE_DELETE && iReg==-1)
|
||||
);
|
||||
|
||||
preupdate.v = v;
|
||||
preupdate.pCsr = pCsr;
|
||||
preupdate.op = op;
|
||||
preupdate.iNewReg = iReg;
|
||||
preupdate.keyinfo.db = db;
|
||||
preupdate.keyinfo.enc = ENC(db);
|
||||
preupdate.keyinfo.nField = pTab->nCol;
|
||||
preupdate.iKey1 = iKey1;
|
||||
preupdate.iKey2 = iKey2;
|
||||
preupdate.iPKey = pTab->iPKey;
|
||||
|
||||
db->pPreUpdate = &preupdate;
|
||||
db->xPreUpdateCallback(db->pPreUpdateArg, db, op, zDb, zTbl, iKey1, iKey2);
|
||||
db->pPreUpdate = 0;
|
||||
sqlite3DbFree(db, preupdate.aRecord);
|
||||
vdbeFreeUnpacked(db, preupdate.pUnpacked);
|
||||
vdbeFreeUnpacked(db, preupdate.pNewUnpacked);
|
||||
if( preupdate.aNew ){
|
||||
int i;
|
||||
for(i=0; i<pCsr->nField; i++){
|
||||
sqlite3VdbeMemRelease(&preupdate.aNew[i]);
|
||||
}
|
||||
sqlite3DbFree(db, preupdate.aNew);
|
||||
}
|
||||
}
|
||||
#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */
|
||||
|
||||
@@ -30,6 +30,8 @@ struct Incrblob {
|
||||
BtCursor *pCsr; /* Cursor pointing at blob row */
|
||||
sqlite3_stmt *pStmt; /* Statement holding cursor open */
|
||||
sqlite3 *db; /* The associated database */
|
||||
char *zDb; /* Database name */
|
||||
Table *pTab; /* Table object */
|
||||
};
|
||||
|
||||
|
||||
@@ -194,6 +196,8 @@ int sqlite3_blob_open(
|
||||
sqlite3BtreeLeaveAll(db);
|
||||
goto blob_open_out;
|
||||
}
|
||||
pBlob->pTab = pTab;
|
||||
pBlob->zDb = db->aDb[sqlite3SchemaToIndex(db, pTab->pSchema)].zName;
|
||||
|
||||
/* Now search pTab for the exact column. */
|
||||
for(iCol=0; iCol<pTab->nCol; iCol++) {
|
||||
@@ -386,6 +390,30 @@ static int blobReadWrite(
|
||||
*/
|
||||
assert( db == v->db );
|
||||
sqlite3BtreeEnterCursor(p->pCsr);
|
||||
|
||||
#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
|
||||
if( xCall==sqlite3BtreePutData && db->xPreUpdateCallback ){
|
||||
/* If a pre-update hook is registered and this is a write cursor,
|
||||
** invoke it here.
|
||||
**
|
||||
** TODO: The preupdate-hook is passed SQLITE_DELETE, even though this
|
||||
** operation should really be an SQLITE_UPDATE. This is probably
|
||||
** incorrect, but is convenient because at this point the new.* values
|
||||
** are not easily obtainable. And for the sessions module, an
|
||||
** SQLITE_UPDATE where the PK columns do not change is handled in the
|
||||
** same way as an SQLITE_DELETE (the SQLITE_DELETE code is actually
|
||||
** slightly more efficient). Since you cannot write to a PK column
|
||||
** using the incremental-blob API, this works. For the sessions module
|
||||
** anyhow.
|
||||
*/
|
||||
sqlite3_int64 iKey;
|
||||
sqlite3BtreeKeySize(p->pCsr, &iKey);
|
||||
sqlite3VdbePreUpdateHook(
|
||||
v, v->apCsr[0], SQLITE_DELETE, p->zDb, p->pTab, iKey, -1
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
||||
rc = xCall(p->pCsr, iOffset+p->iOffset, n, z);
|
||||
sqlite3BtreeLeaveCursor(p->pCsr);
|
||||
if( rc==SQLITE_ABORT ){
|
||||
|
||||
+1
-1
@@ -4447,7 +4447,7 @@ static Bitmask codeOneLoopStart(
|
||||
}
|
||||
}
|
||||
pLevel->u.pCovidx = pCov;
|
||||
pLevel->iIdxCur = iCovCur;
|
||||
if( pCov ) pLevel->iIdxCur = iCovCur;
|
||||
if( pAndExpr ){
|
||||
pAndExpr->pLeft = 0;
|
||||
sqlite3ExprDelete(pParse->db, pAndExpr);
|
||||
|
||||
+408
@@ -21,6 +21,7 @@
|
||||
|
||||
set testdir [file dirname $argv0]
|
||||
source $testdir/tester.tcl
|
||||
set ::testprefix hook
|
||||
|
||||
do_test hook-1.2 {
|
||||
db commit_hook
|
||||
@@ -389,4 +390,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
|
||||
|
||||
@@ -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 {
|
||||
@@ -782,6 +785,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.
|
||||
|
||||
@@ -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
|
||||
+11
-2
@@ -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
|
||||
@@ -319,14 +319,23 @@ do_test tcl-8.1 {
|
||||
execsql {INSERT INTO t1 VALUES(30,NULL)}
|
||||
db eval {SELECT * FROM t1 WHERE b IS NULL}
|
||||
} {30 NaN}
|
||||
proc concatFunc args {return [join $args {}]}
|
||||
do_test tcl-8.2 {
|
||||
db function concat concatFunc
|
||||
db eval {SELECT concat('a', b, 'z') FROM t1 WHERE b is NULL}
|
||||
} {aNaNz}
|
||||
do_test tcl-8.3 {
|
||||
db nullvalue NULL
|
||||
db nullvalue
|
||||
} {NULL}
|
||||
do_test tcl-8.3 {
|
||||
do_test tcl-8.4 {
|
||||
db nullvalue {}
|
||||
db eval {SELECT * FROM t1 WHERE b IS NULL}
|
||||
} {30 {}}
|
||||
do_test tcl-8.5 {
|
||||
db function concat concatFunc
|
||||
db eval {SELECT concat('a', b, 'z') FROM t1 WHERE b is NULL}
|
||||
} {az}
|
||||
|
||||
# Test the return type of user-defined functions
|
||||
#
|
||||
|
||||
@@ -603,6 +603,12 @@ proc fix_testname {varname} {
|
||||
set testname "${::testprefix}-$testname"
|
||||
}
|
||||
}
|
||||
|
||||
proc normalize_list {L} {
|
||||
set L2 [list]
|
||||
foreach l $L {lappend L2 $l}
|
||||
set L2
|
||||
}
|
||||
|
||||
proc do_execsql_test {testname sql {result {}}} {
|
||||
fix_testname testname
|
||||
|
||||
@@ -161,4 +161,29 @@ do_searchcount_test 3.5.2 {
|
||||
SELECT a, c FROM t3 WHERE (a=1 AND b='one') OR rowid=4
|
||||
} {1 i 2 ii search 2}
|
||||
|
||||
# Ticket [d02e1406a58ea02d] (2012-10-04)
|
||||
# LEFT JOIN with an OR in the ON clause causes segfault
|
||||
#
|
||||
do_test 4.1 {
|
||||
db eval {
|
||||
CREATE TABLE t41(a,b,c);
|
||||
INSERT INTO t41 VALUES(1,2,3), (4,5,6);
|
||||
CREATE TABLE t42(d,e,f);
|
||||
INSERT INTO t42 VALUES(3,6,9), (4,8,12);
|
||||
SELECT * FROM t41 AS x LEFT JOIN t42 AS y ON (y.d=x.c) OR (y.e=x.b);
|
||||
}
|
||||
} {1 2 3 3 6 9 4 5 6 {} {} {}}
|
||||
do_test 4.2 {
|
||||
db eval {
|
||||
CREATE INDEX t42d ON t42(d);
|
||||
CREATE INDEX t42e ON t42(e);
|
||||
SELECT * FROM t41 AS x LEFT JOIN t42 AS y ON (y.d=x.c) OR (y.e=x.b);
|
||||
}
|
||||
} {1 2 3 3 6 9 4 5 6 {} {} {}}
|
||||
do_test 4.2 {
|
||||
db eval {
|
||||
SELECT * FROM t41 AS x LEFT JOIN t42 AS y ON (y.d=x.c) OR (y.d=x.b);
|
||||
}
|
||||
} {1 2 3 3 6 9 4 5 6 {} {} {}}
|
||||
|
||||
finish_test
|
||||
|
||||
Reference in New Issue
Block a user