diff --git a/Makefile.msc b/Makefile.msc index 220367810c..763deb6544 100644 --- a/Makefile.msc +++ b/Makefile.msc @@ -1568,7 +1568,8 @@ TESTSRC = \ $(TOP)\ext\fts3\fts3_term.c \ $(TOP)\ext\fts3\fts3_test.c \ $(TOP)\ext\rbu\test_rbu.c \ - $(TOP)\ext\session\test_session.c + $(TOP)\ext\session\test_session.c \ + $(TOP)\ext\session\sqlite3changebatch.c # Statically linked extensions. # diff --git a/autosetup/teaish/core.tcl b/autosetup/teaish/core.tcl index c9abfa0626..381597ec56 100644 --- a/autosetup/teaish/core.tcl +++ b/autosetup/teaish/core.tcl @@ -92,7 +92,6 @@ array set teaish__Config [proj-strip-hash-comments { -tm.tcl.in TEAISH_TM_TCL_IN -options {} -pragmas {} - -src {} } # @@ -220,9 +219,7 @@ proc teaish-configure-core {} { => {Full pathname of tclsh to use. It is used for trying to find tclConfig.sh. Warning: if its containing dir has multiple tclsh versions, it may select the wrong tclConfig.sh! - Defaults to the $TCLSH environment variable.} - - tcl-stubs=0 => {Enable use of Tcl stubs library.} + Defaults to the $TCLSH environment variable.} # TEA has --with-tclinclude but it appears to only be useful for # building an extension against an uninstalled copy of TCL's own @@ -334,33 +331,29 @@ proc teaish-configure-core {} { -url - -v "" -tm.tcl - -v "" -tm.tcl.in - -v "" - -src - -v "" } { - #proj-assert 0 {Just testing} set isPIFlag [expr {"-" ne $pflag}] if {$isPIFlag} { if {[info exists ::teaish__PkgInfo($pflag)]} { # Was already set - skip it. continue; } - proj-assert {{-} eq $key};# "Unexpected pflag=$pflag key=$key type=$type val=$val" + proj-assert {{-} eq $key} set key $f2d($pflag) } - if {"" ne $key} { - if {"" ne [get-define $key ""]} { - # Was already set - skip it. - continue - } + proj-assert {"" ne $key} + set got [get-define $key ""] + if {"" ne $got} { + # Was already set - skip it. + continue } switch -exact -- $type { -v {} -e { set val [eval $val] } default { proj-error "Invalid type flag: $type" } } - #puts "***** defining default $pflag $key {$val} isPIFlag=$isPIFlag" - if {$key ne ""} { - define $key $val - } + #puts "***** defining default $pflag $key {$val} isPIFlag=$isPIFlag got=$got" + define $key $val if {$isPIFlag} { set ::teaish__PkgInfo($pflag) $val } @@ -500,8 +493,6 @@ proc teaish__configure_phase1 {} { } teaish-checks-run -post - define TEAISH_USE_STUBS [opt-bool tcl-stubs] - apply {{} { # Set up "vsatisfies" code for pkgIndex.tcl.in, # _teaish.tester.tcl.in, and for a configure-time check. We would @@ -531,7 +522,7 @@ proc teaish__configure_phase1 {} { set vputs "puts \[ $vsat \]" #puts "*** vputs = $vputs" scan [exec echo $vputs | $tclsh] %d vvcheck - if {![info exists vvcheck] || 0 == $vvcheck} { + if {0 == $vvcheck} { proj-fatal -up $tclsh "check failed:" $vsat } } @@ -594,8 +585,7 @@ proc teaish__configure_phase1 {} { # if {0x0f & $::teaish__Config(pkginit-policy)} { file delete -force -- [get-define TEAISH_PKGINIT_TCL] - proj-dot-ins-append [get-define TEAISH_PKGINIT_TCL_IN] \ - [get-define TEAISH_PKGINIT_TCL] + proj-dot-ins-append [get-define TEAISH_PKGINIT_TCL_IN] } if {0x0f & $::teaish__Config(tm-policy)} { file delete -force -- [get-define TEAISH_TM_TCL] @@ -605,20 +595,17 @@ proc teaish__configure_phase1 {} { apply {{} { # Queue up any remaining dot-in files set dotIns [list] - foreach {dIn => dOut} { - TEAISH_TESTER_TCL_IN => TEAISH_TESTER_TCL - TEAISH_TEST_TCL_IN => TEAISH_TEST_TCL - TEAISH_MAKEFILE_IN => TEAISH_MAKEFILE + foreach d { + TEAISH_TESTER_TCL_IN + TEAISH_TEST_TCL_IN + TEAISH_MAKEFILE_IN } { - lappend dotIns [get-define $dIn ""] [get-define $dOut ""] - } - lappend dotIns $::autosetup(srcdir)/Makefile.in Makefile; # must be after TEAISH_MAKEFILE_IN. - # Much later: probably because of timestamps for deps purposes :-? - #puts "dotIns=$dotIns" - foreach {i o} $dotIns { - if {"" ne $i && "" ne $o} { - #puts " pre-dot-ins-append: \[$i\] -> \[$o\]" - proj-dot-ins-append $i $o + lappend dotIns [get-define $d ""] + } + lappend dotIns $::autosetup(srcdir)/Makefile.in; # must be after TEAISH_MAKEFILE_IN + foreach f $dotIns { + if {"" ne $f} { + proj-dot-ins-append $f } } }} @@ -653,10 +640,10 @@ proc teaish__configure_phase1 {} { # # NO [define]s after this point! # + proj-dot-ins-process -validate proj-if-opt-truthy teaish-dump-defines { proj-file-write config.defines.txt $tdefs } - proj-dot-ins-process -validate }; # teaish__configure_phase1 @@ -830,9 +817,7 @@ proc teaish__check_tcl {} { if {"" ne $withSh && [catch {exec echo "puts stdout \$auto_path" | "$withSh"} result] == 0} { foreach i $result { - if {![string match //zip* $i] && [file isdirectory $i]} { - # isdirectory actually passes on //zipfs:/..., but those are - # useless for our purposes + if {[file isdirectory $i]} { set tcllibdir $i/$extDirName break } @@ -1081,7 +1066,7 @@ If you are attempting an out-of-tree build, use ]]} { if {[string match *.in $extM]} { define TEAISH_MAKEFILE_IN $extM - define TEAISH_MAKEFILE _[file rootname [file tail $extM]] + define TEAISH_MAKEFILE [file rootname [file tail $extM]] } else { define TEAISH_MAKEFILE_IN "" define TEAISH_MAKEFILE $extM @@ -1149,8 +1134,8 @@ If you are attempting an out-of-tree build, use set flist [list $dirExt/teaish.test.tcl.in $dirExt/teaish.test.tcl] if {[proj-first-file-found ttt $flist]} { if {[string match *.in $ttt]} { - # Generate _teaish.test.tcl from $ttt - set xt _[file rootname [file tail $ttt]] + # Generate teaish.test.tcl from $ttt + set xt [file rootname [file tail $ttt]] file delete -force -- $xt; # ensure no stale copy is used define TEAISH_TEST_TCL $xt define TEAISH_TEST_TCL_IN $ttt @@ -1317,6 +1302,7 @@ proc teaish-ldflags-prepend {args} { # object files (which are typically in the build tree)). # proc teaish-src-add {args} { + set i 0 proj-parse-simple-flags args flags { -dist 0 {expr 1} -dir 0 {expr 1} @@ -1401,7 +1387,7 @@ proc teaish__cleanup_rule {{tgt clean}} { return ${tgt}-_${x}_ } -# @teaish-make-obj ?flags? ?...args? +# @teaish-make-obj objfile srcfile ?...args? # # Uses teaish-make-add to inject makefile rules for $objfile from # $srcfile, which is assumed to be C code which uses libtcl. Unless @@ -1415,45 +1401,43 @@ proc teaish__cleanup_rule {{tgt clean}} { # Any arguments after the 2nd may be flags described below or, if no # -recipe is provided, flags for the compiler call. # -# -obj obj-filename.o -# -# -src src-filename.c -# # -recipe {...} # Uses the trimmed value of {...} as the recipe, prefixing it with # a single hard-tab character. # # -deps {...} -# List of extra files to list as dependencies of $o. +# List of extra files to list as dependencies of $o. Good luck +# escaping non-trivial cases properly. # # -clean # Generate cleanup rules as well. -proc teaish-make-obj {args} { - proj-parse-simple-flags args flags { - -clean 0 {expr 1} - -recipe => {} - -deps => {} - -obj => {} - -src => {} - } - #parray flags - if {"" eq $flags(-obj)} { - set args [lassign $args flags(-obj)] - if {"" eq $flags(-obj)} { - proj-error "Missing -obj flag." +proc teaish-make-obj {o src args} { + set consume 0 + set clean 0 + set flag "" + array set flags {} + set xargs {} + foreach arg $args { + if {$consume} { + set consume 0 + set flags($flag) $arg + continue + } + switch -exact -- $arg { + -clean {incr clean} + -recipe - + -deps { + set flag $arg + incr consume + } + default { + lappend xargs $arg + } } } - foreach f {-deps -src} { - set flags($f) [string trim [string map {\n " "} $flags($f)]] - } - foreach f {-deps -src} { - set flags($f) [string trim $flags($f)] - } - #parray flags - #puts "-- args=$args" teaish-make-add \ - "# [proj-scope 1] -> [proj-scope] $flags(-obj) $flags(-src)" -nl \ - "$flags(-obj): $flags(-src) $::teaish__Config(teaish.tcl)" + "# [proj-scope 1] -> [proj-scope] $o $src" -nl \ + "$o: $src $::teaish__Config(teaish.tcl)" if {[info exists flags(-deps)]} { teaish-make-add " " [join $flags(-deps)] } @@ -1461,12 +1445,12 @@ proc teaish-make-obj {args} { if {[info exists flags(-recipe)]} { teaish-make-add [string trim $flags(-recipe)] -nl } else { - teaish-make-add [join [list \$(CC.tcl) -c $flags(-src) {*}$args]] -nl + teaish-make-add [join [list \$(CC.tcl) -c $src {*}$xargs]] -nl } - if {$flags(-clean)} { + if {$clean} { set rule [teaish__cleanup_rule] teaish-make-add \ - "clean: $rule\n$rule:\n\trm -f \"$flags(-obj)\"\n" + "clean: $rule\n$rule:\n\trm -f \"$o\"\n" } } @@ -2094,17 +2078,6 @@ proc teaish-pkginfo-set {args} { set v $x } - -src { - set d $::teaish__Config(extension-dir) - foreach f $v { - lappend ::teaish__Config(dist-files) $f - lappend ::teaish__Config(extension-src) $d/$f - lappend ::teaish__PkgInfo(-src) $f - # ^^^ so that default-value initialization in - # teaish-configure-core recognizes that it's been set. - } - } - -tm.tcl - -tm.tcl.in { if {0x30 & $::teaish__Config(pkgindex-policy)} { @@ -2542,7 +2515,7 @@ proc teaish__install {{dDest ""}} { ] { teaish__verbose 1 msg-result "Copying files to $destDir..." file mkdir $destDir - foreach f [glob -nocomplain -directory $srcDir *] { + foreach f [glob -directory $srcDir *] { if {[string match {*~} $f] || [string match "#*#" [file tail $f]]} { # Editor-generated backups and emacs lock files continue diff --git a/autosetup/teaish/tester.tcl b/autosetup/teaish/tester.tcl index a25b366e8d..d8b5f7a0e8 100644 --- a/autosetup/teaish/tester.tcl +++ b/autosetup/teaish/tester.tcl @@ -99,7 +99,7 @@ proc test__affert {failMode args} { lassign $args script msg } incr ::test__Counters($what) - if {![uplevel 1 expr [list $script]]} { + if {![uplevel 1 [concat expr [list $script]]]} { if {"" eq $msg} { set msg $script } @@ -136,40 +136,6 @@ proc assert {args} { tailcall test__affert 1 {*}$args } -# -# @assert-matches ?-e? pattern ?-e? rhs ?msg? -# -# Equivalent to assert {[string match $pattern $rhs]} except that -# if either of those are prefixed with an -e flag, they are eval'd -# and their results are used. -# -proc assert-matches {args} { - set evalLhs 0 - set evalRhs 0 - if {"-e" eq [lindex $args 0]} { - incr evalLhs - set args [lassign $args -] - } - set args [lassign $args pattern] - if {"-e" eq [lindex $args 0]} { - incr evalRhs - set args [lassign $args -] - } - set args [lassign $args rhs msg] - - if {$evalLhs} { - set pattern [uplevel 1 $pattern] - } - if {$evalRhs} { - set rhs [uplevel 1 $rhs] - } - #puts "***pattern=$pattern\n***rhs=$rhs" - tailcall test__affert 1 \ - [join [list \[ string match [list $pattern] [list $rhs] \]]] $msg - # why does this not work? [list \[ string match [list $pattern] [list $rhs] \]] $msg - # "\[string match [list $pattern] [list $rhs]\]" -} - # # @test-assert testId script ?msg? # @@ -191,7 +157,7 @@ proc test-expect {testId script result} { puts "test $testId" set x [string trim [uplevel 1 $script]] set result [string trim $result] - tailcall test__affert 0 [list "{$x}" eq "{$result}"] \ + tailcall test__affert 0 [list $x eq $result] \ "\nEXPECTED: <<$result>>\nGOT: <<$x>>" } @@ -203,7 +169,7 @@ proc test-expect {testId script result} { # proc test-catch {cmd args} { if {[catch { - uplevel 1 $cmd {*}$args + $cmd {*}$args } rc xopts]} { puts "[test-current-scope] ignoring failure of: $cmd [lindex $args 0]: $rc" return 1 @@ -211,37 +177,6 @@ proc test-catch {cmd args} { return 0 } -# -# @test-catch-matching pattern (script|cmd args...) -# -# Works like test-catch, but it expects its argument(s) to to throw an -# error matching the given string (checked with [string match]). If -# they do not throw, or the error does not match $pattern, this -# function throws, else it returns 1. -# -# If there is no second argument, the $cmd is assumed to be a script, -# and will be eval'd in the caller's scope. -# -# TODO: add -glob and -regex flags to control matching flavor. -# -proc test-catch-matching {pattern cmd args} { - if {[catch { - #puts "**** catch-matching cmd=$cmd args=$args" - if {0 == [llength $args]} { - uplevel 1 $cmd {*}$args - } else { - $cmd {*}$args - } - } rc xopts]} { - if {[string match $pattern $rc]} { - return 1 - } else { - error "[test-current-scope] exception does not match {$pattern}: {$rc}" - } - } - error "[test-current-scope] expecting to see an error matching {$pattern}" -} - if {![array exists ::teaish__BuildFlags]} { array set ::teaish__BuildFlags {} } diff --git a/doc/begin_concurrent.md b/doc/begin_concurrent.md new file mode 100644 index 0000000000..0575972942 --- /dev/null +++ b/doc/begin_concurrent.md @@ -0,0 +1,106 @@ + +Begin Concurrent +================ + +## Overview + +Usually, SQLite allows at most one writer to proceed concurrently. The +BEGIN CONCURRENT enhancement allows multiple writers to process write +transactions simultanously if the database is in "wal" or "wal2" mode, +although the system still serializes COMMIT commands. + +When a write-transaction is opened with "BEGIN CONCURRENT", actually +locking the database is deferred until a COMMIT is executed. This means +that any number of transactions started with BEGIN CONCURRENT may proceed +concurrently. The system uses optimistic page-level-locking to prevent +conflicting concurrent transactions from being committed. + +When a BEGIN CONCURRENT transaction is committed, the system checks whether +or not any of the database pages that the transaction has read have been +modified since the BEGIN CONCURRENT was opened. In other words - it asks +if the transaction being committed operates on a different set of data than +all other concurrently executing transactions. If the answer is "yes, this +transaction did not read or modify any data modified by any concurrent +transaction", then the transaction is committed as normal. Otherwise, if the +transaction does conflict, it cannot be committed and an SQLITE_BUSY_SNAPSHOT +error is returned. At this point, all the client can do is ROLLBACK the +transaction. + +If SQLITE_BUSY_SNAPSHOT is returned, messages are output via the sqlite3_log +mechanism indicating the page and table or index on which the conflict +occurred. This can be useful when optimizing concurrency. + +## Application Programming Notes + +In order to serialize COMMIT processing, SQLite takes a lock on the database +as part of each COMMIT command and releases it before returning. At most one +writer may hold this lock at any one time. If a writer cannot obtain the lock, +it uses SQLite's busy-handler to pause and retry for a while: + + + https://www.sqlite.org/c3ref/busy_handler.html + + +If there is significant contention for the writer lock, this mechanism can be +inefficient. In this case it is better for the application to use a mutex or +some other mechanism that supports blocking to ensure that at most one writer +is attempting to COMMIT a BEGIN CONCURRENT transaction at a time. This is +usually easier if all writers are part of the same operating system process. + +If all database clients (readers and writers) are located in the same OS +process, and if that OS is a Unix variant, then it can be more efficient to +the built-in VFS "unix-excl" instead of the default "unix". This is because it +uses more efficient locking primitives. + +The key to maximizing concurrency using BEGIN CONCURRENT is to ensure that +there are a large number of non-conflicting transactions. In SQLite, each +table and each index is stored as a separate b-tree, each of which is +distributed over a discrete set of database pages. This means that: + + * Two transactions that write to different sets of tables never + conflict, and that + + * Two transactions that write to the same tables or indexes only + conflict if the values of the keys (either primary keys or indexed + rows) are fairly close together. For example, given a large + table with the schema: + +
     CREATE TABLE t1(a INTEGER PRIMARY KEY, b BLOB);
