diff --git a/Makefile.msc b/Makefile.msc index 220367810c..ee71956ad7 100644 --- a/Makefile.msc +++ b/Makefile.msc @@ -1578,6 +1578,7 @@ TESTEXT = \ $(TOP)\ext\misc\amatch.c \ $(TOP)\ext\misc\appendvfs.c \ $(TOP)\ext\misc\basexx.c \ + $(TOP)\ext\misc\bgckpt.c \ $(TOP)\ext\misc\cksumvfs.c \ $(TOP)\ext\misc\closure.c \ $(TOP)\ext\misc\csv.c \ 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/wal2.md b/doc/wal2.md new file mode 100644 index 0000000000..5afd8ae606 --- /dev/null +++ b/doc/wal2.md @@ -0,0 +1,98 @@ + +Wal2 Mode Notes +=============== + +## Activating/Deactivating Wal2 Mode + +"Wal2" mode is very similar to "wal" mode. To change a database to wal2 mode, +use the command: + +> + PRAGMA journal_mode = wal2; + +It is not possible to change a database directly from "wal" mode to "wal2" +mode. Instead, it must first be changed to rollback mode. So, to change a wal +mode database to wal2 mode, the following two commands may be used: + +> + PRAGMA journal_mode = delete; + PRAGMA journal_mode = wal2; + +A database in wal2 mode may only be accessed by versions of SQLite compiled +from this branch. Attempting to use any other version of SQLite results in an +SQLITE_NOTADB error. A wal2 mode database may be changed back to rollback mode +(making it accessible by all versions of SQLite) using: + +> + PRAGMA journal_mode = delete; + +## The Advantage of Wal2 Mode + +In legacy wal mode, when a writer writes data to the database, it doesn't +modify the database file directly. Instead, it appends new data to the +"<database>-wal" file. Readers read data from both the original database +file and the "<database>-wal" file. At some point, data is copied from the +"<database>-wal" file into the database file, after which the wal file can +be deleted or overwritten. Copying data from the wal file into the database +file is called a "checkpoint", and may be done explictly (either by "PRAGMA +wal_checkpoint" or sqlite3_wal_checkpoint_v2()), or +automatically (by configuring "PRAGMA wal_autocheckpoint" - this is the +default). + +Checkpointers do not block writers, and writers do not block checkpointers. +However, if a writer writes to the database while a checkpoint is ongoing, +then the new data is appended to the end of the wal file. This means that, +even following the checkpoint, the wal file cannot be overwritten or deleted, +and so all subsequent transactions must also be appended to the wal file. The +work of the checkpointer is not wasted - SQLite remembers which parts of the +wal file have already been copied into the db file so that the next checkpoint +does not have to do so again - but it does mean that the wal file may grow +indefinitely if the checkpointer never gets a chance to finish without a +writer appending to the wal file. There are also circumstances in which +long-running readers may prevent a checkpointer from checkpointing the entire +wal file - also causing the wal file to grow indefinitely in a busy system. + +Wal2 mode does not have this problem. In wal2 mode, wal files do not grow +indefinitely even if the checkpointer never has a chance to finish +uninterrupted. + +In wal2 mode, the system uses two wal files instead of one. The files are named +"<database>-wal" and "<database>-wal2", where "<database>" is of +course the name of the database file. When data is written to the database, the +writer begins by appending the new data to the first wal file. Once the first +wal file has grown large enough, writers switch to appending data to the second +wal file. At this point the first wal file can be checkpointed (after which it +can be overwritten). Then, once the second wal file has grown large enough and +the first wal file has been checkpointed, writers switch back to the first wal +file. And so on. + +## Application Programming + +From the point of view of the user, the main differences between wal and +wal2 mode are to do with checkpointing: + + * In wal mode, a checkpoint may be attempted at any time. In wal2 + mode, the checkpointer has to wait until writers have switched + to the "other" wal file before a checkpoint can take place. + + * In wal mode, the wal-hook (callback registered using + sqlite3_wal_hook()) is invoked after a transaction is committed + with the total number of pages in the wal file as an argument. In wal2 + mode, the argument is either the total number of uncheckpointed pages in + both wal files, or - if the "other" wal file is empty or already + checkpointed - 0. + +Clients are recommended to use the same strategies for checkpointing wal2 mode +databases as for wal databases - by registering a wal-hook using +sqlite3_wal_hook() and attempting a checkpoint when the parameter +exceeds a certain threshold. + +However, it should be noted that although the wal-hook is invoked after each +transaction is committed to disk and database locks released, it is still +invoked from within the sqlite3_step() call used to execute the "COMMIT" +command. In BEGIN CONCURRENT systems, where the "COMMIT" is often protected by +an application mutex, this may reduce concurrency. In such systems, instead of +executing a checkpoint from within the wal-hook, a thread might defer this +action until after the application mutex has been released. + + diff --git a/ext/misc/bgckpt.c b/ext/misc/bgckpt.c new file mode 100644 index 0000000000..cac3b84831 --- /dev/null +++ b/ext/misc/bgckpt.c @@ -0,0 +1,228 @@ +/* +** 2017-10-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. +** +****************************************************************************** +** +*/ + +#if !defined(SQLITE_TEST) || defined(SQLITE_OS_UNIX) + +#include "sqlite3.h" +#include +#include + +/* +** API declarations. +*/ +typedef struct Checkpointer Checkpointer; +int sqlite3_bgckpt_create(const char *zFilename, Checkpointer **pp); +int sqlite3_bgckpt_checkpoint(Checkpointer *p, int bBlock); +void sqlite3_bgckpt_destroy(Checkpointer *p); + + +struct Checkpointer { + sqlite3 *db; /* Database handle */ + + pthread_t thread; /* Background thread */ + pthread_mutex_t mutex; + pthread_cond_t cond; + + int rc; /* Error from "PRAGMA wal_checkpoint" */ + int bCkpt; /* True if checkpoint requested */ + int bExit; /* True if exit requested */ +}; + +static void *bgckptThreadMain(void *pCtx){ + int rc = SQLITE_OK; + Checkpointer *p = (Checkpointer*)pCtx; + + while( rc==SQLITE_OK ){ + int bExit; + + pthread_mutex_lock(&p->mutex); + if( p->bCkpt==0 && p->bExit==0 ){ + pthread_cond_wait(&p->cond, &p->mutex); + } + p->bCkpt = 0; + bExit = p->bExit; + pthread_mutex_unlock(&p->mutex); + + if( bExit ) break; + rc = sqlite3_exec(p->db, "PRAGMA wal_checkpoint", 0, 0, 0); + if( rc==SQLITE_BUSY ){ + rc = SQLITE_OK; + } + } + + pthread_mutex_lock(&p->mutex); + p->rc = rc; + pthread_mutex_unlock(&p->mutex); + return 0; +} + +void sqlite3_bgckpt_destroy(Checkpointer *p){ + if( p ){ + void *ret = 0; + + /* Signal the background thread to exit */ + pthread_mutex_lock(&p->mutex); + p->bExit = 1; + pthread_cond_broadcast(&p->cond); + pthread_mutex_unlock(&p->mutex); + + pthread_join(p->thread, &ret); + sqlite3_close(p->db); + sqlite3_free(p); + } +} + + +int sqlite3_bgckpt_create(const char *zFilename, Checkpointer **pp){ + Checkpointer *pNew = 0; + int rc; + + pNew = (Checkpointer*)sqlite3_malloc(sizeof(Checkpointer)); + if( pNew==0 ){ + rc = SQLITE_NOMEM; + }else{ + memset(pNew, 0, sizeof(Checkpointer)); + rc = sqlite3_open(zFilename, &pNew->db); + } + + if( rc==SQLITE_OK ){ + pthread_mutex_init(&pNew->mutex, 0); + pthread_cond_init(&pNew->cond, 0); + pthread_create(&pNew->thread, 0, bgckptThreadMain, (void*)pNew); + } + + if( rc!=SQLITE_OK ){ + sqlite3_bgckpt_destroy(pNew); + pNew = 0; + } + *pp = pNew; + return rc; +} + +int sqlite3_bgckpt_checkpoint(Checkpointer *p, int bBlock){ + int rc; + pthread_mutex_lock(&p->mutex); + rc = p->rc; + if( rc==SQLITE_OK ){ + p->bCkpt = 1; + pthread_cond_broadcast(&p->cond); + } + pthread_mutex_unlock(&p->mutex); + return rc; +} + +#ifdef SQLITE_TEST +#include "tclsqlite.h" + +const char *sqlite3ErrName(int rc); + +static void SQLITE_TCLAPI bgckpt_del(void * clientData){ + Checkpointer *pCkpt = (Checkpointer*)clientData; + sqlite3_bgckpt_destroy(pCkpt); +} + +/* +** Tclcmd: $ckpt SUBCMD ... +*/ +static int SQLITE_TCLAPI bgckpt_obj_cmd( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + Checkpointer *pCkpt = (Checkpointer*)clientData; + const char *aCmd[] = { "checkpoint", "destroy", 0 }; + int iCmd; + + if( objc<2 ){ + Tcl_WrongNumArgs(interp, 1, objv, "SUBCMD ..."); + return TCL_ERROR; + } + + if( Tcl_GetIndexFromObj(interp, objv[1], aCmd, "sub-command", 0, &iCmd) ){ + return TCL_ERROR; + } + + switch( iCmd ){ + case 0: { + int rc; + int bBlock = 0; + + if( objc>3 ){ + Tcl_WrongNumArgs(interp, 2, objv, "?BLOCKING?"); + return TCL_ERROR; + } + if( objc==3 && Tcl_GetBooleanFromObj(interp, objv[2], &bBlock) ){ + return TCL_ERROR; + } + + rc = sqlite3_bgckpt_checkpoint(pCkpt, bBlock); + if( rc!=SQLITE_OK ){ + Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1)); + return TCL_ERROR; + } + break; + } + + case 1: { + Tcl_DeleteCommand(interp, Tcl_GetString(objv[0])); + break; + } + } + + return TCL_OK; +} + +/* +** Tclcmd: bgckpt CMDNAME FILENAME +*/ +static int SQLITE_TCLAPI bgckpt_cmd( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + const char *zCmd; + const char *zFilename; + int rc; + Checkpointer *pCkpt; + + if( objc!=3 ){ + Tcl_WrongNumArgs(interp, 1, objv, "CMDNAME FILENAME"); + return TCL_ERROR; + } + zCmd = Tcl_GetString(objv[1]); + zFilename = Tcl_GetString(objv[2]); + + rc = sqlite3_bgckpt_create(zFilename, &pCkpt); + if( rc!=SQLITE_OK ){ + Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1)); + return TCL_ERROR; + } + + Tcl_CreateObjCommand(interp, zCmd, bgckpt_obj_cmd, (void*)pCkpt, bgckpt_del); + Tcl_SetObjResult(interp, objv[1]); + return TCL_OK; +} + +int Bgckpt_Init(Tcl_Interp *interp){ + Tcl_CreateObjCommand(interp, "bgckpt", bgckpt_cmd, 0, 0); + return TCL_OK; +} +#endif /* SQLITE_TEST */ + +#else +# include "tclsqlite.h" +int Bgckpt_Init(Tcl_Interp *interp){ return TCL_OK; } +#endif 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