+ + writing two rows with adjacent values for "a" probably will cause a + conflict (as the two keys are stored on the same page), but writing two + rows with vastly different values for "a" will not (as the keys will likly + be stored on different pages). + +Note that, in SQLite, if values are not explicitly supplied for an INTEGER +PRIMARY KEY, as for example in: + +> + INSERT INTO t1(b) VALUES(); + +then monotonically increasing values are assigned automatically. This is +terrible for concurrency, as it all but ensures that all new rows are +added to the same database page. In such situations, it is better to +explicitly assign random values to INTEGER PRIMARY KEY fields. + +This problem also comes up for non-WITHOUT ROWID tables that do not have an +explicit INTEGER PRIMARY KEY column. In these cases each table has an implicit +INTEGER PRIMARY KEY column that is assigned increasing values, leading to the +same problem as omitting to assign a value to an explicit INTEGER PRIMARY KEY +column. + +For both explicit and implicit INTEGER PRIMARY KEYs, it is possible to have +SQLite assign values at random (instead of the monotonically increasing +values) by writing a row with a rowid equal to the largest possible signed +64-bit integer to the table. For example: + + INSERT INTO t1(a) VALUES(9223372036854775807); + +Applications should take care not to malfunction due to the presence of such +rows. + +The nature of some types of indexes, for example indexes on timestamp fields, +can also cause problems (as concurrent transactions may assign similar +timestamps that will be stored on the same db page to new records). In these +cases the database schema may need to be rethought to increase the concurrency +provided by page-level-locking. diff --git a/ext/session/changebatch1.test b/ext/session/changebatch1.test new file mode 100644 index 0000000000..c3b5a2460d --- /dev/null +++ b/ext/session/changebatch1.test @@ -0,0 +1,222 @@ +# 2016 August 23 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# This file implements regression tests for SQLite library. +# + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. test] +} +source $testdir/tester.tcl +ifcapable !session {finish_test; return} + +set testprefix changebatch1 + + +proc sql_to_changeset {method sql} { + sqlite3session S db main + S attach * + execsql $sql + set ret [S $method] + S delete + return $ret +} + +proc do_changebatch_test {tn method args} { + set C [list] + foreach a $args { + lappend C [sql_to_changeset $method $a] + } + + sqlite3changebatch cb db + set i 1 + foreach ::cs [lrange $C 0 end-1] { + set rc [cb add $::cs] + if {$rc!="SQLITE_OK"} { error "expected SQLITE_OK, got $rc (i=$i)" } + incr i + } + + set ::cs [lindex $C end] + do_test $tn { cb add [set ::cs] } SQLITE_CONSTRAINT + cb delete +} + +proc do_changebatch_test1 {tn args} { + uplevel do_changebatch_test $tn changeset $args +} +proc do_changebatch_test2 {tn args} { + uplevel do_changebatch_test $tn fullchangeset $args +} + +#------------------------------------------------------------------------- +# The body of the following loop contains tests for database schemas +# that do not feature multi-column UNIQUE constraints. In this case +# it doesn't matter if the changesets are generated using +# sqlite3session_changeset() or sqlite3session_fullchangeset(). +# +foreach {tn testfunction} { + 1 do_changebatch_test1 + 2 do_changebatch_test2 +} { + reset_db + + #------------------------------------------------------------------------- + # + do_execsql_test $tn.1.0 { + CREATE TABLE t1(a PRIMARY KEY, b); + } + + $testfunction $tn.1.1 { + INSERT INTO t1 VALUES(1, 1); + } { + DELETE FROM t1 WHERE a=1; + } + + do_execsql_test $tn.1.2.0 { + INSERT INTO t1 VALUES(1, 1); + INSERT INTO t1 VALUES(2, 2); + INSERT INTO t1 VALUES(3, 3); + } + $testfunction $tn.1.2.1 { + DELETE FROM t1 WHERE a=2; + } { + INSERT INTO t1 VALUES(2, 2); + } + + #------------------------------------------------------------------------- + # + do_execsql_test $tn.2.0 { + CREATE TABLE x1(a, b PRIMARY KEY, c UNIQUE); + CREATE TABLE x2(a PRIMARY KEY, b UNIQUE, c UNIQUE); + CREATE INDEX x1a ON x1(a); + + INSERT INTO x1 VALUES(1, 1, 'a'); + INSERT INTO x1 VALUES(1, 2, 'b'); + INSERT INTO x1 VALUES(1, 3, 'c'); + } + + $testfunction $tn.2.1 { + DELETE FROM x1 WHERE b=2; + } { + UPDATE x1 SET c='b' WHERE b=3; + } + + $testfunction $tn.2.2 { + DELETE FROM x1 WHERE b=1; + } { + INSERT INTO x1 VALUES(1, 5, 'a'); + } + + set L [list] + for {set i 1000} {$i < 10000} {incr i} { + lappend L "INSERT INTO x2 VALUES($i, $i, 'x' || $i)" + } + lappend L "DELETE FROM x2 WHERE b=1005" + $testfunction $tn.2.3 {*}$L + + execsql { INSERT INTO x1 VALUES('f', 'f', 'f') } + $testfunction $tn.2.4 { + INSERT INTO x2 VALUES('f', 'f', 'f'); + } { + INSERT INTO x1 VALUES('g', 'g', 'g'); + } { + DELETE FROM x1 WHERE b='f'; + } { + INSERT INTO x2 VALUES('g', 'g', 'g'); + } { + INSERT INTO x1 VALUES('f', 'f', 'f'); + } + + execsql { + DELETE FROM x1; + INSERT INTO x1 VALUES(1.5, 1.5, 1.5); + } + $testfunction $tn.2.5 { + DELETE FROM x1 WHERE b BETWEEN 1 AND 2; + } { + INSERT INTO x1 VALUES(2.5, 2.5, 2.5); + } { + INSERT INTO x1 VALUES(1.5, 1.5, 1.5); + } + + execsql { + DELETE FROM x2; + INSERT INTO x2 VALUES(X'abcd', X'1234', X'7890'); + INSERT INTO x2 VALUES(X'0000', X'0000', X'0000'); + } + breakpoint + $testfunction $tn.2.6 { + UPDATE x2 SET c = X'1234' WHERE a=X'abcd'; + INSERT INTO x2 VALUES(X'1234', X'abcd', X'7890'); + } { + DELETE FROM x2 WHERE b=X'0000'; + } { + INSERT INTO x2 VALUES(1, X'0000', NULL); + } +} + +#------------------------------------------------------------------------- +# Test some multi-column UNIQUE constraints. First Using _changeset() to +# demonstrate the problem, then using _fullchangeset() to show that it has +# been fixed. +# +reset_db +do_execsql_test 3.0 { + CREATE TABLE y1(a PRIMARY KEY, b, c, UNIQUE(b, c)); + INSERT INTO y1 VALUES(1, 1, 1); + INSERT INTO y1 VALUES(2, 2, 2); + INSERT INTO y1 VALUES(3, 3, 3); + INSERT INTO y1 VALUES(4, 3, 4); + BEGIN; +} + +do_test 3.1.1 { + set c1 [sql_to_changeset changeset { DELETE FROM y1 WHERE a=4 }] + set c2 [sql_to_changeset changeset { UPDATE y1 SET c=4 WHERE a=3 }] + sqlite3changebatch cb db + cb add $c1 + cb add $c2 +} {SQLITE_OK} +do_test 3.1.2 { + cb delete + execsql ROLLBACK +} {} + +do_test 3.1.1 { + set c1 [sql_to_changeset fullchangeset { DELETE FROM y1 WHERE a=4 }] + set c2 [sql_to_changeset fullchangeset { UPDATE y1 SET c=4 WHERE a=3 }] + sqlite3changebatch cb db + cb add $c1 + cb add $c2 +} {SQLITE_OK} +do_test 3.1.2 { + cb delete +} {} + +#------------------------------------------------------------------------- +# +reset_db +do_execsql_test 4.0 { + CREATE TABLE t1(x, y, z, PRIMARY KEY(x, y), UNIQUE(z)); +} + +do_test 4.1 { + set c1 [sql_to_changeset fullchangeset { INSERT INTO t1 VALUES(1, 2, 3) }] + execsql { + DROP TABLE t1; + CREATE TABLE t1(w, x, y, z, PRIMARY KEY(x, y), UNIQUE(z)); + } + sqlite3changebatch cb db + list [catch { cb add $c1 } msg] $msg +} {1 SQLITE_RANGE} + +cb delete + +finish_test diff --git a/ext/session/changebatchfault.test b/ext/session/changebatchfault.test new file mode 100644 index 0000000000..209b60e0c8 --- /dev/null +++ b/ext/session/changebatchfault.test @@ -0,0 +1,42 @@ +# 2011 Mar 21 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# +# The focus of this file is testing the session module. +# + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. test] +} +source [file join [file dirname [info script]] session_common.tcl] +source $testdir/tester.tcl +ifcapable !session {finish_test; return} +set testprefix changebatchfault + +do_execsql_test 1.0 { + CREATE TABLE t1(a, b, c PRIMARY KEY, UNIQUE(a, b)); + INSERT INTO t1 VALUES('a', 'a', 'a'); + INSERT INTO t1 VALUES('b', 'b', 'b'); +} + +set ::c1 [changeset_from_sql { delete from t1 where c='a' }] +set ::c2 [changeset_from_sql { insert into t1 values('c', 'c', 'c') }] + +do_faultsim_test 1 -faults oom-* -body { + sqlite3changebatch cb db + cb add $::c1 + cb add $::c2 +} -test { + faultsim_test_result {0 SQLITE_OK} {1 SQLITE_NOMEM} + catch { cb delete } +} + + +finish_test diff --git a/ext/session/sessionH.test b/ext/session/sessionH.test index e1b12571c6..c8b41600ea 100644 --- a/ext/session/sessionH.test +++ b/ext/session/sessionH.test @@ -29,7 +29,7 @@ do_test 1.0 { WITH s(i) AS ( VALUES(1) UNION ALL SELECT i+1 FROM s WHERe i<10000 ) - INSERT INTO t1 SELECT 'abcde', randomblob(16), i FROM s; + INSERT INTO t1 SELECT 'abcde', randomblob(18), i FROM s; } compare_db db db2 } {} diff --git a/ext/session/sqlite3changebatch.c b/ext/session/sqlite3changebatch.c new file mode 100644 index 0000000000..926a7d7785 --- /dev/null +++ b/ext/session/sqlite3changebatch.c @@ -0,0 +1,485 @@ + +#if !defined(SQLITE_TEST) || (defined(SQLITE_ENABLE_SESSION) && defined(SQLITE_ENABLE_PREUPDATE_HOOK)) + +#include "sqlite3session.h" +#include "sqlite3changebatch.h" + +#include +#include + +typedef struct BatchTable BatchTable; +typedef struct BatchIndex BatchIndex; +typedef struct BatchIndexEntry BatchIndexEntry; +typedef struct BatchHash BatchHash; + +struct sqlite3_changebatch { + sqlite3 *db; /* Database handle used to read schema */ + BatchTable *pTab; /* First in linked list of tables */ + int iChangesetId; /* Current changeset id */ + int iNextIdxId; /* Next available index id */ + int nEntry; /* Number of entries in hash table */ + int nHash; /* Number of hash buckets */ + BatchIndexEntry **apHash; /* Array of hash buckets */ +}; + +struct BatchTable { + BatchIndex *pIdx; /* First in linked list of UNIQUE indexes */ + BatchTable *pNext; /* Next table */ + char zTab[1]; /* Table name */ +}; + +struct BatchIndex { + BatchIndex *pNext; /* Next index on same table */ + int iId; /* Index id (assigned internally) */ + int bPk; /* True for PK index */ + int nCol; /* Size of aiCol[] array */ + int *aiCol; /* Array of columns that make up index */ +}; + +struct BatchIndexEntry { + BatchIndexEntry *pNext; /* Next colliding hash table entry */ + int iChangesetId; /* Id of associated changeset */ + int iIdxId; /* Id of index this key is from */ + int szRecord; + char aRecord[1]; +}; + +/* +** Allocate and zero a block of nByte bytes. Must be freed using cbFree(). +*/ +static void *cbMalloc(int *pRc, int nByte){ + void *pRet; + + if( *pRc ){ + pRet = 0; + }else{ + pRet = sqlite3_malloc(nByte); + if( pRet ){ + memset(pRet, 0, nByte); + }else{ + *pRc = SQLITE_NOMEM; + } + } + + return pRet; +} + +/* +** Free an allocation made by cbMalloc(). +*/ +static void cbFree(void *p){ + sqlite3_free(p); +} + +/* +** Return the hash bucket that pEntry belongs in. +*/ +static int cbHash(sqlite3_changebatch *p, BatchIndexEntry *pEntry){ + unsigned int iHash = (unsigned int)pEntry->iIdxId; + unsigned char *pEnd = (unsigned char*)&pEntry->aRecord[pEntry->szRecord]; + unsigned char *pIter; + + for(pIter=(unsigned char*)pEntry->aRecord; pIternHash); +} + +/* +** Resize the hash table. +*/ +static int cbHashResize(sqlite3_changebatch *p){ + int rc = SQLITE_OK; + BatchIndexEntry **apNew; + int nNew = (p->nHash ? p->nHash*2 : 512); + int i; + + apNew = cbMalloc(&rc, sizeof(BatchIndexEntry*) * nNew); + if( rc==SQLITE_OK ){ + int nHash = p->nHash; + p->nHash = nNew; + for(i=0; iapHash[i])!=0 ){ + int iHash = cbHash(p, pEntry); + p->apHash[i] = pEntry->pNext; + pEntry->pNext = apNew[iHash]; + apNew[iHash] = pEntry; + } + } + + cbFree(p->apHash); + p->apHash = apNew; + } + + return rc; +} + + +/* +** Allocate a new sqlite3_changebatch object. +*/ +int sqlite3changebatch_new(sqlite3 *db, sqlite3_changebatch **pp){ + sqlite3_changebatch *pRet; + int rc = SQLITE_OK; + *pp = pRet = (sqlite3_changebatch*)cbMalloc(&rc, sizeof(sqlite3_changebatch)); + if( pRet ){ + pRet->db = db; + } + return rc; +} + +/* +** Add a BatchIndex entry for index zIdx to table pTab. +*/ +static int cbAddIndex( + sqlite3_changebatch *p, + BatchTable *pTab, + const char *zIdx, + int bPk +){ + int nCol = 0; + sqlite3_stmt *pIndexInfo = 0; + BatchIndex *pNew = 0; + int rc; + char *zIndexInfo; + + zIndexInfo = (char*)sqlite3_mprintf("PRAGMA main.index_info = %Q", zIdx); + if( zIndexInfo ){ + rc = sqlite3_prepare_v2(p->db, zIndexInfo, -1, &pIndexInfo, 0); + sqlite3_free(zIndexInfo); + }else{ + rc = SQLITE_NOMEM; + } + + if( rc==SQLITE_OK ){ + while( SQLITE_ROW==sqlite3_step(pIndexInfo) ){ nCol++; } + rc = sqlite3_reset(pIndexInfo); + } + + pNew = (BatchIndex*)cbMalloc(&rc, sizeof(BatchIndex) + sizeof(int) * nCol); + if( rc==SQLITE_OK ){ + pNew->nCol = nCol; + pNew->bPk = bPk; + pNew->aiCol = (int*)&pNew[1]; + pNew->iId = p->iNextIdxId++; + while( SQLITE_ROW==sqlite3_step(pIndexInfo) ){ + int i = sqlite3_column_int(pIndexInfo, 0); + int j = sqlite3_column_int(pIndexInfo, 1); + pNew->aiCol[i] = j; + } + rc = sqlite3_reset(pIndexInfo); + } + + if( rc==SQLITE_OK ){ + pNew->pNext = pTab->pIdx; + pTab->pIdx = pNew; + }else{ + cbFree(pNew); + } + sqlite3_finalize(pIndexInfo); + + return rc; +} + +/* +** Free the object passed as the first argument. +*/ +static void cbFreeTable(BatchTable *pTab){ + BatchIndex *pIdx; + BatchIndex *pIdxNext; + for(pIdx=pTab->pIdx; pIdx; pIdx=pIdxNext){ + pIdxNext = pIdx->pNext; + cbFree(pIdx); + } + cbFree(pTab); +} + +/* +** Find or create the BatchTable object named zTab. +*/ +static int cbFindTable( + sqlite3_changebatch *p, + const char *zTab, + BatchTable **ppTab +){ + BatchTable *pRet = 0; + int rc = SQLITE_OK; + + for(pRet=p->pTab; pRet; pRet=pRet->pNext){ + if( 0==sqlite3_stricmp(zTab, pRet->zTab) ) break; + } + + if( pRet==0 ){ + int nTab = strlen(zTab); + pRet = (BatchTable*)cbMalloc(&rc, nTab + sizeof(BatchTable)); + if( pRet ){ + sqlite3_stmt *pIndexList = 0; + char *zIndexList = 0; + int rc2; + memcpy(pRet->zTab, zTab, nTab); + + zIndexList = sqlite3_mprintf("PRAGMA main.index_list = %Q", zTab); + if( zIndexList==0 ){ + rc = SQLITE_NOMEM; + }else{ + rc = sqlite3_prepare_v2(p->db, zIndexList, -1, &pIndexList, 0); + sqlite3_free(zIndexList); + } + + while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pIndexList) ){ + if( sqlite3_column_int(pIndexList, 2) ){ + const char *zIdx = (const char*)sqlite3_column_text(pIndexList, 1); + const char *zTyp = (const char*)sqlite3_column_text(pIndexList, 3); + rc = cbAddIndex(p, pRet, zIdx, (zTyp[0]=='p')); + } + } + rc2 = sqlite3_finalize(pIndexList); + if( rc==SQLITE_OK ) rc = rc2; + + if( rc==SQLITE_OK ){ + pRet->pNext = p->pTab; + p->pTab = pRet; + }else{ + cbFreeTable(pRet); + pRet = 0; + } + } + } + + *ppTab = pRet; + return rc; +} + +/* +** Extract value iVal from the changeset iterator passed as the first +** argument. Set *ppVal to point to the value before returning. +** +** This function attempts to extract the value using function xVal +** (which is always either sqlite3changeset_new or sqlite3changeset_old). +** If the call returns SQLITE_OK but does not supply an sqlite3_value* +** pointer, an attempt to extract the value is made using the xFallback +** function. +*/ +static int cbGetChangesetValue( + sqlite3_changeset_iter *pIter, + int (*xVal)(sqlite3_changeset_iter*,int,sqlite3_value**), + int (*xFallback)(sqlite3_changeset_iter*,int,sqlite3_value**), + int iVal, + sqlite3_value **ppVal +){ + int rc = xVal(pIter, iVal, ppVal); + if( rc==SQLITE_OK && *ppVal==0 && xFallback ){ + rc = xFallback(pIter, iVal, ppVal); + } + return rc; +} + +static int cbAddToHash( + sqlite3_changebatch *p, + sqlite3_changeset_iter *pIter, + BatchIndex *pIdx, + int (*xVal)(sqlite3_changeset_iter*,int,sqlite3_value**), + int (*xFallback)(sqlite3_changeset_iter*,int,sqlite3_value**), + int *pbConf +){ + BatchIndexEntry *pNew; + int sz = pIdx->nCol; + int i; + int iOut = 0; + int rc = SQLITE_OK; + + for(i=0; rc==SQLITE_OK && inCol; i++){ + sqlite3_value *pVal; + rc = cbGetChangesetValue(pIter, xVal, xFallback, pIdx->aiCol[i], &pVal); + if( rc==SQLITE_OK ){ + int eType = 0; + if( pVal ) eType = sqlite3_value_type(pVal); + switch( eType ){ + case 0: + case SQLITE_NULL: + return SQLITE_OK; + + case SQLITE_INTEGER: + sz += 8; + break; + case SQLITE_FLOAT: + sz += 8; + break; + + default: + assert( eType==SQLITE_TEXT || eType==SQLITE_BLOB ); + sz += sqlite3_value_bytes(pVal); + break; + } + } + } + + pNew = cbMalloc(&rc, sizeof(BatchIndexEntry) + sz); + if( pNew ){ + pNew->iChangesetId = p->iChangesetId; + pNew->iIdxId = pIdx->iId; + pNew->szRecord = sz; + + for(i=0; inCol; i++){ + int eType; + sqlite3_value *pVal; + rc = cbGetChangesetValue(pIter, xVal, xFallback, pIdx->aiCol[i], &pVal); + if( rc!=SQLITE_OK ) break; /* coverage: condition is never true */ + eType = sqlite3_value_type(pVal); + pNew->aRecord[iOut++] = eType; + switch( eType ){ + case SQLITE_INTEGER: { + sqlite3_int64 i64 = sqlite3_value_int64(pVal); + memcpy(&pNew->aRecord[iOut], &i64, 8); + iOut += 8; + break; + } + case SQLITE_FLOAT: { + double d64 = sqlite3_value_double(pVal); + memcpy(&pNew->aRecord[iOut], &d64, sizeof(double)); + iOut += sizeof(double); + break; + } + + default: { + int nByte = sqlite3_value_bytes(pVal); + const char *z = (const char*)sqlite3_value_blob(pVal); + memcpy(&pNew->aRecord[iOut], z, nByte); + iOut += nByte; + break; + } + } + } + } + + if( rc==SQLITE_OK && p->nEntry>=(p->nHash/2) ){ + rc = cbHashResize(p); + } + + if( rc==SQLITE_OK ){ + BatchIndexEntry *pIter; + int iHash = cbHash(p, pNew); + + assert( iHash>=0 && iHashnHash ); + for(pIter=p->apHash[iHash]; pIter; pIter=pIter->pNext){ + if( pNew->szRecord==pIter->szRecord + && 0==memcmp(pNew->aRecord, pIter->aRecord, pNew->szRecord) + ){ + if( pNew->iChangesetId!=pIter->iChangesetId ){ + *pbConf = 1; + } + cbFree(pNew); + pNew = 0; + break; + } + } + + if( pNew ){ + pNew->pNext = p->apHash[iHash]; + p->apHash[iHash] = pNew; + p->nEntry++; + } + }else{ + cbFree(pNew); + } + + return rc; +} + + +/* +** Add a changeset to the current batch. +*/ +int sqlite3changebatch_add(sqlite3_changebatch *p, void *pBuf, int nBuf){ + sqlite3_changeset_iter *pIter; /* Iterator opened on pBuf/nBuf */ + int rc; /* Return code */ + int bConf = 0; /* Conflict was detected */ + + rc = sqlite3changeset_start(&pIter, nBuf, pBuf); + if( rc==SQLITE_OK ){ + int rc2; + for(rc2 = sqlite3changeset_next(pIter); + rc2==SQLITE_ROW; + rc2 = sqlite3changeset_next(pIter) + ){ + BatchTable *pTab; + BatchIndex *pIdx; + const char *zTab; /* Table this change applies to */ + int nCol; /* Number of columns in table */ + int op; /* UPDATE, INSERT or DELETE */ + + sqlite3changeset_op(pIter, &zTab, &nCol, &op, 0); + assert( op==SQLITE_INSERT || op==SQLITE_UPDATE || op==SQLITE_DELETE ); + + rc = cbFindTable(p, zTab, &pTab); + assert( pTab || rc!=SQLITE_OK ); + if( pTab ){ + for(pIdx=pTab->pIdx; pIdx && rc==SQLITE_OK; pIdx=pIdx->pNext){ + if( op==SQLITE_UPDATE && pIdx->bPk ) continue; + if( op==SQLITE_UPDATE || op==SQLITE_DELETE ){ + rc = cbAddToHash(p, pIter, pIdx, sqlite3changeset_old, 0, &bConf); + } + if( op==SQLITE_UPDATE || op==SQLITE_INSERT ){ + rc = cbAddToHash(p, pIter, pIdx, + sqlite3changeset_new, sqlite3changeset_old, &bConf + ); + } + } + } + if( rc!=SQLITE_OK ) break; + } + + rc2 = sqlite3changeset_finalize(pIter); + if( rc==SQLITE_OK ) rc = rc2; + } + + if( rc==SQLITE_OK && bConf ){ + rc = SQLITE_CONSTRAINT; + } + p->iChangesetId++; + return rc; +} + +/* +** Zero an existing changebatch object. +*/ +void sqlite3changebatch_zero(sqlite3_changebatch *p){ + int i; + for(i=0; inHash; i++){ + BatchIndexEntry *pEntry; + BatchIndexEntry *pNext; + for(pEntry=p->apHash[i]; pEntry; pEntry=pNext){ + pNext = pEntry->pNext; + cbFree(pEntry); + } + } + cbFree(p->apHash); + p->nHash = 0; + p->apHash = 0; +} + +/* +** Delete a changebatch object. +*/ +void sqlite3changebatch_delete(sqlite3_changebatch *p){ + BatchTable *pTab; + BatchTable *pTabNext; + + sqlite3changebatch_zero(p); + for(pTab=p->pTab; pTab; pTab=pTabNext){ + pTabNext = pTab->pNext; + cbFreeTable(pTab); + } + cbFree(p); +} + +/* +** Return the db handle. +*/ +sqlite3 *sqlite3changebatch_db(sqlite3_changebatch *p){ + return p->db; +} + +#endif /* SQLITE_ENABLE_SESSION && SQLITE_ENABLE_PREUPDATE_HOOK */ diff --git a/ext/session/sqlite3changebatch.h b/ext/session/sqlite3changebatch.h new file mode 100644 index 0000000000..3476629e58 --- /dev/null +++ b/ext/session/sqlite3changebatch.h @@ -0,0 +1,82 @@ + +#if !defined(SQLITECHANGEBATCH_H_) +#define SQLITECHANGEBATCH_H_ 1 + +typedef struct sqlite3_changebatch sqlite3_changebatch; + +/* +** Create a new changebatch object for detecting conflicts between +** changesets associated with a schema equivalent to that of the "main" +** database of the open database handle db passed as the first +** parameter. It is the responsibility of the caller to ensure that +** the database handle is not closed until after the changebatch +** object has been deleted. +** +** A changebatch object is used to detect batches of non-conflicting +** changesets. Changesets that do not conflict may be applied to the +** target database in any order without affecting the final state of +** the database. +** +** The changebatch object only works reliably if PRIMARY KEY and UNIQUE +** constraints on tables affected by the changesets use collation +** sequences that are equivalent to built-in collation sequence +** BINARY for the == operation. +** +** If successful, SQLITE_OK is returned and (*pp) set to point to +** the new changebatch object. If an error occurs, an SQLite error +** code is returned and the final value of (*pp) is undefined. +*/ +int sqlite3changebatch_new(sqlite3 *db, sqlite3_changebatch **pp); + +/* +** Argument p points to a buffer containing a changeset n bytes in +** size. Assuming no error occurs, this function returns SQLITE_OK +** if the changeset does not conflict with any changeset passed +** to an sqlite3changebatch_add() call made on the same +** sqlite3_changebatch* handle since the most recent call to +** sqlite3changebatch_zero(). If the changeset does conflict with +** an earlier such changeset, SQLITE_CONSTRAINT is returned. Or, +** if an error occurs, some other SQLite error code may be returned. +** +** One changeset is said to conflict with another if +** either: +** +** * the two changesets contain operations (INSERT, UPDATE or +** DELETE) on the same row, identified by primary key, or +** +** * the two changesets contain operations (INSERT, UPDATE or +** DELETE) on rows with identical values in any combination +** of fields constrained by a UNIQUE constraint. +** +** Even if this function returns SQLITE_CONFLICT, the current +** changeset is added to the internal data structures - so future +** calls to this function may conflict with it. If this function +** returns any result code other than SQLITE_OK or SQLITE_CONFLICT, +** the result of any future call to sqlite3changebatch_add() is +** undefined. +** +** Only changesets may be passed to this function. Passing a +** patchset to this function results in an SQLITE_MISUSE error. +*/ +int sqlite3changebatch_add(sqlite3_changebatch*, void *p, int n); + +/* +** Zero a changebatch object. This causes the records of all earlier +** calls to sqlite3changebatch_add() to be discarded. +*/ +void sqlite3changebatch_zero(sqlite3_changebatch*); + +/* +** Return a copy of the first argument passed to the sqlite3changebatch_new() +** call used to create the changebatch object passed as the only argument +** to this function. +*/ +sqlite3 *sqlite3changebatch_db(sqlite3_changebatch*); + +/* +** Delete a changebatch object. +*/ +void sqlite3changebatch_delete(sqlite3_changebatch*); + +#endif /* !defined(SQLITECHANGEBATCH_H_) */ + diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c index 89a52b420a..a54c87ccd9 100644 --- a/ext/session/sqlite3session.c +++ b/ext/session/sqlite3session.c @@ -27,6 +27,13 @@ typedef struct SessionInput SessionInput; #define SESSIONS_ROWID "_rowid_" +/* +** The three different types of changesets generated. +*/ +#define SESSIONS_PATCHSET 0 +#define SESSIONS_CHANGESET 1 +#define SESSIONS_FULLCHANGESET 2 + static int sessions_strm_chunk_size = SESSIONS_STRM_CHUNK_SIZE; typedef struct SessionHook SessionHook; @@ -2702,7 +2709,7 @@ static void sessionAppendCol( */ static int sessionAppendUpdate( SessionBuffer *pBuf, /* Buffer to append to */ - int bPatchset, /* True for "patchset", 0 for "changeset" */ + int ePatchset, /* True for "patchset", 0 for "changeset" */ sqlite3_stmt *pStmt, /* Statement handle pointing at new row */ SessionChange *p, /* Object containing old values */ u8 *abPK /* Boolean array - true for PK columns */ @@ -2766,8 +2773,8 @@ static int sessionAppendUpdate( /* Add a field to the old.* record. This is omitted if this module is ** currently generating a patchset. */ - if( bPatchset==0 ){ - if( bChanged || abPK[i] ){ + if( ePatchset!=SESSIONS_PATCHSET ){ + if( ePatchset==SESSIONS_FULLCHANGESET || bChanged || abPK[i] ){ sessionAppendBlob(pBuf, pCsr, nAdvance, &rc); }else{ sessionAppendByte(pBuf, 0, &rc); @@ -2776,7 +2783,7 @@ static int sessionAppendUpdate( /* Add a field to the new.* record. Or the only record if currently ** generating a patchset. */ - if( bChanged || (bPatchset && abPK[i]) ){ + if( bChanged || (ePatchset==SESSIONS_PATCHSET && abPK[i]) ){ sessionAppendCol(&buf2, pStmt, i, &rc); }else{ sessionAppendByte(&buf2, 0, &rc); @@ -2802,7 +2809,7 @@ static int sessionAppendUpdate( */ static int sessionAppendDelete( SessionBuffer *pBuf, /* Buffer to append to */ - int bPatchset, /* True for "patchset", 0 for "changeset" */ + int eChangeset, /* One of SESSIONS_CHANGESET etc. */ SessionChange *p, /* Object containing old values */ int nCol, /* Number of columns in table */ u8 *abPK /* Boolean array - true for PK columns */ @@ -2812,7 +2819,7 @@ static int sessionAppendDelete( sessionAppendByte(pBuf, SQLITE_DELETE, &rc); sessionAppendByte(pBuf, p->bIndirect, &rc); - if( bPatchset==0 ){ + if( eChangeset!=SESSIONS_PATCHSET ){ sessionAppendBlob(pBuf, p->aRecord, p->nRecord, &rc); }else{ int i; @@ -3059,12 +3066,12 @@ static int sessionSelectBind( */ static void sessionAppendTableHdr( SessionBuffer *pBuf, /* Append header to this buffer */ - int bPatchset, /* Use the patchset format if true */ + int ePatchset, /* Use the patchset format if true */ SessionTable *pTab, /* Table object to append header for */ int *pRc /* IN/OUT: Error code */ ){ /* Write a table header */ - sessionAppendByte(pBuf, (bPatchset ? 'P' : 'T'), pRc); + sessionAppendByte(pBuf, (ePatchset==SESSIONS_PATCHSET) ? 'P' : 'T', pRc); sessionAppendVarint(pBuf, pTab->nCol, pRc); sessionAppendBlob(pBuf, pTab->abPK, pTab->nCol, pRc); sessionAppendBlob(pBuf, (u8 *)pTab->zName, (int)strlen(pTab->zName)+1, pRc); @@ -3082,7 +3089,7 @@ static void sessionAppendTableHdr( */ static int sessionGenerateChangeset( sqlite3_session *pSession, /* Session object */ - int bPatchset, /* True for patchset, false for changeset */ + int ePatchset, /* One of SESSIONS_CHANGESET etc. */ int (*xOutput)(void *pOut, const void *pData, int nData), void *pOut, /* First argument for xOutput */ int *pnChangeset, /* OUT: Size of buffer at *ppChangeset */ @@ -3130,7 +3137,7 @@ static int sessionGenerateChangeset( } /* Write a table header */ - sessionAppendTableHdr(&buf, bPatchset, pTab, &rc); + sessionAppendTableHdr(&buf, ePatchset, pTab, &rc); /* Build and compile a statement to execute: */ if( rc==SQLITE_OK ){ @@ -3156,10 +3163,10 @@ static int sessionGenerateChangeset( } }else{ assert( pTab->abPK!=0 ); - rc = sessionAppendUpdate(&buf, bPatchset, pSel, p, pTab->abPK); + rc = sessionAppendUpdate(&buf, ePatchset, pSel, p, pTab->abPK); } }else if( p->op!=SQLITE_INSERT ){ - rc = sessionAppendDelete(&buf, bPatchset, p, pTab->nCol,pTab->abPK); + rc = sessionAppendDelete(&buf, ePatchset, p, pTab->nCol,pTab->abPK); } if( rc==SQLITE_OK ){ rc = sqlite3_reset(pSel); @@ -3218,7 +3225,8 @@ int sqlite3session_changeset( int rc; if( pnChangeset==0 || ppChangeset==0 ) return SQLITE_MISUSE; - rc = sessionGenerateChangeset(pSession, 0, 0, 0, pnChangeset, ppChangeset); + rc = sessionGenerateChangeset( + pSession, SESSIONS_CHANGESET, 0, 0, pnChangeset, ppChangeset); assert( rc || pnChangeset==0 || pSession->bEnableSize==0 || *pnChangeset<=pSession->nMaxChangesetSize ); @@ -3234,7 +3242,8 @@ int sqlite3session_changeset_strm( void *pOut ){ if( xOutput==0 ) return SQLITE_MISUSE; - return sessionGenerateChangeset(pSession, 0, xOutput, pOut, 0, 0); + return sessionGenerateChangeset( + pSession, SESSIONS_CHANGESET, xOutput, pOut, 0, 0); } /* @@ -3246,7 +3255,8 @@ int sqlite3session_patchset_strm( void *pOut ){ if( xOutput==0 ) return SQLITE_MISUSE; - return sessionGenerateChangeset(pSession, 1, xOutput, pOut, 0, 0); + return sessionGenerateChangeset( + pSession, SESSIONS_PATCHSET, xOutput, pOut, 0, 0); } /* @@ -3262,9 +3272,20 @@ int sqlite3session_patchset( void **ppPatchset /* OUT: Buffer containing changeset */ ){ if( pnPatchset==0 || ppPatchset==0 ) return SQLITE_MISUSE; - return sessionGenerateChangeset(pSession, 1, 0, 0, pnPatchset, ppPatchset); + return sessionGenerateChangeset( + pSession, SESSIONS_PATCHSET, 0, 0, pnPatchset, ppPatchset); +} + +int sqlite3session_fullchangeset( + sqlite3_session *pSession, /* Session object */ + int *pnChangeset, /* OUT: Size of buffer at *ppChangeset */ + void **ppChangeset /* OUT: Buffer containing changeset */ +){ + return sessionGenerateChangeset( + pSession, SESSIONS_FULLCHANGESET, 0, 0, pnChangeset, ppChangeset); } + /* ** Enable or disable the session object passed as the first argument. */ @@ -6609,10 +6630,11 @@ static int sessionChangegroupOutput( ** hash tables attached to the SessionTable objects in list p->pList. */ for(pTab=pGrp->pList; rc==SQLITE_OK && pTab; pTab=pTab->pNext){ + int eChangeset = pGrp->bPatch ? SESSIONS_PATCHSET : SESSIONS_CHANGESET; int i; if( pTab->nEntry==0 ) continue; - sessionAppendTableHdr(&buf, pGrp->bPatch, pTab, &rc); + sessionAppendTableHdr(&buf, eChangeset, pTab, &rc); for(i=0; inChange; i++){ SessionChange *p; for(p=pTab->apChange[i]; p; p=p->pNext){ diff --git a/ext/session/sqlite3session.h b/ext/session/sqlite3session.h index 045a1ffeef..854f5b51de 100644 --- a/ext/session/sqlite3session.h +++ b/ext/session/sqlite3session.h @@ -369,6 +369,19 @@ int sqlite3session_changeset( void **ppChangeset /* OUT: Buffer containing changeset */ ); +/* +** CAPI3REF: Generate A Full Changeset From A Session Object +** +** This function is similar to sqlite3session_changeset(), except that for +** each row affected by an UPDATE statement, all old.* values are recorded +** as part of the changeset, not just those modified. +*/ +int sqlite3session_fullchangeset( + sqlite3_session *pSession, /* Session object */ + int *pnChangeset, /* OUT: Size of buffer at *ppChangeset */ + void **ppChangeset /* OUT: Buffer containing changeset */ +); + /* ** CAPI3REF: Return An Upper-limit For The Size Of The Changeset ** METHOD: sqlite3_session diff --git a/ext/session/test_session.c b/ext/session/test_session.c index 40680ead74..a5a146e48f 100644 --- a/ext/session/test_session.c +++ b/ext/session/test_session.c @@ -233,6 +233,7 @@ static int testStreamOutput( ** $session indirect INTEGER ** $session patchset ** $session table_filter SCRIPT +** $session fullchangeset */ static int SQLITE_TCLAPI test_session_cmd( void *clientData, @@ -246,20 +247,20 @@ static int SQLITE_TCLAPI test_session_cmd( const char *zSub; int nArg; const char *zMsg; - int iSub; } aSub[] = { - { "attach", 1, "TABLE", }, /* 0 */ - { "changeset", 0, "", }, /* 1 */ - { "delete", 0, "", }, /* 2 */ - { "enable", 1, "BOOL", }, /* 3 */ - { "indirect", 1, "BOOL", }, /* 4 */ - { "isempty", 0, "", }, /* 5 */ - { "table_filter", 1, "SCRIPT", }, /* 6 */ + { "attach", 1, "TABLE" }, /* 0 */ + { "changeset", 0, "" }, /* 1 */ + { "delete", 0, "" }, /* 2 */ + { "enable", 1, "BOOL" }, /* 3 */ + { "indirect", 1, "BOOL" }, /* 4 */ + { "isempty", 0, "" }, /* 5 */ + { "table_filter", 1, "SCRIPT" }, /* 6 */ { "patchset", 0, "", }, /* 7 */ - { "diff", 2, "FROMDB TBL", }, /* 8 */ - { "memory_used", 0, "", }, /* 9 */ - { "changeset_size", 0, "", }, /* 10 */ - { "object_config", 2, "OPTION INTEGER", }, /* 11 */ + { "diff", 2, "FROMDB TBL" }, /* 8 */ + { "fullchangeset",0, "" }, /* 9 */ + { "memory_used", 0, "", }, /* 10 */ + { "changeset_size", 0, "", }, /* 11 */ + { "object_config", 2, "OPTION INTEGER", }, /* 12 */ { 0 } }; int iSub; @@ -289,10 +290,11 @@ static int SQLITE_TCLAPI test_session_cmd( break; } + case 9: /* fullchangeset */ case 7: /* patchset */ case 1: { /* changeset */ TestSessionsBlob o = {0, 0}; - if( test_tcl_integer(interp, SESSION_STREAM_TCL_VAR) ){ + if( iSub!=9 && test_tcl_integer(interp, SESSION_STREAM_TCL_VAR) ){ void *pCtx = (void*)&o; if( iSub==7 ){ rc = sqlite3session_patchset_strm(pSession, testStreamOutput, pCtx); @@ -302,6 +304,8 @@ static int SQLITE_TCLAPI test_session_cmd( }else{ if( iSub==7 ){ rc = sqlite3session_patchset(pSession, &o.n, &o.p); + }else if( iSub==9 ){ + rc = sqlite3session_fullchangeset(pSession, &o.n, &o.p); }else{ rc = sqlite3session_changeset(pSession, &o.n, &o.p); } @@ -317,6 +321,7 @@ static int SQLITE_TCLAPI test_session_cmd( break; } + case 2: /* delete */ Tcl_DeleteCommand(interp, Tcl_GetString(objv[0])); break; @@ -367,18 +372,18 @@ static int SQLITE_TCLAPI test_session_cmd( break; } - case 9: { /* memory_used */ + case 10: { /* memory_used */ sqlite3_int64 nMalloc = sqlite3session_memory_used(pSession); Tcl_SetObjResult(interp, Tcl_NewWideIntObj(nMalloc)); break; } - case 10: { + case 11: { sqlite3_int64 nSize = sqlite3session_changeset_size(pSession); Tcl_SetObjResult(interp, Tcl_NewWideIntObj(nSize)); break; } - case 11: { /* object_config */ + case 12: { /* object_config */ struct ObjConfOpt { const char *zName; int opt; @@ -388,7 +393,6 @@ static int SQLITE_TCLAPI test_session_cmd( { 0, 0 } }; int sz = (int)sizeof(aOpt[0]); - int iArg; Tcl_Size iOpt; if( Tcl_GetIndexFromObjStruct(interp,objv[2],aOpt,sz,"option",0,&iOpt) ){ @@ -626,7 +630,7 @@ static int test_filter_v3_handler( Tcl_DecrRefCount(pEval); return res; -} +} static int test_conflict_handler( void *pCtx, /* Pointer to TestConflictHandler structure */ @@ -1295,6 +1299,127 @@ static int SQLITE_TCLAPI test_sqlite3session_foreach( return TCL_OK; } +#include "sqlite3changebatch.h" + +typedef struct TestChangebatch TestChangebatch; +struct TestChangebatch { + sqlite3_changebatch *pChangebatch; +}; + +/* +** Tclcmd: $changebatch add BLOB +** $changebatch zero +** $changebatch delete +*/ +static int SQLITE_TCLAPI test_changebatch_cmd( + void *clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + TestChangebatch *p = (TestChangebatch*)clientData; + sqlite3_changebatch *pChangebatch = p->pChangebatch; + struct SessionSubcmd { + const char *zSub; + int nArg; + const char *zMsg; + int iSub; + } aSub[] = { + { "add", 1, "CHANGESET", }, /* 0 */ + { "zero", 0, "", }, /* 1 */ + { "delete", 0, "", }, /* 2 */ + { 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: { /* add */ + Tcl_Size nArg; + unsigned char *pArg = Tcl_GetByteArrayFromObj(objv[2], &nArg); + rc = sqlite3changebatch_add(pChangebatch, pArg, (int)nArg); + if( rc!=SQLITE_OK && rc!=SQLITE_CONSTRAINT ){ + return test_session_error(interp, rc, 0); + }else{ + extern const char *sqlite3ErrName(int); + Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1)); + } + break; + } + + case 1: { /* zero */ + sqlite3changebatch_zero(pChangebatch); + break; + } + + case 2: /* delete */ + Tcl_DeleteCommand(interp, Tcl_GetString(objv[0])); + break; + } + + return TCL_OK; +} + +static void SQLITE_TCLAPI test_changebatch_del(void *clientData){ + TestChangebatch *p = (TestChangebatch*)clientData; + sqlite3changebatch_delete(p->pChangebatch); + ckfree((char*)p); +} + +/* +** Tclcmd: sqlite3changebatch CMD DB-HANDLE +*/ +static int SQLITE_TCLAPI test_sqlite3changebatch( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + sqlite3 *db; + Tcl_CmdInfo info; + int rc; /* sqlite3session_create() return code */ + TestChangebatch *p; /* New wrapper object */ + + if( objc!=3 ){ + Tcl_WrongNumArgs(interp, 1, objv, "CMD DB-HANDLE"); + return TCL_ERROR; + } + + if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(objv[2]), &info) ){ + Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(objv[2]), 0); + return TCL_ERROR; + } + db = *(sqlite3 **)info.objClientData; + + p = (TestChangebatch*)ckalloc(sizeof(TestChangebatch)); + memset(p, 0, sizeof(TestChangebatch)); + rc = sqlite3changebatch_new(db, &p->pChangebatch); + if( rc!=SQLITE_OK ){ + ckfree((char*)p); + return test_session_error(interp, rc, 0); + } + + Tcl_CreateObjCommand( + interp, Tcl_GetString(objv[1]), test_changebatch_cmd, (ClientData)p, + test_changebatch_del + ); + Tcl_SetObjResult(interp, objv[1]); + return TCL_OK; +} + /* ** tclcmd: CMD configure REBASE-BLOB ** tclcmd: CMD rebase CHANGESET @@ -2085,6 +2210,10 @@ int TestSession_Init(Tcl_Interp *interp){ Tcl_CreateObjCommand(interp, p->zCmd, p->xProc, 0, 0); } + + Tcl_CreateObjCommand( + interp, "sqlite3changebatch", test_sqlite3changebatch, 0, 0 + ); return TCL_OK; } diff --git a/ext/wasm/api/sqlite3-opfs-async-proxy.c-pp.js b/ext/wasm/api/sqlite3-opfs-async-proxy.c-pp.js index 286db83bac..a3a86067d4 100644 --- a/ext/wasm/api/sqlite3-opfs-async-proxy.c-pp.js +++ b/ext/wasm/api/sqlite3-opfs-async-proxy.c-pp.js @@ -49,36 +49,6 @@ versions (approximately) 104-107 are extinct) we should change our usage of those methods to remove the "await". */ -//#if 0 -/** - 2026-04-04: this file gets included by both the "opfs" and "opfs-wl" - VFSes. It would, in hindsight, hypothetically be possible to restructure - it very slightly to support both VFSes via a single Worker instance. - - Some of the changes we would need for that: - - - The xLock/xUnlock "op codes" would need to differ for each impl. - i.e. we'd need state.opIds.xLock{,WL} and state.opIds.xUnlock{,WL} - to distinguish between the two, rather than doing so when this Worker - is loaded. - - - We would need to centralize loading of this Worker, outside of - the VFS-specific pieces, and change the handshake in order to be - able to distinguish between clients which support - Atomics.waitAsync() and those which do not ("opfs-wl" requires - waitAsync()). - - One down-side would be for clients which, for whatever reason, want - to use both "opfs" and "opfs-wl" within the same session: because - both would go through the same Worker, any operations for one VFS - would, while they're being processed on this side of the proxy, - effectively block the other VFS from doing anything, potentially - deadlocking. This use case seems unlikely enough that it can - possibly be ruled out (or even reasonably flat-out prohibited by - the library). -*/ -//#/if - "use strict"; const urlParams = new URL(globalThis.location.href).searchParams; const vfsName = urlParams.get('vfs'); @@ -112,7 +82,7 @@ const installAsyncProxy = function(){ copies in the synchronous and async halves. This side does not load the SQLite library, so does not have access to that copy. */ //#define opfs-async-proxy -//#include "api/opfs-common-inline.c-pp.js" +//#include api/opfs-common-inline.c-pp.js //#undef opfs-async-proxy /** @@ -708,9 +678,9 @@ const installAsyncProxy = function(){ const lockName = "sqlite3-vfs-opfs:" + fh.filenameAbs; const oldLockType = fh.xLock; return new Promise((resolveWaitLoop) => { - //log("xLock() initial promise entered..."); + //error("xLock() initial promise entered..."); navigator.locks.request(lockName, { mode: requestedMode }, async (lock) => { - //log("xLock() Web Lock entered.", fh); + //error("xLock() Web Lock entered.", fh); __implicitLocks.delete(fid); let rc = 0; try{ @@ -756,7 +726,7 @@ const installAsyncProxy = function(){ storeAndNotify('xUnlock', rc); return rc; } - //log("xUnlock()",fid, lockType, fh); + //error("xUnlock()",fid, lockType, fh); let rc = 0; if( lockType === state.sq3Codes.SQLITE_LOCK_NONE ){ /* SQLite usually unlocks all the way to NONE */ @@ -766,7 +736,7 @@ const installAsyncProxy = function(){ fh.xLock = lockType; }else if( lockType === state.sq3Codes.SQLITE_LOCK_SHARED && existing.mode === 'exclusive' ){ - /* downgrade EXCLUSIVE -> SHARED */ + /* downgrade Exclusive -> Shared */ rc = await wlCloseHandle(fh); if( 0===rc ){ fh.xLock = lockType; @@ -912,6 +882,16 @@ const installAsyncProxy = function(){ operation */ ) || []; //error("waitLoop() whichOp =",opId, f.opHandlers[opId].key, args); +//#if 0 + if( isWebLocker && (opId==opIds.xLock || opIds==opIds.xUnlock) ){ + /* An expert suggests that this introduces a race condition, + but my eyes aren't seeing it. The hope was that this + would improve the lock speed a tick, but it does not + appear to. */ + hnd(...args); + continue; + } +//#/if await hnd(...args); }catch(e){ error('in waitLoop():', e); diff --git a/ext/wasm/api/sqlite3-worker1-promiser.c-pp.js b/ext/wasm/api/sqlite3-worker1-promiser.c-pp.js index bcbf3fa9f8..55e497ead5 100644 --- a/ext/wasm/api/sqlite3-worker1-promiser.c-pp.js +++ b/ext/wasm/api/sqlite3-worker1-promiser.c-pp.js @@ -1,4 +1,4 @@ -//#if not omit-oo1 +//#ifnot omit-oo1 /* 2022-08-24 @@ -19,12 +19,10 @@ slightly simpler client-side interface than the slightly-lower-level Worker API does. - In non-ESM builds this file necessarily exposes one global symbol, - but clients may freely `delete` that symbol after calling it. + This script necessarily exposes one global symbol, but clients may + freely `delete` that symbol after calling it. */ -//#if not defined target:es6-module 'use strict'; -//#/if /** Configures an sqlite3 Worker API #1 Worker such that it can be manipulated via a Promise-based interface and returns a factory @@ -111,12 +109,10 @@ the callback is called one time for each row of the result set, passed the same worker message format as the worker API emits: - { - type:typeString, + {type:typeString, row:VALUE, rowNumber:1-based-#, - columnNames: array - } + columnNames: array} Where `typeString` is an internally-synthesized message type string used temporarily for worker message dispatching. It can be ignored @@ -127,16 +123,19 @@ callback. At the end of the result set, the same event is fired with - (row=undefined, rowNumber=null) to indicate that the end of the - result set has been reached. The rows arrive via worker-posted - messages, with all the implications of that. + (row=undefined, rowNumber=null) to indicate that + the end of the result set has been reached. Note that the rows + arrive via worker-posted messages, with all the implications + of that. Notable shortcomings: - - "v1" of this this API is not suitable for use as an ESM module - because ESM worker modules were not widely supported when it was - developed. For use as an ESM module, see the "v2" interface later - on in this file. + - This API was not designed with ES6 modules in mind. Neither Firefox + nor Safari support, as of March 2023, the {type:"module"} flag to the + Worker constructor, so that particular usage is not something we're going + to target for the time being: + + https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker */ globalThis.sqlite3Worker1Promiser = function callee(config = callee.defaultConfig){ // Inspired by: https://stackoverflow.com/a/52439530 @@ -255,14 +254,10 @@ globalThis.sqlite3Worker1Promiser = function callee(config = callee.defaultConfi globalThis.sqlite3Worker1Promiser.defaultConfig = { worker: function(){ -//#if target:es6-bundler-friendly +//#if target=es6-module return new Worker(new URL("sqlite3-worker1-bundler-friendly.mjs", import.meta.url),{ type: 'module' }); -//#elif target:es6-module - return new Worker(new URL("sqlite3-worker1.mjs", import.meta.url),{ - type: 'module' - }); //#else let theJs = "sqlite3-worker1.js"; if(this.currentScript){ @@ -278,15 +273,15 @@ globalThis.sqlite3Worker1Promiser.defaultConfig = { } } return new Worker(theJs + globalThis.location.search); -//#/if +//#endif } -//#if not target:es6-module +//#ifnot target=es6-module .bind({ currentScript: globalThis?.document?.currentScript }) -//#/if +//#endif , - onerror: (...args)=>console.error('sqlite3Worker1Promiser():',...args) + onerror: (...args)=>console.error('worker1 promiser error',...args) }/*defaultConfig*/; /** @@ -301,7 +296,7 @@ globalThis.sqlite3Worker1Promiser.defaultConfig = { after calling the original function and will reject if that function throws. */ -globalThis.sqlite3Worker1Promiser.v2 = function callee(config = callee.defaultConfig){ +sqlite3Worker1Promiser.v2 = function(config){ let oldFunc; if( 'function' == typeof config ){ oldFunc = config; @@ -331,25 +326,21 @@ globalThis.sqlite3Worker1Promiser.v2 = function callee(config = callee.defaultCo } return p; }.bind({ - /* We do this because clients are recommended to delete - globalThis.sqlite3Worker1Promiser. */ + /* We do this because clients are + recommended to delete globalThis.sqlite3Worker1Promiser. */ original: sqlite3Worker1Promiser }); -globalThis.sqlite3Worker1Promiser.v2.defaultConfig = - globalThis.sqlite3Worker1Promiser.defaultConfig; - -//#if target:es6-module +//#if target=es6-module /** When built as a module, we export sqlite3Worker1Promiser.v2() instead of sqlite3Worker1Promise() because (A) its interface is more - conventional for ESM usage and (B) the ESM export option for this - API did not exist until v2 was created, so there's no backwards + conventional for ESM usage and (B) the ESM option export option for + this API did not exist until v2 was created, so there's no backwards incompatibility. */ export default sqlite3Worker1Promiser.v2; -delete globalThis.sqlite3Worker1Promiser; -//#/if /* target:es6-module */ +//#endif /* target=es6-module */ //#else /* Built with the omit-oo1 flag. */ -//#/if if not omit-oo1 +//#endif ifnot omit-oo1 diff --git a/ext/wasm/demo-worker1-promiser.c-pp.html b/ext/wasm/demo-worker1-promiser.c-pp.html index c54b46aadb..e0b487bdf3 100644 --- a/ext/wasm/demo-worker1-promiser.c-pp.html +++ b/ext/wasm/demo-worker1-promiser.c-pp.html @@ -6,11 +6,11 @@ -//#if target:es6-module - Worker1-promiser (ESM) tests +//#if target=es6-module + worker-promise (via ESM) tests //#else - Worker1-promiser tests -//#/if + worker-promise tests +//#endif
worker-promise tests
@@ -32,11 +32,11 @@
-//#if target:es6-module +//#if target=es6-module //#else -//#/if +//#endif diff --git a/ext/wasm/demo-worker1-promiser.c-pp.js b/ext/wasm/demo-worker1-promiser.c-pp.js index 1521edfc17..f6fc9568ae 100644 --- a/ext/wasm/demo-worker1-promiser.c-pp.js +++ b/ext/wasm/demo-worker1-promiser.c-pp.js @@ -13,13 +13,13 @@ Demonstration of the sqlite3 Worker API #1 Promiser: a Promise-based proxy for for the sqlite3 Worker #1 API. */ -//#if target:es6-module +//#if target=es6-module import {default as promiserFactory} from "./jswasm/sqlite3-worker1-promiser.mjs"; //#else "use strict"; const promiserFactory = globalThis.sqlite3Worker1Promiser.v2; delete globalThis.sqlite3Worker1Promiser; -//#/if +//#endif (async function(){ const T = globalThis.SqliteTestUtil; const eOutput = document.querySelector('#test-output'); @@ -40,7 +40,7 @@ delete globalThis.sqlite3Worker1Promiser; }; const promiserConfig = { -//#if not target:es6-module +//#ifnot target=es6-module /** The v1 interfaces uses an onready function. The v2 interface optionally accepts one but does not require it. If provided, it is called _before_ @@ -53,7 +53,7 @@ delete globalThis.sqlite3Worker1Promiser; before workerPromise is set. */ console.warn("This is the v2 interface - you don't need an onready() function."); }, -//#/if +//#endif debug: 1 ? undefined : (...args)=>console.debug('worker debug',...args), onunhandled: function(ev){ error("Unhandled worker message:",ev.data); @@ -115,7 +115,6 @@ delete globalThis.sqlite3Worker1Promiser; "insert into t(a,b) values(1,2),(3,4),(5,6)" ].join(';'), resultRows: [], columnNames: [], - lastInsertRowId: true, countChanges: sqConfig.bigIntEnabled ? 64 : true }, function(ev){ ev = ev.result; @@ -123,9 +122,7 @@ delete globalThis.sqlite3Worker1Promiser; .assert(0===ev.columnNames.length) .assert(sqConfig.bigIntEnabled ? (3n===ev.changeCount) - : (3===ev.changeCount)) - .assert('bigint'===typeof ev.lastInsertRowId) - .assert(ev.lastInsertRowId>=3); + : (3===ev.changeCount)); }); await wtest('exec',{ diff --git a/ext/wasm/fiddle/index.c-pp.html b/ext/wasm/fiddle/index.c-pp.html index 064f888b84..1f818286b5 100644 --- a/ext/wasm/fiddle/index.c-pp.html +++ b/ext/wasm/fiddle/index.c-pp.html @@ -27,7 +27,7 @@ --> -//#/if +//#endif