summaryrefslogtreecommitdiffstats
path: root/ext/wasm
diff options
context:
space:
mode:
Diffstat (limited to 'ext/wasm')
-rw-r--r--ext/wasm/EXPORTED_FUNCTIONS.fiddle.in10
-rw-r--r--ext/wasm/GNUmakefile1182
-rw-r--r--ext/wasm/README-dist.txt46
-rw-r--r--ext/wasm/README.md106
-rw-r--r--ext/wasm/SQLTester/GNUmakefile55
-rw-r--r--ext/wasm/SQLTester/SQLTester.mjs1339
-rw-r--r--ext/wasm/SQLTester/SQLTester.run.mjs148
-rw-r--r--ext/wasm/SQLTester/index.html127
-rw-r--r--ext/wasm/SQLTester/touint8array.c29
-rw-r--r--ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api204
-rw-r--r--ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-see5
-rw-r--r--ext/wasm/api/EXPORTED_RUNTIME_METHODS.sqlite3-api3
-rw-r--r--ext/wasm/api/README.md164
-rw-r--r--ext/wasm/api/extern-post-js.c-pp.js126
-rw-r--r--ext/wasm/api/extern-pre-js.js7
-rw-r--r--ext/wasm/api/post-js-footer.js4
-rw-r--r--ext/wasm/api/post-js-header.js26
-rw-r--r--ext/wasm/api/pre-js.c-pp.js110
-rw-r--r--ext/wasm/api/sqlite3-api-cleanup.js63
-rw-r--r--ext/wasm/api/sqlite3-api-glue.js1671
-rw-r--r--ext/wasm/api/sqlite3-api-oo1.js1946
-rw-r--r--ext/wasm/api/sqlite3-api-prologue.js2154
-rw-r--r--ext/wasm/api/sqlite3-api-worker1.js695
-rw-r--r--ext/wasm/api/sqlite3-license-version-header.js25
-rw-r--r--ext/wasm/api/sqlite3-opfs-async-proxy.js915
-rw-r--r--ext/wasm/api/sqlite3-v-helper.js718
-rw-r--r--ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js1287
-rw-r--r--ext/wasm/api/sqlite3-vfs-opfs.c-pp.js1469
-rw-r--r--ext/wasm/api/sqlite3-wasm.c1928
-rw-r--r--ext/wasm/api/sqlite3-worker1-promiser.c-pp.js282
-rw-r--r--ext/wasm/api/sqlite3-worker1.c-pp.js54
-rw-r--r--ext/wasm/batch-runner-sahpool.html86
-rw-r--r--ext/wasm/batch-runner-sahpool.js341
-rw-r--r--ext/wasm/batch-runner.html90
-rw-r--r--ext/wasm/batch-runner.js604
-rw-r--r--ext/wasm/c-pp.c1530
-rw-r--r--ext/wasm/common/SqliteTestUtil.js235
-rw-r--r--ext/wasm/common/emscripten.css24
-rw-r--r--ext/wasm/common/testing.css102
-rw-r--r--ext/wasm/common/whwasmutil.js2263
-rw-r--r--ext/wasm/demo-123-worker.html44
-rw-r--r--ext/wasm/demo-123.html24
-rw-r--r--ext/wasm/demo-123.js290
-rw-r--r--ext/wasm/demo-jsstorage.html49
-rw-r--r--ext/wasm/demo-jsstorage.js114
-rw-r--r--ext/wasm/demo-worker1-promiser.html34
-rw-r--r--ext/wasm/demo-worker1-promiser.js275
-rw-r--r--ext/wasm/demo-worker1.html34
-rw-r--r--ext/wasm/demo-worker1.js345
-rw-r--r--ext/wasm/dist.make131
-rw-r--r--ext/wasm/example_extra_init.c23
-rw-r--r--ext/wasm/fiddle.make194
-rw-r--r--ext/wasm/fiddle/emscripten.css24
-rw-r--r--ext/wasm/fiddle/fiddle-worker.js384
-rw-r--r--ext/wasm/fiddle/fiddle.js815
-rw-r--r--ext/wasm/fiddle/index.html278
-rw-r--r--ext/wasm/index-dist.html113
-rw-r--r--ext/wasm/index.html147
-rw-r--r--ext/wasm/jaccwabyt/jaccwabyt.js696
-rw-r--r--ext/wasm/jaccwabyt/jaccwabyt.md1065
-rw-r--r--ext/wasm/module-symbols.html531
-rw-r--r--ext/wasm/scratchpad-wasmfs.html30
-rw-r--r--ext/wasm/scratchpad-wasmfs.mjs70
-rw-r--r--ext/wasm/speedtest1-wasmfs.html55
-rw-r--r--ext/wasm/speedtest1-wasmfs.mjs90
-rw-r--r--ext/wasm/speedtest1-worker.html372
-rw-r--r--ext/wasm/speedtest1-worker.js127
-rw-r--r--ext/wasm/speedtest1.html174
-rwxr-xr-xext/wasm/split-speedtest1-script.sh17
-rw-r--r--ext/wasm/sql/000-mandelbrot.sql17
-rw-r--r--ext/wasm/sql/001-sudoku.sql28
-rw-r--r--ext/wasm/test-opfs-vfs.html26
-rw-r--r--ext/wasm/test-opfs-vfs.js85
-rw-r--r--ext/wasm/tester1-worker.html81
-rw-r--r--ext/wasm/tester1.c-pp.html41
-rw-r--r--ext/wasm/tester1.c-pp.js3237
-rw-r--r--ext/wasm/tests/opfs/concurrency/index.html49
-rw-r--r--ext/wasm/tests/opfs/concurrency/test.js136
-rw-r--r--ext/wasm/tests/opfs/concurrency/worker.js113
-rw-r--r--ext/wasm/wasmfs.make115
80 files changed, 32646 insertions, 0 deletions
diff --git a/ext/wasm/EXPORTED_FUNCTIONS.fiddle.in b/ext/wasm/EXPORTED_FUNCTIONS.fiddle.in
new file mode 100644
index 0000000..103704d
--- /dev/null
+++ b/ext/wasm/EXPORTED_FUNCTIONS.fiddle.in
@@ -0,0 +1,10 @@
+_fiddle_db_arg
+_fiddle_db_filename
+_fiddle_exec
+_fiddle_experiment
+_fiddle_interrupt
+_fiddle_main
+_fiddle_reset_db
+_fiddle_db_handle
+_fiddle_db_vfs
+_fiddle_export_db
diff --git a/ext/wasm/GNUmakefile b/ext/wasm/GNUmakefile
new file mode 100644
index 0000000..8f733b6
--- /dev/null
+++ b/ext/wasm/GNUmakefile
@@ -0,0 +1,1182 @@
+#######################################################################
+# This GNU makefile drives the build of the sqlite3 WASM
+# components. It is not part of the canonical build process.
+#
+# This build assumes a Linux platform and is not intended for
+# general-purpose client-level use, except for creating builds with
+# custom configurations. It is primarily intended for the sqlite
+# project's own development of the JS/WASM components.
+#
+# Primary targets:
+#
+# default, all = build in dev mode
+#
+# o0, o1, o2, o3, os, oz = full clean/rebuild with the -Ox level indicated
+# by the target name. Rebuild is necessary for all components to get
+# the desired optimization level.
+#
+# quick, q = do just a minimal build (sqlite3.js/wasm, tester1) for
+# faster development-mode turnaround.
+#
+# dist = create end user deliverables. Add dist.build=oX to build
+# with a specific optimization level, where oX is one of the
+# above-listed o? or qo? target names.
+#
+# snapshot = like dist, but uses a zip file name which clearly
+# marks it as a prerelease/snapshot build.
+#
+# clean = clean up
+#
+# Required tools beyond those needed for the canonical builds:
+#
+# - Emscripten SDK: https://emscripten.org/docs/getting_started/downloads.html
+# - The bash shell
+# - GNU make, GNU sed, GNU awk, GNU grep (all in the $PATH)
+# - wasm-strip for release builds: https://github.com/WebAssembly/wabt
+# - InfoZip for 'dist' zip file
+########################################################################
+#
+# Significant TODOs for this build include, but are not necessarily
+# limited to:
+#
+# 1) Consolidate the code generation for sqlite3*.*js into a script
+# which generates the makefile code, rather than using $(call) and
+# $(eval), or at least centralize the setup of the numerous vars
+# related to each build variant $(JS_BUILD_MODES). (Update: an
+# external script was attempted but it's even less legible than the
+# $(eval) indirection going on in this file.
+#
+default: all
+#default: quick
+SHELL := $(shell which bash 2>/dev/null)
+MAKEFILE := $(lastword $(MAKEFILE_LIST))
+CLEAN_FILES :=
+DISTCLEAN_FILES := ./--dummy--
+release: oz
+
+########################################################################
+# JS_BUILD_NAMES exists for documentation purposes only. It enumerates
+# the core build styles:
+#
+# - sqlite3 = canonical library build
+#
+# - sqlite3-wasmfs = WASMFS-capable library build
+#
+JS_BUILD_NAMES := sqlite3 sqlite3-wasmfs
+
+########################################################################
+# JS_BUILD_MODES exists for documentation purposes only. It enumerates
+# the various "flavors" of build, each of which requires slight
+# customization of the output:
+#
+# - vanilla = plain-vanilla JS for use in browsers. This is the
+# canonical build mode.
+#
+# - esm = ES6 module, a.k.a. ESM, for use in browsers.
+#
+# - bundler-friendly = esm slightly tweaked for "bundler"
+# tools. Bundlers are invariably based on node.js, so these builds
+# are intended to be read at build-time by node.js but with a final
+# target of browsers.
+#
+# - node = for use by node.js for node.js, as opposed to by node.js on
+# behalf o browser-side code (use bundler-friendly for that). Note
+# that persistent storage (OPFS) is not available in these builds.
+#
+JS_BUILD_MODES := vanilla esm bunder-friendly node
+
+########################################################################
+# Emscripten SDK home dir and related binaries...
+EMSDK_HOME ?= $(word 1,$(wildcard $(HOME)/emsdk $(HOME)/src/emsdk))
+emcc.bin ?= $(word 1,$(wildcard $(EMSDK_HOME)/upstream/emscripten/emcc) $(shell which emcc))
+ifeq (,$(emcc.bin))
+ $(error Cannot find emcc.)
+endif
+emcc.version := $(shell "$(emcc.bin)" --version | sed -n 1p \
+ | sed -e 's/^.* \([3-9][^ ]*\) .*$$/\1/;')
+ifeq (,$(emcc.version))
+ $(warning Cannot determine emcc version. This might unduly impact build flags.)
+else
+ $(info using emcc version [$(emcc.version)])
+endif
+
+wasm-strip ?= $(shell which wasm-strip 2>/dev/null)
+ifeq (,$(filter clean,$(MAKECMDGOALS)))
+ifeq (,$(wasm-strip))
+ $(info WARNING: *******************************************************************)
+ $(info WARNING: builds using -O2/-O3/-Os/-Oz will minify WASM-exported names,)
+ $(info WARNING: breaking _All The Things_. The workaround for that is to build)
+ $(info WARNING: with -g3 (which explodes the file size) and then strip the debug)
+ $(info WARNING: info after compilation, using wasm-strip, to shrink the wasm file.)
+ $(info WARNING: wasm-strip was not found in the PATH so we cannot strip those.)
+ $(info WARNING: If this build uses any optimization level higher than -O1 then)
+ $(info WARNING: the ***resulting JS code WILL NOT BE USABLE***.)
+ $(info WARNING: wasm-strip is part of the wabt package:)
+ $(info WARNING: https://github.com/WebAssembly/wabt)
+ $(info WARNING: on Ubuntu-like systems it can be installed with:)
+ $(info WARNING: sudo apt install wabt)
+ $(info WARNING: *******************************************************************)
+endif
+endif # 'make clean' check
+
+ifeq (,$(wasm-strip))
+ maybe-wasm-strip = echo "not wasm-stripping"
+else
+ maybe-wasm-strip = $(wasm-strip)
+endif
+
+########################################################################
+# dir.top = the top dir of the canonical build tree, where
+# sqlite3.[ch] live.
+dir.top := ../..
+# Maintenance reminder: some Emscripten flags require absolute paths
+# but we want relative paths for most stuff simply to reduce
+# noise. The $(abspath...) GNU make function can transform relative
+# paths to absolute.
+dir.wasm := $(patsubst %/,%,$(dir $(MAKEFILE)))
+dir.api := api
+dir.jacc := jaccwabyt
+dir.common := common
+dir.fiddle := fiddle
+dir.tool := $(dir.top)/tool
+CLEAN_FILES += *~ $(dir.jacc)/*~ $(dir.api)/*~ $(dir.common)/*~ $(dir.fiddle)/*~
+
+########################################################################
+# dir.dout = output dir for deliverables.
+#
+# MAINTENANCE REMINDER: the output .js and .wasm files of certain emcc
+# buildables must be in _this_ dir, rather than a subdir, or else
+# parts of the generated code get confused and cannot load
+# property. Specifically, when X.js loads X.wasm, whether or not X.js
+# uses the correct path for X.wasm depends on how it's loaded: an HTML
+# script tag will resolve it intuitively, whereas a Worker's call to
+# importScripts() will not. That's a fundamental incompatibility with
+# how URL resolution in JS happens between those two contexts. See:
+#
+# https://zzz.buzz/2017/03/14/relative-uris-in-web-development/
+#
+# We unfortunately have no way, from Worker-initiated code, to
+# automatically resolve the path from X.js to X.wasm.
+#
+# We have an "only slightly unsightly" solution for our main builds
+# but it does not work for the WASMFS builds, so those builds have to
+# be built to _this_ directory and can only run when the client app is
+# loaded from the same directory.
+dir.dout := $(dir.wasm)/jswasm
+# dir.tmp = output dir for intermediary build files, as opposed to
+# end-user deliverables.
+dir.tmp := $(dir.wasm)/bld
+CLEAN_FILES += $(dir.tmp)/* $(dir.dout)/*
+ifeq (,$(wildcard $(dir.dout)))
+ dir._tmp := $(shell mkdir -p $(dir.dout))
+endif
+ifeq (,$(wildcard $(dir.tmp)))
+ dir._tmp := $(shell mkdir -p $(dir.tmp))
+endif
+
+########################################################################
+# Set up sqlite3.c and sqlite3.h...
+#
+# To build with SEE (https://sqlite.org/see), either put sqlite3-see.c
+# in $(dir.top) or pass sqlite3.c=PATH_TO_sqlite3-see.c to the $(MAKE)
+# invocation. Note that only encryption modules with no 3rd-party
+# dependencies will currently work here: AES256-OFB, AES128-OFB, and
+# AES128-CCM. Not coincidentally, those 3 modules are included in the
+# sqlite3-see.c bundle. Note, however, that distributing an SEE build
+# of the WASM on a public site is in violation of the SEE license
+# because it effectively provides a usable copy of the SEE build to
+# all visitors.
+#
+# A custom sqlite3.c must not have any spaces in its name.
+# $(sqlite3.canonical.c) must point to the sqlite3.c in
+# the sqlite3 canonical source tree, as that source file
+# is required for certain utility and test code.
+sqlite3.canonical.c := $(dir.top)/sqlite3.c
+sqlite3.c ?= $(firstword $(wildcard $(dir.top)/sqlite3-see.c) $(sqlite3.canonical.c))
+sqlite3.h := $(dir.top)/sqlite3.h
+ifeq (,$(shell grep sqlite3_activate_see $(sqlite3.c) 2>/dev/null))
+ SQLITE_C_IS_SEE := 0
+else
+ SQLITE_C_IS_SEE := 1
+ $(info This is an SEE build.)
+endif
+# Most SQLITE_OPT flags are set in sqlite3-wasm.c but we need them
+# made explicit here for building speedtest1.c.
+SQLITE_OPT = \
+ -DSQLITE_ENABLE_FTS5 \
+ -DSQLITE_ENABLE_RTREE \
+ -DSQLITE_ENABLE_EXPLAIN_COMMENTS \
+ -DSQLITE_ENABLE_UNKNOWN_SQL_FUNCTION \
+ -DSQLITE_ENABLE_STMTVTAB \
+ -DSQLITE_ENABLE_DBPAGE_VTAB \
+ -DSQLITE_ENABLE_DBSTAT_VTAB \
+ -DSQLITE_ENABLE_BYTECODE_VTAB \
+ -DSQLITE_ENABLE_OFFSET_SQL_FUNC \
+ -DSQLITE_OMIT_LOAD_EXTENSION \
+ -DSQLITE_OMIT_DEPRECATED \
+ -DSQLITE_OMIT_UTF16 \
+ -DSQLITE_OMIT_SHARED_CACHE \
+ -DSQLITE_THREADSAFE=0 \
+ -DSQLITE_TEMP_STORE=2 \
+ -DSQLITE_OS_KV_OPTIONAL=1 \
+ '-DSQLITE_DEFAULT_UNIX_VFS="unix-none"' \
+ -DSQLITE_USE_URI=1 \
+ -DSQLITE_WASM_ENABLE_C_TESTS \
+ -DSQLITE_C=$(sqlite3.c)
+#SQLITE_OPT += -DSQLITE_DEBUG
+# Enabling SQLITE_DEBUG will break sqlite3_wasm_vfs_create_file()
+# (and thus sqlite3_js_vfs_create_file()). Those functions are
+# deprecated and alternatives are in place, but this crash behavior
+# can be used to find errant uses of sqlite3_js_vfs_create_file()
+# in client code.
+
+########################################################################@
+# It's important that sqlite3.h be built to completion before any
+# other parts of the build run, thus we use .NOTPARALLEL to disable
+# parallel build of that file and its dependants.
+.NOTPARALLEL: $(sqlite3.h)
+$(sqlite3.h):
+ $(MAKE) -C $(dir.top) sqlite3.c
+$(sqlite3.c): $(sqlite3.h)
+
+.PHONY: clean distclean
+clean:
+ -rm -f $(CLEAN_FILES)
+distclean: clean
+ -rm -f $(DISTCLEAN_FILES)
+
+ifeq (release,$(filter release,$(MAKECMDGOALS)))
+ ifeq (,$(wasm-strip))
+ $(error Cannot make release-quality binary because wasm-strip is not available. \
+ See notes in the warning above)
+ endif
+else
+ $(info Development build. Use '$(MAKE) release' for a smaller release build.)
+endif
+
+########################################################################
+# Adding custom C code via sqlite3_wasm_extra_init.c:
+#
+# If the canonical build process finds the file
+# sqlite3_wasm_extra_init.c in the main wasm build directory, it
+# arranges to include that file in the build of sqlite3.wasm and
+# defines SQLITE_EXTRA_INIT=sqlite3_wasm_extra_init.
+#
+# sqlite3_wasm_extra_init() must be a function with this signature:
+#
+# int sqlite3_wasm_extra_init(const char *)
+#
+# and the sqlite3 library will call it with an argument of NULL one
+# time during sqlite3_initialize(). If it returns non-0,
+# initialization of the library will fail.
+#
+# The filename can be overridden with:
+#
+# make sqlite3_wasm_extra_init.c=my_custom_stuff.c
+#
+# See example_extra_init.c for an example implementation.
+########################################################################
+sqlite3_wasm_extra_init.c ?= $(wildcard sqlite3_wasm_extra_init.c)
+cflags.wasm_extra_init :=
+ifneq (,$(sqlite3_wasm_extra_init.c))
+ $(info Enabling SQLITE_EXTRA_INIT via $(sqlite3_wasm_extra_init.c).)
+ cflags.wasm_extra_init := -DSQLITE_WASM_EXTRA_INIT
+endif
+
+#########################################################################
+# bin.version-info = binary to output various sqlite3 version info for
+# embedding in the JS files and in building the distribution zip file.
+# It must NOT be in $(dir.tmp) because we need it to survive the
+# cleanup process for the dist build to work properly.
+bin.version-info := $(dir.top)/version-info
+.NOTPARALLEL: $(bin.version-info)
+$(bin.version-info): $(dir.tool)/version-info.c $(sqlite3.h) $(dir.top)/Makefile
+ $(MAKE) -C $(dir.top) version-info
+
+#########################################################################
+# bin.stripcomments is used for stripping C/C++-style comments from JS
+# files. The JS files contain large chunks of documentation which we
+# don't need for all builds. That app's -k flag is of particular
+# importance here, as it allows us to retain the opening comment
+# block(s), which contain the license header and version info.
+bin.stripccomments := $(dir.tool)/stripccomments
+$(bin.stripccomments): $(bin.stripccomments).c $(MAKEFILE)
+ $(CC) -o $@ $<
+DISTCLEAN_FILES += $(bin.stripccomments)
+
+
+########################################################################
+# C-PP.FILTER: a $(call)able to transform $(1) to $(2) via ./c-pp -f
+# $(1) ...
+#
+# Historical notes:
+#
+# - We first attempted to use gcc and/or clang to preprocess JS files
+# in the same way we would normally do C files, but C-specific quirks
+# of each makes that untennable.
+#
+# - We implemented c-pp.c (the C-Minus Pre-processor) as a custom
+# generic/file-format-agnostic preprocessor to enable us to pack
+# code for different target builds into the same JS files. Most
+# notably, some ES6 module (a.k.a. ESM) features cannot legally be
+# referenced at all in non-ESM code, e.g. the "import" and "export"
+# keywords. This preprocessing step permits us to swap out sections
+# of code where necessary for ESM and non-ESM (a.k.a. vanilla JS)
+# require different implementations. The alternative to such
+# preprocessing, would be to have separate source files for ES6
+# builds, which would have a higher maintenance burden than c-pp.c
+# seems likely to.
+#
+# c-pp.c was written specifically for the sqlite project's JavaScript
+# builds but is maintained as a standalone project:
+# https://fossil.wanderinghorse.net/r/c-pp
+#
+# Note that the SQLITE_... build flags used here have NO EFFECT on the
+# JS/WASM build. They are solely for use with $(bin.c-pp) itself.
+bin.c-pp := ./c-pp
+$(bin.c-pp): c-pp.c $(sqlite3.c) $(MAKEFILE)
+ $(CC) -O0 -o $@ c-pp.c $(sqlite3.c) '-DCMPP_DEFAULT_DELIM="//#"' -I$(dir.top) \
+ -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_UTF16 \
+ -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_OMIT_WAL -DSQLITE_THREADSAFE=0 \
+ -DSQLITE_TEMP_STORE=3
+define C-PP.FILTER
+# Create $2 from $1 using $(bin.c-pp)
+# $1 = Input file: c-pp -f $(1).js
+# $2 = Output file: c-pp -o $(2).js
+# $3 = optional c-pp -D... flags
+$(2): $(1) $$(MAKEFILE) $$(bin.c-pp)
+ $$(bin.c-pp) -f $(1) -o $$@ $(3)
+CLEAN_FILES += $(2)
+endef
+# /end C-PP.FILTER
+########################################################################
+
+# cflags.common = C compiler flags for all builds
+cflags.common := -I. -I$(dir $(sqlite3.c))
+# emcc.WASM_BIGINT = 1 for BigInt (C int64) support, else 0. The API
+# disables certain features if BigInt is not enabled and such builds
+# _are not tested_ on any regular basis.
+emcc.WASM_BIGINT ?= 1
+
+# emcc_opt = optimization-related flags. These are primarily used by
+# the various oX targets. build times for -O levels higher than 0 are
+# painful at dev-time.
+emcc_opt ?= -O0
+
+# When passing emcc_opt from the CLI, += and re-assignment have no
+# effect, so emcc_opt+=-g3 doesn't work. So...
+emcc_opt_full := $(emcc_opt) -g3
+# ^^^ ALWAYS use -g3. See below for why.
+#
+# ^^^ -flto improves runtime speed at -O0 considerably but doubles
+# build time.
+#
+# ^^^^ -O3, -Oz, -Os minify symbol names and there appears to be no
+# way around that except to use -g3, but -g3 causes the binary file
+# size to absolutely explode (approx. 5x larger). This minification
+# utterly breaks the resulting module, making it unsable except as
+# self-contained/self-referential-only code, as ALL of the exported
+# symbols get minified names.
+#
+# However, we have an option for using -Oz or -Os:
+#
+# Build with (-Os -g3) or (-Oz -g3) then use wasm-strip, from the wabt
+# tools package (https://github.com/WebAssembly/wabt), to strip the
+# debugging symbols. That results in a small build with unmangled
+# symbol names. -Oz gives ever-so-slightly better compression than
+# -Os: not quite 1% in some completely unscientific tests. Runtime
+# speed for the unit tests is all over the place either way so it's
+# difficult to say whether -Os gives any speed benefit over -Oz.
+#
+# Much practice has demonstrated that -O2 consistently gives the best
+# runtime speeds, but not by a large enough factor to rule out use of
+# -Oz when small deliverable size is a priority.
+########################################################################
+
+########################################################################
+# EXPORTED_FUNCTIONS.* = files for use with Emscripten's
+# -sEXPORTED_FUNCTION flag.
+EXPORTED_FUNCTIONS.api.main := $(abspath $(dir.api)/EXPORTED_FUNCTIONS.sqlite3-api)
+EXPORTED_FUNCTIONS.api.in := $(EXPORTED_FUNCTIONS.api.main)
+ifeq (1,$(SQLITE_C_IS_SEE))
+ EXPORTED_FUNCTIONS.api.in += $(abspath $(dir.api)/EXPORTED_FUNCTIONS.sqlite3-see)
+endif
+EXPORTED_FUNCTIONS.api := $(dir.tmp)/EXPORTED_FUNCTIONS.api
+$(EXPORTED_FUNCTIONS.api): $(EXPORTED_FUNCTIONS.api.in) $(sqlite3.c) $(MAKEFILE)
+ cat $(EXPORTED_FUNCTIONS.api.in) > $@
+
+########################################################################
+# sqlite3-license-version.js = generated JS file with the license
+# header and version info.
+sqlite3-license-version.js := $(dir.tmp)/sqlite3-license-version.js
+# sqlite3-license-version-header.js = JS file containing only the
+# license header.
+sqlite3-license-version-header.js := $(dir.api)/sqlite3-license-version-header.js
+# sqlite3-api-build-version.js = generated JS file which populates the
+# sqlite3.version object using $(bin.version-info).
+sqlite3-api-build-version.js := $(dir.tmp)/sqlite3-api-build-version.js
+# sqlite3-api.jses = the list of JS files which make up
+# $(sqlite3-api.js.in), in the order they need to be assembled.
+sqlite3-api.jses := $(sqlite3-license-version.js)
+# sqlite3-api-prologue.js: initial boostrapping bits:
+sqlite3-api.jses += $(dir.api)/sqlite3-api-prologue.js
+# whwhasm.js and jaccwabyt.js: Low-level utils, mostly replacing
+# Emscripten glue:
+sqlite3-api.jses += $(dir.common)/whwasmutil.js
+sqlite3-api.jses += $(dir.jacc)/jaccwabyt.js
+# sqlite3-api-glue.js Glues the previous part together:
+sqlite3-api.jses += $(dir.api)/sqlite3-api-glue.js
+# $(sqlite3-api-build-version.js) = library version info
+sqlite3-api.jses += $(sqlite3-api-build-version.js)
+# sqlite3-api-oo1.js = the oo1 API:
+sqlite3-api.jses += $(dir.api)/sqlite3-api-oo1.js
+# sqlite3-api-worker.js = the Worker1 API:
+sqlite3-api.jses += $(dir.api)/sqlite3-api-worker1.js
+# sqlite3-v-helper = helper APIs for VFSes and VTABLEs:
+sqlite3-api.jses += $(dir.api)/sqlite3-v-helper.js
+# sqlite3-vfs-opfs.c-pp.js = the first OPFS VFS:
+sqlite3-api.jses += $(dir.api)/sqlite3-vfs-opfs.c-pp.js
+# sqlite3-vfs-opfs-sahpool.c-pp.js = the second OPFS VFS:
+sqlite3-api.jses += $(dir.api)/sqlite3-vfs-opfs-sahpool.c-pp.js
+# sqlite3-api-cleanup.js = "finalizes" the build and cleans up
+# any extraneous global symbols which are needed temporarily
+# by the previous files.
+sqlite3-api.jses += $(dir.api)/sqlite3-api-cleanup.js
+
+########################################################################
+# SOAP.js is an external API file which is part of our distribution
+# but not part of the sqlite3-api.js amalgamation. It's a component of
+# the first OPFS VFS and necessarily an external file.
+SOAP.js := $(dir.api)/sqlite3-opfs-async-proxy.js
+SOAP.js.bld := $(dir.dout)/$(notdir $(SOAP.js))
+sqlite3-api.ext.jses += $(SOAP.js.bld)
+$(SOAP.js.bld): $(SOAP.js)
+ cp $< $@
+
+all quick: $(sqlite3-api.ext.jses)
+q: quick
+
+########################################################################
+# $(sqlite3-api*.*js) contain the core library code but not the
+# Emscripten-related glue which deals with loading sqlite3.wasm. In
+# theory they can be used by arbitrary build environments and WASM
+# loaders, but in practice that breaks down because the WASM loader
+# has to be able to provide all of the necessary "imports" to
+# sqlite3.wasm, and that list of imports is unknown until sqlite3.wasm
+# is compiled, at which point Emscripten sets up the imports
+# appropriately. Abstractly speaking, it's impossible for other build
+# environments to know exactly which imports are needed and provide
+# them. Tools like wasm-objdump can be used to find the list of
+# imports but it's questionable whether a non-Emscripten tool could
+# realistically use that info to provide proper implementations.
+# Sidebar: some of the imports are used soley by the Emscripten glue,
+# which the sqlite3 JS code does not rely on.
+#
+# We build $(sqlite3-api*.*) "because we can" and because it might be
+# a useful point of experimentation for some clients, but the
+# above-described caveat may well make them unusable for real-life
+# clients.
+#
+# sqlite3-api.js.in = the generated sqlite3-api.js before it gets
+# preprocessed. It contains all of $(sqlite3-api.jses) but none of the
+# Emscripten-specific headers and footers.
+sqlite3-api.js.in := $(dir.tmp)/sqlite3-api.c-pp.js
+$(sqlite3-api.js.in): $(sqlite3-api.jses) $(MAKEFILE)
+ @echo "Making $@..."
+ @for i in $(sqlite3-api.jses); do \
+ echo "/* BEGIN FILE: $$i */"; \
+ cat $$i; \
+ echo "/* END FILE: $$i */"; \
+ done > $@
+
+########################################################################
+# emcc flags for .c/.o/.wasm/.js.
+emcc.flags :=
+ifeq (1,$(emcc.verbose))
+emcc.flags += -v
+# -v is _very_ loud but also informative about what it's doing
+endif
+
+########################################################################
+# emcc flags for .c/.o.
+emcc.cflags :=
+emcc.cflags += -std=c99 -fPIC
+# -------------^^^^^^^^ we need c99 for $(sqlite3-wasm.c), primarily
+# for variadic macros and snprintf() to implement
+# sqlite3_wasm_enum_json().
+emcc.cflags += -I. -I$(dir.top)
+########################################################################
+# emcc flags specific to building .js/.wasm files...
+emcc.jsflags := -fPIC
+emcc.jsflags += --minify 0
+emcc.jsflags += --no-entry
+emcc.jsflags += -sWASM_BIGINT=$(emcc.WASM_BIGINT)
+emcc.jsflags += -sMODULARIZE
+emcc.jsflags += -sDYNAMIC_EXECUTION=0
+emcc.jsflags += -sNO_POLYFILL
+emcc.jsflags += -sEXPORTED_FUNCTIONS=@$(EXPORTED_FUNCTIONS.api)
+emcc.exportedRuntimeMethods := \
+ -sEXPORTED_RUNTIME_METHODS=wasmMemory
+ # wasmMemory ==> required by our code for use with -sIMPORTED_MEMORY
+emcc.jsflags += $(emcc.exportedRuntimeMethods)
+emcc.jsflags += -sUSE_CLOSURE_COMPILER=0
+emcc.jsflags += -sIMPORTED_MEMORY
+emcc.jsflags += -sSTRICT_JS=0
+# STRICT_JS disabled due to:
+# https://github.com/emscripten-core/emscripten/issues/18610
+# TL;DR: does not work with MODULARIZE or EXPORT_ES6 as of version
+# 3.1.31. The fix for that in newer emcc's is to throw a built-time
+# error if STRICT_JS is used together with those options.
+
+# -sENVIRONMENT values for the various build modes:
+emcc.environment.vanilla := web,worker
+emcc.environment.bundler-friendly := $(emcc.environment.vanilla)
+emcc.environment.esm := $(emcc.environment.vanilla)
+emcc.environment.node := node
+# Note that adding ",node" to the list for the other builds causes
+# Emscripten to generate code which confuses node: it cannot reliably
+# determine whether the build is for a browser or for node.
+
+########################################################################
+# -sINITIAL_MEMORY: How much memory we need to start with is governed
+# at least in part by whether -sALLOW_MEMORY_GROWTH is enabled. If so,
+# we can start with less. If not, we need as much as we'll ever
+# possibly use (which, of course, we can't know for sure). Note,
+# however, that speedtest1 shows that performance for even moderate
+# workloads MAY suffer considerably if we start small and have to grow
+# at runtime. e.g. OPFS-backed (speedtest1 --size 75) take MAY take X
+# time with 16mb+ memory and 3X time when starting with 8MB. However,
+# such test results are inconsistent due to browser internals which
+# are opaque to us.
+emcc.jsflags += -sALLOW_MEMORY_GROWTH
+emcc.INITIAL_MEMORY.128 := 134217728
+emcc.INITIAL_MEMORY.96 := 100663296
+emcc.INITIAL_MEMORY.64 := 67108864
+emcc.INITIAL_MEMORY.32 := 33554432
+emcc.INITIAL_MEMORY.16 := 16777216
+emcc.INITIAL_MEMORY.8 := 8388608
+emcc.INITIAL_MEMORY ?= 16
+ifeq (,$(emcc.INITIAL_MEMORY.$(emcc.INITIAL_MEMORY)))
+$(error emcc.INITIAL_MEMORY must be one of: 8, 16, 32, 64, 96, 128 (megabytes))
+endif
+emcc.jsflags += -sINITIAL_MEMORY=$(emcc.INITIAL_MEMORY.$(emcc.INITIAL_MEMORY))
+# /INITIAL_MEMORY
+########################################################################
+
+emcc.jsflags += $(emcc.environment)
+emcc.jsflags += -sSTACK_SIZE=512KB
+# ^^^ ACHTUNG: emsdk 3.1.27 reduced the default stack size from 5MB to
+# a mere 64KB, which leads to silent memory corruption via the kvvfs
+# VFS, which requires twice that for its xRead() and xWrite() methods.
+# 2023-03: those methods have since been adapted to use a malloc()'d
+# buffer.
+########################################################################
+# $(sqlite3.js.init-func) is the name Emscripten assigns our exported
+# module init/load function. This symbol name is hard-coded in
+# $(extern-post-js.js) as well as in numerous docs.
+#
+# "sqlite3InitModule" is the symbol we document for client use, so
+# that's the symbol name which must be exported, whether it comes from
+# Emscripten or our own code in extern-post-js.js.
+#
+# That said... we can change $(sqlite3.js.init-func) as long as the
+# name "sqlite3InitModule" is the one which gets exposed via the
+# resulting JS files. That can be accomplished via
+# extern-post-js.js. However... using a temporary symbol name here
+# and then adding sqlite3InitModule() ourselves results in 2 global
+# symbols: we cannot "delete" the Emscripten-defined
+# $(sqlite3.js.init-func) from vanilla builds (as opposed to ESM
+# builds) because it's declared with "var".
+sqlite3.js.init-func := sqlite3InitModule
+emcc.jsflags += -sEXPORT_NAME=$(sqlite3.js.init-func)
+emcc.jsflags += -sGLOBAL_BASE=4096 # HYPOTHETICALLY keep func table indexes from overlapping w/ heap addr.
+#emcc.jsflags += -sSTRICT # fails due to missing __syscall_...()
+#emcc.jsflags += -sALLOW_UNIMPLEMENTED_SYSCALLS
+#emcc.jsflags += -sFILESYSTEM=0 # only for experimentation. fiddle needs the FS API
+#emcc.jsflags += -sABORTING_MALLOC # only for experimentation
+emcc.jsflags += -sALLOW_TABLE_GROWTH
+# ^^^^ -sALLOW_TABLE_GROWTH is required for installing new SQL UDFs
+emcc.jsflags += -Wno-limited-postlink-optimizations
+# ^^^^ emcc likes to warn when we have "limited optimizations" via the
+# -g3 flag.
+# emcc.jsflags += -sSTANDALONE_WASM # causes OOM errors, not sure why.
+
+# Re. undefined symbol handling, see: https://lld.llvm.org/WebAssembly.html
+emcc.jsflags += -sERROR_ON_UNDEFINED_SYMBOLS=1
+emcc.jsflags += -sLLD_REPORT_UNDEFINED
+#emcc.jsflags += --allow-undefined
+#emcc.jsflags += --import-undefined
+#emcc.jsflags += --unresolved-symbols=import-dynamic --experimental-pic
+#emcc.jsflags += --experimental-pic --unresolved-symbols=ingore-all --import-undefined
+#emcc.jsflags += --unresolved-symbols=ignore-all
+
+########################################################################
+# -sMEMORY64=1 fails to load, erroring with:
+# invalid memory limits flags 0x5
+# (enable via --experimental-wasm-memory64)
+#
+# ^^^^ MEMORY64=2 builds and loads but dies when we do things like:
+#
+# new Uint8Array(wasm.heap8u().buffer, ptr, n)
+#
+# because ptr is now a BigInt, so is invalid for passing to arguments
+# which have strict must-be-a-Number requirements. That aspect will
+# make any eventual port to 64-bit address space extremely painful, as
+# such constructs are found all over the place in the source code.
+########################################################################
+
+########################################################################
+# -sSINGLE_FILE:
+# https://github.com/emscripten-core/emscripten/blob/main/src/settings.js
+#
+# -sSINGLE_FILE=1 would be _really_ nice but we have to build with -g3
+# for -O2 and higher to work (else minification breaks the code) and
+# cannot wasm-strip the binary before it gets encoded into the JS
+# file. The result is that the generated JS file is, because of the
+# -g3 debugging info, _huge_.
+########################################################################
+
+########################################################################
+# $(sqlite3-api-build-version.js) injects the build version info into
+# the bundle in JSON form.
+$(sqlite3-api-build-version.js): $(bin.version-info) $(MAKEFILE)
+ @echo "Making $@..."
+ @{ \
+ echo 'globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){'; \
+ echo -n ' sqlite3.version = '; \
+ $(bin.version-info) --json; \
+ echo ';'; \
+ echo '});'; \
+ } > $@
+
+########################################################################
+# $(sqlite3-license-version.js) contains the license header and
+# in-comment build version info.
+#
+# Maintenance reminder: there are awk binaries out there which do not
+# support -e SCRIPT.
+$(sqlite3-license-version.js): $(sqlite3.h) $(sqlite3-license-version-header.js) \
+ $(MAKEFILE)
+ @echo "Making $@..."; { \
+ cat $(sqlite3-license-version-header.js); \
+ echo '/*'; \
+ echo '** This code was built from sqlite3 version...'; \
+ echo "**"; \
+ awk '/define SQLITE_VERSION/{$$1=""; print "**" $$0}' $(sqlite3.h); \
+ awk '/define SQLITE_SOURCE_ID/{$$1=""; print "**" $$0}' $(sqlite3.h); \
+ echo "**"; \
+ echo "** Using the Emscripten SDK version $(emcc.version)."; \
+ echo '*/'; \
+ } > $@
+
+########################################################################
+# --post-js and --pre-js are emcc flags we use to append/prepend JS to
+# the generated emscripten module file. These rules set up the core
+# pre/post files for use by the various builds. --pre-js is used to
+# inject code which needs to run as part of the pre-WASM-load phase.
+# --post-js injects code which runs after the WASM module is loaded
+# and includes the entirety of the library plus some
+# Emscripten-specific post-bootstrapping code.
+pre-js.js.in := $(dir.api)/pre-js.c-pp.js
+post-js.js.in := $(dir.tmp)/post-js.c-pp.js
+post-jses.js := \
+ $(dir.api)/post-js-header.js \
+ $(sqlite3-api.js.in) \
+ $(dir.api)/post-js-footer.js
+$(post-js.js.in): $(post-jses.js) $(MAKEFILE)
+ @echo "Making $@..."
+ @for i in $(post-jses.js); do \
+ echo "/* BEGIN FILE: $$i */"; \
+ cat $$i; \
+ echo "/* END FILE: $$i */"; \
+ done > $@
+
+
+########################################################################
+# call-make-pre-post is a $(call)able which creates rules for
+# pre-js.$(1)-$(2).js. $1 = the base name of the JS file on whose
+# behalf this pre-js is for (one of: $(JS_BUILD_NAMES)). $2 is
+# the build mode: one of $(JS_BUILD_MODES). This sets up
+# --[extern-][pre/post]-js flags in $(pre-post-$(1)-$(2).flags) and
+# dependencies in $(pre-post-$(1)-$(2).deps). The resulting files get
+# filtered using $(C-PP.FILTER). Any flags necessary for such
+# filtering need to be set in $(c-pp.D.$(1)-$(2)) before $(call)ing
+# this.
+#
+# Maintenance note: a shell script was written to generate these rules
+# with the hope that it would make them more legible and maintainable,
+# but embedding makefile code in another language makes it even less
+# legible than having the level of $(eval) indirection which we have
+# here.
+define call-make-pre-post
+pre-post-$(1)-$(2).flags ?=
+pre-js.js.$(1)-$(2).intermediary := $$(dir.tmp)/pre-js.$(1)-$(2).intermediary.js
+pre-js.js.$(1)-$(2) := $$(dir.tmp)/pre-js.$(1)-$(2).js
+#$$(error $$(pre-js.js.$(1)-$(2).intermediary) $$(pre-js.js.$(1)-$(2)))
+$$(eval $$(call C-PP.FILTER,$$(pre-js.js.in),$$(pre-js.js.$(1)-$(2).intermediary),$$(c-pp.D.$(1)-$(2))))
+post-js.js.$(1)-$(2) := $$(dir.tmp)/post-js.$(1)-$(2).js
+$$(eval $$(call C-PP.FILTER,$$(post-js.js.in),$$(post-js.js.$(1)-$(2)),$$(c-pp.D.$(1)-$(2))))
+extern-post-js.js.$(1)-$(2) := $$(dir.tmp)/extern-post-js.$(1)-$(2).js
+$$(eval $$(call C-PP.FILTER,$$(extern-post-js.js.in),$$(extern-post-js.js.$(1)-$(2)),$$(c-pp.D.$(1)-$(2))))
+pre-post-common.flags.$(1)-$(2) := \
+ $$(pre-post-common.flags) \
+ --post-js=$$(post-js.js.$(1)-$(2)) \
+ --extern-post-js=$$(extern-post-js.js.$(1)-$(2))
+pre-post-jses.$(1)-$(2).deps := $$(pre-post-jses.deps.common) \
+ $$(post-js.js.$(1)-$(2)) $$(extern-post-js.js.$(1)-$(2))
+$$(pre-js.js.$(1)-$(2)): $$(pre-js.js.$(1)-$(2).intermediary) $$(MAKEFILE)
+ cp $$(pre-js.js.$(1)-$(2).intermediary) $$@
+ @if [ sqlite3-wasmfs = $(1) ]; then \
+ echo "delete Module[xNameOfInstantiateWasm] /*for WASMFS build*/;"; \
+ elif [ sqlite3 != $(1) ]; then \
+ echo "Module[xNameOfInstantiateWasm].uri = '$(1).wasm';"; \
+ fi >> $$@
+pre-post-$(1)-$(2).deps := \
+ $$(pre-post-jses.$(1)-$(2).deps) \
+ $$(dir.tmp)/pre-js.$(1)-$(2).js
+pre-post-$(1)-$(2).flags += \
+ $$(pre-post-common.flags.$(1)-$(2)) \
+ --pre-js=$$(dir.tmp)/pre-js.$(1)-$(2).js
+endef
+# /post-js and pre-js
+########################################################################
+
+# Undocumented Emscripten feature: if the target file extension is
+# "mjs", it defaults to ES6 module builds:
+# https://github.com/emscripten-core/emscripten/issues/14383
+sqlite3.wasm := $(dir.dout)/sqlite3.wasm
+sqlite3-wasm.c := $(dir.api)/sqlite3-wasm.c
+sqlite3-wasm.cfiles := $(sqlite3-wasm.c) $(sqlite3_wasm_extra_init.c)
+sqlite3-wasmfs.cfiles := $(sqlite3-wasm.cfiles)
+# sqlite3-wasm.o vs sqlite3-wasm.c: building against the latter
+# (predictably) results in a slightly faster binary. We're close
+# enough to the target speed requirements that the 500ms makes a
+# difference, so we build all binaries against sqlite3-wasm.c instead
+# of building a shared copy of sqlite3-wasm.o to link against.
+########################################################################
+# SQLITE3.xJS.EXPORT-DEFAULT is part of SQLITE3-WASMFS.xJS.RECIPE and
+# SETUP_LIB_BUILD_MODE, factored into a separate piece to avoid code
+# duplication. $1 is 1 if the build mode needs this workaround (esm,
+# bundler-friendly, node) and 0 if not (vanilla). $2 must be empty for
+# all builds except sqlite3-wasmfs.mjs, in which case it must be 1.
+#
+# Reminder for ESM builds: even if we use -sEXPORT_ES6=0, emcc _still_
+# adds:
+#
+# export default $(sqlite3.js.init-func);
+#
+# when building *.mjs, which is bad because we need to export an
+# overwritten version of that function and cannot "export default"
+# twice. Because of this, we have to sed *.mjs to remove the _first_
+# instance (only) of /^export default/.
+#
+# Upstream RFE:
+# https://github.com/emscripten-core/emscripten/issues/18237
+#
+# Maintenance reminder: Mac sed works differently than GNU sed, so we
+# use awk instead of sed for this.
+define SQLITE3.xJS.ESM-EXPORT-DEFAULT
+if [ x1 = x$(1) ]; then \
+ echo "Fragile workaround for emscripten/issues/18237. See SQLITE3.xJS.RECIPE."; \
+ {\
+ awk '/^export default/ && !f{f=1; next} 1' $@ > $@.tmp && mv $@.tmp $@; \
+ } || exit $$?; \
+ if [ x != x$(2) ]; then \
+ if ! grep -q '^export default' $@; then \
+ echo "Cannot find export default." 1>&2; \
+ exit 1; \
+ fi; \
+ fi; \
+fi
+endef
+
+########################################################################
+# extern-post-js* and extern-pre-js* are files for use with
+# Emscripten's --extern-pre-js and --extern-post-js flags.
+extern-pre-js.js := $(dir.api)/extern-pre-js.js
+extern-post-js.js.in := $(dir.api)/extern-post-js.c-pp.js
+# Emscripten flags for --[extern-][pre|post]-js=... for the
+# various builds.
+pre-post-common.flags := \
+ --extern-pre-js=$(sqlite3-license-version.js)
+# pre-post-jses.deps.* = a list of dependencies for the
+# --[extern-][pre/post]-js files.
+pre-post-jses.deps.common := $(extern-pre-js.js) $(sqlite3-license-version.js)
+
+########################################################################
+# SETUP_LIB_BUILD_MODE is a $(call)'able which sets up numerous pieces
+# for one of the build modes.
+#
+# $1 = one of: $(JS_BUILD_NAMES)
+# $2 = build mode name: one of $(JS_BUILD_MODES)
+# $3 = 1 for ESM build mode, else 0
+# $4 = resulting sqlite-api JS/MJS file
+# $5 = resulting JS/MJS file
+# $6 = -D... flags for $(bin.c-pp)
+# $7 = emcc -sXYZ flags (CURRENTLY UNUSED - was factored out)
+#
+# Maintenance reminder: be careful not to introduce spaces around args
+# ($1, $2), otherwise string concatenation will malfunction.
+#
+# emcc.environment.$(2) must be set to a value for emcc's
+# -sENVIRONMENT flag.
+#
+# $(cflags.$(1)) and $(cflags.$(1).$(2)) may be defined to append
+# CFLAGS to a given build mode.
+#
+# $(emcc.flags.$(1)) and $(emcc.flags.$(1).$(2)) may be defined to
+# append emcc-specific flags to a given build mode.
+define SETUP_LIB_BUILD_MODE
+$(info Setting up build [$(1)-$(2)]: $(5))
+c-pp.D.$(1)-$(2) := $(6)
+$$(eval $$(call call-make-pre-post,$(1),$(2)))
+emcc.flags.$(1).$(2) ?=
+emcc.flags.$(1).$(2) += $(7)
+$$(eval $$(call C-PP.FILTER, $$(sqlite3-api.js.in), $(4), $(6)))
+$(5): $(4) $$(MAKEFILE) $$(sqlite3-wasm.cfiles) $$(EXPORTED_FUNCTIONS.api) $$(pre-post-$(1)-$(2).deps)
+ @echo "Building $$@ ..."
+ $$(emcc.bin) -o $$@ $$(emcc_opt_full) $$(emcc.flags) \
+ $$(emcc.jsflags) \
+ -sENVIRONMENT=$$(emcc.environment.$(2)) \
+ $$(pre-post-$(1)-$(2).flags) \
+ $$(emcc.flags.$(1)) $$(emcc.flags.$(1).$(2)) \
+ $$(cflags.common) $$(SQLITE_OPT) \
+ $$(cflags.$(1)) $$(cflags.$(1).$(2)) \
+ $$(cflags.wasm_extra_init) $$(sqlite3-wasm.cfiles)
+ @$$(call SQLITE3.xJS.ESM-EXPORT-DEFAULT,$(3))
+ @dotwasm=$$(basename $$@).wasm; \
+ chmod -x $$$$dotwasm; \
+ $(maybe-wasm-strip) $$$$dotwasm; \
+ case $(2) in \
+ bundler-friendly|node) \
+ echo "Patching $$@ for $(1).wasm..."; \
+ rm -f $$$$dotwasm; \
+ dotwasm=; \
+ sed -i -e 's/$(1)-$(2).wasm/$(1).wasm/g' $$@ || exit $$$$?; \
+ ;; \
+ esac; \
+ ls -la $$$$dotwasm $$@
+all: $(5)
+#quick: $(5)
+CLEAN_FILES += $(4) $(5)
+endef
+# ^^^ /SETUP_LIB_BUILD_MODE
+########################################################################
+sqlite3-api.js := $(dir.dout)/sqlite3-api.js
+sqlite3.js := $(dir.dout)/sqlite3.js
+sqlite3-api.mjs := $(dir.dout)/sqlite3-api.mjs
+sqlite3.mjs := $(dir.dout)/sqlite3.mjs
+sqlite3-api-bundler-friendly.mjs := $(dir.dout)/sqlite3-api-bundler-friendly.mjs
+sqlite3-bundler-friendly.mjs := $(dir.dout)/sqlite3-bundler-friendly.mjs
+sqlite3-api-node.mjs := $(dir.dout)/sqlite3-api-node.mjs
+sqlite3-node.mjs := $(dir.dout)/sqlite3-node.mjs
+#$(info $(call SETUP_LIB_BUILD_MODE,sqlite3,vanilla,0, $(sqlite3-api.js), $(sqlite3.js)))
+$(eval $(call SETUP_LIB_BUILD_MODE,sqlite3,vanilla,0,\
+ $(sqlite3-api.js), $(sqlite3.js)))
+$(eval $(call SETUP_LIB_BUILD_MODE,sqlite3,esm,1,\
+ $(sqlite3-api.mjs), $(sqlite3.mjs), -Dtarget=es6-module))
+$(eval $(call SETUP_LIB_BUILD_MODE,sqlite3,bundler-friendly,1,\
+ $(sqlite3-api-bundler-friendly.mjs),$(sqlite3-bundler-friendly.mjs),\
+ $(c-pp.D.sqlite3-esm) -Dtarget=es6-bundler-friendly))
+$(eval $(call SETUP_LIB_BUILD_MODE,sqlite3,node,1,\
+ $(sqlite3-api-node.mjs),$(sqlite3-node.mjs),\
+ $(c-pp.D.sqlite3-bundler-friendly) -Dtarget=node))
+# The various -D... values used by *.c-pp.js include:
+#
+# -Dtarget=es6-module: for all ESM module builds
+#
+# -Dtarget=node: for node.js builds
+#
+# -Dtarget=es6-module -Dtarget=es6-bundler-friendly: intended for
+# "bundler-friendly" ESM module build. These have some restrictions
+# on how URL() objects are constructed in some contexts: URLs which
+# refer to files which are part of this project must be referenced
+# as string literals so that bundlers' static-analysis tools can
+# find those files and include them in their bundles.
+#
+# -Dtarget=es6-module -Dtarget=es6-bundler-friendly -Dtarget=node: is
+# intended for use by node.js for node.js, as opposed to by
+# node.js on behalf of a browser. Mixing -sENVIRONMENT=web and
+# -sENVIRONMENT=node leads to ambiguity and confusion on node's
+# part, as it's unable to reliably determine whether the target is
+# a browser or node.
+#
+########################################################################
+########################################################################
+# We have to ensure that we do not build $(sqlite3*.*js) in parallel
+# because they all result in the creation of $(sqlite3.wasm). We have
+# no way to build just a .[m]js file without also building the .wasm
+# file because the generated .[m]js file has to include info about the
+# imports needed by the wasm file, so they have to be built
+# together. i.e. we're building $(sqlite3.wasm) multiple times, but
+# that's unavoidable (and harmless, just a waste of build time).
+$(sqlite3.wasm): $(sqlite3.js)
+$(sqlite3.mjs): $(sqlite3.js)
+$(sqlite3-bundler-friendly.mjs): $(sqlite3.mjs)
+$(sqlite3-node.mjs): $(sqlite3.mjs)
+CLEAN_FILES += $(sqlite3.wasm)
+
+########################################################################
+# We need separate copies of certain supplementary JS files for the
+# bundler-friendly build. Concretely, any supplemental JS files which
+# themselves use importScripts() or Workers or URL() constructors
+# which refer to other in-tree (m)JS files quire a bundler-friendly
+# copy.
+sqlite3-worker1.js.in := $(dir.api)/sqlite3-worker1.c-pp.js
+sqlite3-worker1-promiser.js.in := $(dir.api)/sqlite3-worker1-promiser.c-pp.js
+sqlite3-worker1.js := $(dir.dout)/sqlite3-worker1.js
+sqlite3-worker1-promiser.js := $(dir.dout)/sqlite3-worker1-promiser.js
+sqlite3-worker1-bundler-friendly.js := $(dir.dout)/sqlite3-worker1-bundler-friendly.mjs
+sqlite3-worker1-promiser-bundler-friendly.js := $(dir.dout)/sqlite3-worker1-promiser-bundler-friendly.js
+$(eval $(call C-PP.FILTER,$(sqlite3-worker1.js.in),$(sqlite3-worker1.js)))
+$(eval $(call C-PP.FILTER,$(sqlite3-worker1.js.in),$(sqlite3-worker1-bundler-friendly.js),\
+ $(c-pp.D.sqlite3-bundler-friendly)))
+$(eval $(call C-PP.FILTER,$(sqlite3-worker1-promiser.js.in),$(sqlite3-worker1-promiser.js)))
+$(eval $(call C-PP.FILTER,$(sqlite3-worker1-promiser.js.in),\
+ $(sqlite3-worker1-promiser-bundler-friendly.js),\
+ $(c-pp.D.sqlite3-bundler-friendly)))
+$(sqlite3-bundler-friendly.mjs): $(sqlite3-worker1-bundler-friendly.js) \
+ $(sqlite3-worker1-promiser-bundler-friendly.js)
+$(sqlite3.js) $(sqlite3.mjs): $(sqlite3-worker1.js) $(sqlite3-worker1-promiser.js)
+
+########################################################################
+# batch-runner.js is part of one of the test apps which reads in SQL
+# dumps generated by $(speedtest1) and executes them.
+dir.sql := sql
+speedtest1 := ../../speedtest1
+speedtest1.c := ../../test/speedtest1.c
+speedtest1.sql := $(dir.sql)/speedtest1.sql
+speedtest1.cliflags := --size 10 --big-transactions
+$(speedtest1):
+ $(MAKE) -C ../.. speedtest1
+$(speedtest1.sql): $(speedtest1) $(MAKEFILE)
+ $(speedtest1) $(speedtest1.cliflags) --script $@
+batch-runner.list: $(MAKEFILE) $(speedtest1.sql) $(dir.sql)/000-mandelbrot.sql
+ bash split-speedtest1-script.sh $(dir.sql)/speedtest1.sql
+ ls -1 $(dir.sql)/*.sql | grep -v speedtest1.sql | sort > $@
+clean-batch:
+ rm -f batch-runner.list $(dir.sql)/speedtest1*.sql
+# ^^^ we don't do this along with 'clean' because we clean/rebuild on
+# a regular basis with different -Ox flags and rebuilding the batch
+# pieces each time is an unnecessary time sink.
+batch: batch-runner.list
+#all: batch
+# end batch-runner.js
+########################################################################
+# Wasmified speedtest1 is our primary benchmarking tool.
+#
+# emcc.speedtest1.common = emcc flags used by multiple builds of speedtest1
+# emcc.speedtest1 = emcc flags used by main build of speedtest1
+emcc.speedtest1.common := $(emcc_opt_full)
+emcc.speedtest1 := -I. -I$(dir $(sqlite3.canonical.c))
+emcc.speedtest1 += -sENVIRONMENT=web
+emcc.speedtest1 += -sALLOW_MEMORY_GROWTH
+emcc.speedtest1 += -sINITIAL_MEMORY=$(emcc.INITIAL_MEMORY.$(emcc.INITIAL_MEMORY))
+emcc.speedtest1.common += -sINVOKE_RUN=0
+emcc.speedtest1.common += --no-entry
+emcc.speedtest1.common += -sABORTING_MALLOC
+emcc.speedtest1.common += -sSTRICT_JS=0
+emcc.speedtest1.common += -sMODULARIZE
+emcc.speedtest1.common += -Wno-limited-postlink-optimizations
+EXPORTED_FUNCTIONS.speedtest1 := $(abspath $(dir.tmp)/EXPORTED_FUNCTIONS.speedtest1)
+emcc.speedtest1.common += -sSTACK_SIZE=512KB
+emcc.speedtest1.common += -sEXPORTED_FUNCTIONS=@$(EXPORTED_FUNCTIONS.speedtest1)
+emcc.speedtest1.common += $(emcc.exportedRuntimeMethods)
+emcc.speedtest1.common += -sALLOW_TABLE_GROWTH
+emcc.speedtest1.common += -sDYNAMIC_EXECUTION=0
+emcc.speedtest1.common += --minify 0
+emcc.speedtest1.common += -sEXPORT_NAME=$(sqlite3.js.init-func)
+emcc.speedtest1.common += -sWASM_BIGINT=$(emcc.WASM_BIGINT)
+speedtest1.exit-runtime0 := -sEXIT_RUNTIME=0
+speedtest1.exit-runtime1 := -sEXIT_RUNTIME=1
+# Re -sEXIT_RUNTIME=1 vs 0: if it's 1 and speedtest1 crashes, we get
+# this error from emscripten:
+#
+# > native function `free` called after runtime exit (use
+# NO_EXIT_RUNTIME to keep it alive after main() exits))
+#
+# If it's 0 and it crashes, we get:
+#
+# > stdio streams had content in them that was not flushed. you should
+# set EXIT_RUNTIME to 1 (see the FAQ), or make sure to emit a newline
+# when you printf etc.
+#
+# and pending output is not flushed because it didn't end with a
+# newline (by design). The lesser of the two evils seems to be
+# -sEXIT_RUNTIME=1 but we need EXIT_RUNTIME=0 for the worker-based app
+# which runs speedtest1 multiple times.
+
+$(EXPORTED_FUNCTIONS.speedtest1): $(EXPORTED_FUNCTIONS.api.main)
+ @echo "Making $@ ..."
+ @{ echo _wasm_main; cat $(EXPORTED_FUNCTIONS.api.main); } > $@
+speedtest1.js := $(dir.dout)/speedtest1.js
+speedtest1.wasm := $(dir.dout)/speedtest1.wasm
+emcc.flags.speedtest1-vanilla := $(cflags.common) -DSQLITE_SPEEDTEST1_WASM
+
+speedtest1.cfiles := $(speedtest1.c) $(sqlite3-wasm.c)
+$(eval $(call call-make-pre-post,speedtest1,vanilla))
+$(speedtest1.js): $(MAKEFILE) $(speedtest1.cfiles) \
+ $(pre-post-speedtest1-vanilla.deps) \
+ $(EXPORTED_FUNCTIONS.speedtest1)
+ @echo "Building $@ ..."
+ $(emcc.bin) \
+ $(emcc.speedtest1) \
+ $(emcc.speedtest1.common) \
+ $(emcc.flags.speedtest1-vanilla) $(pre-post-speedtest1-vanilla.flags) \
+ $(SQLITE_OPT) \
+ -USQLITE_C -DSQLITE_C=$(sqlite3.canonical.c) \
+ $(speedtest1.exit-runtime0) \
+ -o $@ $(speedtest1.cfiles) -lm
+ $(maybe-wasm-strip) $(speedtest1.wasm)
+ chmod -x $(speedtest1.wasm)
+ ls -la $@ $(speedtest1.wasm)
+
+speedtest1: $(speedtest1.js)
+all: speedtest1
+CLEAN_FILES += $(speedtest1.js) $(speedtest1.wasm)
+# end speedtest1.js
+########################################################################
+
+########################################################################
+# tester1 is the main unit and regression test application and needs
+# to be able to run in 4 separate modes to cover the primary
+# client-side use cases:
+#
+# 1) Load sqlite3 in the main UI thread of a conventional script.
+# 2) Load sqlite3 in a conventional Worker thread.
+# 3) Load sqlite3 as an ES6 module (ESM) in the main thread.
+# 4) Load sqlite3 as an ESM worker. (Not all browsers support this.)
+#
+# To that end, we require two separate builds of tester1.js:
+#
+# tester1.js: cases 1 and 2
+# tester1.mjs: cases 3 and 4
+#
+# To create those, we filter tester1.c-pp.js with $(bin.c-pp)...
+$(eval $(call C-PP.FILTER,tester1.c-pp.js,tester1.js))
+$(eval $(call C-PP.FILTER,tester1.c-pp.js,tester1.mjs,$(c-pp.D.sqlite3-esm)))
+$(eval $(call C-PP.FILTER,tester1.c-pp.html,tester1.html))
+$(eval $(call C-PP.FILTER,tester1.c-pp.html,tester1-esm.html,$(c-pp.D.sqlite3-esm)))
+tester1: tester1.js tester1.mjs tester1.html tester1-esm.html
+# Note that we do not include $(sqlite3-bundler-friendly.mjs) in this
+# because bundlers are client-specific.
+all quick: tester1
+quick: $(sqlite3.js)
+
+########################################################################
+# Convenience rules to rebuild with various -Ox levels. Much
+# experimentation shows -O2 to be the clear winner in terms of speed.
+# Note that build times with anything higher than -O0 are somewhat
+# painful.
+
+.PHONY: o0 o1 o2 o3 os oz
+o-xtra :=
+#o-xtra ?= -flto
+# ^^^^ -flto can have a considerably performance boost at -O0 but
+# doubles the build time and seems to have negligible, if any, effect
+# on higher optimization levels.
+o0: clean
+ $(MAKE) -e "emcc_opt=-O0"
+o1: clean
+ $(MAKE) -e "emcc_opt=-O1 $(o-xtra)"
+o2: clean
+ $(MAKE) -j2 -e "emcc_opt=-O2 $(o-xtra)"
+o3: clean
+ $(MAKE) -e "emcc_opt=-O3 $(o-xtra)"
+os: clean
+ @echo "WARNING: -Os can result in a build with mysteriously missing pieces!"
+ $(MAKE) -e "emcc_opt=-Os $(o-xtra)"
+oz: clean
+ $(MAKE) -j2 -e "emcc_opt=-Oz $(o-xtra)"
+
+########################################################################
+# Sub-makes...
+
+# sqlite.org/fiddle application...
+include fiddle.make
+
+# Only add wasmfs if wasmfs.enable=1 or we're running (dist)clean
+ifneq (,$(filter wasmfs,$(MAKECMDGOALS)))
+wasmfs.enable ?= 1
+else
+# Unconditionally enable wasmfs for [dist]clean so that the wasmfs
+# sub-make can clean up.
+wasmfs.enable ?= $(if $(filter %clean,$(MAKECMDGOALS)),1,0)
+endif
+ifeq (1,$(wasmfs.enable))
+# wasmfs build disabled 2022-10-19 per /chat discussion.
+# OPFS-over-wasmfs was initially a stopgap measure and a convenient
+# point of comparison for the OPFS sqlite3_vfs's performance, but it
+# currently doubles our deliverables and build maintenance burden for
+# little benefit.
+#
+########################################################################
+# Some platforms do not support the WASMFS build. Raspberry Pi OS is one
+# of them. As such platforms are discovered, add their (uname -m) name
+# to PLATFORMS_WITH_NO_WASMFS to exclude the wasmfs build parts.
+PLATFORMS_WITH_NO_WASMFS := aarch64 # add any others here
+THIS_ARCH := $(shell /usr/bin/uname -m)
+ifneq (,$(filter $(THIS_ARCH),$(PLATFORMS_WITH_NO_WASMFS)))
+$(info This platform does not support the WASMFS build.)
+HAVE_WASMFS := 0
+else
+HAVE_WASMFS := 1
+include wasmfs.make
+endif
+endif
+# /wasmfs
+########################################################################
+
+########################################################################
+# Push files to public wasm-testing.sqlite.org server
+wasm-testing.include = *.js *.mjs *.html \
+ ./tests \
+ $(dir.dout) $(dir.common) $(dir.fiddle) $(dir.jacc)
+wasm-testing.exclude = sql/speedtest1.sql
+wasm-testing.dir = /jail/sites/wasm-testing
+wasm-testing.dest ?= wasm-testing:$(wasm-testing.dir)
+# ---------------------^^^^^^^^^^^^ ssh alias
+.PHONY: push-testing
+push-testing:
+ rsync -z -e ssh --ignore-times --chown=stephan:www-data --group -r \
+ $(patsubst %,--exclude=%,$(wasm-testing.exclude)) \
+ $(wasm-testing.include) $(wasm-testing.dest)
+ @echo "Updating gzipped copies..."; \
+ ssh wasm-testing 'cd $(wasm-testing.dir) && bash .gzip' || \
+ echo "SSH failed: it's likely that stale content will be served via old gzip files."
+
+########################################################################
+# If we find a copy of the sqlite.org/wasm docs checked out, copy
+# certain files over to it, noting that some need automatable edits...
+wasm.docs.home ?= ../../../wasm
+wasm.docs.found = $(if $(wildcard $(wasm.docs.home)/api-index.md),\
+ $(wildcard $(wasm.docs.home)),)
+.PHONY: update-docs
+ifeq (,$(wasm.docs.found))
+update-docs:
+ @echo "Cannot find wasm docs checkout."; \
+ echo "Pass wasm.docs.home=/path/to/wasm/docs/checkout or edit this makefile to suit."; \
+ exit 127
+else
+wasm.docs.jswasm := $(wasm.docs.home)/jswasm
+update-docs: $(bin.stripccomments) $(sqlite3.js) $(sqlite3.wasm)
+ @echo "Copying files to the /wasm docs. Be sure to use an -Oz build for this!"
+ cp $(sqlite3.wasm) $(wasm.docs.jswasm)/.
+ $(bin.stripccomments) -k -k < $(sqlite3.js) \
+ | sed -e '/^[ \t]*$$/d' > $(wasm.docs.jswasm)/sqlite3.js
+ cp demo-123.js demo-123.html demo-123-worker.html $(wasm.docs.home)
+ sed -n -e '/EXTRACT_BEGIN/,/EXTRACT_END/p' \
+ module-symbols.html > $(wasm.docs.home)/module-symbols.html
+endif
+# end /wasm docs
+########################################################################
+
+########################################################################
+# Create main client downloadable zip file:
+ifneq (,$(filter dist snapshot,$(MAKECMDGOALS)))
+include dist.make
+endif
+
+# Run local web server for the test/demo pages.
+httpd:
+ althttpd -max-age 1 -enable-sab 1 -page index.html
diff --git a/ext/wasm/README-dist.txt b/ext/wasm/README-dist.txt
new file mode 100644
index 0000000..6656a20
--- /dev/null
+++ b/ext/wasm/README-dist.txt
@@ -0,0 +1,46 @@
+This is the README for the sqlite3 WASM/JS distribution.
+
+Main project page: https://sqlite.org
+
+Documentation: https://sqlite.org/wasm
+
+This archive contains the following deliverables for the WASM/JS
+build:
+
+- jswasm/sqlite3.js is the canonical "vanilla JS" version.
+
+- jswasm/sqlite3.mjs is the same but in ES6 module form
+
+- jswasm/*-bundler-friendly.js and .mjs are variants which are
+ intended to be compatible with "bundler" tools commonly seen in
+ node.js-based projects. Projects using such tools should use those
+ variants, where available, instead of files without the
+ "-bundler-friendly" suffix. Some files do not have separate
+ variants.
+
+- jswasm/sqlite3.wasm is the binary WASM file imported by all of the
+ above-listed JS files.
+
+- The jswasm directory additionally contains a number of supplemental
+ JS files which cannot be bundled directly with the main JS files
+ but are necessary for certain usages.
+
+- The top-level directory contains various demonstration and test
+ applications for sqlite3.js and sqlite3.mjs.
+ sqlite3-bundler-friendly.mjs requires client-side build tools to make
+ use of and is not demonstrated here.
+
+Browsers will not serve WASM files from file:// URLs, so the test and
+demonstration apps require a web server and that server must include
+the following headers in its response when serving the files:
+
+ Cross-Origin-Opener-Policy: same-origin
+ Cross-Origin-Embedder-Policy: require-corp
+
+The core library will function without those headers but certain
+features, most notably OPFS storage, will not be available.
+
+One simple way to get the demo apps up and running on Unix-style
+systems is to install althttpd (https://sqlite.org/althttpd) and run:
+
+ althttpd --enable-sab --page index.html
diff --git a/ext/wasm/README.md b/ext/wasm/README.md
new file mode 100644
index 0000000..0c32831
--- /dev/null
+++ b/ext/wasm/README.md
@@ -0,0 +1,106 @@
+This directory houses the [Web Assembly (WASM)](https://en.wikipedia.org/wiki/WebAssembly)
+parts of the sqlite3 build.
+
+It requires [emscripten][] and that the build environment be set up for
+emscripten. A mini-HOWTO for setting that up follows...
+
+First, install the Emscripten SDK, as documented
+[here](https://emscripten.org/docs/getting_started/downloads.html) and summarized
+below for Linux environments:
+
+```
+# Clone the emscripten repository:
+$ sudo apt install git
+$ git clone https://github.com/emscripten-core/emsdk.git
+$ cd emsdk
+
+# Download and install the latest SDK tools:
+$ ./emsdk install latest
+
+# Make the "latest" SDK "active" for the current user:
+$ ./emsdk activate latest
+```
+
+Those parts only need to be run once, but the SDK can be updated using:
+
+```
+$ git pull
+$ ./emsdk install latest
+$ ./emsdk activate latest
+```
+
+The following needs to be run for each shell instance which needs the
+`emcc` compiler:
+
+```
+# Activate PATH and other environment variables in the current terminal:
+$ source ./emsdk_env.sh
+
+$ which emcc
+/path/to/emsdk/upstream/emscripten/emcc
+```
+
+Optionally, add that to your login shell's resource file (`~/.bashrc`
+or equivalent).
+
+That `env` script needs to be sourced for building this application
+from the top of the sqlite3 build tree:
+
+```
+$ make fiddle
+```
+
+Or:
+
+```
+$ cd ext/wasm
+$ make
+```
+
+That will generate the a number of files required for a handful of
+test and demo applications which can be accessed via
+`index.html`. WASM content cannot, due to XMLHttpRequest security
+limitations, be loaded if the containing HTML file is opened directly
+in the browser (i.e. if it is opened using a `file://` URL), so it
+needs to be served via an HTTP server. For example, using
+[althttpd][]:
+
+```
+$ cd ext/wasm
+$ althttpd --enable-sab --max-age 1 --page index.html
+```
+
+That will open the system's browser and run the index page, from which
+all of the test and demo applications can be accessed.
+
+Note that when serving this app via [althttpd][], it must be a version
+from 2022-09-26 or newer so that it recognizes the `--enable-sab`
+flag, which causes althttpd to emit two HTTP response headers which
+are required to enable JavaScript's `SharedArrayBuffer` and `Atomics`
+APIs. Those APIs are required in order to enable the OPFS-related
+features in the apps which use them.
+
+# Testing on a remote machine that is accessed via SSH
+
+*NB: The following are developer notes, last validated on 2023-07-19*
+
+ * Remote: Install git, emsdk, and althttpd
+ * Use a [version of althttpd][althttpd] from
+ September 26, 2022 or newer.
+ * Remote: Install the SQLite source tree. CD to ext/wasm
+ * Remote: "`make`" to build WASM
+ * Remote: `althttpd --enable-sab --port 8080 --popup`
+ * Local: `ssh -L 8180:localhost:8080 remote`
+ * Local: Point your web-browser at http://localhost:8180/index.html
+
+In order to enable [SharedArrayBuffer][], the web-browser requires
+that the two extra Cross-Origin lines be present in HTTP reply headers
+and that the request must come from "localhost" (_or_ over an SSL
+connection). Since the web-server is on a different machine from the
+web-broser, the localhost requirement means that the connection must
+be tunneled using SSH.
+
+
+[emscripten]: https://emscripten.org
+[althttpd]: https://sqlite.org/althttpd
+[SharedArrayBuffer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer
diff --git a/ext/wasm/SQLTester/GNUmakefile b/ext/wasm/SQLTester/GNUmakefile
new file mode 100644
index 0000000..8fa1247
--- /dev/null
+++ b/ext/wasm/SQLTester/GNUmakefile
@@ -0,0 +1,55 @@
+#!/this/is/make
+#
+# This makefile compiles SQLTester test files into something
+# we can readily import into JavaScript.
+all:
+
+SHELL := $(shell which bash 2>/dev/null)
+MAKEFILE := $(lastword $(MAKEFILE_LIST))
+CLEAN_FILES :=
+DISTCLEAN_FILES := ./--dummy-- *~
+
+test-list.mjs := test-list.mjs
+test-list.mjs.gz := $(test-list.mjs).gz
+CLEAN_FILES += $(test-list.mjs)
+
+tests.dir := $(firstword $(wildcard tests ../../jni/src/tests))
+$(info test script dir=$(tests.dir))
+
+tests.all := $(wildcard $(tests.dir)/*.test)
+
+bin.touint8array := ./touint8array
+$(bin.touint8array): $(bin.touint8array).c $(MAKEFILE)
+ $(CC) -o $@ $<
+CLEAN_FILES += $(bin.touint8array)
+
+ifneq (,$(tests.all))
+$(test-list.mjs): $(bin.touint8array) $(tests.all) $(MAKEFILE)
+ @{\
+ echo 'export default ['; \
+ sep=''; \
+ for f in $(sort $(tests.all)); do \
+ echo -en $$sep'{"name": "'$${f##*/}'", "content":'; \
+ $(bin.touint8array) < $$f; \
+ echo -n '}'; \
+ sep=',\n'; \
+ done; \
+ echo '];'; \
+ } > $@
+ @echo "Created $@"
+$(test-list.mjs.gz): $(test-list.mjs)
+ gzip -c $< > $@
+CLEAN_FILES += $(test-list.mjs.gz)
+all: $(test-list.mjs.gz)
+else
+ @echo "Cannot build $(test-list.mjs) for lack of input test files."; \
+ echo "Symlink ./tests to a directory containing SQLTester-format "; \
+ echo "test scripts named *.test, then try again"; \
+ exit 1
+endif
+
+.PHONY: clean distclean
+clean:
+ -rm -f $(CLEAN_FILES)
+distclean: clean
+ -rm -f $(DISTCLEAN_FILES)
diff --git a/ext/wasm/SQLTester/SQLTester.mjs b/ext/wasm/SQLTester/SQLTester.mjs
new file mode 100644
index 0000000..a72399a
--- /dev/null
+++ b/ext/wasm/SQLTester/SQLTester.mjs
@@ -0,0 +1,1339 @@
+/*
+** 2023-08-29
+**
+** 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 contains the main application entry pointer for the JS
+** implementation of the SQLTester framework.
+**
+** This version is not well-documented because it's a direct port of
+** the Java immplementation, which is documented: in the main SQLite3
+** source tree, see ext/jni/src/org/sqlite/jni/tester/SQLite3Tester.java.
+*/
+
+import sqlite3ApiInit from '/jswasm/sqlite3.mjs';
+
+const sqlite3 = await sqlite3ApiInit();
+
+const log = (...args)=>{
+ console.log('SQLTester:',...args);
+};
+
+/**
+ Try to install vfsName as the new default VFS. Once this succeeds
+ (returns true) then it becomes a no-op on future calls. Throws if
+ vfs registration as the default VFS fails but has no side effects
+ if vfsName is not currently registered.
+*/
+const tryInstallVfs = function f(vfsName){
+ if(f.vfsName) return false;
+ const pVfs = sqlite3.capi.sqlite3_vfs_find(vfsName);
+ if(pVfs){
+ log("Installing",'"'+vfsName+'"',"as default VFS.");
+ const rc = sqlite3.capi.sqlite3_vfs_register(pVfs, 1);
+ if(rc){
+ sqlite3.SQLite3Error.toss(rc,"While trying to register",vfsName,"vfs.");
+ }
+ f.vfsName = vfsName;
+ }
+ return !!pVfs;
+};
+tryInstallVfs.vfsName = undefined;
+
+if( 0 && globalThis.WorkerGlobalScope ){
+ // Try OPFS storage, if available...
+ if( 0 && sqlite3.oo1.OpfsDb ){
+ /* Really slow with these tests */
+ tryInstallVfs("opfs");
+ }
+ if( sqlite3.installOpfsSAHPoolVfs ){
+ await sqlite3.installOpfsSAHPoolVfs({
+ clearOnInit: true,
+ initialCapacity: 15,
+ name: 'opfs-SQLTester'
+ }).then(pool=>{
+ tryInstallVfs(pool.vfsName);
+ }).catch(e=>{
+ log("OpfsSAHPool could not load:",e);
+ });
+ }
+}
+
+const wPost = (function(){
+ return (('undefined'===typeof WorkerGlobalScope)
+ ? ()=>{}
+ : (type, payload)=>{
+ postMessage({type, payload});
+ });
+})();
+//log("WorkerGlobalScope",globalThis.WorkerGlobalScope);
+
+// Return a new enum entry value
+const newE = ()=>Object.create(null);
+
+const newObj = (props)=>Object.assign(newE(), props);
+
+/**
+ Modes for how to escape (or not) column values and names from
+ SQLTester.execSql() to the result buffer output.
+*/
+const ResultBufferMode = Object.assign(Object.create(null),{
+ //! Do not append to result buffer
+ NONE: newE(),
+ //! Append output escaped.
+ ESCAPED: newE(),
+ //! Append output as-is
+ ASIS: newE()
+});
+
+/**
+ Modes to specify how to emit multi-row output from
+ SQLTester.execSql() to the result buffer.
+*/
+const ResultRowMode = newObj({
+ //! Keep all result rows on one line, space-separated.
+ ONLINE: newE(),
+ //! Add a newline between each result row.
+ NEWLINE: newE()
+});
+
+class SQLTesterException extends globalThis.Error {
+ constructor(testScript, ...args){
+ if(testScript){
+ super( [testScript.getOutputPrefix()+": ", ...args].join('') );
+ }else{
+ super( args.join('') );
+ }
+ this.name = 'SQLTesterException';
+ }
+ isFatal() { return false; }
+}
+
+SQLTesterException.toss = (...args)=>{
+ throw new SQLTesterException(...args);
+}
+
+class DbException extends SQLTesterException {
+ constructor(testScript, pDb, rc, closeDb=false){
+ super(testScript, "DB error #"+rc+": "+sqlite3.capi.sqlite3_errmsg(pDb));
+ this.name = 'DbException';
+ if( closeDb ) sqlite3.capi.sqlite3_close_v2(pDb);
+ }
+ isFatal() { return true; }
+}
+
+class TestScriptFailed extends SQLTesterException {
+ constructor(testScript, ...args){
+ super(testScript,...args);
+ this.name = 'TestScriptFailed';
+ }
+ isFatal() { return true; }
+}
+
+class UnknownCommand extends SQLTesterException {
+ constructor(testScript, cmdName){
+ super(testScript, cmdName);
+ this.name = 'UnknownCommand';
+ }
+ isFatal() { return true; }
+}
+
+class IncompatibleDirective extends SQLTesterException {
+ constructor(testScript, ...args){
+ super(testScript,...args);
+ this.name = 'IncompatibleDirective';
+ }
+}
+
+//! For throwing where an expression is required.
+const toss = (errType, ...args)=>{
+ throw new errType(...args);
+};
+
+const __utf8Decoder = new TextDecoder();
+const __utf8Encoder = new TextEncoder('utf-8');
+//! Workaround for Util.utf8Decode()
+const __SAB = ('undefined'===typeof globalThis.SharedArrayBuffer)
+ ? function(){} : globalThis.SharedArrayBuffer;
+
+
+/* Frequently-reused regexes. */
+const Rx = newObj({
+ requiredProperties: / REQUIRED_PROPERTIES:[ \t]*(\S.*)\s*$/,
+ scriptModuleName: / SCRIPT_MODULE_NAME:[ \t]*(\S+)\s*$/,
+ mixedModuleName: / ((MIXED_)?MODULE_NAME):[ \t]*(\S+)\s*$/,
+ command: /^--(([a-z-]+)( .*)?)$/,
+ //! "Special" characters - we have to escape output if it contains any.
+ special: /[\x00-\x20\x22\x5c\x7b\x7d]/,
+ squiggly: /[{}]/
+});
+
+const Util = newObj({
+ toss,
+
+ unlink: function(fn){
+ return 0==sqlite3.wasm.sqlite3_wasm_vfs_unlink(0,fn);
+ },
+
+ argvToString: (list)=>{
+ const m = [...list];
+ m.shift() /* strip command name */;
+ return m.join(" ")
+ },
+
+ utf8Decode: function(arrayBuffer, begin, end){
+ return __utf8Decoder.decode(
+ (arrayBuffer.buffer instanceof __SAB)
+ ? arrayBuffer.slice(begin, end)
+ : arrayBuffer.subarray(begin, end)
+ );
+ },
+
+ utf8Encode: (str)=>__utf8Encoder.encode(str),
+
+ strglob: sqlite3.wasm.xWrap('sqlite3_wasm_SQLTester_strglob','int',
+ ['string','string'])
+})/*Util*/;
+
+class Outer {
+ #lnBuf = [];
+ #verbosity = 0;
+ #logger = console.log.bind(console);
+
+ constructor(func){
+ if(func) this.setFunc(func);
+ }
+
+ logger(...args){
+ if(args.length){
+ this.#logger = args[0];
+ return this;
+ }
+ return this.#logger;
+ }
+
+ out(...args){
+ if( this.getOutputPrefix && !this.#lnBuf.length ){
+ this.#lnBuf.push(this.getOutputPrefix());
+ }
+ this.#lnBuf.push(...args);
+ return this;
+ }
+
+ #outlnImpl(vLevel, ...args){
+ if( this.getOutputPrefix && !this.#lnBuf.length ){
+ this.#lnBuf.push(this.getOutputPrefix());
+ }
+ this.#lnBuf.push(...args,'\n');
+ const msg = this.#lnBuf.join('');
+ this.#lnBuf.length = 0;
+ this.#logger(msg);
+ return this;
+ }
+
+ outln(...args){
+ return this.#outlnImpl(0,...args);
+ }
+
+ outputPrefix(){
+ if( 0==arguments.length ){
+ return (this.getOutputPrefix
+ ? (this.getOutputPrefix() ?? '') : '');
+ }else{
+ this.getOutputPrefix = arguments[0];
+ return this;
+ }
+ }
+
+ static #verboseLabel = ["🔈",/*"🔉",*/"🔊","📢"];
+ verboseN(lvl, args){
+ if( this.#verbosity>=lvl ){
+ this.#outlnImpl(lvl, Outer.#verboseLabel[lvl-1],': ',...args);
+ }
+ }
+ verbose1(...args){ return this.verboseN(1,args); }
+ verbose2(...args){ return this.verboseN(2,args); }
+ verbose3(...args){ return this.verboseN(3,args); }
+
+ verbosity(){
+ const rc = this.#verbosity;
+ if(arguments.length) this.#verbosity = +arguments[0];
+ return rc;
+ }
+
+}/*Outer*/
+
+class SQLTester {
+
+ //! Console output utility.
+ #outer = new Outer().outputPrefix( ()=>'SQLTester: ' );
+ //! List of input scripts.
+ #aScripts = [];
+ //! Test input buffer.
+ #inputBuffer = [];
+ //! Test result buffer.
+ #resultBuffer = [];
+ //! Output representation of SQL NULL.
+ #nullView;
+ metrics = newObj({
+ //! Total tests run
+ nTotalTest: 0,
+ //! Total test script files run
+ nTestFile: 0,
+ //! Test-case count for to the current TestScript
+ nTest: 0,
+ //! Names of scripts which were aborted.
+ failedScripts: []
+ });
+ #emitColNames = false;
+ //! True to keep going regardless of how a test fails.
+ #keepGoing = false;
+ #db = newObj({
+ //! The list of available db handles.
+ list: new Array(7),
+ //! Index into this.list of the current db.
+ iCurrentDb: 0,
+ //! Name of the default db, re-created for each script.
+ initialDbName: "test.db",
+ //! Buffer for REQUIRED_PROPERTIES pragmas.
+ initSql: ['select 1;'],
+ //! (sqlite3*) to the current db.
+ currentDb: function(){
+ return this.list[this.iCurrentDb];
+ }
+ });
+
+ constructor(){
+ this.reset();
+ }
+
+ outln(...args){ return this.#outer.outln(...args); }
+ out(...args){ return this.#outer.out(...args); }
+ outer(...args){
+ if(args.length){
+ this.#outer = args[0];
+ return this;
+ }
+ return this.#outer;
+ }
+ verbose1(...args){ return this.#outer.verboseN(1,args); }
+ verbose2(...args){ return this.#outer.verboseN(2,args); }
+ verbose3(...args){ return this.#outer.verboseN(3,args); }
+ verbosity(...args){
+ const rc = this.#outer.verbosity(...args);
+ return args.length ? this : rc;
+ }
+ setLogger(func){
+ this.#outer.logger(func);
+ return this;
+ }
+
+ incrementTestCounter(){
+ ++this.metrics.nTotalTest;
+ ++this.metrics.nTest;
+ }
+
+ reset(){
+ this.clearInputBuffer();
+ this.clearResultBuffer();
+ this.#clearBuffer(this.#db.initSql);
+ this.closeAllDbs();
+ this.metrics.nTest = 0;
+ this.#nullView = "nil";
+ this.emitColNames = false;
+ this.#db.iCurrentDb = 0;
+ //this.#db.initSql.push("SELECT 1;");
+ }
+
+ appendInput(line, addNL){
+ this.#inputBuffer.push(line);
+ if( addNL ) this.#inputBuffer.push('\n');
+ }
+ appendResult(line, addNL){
+ this.#resultBuffer.push(line);
+ if( addNL ) this.#resultBuffer.push('\n');
+ }
+ appendDbInitSql(sql){
+ this.#db.initSql.push(sql);
+ if( this.currentDb() ){
+ this.execSql(null, true, ResultBufferMode.NONE, null, sql);
+ }
+ }
+
+ #runInitSql(pDb){
+ let rc = 0;
+ for(const sql of this.#db.initSql){
+ this.#outer.verbose2("RUNNING DB INIT CODE: ",sql);
+ rc = this.execSql(pDb, false, ResultBufferMode.NONE, null, sql);
+ if( rc ) break;
+ }
+ return rc;
+ }
+
+#clearBuffer(buffer){
+ buffer.length = 0;
+ return buffer;
+ }
+
+ clearInputBuffer(){ return this.#clearBuffer(this.#inputBuffer); }
+ clearResultBuffer(){return this.#clearBuffer(this.#resultBuffer); }
+
+ getInputText(){ return this.#inputBuffer.join(''); }
+ getResultText(){ return this.#resultBuffer.join(''); }
+
+ #takeBuffer(buffer){
+ const s = buffer.join('');
+ buffer.length = 0;
+ return s;
+ }
+
+ takeInputBuffer(){
+ return this.#takeBuffer(this.#inputBuffer);
+ }
+ takeResultBuffer(){
+ return this.#takeBuffer(this.#resultBuffer);
+ }
+
+ nullValue(){
+ return (0==arguments.length)
+ ? this.#nullView
+ : (this.#nullView = ''+arguments[0]);
+ }
+
+ outputColumnNames(){
+ return (0==arguments.length)
+ ? this.#emitColNames
+ : (this.#emitColNames = !!arguments[0]);
+ }
+
+ currentDbId(){
+ return (0==arguments.length)
+ ? this.#db.iCurrentDb
+ : (this.#affirmDbId(arguments[0]).#db.iCurrentDb = arguments[0]);
+ }
+
+ #affirmDbId(id){
+ if(id<0 || id>=this.#db.list.length){
+ toss(SQLTesterException, "Database index ",id," is out of range.");
+ }
+ return this;
+ }
+
+ currentDb(...args){
+ if( 0!=args.length ){
+ this.#affirmDbId(id).#db.iCurrentDb = id;
+ }
+ return this.#db.currentDb();
+ }
+
+ getDbById(id){
+ return this.#affirmDbId(id).#db.list[id];
+ }
+
+ getCurrentDb(){ return this.#db.list[this.#db.iCurrentDb]; }
+
+
+ closeDb(id) {
+ if( 0==arguments.length ){
+ id = this.#db.iCurrentDb;
+ }
+ const pDb = this.#affirmDbId(id).#db.list[id];
+ if( pDb ){
+ sqlite3.capi.sqlite3_close_v2(pDb);
+ this.#db.list[id] = null;
+ }
+ }
+
+ closeAllDbs(){
+ for(let i = 0; i<this.#db.list.length; ++i){
+ if(this.#db.list[i]){
+ sqlite3.capi.sqlite3_close_v2(this.#db.list[i]);
+ this.#db.list[i] = null;
+ }
+ }
+ this.#db.iCurrentDb = 0;
+ }
+
+ openDb(name, createIfNeeded){
+ if( 3===arguments.length ){
+ const slot = arguments[0];
+ this.#affirmDbId(slot).#db.iCurrentDb = slot;
+ name = arguments[1];
+ createIfNeeded = arguments[2];
+ }
+ this.closeDb();
+ const capi = sqlite3.capi, wasm = sqlite3.wasm;
+ let pDb = 0;
+ let flags = capi.SQLITE_OPEN_READWRITE;
+ if( createIfNeeded ) flags |= capi.SQLITE_OPEN_CREATE;
+ try{
+ let rc;
+ wasm.pstack.call(function(){
+ let ppOut = wasm.pstack.allocPtr();
+ rc = sqlite3.capi.sqlite3_open_v2(name, ppOut, flags, null);
+ pDb = wasm.peekPtr(ppOut);
+ });
+ let sql;
+ if( 0==rc && this.#db.initSql.length > 0){
+ rc = this.#runInitSql(pDb);
+ }
+ if( 0!=rc ){
+ sqlite3.SQLite3Error.toss(
+ rc,
+ "sqlite3 result code",rc+":",
+ (pDb ? sqlite3.capi.sqlite3_errmsg(pDb)
+ : sqlite3.capi.sqlite3_errstr(rc))
+ );
+ }
+ return this.#db.list[this.#db.iCurrentDb] = pDb;
+ }catch(e){
+ sqlite3.capi.sqlite3_close_v2(pDb);
+ throw e;
+ }
+ }
+
+ addTestScript(ts){
+ if( 2===arguments.length ){
+ ts = new TestScript(arguments[0], arguments[1]);
+ }else if(ts instanceof Uint8Array){
+ ts = new TestScript('<unnamed>', ts);
+ }else if('string' === typeof arguments[1]){
+ ts = new TestScript('<unnamed>', Util.utf8Encode(arguments[1]));
+ }
+ if( !(ts instanceof TestScript) ){
+ Util.toss(SQLTesterException, "Invalid argument type for addTestScript()");
+ }
+ this.#aScripts.push(ts);
+ return this;
+ }
+
+ runTests(){
+ const tStart = (new Date()).getTime();
+ let isVerbose = this.verbosity();
+ this.metrics.failedScripts.length = 0;
+ this.metrics.nTotalTest = 0;
+ this.metrics.nTestFile = 0;
+ for(const ts of this.#aScripts){
+ this.reset();
+ ++this.metrics.nTestFile;
+ let threw = false;
+ const timeStart = (new Date()).getTime();
+ let msgTail = '';
+ try{
+ ts.run(this);
+ }catch(e){
+ if(e instanceof SQLTesterException){
+ threw = true;
+ this.outln("🔥EXCEPTION: ",e);
+ this.metrics.failedScripts.push({script: ts.filename(), message:e.toString()});
+ if( this.#keepGoing ){
+ this.outln("Continuing anyway because of the keep-going option.");
+ }else if( e.isFatal() ){
+ throw e;
+ }
+ }else{
+ throw e;
+ }
+ }finally{
+ const timeEnd = (new Date()).getTime();
+ this.out("🏁", (threw ? "❌" : "✅"), " ",
+ this.metrics.nTest, " test(s) in ",
+ (timeEnd-timeStart),"ms. ");
+ const mod = ts.moduleName();
+ if( mod ){
+ this.out( "[",mod,"] " );
+ }
+ this.outln(ts.filename());
+ }
+ }
+ const tEnd = (new Date()).getTime();
+ Util.unlink(this.#db.initialDbName);
+ this.outln("Took ",(tEnd-tStart),"ms. Test count = ",
+ this.metrics.nTotalTest,", script count = ",
+ this.#aScripts.length,(
+ this.metrics.failedScripts.length
+ ? ", failed scripts = "+this.metrics.failedScripts.length
+ : ""
+ )
+ );
+ return this;
+ }
+
+ #setupInitialDb(){
+ if( !this.#db.list[0] ){
+ Util.unlink(this.#db.initialDbName);
+ this.openDb(0, this.#db.initialDbName, true);
+ }else{
+ this.#outer.outln("WARNING: setupInitialDb() was unexpectedly ",
+ "triggered while it is opened.");
+ }
+ }
+
+ #escapeSqlValue(v){
+ if( !v ) return "{}";
+ if( !Rx.special.test(v) ){
+ return v /* no escaping needed */;
+ }
+ if( !Rx.squiggly.test(v) ){
+ return "{"+v+"}";
+ }
+ const sb = ["\""];
+ const n = v.length;
+ for(let i = 0; i < n; ++i){
+ const ch = v.charAt(i);
+ switch(ch){
+ case '\\': sb.push("\\\\"); break;
+ case '"': sb.push("\\\""); break;
+ default:{
+ //verbose("CHAR ",(int)ch," ",ch," octal=",String.format("\\%03o", (int)ch));
+ const ccode = ch.charCodeAt(i);
+ if( ccode < 32 ) sb.push('\\',ccode.toString(8),'o');
+ else sb.push(ch);
+ break;
+ }
+ }
+ }
+ sb.append("\"");
+ return sb.join('');
+ }
+
+ #appendDbErr(pDb, sb, rc){
+ sb.push(sqlite3.capi.sqlite3_js_rc_str(rc), ' ');
+ const msg = this.#escapeSqlValue(sqlite3.capi.sqlite3_errmsg(pDb));
+ if( '{' === msg.charAt(0) ){
+ sb.push(msg);
+ }else{
+ sb.push('{', msg, '}');
+ }
+ }
+
+ #checkDbRc(pDb,rc){
+ sqlite3.oo1.DB.checkRc(pDb, rc);
+ }
+
+ execSql(pDb, throwOnError, appendMode, rowMode, sql){
+ if( !pDb && !this.#db.list[0] ){
+ this.#setupInitialDb();
+ }
+ if( !pDb ) pDb = this.#db.currentDb();
+ const wasm = sqlite3.wasm, capi = sqlite3.capi;
+ sql = (sql instanceof Uint8Array)
+ ? sql
+ : Util.utf8Encode(capi.sqlite3_js_sql_to_string(sql));
+ const self = this;
+ const sb = (ResultBufferMode.NONE===appendMode) ? null : this.#resultBuffer;
+ let rc = 0;
+ wasm.scopedAllocCall(function(){
+ let sqlByteLen = sql.byteLength;
+ const ppStmt = wasm.scopedAlloc(
+ /* output (sqlite3_stmt**) arg and pzTail */
+ (2 * wasm.ptrSizeof) + (sqlByteLen + 1/* SQL + NUL */)
+ );
+ const pzTail = ppStmt + wasm.ptrSizeof /* final arg to sqlite3_prepare_v2() */;
+ let pSql = pzTail + wasm.ptrSizeof;
+ const pSqlEnd = pSql + sqlByteLen;
+ wasm.heap8().set(sql, pSql);
+ wasm.poke8(pSql + sqlByteLen, 0/*NUL terminator*/);
+ let pos = 0, n = 1, spacing = 0;
+ while( pSql && wasm.peek8(pSql) ){
+ wasm.pokePtr([ppStmt, pzTail], 0);
+ rc = capi.sqlite3_prepare_v3(
+ pDb, pSql, sqlByteLen, 0, ppStmt, pzTail
+ );
+ if( 0!==rc ){
+ if(throwOnError){
+ throw new DbException(self, pDb, rc);
+ }else if( sb ){
+ self.#appendDbErr(pDb, sb, rc);
+ }
+ break;
+ }
+ const pStmt = wasm.peekPtr(ppStmt);
+ pSql = wasm.peekPtr(pzTail);
+ sqlByteLen = pSqlEnd - pSql;
+ if(!pStmt) continue /* only whitespace or comments */;
+ if( sb ){
+ const nCol = capi.sqlite3_column_count(pStmt);
+ let colName, val;
+ while( capi.SQLITE_ROW === (rc = capi.sqlite3_step(pStmt)) ) {
+ for( let i=0; i < nCol; ++i ){
+ if( spacing++ > 0 ) sb.push(' ');
+ if( self.#emitColNames ){
+ colName = capi.sqlite3_column_name(pStmt, i);
+ switch(appendMode){
+ case ResultBufferMode.ASIS: sb.push( colName ); break;
+ case ResultBufferMode.ESCAPED:
+ sb.push( self.#escapeSqlValue(colName) );
+ break;
+ default:
+ self.toss("Unhandled ResultBufferMode.");
+ }
+ sb.push(' ');
+ }
+ val = capi.sqlite3_column_text(pStmt, i);
+ if( null===val ){
+ sb.push( self.#nullView );
+ continue;
+ }
+ switch(appendMode){
+ case ResultBufferMode.ASIS: sb.push( val ); break;
+ case ResultBufferMode.ESCAPED:
+ sb.push( self.#escapeSqlValue(val) );
+ break;
+ }
+ }/* column loop */
+ }/* row loop */
+ if( ResultRowMode.NEWLINE === rowMode ){
+ spacing = 0;
+ sb.push('\n');
+ }
+ }else{ // no output but possibly other side effects
+ while( capi.SQLITE_ROW === (rc = capi.sqlite3_step(pStmt)) ) {}
+ }
+ capi.sqlite3_finalize(pStmt);
+ if( capi.SQLITE_ROW===rc || capi.SQLITE_DONE===rc) rc = 0;
+ else if( rc!=0 ){
+ if( sb ){
+ self.#appendDbErr(db, sb, rc);
+ }
+ break;
+ }
+ }/* SQL script loop */;
+ })/*scopedAllocCall()*/;
+ return rc;
+ }
+
+}/*SQLTester*/
+
+class Command {
+ constructor(){
+ }
+
+ process(sqlTester,testScript,argv){
+ SQLTesterException.toss("process() must be overridden");
+ }
+
+ argcCheck(testScript,argv,min,max){
+ const argc = argv.length-1;
+ if(argc<min || (max>=0 && argc>max)){
+ if( min==max ){
+ testScript.toss(argv[0]," requires exactly ",min," argument(s)");
+ }else if(max>0){
+ testScript.toss(argv[0]," requires ",min,"-",max," arguments.");
+ }else{
+ testScript.toss(argv[0]," requires at least ",min," arguments.");
+ }
+ }
+ }
+}
+
+class Cursor {
+ src;
+ sb = [];
+ pos = 0;
+ //! Current line number. Starts at 0 for internal reasons and will
+ // line up with 1-based reality once parsing starts.
+ lineNo = 0 /* yes, zero */;
+ //! Putback value for this.pos.
+ putbackPos = 0;
+ //! Putback line number
+ putbackLineNo = 0;
+ //! Peeked-to pos, used by peekLine() and consumePeeked().
+ peekedPos = 0;
+ //! Peeked-to line number.
+ peekedLineNo = 0;
+
+ constructor(){
+ }
+
+ //! Restore parsing state to the start of the stream.
+ rewind(){
+ this.sb.length = this.pos = this.lineNo
+ = this.putbackPos = this.putbackLineNo
+ = this.peekedPos = this.peekedLineNo = 0;
+ }
+}
+
+class TestScript {
+ #cursor = new Cursor();
+ #moduleName = null;
+ #filename = null;
+ #testCaseName = null;
+ #outer = new Outer().outputPrefix( ()=>this.getOutputPrefix()+': ' );
+
+ constructor(...args){
+ let content, filename;
+ if( 2 == args.length ){
+ filename = args[0];
+ content = args[1];
+ }else if( 1 == args.length ){
+ if(args[0] instanceof Object){
+ const o = args[0];
+ filename = o.name;
+ content = o.content;
+ }else{
+ content = args[0];
+ }
+ }
+ if(!(content instanceof Uint8Array)){
+ if('string' === typeof content){
+ content = Util.utf8Encode(content);
+ }else if((content instanceof ArrayBuffer)
+ ||(content instanceof Array)){
+ content = new Uint8Array(content);
+ }else{
+ toss(Error, "Invalid content type for TestScript constructor.");
+ }
+ }
+ this.#filename = filename;
+ this.#cursor.src = content;
+ }
+
+ moduleName(){
+ return (0==arguments.length)
+ ? this.#moduleName : (this.#moduleName = arguments[0]);
+ }
+
+ testCaseName(){
+ return (0==arguments.length)
+ ? this.#testCaseName : (this.#testCaseName = arguments[0]);
+ }
+ filename(){
+ return (0==arguments.length)
+ ? this.#filename : (this.#filename = arguments[0]);
+ }
+
+ getOutputPrefix() {
+ let rc = "["+(this.#moduleName || '<unnamed>')+"]";
+ if( this.#testCaseName ) rc += "["+this.#testCaseName+"]";
+ if( this.#filename ) rc += '['+this.#filename+']';
+ return rc + " line "+ this.#cursor.lineNo;
+ }
+
+ reset(){
+ this.#testCaseName = null;
+ this.#cursor.rewind();
+ return this;
+ }
+
+ toss(...args){
+ throw new TestScriptFailed(this,...args);
+ }
+
+ verbose1(...args){ return this.#outer.verboseN(1,args); }
+ verbose2(...args){ return this.#outer.verboseN(2,args); }
+ verbose3(...args){ return this.#outer.verboseN(3,args); }
+ verbosity(...args){
+ const rc = this.#outer.verbosity(...args);
+ return args.length ? this : rc;
+ }
+
+ #checkRequiredProperties(tester, props){
+ if(true) return false;
+ let nOk = 0;
+ for(const rp of props){
+ this.verbose2("REQUIRED_PROPERTIES: ",rp);
+ switch(rp){
+ case "RECURSIVE_TRIGGERS":
+ tester.appendDbInitSql("pragma recursive_triggers=on;");
+ ++nOk;
+ break;
+ case "TEMPSTORE_FILE":
+ /* This _assumes_ that the lib is built with SQLITE_TEMP_STORE=1 or 2,
+ which we just happen to know is the case */
+ tester.appendDbInitSql("pragma temp_store=1;");
+ ++nOk;
+ break;
+ case "TEMPSTORE_MEM":
+ /* This _assumes_ that the lib is built with SQLITE_TEMP_STORE=1 or 2,
+ which we just happen to know is the case */
+ tester.appendDbInitSql("pragma temp_store=0;");
+ ++nOk;
+ break;
+ case "AUTOVACUUM":
+ tester.appendDbInitSql("pragma auto_vacuum=full;");
+ ++nOk;
+ break;
+ case "INCRVACUUM":
+ tester.appendDbInitSql("pragma auto_vacuum=incremental;");
+ ++nOk;
+ default:
+ break;
+ }
+ }
+ return props.length == nOk;
+ }
+
+ #checkForDirective(tester,line){
+ if(line.startsWith("#")){
+ throw new IncompatibleDirective(this, "C-preprocessor input: "+line);
+ }else if(line.startsWith("---")){
+ throw new IncompatibleDirective(this, "triple-dash: ",line);
+ }
+ let m = Rx.scriptModuleName.exec(line);
+ if( m ){
+ this.#moduleName = m[1];
+ return;
+ }
+ m = Rx.requiredProperties.exec(line);
+ if( m ){
+ const rp = m[1];
+ if( !this.#checkRequiredProperties( tester, rp.split(/\s+/).filter(v=>!!v) ) ){
+ throw new IncompatibleDirective(this, "REQUIRED_PROPERTIES: "+rp);
+ }
+ }
+
+ m = Rx.mixedModuleName.exec(line);
+ if( m ){
+ throw new IncompatibleDirective(this, m[1]+": "+m[3]);
+ }
+ if( line.indexOf("\n|")>=0 ){
+ throw new IncompatibleDirective(this, "newline-pipe combination.");
+ }
+
+ }
+
+ #getCommandArgv(line){
+ const m = Rx.command.exec(line);
+ return m ? m[1].trim().split(/\s+/) : null;
+ }
+
+
+ #isCommandLine(line, checkForImpl){
+ let m = Rx.command.exec(line);
+ if( m && checkForImpl ){
+ m = !!CommandDispatcher.getCommandByName(m[2]);
+ }
+ return !!m;
+ }
+
+ fetchCommandBody(tester){
+ const sb = [];
+ let line;
+ while( (null !== (line = this.peekLine())) ){
+ this.#checkForDirective(tester, line);
+ if( this.#isCommandLine(line, true) ) break;
+ sb.push(line,"\n");
+ this.consumePeeked();
+ }
+ line = sb.join('');
+ return !!line.trim() ? line : null;
+ }
+
+ run(tester){
+ this.reset();
+ this.#outer.verbosity( tester.verbosity() );
+ this.#outer.logger( tester.outer().logger() );
+ let line, directive, argv = [];
+ while( null != (line = this.getLine()) ){
+ this.verbose3("run() input line: ",line);
+ this.#checkForDirective(tester, line);
+ argv = this.#getCommandArgv(line);
+ if( argv ){
+ this.#processCommand(tester, argv);
+ continue;
+ }
+ tester.appendInput(line,true);
+ }
+ return true;
+ }
+
+ #processCommand(tester, argv){
+ this.verbose2("processCommand(): ",argv[0], " ", Util.argvToString(argv));
+ if(this.#outer.verbosity()>1){
+ const input = tester.getInputText();
+ this.verbose3("processCommand() input buffer = ",input);
+ }
+ CommandDispatcher.dispatch(tester, this, argv);
+ }
+
+ getLine(){
+ const cur = this.#cursor;
+ if( cur.pos==cur.src.byteLength ){
+ return null/*EOF*/;
+ }
+ cur.putbackPos = cur.pos;
+ cur.putbackLineNo = cur.lineNo;
+ cur.sb.length = 0;
+ let b = 0, prevB = 0, i = cur.pos;
+ let doBreak = false;
+ let nChar = 0 /* number of bytes in the aChar char */;
+ const end = cur.src.byteLength;
+ for(; i < end && !doBreak; ++i){
+ b = cur.src[i];
+ switch( b ){
+ case 13/*CR*/: continue;
+ case 10/*NL*/:
+ ++cur.lineNo;
+ if(cur.sb.length>0) doBreak = true;
+ // Else it's an empty string
+ break;
+ default:{
+ /* Multi-byte chars need to be gathered up and appended at
+ one time so that we can get them as string objects. */
+ nChar = 1;
+ switch( b & 0xF0 ){
+ case 0xC0: nChar = 2; break;
+ case 0xE0: nChar = 3; break;
+ case 0xF0: nChar = 4; break;
+ default:
+ if( b > 127 ) this.toss("Invalid character (#"+b+").");
+ break;
+ }
+ if( 1==nChar ){
+ cur.sb.push(String.fromCharCode(b));
+ }else{
+ const aChar = [] /* multi-byte char buffer */;
+ for(let x = 0; (x < nChar) && (i+x < end); ++x) aChar[x] = cur.src[i+x];
+ cur.sb.push(
+ Util.utf8Decode( new Uint8Array(aChar) )
+ );
+ i += nChar-1;
+ }
+ break;
+ }
+ }
+ }
+ cur.pos = i;
+ const rv = cur.sb.join('');
+ if( i==cur.src.byteLength && 0==rv.length ){
+ return null /* EOF */;
+ }
+ return rv;
+ }/*getLine()*/
+
+ /**
+ Fetches the next line then resets the cursor to its pre-call
+ state. consumePeeked() can be used to consume this peeked line
+ without having to re-parse it.
+ */
+ peekLine(){
+ const cur = this.#cursor;
+ const oldPos = cur.pos;
+ const oldPB = cur.putbackPos;
+ const oldPBL = cur.putbackLineNo;
+ const oldLine = cur.lineNo;
+ try {
+ return this.getLine();
+ }finally{
+ cur.peekedPos = cur.pos;
+ cur.peekedLineNo = cur.lineNo;
+ cur.pos = oldPos;
+ cur.lineNo = oldLine;
+ cur.putbackPos = oldPB;
+ cur.putbackLineNo = oldPBL;
+ }
+ }
+
+
+ /**
+ Only valid after calling peekLine() and before calling getLine().
+ This places the cursor to the position it would have been at had
+ the peekLine() had been fetched with getLine().
+ */
+ consumePeeked(){
+ const cur = this.#cursor;
+ cur.pos = cur.peekedPos;
+ cur.lineNo = cur.peekedLineNo;
+ }
+
+ /**
+ Restores the cursor to the position it had before the previous
+ call to getLine().
+ */
+ putbackLine(){
+ const cur = this.#cursor;
+ cur.pos = cur.putbackPos;
+ cur.lineNo = cur.putbackLineNo;
+ }
+
+}/*TestScript*/;
+
+//! --close command
+class CloseDbCommand extends Command {
+ process(t, ts, argv){
+ this.argcCheck(ts,argv,0,1);
+ let id;
+ if(argv.length>1){
+ const arg = argv[1];
+ if( "all" === arg ){
+ t.closeAllDbs();
+ return;
+ }
+ else{
+ id = parseInt(arg);
+ }
+ }else{
+ id = t.currentDbId();
+ }
+ t.closeDb(id);
+ }
+}
+
+//! --column-names command
+class ColumnNamesCommand extends Command {
+ process( st, ts, argv ){
+ this.argcCheck(ts,argv,1);
+ st.outputColumnNames( !!parseInt(argv[1]) );
+ }
+}
+
+//! --db command
+class DbCommand extends Command {
+ process(t, ts, argv){
+ this.argcCheck(ts,argv,1);
+ t.currentDbId( parseInt(argv[1]) );
+ }
+}
+
+//! --glob command
+class GlobCommand extends Command {
+ #negate = false;
+ constructor(negate=false){
+ super();
+ this.#negate = negate;
+ }
+
+ process(t, ts, argv){
+ this.argcCheck(ts,argv,1,-1);
+ t.incrementTestCounter();
+ const sql = t.takeInputBuffer();
+ let rc = t.execSql(null, true, ResultBufferMode.ESCAPED,
+ ResultRowMode.ONELINE, sql);
+ const result = t.getResultText();
+ const sArgs = Util.argvToString(argv);
+ //t2.verbose2(argv[0]," rc = ",rc," result buffer:\n", result,"\nargs:\n",sArgs);
+ const glob = Util.argvToString(argv);
+ rc = Util.strglob(glob, result);
+ if( (this.#negate && 0===rc) || (!this.#negate && 0!==rc) ){
+ ts.toss(argv[0], " mismatch: ", glob," vs input: ",result);
+ }
+ }
+}
+
+//! --notglob command
+class NotGlobCommand extends GlobCommand {
+ constructor(){super(true);}
+}
+
+//! --open command
+class OpenDbCommand extends Command {
+ #createIfNeeded = false;
+ constructor(createIfNeeded=false){
+ super();
+ this.#createIfNeeded = createIfNeeded;
+ }
+ process(t, ts, argv){
+ this.argcCheck(ts,argv,1);
+ t.openDb(argv[1], this.#createIfNeeded);
+ }
+}
+
+//! --new command
+class NewDbCommand extends OpenDbCommand {
+ constructor(){ super(true); }
+}
+
+//! Placeholder dummy/no-op commands
+class NoopCommand extends Command {
+ process(t, ts, argv){}
+}
+
+//! --null command
+class NullCommand extends Command {
+ process(st, ts, argv){
+ this.argcCheck(ts,argv,1);
+ st.nullValue( argv[1] );
+ }
+}
+
+//! --print command
+class PrintCommand extends Command {
+ process(st, ts, argv){
+ st.out(ts.getOutputPrefix(),': ');
+ if( 1==argv.length ){
+ st.out( st.getInputText() );
+ }else{
+ st.outln( Util.argvToString(argv) );
+ }
+ }
+}
+
+//! --result command
+class ResultCommand extends Command {
+ #bufferMode;
+ constructor(resultBufferMode = ResultBufferMode.ESCAPED){
+ super();
+ this.#bufferMode = resultBufferMode;
+ }
+ process(t, ts, argv){
+ this.argcCheck(ts,argv,0,-1);
+ t.incrementTestCounter();
+ const sql = t.takeInputBuffer();
+ //ts.verbose2(argv[0]," SQL =\n",sql);
+ t.execSql(null, false, this.#bufferMode, ResultRowMode.ONELINE, sql);
+ const result = t.getResultText().trim();
+ const sArgs = argv.length>1 ? Util.argvToString(argv) : "";
+ if( result !== sArgs ){
+ t.outln(argv[0]," FAILED comparison. Result buffer:\n",
+ result,"\nExpected result:\n",sArgs);
+ ts.toss(argv[0]+" comparison failed.");
+ }
+ }
+}
+
+//! --json command
+class JsonCommand extends ResultCommand {
+ constructor(){ super(ResultBufferMode.ASIS); }
+}
+
+//! --run command
+class RunCommand extends Command {
+ process(t, ts, argv){
+ this.argcCheck(ts,argv,0,1);
+ const pDb = (1==argv.length)
+ ? t.currentDb() : t.getDbById( parseInt(argv[1]) );
+ const sql = t.takeInputBuffer();
+ const rc = t.execSql(pDb, false, ResultBufferMode.NONE,
+ ResultRowMode.ONELINE, sql);
+ if( 0!==rc && t.verbosity()>0 ){
+ const msg = sqlite3.capi.sqlite3_errmsg(pDb);
+ ts.verbose2(argv[0]," non-fatal command error #",rc,": ",
+ msg,"\nfor SQL:\n",sql);
+ }
+ }
+}
+
+//! --tableresult command
+class TableResultCommand extends Command {
+ #jsonMode;
+ constructor(jsonMode=false){
+ super();
+ this.#jsonMode = jsonMode;
+ }
+ process(t, ts, argv){
+ this.argcCheck(ts,argv,0);
+ t.incrementTestCounter();
+ let body = ts.fetchCommandBody(t);
+ if( null===body ) ts.toss("Missing ",argv[0]," body.");
+ body = body.trim();
+ if( !body.endsWith("\n--end") ){
+ ts.toss(argv[0], " must be terminated with --end\\n");
+ }else{
+ body = body.substring(0, body.length-6);
+ }
+ const globs = body.split(/\s*\n\s*/);
+ if( globs.length < 1 ){
+ ts.toss(argv[0], " requires 1 or more ",
+ (this.#jsonMode ? "json snippets" : "globs"),".");
+ }
+ const sql = t.takeInputBuffer();
+ t.execSql(null, true,
+ this.#jsonMode ? ResultBufferMode.ASIS : ResultBufferMode.ESCAPED,
+ ResultRowMode.NEWLINE, sql);
+ const rbuf = t.getResultText().trim();
+ const res = rbuf.split(/\r?\n/);
+ if( res.length !== globs.length ){
+ ts.toss(argv[0], " failure: input has ", res.length,
+ " row(s) but expecting ",globs.length);
+ }
+ for(let i = 0; i < res.length; ++i){
+ const glob = globs[i].replaceAll(/\s+/g," ").trim();
+ //ts.verbose2(argv[0]," <<",glob,">> vs <<",res[i],">>");
+ if( this.#jsonMode ){
+ if( glob!==res[i] ){
+ ts.toss(argv[0], " json <<",glob, ">> does not match: <<",
+ res[i],">>");
+ }
+ }else if( 0!=Util.strglob(glob, res[i]) ){
+ ts.toss(argv[0], " glob <<",glob,">> does not match: <<",res[i],">>");
+ }
+ }
+ }
+}
+
+//! --json-block command
+class JsonBlockCommand extends TableResultCommand {
+ constructor(){ super(true); }
+}
+
+//! --testcase command
+class TestCaseCommand extends Command {
+ process(tester, script, argv){
+ this.argcCheck(script, argv,1);
+ script.testCaseName(argv[1]);
+ tester.clearResultBuffer();
+ tester.clearInputBuffer();
+ }
+}
+
+
+//! --verbosity command
+class VerbosityCommand extends Command {
+ process(t, ts, argv){
+ this.argcCheck(ts,argv,1);
+ ts.verbosity( parseInt(argv[1]) );
+ }
+}
+
+class CommandDispatcher {
+ static map = newObj();
+
+ static getCommandByName(name){
+ let rv = CommandDispatcher.map[name];
+ if( rv ) return rv;
+ switch(name){
+ case "close": rv = new CloseDbCommand(); break;
+ case "column-names": rv = new ColumnNamesCommand(); break;
+ case "db": rv = new DbCommand(); break;
+ case "glob": rv = new GlobCommand(); break;
+ case "json": rv = new JsonCommand(); break;
+ case "json-block": rv = new JsonBlockCommand(); break;
+ case "new": rv = new NewDbCommand(); break;
+ case "notglob": rv = new NotGlobCommand(); break;
+ case "null": rv = new NullCommand(); break;
+ case "oom": rv = new NoopCommand(); break;
+ case "open": rv = new OpenDbCommand(); break;
+ case "print": rv = new PrintCommand(); break;
+ case "result": rv = new ResultCommand(); break;
+ case "run": rv = new RunCommand(); break;
+ case "tableresult": rv = new TableResultCommand(); break;
+ case "testcase": rv = new TestCaseCommand(); break;
+ case "verbosity": rv = new VerbosityCommand(); break;
+ }
+ if( rv ){
+ CommandDispatcher.map[name] = rv;
+ }
+ return rv;
+ }
+
+ static dispatch(tester, testScript, argv){
+ const cmd = CommandDispatcher.getCommandByName(argv[0]);
+ if( !cmd ){
+ toss(UnknownCommand,testScript,argv[0]);
+ }
+ cmd.process(tester, testScript, argv);
+ }
+}/*CommandDispatcher*/
+
+const namespace = newObj({
+ Command,
+ DbException,
+ IncompatibleDirective,
+ Outer,
+ SQLTester,
+ SQLTesterException,
+ TestScript,
+ TestScriptFailed,
+ UnknownCommand,
+ Util,
+ sqlite3
+});
+
+export {namespace as default};
diff --git a/ext/wasm/SQLTester/SQLTester.run.mjs b/ext/wasm/SQLTester/SQLTester.run.mjs
new file mode 100644
index 0000000..735fe4d
--- /dev/null
+++ b/ext/wasm/SQLTester/SQLTester.run.mjs
@@ -0,0 +1,148 @@
+/*
+** 2023-08-29
+**
+** 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 contains a test application for SQLTester.js.
+*/
+import {default as ns} from './SQLTester.mjs';
+import {default as allTests} from './test-list.mjs';
+
+globalThis.sqlite3 = ns.sqlite3;
+const log = function f(...args){
+ console.log('SQLTester.run:',...args);
+ return f;
+};
+
+const out = function f(...args){ return f.outer.out(...args) };
+out.outer = new ns.Outer();
+out.outer.getOutputPrefix = ()=>'SQLTester.run: ';
+const outln = (...args)=>{ return out.outer.outln(...args) };
+
+const affirm = function(expr, msg){
+ if( !expr ){
+ throw new Error(arguments[1]
+ ? ("Assertion failed: "+arguments[1])
+ : "Assertion failed");
+ }
+}
+
+let ts = new ns.TestScript('/foo.test',`
+/*
+** This is a comment. There are many like it but this one is mine.
+**
+** SCRIPT_MODULE_NAME: sanity-check-0
+** xMIXED_MODULE_NAME: mixed-module
+** xMODULE_NAME: module-name
+** xREQUIRED_PROPERTIES: small fast reliable
+** xREQUIRED_PROPERTIES: RECURSIVE_TRIGGERS
+** xREQUIRED_PROPERTIES: TEMPSTORE_FILE TEMPSTORE_MEM
+** xREQUIRED_PROPERTIES: AUTOVACUUM INCRVACUUM
+**
+*/
+/* --verbosity 3 */
+/* ---must-fail */
+/* # must fail */
+/* --verbosity 0 */
+--print Hello, world.
+--close all
+--oom
+--db 0
+--new my.db
+--null zilch
+--testcase 1.0
+SELECT 1, null;
+--result 1 zilch
+--glob *zil*
+--notglob *ZIL*
+SELECT 1, 2;
+intentional error;
+--run
+/* ---intentional-failure */
+--testcase json-1
+SELECT json_array(1,2,3)
+--json [1,2,3]
+--testcase tableresult-1
+ select 1, 'a';
+ select 2, 'b';
+--tableresult
+ # [a-z]
+ 2 b
+--end
+--testcase json-block-1
+ select json_array(1,2,3);
+ select json_object('a',1,'b',2);
+--json-block
+ [1,2,3]
+ {"a":1,"b":2}
+--end
+--testcase col-names-on
+--column-names 1
+ select 1 as 'a', 2 as 'b';
+--result a 1 b 2
+--testcase col-names-off
+--column-names 0
+ select 1 as 'a', 2 as 'b';
+--result 1 2
+--close
+--print Until next time
+`);
+
+const sqt = new ns.SQLTester()
+ .setLogger(console.log.bind(console))
+ .verbosity(1)
+ .addTestScript(ts);
+sqt.outer().outputPrefix('');
+
+const runTests = function(){
+ try{
+ if( 0 ){
+ affirm( !sqt.getCurrentDb(), 'sqt.getCurrentDb()' );
+ sqt.openDb('/foo.db', true);
+ affirm( !!sqt.getCurrentDb(),'sqt.getCurrentDb()' );
+ affirm( 'zilch' !== sqt.nullValue() );
+ ts.run(sqt);
+ affirm( 'zilch' === sqt.nullValue() );
+ sqt.addTestScript(ts);
+ sqt.runTests();
+ }else{
+ for(const t of allTests){
+ sqt.addTestScript( new ns.TestScript(t) );
+ }
+ allTests.length = 0;
+ sqt.runTests();
+ }
+ }finally{
+ //log( "Metrics:", sqt.metrics );
+ sqt.reset();
+ }
+};
+
+if( globalThis.WorkerGlobalScope ){
+ const wPost = (type,payload)=>globalThis.postMessage({type, payload});
+ globalThis.onmessage = function({data}){
+ switch(data.type){
+ case 'run-tests':{
+ try{ runTests(); }
+ finally{ wPost('tests-end', sqt.metrics); }
+ break;
+ }
+ default:
+ log("unhandled onmessage: ",data);
+ break;
+ }
+ };
+ sqt.setLogger((msg)=>{
+ wPost('stdout', {message: msg});
+ });
+ wPost('is-ready');
+ //globalThis.onmessage({data:{type:'run-tests'}});
+}else{
+ runTests();
+}
diff --git a/ext/wasm/SQLTester/index.html b/ext/wasm/SQLTester/index.html
new file mode 100644
index 0000000..1dffad6
--- /dev/null
+++ b/ext/wasm/SQLTester/index.html
@@ -0,0 +1,127 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <!--link rel="stylesheet" href="../common/emscripten.css"/-->
+ <link rel="stylesheet" href="../common/testing.css"/>
+ <title>SQLTester</title>
+ </head>
+ <style>
+ fieldset {
+ display: flex;
+ flex-direction: row;
+ padding-right: 1em;
+ }
+ fieldset > :not(.legend) {
+ display: flex;
+ flex-direction: row;
+ padding-right: 1em;
+ }
+ </style>
+ <body>
+ <h1>SQLTester for JS/WASM</h1>
+ <p>This app reads in a build-time-defined set of SQLTester test
+ scripts and runs them through the test suite.
+ </p>
+ <fieldset>
+ <legend>Options</legend>
+ <span class='input-wrapper'>
+ <input type='checkbox' id='cb-log-reverse' checked>
+ <label for='cb-log-reverse'>Reverse log order?</label>
+ </span>
+ <input type='button' id='btn-run-tests' value='Run tests'/>
+ </fieldset>
+ <div id='test-output'>Test output will go here.</div>
+ <!--script src='SQLTester.run.mjs' type='module'></script-->
+ <script>
+ (async function(){
+ const W = new Worker('SQLTester.run.mjs',{
+ type: 'module'
+ });
+ const wPost = (type,payload)=>W.postMessage({type,payload});
+ const mapToString = (v)=>{
+ switch(typeof v){
+ case 'string': return v;
+ case 'number': case 'boolean':
+ case 'undefined': case 'bigint':
+ return ''+v;
+ default: break;
+ }
+ if(null===v) return 'null';
+ if(v instanceof Error){
+ v = {
+ message: v.message,
+ stack: v.stack,
+ errorClass: v.name
+ };
+ }
+ return JSON.stringify(v,undefined,2);
+ };
+ const normalizeArgs = (args)=>args.map(mapToString);
+ const logTarget = document.querySelector('#test-output');
+ const logClass = function(cssClass,...args){
+ const ln = document.createElement('div');
+ if(cssClass){
+ for(const c of (Array.isArray(cssClass) ? cssClass : [cssClass])){
+ ln.classList.add(c);
+ }
+ }
+ ln.append(document.createTextNode(normalizeArgs(args).join(' ')));
+ logTarget.append(ln);
+ };
+ {
+ const cbReverse = document.querySelector('#cb-log-reverse');
+ const cbReverseKey = 'SQLTester:cb-log-reverse';
+ const cbReverseIt = ()=>{
+ logTarget.classList[cbReverse.checked ? 'add' : 'remove']('reverse');
+ };
+ cbReverse.addEventListener('change', cbReverseIt, true);
+ cbReverseIt();
+ }
+
+ const btnRun = document.querySelector('#btn-run-tests');
+ const runTests = ()=>{
+ btnRun.setAttribute('disabled','disabled');
+ wPost('run-tests');
+ logTarget.innerText = 'Running tests...';
+ }
+ btnRun.addEventListener('click', runTests);
+ const log2 = function f(...args){
+ logClass('', ...args);
+ return f;
+ };
+ const log = function f(...args){
+ logClass('','index.html:',...args);
+ return f;
+ };
+
+ const timerId = setTimeout( ()=>{
+ logClass('error',"The SQLTester module is taking an unusually ",
+ "long time to load. More information may be available",
+ "in the dev console.");
+ }, 3000 /* assuming localhost */ );
+
+ W.onmessage = function({data}){
+ switch(data.type){
+ case 'stdout': log2(data.payload.message); break;
+ case 'tests-end':
+ btnRun.removeAttribute('disabled');
+ delete data.payload.nTest;
+ log("test results:",data.payload);
+ break;
+ case 'is-ready':
+ clearTimeout(timerId);
+ runTests(); break;
+ default:
+ log("unhandled onmessage",data);
+ break;
+ }
+ };
+ //runTests()
+ /* Inexplicably, */
+ })();
+ </script>
+ </body>
+</html>
diff --git a/ext/wasm/SQLTester/touint8array.c b/ext/wasm/SQLTester/touint8array.c
new file mode 100644
index 0000000..b03ad42
--- /dev/null
+++ b/ext/wasm/SQLTester/touint8array.c
@@ -0,0 +1,29 @@
+/*
+** 2023-08-29
+**
+** 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 contains a tool for writing out the contents of stdin as
+** a comma-separated list of numbers, one per byte.
+*/
+
+#include <stdio.h>
+int main(int argc, char const **argv){
+ int i;
+ int rc = 0, colWidth = 30;
+ int ch;
+ printf("[");
+ for( i=0; EOF!=(ch = fgetc(stdin)); ++i ){
+ if( 0!=i ) printf(",");
+ if( i && 0==(i%colWidth) ) puts("");
+ printf("%d",ch);
+ }
+ printf("]");
+ return rc;
+}
diff --git a/ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api b/ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api
new file mode 100644
index 0000000..57cd61e
--- /dev/null
+++ b/ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api
@@ -0,0 +1,204 @@
+_malloc
+_free
+_realloc
+_sqlite3_aggregate_context
+_sqlite3_auto_extension
+_sqlite3_bind_blob
+_sqlite3_bind_double
+_sqlite3_bind_int
+_sqlite3_bind_int64
+_sqlite3_bind_null
+_sqlite3_bind_parameter_count
+_sqlite3_bind_parameter_index
+_sqlite3_bind_pointer
+_sqlite3_bind_text
+_sqlite3_busy_handler
+_sqlite3_busy_timeout
+_sqlite3_cancel_auto_extension
+_sqlite3_changes
+_sqlite3_changes64
+_sqlite3_clear_bindings
+_sqlite3_close_v2
+_sqlite3_collation_needed
+_sqlite3_column_blob
+_sqlite3_column_bytes
+_sqlite3_column_count
+_sqlite3_column_count
+_sqlite3_column_double
+_sqlite3_column_int
+_sqlite3_column_int64
+_sqlite3_column_name
+_sqlite3_column_text
+_sqlite3_column_type
+_sqlite3_column_value
+_sqlite3_commit_hook
+_sqlite3_compileoption_get
+_sqlite3_compileoption_used
+_sqlite3_complete
+_sqlite3_context_db_handle
+_sqlite3_create_collation
+_sqlite3_create_collation_v2
+_sqlite3_create_function
+_sqlite3_create_function_v2
+_sqlite3_create_module
+_sqlite3_create_module_v2
+_sqlite3_create_window_function
+_sqlite3_data_count
+_sqlite3_db_filename
+_sqlite3_db_handle
+_sqlite3_db_name
+_sqlite3_db_status
+_sqlite3_declare_vtab
+_sqlite3_deserialize
+_sqlite3_drop_modules
+_sqlite3_errcode
+_sqlite3_errmsg
+_sqlite3_error_offset
+_sqlite3_errstr
+_sqlite3_exec
+_sqlite3_expanded_sql
+_sqlite3_extended_errcode
+_sqlite3_extended_result_codes
+_sqlite3_file_control
+_sqlite3_finalize
+_sqlite3_free
+_sqlite3_get_auxdata
+_sqlite3_get_autocommit
+_sqlite3_initialize
+_sqlite3_keyword_count
+_sqlite3_keyword_name
+_sqlite3_keyword_check
+_sqlite3_last_insert_rowid
+_sqlite3_libversion
+_sqlite3_libversion_number
+_sqlite3_limit
+_sqlite3_malloc
+_sqlite3_malloc64
+_sqlite3_msize
+_sqlite3_open
+_sqlite3_open_v2
+_sqlite3_overload_function
+_sqlite3_prepare_v2
+_sqlite3_prepare_v3
+_sqlite3_preupdate_blobwrite
+_sqlite3_preupdate_count
+_sqlite3_preupdate_depth
+_sqlite3_preupdate_hook
+_sqlite3_preupdate_new
+_sqlite3_preupdate_old
+_sqlite3_progress_handler
+_sqlite3_randomness
+_sqlite3_realloc
+_sqlite3_realloc64
+_sqlite3_reset
+_sqlite3_reset_auto_extension
+_sqlite3_result_blob
+_sqlite3_result_double
+_sqlite3_result_error
+_sqlite3_result_error_code
+_sqlite3_result_error_nomem
+_sqlite3_result_error_toobig
+_sqlite3_result_int
+_sqlite3_result_int64
+_sqlite3_result_null
+_sqlite3_result_pointer
+_sqlite3_result_subtype
+_sqlite3_result_text
+_sqlite3_result_zeroblob
+_sqlite3_result_zeroblob64
+_sqlite3_rollback_hook
+_sqlite3_serialize
+_sqlite3_set_authorizer
+_sqlite3_set_auxdata
+_sqlite3_set_last_insert_rowid
+_sqlite3_shutdown
+_sqlite3_sourceid
+_sqlite3_sql
+_sqlite3_status
+_sqlite3_status64
+_sqlite3_step
+_sqlite3_stmt_isexplain
+_sqlite3_stmt_readonly
+_sqlite3_stmt_status
+_sqlite3_strglob
+_sqlite3_stricmp
+_sqlite3_strlike
+_sqlite3_strnicmp
+_sqlite3_table_column_metadata
+_sqlite3_total_changes
+_sqlite3_total_changes64
+_sqlite3_trace_v2
+_sqlite3_txn_state
+_sqlite3_update_hook
+_sqlite3_uri_boolean
+_sqlite3_uri_int64
+_sqlite3_uri_key
+_sqlite3_uri_parameter
+_sqlite3_user_data
+_sqlite3_value_blob
+_sqlite3_value_bytes
+_sqlite3_value_double
+_sqlite3_value_dup
+_sqlite3_value_free
+_sqlite3_value_frombind
+_sqlite3_value_int
+_sqlite3_value_int64
+_sqlite3_value_nochange
+_sqlite3_value_numeric_type
+_sqlite3_value_pointer
+_sqlite3_value_subtype
+_sqlite3_value_text
+_sqlite3_value_type
+_sqlite3_vfs_find
+_sqlite3_vfs_register
+_sqlite3_vfs_unregister
+_sqlite3_vtab_collation
+_sqlite3_vtab_distinct
+_sqlite3_vtab_in
+_sqlite3_vtab_in_first
+_sqlite3_vtab_in_next
+_sqlite3_vtab_nochange
+_sqlite3_vtab_on_conflict
+_sqlite3_vtab_rhs_value
+_sqlite3changegroup_add
+_sqlite3changegroup_add_strm
+_sqlite3changegroup_delete
+_sqlite3changegroup_new
+_sqlite3changegroup_output
+_sqlite3changegroup_output_strm
+_sqlite3changeset_apply
+_sqlite3changeset_apply_strm
+_sqlite3changeset_apply_v2
+_sqlite3changeset_apply_v2_strm
+_sqlite3changeset_concat
+_sqlite3changeset_concat_strm
+_sqlite3changeset_conflict
+_sqlite3changeset_finalize
+_sqlite3changeset_fk_conflicts
+_sqlite3changeset_invert
+_sqlite3changeset_invert_strm
+_sqlite3changeset_new
+_sqlite3changeset_next
+_sqlite3changeset_old
+_sqlite3changeset_op
+_sqlite3changeset_pk
+_sqlite3changeset_start
+_sqlite3changeset_start_strm
+_sqlite3changeset_start_v2
+_sqlite3changeset_start_v2_strm
+_sqlite3session_attach
+_sqlite3session_changeset
+_sqlite3session_changeset_size
+_sqlite3session_changeset_strm
+_sqlite3session_config
+_sqlite3session_create
+_sqlite3session_delete
+_sqlite3session_diff
+_sqlite3session_enable
+_sqlite3session_indirect
+_sqlite3session_isempty
+_sqlite3session_memory_used
+_sqlite3session_object_config
+_sqlite3session_patchset
+_sqlite3session_patchset_strm
+_sqlite3session_table_filter
diff --git a/ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-see b/ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-see
new file mode 100644
index 0000000..83f3a97
--- /dev/null
+++ b/ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-see
@@ -0,0 +1,5 @@
+_sqlite3_key
+_sqlite3_key_v2
+_sqlite3_rekey
+_sqlite3_rekey_v2
+_sqlite3_activate_see
diff --git a/ext/wasm/api/EXPORTED_RUNTIME_METHODS.sqlite3-api b/ext/wasm/api/EXPORTED_RUNTIME_METHODS.sqlite3-api
new file mode 100644
index 0000000..aab1d8b
--- /dev/null
+++ b/ext/wasm/api/EXPORTED_RUNTIME_METHODS.sqlite3-api
@@ -0,0 +1,3 @@
+FS
+wasmMemory
+
diff --git a/ext/wasm/api/README.md b/ext/wasm/api/README.md
new file mode 100644
index 0000000..eb0f073
--- /dev/null
+++ b/ext/wasm/api/README.md
@@ -0,0 +1,164 @@
+# sqlite3-api.js And Friends
+
+This is the README for the files `sqlite3-*.js` and
+`sqlite3-wasm.c`. This collection of files is used to build a
+single-file distribution of the sqlite3 WASM API. It is broken into
+multiple JS files because:
+
+1. To facilitate including or excluding certain components for
+ specific use cases. e.g. by removing `sqlite3-api-oo1.js` if the
+ OO#1 API is not needed.
+
+2. To facilitate modularizing the pieces for use in different WASM
+ build environments. e.g. the files `post-js-*.js` are for use with
+ Emscripten's `--post-js` feature, and nowhere else.
+
+3. Certain components must be in their own standalone files in order
+ to be loaded as JS Workers.
+
+Note that the structure described here is the current state of things,
+as of this writing, but is not set in stone forever and may change
+at any time.
+
+The overall idea is that the following files get concatenated
+together, in the listed order, the resulting file is loaded by a
+browser client:
+
+- **`sqlite3-api-prologue.js`**\
+ Contains the initial bootstrap setup of the sqlite3 API
+ objects. This is exposed as a function, rather than objects, so that
+ the next step can pass in a config object which abstracts away parts
+ of the WASM environment, to facilitate plugging it in to arbitrary
+ WASM toolchains.
+- **`../common/whwasmutil.js`**\
+ A semi-third-party collection of JS/WASM utility code intended to
+ replace much of the Emscripten glue. The sqlite3 APIs internally use
+ these APIs instead of their Emscripten counterparts, in order to be
+ more portable to arbitrary WASM toolchains. This API is
+ configurable, in principle, for use with arbitrary WASM
+ toolchains. It is "semi-third-party" in that it was created in order
+ to support this tree but is standalone and maintained together
+ with...
+- **`../jaccwabyt/jaccwabyt.js`**\
+ Another semi-third-party API which creates bindings between JS
+ and C structs, such that changes to the struct state from either JS
+ or C are visible to the other end of the connection. This is also an
+ independent spinoff project, conceived for the sqlite3 project but
+ maintained separately.
+- **`sqlite3-api-glue.js`**\
+ Invokes functionality exposed by the previous two files to flesh out
+ low-level parts of `sqlite3-api-prologue.js`. Most of these pieces
+ related to populating the `sqlite3.capi.wasm` object. This file
+ also deletes most global-scope symbols the above files create,
+ effectively moving them into the scope being used for initializing
+ the API.
+- **`<build>/sqlite3-api-build-version.js`**\
+ Gets created by the build process and populates the
+ `sqlite3.version` object. This part is not critical, but records the
+ version of the library against which this module was built.
+- **`sqlite3-api-oo1.js`**\
+ Provides a high-level object-oriented wrapper to the lower-level C
+ API, colloquially known as OO API #1. Its API is similar to other
+ high-level sqlite3 JS wrappers and should feel relatively familiar
+ to anyone familiar with such APIs. That said, it is not a "required
+ component" and can be elided from builds which do not want it.
+- **`sqlite3-api-worker1.js`**\
+ A Worker-thread-based API which uses OO API #1 to provide an
+ interface to a database which can be driven from the main Window
+ thread via the Worker message-passing interface. Like OO API #1,
+ this is an optional component, offering one of any number of
+ potential implementations for such an API.
+ - **`sqlite3-worker1.js`**\
+ Is not part of the amalgamated sources and is intended to be
+ loaded by a client Worker thread. It loads the sqlite3 module
+ and runs the Worker #1 API which is implemented in
+ `sqlite3-api-worker1.js`.
+ - **`sqlite3-worker1-promiser.js`**\
+ Is likewise not part of the amalgamated sources and provides
+ a Promise-based interface into the Worker #1 API. This is
+ a far user-friendlier way to interface with databases running
+ in a Worker thread.
+- **`sqlite3-v-helper.js`**\
+ Installs `sqlite3.vfs` and `sqlite3.vtab`, namespaces which contain
+ helpers for use by downstream code which creates `sqlite3_vfs`
+ and `sqlite3_module` implementations.
+- **`sqlite3-vfs-opfs.c-pp.js`**\
+ is an sqlite3 VFS implementation which supports the Origin-Private
+ FileSystem (OPFS) as a storage layer to provide persistent storage
+ for database files in a browser. It requires...
+ - **`sqlite3-opfs-async-proxy.js`**\
+ is the asynchronous backend part of the OPFS proxy. It speaks
+ directly to the (async) OPFS API and channels those results back
+ to its synchronous counterpart. This file, because it must be
+ started in its own Worker, is not part of the amalgamation.
+- **`sqlite3-vfs-opfs-sahpool.c-pp.js`**\
+ is another sqlite3 VFS supporting the OPFS, but uses a completely
+ different approach that the above-listed one.
+- **`sqlite3-api-cleanup.js`**\
+ The previous files do not immediately extend the library. Instead
+ they add callback functions to be called during its
+ bootstrapping. Some also temporarily create global objects in order
+ to communicate their state to the files which follow them. This file
+ cleans up any dangling globals and runs the API bootstrapping
+ process, which is what finally executes the initialization code
+ installed by the previous files. As of this writing, this code
+ ensures that the previous files leave no more than a single global
+ symbol installed. When adapting the API for non-Emscripten
+ toolchains, this "should" be the only file where changes are needed.
+
+
+**Files with the extension `.c-pp.js`** are intended [to be processed
+with `c-pp`](#c-pp), noting that such preprocessing may be applied
+after all of the relevant files are concatenated. That extension is
+used primarily to keep the code maintainers cognisant of the fact that
+those files contain constructs which may not run as-is in any given
+JavaScript environment.
+
+The build process glues those files together, resulting in
+`sqlite3-api.js`, which is everything except for the
+`pre/post-js-*.js` files, and `sqlite3.js`, which is the
+Emscripten-generated amalgamated output and includes the
+`pre/post-js-*.js` parts, as well as the Emscripten-provided module
+loading pieces.
+
+The non-JS outlier file is `sqlite3-wasm.c`: it is a proxy for
+`sqlite3.c` which `#include`'s that file and adds a couple more
+WASM-specific helper functions, at least one of which requires access
+to private/static `sqlite3.c` internals. `sqlite3.wasm` is compiled
+from this file rather than `sqlite3.c`.
+
+The following files are part of the build process but are injected
+into the build-generated `sqlite3.js` along with `sqlite3-api.js`.
+
+- `extern-pre-js.js`\
+ Emscripten-specific header for Emscripten's `--extern-pre-js`
+ flag. As of this writing, that file is only used for experimentation
+ purposes and holds no code relevant to the production deliverables.
+- `pre-js.c-pp.js`\
+ Emscripten-specific header for Emscripten's `--pre-js` flag. This
+ file is intended as a place to override certain Emscripten behavior
+ before it starts up, but corner-case Emscripten bugs keep that from
+ being a reality.
+- `post-js-header.js`\
+ Emscripten-specific header for the `--post-js` input. It opens up
+ a lexical scope by starting a post-run handler for Emscripten.
+- `post-js-footer.js`\
+ Emscripten-specific footer for the `--post-js` input. This closes
+ off the lexical scope opened by `post-js-header.js`.
+- `extern-post-js.c-pp.js`\
+ Emscripten-specific header for Emscripten's `--extern-post-js`
+ flag. This file overwrites the Emscripten-installed
+ `sqlite3InitModule()` function with one which, after the module is
+ loaded, also initializes the asynchronous parts of the sqlite3
+ module. For example, the OPFS VFS support.
+
+<a id='c-pp'></a>
+Preprocessing of Source Files
+------------------------------------------------------------------------
+
+Certain files in the build require preprocessing to filter in/out
+parts which differ between vanilla JS, ES6 Modules, and node.js
+builds. The preprocessor application itself is in
+[`c-pp.c`](/file/ext/wasm/c-pp.c) and the complete technical details
+of such preprocessing are maintained in
+[`GNUMakefile`](/file/ext/wasm/GNUmakefile).
diff --git a/ext/wasm/api/extern-post-js.c-pp.js b/ext/wasm/api/extern-post-js.c-pp.js
new file mode 100644
index 0000000..63e5505
--- /dev/null
+++ b/ext/wasm/api/extern-post-js.c-pp.js
@@ -0,0 +1,126 @@
+
+/* ^^^^ ACHTUNG: blank line at the start is necessary because
+ Emscripten will not add a newline in some cases and we need
+ a blank line for a sed-based kludge for the ES6 build. */
+/* extern-post-js.js must be appended to the resulting sqlite3.js
+ file. It gets its name from being used as the value for the
+ --extern-post-js=... Emscripten flag. Note that this code, unlike
+ most of the associated JS code, runs outside of the
+ Emscripten-generated module init scope, in the current
+ global scope. */
+//#if target=es6-module
+const toExportForESM =
+//#endif
+(function(){
+ /**
+ In order to hide the sqlite3InitModule()'s resulting
+ Emscripten module from downstream clients (and simplify our
+ documentation by being able to elide those details), we hide that
+ function and expose a hand-written sqlite3InitModule() to return
+ the sqlite3 object (most of the time).
+
+ Unfortunately, we cannot modify the module-loader/exporter-based
+ impls which Emscripten installs at some point in the file above
+ this.
+ */
+ const originalInit = sqlite3InitModule;
+ if(!originalInit){
+ throw new Error("Expecting globalThis.sqlite3InitModule to be defined by the Emscripten build.");
+ }
+ /**
+ We need to add some state which our custom Module.locateFile()
+ can see, but an Emscripten limitation currently prevents us from
+ attaching it to the sqlite3InitModule function object:
+
+ https://github.com/emscripten-core/emscripten/issues/18071
+
+ The only(?) current workaround is to temporarily stash this state
+ into the global scope and delete it when sqlite3InitModule()
+ is called.
+ */
+ const initModuleState = globalThis.sqlite3InitModuleState = Object.assign(Object.create(null),{
+ moduleScript: globalThis?.document?.currentScript,
+ isWorker: ('undefined' !== typeof WorkerGlobalScope),
+ location: globalThis.location,
+ urlParams: globalThis?.location?.href
+ ? new URL(globalThis.location.href).searchParams
+ : new URLSearchParams()
+ });
+ initModuleState.debugModule =
+ initModuleState.urlParams.has('sqlite3.debugModule')
+ ? (...args)=>console.warn('sqlite3.debugModule:',...args)
+ : ()=>{};
+
+ if(initModuleState.urlParams.has('sqlite3.dir')){
+ initModuleState.sqlite3Dir = initModuleState.urlParams.get('sqlite3.dir') +'/';
+ }else if(initModuleState.moduleScript){
+ const li = initModuleState.moduleScript.src.split('/');
+ li.pop();
+ initModuleState.sqlite3Dir = li.join('/') + '/';
+ }
+
+ globalThis.sqlite3InitModule = function ff(...args){
+ //console.warn("Using replaced sqlite3InitModule()",globalThis.location);
+ return originalInit(...args).then((EmscriptenModule)=>{
+//#if wasmfs
+ if('undefined'!==typeof WorkerGlobalScope &&
+ EmscriptenModule['ENVIRONMENT_IS_PTHREAD']){
+ /** Workaround for wasmfs-generated worker, which calls this
+ routine from each individual thread and requires that its
+ argument be returned. The conditional criteria above are
+ fragile, based solely on inspection of the offending code,
+ not public Emscripten details. */
+ //console.warn("sqlite3InitModule() returning E-module.",EmscriptenModule);
+ return EmscriptenModule;
+ }
+//#endif
+ //console.warn("sqlite3InitModule() returning sqlite3 object.");
+ const s = EmscriptenModule.sqlite3;
+ s.scriptInfo = initModuleState;
+ //console.warn("sqlite3.scriptInfo =",s.scriptInfo);
+ if(ff.__isUnderTest) s.__isUnderTest = true;
+ const f = s.asyncPostInit;
+ delete s.asyncPostInit;
+ return f();
+ }).catch((e)=>{
+ console.error("Exception loading sqlite3 module:",e);
+ throw e;
+ });
+ };
+ globalThis.sqlite3InitModule.ready = originalInit.ready;
+
+ if(globalThis.sqlite3InitModuleState.moduleScript){
+ const sim = globalThis.sqlite3InitModuleState;
+ let src = sim.moduleScript.src.split('/');
+ src.pop();
+ sim.scriptDir = src.join('/') + '/';
+ }
+ initModuleState.debugModule('sqlite3InitModuleState =',initModuleState);
+ if(0){
+ console.warn("Replaced sqlite3InitModule()");
+ console.warn("globalThis.location.href =",globalThis.location.href);
+ if('undefined' !== typeof document){
+ console.warn("document.currentScript.src =",
+ document?.currentScript?.src);
+ }
+ }
+//#ifnot target=es6-module
+// Emscripten does not inject these module-loader bits in ES6 module
+// builds and including them here breaks JS bundlers, so elide them
+// from ESM builds.
+ /* Replace the various module exports performed by the Emscripten
+ glue... */
+ if (typeof exports === 'object' && typeof module === 'object'){
+ module.exports = sqlite3InitModule;
+ }else if (typeof exports === 'object'){
+ exports["sqlite3InitModule"] = sqlite3InitModule;
+ }
+ /* AMD modules get injected in a way we cannot override,
+ so we can't handle those here. */
+//#endif // !target=es6-module
+ return globalThis.sqlite3InitModule /* required for ESM */;
+})();
+//#if target=es6-module
+sqlite3InitModule = toExportForESM;
+export default sqlite3InitModule;
+//#endif
diff --git a/ext/wasm/api/extern-pre-js.js b/ext/wasm/api/extern-pre-js.js
new file mode 100644
index 0000000..7d47d33
--- /dev/null
+++ b/ext/wasm/api/extern-pre-js.js
@@ -0,0 +1,7 @@
+/* extern-pre-js.js must be prepended to the resulting sqlite3.js
+ file. This file is currently only used for holding snippets during
+ test and development.
+
+ It gets its name from being used as the value for the
+ --extern-pre-js=... Emscripten flag.
+*/
diff --git a/ext/wasm/api/post-js-footer.js b/ext/wasm/api/post-js-footer.js
new file mode 100644
index 0000000..58882cb
--- /dev/null
+++ b/ext/wasm/api/post-js-footer.js
@@ -0,0 +1,4 @@
+/* The current function scope was opened via post-js-header.js, which
+ gets prepended to this at build-time. This file closes that
+ scope. */
+})/*postRun.push(...)*/;
diff --git a/ext/wasm/api/post-js-header.js b/ext/wasm/api/post-js-header.js
new file mode 100644
index 0000000..0e27e1f
--- /dev/null
+++ b/ext/wasm/api/post-js-header.js
@@ -0,0 +1,26 @@
+/**
+ post-js-header.js is to be prepended to other code to create
+ post-js.js for use with Emscripten's --post-js flag. This code
+ requires that it be running in that context. The Emscripten
+ environment must have been set up already but it will not have
+ loaded its WASM when the code in this file is run. The function it
+ installs will be run after the WASM module is loaded, at which
+ point the sqlite3 JS API bits will get set up.
+*/
+if(!Module.postRun) Module.postRun = [];
+Module.postRun.push(function(Module/*the Emscripten-style module object*/){
+ 'use strict';
+ /* This function will contain at least the following:
+
+ - post-js-header.js (this file)
+ - sqlite3-api-prologue.js => Bootstrapping bits to attach the rest to
+ - common/whwasmutil.js => Replacements for much of Emscripten's glue
+ - jaccwaby/jaccwabyt.js => Jaccwabyt (C/JS struct binding)
+ - sqlite3-api-glue.js => glues previous parts together
+ - sqlite3-api-oo.js => SQLite3 OO API #1
+ - sqlite3-api-worker1.js => Worker-based API
+ - sqlite3-vfs-helper.js => Internal-use utilities for...
+ - sqlite3-vfs-opfs.js => OPFS VFS
+ - sqlite3-api-cleanup.js => final API cleanup
+ - post-js-footer.js => closes this postRun() function
+ */
diff --git a/ext/wasm/api/pre-js.c-pp.js b/ext/wasm/api/pre-js.c-pp.js
new file mode 100644
index 0000000..878f3e0
--- /dev/null
+++ b/ext/wasm/api/pre-js.c-pp.js
@@ -0,0 +1,110 @@
+/**
+ BEGIN FILE: api/pre-js.js
+
+ This file is intended to be prepended to the sqlite3.js build using
+ Emscripten's --pre-js=THIS_FILE flag (or equivalent).
+*/
+
+// See notes in extern-post-js.js
+const sqlite3InitModuleState = globalThis.sqlite3InitModuleState
+ || Object.assign(Object.create(null),{
+ debugModule: ()=>{}
+ });
+delete globalThis.sqlite3InitModuleState;
+sqlite3InitModuleState.debugModule('globalThis.location =',globalThis.location);
+
+//#ifnot target=es6-bundler-friendly
+/**
+ This custom locateFile() tries to figure out where to load `path`
+ from. The intent is to provide a way for foo/bar/X.js loaded from a
+ Worker constructor or importScripts() to be able to resolve
+ foo/bar/X.wasm (in the latter case, with some help):
+
+ 1) If URL param named the same as `path` is set, it is returned.
+
+ 2) If sqlite3InitModuleState.sqlite3Dir is set, then (thatName + path)
+ is returned (note that it's assumed to end with '/').
+
+ 3) If this code is running in the main UI thread AND it was loaded
+ from a SCRIPT tag, the directory part of that URL is used
+ as the prefix. (This form of resolution unfortunately does not
+ function for scripts loaded via importScripts().)
+
+ 4) If none of the above apply, (prefix+path) is returned.
+*/
+Module['locateFile'] = function(path, prefix) {
+//#if target=es6-module
+ return new URL(path, import.meta.url).href;
+//#else
+ 'use strict';
+ let theFile;
+ const up = this.urlParams;
+ if(up.has(path)){
+ theFile = up.get(path);
+ }else if(this.sqlite3Dir){
+ theFile = this.sqlite3Dir + path;
+ }else if(this.scriptDir){
+ theFile = this.scriptDir + path;
+ }else{
+ theFile = prefix + path;
+ }
+ sqlite3InitModuleState.debugModule(
+ "locateFile(",arguments[0], ',', arguments[1],")",
+ 'sqlite3InitModuleState.scriptDir =',this.scriptDir,
+ 'up.entries() =',Array.from(up.entries()),
+ "result =", theFile
+ );
+ return theFile;
+//#endif target=es6-module
+}.bind(sqlite3InitModuleState);
+//#endif ifnot target=es6-bundler-friendly
+
+/**
+ Bug warning: a custom Module.instantiateWasm() does not work
+ in WASMFS builds:
+
+ https://github.com/emscripten-core/emscripten/issues/17951
+
+ In such builds we must disable this.
+*/
+const xNameOfInstantiateWasm = false
+ ? 'instantiateWasm'
+ : 'emscripten-bug-17951';
+Module[xNameOfInstantiateWasm] = function callee(imports,onSuccess){
+ imports.env.foo = function(){};
+ const uri = Module.locateFile(
+ callee.uri, (
+ ('undefined'===typeof scriptDirectory/*var defined by Emscripten glue*/)
+ ? "" : scriptDirectory)
+ );
+ sqlite3InitModuleState.debugModule(
+ "instantiateWasm() uri =", uri
+ );
+ const wfetch = ()=>fetch(uri, {credentials: 'same-origin'});
+ const loadWasm = WebAssembly.instantiateStreaming
+ ? async ()=>{
+ return WebAssembly.instantiateStreaming(wfetch(), imports)
+ .then((arg)=>onSuccess(arg.instance, arg.module));
+ }
+ : async ()=>{ // Safari < v15
+ return wfetch()
+ .then(response => response.arrayBuffer())
+ .then(bytes => WebAssembly.instantiate(bytes, imports))
+ .then((arg)=>onSuccess(arg.instance, arg.module));
+ };
+ loadWasm();
+ return {};
+};
+/*
+ It is literally impossible to reliably get the name of _this_ script
+ at runtime, so impossible to derive X.wasm from script name
+ X.js. Thus we need, at build-time, to redefine
+ Module[xNameOfInstantiateWasm].uri by appending it to a build-specific
+ copy of this file with the name of the wasm file. This is apparently
+ why Emscripten hard-codes the name of the wasm file into their glue
+ scripts.
+*/
+Module[xNameOfInstantiateWasm].uri = 'sqlite3.wasm';
+/* END FILE: api/pre-js.js, noting that the build process may add a
+ line after this one to change the above .uri to a build-specific
+ one. */
diff --git a/ext/wasm/api/sqlite3-api-cleanup.js b/ext/wasm/api/sqlite3-api-cleanup.js
new file mode 100644
index 0000000..65dbb4e
--- /dev/null
+++ b/ext/wasm/api/sqlite3-api-cleanup.js
@@ -0,0 +1,63 @@
+/*
+ 2022-07-22
+
+ 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 is the tail end of the sqlite3-api.js constellation,
+ intended to be appended after all other sqlite3-api-*.js files so
+ that it can finalize any setup and clean up any global symbols
+ temporarily used for setting up the API's various subsystems.
+*/
+'use strict';
+if('undefined' !== typeof Module){ // presumably an Emscripten build
+ /**
+ Install a suitable default configuration for sqlite3ApiBootstrap().
+ */
+ const SABC = Object.assign(
+ Object.create(null), {
+ exports: ('undefined'===typeof wasmExports)
+ ? Module['asm']/* emscripten <=3.1.43 */
+ : wasmExports /* emscripten >=3.1.44 */,
+ memory: Module.wasmMemory /* gets set if built with -sIMPORTED_MEMORY */
+ },
+ globalThis.sqlite3ApiConfig || {}
+ );
+
+ /**
+ For current (2022-08-22) purposes, automatically call
+ sqlite3ApiBootstrap(). That decision will be revisited at some
+ point, as we really want client code to be able to call this to
+ configure certain parts. Clients may modify
+ globalThis.sqlite3ApiBootstrap.defaultConfig to tweak the default
+ configuration used by a no-args call to sqlite3ApiBootstrap(),
+ but must have first loaded their WASM module in order to be
+ able to provide the necessary configuration state.
+ */
+ //console.warn("globalThis.sqlite3ApiConfig = ",globalThis.sqlite3ApiConfig);
+ globalThis.sqlite3ApiConfig = SABC;
+ let sqlite3;
+ try{
+ sqlite3 = globalThis.sqlite3ApiBootstrap();
+ }catch(e){
+ console.error("sqlite3ApiBootstrap() error:",e);
+ throw e;
+ }finally{
+ delete globalThis.sqlite3ApiBootstrap;
+ delete globalThis.sqlite3ApiConfig;
+ }
+
+ Module.sqlite3 = sqlite3 /* Needed for customized sqlite3InitModule() to be able to
+ pass the sqlite3 object off to the client. */;
+}else{
+ console.warn("This is not running in an Emscripten module context, so",
+ "globalThis.sqlite3ApiBootstrap() is _not_ being called due to lack",
+ "of config info for the WASM environment.",
+ "It must be called manually.");
+}
diff --git a/ext/wasm/api/sqlite3-api-glue.js b/ext/wasm/api/sqlite3-api-glue.js
new file mode 100644
index 0000000..29efb3e
--- /dev/null
+++ b/ext/wasm/api/sqlite3-api-glue.js
@@ -0,0 +1,1671 @@
+/*
+ 2022-07-22
+
+ 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 glues together disparate pieces of JS which are loaded in
+ previous steps of the sqlite3-api.js bootstrapping process:
+ sqlite3-api-prologue.js, whwasmutil.js, and jaccwabyt.js. It
+ initializes the main API pieces so that the downstream components
+ (e.g. sqlite3-api-oo1.js) have all that they need.
+*/
+globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
+ 'use strict';
+ const toss = (...args)=>{throw new Error(args.join(' '))};
+ const toss3 = sqlite3.SQLite3Error.toss;
+ const capi = sqlite3.capi, wasm = sqlite3.wasm, util = sqlite3.util;
+ globalThis.WhWasmUtilInstaller(wasm);
+ delete globalThis.WhWasmUtilInstaller;
+
+ if(0){
+ /**
+ Please keep this block around as a maintenance reminder
+ that we cannot rely on this type of check.
+
+ This block fails on Safari, per a report at
+ https://sqlite.org/forum/forumpost/e5b20e1feb.
+
+ It turns out that what Safari serves from the indirect function
+ table (e.g. wasm.functionEntry(X)) is anonymous functions which
+ wrap the WASM functions, rather than returning the WASM
+ functions themselves. That means comparison of such functions
+ is useless for determining whether or not we have a specific
+ function from wasm.exports. i.e. if function X is indirection
+ function table entry N then wasm.exports.X is not equal to
+ wasm.functionEntry(N) in Safari, despite being so in the other
+ browsers.
+ */
+ /**
+ Find a mapping for SQLITE_WASM_DEALLOC, which the API
+ guarantees is a WASM pointer to the same underlying function as
+ wasm.dealloc() (noting that wasm.dealloc() is permitted to be a
+ JS wrapper around the WASM function). There is unfortunately no
+ O(1) algorithm for finding this pointer: we have to walk the
+ WASM indirect function table to find it. However, experience
+ indicates that that particular function is always very close to
+ the front of the table (it's been entry #3 in all relevant
+ tests).
+ */
+ const dealloc = wasm.exports[sqlite3.config.deallocExportName];
+ const nFunc = wasm.functionTable().length;
+ let i;
+ for(i = 0; i < nFunc; ++i){
+ const e = wasm.functionEntry(i);
+ if(dealloc === e){
+ capi.SQLITE_WASM_DEALLOC = i;
+ break;
+ }
+ }
+ if(dealloc !== wasm.functionEntry(capi.SQLITE_WASM_DEALLOC)){
+ toss("Internal error: cannot find function pointer for SQLITE_WASM_DEALLOC.");
+ }
+ }
+
+ /**
+ Signatures for the WASM-exported C-side functions. Each entry
+ is an array with 2+ elements:
+
+ [ "c-side name",
+ "result type" (wasm.xWrap() syntax),
+ [arg types in xWrap() syntax]
+ // ^^^ this needn't strictly be an array: it can be subsequent
+ // elements instead: [x,y,z] is equivalent to x,y,z
+ ]
+
+ Note that support for the API-specific data types in the
+ result/argument type strings gets plugged in at a later phase in
+ the API initialization process.
+ */
+ wasm.bindingSignatures = [
+ // Please keep these sorted by function name!
+ ["sqlite3_aggregate_context","void*", "sqlite3_context*", "int"],
+ /* sqlite3_auto_extension() has a hand-written binding. */
+ /* sqlite3_bind_blob() and sqlite3_bind_text() have hand-written
+ bindings to permit more flexible inputs. */
+ ["sqlite3_bind_double","int", "sqlite3_stmt*", "int", "f64"],
+ ["sqlite3_bind_int","int", "sqlite3_stmt*", "int", "int"],
+ ["sqlite3_bind_null",undefined, "sqlite3_stmt*", "int"],
+ ["sqlite3_bind_parameter_count", "int", "sqlite3_stmt*"],
+ ["sqlite3_bind_parameter_index","int", "sqlite3_stmt*", "string"],
+ ["sqlite3_bind_pointer", "int",
+ "sqlite3_stmt*", "int", "*", "string:static", "*"],
+ ["sqlite3_busy_handler","int", [
+ "sqlite3*",
+ new wasm.xWrap.FuncPtrAdapter({
+ signature: 'i(pi)',
+ contextKey: (argv,argIndex)=>argv[0/* sqlite3* */]
+ }),
+ "*"
+ ]],
+ ["sqlite3_busy_timeout","int", "sqlite3*", "int"],
+ /* sqlite3_cancel_auto_extension() has a hand-written binding. */
+ /* sqlite3_close_v2() is implemented by hand to perform some
+ extra work. */
+ ["sqlite3_changes", "int", "sqlite3*"],
+ ["sqlite3_clear_bindings","int", "sqlite3_stmt*"],
+ ["sqlite3_collation_needed", "int", "sqlite3*", "*", "*"/*=>v(ppis)*/],
+ ["sqlite3_column_blob","*", "sqlite3_stmt*", "int"],
+ ["sqlite3_column_bytes","int", "sqlite3_stmt*", "int"],
+ ["sqlite3_column_count", "int", "sqlite3_stmt*"],
+ ["sqlite3_column_double","f64", "sqlite3_stmt*", "int"],
+ ["sqlite3_column_int","int", "sqlite3_stmt*", "int"],
+ ["sqlite3_column_name","string", "sqlite3_stmt*", "int"],
+ ["sqlite3_column_text","string", "sqlite3_stmt*", "int"],
+ ["sqlite3_column_type","int", "sqlite3_stmt*", "int"],
+ ["sqlite3_column_value","sqlite3_value*", "sqlite3_stmt*", "int"],
+ ["sqlite3_commit_hook", "void*", [
+ "sqlite3*",
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'sqlite3_commit_hook',
+ signature: 'i(p)',
+ contextKey: (argv)=>argv[0/* sqlite3* */]
+ }),
+ '*'
+ ]],
+ ["sqlite3_compileoption_get", "string", "int"],
+ ["sqlite3_compileoption_used", "int", "string"],
+ ["sqlite3_complete", "int", "string:flexible"],
+ ["sqlite3_context_db_handle", "sqlite3*", "sqlite3_context*"],
+
+ /* sqlite3_create_function(), sqlite3_create_function_v2(), and
+ sqlite3_create_window_function() use hand-written bindings to
+ simplify handling of their function-type arguments. */
+ /* sqlite3_create_collation() and sqlite3_create_collation_v2()
+ use hand-written bindings to simplify passing of the callback
+ function.
+ ["sqlite3_create_collation", "int",
+ "sqlite3*", "string", "int",//SQLITE_UTF8 is the only legal value
+ "*", "*"],
+ ["sqlite3_create_collation_v2", "int",
+ "sqlite3*", "string", "int",//SQLITE_UTF8 is the only legal value
+ "*", "*", "*"],
+ */
+ ["sqlite3_data_count", "int", "sqlite3_stmt*"],
+ ["sqlite3_db_filename", "string", "sqlite3*", "string"],
+ ["sqlite3_db_handle", "sqlite3*", "sqlite3_stmt*"],
+ ["sqlite3_db_name", "string", "sqlite3*", "int"],
+ ["sqlite3_db_status", "int", "sqlite3*", "int", "*", "*", "int"],
+ ["sqlite3_errcode", "int", "sqlite3*"],
+ ["sqlite3_errmsg", "string", "sqlite3*"],
+ ["sqlite3_error_offset", "int", "sqlite3*"],
+ ["sqlite3_errstr", "string", "int"],
+ ["sqlite3_exec", "int", [
+ "sqlite3*", "string:flexible",
+ new wasm.xWrap.FuncPtrAdapter({
+ signature: 'i(pipp)',
+ bindScope: 'transient',
+ callProxy: (callback)=>{
+ let aNames;
+ return (pVoid, nCols, pColVals, pColNames)=>{
+ try {
+ const aVals = wasm.cArgvToJs(nCols, pColVals);
+ if(!aNames) aNames = wasm.cArgvToJs(nCols, pColNames);
+ return callback(aVals, aNames) | 0;
+ }catch(e){
+ /* If we set the db error state here, the higher-level
+ exec() call replaces it with its own, so we have no way
+ of reporting the exception message except the console. We
+ must not propagate exceptions through the C API. Though
+ we make an effort to report OOM here, sqlite3_exec()
+ translates that into SQLITE_ABORT as well. */
+ return e.resultCode || capi.SQLITE_ERROR;
+ }
+ }
+ }
+ }),
+ "*", "**"
+ ]],
+ ["sqlite3_expanded_sql", "string", "sqlite3_stmt*"],
+ ["sqlite3_extended_errcode", "int", "sqlite3*"],
+ ["sqlite3_extended_result_codes", "int", "sqlite3*", "int"],
+ ["sqlite3_file_control", "int", "sqlite3*", "string", "int", "*"],
+ ["sqlite3_finalize", "int", "sqlite3_stmt*"],
+ ["sqlite3_free", undefined,"*"],
+ ["sqlite3_get_autocommit", "int", "sqlite3*"],
+ ["sqlite3_get_auxdata", "*", "sqlite3_context*", "int"],
+ ["sqlite3_initialize", undefined],
+ /*["sqlite3_interrupt", undefined, "sqlite3*"
+ ^^^ we cannot actually currently support this because JS is
+ single-threaded and we don't have a portable way to access a DB
+ from 2 SharedWorkers concurrently. ],*/
+ ["sqlite3_keyword_count", "int"],
+ ["sqlite3_keyword_name", "int", ["int", "**", "*"]],
+ ["sqlite3_keyword_check", "int", ["string", "int"]],
+ ["sqlite3_libversion", "string"],
+ ["sqlite3_libversion_number", "int"],
+ ["sqlite3_limit", "int", ["sqlite3*", "int", "int"]],
+ ["sqlite3_malloc", "*","int"],
+ ["sqlite3_open", "int", "string", "*"],
+ ["sqlite3_open_v2", "int", "string", "*", "int", "string"],
+ /* sqlite3_prepare_v2() and sqlite3_prepare_v3() are handled
+ separately due to us requiring two different sets of semantics
+ for those, depending on how their SQL argument is provided. */
+ /* sqlite3_randomness() uses a hand-written wrapper to extend
+ the range of supported argument types. */
+ ["sqlite3_progress_handler", undefined, [
+ "sqlite3*", "int", new wasm.xWrap.FuncPtrAdapter({
+ name: 'xProgressHandler',
+ signature: 'i(p)',
+ bindScope: 'context',
+ contextKey: (argv,argIndex)=>argv[0/* sqlite3* */]
+ }), "*"
+ ]],
+ ["sqlite3_realloc", "*","*","int"],
+ ["sqlite3_reset", "int", "sqlite3_stmt*"],
+ /* sqlite3_reset_auto_extension() has a hand-written binding. */
+ ["sqlite3_result_blob", undefined, "sqlite3_context*", "*", "int", "*"],
+ ["sqlite3_result_double", undefined, "sqlite3_context*", "f64"],
+ ["sqlite3_result_error", undefined, "sqlite3_context*", "string", "int"],
+ ["sqlite3_result_error_code", undefined, "sqlite3_context*", "int"],
+ ["sqlite3_result_error_nomem", undefined, "sqlite3_context*"],
+ ["sqlite3_result_error_toobig", undefined, "sqlite3_context*"],
+ ["sqlite3_result_int", undefined, "sqlite3_context*", "int"],
+ ["sqlite3_result_null", undefined, "sqlite3_context*"],
+ ["sqlite3_result_pointer", undefined,
+ "sqlite3_context*", "*", "string:static", "*"],
+ ["sqlite3_result_subtype", undefined, "sqlite3_value*", "int"],
+ ["sqlite3_result_text", undefined, "sqlite3_context*", "string", "int", "*"],
+ ["sqlite3_result_zeroblob", undefined, "sqlite3_context*", "int"],
+ ["sqlite3_rollback_hook", "void*", [
+ "sqlite3*",
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'sqlite3_rollback_hook',
+ signature: 'v(p)',
+ contextKey: (argv)=>argv[0/* sqlite3* */]
+ }),
+ '*'
+ ]],
+ ["sqlite3_set_authorizer", "int", [
+ "sqlite3*",
+ new wasm.xWrap.FuncPtrAdapter({
+ name: "sqlite3_set_authorizer::xAuth",
+ signature: "i(pi"+"ssss)",
+ contextKey: (argv, argIndex)=>argv[0/*(sqlite3*)*/],
+ callProxy: (callback)=>{
+ return (pV, iCode, s0, s1, s2, s3)=>{
+ try{
+ s0 = s0 && wasm.cstrToJs(s0); s1 = s1 && wasm.cstrToJs(s1);
+ s2 = s2 && wasm.cstrToJs(s2); s3 = s3 && wasm.cstrToJs(s3);
+ return callback(pV, iCode, s0, s1, s2, s3) || 0;
+ }catch(e){
+ return e.resultCode || capi.SQLITE_ERROR;
+ }
+ }
+ }
+ }),
+ "*"/*pUserData*/
+ ]],
+ ["sqlite3_set_auxdata", undefined, [
+ "sqlite3_context*", "int", "*",
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xDestroyAuxData',
+ signature: 'v(*)',
+ contextKey: (argv, argIndex)=>argv[0/* sqlite3_context* */]
+ })
+ ]],
+ ["sqlite3_shutdown", undefined],
+ ["sqlite3_sourceid", "string"],
+ ["sqlite3_sql", "string", "sqlite3_stmt*"],
+ ["sqlite3_status", "int", "int", "*", "*", "int"],
+ ["sqlite3_step", "int", "sqlite3_stmt*"],
+ ["sqlite3_stmt_isexplain", "int", ["sqlite3_stmt*"]],
+ ["sqlite3_stmt_readonly", "int", ["sqlite3_stmt*"]],
+ ["sqlite3_stmt_status", "int", "sqlite3_stmt*", "int", "int"],
+ ["sqlite3_strglob", "int", "string","string"],
+ ["sqlite3_stricmp", "int", "string", "string"],
+ ["sqlite3_strlike", "int", "string", "string","int"],
+ ["sqlite3_strnicmp", "int", "string", "string", "int"],
+ ["sqlite3_table_column_metadata", "int",
+ "sqlite3*", "string", "string", "string",
+ "**", "**", "*", "*", "*"],
+ ["sqlite3_total_changes", "int", "sqlite3*"],
+ ["sqlite3_trace_v2", "int", [
+ "sqlite3*", "int",
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'sqlite3_trace_v2::callback',
+ signature: 'i(ippp)',
+ contextKey: (argv,argIndex)=>argv[0/* sqlite3* */]
+ }),
+ "*"
+ ]],
+ ["sqlite3_txn_state", "int", ["sqlite3*","string"]],
+ /* Note that sqlite3_uri_...() have very specific requirements for
+ their first C-string arguments, so we cannot perform any value
+ conversion on those. */
+ ["sqlite3_uri_boolean", "int", "sqlite3_filename", "string", "int"],
+ ["sqlite3_uri_key", "string", "sqlite3_filename", "int"],
+ ["sqlite3_uri_parameter", "string", "sqlite3_filename", "string"],
+ ["sqlite3_user_data","void*", "sqlite3_context*"],
+ ["sqlite3_value_blob", "*", "sqlite3_value*"],
+ ["sqlite3_value_bytes","int", "sqlite3_value*"],
+ ["sqlite3_value_double","f64", "sqlite3_value*"],
+ ["sqlite3_value_dup", "sqlite3_value*", "sqlite3_value*"],
+ ["sqlite3_value_free", undefined, "sqlite3_value*"],
+ ["sqlite3_value_frombind", "int", "sqlite3_value*"],
+ ["sqlite3_value_int","int", "sqlite3_value*"],
+ ["sqlite3_value_nochange", "int", "sqlite3_value*"],
+ ["sqlite3_value_numeric_type", "int", "sqlite3_value*"],
+ ["sqlite3_value_pointer", "*", "sqlite3_value*", "string:static"],
+ ["sqlite3_value_subtype", "int", "sqlite3_value*"],
+ ["sqlite3_value_text", "string", "sqlite3_value*"],
+ ["sqlite3_value_type", "int", "sqlite3_value*"],
+ ["sqlite3_vfs_find", "*", "string"],
+ ["sqlite3_vfs_register", "int", "sqlite3_vfs*", "int"],
+ ["sqlite3_vfs_unregister", "int", "sqlite3_vfs*"]
+ ]/*wasm.bindingSignatures*/;
+
+ if(false && wasm.compileOptionUsed('SQLITE_ENABLE_NORMALIZE')){
+ /* ^^^ "the problem" is that this is an option feature and the
+ build-time function-export list does not currently take
+ optional features into account. */
+ wasm.bindingSignatures.push(["sqlite3_normalized_sql", "string", "sqlite3_stmt*"]);
+ }
+
+ if(wasm.exports.sqlite3_activate_see instanceof Function){
+ wasm.bindingSignatures.push(
+ ["sqlite3_key", "int", "sqlite3*", "string", "int"],
+ ["sqlite3_key_v2","int","sqlite3*","string","*","int"],
+ ["sqlite3_rekey", "int", "sqlite3*", "string", "int"],
+ ["sqlite3_rekey_v2", "int", "sqlite3*", "string", "*", "int"],
+ ["sqlite3_activate_see", undefined, "string"]
+ );
+ }
+ /**
+ Functions which require BigInt (int64) support are separated from
+ the others because we need to conditionally bind them or apply
+ dummy impls, depending on the capabilities of the environment.
+
+ Note that not all of these functions directly require int64
+ but are only for use with APIs which require int64. For example,
+ the vtab-related functions.
+ */
+ wasm.bindingSignatures.int64 = [
+ ["sqlite3_bind_int64","int", ["sqlite3_stmt*", "int", "i64"]],
+ ["sqlite3_changes64","i64", ["sqlite3*"]],
+ ["sqlite3_column_int64","i64", ["sqlite3_stmt*", "int"]],
+ ["sqlite3_create_module", "int",
+ ["sqlite3*","string","sqlite3_module*","*"]],
+ ["sqlite3_create_module_v2", "int",
+ ["sqlite3*","string","sqlite3_module*","*","*"]],
+ ["sqlite3_declare_vtab", "int", ["sqlite3*", "string:flexible"]],
+ ["sqlite3_deserialize", "int", "sqlite3*", "string", "*", "i64", "i64", "int"]
+ /* Careful! Short version: de/serialize() are problematic because they
+ might use a different allocator than the user for managing the
+ deserialized block. de/serialize() are ONLY safe to use with
+ sqlite3_malloc(), sqlite3_free(), and its 64-bit variants. */,
+ ["sqlite3_drop_modules", "int", ["sqlite3*", "**"]],
+ ["sqlite3_last_insert_rowid", "i64", ["sqlite3*"]],
+ ["sqlite3_malloc64", "*","i64"],
+ ["sqlite3_msize", "i64", "*"],
+ ["sqlite3_overload_function", "int", ["sqlite3*","string","int"]],
+ ["sqlite3_preupdate_blobwrite", "int", "sqlite3*"],
+ ["sqlite3_preupdate_count", "int", "sqlite3*"],
+ ["sqlite3_preupdate_depth", "int", "sqlite3*"],
+ ["sqlite3_preupdate_hook", "*", [
+ "sqlite3*",
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'sqlite3_preupdate_hook',
+ signature: "v(ppippjj)",
+ contextKey: (argv)=>argv[0/* sqlite3* */],
+ callProxy: (callback)=>{
+ return (p,db,op,zDb,zTbl,iKey1,iKey2)=>{
+ callback(p, db, op, wasm.cstrToJs(zDb), wasm.cstrToJs(zTbl),
+ iKey1, iKey2);
+ };
+ }
+ }),
+ "*"
+ ]],
+ ["sqlite3_preupdate_new", "int", ["sqlite3*", "int", "**"]],
+ ["sqlite3_preupdate_old", "int", ["sqlite3*", "int", "**"]],
+ ["sqlite3_realloc64", "*","*", "i64"],
+ ["sqlite3_result_int64", undefined, "*", "i64"],
+ ["sqlite3_result_zeroblob64", "int", "*", "i64"],
+ ["sqlite3_serialize","*", "sqlite3*", "string", "*", "int"],
+ ["sqlite3_set_last_insert_rowid", undefined, ["sqlite3*", "i64"]],
+ ["sqlite3_status64", "int", "int", "*", "*", "int"],
+ ["sqlite3_total_changes64", "i64", ["sqlite3*"]],
+ ["sqlite3_update_hook", "*", [
+ "sqlite3*",
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'sqlite3_update_hook',
+ signature: "v(iippj)",
+ contextKey: (argv)=>argv[0/* sqlite3* */],
+ callProxy: (callback)=>{
+ return (p,op,z0,z1,rowid)=>{
+ callback(p, op, wasm.cstrToJs(z0), wasm.cstrToJs(z1), rowid);
+ };
+ }
+ }),
+ "*"
+ ]],
+ ["sqlite3_uri_int64", "i64", ["sqlite3_filename", "string", "i64"]],
+ ["sqlite3_value_int64","i64", "sqlite3_value*"],
+ ["sqlite3_vtab_collation","string","sqlite3_index_info*","int"],
+ ["sqlite3_vtab_distinct","int", "sqlite3_index_info*"],
+ ["sqlite3_vtab_in","int", "sqlite3_index_info*", "int", "int"],
+ ["sqlite3_vtab_in_first", "int", "sqlite3_value*", "**"],
+ ["sqlite3_vtab_in_next", "int", "sqlite3_value*", "**"],
+ /*["sqlite3_vtab_config" is variadic and requires a hand-written
+ proxy.] */
+ ["sqlite3_vtab_nochange","int", "sqlite3_context*"],
+ ["sqlite3_vtab_on_conflict","int", "sqlite3*"],
+ ["sqlite3_vtab_rhs_value","int", "sqlite3_index_info*", "int", "**"]
+ ];
+
+ // Add session/changeset APIs...
+ if(wasm.bigIntEnabled && !!wasm.exports.sqlite3changegroup_add){
+ /* ACHTUNG: 2022-12-23: the session/changeset API bindings are
+ COMPLETELY UNTESTED. */
+ /**
+ FuncPtrAdapter options for session-related callbacks with the
+ native signature "i(ps)". This proxy converts the 2nd argument
+ from a C string to a JS string before passing the arguments on
+ to the client-provided JS callback.
+ */
+ const __ipsProxy = {
+ signature: 'i(ps)',
+ callProxy:(callback)=>{
+ return (p,s)=>{
+ try{return callback(p, wasm.cstrToJs(s)) | 0}
+ catch(e){return e.resultCode || capi.SQLITE_ERROR}
+ }
+ }
+ };
+
+ wasm.bindingSignatures.int64.push(...[
+ ['sqlite3changegroup_add', 'int', ['sqlite3_changegroup*', 'int', 'void*']],
+ ['sqlite3changegroup_add_strm', 'int', [
+ 'sqlite3_changegroup*',
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xInput', signature: 'i(ppp)', bindScope: 'transient'
+ }),
+ 'void*'
+ ]],
+ ['sqlite3changegroup_delete', undefined, ['sqlite3_changegroup*']],
+ ['sqlite3changegroup_new', 'int', ['**']],
+ ['sqlite3changegroup_output', 'int', ['sqlite3_changegroup*', 'int*', '**']],
+ ['sqlite3changegroup_output_strm', 'int', [
+ 'sqlite3_changegroup*',
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xOutput', signature: 'i(ppi)', bindScope: 'transient'
+ }),
+ 'void*'
+ ]],
+ ['sqlite3changeset_apply', 'int', [
+ 'sqlite3*', 'int', 'void*',
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xFilter', bindScope: 'transient', ...__ipsProxy
+ }),
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xConflict', signature: 'i(pip)', bindScope: 'transient'
+ }),
+ 'void*'
+ ]],
+ ['sqlite3changeset_apply_strm', 'int', [
+ 'sqlite3*',
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xInput', signature: 'i(ppp)', bindScope: 'transient'
+ }),
+ 'void*',
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xFilter', bindScope: 'transient', ...__ipsProxy
+ }),
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xConflict', signature: 'i(pip)', bindScope: 'transient'
+ }),
+ 'void*'
+ ]],
+ ['sqlite3changeset_apply_v2', 'int', [
+ 'sqlite3*', 'int', 'void*',
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xFilter', bindScope: 'transient', ...__ipsProxy
+ }),
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xConflict', signature: 'i(pip)', bindScope: 'transient'
+ }),
+ 'void*', '**', 'int*', 'int'
+
+ ]],
+ ['sqlite3changeset_apply_v2_strm', 'int', [
+ 'sqlite3*',
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xInput', signature: 'i(ppp)', bindScope: 'transient'
+ }),
+ 'void*',
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xFilter', bindScope: 'transient', ...__ipsProxy
+ }),
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xConflict', signature: 'i(pip)', bindScope: 'transient'
+ }),
+ 'void*', '**', 'int*', 'int'
+ ]],
+ ['sqlite3changeset_concat', 'int', ['int','void*', 'int', 'void*', 'int*', '**']],
+ ['sqlite3changeset_concat_strm', 'int', [
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xInputA', signature: 'i(ppp)', bindScope: 'transient'
+ }),
+ 'void*',
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xInputB', signature: 'i(ppp)', bindScope: 'transient'
+ }),
+ 'void*',
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xOutput', signature: 'i(ppi)', bindScope: 'transient'
+ }),
+ 'void*'
+ ]],
+ ['sqlite3changeset_conflict', 'int', ['sqlite3_changeset_iter*', 'int', '**']],
+ ['sqlite3changeset_finalize', 'int', ['sqlite3_changeset_iter*']],
+ ['sqlite3changeset_fk_conflicts', 'int', ['sqlite3_changeset_iter*', 'int*']],
+ ['sqlite3changeset_invert', 'int', ['int', 'void*', 'int*', '**']],
+ ['sqlite3changeset_invert_strm', 'int', [
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xInput', signature: 'i(ppp)', bindScope: 'transient'
+ }),
+ 'void*',
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xOutput', signature: 'i(ppi)', bindScope: 'transient'
+ }),
+ 'void*'
+ ]],
+ ['sqlite3changeset_new', 'int', ['sqlite3_changeset_iter*', 'int', '**']],
+ ['sqlite3changeset_next', 'int', ['sqlite3_changeset_iter*']],
+ ['sqlite3changeset_old', 'int', ['sqlite3_changeset_iter*', 'int', '**']],
+ ['sqlite3changeset_op', 'int', [
+ 'sqlite3_changeset_iter*', '**', 'int*', 'int*','int*'
+ ]],
+ ['sqlite3changeset_pk', 'int', ['sqlite3_changeset_iter*', '**', 'int*']],
+ ['sqlite3changeset_start', 'int', ['**', 'int', '*']],
+ ['sqlite3changeset_start_strm', 'int', [
+ '**',
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xInput', signature: 'i(ppp)', bindScope: 'transient'
+ }),
+ 'void*'
+ ]],
+ ['sqlite3changeset_start_v2', 'int', ['**', 'int', '*', 'int']],
+ ['sqlite3changeset_start_v2_strm', 'int', [
+ '**',
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xInput', signature: 'i(ppp)', bindScope: 'transient'
+ }),
+ 'void*', 'int'
+ ]],
+ ['sqlite3session_attach', 'int', ['sqlite3_session*', 'string']],
+ ['sqlite3session_changeset', 'int', ['sqlite3_session*', 'int*', '**']],
+ ['sqlite3session_changeset_size', 'i64', ['sqlite3_session*']],
+ ['sqlite3session_changeset_strm', 'int', [
+ 'sqlite3_session*',
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xOutput', signature: 'i(ppp)', bindScope: 'transient'
+ }),
+ 'void*'
+ ]],
+ ['sqlite3session_config', 'int', ['int', 'void*']],
+ ['sqlite3session_create', 'int', ['sqlite3*', 'string', '**']],
+ //sqlite3session_delete() is bound manually
+ ['sqlite3session_diff', 'int', ['sqlite3_session*', 'string', 'string', '**']],
+ ['sqlite3session_enable', 'int', ['sqlite3_session*', 'int']],
+ ['sqlite3session_indirect', 'int', ['sqlite3_session*', 'int']],
+ ['sqlite3session_isempty', 'int', ['sqlite3_session*']],
+ ['sqlite3session_memory_used', 'i64', ['sqlite3_session*']],
+ ['sqlite3session_object_config', 'int', ['sqlite3_session*', 'int', 'void*']],
+ ['sqlite3session_patchset', 'int', ['sqlite3_session*', '*', '**']],
+ ['sqlite3session_patchset_strm', 'int', [
+ 'sqlite3_session*',
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xOutput', signature: 'i(ppp)', bindScope: 'transient'
+ }),
+ 'void*'
+ ]],
+ ['sqlite3session_table_filter', undefined, [
+ 'sqlite3_session*',
+ new wasm.xWrap.FuncPtrAdapter({
+ name: 'xFilter', ...__ipsProxy,
+ contextKey: (argv,argIndex)=>argv[0/* (sqlite3_session*) */]
+ }),
+ '*'
+ ]]
+ ]);
+ }/*session/changeset APIs*/
+
+ /**
+ Functions which are intended solely for API-internal use by the
+ WASM components, not client code. These get installed into
+ sqlite3.wasm. Some of them get exposed to clients via variants
+ named sqlite3_js_...().
+ */
+ wasm.bindingSignatures.wasm = [
+ ["sqlite3_wasm_db_reset", "int", "sqlite3*"],
+ ["sqlite3_wasm_db_vfs", "sqlite3_vfs*", "sqlite3*","string"],
+ ["sqlite3_wasm_vfs_create_file", "int",
+ "sqlite3_vfs*","string","*", "int"],
+ ["sqlite3_wasm_posix_create_file", "int", "string","*", "int"],
+ ["sqlite3_wasm_vfs_unlink", "int", "sqlite3_vfs*","string"]
+ ];
+
+ /**
+ Install JS<->C struct bindings for the non-opaque struct types we
+ need... */
+ sqlite3.StructBinder = globalThis.Jaccwabyt({
+ heap: 0 ? wasm.memory : wasm.heap8u,
+ alloc: wasm.alloc,
+ dealloc: wasm.dealloc,
+ bigIntEnabled: wasm.bigIntEnabled,
+ memberPrefix: /* Never change this: this prefix is baked into any
+ amount of code and client-facing docs. */ '$'
+ });
+ delete globalThis.Jaccwabyt;
+
+ {// wasm.xWrap() bindings...
+
+ /* Convert Arrays and certain TypedArrays to strings for
+ 'string:flexible'-type arguments */
+ const __xString = wasm.xWrap.argAdapter('string');
+ wasm.xWrap.argAdapter(
+ 'string:flexible', (v)=>__xString(util.flexibleString(v))
+ );
+
+ /**
+ The 'string:static' argument adapter treats its argument as
+ either...
+
+ - WASM pointer: assumed to be a long-lived C-string which gets
+ returned as-is.
+
+ - Anything else: gets coerced to a JS string for use as a map
+ key. If a matching entry is found (as described next), it is
+ returned, else wasm.allocCString() is used to create a a new
+ string, map its pointer to (''+v) for the remainder of the
+ application's life, and returns that pointer value for this
+ call and all future calls which are passed a
+ string-equivalent argument.
+
+ Use case: sqlite3_bind_pointer() and sqlite3_result_pointer()
+ call for "a static string and preferably a string
+ literal". This converter is used to ensure that the string
+ value seen by those functions is long-lived and behaves as they
+ need it to.
+ */
+ wasm.xWrap.argAdapter(
+ 'string:static',
+ function(v){
+ if(wasm.isPtr(v)) return v;
+ v = ''+v;
+ let rc = this[v];
+ return rc || (this[v] = wasm.allocCString(v));
+ }.bind(Object.create(null))
+ );
+
+ /**
+ Add some descriptive xWrap() aliases for '*' intended to (A)
+ initially improve readability/correctness of
+ wasm.bindingSignatures and (B) provide automatic conversion
+ from higher-level representations, e.g. capi.sqlite3_vfs to
+ `sqlite3_vfs*` via capi.sqlite3_vfs.pointer.
+ */
+ const __xArgPtr = wasm.xWrap.argAdapter('*');
+ const nilType = function(){}/*a class no value can ever be an instance of*/;
+ wasm.xWrap.argAdapter('sqlite3_filename', __xArgPtr)
+ ('sqlite3_context*', __xArgPtr)
+ ('sqlite3_value*', __xArgPtr)
+ ('void*', __xArgPtr)
+ ('sqlite3_changegroup*', __xArgPtr)
+ ('sqlite3_changeset_iter*', __xArgPtr)
+ //('sqlite3_rebaser*', __xArgPtr)
+ ('sqlite3_session*', __xArgPtr)
+ ('sqlite3_stmt*', (v)=>
+ __xArgPtr((v instanceof (sqlite3?.oo1?.Stmt || nilType))
+ ? v.pointer : v))
+ ('sqlite3*', (v)=>
+ __xArgPtr((v instanceof (sqlite3?.oo1?.DB || nilType))
+ ? v.pointer : v))
+ ('sqlite3_index_info*', (v)=>
+ __xArgPtr((v instanceof (capi.sqlite3_index_info || nilType))
+ ? v.pointer : v))
+ ('sqlite3_module*', (v)=>
+ __xArgPtr((v instanceof (capi.sqlite3_module || nilType))
+ ? v.pointer : v))
+ /**
+ `sqlite3_vfs*`:
+
+ - v is-a string: use the result of sqlite3_vfs_find(v) but
+ throw if it returns 0.
+ - v is-a capi.sqlite3_vfs: use v.pointer.
+ - Else return the same as the `'*'` argument conversion.
+ */
+ ('sqlite3_vfs*', (v)=>{
+ if('string'===typeof v){
+ /* A NULL sqlite3_vfs pointer will be treated as the default
+ VFS in many contexts. We specifically do not want that
+ behavior here. */
+ return capi.sqlite3_vfs_find(v)
+ || sqlite3.SQLite3Error.toss(
+ capi.SQLITE_NOTFOUND,
+ "Unknown sqlite3_vfs name:", v
+ );
+ }
+ return __xArgPtr((v instanceof (capi.sqlite3_vfs || nilType))
+ ? v.pointer : v);
+ });
+
+ const __xRcPtr = wasm.xWrap.resultAdapter('*');
+ wasm.xWrap.resultAdapter('sqlite3*', __xRcPtr)
+ ('sqlite3_context*', __xRcPtr)
+ ('sqlite3_stmt*', __xRcPtr)
+ ('sqlite3_value*', __xRcPtr)
+ ('sqlite3_vfs*', __xRcPtr)
+ ('void*', __xRcPtr);
+
+ /**
+ Populate api object with sqlite3_...() by binding the "raw" wasm
+ exports into type-converting proxies using wasm.xWrap().
+ */
+ if(0 === wasm.exports.sqlite3_step.length){
+ /* This environment wraps exports in nullary functions, which means
+ we must disable the arg-count validation we otherwise perform
+ on the wrappers. */
+ wasm.xWrap.doArgcCheck = false;
+ sqlite3.config.warn(
+ "Disabling sqlite3.wasm.xWrap.doArgcCheck due to environmental quirks."
+ );
+ }
+ for(const e of wasm.bindingSignatures){
+ capi[e[0]] = wasm.xWrap.apply(null, e);
+ }
+ for(const e of wasm.bindingSignatures.wasm){
+ wasm[e[0]] = wasm.xWrap.apply(null, e);
+ }
+
+ /* For C API functions which cannot work properly unless
+ wasm.bigIntEnabled is true, install a bogus impl which throws
+ if called when bigIntEnabled is false. The alternative would be
+ to elide these functions altogether, which seems likely to
+ cause more confusion. */
+ const fI64Disabled = function(fname){
+ return ()=>toss(fname+"() is unavailable due to lack",
+ "of BigInt support in this build.");
+ };
+ for(const e of wasm.bindingSignatures.int64){
+ capi[e[0]] = wasm.bigIntEnabled
+ ? wasm.xWrap.apply(null, e)
+ : fI64Disabled(e[0]);
+ }
+
+ /* There's no need to expose bindingSignatures to clients,
+ implicitly making it part of the public interface. */
+ delete wasm.bindingSignatures;
+
+ if(wasm.exports.sqlite3_wasm_db_error){
+ const __db_err = wasm.xWrap(
+ 'sqlite3_wasm_db_error', 'int', 'sqlite3*', 'int', 'string'
+ );
+ /**
+ Sets the given db's error state. Accepts:
+
+ - (sqlite3*, int code, string msg)
+ - (sqlite3*, Error e [,string msg = ''+e])
+
+ If passed a WasmAllocError, the message is ignored and the
+ result code is SQLITE_NOMEM. If passed any other Error type,
+ the result code defaults to SQLITE_ERROR unless the Error
+ object has a resultCode property, in which case that is used
+ (e.g. SQLite3Error has that). If passed a non-WasmAllocError
+ exception, the message string defaults to theError.message.
+
+ Returns the resulting code. Pass (pDb,0,0) to clear the error
+ state.
+ */
+ util.sqlite3_wasm_db_error = function(pDb, resultCode, message){
+ if(resultCode instanceof sqlite3.WasmAllocError){
+ resultCode = capi.SQLITE_NOMEM;
+ message = 0 /*avoid allocating message string*/;
+ }else if(resultCode instanceof Error){
+ message = message || ''+resultCode;
+ resultCode = (resultCode.resultCode || capi.SQLITE_ERROR);
+ }
+ return pDb ? __db_err(pDb, resultCode, message) : resultCode;
+ };
+ }else{
+ util.sqlite3_wasm_db_error = function(pDb,errCode,msg){
+ console.warn("sqlite3_wasm_db_error() is not exported.",arguments);
+ return errCode;
+ };
+ }
+ }/*xWrap() bindings*/
+
+ {/* Import C-level constants and structs... */
+ const cJson = wasm.xCall('sqlite3_wasm_enum_json');
+ if(!cJson){
+ toss("Maintenance required: increase sqlite3_wasm_enum_json()'s",
+ "static buffer size!");
+ }
+ //console.debug('wasm.ctype length =',wasm.cstrlen(cJson));
+ wasm.ctype = JSON.parse(wasm.cstrToJs(cJson));
+ // Groups of SQLITE_xyz macros...
+ const defineGroups = ['access', 'authorizer',
+ 'blobFinalizers', 'changeset',
+ 'config', 'dataTypes',
+ 'dbConfig', 'dbStatus',
+ 'encodings', 'fcntl', 'flock', 'ioCap',
+ 'limits', 'openFlags',
+ 'prepareFlags', 'resultCodes',
+ 'sqlite3Status',
+ 'stmtStatus', 'syncFlags',
+ 'trace', 'txnState', 'udfFlags',
+ 'version' ];
+ if(wasm.bigIntEnabled){
+ defineGroups.push('serialize', 'session', 'vtab');
+ }
+ for(const t of defineGroups){
+ for(const e of Object.entries(wasm.ctype[t])){
+ // ^^^ [k,v] there triggers a buggy code transformation via
+ // one of the Emscripten-driven optimizers.
+ capi[e[0]] = e[1];
+ }
+ }
+ if(!wasm.functionEntry(capi.SQLITE_WASM_DEALLOC)){
+ toss("Internal error: cannot resolve exported function",
+ "entry SQLITE_WASM_DEALLOC (=="+capi.SQLITE_WASM_DEALLOC+").");
+ }
+ const __rcMap = Object.create(null);
+ for(const t of ['resultCodes']){
+ for(const e of Object.entries(wasm.ctype[t])){
+ __rcMap[e[1]] = e[0];
+ }
+ }
+ /**
+ For the given integer, returns the SQLITE_xxx result code as a
+ string, or undefined if no such mapping is found.
+ */
+ capi.sqlite3_js_rc_str = (rc)=>__rcMap[rc];
+ /* Bind all registered C-side structs... */
+ const notThese = Object.assign(Object.create(null),{
+ // For each struct to NOT register, map its name to true:
+ WasmTestStruct: true,
+ /* We unregister the kvvfs VFS from Worker threads below. */
+ sqlite3_kvvfs_methods: !util.isUIThread(),
+ /* sqlite3_index_info and friends require int64: */
+ sqlite3_index_info: !wasm.bigIntEnabled,
+ sqlite3_index_constraint: !wasm.bigIntEnabled,
+ sqlite3_index_orderby: !wasm.bigIntEnabled,
+ sqlite3_index_constraint_usage: !wasm.bigIntEnabled
+ });
+ for(const s of wasm.ctype.structs){
+ if(!notThese[s.name]){
+ capi[s.name] = sqlite3.StructBinder(s);
+ }
+ }
+ if(capi.sqlite3_index_info){
+ /* Move these inner structs into sqlite3_index_info. Binding
+ ** them to WASM requires that we create global-scope structs to
+ ** model them with, but those are no longer needed after we've
+ ** passed them to StructBinder. */
+ for(const k of ['sqlite3_index_constraint',
+ 'sqlite3_index_orderby',
+ 'sqlite3_index_constraint_usage']){
+ capi.sqlite3_index_info[k] = capi[k];
+ delete capi[k];
+ }
+ capi.sqlite3_vtab_config = wasm.xWrap(
+ 'sqlite3_wasm_vtab_config','int',[
+ 'sqlite3*', 'int', 'int']
+ );
+ }/* end vtab-related setup */
+ }/*end C constant and struct imports*/
+
+ /**
+ Internal helper to assist in validating call argument counts in
+ the hand-written sqlite3_xyz() wrappers. We do this only for
+ consistency with non-special-case wrappings.
+ */
+ const __dbArgcMismatch = (pDb,f,n)=>{
+ return util.sqlite3_wasm_db_error(pDb, capi.SQLITE_MISUSE,
+ f+"() requires "+n+" argument"+
+ (1===n?"":'s')+".");
+ };
+
+ /** Code duplication reducer for functions which take an encoding
+ argument and require SQLITE_UTF8. Sets the db error code to
+ SQLITE_FORMAT and returns that code. */
+ const __errEncoding = (pDb)=>{
+ return util.sqlite3_wasm_db_error(
+ pDb, capi.SQLITE_FORMAT, "SQLITE_UTF8 is the only supported encoding."
+ );
+ };
+
+ /**
+ __dbCleanupMap is infrastructure for recording registration of
+ UDFs and collations so that sqlite3_close_v2() can clean up any
+ automated JS-to-WASM function conversions installed by those.
+ */
+ const __argPDb = (pDb)=>wasm.xWrap.argAdapter('sqlite3*')(pDb);
+ const __argStr = (str)=>wasm.isPtr(str) ? wasm.cstrToJs(str) : str;
+ const __dbCleanupMap = function(
+ pDb, mode/*0=remove, >0=create if needed, <0=do not create if missing*/
+ ){
+ pDb = __argPDb(pDb);
+ let m = this.dbMap.get(pDb);
+ if(!mode){
+ this.dbMap.delete(pDb);
+ return m;
+ }else if(!m && mode>0){
+ this.dbMap.set(pDb, (m = Object.create(null)));
+ }
+ return m;
+ }.bind(Object.assign(Object.create(null),{
+ dbMap: new Map
+ }));
+
+ __dbCleanupMap.addCollation = function(pDb, name){
+ const m = __dbCleanupMap(pDb, 1);
+ if(!m.collation) m.collation = new Set;
+ m.collation.add(__argStr(name).toLowerCase());
+ };
+
+ __dbCleanupMap._addUDF = function(pDb, name, arity, map){
+ /* Map UDF name to a Set of arity values */
+ name = __argStr(name).toLowerCase();
+ let u = map.get(name);
+ if(!u) map.set(name, (u = new Set));
+ u.add((arity<0) ? -1 : arity);
+ };
+
+ __dbCleanupMap.addFunction = function(pDb, name, arity){
+ const m = __dbCleanupMap(pDb, 1);
+ if(!m.udf) m.udf = new Map;
+ this._addUDF(pDb, name, arity, m.udf);
+ };
+
+ __dbCleanupMap.addWindowFunc = function(pDb, name, arity){
+ const m = __dbCleanupMap(pDb, 1);
+ if(!m.wudf) m.wudf = new Map;
+ this._addUDF(pDb, name, arity, m.wudf);
+ };
+
+ /**
+ Intended to be called _only_ from sqlite3_close_v2(),
+ passed its non-0 db argument.
+
+ This function frees up certain automatically-installed WASM
+ function bindings which were installed on behalf of the given db,
+ as those may otherwise leak.
+
+ Notable caveat: this is only ever run via
+ sqlite3.capi.sqlite3_close_v2(). If a client, for whatever
+ reason, uses sqlite3.wasm.exports.sqlite3_close_v2() (the
+ function directly exported from WASM), this cleanup will not
+ happen.
+
+ This is not a silver bullet for avoiding automation-related
+ leaks but represents "an honest effort."
+
+ The issue being addressed here is covered at:
+
+ https://sqlite.org/wasm/doc/trunk/api-c-style.md#convert-func-ptr
+ */
+ __dbCleanupMap.cleanup = function(pDb){
+ pDb = __argPDb(pDb);
+ //wasm.xWrap.FuncPtrAdapter.debugFuncInstall = false;
+ /**
+ Installing NULL functions in the C API will remove those
+ bindings. The FuncPtrAdapter which sits between us and the C
+ API will also treat that as an opportunity to
+ wasm.uninstallFunction() any WASM function bindings it has
+ installed for pDb.
+ */
+ const closeArgs = [pDb];
+ for(const name of [
+ 'sqlite3_busy_handler',
+ 'sqlite3_commit_hook',
+ 'sqlite3_preupdate_hook',
+ 'sqlite3_progress_handler',
+ 'sqlite3_rollback_hook',
+ 'sqlite3_set_authorizer',
+ 'sqlite3_trace_v2',
+ 'sqlite3_update_hook'
+ ]) {
+ const x = wasm.exports[name];
+ closeArgs.length = x.length/*==argument count*/
+ /* recall that undefined entries translate to 0 when passed to
+ WASM. */;
+ try{ capi[name](...closeArgs) }
+ catch(e){
+ console.warn("close-time call of",name+"(",closeArgs,") threw:",e);
+ }
+ }
+ const m = __dbCleanupMap(pDb, 0);
+ if(!m) return;
+ if(m.collation){
+ for(const name of m.collation){
+ try{
+ capi.sqlite3_create_collation_v2(
+ pDb, name, capi.SQLITE_UTF8, 0, 0, 0
+ );
+ }catch(e){
+ /*ignored*/
+ }
+ }
+ delete m.collation;
+ }
+ let i;
+ for(i = 0; i < 2; ++i){ /* Clean up UDFs... */
+ const fmap = i ? m.wudf : m.udf;
+ if(!fmap) continue;
+ const func = i
+ ? capi.sqlite3_create_window_function
+ : capi.sqlite3_create_function_v2;
+ for(const e of fmap){
+ const name = e[0], arities = e[1];
+ const fargs = [pDb, name, 0/*arity*/, capi.SQLITE_UTF8, 0, 0, 0, 0, 0];
+ if(i) fargs.push(0);
+ for(const arity of arities){
+ try{ fargs[2] = arity; func.apply(null, fargs); }
+ catch(e){/*ignored*/}
+ }
+ arities.clear();
+ }
+ fmap.clear();
+ }
+ delete m.udf;
+ delete m.wudf;
+ }/*__dbCleanupMap.cleanup()*/;
+
+ {/* Binding of sqlite3_close_v2() */
+ const __sqlite3CloseV2 = wasm.xWrap("sqlite3_close_v2", "int", "sqlite3*");
+ capi.sqlite3_close_v2 = function(pDb){
+ if(1!==arguments.length) return __dbArgcMismatch(pDb, 'sqlite3_close_v2', 1);
+ if(pDb){
+ try{__dbCleanupMap.cleanup(pDb)} catch(e){/*ignored*/}
+ }
+ return __sqlite3CloseV2(pDb);
+ };
+ }/*sqlite3_close_v2()*/
+
+ if(capi.sqlite3session_table_filter){
+ const __sqlite3SessionDelete = wasm.xWrap(
+ 'sqlite3session_delete', undefined, ['sqlite3_session*']
+ );
+ capi.sqlite3session_delete = function(pSession){
+ if(1!==arguments.length){
+ return __dbArgcMismatch(pDb, 'sqlite3session_delete', 1);
+ /* Yes, we're returning a value from a void function. That seems
+ like the lesser evil compared to not maintaining arg-count
+ consistency as we do with other similar bindings. */
+ }
+ else if(pSession){
+ //wasm.xWrap.FuncPtrAdapter.debugFuncInstall = true;
+ capi.sqlite3session_table_filter(pSession, 0, 0);
+ }
+ __sqlite3SessionDelete(pSession);
+ };
+ }
+
+ {/* Bindings for sqlite3_create_collation[_v2]() */
+ // contextKey() impl for wasm.xWrap.FuncPtrAdapter
+ const contextKey = (argv,argIndex)=>{
+ return 'argv['+argIndex+']:'+argv[0/* sqlite3* */]+
+ ':'+wasm.cstrToJs(argv[1/* collation name */]).toLowerCase()
+ };
+ const __sqlite3CreateCollationV2 = wasm.xWrap(
+ 'sqlite3_create_collation_v2', 'int', [
+ 'sqlite3*', 'string', 'int', '*',
+ new wasm.xWrap.FuncPtrAdapter({
+ /* int(*xCompare)(void*,int,const void*,int,const void*) */
+ name: 'xCompare', signature: 'i(pipip)', contextKey
+ }),
+ new wasm.xWrap.FuncPtrAdapter({
+ /* void(*xDestroy(void*) */
+ name: 'xDestroy', signature: 'v(p)', contextKey
+ })
+ ]
+ );
+
+ /**
+ Works exactly like C's sqlite3_create_collation_v2() except that:
+
+ 1) It returns capi.SQLITE_FORMAT if the 3rd argument contains
+ any encoding-related value other than capi.SQLITE_UTF8. No
+ other encodings are supported. As a special case, if the
+ bottom 4 bits of that argument are 0, SQLITE_UTF8 is
+ assumed.
+
+ 2) It accepts JS functions for its function-pointer arguments,
+ for which it will install WASM-bound proxies. The bindings
+ are "permanent," in that they will stay in the WASM environment
+ until it shuts down unless the client calls this again with the
+ same collation name and a value of 0 or null for the
+ the function pointer(s).
+
+ For consistency with the C API, it requires the same number of
+ arguments. It returns capi.SQLITE_MISUSE if passed any other
+ argument count.
+
+ Returns 0 on success, non-0 on error, in which case the error
+ state of pDb (of type `sqlite3*` or argument-convertible to it)
+ may contain more information.
+ */
+ capi.sqlite3_create_collation_v2 = function(pDb,zName,eTextRep,pArg,xCompare,xDestroy){
+ if(6!==arguments.length) return __dbArgcMismatch(pDb, 'sqlite3_create_collation_v2', 6);
+ else if( 0 === (eTextRep & 0xf) ){
+ eTextRep |= capi.SQLITE_UTF8;
+ }else if( capi.SQLITE_UTF8 !== (eTextRep & 0xf) ){
+ return __errEncoding(pDb);
+ }
+ try{
+ const rc = __sqlite3CreateCollationV2(pDb, zName, eTextRep, pArg, xCompare, xDestroy);
+ if(0===rc && xCompare instanceof Function){
+ __dbCleanupMap.addCollation(pDb, zName);
+ }
+ return rc;
+ }catch(e){
+ return util.sqlite3_wasm_db_error(pDb, e);
+ }
+ };
+
+ capi.sqlite3_create_collation = (pDb,zName,eTextRep,pArg,xCompare)=>{
+ return (5===arguments.length)
+ ? capi.sqlite3_create_collation_v2(pDb,zName,eTextRep,pArg,xCompare,0)
+ : __dbArgcMismatch(pDb, 'sqlite3_create_collation', 5);
+ };
+
+ }/*sqlite3_create_collation() and friends*/
+
+ {/* Special-case handling of sqlite3_create_function_v2()
+ and sqlite3_create_window_function(). */
+ /** FuncPtrAdapter for contextKey() for sqlite3_create_function()
+ and friends. */
+ const contextKey = function(argv,argIndex){
+ return (
+ argv[0/* sqlite3* */]
+ +':'+(argv[2/*number of UDF args*/] < 0 ? -1 : argv[2])
+ +':'+argIndex/*distinct for each xAbc callback type*/
+ +':'+wasm.cstrToJs(argv[1]).toLowerCase()
+ )
+ };
+
+ /**
+ JS proxies for the various sqlite3_create[_window]_function()
+ callbacks, structured in a form usable by wasm.xWrap.FuncPtrAdapter.
+ */
+ const __cfProxy = Object.assign(Object.create(null), {
+ xInverseAndStep: {
+ signature:'v(pip)', contextKey,
+ callProxy: (callback)=>{
+ return (pCtx, argc, pArgv)=>{
+ try{ callback(pCtx, ...capi.sqlite3_values_to_js(argc, pArgv)) }
+ catch(e){ capi.sqlite3_result_error_js(pCtx, e) }
+ };
+ }
+ },
+ xFinalAndValue: {
+ signature:'v(p)', contextKey,
+ callProxy: (callback)=>{
+ return (pCtx)=>{
+ try{ capi.sqlite3_result_js(pCtx, callback(pCtx)) }
+ catch(e){ capi.sqlite3_result_error_js(pCtx, e) }
+ };
+ }
+ },
+ xFunc: {
+ signature:'v(pip)', contextKey,
+ callProxy: (callback)=>{
+ return (pCtx, argc, pArgv)=>{
+ try{
+ capi.sqlite3_result_js(
+ pCtx,
+ callback(pCtx, ...capi.sqlite3_values_to_js(argc, pArgv))
+ );
+ }catch(e){
+ //console.error('xFunc() caught:',e);
+ capi.sqlite3_result_error_js(pCtx, e);
+ }
+ };
+ }
+ },
+ xDestroy: {
+ signature:'v(p)', contextKey,
+ //Arguable: a well-behaved destructor doesn't require a proxy.
+ callProxy: (callback)=>{
+ return (pVoid)=>{
+ try{ callback(pVoid) }
+ catch(e){ console.error("UDF xDestroy method threw:",e) }
+ };
+ }
+ }
+ })/*__cfProxy*/;
+
+ const __sqlite3CreateFunction = wasm.xWrap(
+ "sqlite3_create_function_v2", "int", [
+ "sqlite3*", "string"/*funcName*/, "int"/*nArg*/,
+ "int"/*eTextRep*/, "*"/*pApp*/,
+ new wasm.xWrap.FuncPtrAdapter({name: 'xFunc', ...__cfProxy.xFunc}),
+ new wasm.xWrap.FuncPtrAdapter({name: 'xStep', ...__cfProxy.xInverseAndStep}),
+ new wasm.xWrap.FuncPtrAdapter({name: 'xFinal', ...__cfProxy.xFinalAndValue}),
+ new wasm.xWrap.FuncPtrAdapter({name: 'xDestroy', ...__cfProxy.xDestroy})
+ ]
+ );
+
+ const __sqlite3CreateWindowFunction = wasm.xWrap(
+ "sqlite3_create_window_function", "int", [
+ "sqlite3*", "string"/*funcName*/, "int"/*nArg*/,
+ "int"/*eTextRep*/, "*"/*pApp*/,
+ new wasm.xWrap.FuncPtrAdapter({name: 'xStep', ...__cfProxy.xInverseAndStep}),
+ new wasm.xWrap.FuncPtrAdapter({name: 'xFinal', ...__cfProxy.xFinalAndValue}),
+ new wasm.xWrap.FuncPtrAdapter({name: 'xValue', ...__cfProxy.xFinalAndValue}),
+ new wasm.xWrap.FuncPtrAdapter({name: 'xInverse', ...__cfProxy.xInverseAndStep}),
+ new wasm.xWrap.FuncPtrAdapter({name: 'xDestroy', ...__cfProxy.xDestroy})
+ ]
+ );
+
+ /* Documented in the api object's initializer. */
+ capi.sqlite3_create_function_v2 = function f(
+ pDb, funcName, nArg, eTextRep, pApp,
+ xFunc, //void (*xFunc)(sqlite3_context*,int,sqlite3_value**)
+ xStep, //void (*xStep)(sqlite3_context*,int,sqlite3_value**)
+ xFinal, //void (*xFinal)(sqlite3_context*)
+ xDestroy //void (*xDestroy)(void*)
+ ){
+ if( f.length!==arguments.length ){
+ return __dbArgcMismatch(pDb,"sqlite3_create_function_v2",f.length);
+ }else if( 0 === (eTextRep & 0xf) ){
+ eTextRep |= capi.SQLITE_UTF8;
+ }else if( capi.SQLITE_UTF8 !== (eTextRep & 0xf) ){
+ return __errEncoding(pDb);
+ }
+ try{
+ const rc = __sqlite3CreateFunction(pDb, funcName, nArg, eTextRep,
+ pApp, xFunc, xStep, xFinal, xDestroy);
+ if(0===rc && (xFunc instanceof Function
+ || xStep instanceof Function
+ || xFinal instanceof Function
+ || xDestroy instanceof Function)){
+ __dbCleanupMap.addFunction(pDb, funcName, nArg);
+ }
+ return rc;
+ }catch(e){
+ console.error("sqlite3_create_function_v2() setup threw:",e);
+ return util.sqlite3_wasm_db_error(pDb, e, "Creation of UDF threw: "+e);
+ }
+ };
+
+ /* Documented in the api object's initializer. */
+ capi.sqlite3_create_function = function f(
+ pDb, funcName, nArg, eTextRep, pApp,
+ xFunc, xStep, xFinal
+ ){
+ return (f.length===arguments.length)
+ ? capi.sqlite3_create_function_v2(pDb, funcName, nArg, eTextRep,
+ pApp, xFunc, xStep, xFinal, 0)
+ : __dbArgcMismatch(pDb,"sqlite3_create_function",f.length);
+ };
+
+ /* Documented in the api object's initializer. */
+ capi.sqlite3_create_window_function = function f(
+ pDb, funcName, nArg, eTextRep, pApp,
+ xStep, //void (*xStep)(sqlite3_context*,int,sqlite3_value**)
+ xFinal, //void (*xFinal)(sqlite3_context*)
+ xValue, //void (*xValue)(sqlite3_context*)
+ xInverse,//void (*xInverse)(sqlite3_context*,int,sqlite3_value**)
+ xDestroy //void (*xDestroy)(void*)
+ ){
+ if( f.length!==arguments.length ){
+ return __dbArgcMismatch(pDb,"sqlite3_create_window_function",f.length);
+ }else if( 0 === (eTextRep & 0xf) ){
+ eTextRep |= capi.SQLITE_UTF8;
+ }else if( capi.SQLITE_UTF8 !== (eTextRep & 0xf) ){
+ return __errEncoding(pDb);
+ }
+ try{
+ const rc = __sqlite3CreateWindowFunction(pDb, funcName, nArg, eTextRep,
+ pApp, xStep, xFinal, xValue,
+ xInverse, xDestroy);
+ if(0===rc && (xStep instanceof Function
+ || xFinal instanceof Function
+ || xValue instanceof Function
+ || xInverse instanceof Function
+ || xDestroy instanceof Function)){
+ __dbCleanupMap.addWindowFunc(pDb, funcName, nArg);
+ }
+ return rc;
+ }catch(e){
+ console.error("sqlite3_create_window_function() setup threw:",e);
+ return util.sqlite3_wasm_db_error(pDb, e, "Creation of UDF threw: "+e);
+ }
+ };
+ /**
+ A _deprecated_ alias for capi.sqlite3_result_js() which
+ predates the addition of that function in the public API.
+ */
+ capi.sqlite3_create_function_v2.udfSetResult =
+ capi.sqlite3_create_function.udfSetResult =
+ capi.sqlite3_create_window_function.udfSetResult = capi.sqlite3_result_js;
+
+ /**
+ A _deprecated_ alias for capi.sqlite3_values_to_js() which
+ predates the addition of that function in the public API.
+ */
+ capi.sqlite3_create_function_v2.udfConvertArgs =
+ capi.sqlite3_create_function.udfConvertArgs =
+ capi.sqlite3_create_window_function.udfConvertArgs = capi.sqlite3_values_to_js;
+
+ /**
+ A _deprecated_ alias for capi.sqlite3_result_error_js() which
+ predates the addition of that function in the public API.
+ */
+ capi.sqlite3_create_function_v2.udfSetError =
+ capi.sqlite3_create_function.udfSetError =
+ capi.sqlite3_create_window_function.udfSetError = capi.sqlite3_result_error_js;
+
+ }/*sqlite3_create_function_v2() and sqlite3_create_window_function() proxies*/;
+
+ {/* Special-case handling of sqlite3_prepare_v2() and
+ sqlite3_prepare_v3() */
+
+ /**
+ Helper for string:flexible conversions which require a
+ byte-length counterpart argument. Passed a value and its
+ ostensible length, this function returns [V,N], where V is
+ either v or a transformed copy of v and N is either n, -1, or
+ the byte length of v (if it's a byte array or ArrayBuffer).
+ */
+ const __flexiString = (v,n)=>{
+ if('string'===typeof v){
+ n = -1;
+ }else if(util.isSQLableTypedArray(v)){
+ n = v.byteLength;
+ v = util.typedArrayToString(
+ (v instanceof ArrayBuffer) ? new Uint8Array(v) : v
+ );
+ }else if(Array.isArray(v)){
+ v = v.join("");
+ n = -1;
+ }
+ return [v, n];
+ };
+
+ /**
+ Scope-local holder of the two impls of sqlite3_prepare_v2/v3().
+ */
+ const __prepare = {
+ /**
+ This binding expects a JS string as its 2nd argument and
+ null as its final argument. In order to compile multiple
+ statements from a single string, the "full" impl (see
+ below) must be used.
+ */
+ basic: wasm.xWrap('sqlite3_prepare_v3',
+ "int", ["sqlite3*", "string",
+ "int"/*ignored for this impl!*/,
+ "int", "**",
+ "**"/*MUST be 0 or null or undefined!*/]),
+ /**
+ Impl which requires that the 2nd argument be a pointer
+ to the SQL string, instead of being converted to a
+ string. This variant is necessary for cases where we
+ require a non-NULL value for the final argument
+ (exec()'ing multiple statements from one input
+ string). For simpler cases, where only the first
+ statement in the SQL string is required, the wrapper
+ named sqlite3_prepare_v2() is sufficient and easier to
+ use because it doesn't require dealing with pointers.
+ */
+ full: wasm.xWrap('sqlite3_prepare_v3',
+ "int", ["sqlite3*", "*", "int", "int",
+ "**", "**"])
+ };
+
+ /* Documented in the capi object's initializer. */
+ capi.sqlite3_prepare_v3 = function f(pDb, sql, sqlLen, prepFlags, ppStmt, pzTail){
+ if(f.length!==arguments.length){
+ return __dbArgcMismatch(pDb,"sqlite3_prepare_v3",f.length);
+ }
+ const [xSql, xSqlLen] = __flexiString(sql, sqlLen);
+ switch(typeof xSql){
+ case 'string': return __prepare.basic(pDb, xSql, xSqlLen, prepFlags, ppStmt, null);
+ case 'number': return __prepare.full(pDb, xSql, xSqlLen, prepFlags, ppStmt, pzTail);
+ default:
+ return util.sqlite3_wasm_db_error(
+ pDb, capi.SQLITE_MISUSE,
+ "Invalid SQL argument type for sqlite3_prepare_v2/v3()."
+ );
+ }
+ };
+
+ /* Documented in the capi object's initializer. */
+ capi.sqlite3_prepare_v2 = function f(pDb, sql, sqlLen, ppStmt, pzTail){
+ return (f.length===arguments.length)
+ ? capi.sqlite3_prepare_v3(pDb, sql, sqlLen, 0, ppStmt, pzTail)
+ : __dbArgcMismatch(pDb,"sqlite3_prepare_v2",f.length);
+ };
+
+ }/*sqlite3_prepare_v2/v3()*/
+
+ {/*sqlite3_bind_text/blob()*/
+ const __bindText = wasm.xWrap("sqlite3_bind_text", "int", [
+ "sqlite3_stmt*", "int", "string", "int", "*"
+ ]);
+ const __bindBlob = wasm.xWrap("sqlite3_bind_blob", "int", [
+ "sqlite3_stmt*", "int", "*", "int", "*"
+ ]);
+
+ /** Documented in the capi object's initializer. */
+ capi.sqlite3_bind_text = function f(pStmt, iCol, text, nText, xDestroy){
+ if(f.length!==arguments.length){
+ return __dbArgcMismatch(capi.sqlite3_db_handle(pStmt),
+ "sqlite3_bind_text", f.length);
+ }else if(wasm.isPtr(text) || null===text){
+ return __bindText(pStmt, iCol, text, nText, xDestroy);
+ }else if(text instanceof ArrayBuffer){
+ text = new Uint8Array(text);
+ }else if(Array.isArray(pMem)){
+ text = pMem.join('');
+ }
+ let p, n;
+ try{
+ if(util.isSQLableTypedArray(text)){
+ p = wasm.allocFromTypedArray(text);
+ n = text.byteLength;
+ }else if('string'===typeof text){
+ [p, n] = wasm.allocCString(text);
+ }else{
+ return util.sqlite3_wasm_db_error(
+ capi.sqlite3_db_handle(pStmt), capi.SQLITE_MISUSE,
+ "Invalid 3rd argument type for sqlite3_bind_text()."
+ );
+ }
+ return __bindText(pStmt, iCol, p, n, capi.SQLITE_WASM_DEALLOC);
+ }catch(e){
+ wasm.dealloc(p);
+ return util.sqlite3_wasm_db_error(
+ capi.sqlite3_db_handle(pStmt), e
+ );
+ }
+ }/*sqlite3_bind_text()*/;
+
+ /** Documented in the capi object's initializer. */
+ capi.sqlite3_bind_blob = function f(pStmt, iCol, pMem, nMem, xDestroy){
+ if(f.length!==arguments.length){
+ return __dbArgcMismatch(capi.sqlite3_db_handle(pStmt),
+ "sqlite3_bind_blob", f.length);
+ }else if(wasm.isPtr(pMem) || null===pMem){
+ return __bindBlob(pStmt, iCol, pMem, nMem, xDestroy);
+ }else if(pMem instanceof ArrayBuffer){
+ pMem = new Uint8Array(pMem);
+ }else if(Array.isArray(pMem)){
+ pMem = pMem.join('');
+ }
+ let p, n;
+ try{
+ if(util.isBindableTypedArray(pMem)){
+ p = wasm.allocFromTypedArray(pMem);
+ n = nMem>=0 ? nMem : pMem.byteLength;
+ }else if('string'===typeof pMem){
+ [p, n] = wasm.allocCString(pMem);
+ }else{
+ return util.sqlite3_wasm_db_error(
+ capi.sqlite3_db_handle(pStmt), capi.SQLITE_MISUSE,
+ "Invalid 3rd argument type for sqlite3_bind_blob()."
+ );
+ }
+ return __bindBlob(pStmt, iCol, p, n, capi.SQLITE_WASM_DEALLOC);
+ }catch(e){
+ wasm.dealloc(p);
+ return util.sqlite3_wasm_db_error(
+ capi.sqlite3_db_handle(pStmt), e
+ );
+ }
+ }/*sqlite3_bind_blob()*/;
+
+ }/*sqlite3_bind_text/blob()*/
+
+ {/* sqlite3_config() */
+ /**
+ Wraps a small subset of the C API's sqlite3_config() options.
+ Unsupported options trigger the return of capi.SQLITE_NOTFOUND.
+ Passing fewer than 2 arguments triggers return of
+ capi.SQLITE_MISUSE.
+ */
+ capi.sqlite3_config = function(op, ...args){
+ if(arguments.length<2) return capi.SQLITE_MISUSE;
+ switch(op){
+ case capi.SQLITE_CONFIG_COVERING_INDEX_SCAN: // 20 /* int */
+ case capi.SQLITE_CONFIG_MEMSTATUS:// 9 /* boolean */
+ case capi.SQLITE_CONFIG_SMALL_MALLOC: // 27 /* boolean */
+ case capi.SQLITE_CONFIG_SORTERREF_SIZE: // 28 /* int nByte */
+ case capi.SQLITE_CONFIG_STMTJRNL_SPILL: // 26 /* int nByte */
+ case capi.SQLITE_CONFIG_URI:// 17 /* int */
+ return wasm.exports.sqlite3_wasm_config_i(op, args[0]);
+ case capi.SQLITE_CONFIG_LOOKASIDE: // 13 /* int int */
+ return wasm.exports.sqlite3_wasm_config_ii(op, args[0], args[1]);
+ case capi.SQLITE_CONFIG_MEMDB_MAXSIZE: // 29 /* sqlite3_int64 */
+ return wasm.exports.sqlite3_wasm_config_j(op, args[0]);
+ case capi.SQLITE_CONFIG_GETMALLOC: // 5 /* sqlite3_mem_methods* */
+ case capi.SQLITE_CONFIG_GETMUTEX: // 11 /* sqlite3_mutex_methods* */
+ case capi.SQLITE_CONFIG_GETPCACHE2: // 19 /* sqlite3_pcache_methods2* */
+ case capi.SQLITE_CONFIG_GETPCACHE: // 15 /* no-op */
+ case capi.SQLITE_CONFIG_HEAP: // 8 /* void*, int nByte, int min */
+ case capi.SQLITE_CONFIG_LOG: // 16 /* xFunc, void* */
+ case capi.SQLITE_CONFIG_MALLOC:// 4 /* sqlite3_mem_methods* */
+ case capi.SQLITE_CONFIG_MMAP_SIZE: // 22 /* sqlite3_int64, sqlite3_int64 */
+ case capi.SQLITE_CONFIG_MULTITHREAD: // 2 /* nil */
+ case capi.SQLITE_CONFIG_MUTEX: // 10 /* sqlite3_mutex_methods* */
+ case capi.SQLITE_CONFIG_PAGECACHE: // 7 /* void*, int sz, int N */
+ case capi.SQLITE_CONFIG_PCACHE2: // 18 /* sqlite3_pcache_methods2* */
+ case capi.SQLITE_CONFIG_PCACHE: // 14 /* no-op */
+ case capi.SQLITE_CONFIG_PCACHE_HDRSZ: // 24 /* int *psz */
+ case capi.SQLITE_CONFIG_PMASZ: // 25 /* unsigned int szPma */
+ case capi.SQLITE_CONFIG_SERIALIZED: // 3 /* nil */
+ case capi.SQLITE_CONFIG_SINGLETHREAD: // 1 /* nil */:
+ case capi.SQLITE_CONFIG_SQLLOG: // 21 /* xSqllog, void* */
+ case capi.SQLITE_CONFIG_WIN32_HEAPSIZE: // 23 /* int nByte */
+ default:
+ return capi.SQLITE_NOTFOUND;
+ }
+ };
+ }/* sqlite3_config() */
+
+ {/*auto-extension bindings.*/
+ const __autoExtFptr = new Set;
+
+ capi.sqlite3_auto_extension = function(fPtr){
+ if( fPtr instanceof Function ){
+ fPtr = wasm.installFunction('i(ppp)', fPtr);
+ }else if( 1!==arguments.length || !wasm.isPtr(fPtr) ){
+ return capi.SQLITE_MISUSE;
+ }
+ const rc = wasm.exports.sqlite3_auto_extension(fPtr);
+ if( fPtr!==arguments[0] ){
+ if(0===rc) __autoExtFptr.add(fPtr);
+ else wasm.uninstallFunction(fPtr);
+ }
+ return rc;
+ };
+
+ capi.sqlite3_cancel_auto_extension = function(fPtr){
+ /* We do not do an automatic JS-to-WASM function conversion here
+ because it would be senseless: the converted pointer would
+ never possibly match an already-installed one. */;
+ if(!fPtr || 1!==arguments.length || !wasm.isPtr(fPtr)) return 0;
+ return wasm.exports.sqlite3_cancel_auto_extension(fPtr);
+ /* Note that it "cannot happen" that a client passes a pointer which
+ is in __autoExtFptr because __autoExtFptr only contains automatic
+ conversions created inside sqlite3_auto_extension() and
+ never exposed to the client. */
+ };
+
+ capi.sqlite3_reset_auto_extension = function(){
+ wasm.exports.sqlite3_reset_auto_extension();
+ for(const fp of __autoExtFptr) wasm.uninstallFunction(fp);
+ __autoExtFptr.clear();
+ };
+ }/* auto-extension */
+
+ const pKvvfs = capi.sqlite3_vfs_find("kvvfs");
+ if( pKvvfs ){/* kvvfs-specific glue */
+ if(util.isUIThread()){
+ const kvvfsMethods = new capi.sqlite3_kvvfs_methods(
+ wasm.exports.sqlite3_wasm_kvvfs_methods()
+ );
+ delete capi.sqlite3_kvvfs_methods;
+
+ const kvvfsMakeKey = wasm.exports.sqlite3_wasm_kvvfsMakeKeyOnPstack,
+ pstack = wasm.pstack;
+
+ const kvvfsStorage = (zClass)=>
+ ((115/*=='s'*/===wasm.peek(zClass))
+ ? sessionStorage : localStorage);
+
+ /**
+ Implementations for members of the object referred to by
+ sqlite3_wasm_kvvfs_methods(). We swap out the native
+ implementations with these, which use localStorage or
+ sessionStorage for their backing store.
+ */
+ const kvvfsImpls = {
+ xRead: (zClass, zKey, zBuf, nBuf)=>{
+ const stack = pstack.pointer,
+ astack = wasm.scopedAllocPush();
+ try {
+ const zXKey = kvvfsMakeKey(zClass,zKey);
+ if(!zXKey) return -3/*OOM*/;
+ const jKey = wasm.cstrToJs(zXKey);
+ const jV = kvvfsStorage(zClass).getItem(jKey);
+ if(!jV) return -1;
+ const nV = jV.length /* Note that we are relying 100% on v being
+ ASCII so that jV.length is equal to the
+ C-string's byte length. */;
+ if(nBuf<=0) return nV;
+ else if(1===nBuf){
+ wasm.poke(zBuf, 0);
+ return nV;
+ }
+ const zV = wasm.scopedAllocCString(jV);
+ if(nBuf > nV + 1) nBuf = nV + 1;
+ wasm.heap8u().copyWithin(zBuf, zV, zV + nBuf - 1);
+ wasm.poke(zBuf + nBuf - 1, 0);
+ return nBuf - 1;
+ }catch(e){
+ console.error("kvstorageRead()",e);
+ return -2;
+ }finally{
+ pstack.restore(stack);
+ wasm.scopedAllocPop(astack);
+ }
+ },
+ xWrite: (zClass, zKey, zData)=>{
+ const stack = pstack.pointer;
+ try {
+ const zXKey = kvvfsMakeKey(zClass,zKey);
+ if(!zXKey) return 1/*OOM*/;
+ const jKey = wasm.cstrToJs(zXKey);
+ kvvfsStorage(zClass).setItem(jKey, wasm.cstrToJs(zData));
+ return 0;
+ }catch(e){
+ console.error("kvstorageWrite()",e);
+ return capi.SQLITE_IOERR;
+ }finally{
+ pstack.restore(stack);
+ }
+ },
+ xDelete: (zClass, zKey)=>{
+ const stack = pstack.pointer;
+ try {
+ const zXKey = kvvfsMakeKey(zClass,zKey);
+ if(!zXKey) return 1/*OOM*/;
+ kvvfsStorage(zClass).removeItem(wasm.cstrToJs(zXKey));
+ return 0;
+ }catch(e){
+ console.error("kvstorageDelete()",e);
+ return capi.SQLITE_IOERR;
+ }finally{
+ pstack.restore(stack);
+ }
+ }
+ }/*kvvfsImpls*/;
+ for(const k of Object.keys(kvvfsImpls)){
+ kvvfsMethods[kvvfsMethods.memberKey(k)] =
+ wasm.installFunction(
+ kvvfsMethods.memberSignature(k),
+ kvvfsImpls[k]
+ );
+ }
+ }else{
+ /* Worker thread: unregister kvvfs to avoid it being used
+ for anything other than local/sessionStorage. It "can"
+ be used that way but it's not really intended to be. */
+ capi.sqlite3_vfs_unregister(pKvvfs);
+ }
+ }/*pKvvfs*/
+
+ wasm.xWrap.FuncPtrAdapter.warnOnUse = true;
+});
diff --git a/ext/wasm/api/sqlite3-api-oo1.js b/ext/wasm/api/sqlite3-api-oo1.js
new file mode 100644
index 0000000..160d59d
--- /dev/null
+++ b/ext/wasm/api/sqlite3-api-oo1.js
@@ -0,0 +1,1946 @@
+//#ifnot omit-oo1
+/*
+ 2022-07-22
+
+ 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 contains the so-called OO #1 API wrapper for the sqlite3
+ WASM build. It requires that sqlite3-api-glue.js has already run
+ and it installs its deliverable as globalThis.sqlite3.oo1.
+*/
+globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
+ const toss = (...args)=>{throw new Error(args.join(' '))};
+ const toss3 = (...args)=>{throw new sqlite3.SQLite3Error(...args)};
+
+ const capi = sqlite3.capi, wasm = sqlite3.wasm, util = sqlite3.util;
+ /* What follows is colloquially known as "OO API #1". It is a
+ binding of the sqlite3 API which is designed to be run within
+ the same thread (main or worker) as the one in which the
+ sqlite3 WASM binding was initialized. This wrapper cannot use
+ the sqlite3 binding if, e.g., the wrapper is in the main thread
+ and the sqlite3 API is in a worker. */
+
+ /**
+ In order to keep clients from manipulating, perhaps
+ inadvertently, the underlying pointer values of DB and Stmt
+ instances, we'll gate access to them via the `pointer` property
+ accessor and store their real values in this map. Keys = DB/Stmt
+ objects, values = pointer values. This also unifies how those are
+ accessed, for potential use downstream via custom
+ wasm.xWrap() function signatures which know how to extract
+ it.
+ */
+ const __ptrMap = new WeakMap();
+ /**
+ Map of DB instances to objects, each object being a map of Stmt
+ wasm pointers to Stmt objects.
+ */
+ const __stmtMap = new WeakMap();
+
+ /** If object opts has _its own_ property named p then that
+ property's value is returned, else dflt is returned. */
+ const getOwnOption = (opts, p, dflt)=>{
+ const d = Object.getOwnPropertyDescriptor(opts,p);
+ return d ? d.value : dflt;
+ };
+
+ // Documented in DB.checkRc()
+ const checkSqlite3Rc = function(dbPtr, sqliteResultCode){
+ if(sqliteResultCode){
+ if(dbPtr instanceof DB) dbPtr = dbPtr.pointer;
+ toss3(
+ sqliteResultCode,
+ "sqlite3 result code",sqliteResultCode+":",
+ (dbPtr
+ ? capi.sqlite3_errmsg(dbPtr)
+ : capi.sqlite3_errstr(sqliteResultCode))
+ );
+ }
+ return arguments[0];
+ };
+
+ /**
+ sqlite3_trace_v2() callback which gets installed by the DB ctor
+ if its open-flags contain "t".
+ */
+ const __dbTraceToConsole =
+ wasm.installFunction('i(ippp)', function(t,c,p,x){
+ if(capi.SQLITE_TRACE_STMT===t){
+ // x == SQL, p == sqlite3_stmt*
+ console.log("SQL TRACE #"+(++this.counter)+' via sqlite3@'+c+':',
+ wasm.cstrToJs(x));
+ }
+ }.bind({counter: 0}));
+
+ /**
+ A map of sqlite3_vfs pointers to SQL code or a callback function
+ to run when the DB constructor opens a database with the given
+ VFS. In the latter case, the call signature is (theDbObject,sqlite3Namespace)
+ and the callback is expected to throw on error.
+ */
+ const __vfsPostOpenSql = Object.create(null);
+
+ /**
+ A proxy for DB class constructors. It must be called with the
+ being-construct DB object as its "this". See the DB constructor
+ for the argument docs. This is split into a separate function
+ in order to enable simple creation of special-case DB constructors,
+ e.g. JsStorageDb and OpfsDb.
+
+ Expects to be passed a configuration object with the following
+ properties:
+
+ - `.filename`: the db filename. It may be a special name like ":memory:"
+ or "".
+
+ - `.flags`: as documented in the DB constructor.
+
+ - `.vfs`: as documented in the DB constructor.
+
+ It also accepts those as the first 3 arguments.
+ */
+ const dbCtorHelper = function ctor(...args){
+ if(!ctor._name2vfs){
+ /**
+ Map special filenames which we handle here (instead of in C)
+ to some helpful metadata...
+
+ As of 2022-09-20, the C API supports the names :localStorage:
+ and :sessionStorage: for kvvfs. However, C code cannot
+ determine (without embedded JS code, e.g. via Emscripten's
+ EM_JS()) whether the kvvfs is legal in the current browser
+ context (namely the main UI thread). In order to help client
+ code fail early on, instead of it being delayed until they
+ try to read or write a kvvfs-backed db, we'll check for those
+ names here and throw if they're not legal in the current
+ context.
+ */
+ ctor._name2vfs = Object.create(null);
+ const isWorkerThread = ('function'===typeof importScripts/*===running in worker thread*/)
+ ? (n)=>toss3("The VFS for",n,"is only available in the main window thread.")
+ : false;
+ ctor._name2vfs[':localStorage:'] = {
+ vfs: 'kvvfs', filename: isWorkerThread || (()=>'local')
+ };
+ ctor._name2vfs[':sessionStorage:'] = {
+ vfs: 'kvvfs', filename: isWorkerThread || (()=>'session')
+ };
+ }
+ const opt = ctor.normalizeArgs(...args);
+ let fn = opt.filename, vfsName = opt.vfs, flagsStr = opt.flags;
+ if(('string'!==typeof fn && 'number'!==typeof fn)
+ || 'string'!==typeof flagsStr
+ || (vfsName && ('string'!==typeof vfsName && 'number'!==typeof vfsName))){
+ sqlite3.config.error("Invalid DB ctor args",opt,arguments);
+ toss3("Invalid arguments for DB constructor.");
+ }
+ let fnJs = ('number'===typeof fn) ? wasm.cstrToJs(fn) : fn;
+ const vfsCheck = ctor._name2vfs[fnJs];
+ if(vfsCheck){
+ vfsName = vfsCheck.vfs;
+ fn = fnJs = vfsCheck.filename(fnJs);
+ }
+ let pDb, oflags = 0;
+ if( flagsStr.indexOf('c')>=0 ){
+ oflags |= capi.SQLITE_OPEN_CREATE | capi.SQLITE_OPEN_READWRITE;
+ }
+ if( flagsStr.indexOf('w')>=0 ) oflags |= capi.SQLITE_OPEN_READWRITE;
+ if( 0===oflags ) oflags |= capi.SQLITE_OPEN_READONLY;
+ oflags |= capi.SQLITE_OPEN_EXRESCODE;
+ const stack = wasm.pstack.pointer;
+ try {
+ const pPtr = wasm.pstack.allocPtr() /* output (sqlite3**) arg */;
+ let rc = capi.sqlite3_open_v2(fn, pPtr, oflags, vfsName || 0);
+ pDb = wasm.peekPtr(pPtr);
+ checkSqlite3Rc(pDb, rc);
+ capi.sqlite3_extended_result_codes(pDb, 1);
+ if(flagsStr.indexOf('t')>=0){
+ capi.sqlite3_trace_v2(pDb, capi.SQLITE_TRACE_STMT,
+ __dbTraceToConsole, pDb);
+ }
+ }catch( e ){
+ if( pDb ) capi.sqlite3_close_v2(pDb);
+ throw e;
+ }finally{
+ wasm.pstack.restore(stack);
+ }
+ this.filename = fnJs;
+ __ptrMap.set(this, pDb);
+ __stmtMap.set(this, Object.create(null));
+ try{
+ // Check for per-VFS post-open SQL/callback...
+ const pVfs = capi.sqlite3_js_db_vfs(pDb);
+ if(!pVfs) toss3("Internal error: cannot get VFS for new db handle.");
+ const postInitSql = __vfsPostOpenSql[pVfs];
+ if(postInitSql instanceof Function){
+ postInitSql(this, sqlite3);
+ }else if(postInitSql){
+ checkSqlite3Rc(
+ pDb, capi.sqlite3_exec(pDb, postInitSql, 0, 0, 0)
+ );
+ }
+ }catch(e){
+ this.close();
+ throw e;
+ }
+ };
+
+ /**
+ Sets SQL which should be exec()'d on a DB instance after it is
+ opened with the given VFS pointer. The SQL may be any type
+ supported by the "string:flexible" function argument conversion.
+ Alternately, the 2nd argument may be a function, in which case it
+ is called with (theOo1DbObject,sqlite3Namespace) at the end of
+ the DB() constructor. The function must throw on error, in which
+ case the db is closed and the exception is propagated. This
+ function is intended only for use by DB subclasses or sqlite3_vfs
+ implementations.
+ */
+ dbCtorHelper.setVfsPostOpenSql = function(pVfs, sql){
+ __vfsPostOpenSql[pVfs] = sql;
+ };
+
+ /**
+ A helper for DB constructors. It accepts either a single
+ config-style object or up to 3 arguments (filename, dbOpenFlags,
+ dbVfsName). It returns a new object containing:
+
+ { filename: ..., flags: ..., vfs: ... }
+
+ If passed an object, any additional properties it has are copied
+ as-is into the new object.
+ */
+ dbCtorHelper.normalizeArgs = function(filename=':memory:',flags = 'c',vfs = null){
+ const arg = {};
+ if(1===arguments.length && arguments[0] && 'object'===typeof arguments[0]){
+ Object.assign(arg, arguments[0]);
+ if(undefined===arg.flags) arg.flags = 'c';
+ if(undefined===arg.vfs) arg.vfs = null;
+ if(undefined===arg.filename) arg.filename = ':memory:';
+ }else{
+ arg.filename = filename;
+ arg.flags = flags;
+ arg.vfs = vfs;
+ }
+ return arg;
+ };
+ /**
+ The DB class provides a high-level OO wrapper around an sqlite3
+ db handle.
+
+ The given db filename must be resolvable using whatever
+ filesystem layer (virtual or otherwise) is set up for the default
+ sqlite3 VFS.
+
+ Note that the special sqlite3 db names ":memory:" and ""
+ (temporary db) have their normal special meanings here and need
+ not resolve to real filenames, but "" uses an on-storage
+ temporary database and requires that the VFS support that.
+
+ The second argument specifies the open/create mode for the
+ database. It must be string containing a sequence of letters (in
+ any order, but case sensitive) specifying the mode:
+
+ - "c": create if it does not exist, else fail if it does not
+ exist. Implies the "w" flag.
+
+ - "w": write. Implies "r": a db cannot be write-only.
+
+ - "r": read-only if neither "w" nor "c" are provided, else it
+ is ignored.
+
+ - "t": enable tracing of SQL executed on this database handle,
+ sending it to `console.log()`. To disable it later, call
+ `sqlite3.capi.sqlite3_trace_v2(thisDb.pointer, 0, 0, 0)`.
+
+ If "w" is not provided, the db is implicitly read-only, noting
+ that "rc" is meaningless
+
+ Any other letters are currently ignored. The default is
+ "c". These modes are ignored for the special ":memory:" and ""
+ names and _may_ be ignored altogether for certain VFSes.
+
+ The final argument is analogous to the final argument of
+ sqlite3_open_v2(): the name of an sqlite3 VFS. Pass a falsy value,
+ or none at all, to use the default. If passed a value, it must
+ be the string name of a VFS.
+
+ The constructor optionally (and preferably) takes its arguments
+ in the form of a single configuration object with the following
+ properties:
+
+ - `filename`: database file name
+ - `flags`: open-mode flags
+ - `vfs`: the VFS fname
+
+ The `filename` and `vfs` arguments may be either JS strings or
+ C-strings allocated via WASM. `flags` is required to be a JS
+ string (because it's specific to this API, which is specific
+ to JS).
+
+ For purposes of passing a DB instance to C-style sqlite3
+ functions, the DB object's read-only `pointer` property holds its
+ `sqlite3*` pointer value. That property can also be used to check
+ whether this DB instance is still open.
+
+ In the main window thread, the filenames `":localStorage:"` and
+ `":sessionStorage:"` are special: they cause the db to use either
+ localStorage or sessionStorage for storing the database using
+ the kvvfs. If one of these names are used, they trump
+ any vfs name set in the arguments.
+ */
+ const DB = function(...args){
+ dbCtorHelper.apply(this, args);
+ };
+ DB.dbCtorHelper = dbCtorHelper;
+
+ /**
+ Internal-use enum for mapping JS types to DB-bindable types.
+ These do not (and need not) line up with the SQLITE_type
+ values. All values in this enum must be truthy and distinct
+ but they need not be numbers.
+ */
+ const BindTypes = {
+ null: 1,
+ number: 2,
+ string: 3,
+ boolean: 4,
+ blob: 5
+ };
+ BindTypes['undefined'] == BindTypes.null;
+ if(wasm.bigIntEnabled){
+ BindTypes.bigint = BindTypes.number;
+ }
+
+ /**
+ This class wraps sqlite3_stmt. Calling this constructor
+ directly will trigger an exception. Use DB.prepare() to create
+ new instances.
+
+ For purposes of passing a Stmt instance to C-style sqlite3
+ functions, its read-only `pointer` property holds its `sqlite3_stmt*`
+ pointer value.
+
+ Other non-function properties include:
+
+ - `db`: the DB object which created the statement.
+
+ - `columnCount`: the number of result columns in the query, or 0
+ for queries which cannot return results. This property is a proxy
+ for sqlite3_column_count() and its use in loops should be avoided
+ because of the call overhead associated with that. The
+ `columnCount` is not cached when the Stmt is created because a
+ schema change made via a separate db connection between this
+ statement's preparation and when it is stepped may invalidate it.
+
+ - `parameterCount`: the number of bindable parameters in the query.
+ */
+ const Stmt = function(){
+ if(BindTypes!==arguments[2]){
+ toss3(capi.SQLITE_MISUSE, "Do not call the Stmt constructor directly. Use DB.prepare().");
+ }
+ this.db = arguments[0];
+ __ptrMap.set(this, arguments[1]);
+ this.parameterCount = capi.sqlite3_bind_parameter_count(this.pointer);
+ };
+
+ /** Throws if the given DB has been closed, else it is returned. */
+ const affirmDbOpen = function(db){
+ if(!db.pointer) toss3("DB has been closed.");
+ return db;
+ };
+
+ /** Throws if ndx is not an integer or if it is out of range
+ for stmt.columnCount, else returns stmt.
+
+ Reminder: this will also fail after the statement is finalized
+ but the resulting error will be about an out-of-bounds column
+ index rather than a statement-is-finalized error.
+ */
+ const affirmColIndex = function(stmt,ndx){
+ if((ndx !== (ndx|0)) || ndx<0 || ndx>=stmt.columnCount){
+ toss3("Column index",ndx,"is out of range.");
+ }
+ return stmt;
+ };
+
+ /**
+ Expects to be passed the `arguments` object from DB.exec(). Does
+ the argument processing/validation, throws on error, and returns
+ a new object on success:
+
+ { sql: the SQL, opt: optionsObj, cbArg: function}
+
+ The opt object is a normalized copy of any passed to this
+ function. The sql will be converted to a string if it is provided
+ in one of the supported non-string formats.
+
+ cbArg is only set if the opt.callback or opt.resultRows are set,
+ in which case it's a function which expects to be passed the
+ current Stmt and returns the callback argument of the type
+ indicated by the input arguments.
+ */
+ const parseExecArgs = function(db, args){
+ const out = Object.create(null);
+ out.opt = Object.create(null);
+ switch(args.length){
+ case 1:
+ if('string'===typeof args[0] || util.isSQLableTypedArray(args[0])){
+ out.sql = args[0];
+ }else if(Array.isArray(args[0])){
+ out.sql = args[0];
+ }else if(args[0] && 'object'===typeof args[0]){
+ out.opt = args[0];
+ out.sql = out.opt.sql;
+ }
+ break;
+ case 2:
+ out.sql = args[0];
+ out.opt = args[1];
+ break;
+ default: toss3("Invalid argument count for exec().");
+ };
+ out.sql = util.flexibleString(out.sql);
+ if('string'!==typeof out.sql){
+ toss3("Missing SQL argument or unsupported SQL value type.");
+ }
+ const opt = out.opt;
+ switch(opt.returnValue){
+ case 'resultRows':
+ if(!opt.resultRows) opt.resultRows = [];
+ out.returnVal = ()=>opt.resultRows;
+ break;
+ case 'saveSql':
+ if(!opt.saveSql) opt.saveSql = [];
+ out.returnVal = ()=>opt.saveSql;
+ break;
+ case undefined:
+ case 'this':
+ out.returnVal = ()=>db;
+ break;
+ default:
+ toss3("Invalid returnValue value:",opt.returnValue);
+ }
+ if(!opt.callback && !opt.returnValue && undefined!==opt.rowMode){
+ if(!opt.resultRows) opt.resultRows = [];
+ out.returnVal = ()=>opt.resultRows;
+ }
+ if(opt.callback || opt.resultRows){
+ switch((undefined===opt.rowMode)
+ ? 'array' : opt.rowMode) {
+ case 'object': out.cbArg = (stmt)=>stmt.get(Object.create(null)); break;
+ case 'array': out.cbArg = (stmt)=>stmt.get([]); break;
+ case 'stmt':
+ if(Array.isArray(opt.resultRows)){
+ toss3("exec(): invalid rowMode for a resultRows array: must",
+ "be one of 'array', 'object',",
+ "a result column number, or column name reference.");
+ }
+ out.cbArg = (stmt)=>stmt;
+ break;
+ default:
+ if(util.isInt32(opt.rowMode)){
+ out.cbArg = (stmt)=>stmt.get(opt.rowMode);
+ break;
+ }else if('string'===typeof opt.rowMode
+ && opt.rowMode.length>1
+ && '$'===opt.rowMode[0]){
+ /* "$X": fetch column named "X" (case-sensitive!). Prior
+ to 2022-12-14 ":X" and "@X" were also permitted, but
+ having so many options is unnecessary and likely to
+ cause confusion. */
+ const $colName = opt.rowMode.substr(1);
+ out.cbArg = (stmt)=>{
+ const rc = stmt.get(Object.create(null))[$colName];
+ return (undefined===rc)
+ ? toss3(capi.SQLITE_NOTFOUND,
+ "exec(): unknown result column:",$colName)
+ : rc;
+ };
+ break;
+ }
+ toss3("Invalid rowMode:",opt.rowMode);
+ }
+ }
+ return out;
+ };
+
+ /**
+ Internal impl of the DB.selectValue(), selectArray(), and
+ selectObject() methods.
+ */
+ const __selectFirstRow = (db, sql, bind, ...getArgs)=>{
+ const stmt = db.prepare(sql);
+ try {
+ const rc = stmt.bind(bind).step() ? stmt.get(...getArgs) : undefined;
+ stmt.reset(/*for INSERT...RETURNING locking case*/);
+ return rc;
+ }finally{
+ stmt.finalize();
+ }
+ };
+
+ /**
+ Internal impl of the DB.selectArrays() and selectObjects()
+ methods.
+ */
+ const __selectAll =
+ (db, sql, bind, rowMode)=>db.exec({
+ sql, bind, rowMode, returnValue: 'resultRows'
+ });
+
+ /**
+ Expects to be given a DB instance or an `sqlite3*` pointer (may
+ be null) and an sqlite3 API result code. If the result code is
+ not falsy, this function throws an SQLite3Error with an error
+ message from sqlite3_errmsg(), using db (or, if db is-a DB,
+ db.pointer) as the db handle, or sqlite3_errstr() if db is
+ falsy. Note that if it's passed a non-error code like SQLITE_ROW
+ or SQLITE_DONE, it will still throw but the error string might be
+ "Not an error." The various non-0 non-error codes need to be
+ checked for in client code where they are expected.
+
+ The thrown exception's `resultCode` property will be the value of
+ the second argument to this function.
+
+ If it does not throw, it returns its first argument.
+ */
+ DB.checkRc = (db,resultCode)=>checkSqlite3Rc(db,resultCode);
+
+ DB.prototype = {
+ /** Returns true if this db handle is open, else false. */
+ isOpen: function(){
+ return !!this.pointer;
+ },
+ /** Throws if this given DB has been closed, else returns `this`. */
+ affirmOpen: function(){
+ return affirmDbOpen(this);
+ },
+ /**
+ Finalizes all open statements and closes this database
+ connection. This is a no-op if the db has already been
+ closed. After calling close(), `this.pointer` will resolve to
+ `undefined`, so that can be used to check whether the db
+ instance is still opened.
+
+ If this.onclose.before is a function then it is called before
+ any close-related cleanup.
+
+ If this.onclose.after is a function then it is called after the
+ db is closed but before auxiliary state like this.filename is
+ cleared.
+
+ Both onclose handlers are passed this object, with the onclose
+ object as their "this," noting that the db will have been
+ closed when onclose.after is called. If this db is not opened
+ when close() is called, neither of the handlers are called. Any
+ exceptions the handlers throw are ignored because "destructors
+ must not throw."
+
+ Note that garbage collection of a db handle, if it happens at
+ all, will never trigger close(), so onclose handlers are not a
+ reliable way to implement close-time cleanup or maintenance of
+ a db.
+ */
+ close: function(){
+ if(this.pointer){
+ if(this.onclose && (this.onclose.before instanceof Function)){
+ try{this.onclose.before(this)}
+ catch(e){/*ignore*/}
+ }
+ const pDb = this.pointer;
+ Object.keys(__stmtMap.get(this)).forEach((k,s)=>{
+ if(s && s.pointer){
+ try{s.finalize()}
+ catch(e){/*ignore*/}
+ }
+ });
+ __ptrMap.delete(this);
+ __stmtMap.delete(this);
+ capi.sqlite3_close_v2(pDb);
+ if(this.onclose && (this.onclose.after instanceof Function)){
+ try{this.onclose.after(this)}
+ catch(e){/*ignore*/}
+ }
+ delete this.filename;
+ }
+ },
+ /**
+ Returns the number of changes, as per sqlite3_changes()
+ (if the first argument is false) or sqlite3_total_changes()
+ (if it's true). If the 2nd argument is true, it uses
+ sqlite3_changes64() or sqlite3_total_changes64(), which
+ will trigger an exception if this build does not have
+ BigInt support enabled.
+ */
+ changes: function(total=false,sixtyFour=false){
+ const p = affirmDbOpen(this).pointer;
+ if(total){
+ return sixtyFour
+ ? capi.sqlite3_total_changes64(p)
+ : capi.sqlite3_total_changes(p);
+ }else{
+ return sixtyFour
+ ? capi.sqlite3_changes64(p)
+ : capi.sqlite3_changes(p);
+ }
+ },
+ /**
+ Similar to the this.filename but returns the
+ sqlite3_db_filename() value for the given database name,
+ defaulting to "main". The argument may be either a JS string
+ or a pointer to a WASM-allocated C-string.
+ */
+ dbFilename: function(dbName='main'){
+ return capi.sqlite3_db_filename(affirmDbOpen(this).pointer, dbName);
+ },
+ /**
+ Returns the name of the given 0-based db number, as documented
+ for sqlite3_db_name().
+ */
+ dbName: function(dbNumber=0){
+ return capi.sqlite3_db_name(affirmDbOpen(this).pointer, dbNumber);
+ },
+ /**
+ Returns the name of the sqlite3_vfs used by the given database
+ of this connection (defaulting to 'main'). The argument may be
+ either a JS string or a WASM C-string. Returns undefined if the
+ given db name is invalid. Throws if this object has been
+ close()d.
+ */
+ dbVfsName: function(dbName=0){
+ let rc;
+ const pVfs = capi.sqlite3_js_db_vfs(
+ affirmDbOpen(this).pointer, dbName
+ );
+ if(pVfs){
+ const v = new capi.sqlite3_vfs(pVfs);
+ try{ rc = wasm.cstrToJs(v.$zName) }
+ finally { v.dispose() }
+ }
+ return rc;
+ },
+ /**
+ Compiles the given SQL and returns a prepared Stmt. This is
+ the only way to create new Stmt objects. Throws on error.
+
+ The given SQL must be a string, a Uint8Array holding SQL, a
+ WASM pointer to memory holding the NUL-terminated SQL string,
+ or an array of strings. In the latter case, the array is
+ concatenated together, with no separators, to form the SQL
+ string (arrays are often a convenient way to formulate long
+ statements). If the SQL contains no statements, an
+ SQLite3Error is thrown.
+
+ Design note: the C API permits empty SQL, reporting it as a 0
+ result code and a NULL stmt pointer. Supporting that case here
+ would cause extra work for all clients: any use of the Stmt API
+ on such a statement will necessarily throw, so clients would be
+ required to check `stmt.pointer` after calling `prepare()` in
+ order to determine whether the Stmt instance is empty or not.
+ Long-time practice (with other sqlite3 script bindings)
+ suggests that the empty-prepare case is sufficiently rare that
+ supporting it here would simply hurt overall usability.
+ */
+ prepare: function(sql){
+ affirmDbOpen(this);
+ const stack = wasm.pstack.pointer;
+ let ppStmt, pStmt;
+ try{
+ ppStmt = wasm.pstack.alloc(8)/* output (sqlite3_stmt**) arg */;
+ DB.checkRc(this, capi.sqlite3_prepare_v2(this.pointer, sql, -1, ppStmt, null));
+ pStmt = wasm.peekPtr(ppStmt);
+ }
+ finally {
+ wasm.pstack.restore(stack);
+ }
+ if(!pStmt) toss3("Cannot prepare empty SQL.");
+ const stmt = new Stmt(this, pStmt, BindTypes);
+ __stmtMap.get(this)[pStmt] = stmt;
+ return stmt;
+ },
+ /**
+ Executes one or more SQL statements in the form of a single
+ string. Its arguments must be either (sql,optionsObject) or
+ (optionsObject). In the latter case, optionsObject.sql must
+ contain the SQL to execute. By default it returns this object
+ but that can be changed via the `returnValue` option as
+ described below. Throws on error.
+
+ If no SQL is provided, or a non-string is provided, an
+ exception is triggered. Empty SQL, on the other hand, is
+ simply a no-op.
+
+ The optional options object may contain any of the following
+ properties:
+
+ - `sql` = the SQL to run (unless it's provided as the first
+ argument). This must be of type string, Uint8Array, or an array
+ of strings. In the latter case they're concatenated together
+ as-is, _with no separator_ between elements, before evaluation.
+ The array form is often simpler for long hand-written queries.
+
+ - `bind` = a single value valid as an argument for
+ Stmt.bind(). This is _only_ applied to the _first_ non-empty
+ statement in the SQL which has any bindable parameters. (Empty
+ statements are skipped entirely.)
+
+ - `saveSql` = an optional array. If set, the SQL of each
+ executed statement is appended to this array before the
+ statement is executed (but after it is prepared - we don't have
+ the string until after that). Empty SQL statements are elided
+ but can have odd effects in the output. e.g. SQL of: `"select
+ 1; -- empty\n; select 2"` will result in an array containing
+ `["select 1;", "--empty \n; select 2"]`. That's simply how
+ sqlite3 records the SQL for the 2nd statement.
+
+ ==================================================================
+ The following options apply _only_ to the _first_ statement
+ which has a non-zero result column count, regardless of whether
+ the statement actually produces any result rows.
+ ==================================================================
+
+ - `columnNames`: if this is an array, the column names of the
+ result set are stored in this array before the callback (if
+ any) is triggered (regardless of whether the query produces any
+ result rows). If no statement has result columns, this value is
+ unchanged. Achtung: an SQL result may have multiple columns
+ with identical names.
+
+ - `callback` = a function which gets called for each row of the
+ result set, but only if that statement has any result rows. The
+ callback's "this" is the options object, noting that this
+ function synthesizes one if the caller does not pass one to
+ exec(). The second argument passed to the callback is always
+ the current Stmt object, as it's needed if the caller wants to
+ fetch the column names or some such (noting that they could
+ also be fetched via `this.columnNames`, if the client provides
+ the `columnNames` option). If the callback returns a literal
+ `false` (as opposed to any other falsy value, e.g. an implicit
+ `undefined` return), any ongoing statement-`step()` iteration
+ stops without an error. The return value of the callback is
+ otherwise ignored.
+
+ ACHTUNG: The callback MUST NOT modify the Stmt object. Calling
+ any of the Stmt.get() variants, Stmt.getColumnName(), or
+ similar, is legal, but calling step() or finalize() is
+ not. Member methods which are illegal in this context will
+ trigger an exception, but clients must also refrain from using
+ any lower-level (C-style) APIs which might modify the
+ statement.
+
+ The first argument passed to the callback defaults to an array of
+ values from the current result row but may be changed with ...
+
+ - `rowMode` = specifies the type of he callback's first argument.
+ It may be any of...
+
+ A) A string describing what type of argument should be passed
+ as the first argument to the callback:
+
+ A.1) `'array'` (the default) causes the results of
+ `stmt.get([])` to be passed to the `callback` and/or appended
+ to `resultRows`.
+
+ A.2) `'object'` causes the results of
+ `stmt.get(Object.create(null))` to be passed to the
+ `callback` and/or appended to `resultRows`. Achtung: an SQL
+ result may have multiple columns with identical names. In
+ that case, the right-most column will be the one set in this
+ object!
+
+ A.3) `'stmt'` causes the current Stmt to be passed to the
+ callback, but this mode will trigger an exception if
+ `resultRows` is an array because appending the transient
+ statement to the array would be downright unhelpful.
+
+ B) An integer, indicating a zero-based column in the result
+ row. Only that one single value will be passed on.
+
+ C) A string with a minimum length of 2 and leading character of
+ '$' will fetch the row as an object, extract that one field,
+ and pass that field's value to the callback. Note that these
+ keys are case-sensitive so must match the case used in the
+ SQL. e.g. `"select a A from t"` with a `rowMode` of `'$A'`
+ would work but `'$a'` would not. A reference to a column not in
+ the result set will trigger an exception on the first row (as
+ the check is not performed until rows are fetched). Note also
+ that `$` is a legal identifier character in JS so need not be
+ quoted.
+
+ Any other `rowMode` value triggers an exception.
+
+ - `resultRows`: if this is an array, it functions similarly to
+ the `callback` option: each row of the result set (if any),
+ with the exception that the `rowMode` 'stmt' is not legal. It
+ is legal to use both `resultRows` and `callback`, but
+ `resultRows` is likely much simpler to use for small data sets
+ and can be used over a WebWorker-style message interface.
+ exec() throws if `resultRows` is set and `rowMode` is 'stmt'.
+
+ - `returnValue`: is a string specifying what this function
+ should return:
+
+ A) The default value is (usually) `"this"`, meaning that the
+ DB object itself should be returned. The exception is if
+ the caller passes neither of `callback` nor `returnValue`
+ but does pass an explicit `rowMode` then the default
+ `returnValue` is `"resultRows"`, described below.
+
+ B) `"resultRows"` means to return the value of the
+ `resultRows` option. If `resultRows` is not set, this
+ function behaves as if it were set to an empty array.
+
+ C) `"saveSql"` means to return the value of the
+ `saveSql` option. If `saveSql` is not set, this
+ function behaves as if it were set to an empty array.
+
+ Potential TODOs:
+
+ - `bind`: permit an array of arrays/objects to bind. The first
+ sub-array would act on the first statement which has bindable
+ parameters (as it does now). The 2nd would act on the next such
+ statement, etc.
+
+ - `callback` and `resultRows`: permit an array entries with
+ semantics similar to those described for `bind` above.
+
+ */
+ exec: function(/*(sql [,obj]) || (obj)*/){
+ affirmDbOpen(this);
+ const arg = parseExecArgs(this, arguments);
+ if(!arg.sql){
+ return toss3("exec() requires an SQL string.");
+ }
+ const opt = arg.opt;
+ const callback = opt.callback;
+ const resultRows =
+ Array.isArray(opt.resultRows) ? opt.resultRows : undefined;
+ let stmt;
+ let bind = opt.bind;
+ let evalFirstResult = !!(
+ arg.cbArg || opt.columnNames || resultRows
+ ) /* true to step through the first result-returning statement */;
+ const stack = wasm.scopedAllocPush();
+ const saveSql = Array.isArray(opt.saveSql) ? opt.saveSql : undefined;
+ try{
+ const isTA = util.isSQLableTypedArray(arg.sql)
+ /* Optimization: if the SQL is a TypedArray we can save some string
+ conversion costs. */;
+ /* Allocate the two output pointers (ppStmt, pzTail) and heap
+ space for the SQL (pSql). When prepare_v2() returns, pzTail
+ will point to somewhere in pSql. */
+ let sqlByteLen = isTA ? arg.sql.byteLength : wasm.jstrlen(arg.sql);
+ const ppStmt = wasm.scopedAlloc(
+ /* output (sqlite3_stmt**) arg and pzTail */
+ (2 * wasm.ptrSizeof) + (sqlByteLen + 1/* SQL + NUL */)
+ );
+ const pzTail = ppStmt + wasm.ptrSizeof /* final arg to sqlite3_prepare_v2() */;
+ let pSql = pzTail + wasm.ptrSizeof;
+ const pSqlEnd = pSql + sqlByteLen;
+ if(isTA) wasm.heap8().set(arg.sql, pSql);
+ else wasm.jstrcpy(arg.sql, wasm.heap8(), pSql, sqlByteLen, false);
+ wasm.poke(pSql + sqlByteLen, 0/*NUL terminator*/);
+ while(pSql && wasm.peek(pSql, 'i8')
+ /* Maintenance reminder:^^^ _must_ be 'i8' or else we
+ will very likely cause an endless loop. What that's
+ doing is checking for a terminating NUL byte. If we
+ use i32 or similar then we read 4 bytes, read stuff
+ around the NUL terminator, and get stuck in and
+ endless loop at the end of the SQL, endlessly
+ re-preparing an empty statement. */ ){
+ wasm.pokePtr([ppStmt, pzTail], 0);
+ DB.checkRc(this, capi.sqlite3_prepare_v3(
+ this.pointer, pSql, sqlByteLen, 0, ppStmt, pzTail
+ ));
+ const pStmt = wasm.peekPtr(ppStmt);
+ pSql = wasm.peekPtr(pzTail);
+ sqlByteLen = pSqlEnd - pSql;
+ if(!pStmt) continue;
+ if(saveSql) saveSql.push(capi.sqlite3_sql(pStmt).trim());
+ stmt = new Stmt(this, pStmt, BindTypes);
+ if(bind && stmt.parameterCount){
+ stmt.bind(bind);
+ bind = null;
+ }
+ if(evalFirstResult && stmt.columnCount){
+ /* Only forward SELECT-style results for the FIRST query
+ in the SQL which potentially has them. */
+ let gotColNames = Array.isArray(
+ opt.columnNames
+ /* As reported in
+ https://sqlite.org/forum/forumpost/7774b773937cbe0a
+ we need to delay fetching of the column names until
+ after the first step() (if we step() at all) because
+ a schema change between the prepare() and step(), via
+ another connection, may invalidate the column count
+ and names. */) ? 0 : 1;
+ evalFirstResult = false;
+ if(arg.cbArg || resultRows){
+ for(; stmt.step(); stmt._lockedByExec = false){
+ if(0===gotColNames++) stmt.getColumnNames(opt.columnNames);
+ stmt._lockedByExec = true;
+ const row = arg.cbArg(stmt);
+ if(resultRows) resultRows.push(row);
+ if(callback && false === callback.call(opt, row, stmt)){
+ break;
+ }
+ }
+ stmt._lockedByExec = false;
+ }
+ if(0===gotColNames){
+ /* opt.columnNames was provided but we visited no result rows */
+ stmt.getColumnNames(opt.columnNames);
+ }
+ }else{
+ stmt.step();
+ }
+ stmt.reset(
+ /* In order to trigger an exception in the
+ INSERT...RETURNING locking scenario:
+ https://sqlite.org/forum/forumpost/36f7a2e7494897df
+ */).finalize();
+ stmt = null;
+ }/*prepare() loop*/
+ }/*catch(e){
+ sqlite3.config.warn("DB.exec() is propagating exception",opt,e);
+ throw e;
+ }*/finally{
+ wasm.scopedAllocPop(stack);
+ if(stmt){
+ delete stmt._lockedByExec;
+ stmt.finalize();
+ }
+ }
+ return arg.returnVal();
+ }/*exec()*/,
+
+ /**
+ Creates a new UDF (User-Defined Function) which is accessible
+ via SQL code. This function may be called in any of the
+ following forms:
+
+ - (name, function)
+ - (name, function, optionsObject)
+ - (name, optionsObject)
+ - (optionsObject)
+
+ In the final two cases, the function must be defined as the
+ `callback` property of the options object (optionally called
+ `xFunc` to align with the C API documentation). In the final
+ case, the function's name must be the 'name' property.
+
+ The first two call forms can only be used for creating scalar
+ functions. Creating an aggregate or window function requires
+ the options-object form (see below for details).
+
+ UDFs can be removed as documented for
+ sqlite3_create_function_v2() and
+ sqlite3_create_window_function(), but doing so will "leak" the
+ JS-created WASM binding of those functions (meaning that their
+ entries in the WASM indirect function table still
+ exist). Eliminating that potential leak is a pending TODO.
+
+ On success, returns this object. Throws on error.
+
+ When called from SQL arguments to the UDF, and its result,
+ will be converted between JS and SQL with as much fidelity as
+ is feasible, triggering an exception if a type conversion
+ cannot be determined. The docs for sqlite3_create_function_v2()
+ describe the conversions in more detail.
+
+ The values set in the options object differ for scalar and
+ aggregate functions:
+
+ - Scalar: set the `xFunc` function-type property to the UDF
+ function.
+
+ - Aggregate: set the `xStep` and `xFinal` function-type
+ properties to the "step" and "final" callbacks for the
+ aggregate. Do not set the `xFunc` property.
+
+ - Window: set the `xStep`, `xFinal`, `xValue`, and `xInverse`
+ function-type properties. Do not set the `xFunc` property.
+
+ The options object may optionally have an `xDestroy`
+ function-type property, as per sqlite3_create_function_v2().
+ Its argument will be the WASM-pointer-type value of the `pApp`
+ property, and this function will throw if `pApp` is defined but
+ is not null, undefined, or a numeric (WASM pointer)
+ value. i.e. `pApp`, if set, must be value suitable for use as a
+ WASM pointer argument, noting that `null` or `undefined` will
+ translate to 0 for that purpose.
+
+ The options object may contain flags to modify how
+ the function is defined:
+
+ - `arity`: the number of arguments which SQL calls to this
+ function expect or require. The default value is `xFunc.length`
+ or `xStep.length` (i.e. the number of declared parameters it
+ has) **MINUS 1** (see below for why). As a special case, if the
+ `length` is 0, its arity is also 0 instead of -1. A negative
+ arity value means that the function is variadic and may accept
+ any number of arguments, up to sqlite3's compile-time
+ limits. sqlite3 will enforce the argument count if is zero or
+ greater. The callback always receives a pointer to an
+ `sqlite3_context` object as its first argument. Any arguments
+ after that are from SQL code. The leading context argument does
+ _not_ count towards the function's arity. See the docs for
+ sqlite3.capi.sqlite3_create_function_v2() for why that argument
+ is needed in the interface.
+
+ The following options-object properties correspond to flags
+ documented at:
+
+ https://sqlite.org/c3ref/create_function.html
+
+ - `deterministic` = sqlite3.capi.SQLITE_DETERMINISTIC
+ - `directOnly` = sqlite3.capi.SQLITE_DIRECTONLY
+ - `innocuous` = sqlite3.capi.SQLITE_INNOCUOUS
+
+ Sidebar: the ability to add new WASM-accessible functions to
+ the runtime requires that the WASM build is compiled with the
+ equivalent functionality as that provided by Emscripten's
+ `-sALLOW_TABLE_GROWTH` flag.
+ */
+ createFunction: function f(name, xFunc, opt){
+ const isFunc = (f)=>(f instanceof Function);
+ switch(arguments.length){
+ case 1: /* (optionsObject) */
+ opt = name;
+ name = opt.name;
+ xFunc = opt.xFunc || 0;
+ break;
+ case 2: /* (name, callback|optionsObject) */
+ if(!isFunc(xFunc)){
+ opt = xFunc;
+ xFunc = opt.xFunc || 0;
+ }
+ break;
+ case 3: /* name, xFunc, opt */
+ break;
+ default: break;
+ }
+ if(!opt) opt = {};
+ if('string' !== typeof name){
+ toss3("Invalid arguments: missing function name.");
+ }
+ let xStep = opt.xStep || 0;
+ let xFinal = opt.xFinal || 0;
+ const xValue = opt.xValue || 0;
+ const xInverse = opt.xInverse || 0;
+ let isWindow = undefined;
+ if(isFunc(xFunc)){
+ isWindow = false;
+ if(isFunc(xStep) || isFunc(xFinal)){
+ toss3("Ambiguous arguments: scalar or aggregate?");
+ }
+ xStep = xFinal = null;
+ }else if(isFunc(xStep)){
+ if(!isFunc(xFinal)){
+ toss3("Missing xFinal() callback for aggregate or window UDF.");
+ }
+ xFunc = null;
+ }else if(isFunc(xFinal)){
+ toss3("Missing xStep() callback for aggregate or window UDF.");
+ }else{
+ toss3("Missing function-type properties.");
+ }
+ if(false === isWindow){
+ if(isFunc(xValue) || isFunc(xInverse)){
+ toss3("xValue and xInverse are not permitted for non-window UDFs.");
+ }
+ }else if(isFunc(xValue)){
+ if(!isFunc(xInverse)){
+ toss3("xInverse must be provided if xValue is.");
+ }
+ isWindow = true;
+ }else if(isFunc(xInverse)){
+ toss3("xValue must be provided if xInverse is.");
+ }
+ const pApp = opt.pApp;
+ if(undefined!==pApp &&
+ null!==pApp &&
+ (('number'!==typeof pApp) || !util.isInt32(pApp))){
+ toss3("Invalid value for pApp property. Must be a legal WASM pointer value.");
+ }
+ const xDestroy = opt.xDestroy || 0;
+ if(xDestroy && !isFunc(xDestroy)){
+ toss3("xDestroy property must be a function.");
+ }
+ let fFlags = 0 /*flags for sqlite3_create_function_v2()*/;
+ if(getOwnOption(opt, 'deterministic')) fFlags |= capi.SQLITE_DETERMINISTIC;
+ if(getOwnOption(opt, 'directOnly')) fFlags |= capi.SQLITE_DIRECTONLY;
+ if(getOwnOption(opt, 'innocuous')) fFlags |= capi.SQLITE_INNOCUOUS;
+ name = name.toLowerCase();
+ const xArity = xFunc || xStep;
+ const arity = getOwnOption(opt, 'arity');
+ const arityArg = ('number'===typeof arity
+ ? arity
+ : (xArity.length ? xArity.length-1/*for pCtx arg*/ : 0));
+ let rc;
+ if( isWindow ){
+ rc = capi.sqlite3_create_window_function(
+ this.pointer, name, arityArg,
+ capi.SQLITE_UTF8 | fFlags, pApp || 0,
+ xStep, xFinal, xValue, xInverse, xDestroy);
+ }else{
+ rc = capi.sqlite3_create_function_v2(
+ this.pointer, name, arityArg,
+ capi.SQLITE_UTF8 | fFlags, pApp || 0,
+ xFunc, xStep, xFinal, xDestroy);
+ }
+ DB.checkRc(this, rc);
+ return this;
+ }/*createFunction()*/,
+ /**
+ Prepares the given SQL, step()s it one time, and returns
+ the value of the first result column. If it has no results,
+ undefined is returned.
+
+ If passed a second argument, it is treated like an argument
+ to Stmt.bind(), so may be any type supported by that
+ function. Passing the undefined value is the same as passing
+ no value, which is useful when...
+
+ If passed a 3rd argument, it is expected to be one of the
+ SQLITE_{typename} constants. Passing the undefined value is
+ the same as not passing a value.
+
+ Throws on error (e.g. malformed SQL).
+ */
+ selectValue: function(sql,bind,asType){
+ return __selectFirstRow(this, sql, bind, 0, asType);
+ },
+
+ /**
+ Runs the given query and returns an array of the values from
+ the first result column of each row of the result set. The 2nd
+ argument is an optional value for use in a single-argument call
+ to Stmt.bind(). The 3rd argument may be any value suitable for
+ use as the 2nd argument to Stmt.get(). If a 3rd argument is
+ desired but no bind data are needed, pass `undefined` for the 2nd
+ argument.
+
+ If there are no result rows, an empty array is returned.
+ */
+ selectValues: function(sql,bind,asType){
+ const stmt = this.prepare(sql), rc = [];
+ try {
+ stmt.bind(bind);
+ while(stmt.step()) rc.push(stmt.get(0,asType));
+ stmt.reset(/*for INSERT...RETURNING locking case*/);
+ }finally{
+ stmt.finalize();
+ }
+ return rc;
+ },
+
+ /**
+ Prepares the given SQL, step()s it one time, and returns an
+ array containing the values of the first result row. If it has
+ no results, `undefined` is returned.
+
+ If passed a second argument other than `undefined`, it is
+ treated like an argument to Stmt.bind(), so may be any type
+ supported by that function.
+
+ Throws on error (e.g. malformed SQL).
+ */
+ selectArray: function(sql,bind){
+ return __selectFirstRow(this, sql, bind, []);
+ },
+
+ /**
+ Prepares the given SQL, step()s it one time, and returns an
+ object containing the key/value pairs of the first result
+ row. If it has no results, `undefined` is returned.
+
+ Note that the order of returned object's keys is not guaranteed
+ to be the same as the order of the fields in the query string.
+
+ If passed a second argument other than `undefined`, it is
+ treated like an argument to Stmt.bind(), so may be any type
+ supported by that function.
+
+ Throws on error (e.g. malformed SQL).
+ */
+ selectObject: function(sql,bind){
+ return __selectFirstRow(this, sql, bind, {});
+ },
+
+ /**
+ Runs the given SQL and returns an array of all results, with
+ each row represented as an array, as per the 'array' `rowMode`
+ option to `exec()`. An empty result set resolves
+ to an empty array. The second argument, if any, is treated as
+ the 'bind' option to a call to exec().
+ */
+ selectArrays: function(sql,bind){
+ return __selectAll(this, sql, bind, 'array');
+ },
+
+ /**
+ Works identically to selectArrays() except that each value
+ in the returned array is an object, as per the 'object' `rowMode`
+ option to `exec()`.
+ */
+ selectObjects: function(sql,bind){
+ return __selectAll(this, sql, bind, 'object');
+ },
+
+ /**
+ Returns the number of currently-opened Stmt handles for this db
+ handle, or 0 if this DB instance is closed. Note that only
+ handles prepared via this.prepare() are counted, and not
+ handles prepared using capi.sqlite3_prepare_v3() (or
+ equivalent).
+ */
+ openStatementCount: function(){
+ return this.pointer ? Object.keys(__stmtMap.get(this)).length : 0;
+ },
+
+ /**
+ Starts a transaction, calls the given callback, and then either
+ rolls back or commits the savepoint, depending on whether the
+ callback throws. The callback is passed this db object as its
+ only argument. On success, returns the result of the
+ callback. Throws on error.
+
+ Note that transactions may not be nested, so this will throw if
+ it is called recursively. For nested transactions, use the
+ savepoint() method or manually manage SAVEPOINTs using exec().
+
+ If called with 2 arguments, the first must be a keyword which
+ is legal immediately after a BEGIN statement, e.g. one of
+ "DEFERRED", "IMMEDIATE", or "EXCLUSIVE". Though the exact list
+ of supported keywords is not hard-coded here, in order to be
+ future-compatible, if the argument does not look like a single
+ keyword then an exception is triggered with a description of
+ the problem.
+ */
+ transaction: function(/* [beginQualifier,] */callback){
+ let opener = 'BEGIN';
+ if(arguments.length>1){
+ if(/[^a-zA-Z]/.test(arguments[0])){
+ toss3(capi.SQLITE_MISUSE, "Invalid argument for BEGIN qualifier.");
+ }
+ opener += ' '+arguments[0];
+ callback = arguments[1];
+ }
+ affirmDbOpen(this).exec(opener);
+ try {
+ const rc = callback(this);
+ this.exec("COMMIT");
+ return rc;
+ }catch(e){
+ this.exec("ROLLBACK");
+ throw e;
+ }
+ },
+
+ /**
+ This works similarly to transaction() but uses sqlite3's SAVEPOINT
+ feature. This function starts a savepoint (with an unspecified name)
+ and calls the given callback function, passing it this db object.
+ If the callback returns, the savepoint is released (committed). If
+ the callback throws, the savepoint is rolled back. If it does not
+ throw, it returns the result of the callback.
+ */
+ savepoint: function(callback){
+ affirmDbOpen(this).exec("SAVEPOINT oo1");
+ try {
+ const rc = callback(this);
+ this.exec("RELEASE oo1");
+ return rc;
+ }catch(e){
+ this.exec("ROLLBACK to SAVEPOINT oo1; RELEASE SAVEPOINT oo1");
+ throw e;
+ }
+ },
+
+ /**
+ A convenience form of DB.checkRc(this,resultCode). If it does
+ not throw, it returns this object.
+ */
+ checkRc: function(resultCode){
+ return checkSqlite3Rc(this, resultCode);
+ }
+ }/*DB.prototype*/;
+
+
+ /** Throws if the given Stmt has been finalized, else stmt is
+ returned. */
+ const affirmStmtOpen = function(stmt){
+ if(!stmt.pointer) toss3("Stmt has been closed.");
+ return stmt;
+ };
+
+ /** Returns an opaque truthy value from the BindTypes
+ enum if v's type is a valid bindable type, else
+ returns a falsy value. As a special case, a value of
+ undefined is treated as a bind type of null. */
+ const isSupportedBindType = function(v){
+ let t = BindTypes[(null===v||undefined===v) ? 'null' : typeof v];
+ switch(t){
+ case BindTypes.boolean:
+ case BindTypes.null:
+ case BindTypes.number:
+ case BindTypes.string:
+ return t;
+ case BindTypes.bigint:
+ if(wasm.bigIntEnabled) return t;
+ /* else fall through */
+ default:
+ return util.isBindableTypedArray(v) ? BindTypes.blob : undefined;
+ }
+ };
+
+ /**
+ If isSupportedBindType(v) returns a truthy value, this
+ function returns that value, else it throws.
+ */
+ const affirmSupportedBindType = function(v){
+ //sqlite3.config.log('affirmSupportedBindType',v);
+ return isSupportedBindType(v) || toss3("Unsupported bind() argument type:",typeof v);
+ };
+
+ /**
+ If key is a number and within range of stmt's bound parameter
+ count, key is returned.
+
+ If key is not a number then it is checked against named
+ parameters. If a match is found, its index is returned.
+
+ Else it throws.
+ */
+ const affirmParamIndex = function(stmt,key){
+ const n = ('number'===typeof key)
+ ? key : capi.sqlite3_bind_parameter_index(stmt.pointer, key);
+ if(0===n || !util.isInt32(n)){
+ toss3("Invalid bind() parameter name: "+key);
+ }
+ else if(n<1 || n>stmt.parameterCount) toss3("Bind index",key,"is out of range.");
+ return n;
+ };
+
+ /**
+ If stmt._lockedByExec is truthy, this throws an exception
+ complaining that the 2nd argument (an operation name,
+ e.g. "bind()") is not legal while the statement is "locked".
+ Locking happens before an exec()-like callback is passed a
+ statement, to ensure that the callback does not mutate or
+ finalize the statement. If it does not throw, it returns stmt.
+ */
+ const affirmNotLockedByExec = function(stmt,currentOpName){
+ if(stmt._lockedByExec){
+ toss3("Operation is illegal when statement is locked:",currentOpName);
+ }
+ return stmt;
+ };
+
+ /**
+ Binds a single bound parameter value on the given stmt at the
+ given index (numeric or named) using the given bindType (see
+ the BindTypes enum) and value. Throws on error. Returns stmt on
+ success.
+ */
+ const bindOne = function f(stmt,ndx,bindType,val){
+ affirmNotLockedByExec(affirmStmtOpen(stmt), 'bind()');
+ if(!f._){
+ f._tooBigInt = (v)=>toss3(
+ "BigInt value is too big to store without precision loss:", v
+ );
+ f._ = {
+ string: function(stmt, ndx, val, asBlob){
+ const [pStr, n] = wasm.allocCString(val, true);
+ const f = asBlob ? capi.sqlite3_bind_blob : capi.sqlite3_bind_text;
+ return f(stmt.pointer, ndx, pStr, n, capi.SQLITE_WASM_DEALLOC);
+ }
+ };
+ }/* static init */
+ affirmSupportedBindType(val);
+ ndx = affirmParamIndex(stmt,ndx);
+ let rc = 0;
+ switch((null===val || undefined===val) ? BindTypes.null : bindType){
+ case BindTypes.null:
+ rc = capi.sqlite3_bind_null(stmt.pointer, ndx);
+ break;
+ case BindTypes.string:
+ rc = f._.string(stmt, ndx, val, false);
+ break;
+ case BindTypes.number: {
+ let m;
+ if(util.isInt32(val)) m = capi.sqlite3_bind_int;
+ else if('bigint'===typeof val){
+ if(!util.bigIntFits64(val)){
+ f._tooBigInt(val);
+ }else if(wasm.bigIntEnabled){
+ m = capi.sqlite3_bind_int64;
+ }else if(util.bigIntFitsDouble(val)){
+ val = Number(val);
+ m = capi.sqlite3_bind_double;
+ }else{
+ f._tooBigInt(val);
+ }
+ }else{ // !int32, !bigint
+ val = Number(val);
+ if(wasm.bigIntEnabled && Number.isInteger(val)){
+ m = capi.sqlite3_bind_int64;
+ }else{
+ m = capi.sqlite3_bind_double;
+ }
+ }
+ rc = m(stmt.pointer, ndx, val);
+ break;
+ }
+ case BindTypes.boolean:
+ rc = capi.sqlite3_bind_int(stmt.pointer, ndx, val ? 1 : 0);
+ break;
+ case BindTypes.blob: {
+ if('string'===typeof val){
+ rc = f._.string(stmt, ndx, val, true);
+ break;
+ }else if(val instanceof ArrayBuffer){
+ val = new Uint8Array(val);
+ }else if(!util.isBindableTypedArray(val)){
+ toss3("Binding a value as a blob requires",
+ "that it be a string, Uint8Array, Int8Array, or ArrayBuffer.");
+ }
+ const pBlob = wasm.alloc(val.byteLength || 1);
+ wasm.heap8().set(val.byteLength ? val : [0], pBlob)
+ rc = capi.sqlite3_bind_blob(stmt.pointer, ndx, pBlob, val.byteLength,
+ capi.SQLITE_WASM_DEALLOC);
+ break;
+ }
+ default:
+ sqlite3.config.warn("Unsupported bind() argument type:",val);
+ toss3("Unsupported bind() argument type: "+(typeof val));
+ }
+ if(rc) DB.checkRc(stmt.db.pointer, rc);
+ stmt._mayGet = false;
+ return stmt;
+ };
+
+ Stmt.prototype = {
+ /**
+ "Finalizes" this statement. This is a no-op if the statement
+ has already been finalized. Returns the result of
+ sqlite3_finalize() (0 on success, non-0 on error), or the
+ undefined value if the statement has already been
+ finalized. Regardless of success or failure, most methods in
+ this class will throw if called after this is.
+
+ This method always throws if called when it is illegal to do
+ so. Namely, when triggered via a per-row callback handler of a
+ DB.exec() call.
+ */
+ finalize: function(){
+ if(this.pointer){
+ affirmNotLockedByExec(this,'finalize()');
+ const rc = capi.sqlite3_finalize(this.pointer);
+ delete __stmtMap.get(this.db)[this.pointer];
+ __ptrMap.delete(this);
+ delete this._mayGet;
+ delete this.parameterCount;
+ delete this._lockedByExec;
+ delete this.db;
+ return rc;
+ }
+ },
+ /**
+ Clears all bound values. Returns this object. Throws if this
+ statement has been finalized or if modification of the
+ statement is currently illegal (e.g. in the per-row callback of
+ a DB.exec() call).
+ */
+ clearBindings: function(){
+ affirmNotLockedByExec(affirmStmtOpen(this), 'clearBindings()')
+ capi.sqlite3_clear_bindings(this.pointer);
+ this._mayGet = false;
+ return this;
+ },
+ /**
+ Resets this statement so that it may be step()ed again from the
+ beginning. Returns this object. Throws if this statement has
+ been finalized, if it may not legally be reset because it is
+ currently being used from a DB.exec() callback, or if the
+ underlying call to sqlite3_reset() returns non-0.
+
+ If passed a truthy argument then this.clearBindings() is
+ also called, otherwise any existing bindings, along with
+ any memory allocated for them, are retained.
+
+ In versions 3.42.0 and earlier, this function did not throw if
+ sqlite3_reset() returns non-0, but it was discovered that
+ throwing (or significant extra client-side code) is necessary
+ in order to avoid certain silent failure scenarios, as
+ discussed at:
+
+ https://sqlite.org/forum/forumpost/36f7a2e7494897df
+ */
+ reset: function(alsoClearBinds){
+ affirmNotLockedByExec(this,'reset()');
+ if(alsoClearBinds) this.clearBindings();
+ const rc = capi.sqlite3_reset(affirmStmtOpen(this).pointer);
+ this._mayGet = false;
+ checkSqlite3Rc(this.db, rc);
+ return this;
+ },
+ /**
+ Binds one or more values to its bindable parameters. It
+ accepts 1 or 2 arguments:
+
+ If passed a single argument, it must be either an array, an
+ object, or a value of a bindable type (see below).
+
+ If passed 2 arguments, the first one is the 1-based bind
+ index or bindable parameter name and the second one must be
+ a value of a bindable type.
+
+ Bindable value types:
+
+ - null is bound as NULL.
+
+ - undefined as a standalone value is a no-op intended to
+ simplify certain client-side use cases: passing undefined as
+ a value to this function will not actually bind anything and
+ this function will skip confirmation that binding is even
+ legal. (Those semantics simplify certain client-side uses.)
+ Conversely, a value of undefined as an array or object
+ property when binding an array/object (see below) is treated
+ the same as null.
+
+ - Numbers are bound as either doubles or integers: doubles if
+ they are larger than 32 bits, else double or int32, depending
+ on whether they have a fractional part. Booleans are bound as
+ integer 0 or 1. It is not expected the distinction of binding
+ doubles which have no fractional parts is integers is
+ significant for the majority of clients due to sqlite3's data
+ typing model. If [BigInt] support is enabled then this
+ routine will bind BigInt values as 64-bit integers if they'll
+ fit in 64 bits. If that support disabled, it will store the
+ BigInt as an int32 or a double if it can do so without loss
+ of precision. If the BigInt is _too BigInt_ then it will
+ throw.
+
+ - Strings are bound as strings (use bindAsBlob() to force
+ blob binding).
+
+ - Uint8Array, Int8Array, and ArrayBuffer instances are bound as
+ blobs.
+
+ If passed an array, each element of the array is bound at
+ the parameter index equal to the array index plus 1
+ (because arrays are 0-based but binding is 1-based).
+
+ If passed an object, each object key is treated as a
+ bindable parameter name. The object keys _must_ match any
+ bindable parameter names, including any `$`, `@`, or `:`
+ prefix. Because `$` is a legal identifier chararacter in
+ JavaScript, that is the suggested prefix for bindable
+ parameters: `stmt.bind({$a: 1, $b: 2})`.
+
+ It returns this object on success and throws on
+ error. Errors include:
+
+ - Any bind index is out of range, a named bind parameter
+ does not match, or this statement has no bindable
+ parameters.
+
+ - Any value to bind is of an unsupported type.
+
+ - Passed no arguments or more than two.
+
+ - The statement has been finalized.
+ */
+ bind: function(/*[ndx,] arg*/){
+ affirmStmtOpen(this);
+ let ndx, arg;
+ switch(arguments.length){
+ case 1: ndx = 1; arg = arguments[0]; break;
+ case 2: ndx = arguments[0]; arg = arguments[1]; break;
+ default: toss3("Invalid bind() arguments.");
+ }
+ if(undefined===arg){
+ /* It might seem intuitive to bind undefined as NULL
+ but this approach simplifies certain client-side
+ uses when passing on arguments between 2+ levels of
+ functions. */
+ return this;
+ }else if(!this.parameterCount){
+ toss3("This statement has no bindable parameters.");
+ }
+ this._mayGet = false;
+ if(null===arg){
+ /* bind NULL */
+ return bindOne(this, ndx, BindTypes.null, arg);
+ }
+ else if(Array.isArray(arg)){
+ /* bind each entry by index */
+ if(1!==arguments.length){
+ toss3("When binding an array, an index argument is not permitted.");
+ }
+ arg.forEach((v,i)=>bindOne(this, i+1, affirmSupportedBindType(v), v));
+ return this;
+ }else if(arg instanceof ArrayBuffer){
+ arg = new Uint8Array(arg);
+ }
+ if('object'===typeof arg/*null was checked above*/
+ && !util.isBindableTypedArray(arg)){
+ /* Treat each property of arg as a named bound parameter. */
+ if(1!==arguments.length){
+ toss3("When binding an object, an index argument is not permitted.");
+ }
+ Object.keys(arg)
+ .forEach(k=>bindOne(this, k,
+ affirmSupportedBindType(arg[k]),
+ arg[k]));
+ return this;
+ }else{
+ return bindOne(this, ndx, affirmSupportedBindType(arg), arg);
+ }
+ toss3("Should not reach this point.");
+ },
+ /**
+ Special case of bind() which binds the given value using the
+ BLOB binding mechanism instead of the default selected one for
+ the value. The ndx may be a numbered or named bind index. The
+ value must be of type string, null/undefined (both get treated
+ as null), or a TypedArray of a type supported by the bind()
+ API. This API cannot bind numbers as blobs.
+
+ If passed a single argument, a bind index of 1 is assumed and
+ the first argument is the value.
+ */
+ bindAsBlob: function(ndx,arg){
+ affirmStmtOpen(this);
+ if(1===arguments.length){
+ arg = ndx;
+ ndx = 1;
+ }
+ const t = affirmSupportedBindType(arg);
+ if(BindTypes.string !== t && BindTypes.blob !== t
+ && BindTypes.null !== t){
+ toss3("Invalid value type for bindAsBlob()");
+ }
+ return bindOne(this, ndx, BindTypes.blob, arg);
+ },
+ /**
+ Steps the statement one time. If the result indicates that a
+ row of data is available, a truthy value is returned.
+ If no row of data is available, a falsy
+ value is returned. Throws on error.
+ */
+ step: function(){
+ affirmNotLockedByExec(this, 'step()');
+ const rc = capi.sqlite3_step(affirmStmtOpen(this).pointer);
+ switch(rc){
+ case capi.SQLITE_DONE: return this._mayGet = false;
+ case capi.SQLITE_ROW: return this._mayGet = true;
+ default:
+ this._mayGet = false;
+ sqlite3.config.warn("sqlite3_step() rc=",rc,
+ capi.sqlite3_js_rc_str(rc),
+ "SQL =", capi.sqlite3_sql(this.pointer));
+ DB.checkRc(this.db.pointer, rc);
+ }
+ },
+ /**
+ Functions exactly like step() except that...
+
+ 1) On success, it calls this.reset() and returns this object.
+ 2) On error, it throws and does not call reset().
+
+ This is intended to simplify constructs like:
+
+ ```
+ for(...) {
+ stmt.bind(...).stepReset();
+ }
+ ```
+
+ Note that the reset() call makes it illegal to call this.get()
+ after the step.
+ */
+ stepReset: function(){
+ this.step();
+ return this.reset();
+ },
+ /**
+ Functions like step() except that it calls finalize() on this
+ statement immediately after stepping, even if the step() call
+ throws.
+
+ On success, it returns true if the step indicated that a row of
+ data was available, else it returns false.
+
+ This is intended to simplify use cases such as:
+
+ ```
+ aDb.prepare("insert into foo(a) values(?)").bind(123).stepFinalize();
+ ```
+ */
+ stepFinalize: function(){
+ try{
+ const rc = this.step();
+ this.reset(/*for INSERT...RETURNING locking case*/);
+ return rc;
+ }finally{
+ try{this.finalize()}
+ catch(e){/*ignored*/}
+ }
+ },
+ /**
+ Fetches the value from the given 0-based column index of
+ the current data row, throwing if index is out of range.
+
+ Requires that step() has just returned a truthy value, else
+ an exception is thrown.
+
+ By default it will determine the data type of the result
+ automatically. If passed a second arugment, it must be one
+ of the enumeration values for sqlite3 types, which are
+ defined as members of the sqlite3 module: SQLITE_INTEGER,
+ SQLITE_FLOAT, SQLITE_TEXT, SQLITE_BLOB. Any other value,
+ except for undefined, will trigger an exception. Passing
+ undefined is the same as not passing a value. It is legal
+ to, e.g., fetch an integer value as a string, in which case
+ sqlite3 will convert the value to a string.
+
+ If ndx is an array, this function behaves a differently: it
+ assigns the indexes of the array, from 0 to the number of
+ result columns, to the values of the corresponding column,
+ and returns that array.
+
+ If ndx is a plain object, this function behaves even
+ differentlier: it assigns the properties of the object to
+ the values of their corresponding result columns.
+
+ Blobs are returned as Uint8Array instances.
+
+ Potential TODO: add type ID SQLITE_JSON, which fetches the
+ result as a string and passes it (if it's not null) to
+ JSON.parse(), returning the result of that. Until then,
+ getJSON() can be used for that.
+ */
+ get: function(ndx,asType){
+ if(!affirmStmtOpen(this)._mayGet){
+ toss3("Stmt.step() has not (recently) returned true.");
+ }
+ if(Array.isArray(ndx)){
+ let i = 0;
+ const n = this.columnCount;
+ while(i<n){
+ ndx[i] = this.get(i++);
+ }
+ return ndx;
+ }else if(ndx && 'object'===typeof ndx){
+ let i = 0;
+ const n = this.columnCount;
+ while(i<n){
+ ndx[capi.sqlite3_column_name(this.pointer,i)] = this.get(i++);
+ }
+ return ndx;
+ }
+ affirmColIndex(this, ndx);
+ switch(undefined===asType
+ ? capi.sqlite3_column_type(this.pointer, ndx)
+ : asType){
+ case capi.SQLITE_NULL: return null;
+ case capi.SQLITE_INTEGER:{
+ if(wasm.bigIntEnabled){
+ const rc = capi.sqlite3_column_int64(this.pointer, ndx);
+ if(rc>=Number.MIN_SAFE_INTEGER && rc<=Number.MAX_SAFE_INTEGER){
+ /* Coerce "normal" number ranges to normal number values,
+ and only return BigInt-type values for numbers out of this
+ range. */
+ return Number(rc).valueOf();
+ }
+ return rc;
+ }else{
+ const rc = capi.sqlite3_column_double(this.pointer, ndx);
+ if(rc>Number.MAX_SAFE_INTEGER || rc<Number.MIN_SAFE_INTEGER){
+ /* Throwing here is arguable but, since we're explicitly
+ extracting an SQLITE_INTEGER-type value, it seems fair to throw
+ if the extracted number is out of range for that type.
+ This policy may be laxened to simply pass on the number and
+ hope for the best, as the C API would do. */
+ toss3("Integer is out of range for JS integer range: "+rc);
+ }
+ //sqlite3.config.log("get integer rc=",rc,isInt32(rc));
+ return util.isInt32(rc) ? (rc | 0) : rc;
+ }
+ }
+ case capi.SQLITE_FLOAT:
+ return capi.sqlite3_column_double(this.pointer, ndx);
+ case capi.SQLITE_TEXT:
+ return capi.sqlite3_column_text(this.pointer, ndx);
+ case capi.SQLITE_BLOB: {
+ const n = capi.sqlite3_column_bytes(this.pointer, ndx),
+ ptr = capi.sqlite3_column_blob(this.pointer, ndx),
+ rc = new Uint8Array(n);
+ //heap = n ? wasm.heap8() : false;
+ if(n) rc.set(wasm.heap8u().slice(ptr, ptr+n), 0);
+ //for(let i = 0; i < n; ++i) rc[i] = heap[ptr + i];
+ if(n && this.db._blobXfer instanceof Array){
+ /* This is an optimization soley for the
+ Worker-based API. These values will be
+ transfered to the main thread directly
+ instead of being copied. */
+ this.db._blobXfer.push(rc.buffer);
+ }
+ return rc;
+ }
+ default: toss3("Don't know how to translate",
+ "type of result column #"+ndx+".");
+ }
+ toss3("Not reached.");
+ },
+ /** Equivalent to get(ndx) but coerces the result to an
+ integer. */
+ getInt: function(ndx){return this.get(ndx,capi.SQLITE_INTEGER)},
+ /** Equivalent to get(ndx) but coerces the result to a
+ float. */
+ getFloat: function(ndx){return this.get(ndx,capi.SQLITE_FLOAT)},
+ /** Equivalent to get(ndx) but coerces the result to a
+ string. */
+ getString: function(ndx){return this.get(ndx,capi.SQLITE_TEXT)},
+ /** Equivalent to get(ndx) but coerces the result to a
+ Uint8Array. */
+ getBlob: function(ndx){return this.get(ndx,capi.SQLITE_BLOB)},
+ /**
+ A convenience wrapper around get() which fetches the value
+ as a string and then, if it is not null, passes it to
+ JSON.parse(), returning that result. Throws if parsing
+ fails. If the result is null, null is returned. An empty
+ string, on the other hand, will trigger an exception.
+ */
+ getJSON: function(ndx){
+ const s = this.get(ndx, capi.SQLITE_STRING);
+ return null===s ? s : JSON.parse(s);
+ },
+ // Design note: the only reason most of these getters have a 'get'
+ // prefix is for consistency with getVALUE_TYPE(). The latter
+ // arguably really need that prefix for API readability and the
+ // rest arguably don't, but consistency is a powerful thing.
+ /**
+ Returns the result column name of the given index, or
+ throws if index is out of bounds or this statement has been
+ finalized. This can be used without having run step()
+ first.
+ */
+ getColumnName: function(ndx){
+ return capi.sqlite3_column_name(
+ affirmColIndex(affirmStmtOpen(this),ndx).pointer, ndx
+ );
+ },
+ /**
+ If this statement potentially has result columns, this function
+ returns an array of all such names. If passed an array, it is
+ used as the target and all names are appended to it. Returns
+ the target array. Throws if this statement cannot have result
+ columns. This object's columnCount property holds the number of
+ columns.
+ */
+ getColumnNames: function(tgt=[]){
+ affirmColIndex(affirmStmtOpen(this),0);
+ const n = this.columnCount;
+ for(let i = 0; i < n; ++i){
+ tgt.push(capi.sqlite3_column_name(this.pointer, i));
+ }
+ return tgt;
+ },
+ /**
+ If this statement has named bindable parameters and the
+ given name matches one, its 1-based bind index is
+ returned. If no match is found, 0 is returned. If it has no
+ bindable parameters, the undefined value is returned.
+ */
+ getParamIndex: function(name){
+ return (affirmStmtOpen(this).parameterCount
+ ? capi.sqlite3_bind_parameter_index(this.pointer, name)
+ : undefined);
+ }
+ }/*Stmt.prototype*/;
+
+ {/* Add the `pointer` property to DB and Stmt. */
+ const prop = {
+ enumerable: true,
+ get: function(){return __ptrMap.get(this)},
+ set: ()=>toss3("The pointer property is read-only.")
+ }
+ Object.defineProperty(Stmt.prototype, 'pointer', prop);
+ Object.defineProperty(DB.prototype, 'pointer', prop);
+ }
+ /**
+ Stmt.columnCount is an interceptor for sqlite3_column_count().
+
+ This requires an unfortunate performance hit compared to caching
+ columnCount when the Stmt is created/prepared (as was done in
+ SQLite <=3.42.0), but is necessary in order to handle certain
+ corner cases, as described in
+ https://sqlite.org/forum/forumpost/7774b773937cbe0a.
+ */
+ Object.defineProperty(Stmt.prototype, 'columnCount', {
+ enumerable: false,
+ get: function(){return capi.sqlite3_column_count(this.pointer)},
+ set: ()=>toss3("The columnCount property is read-only.")
+ });
+
+ /** The OO API's public namespace. */
+ sqlite3.oo1 = {
+ DB,
+ Stmt
+ }/*oo1 object*/;
+
+ if(util.isUIThread()){
+ /**
+ Functionally equivalent to DB(storageName,'c','kvvfs') except
+ that it throws if the given storage name is not one of 'local'
+ or 'session'.
+ */
+ sqlite3.oo1.JsStorageDb = function(storageName='session'){
+ if('session'!==storageName && 'local'!==storageName){
+ toss3("JsStorageDb db name must be one of 'session' or 'local'.");
+ }
+ dbCtorHelper.call(this, {
+ filename: storageName,
+ flags: 'c',
+ vfs: "kvvfs"
+ });
+ };
+ const jdb = sqlite3.oo1.JsStorageDb;
+ jdb.prototype = Object.create(DB.prototype);
+ /** Equivalent to sqlite3_js_kvvfs_clear(). */
+ jdb.clearStorage = capi.sqlite3_js_kvvfs_clear;
+ /**
+ Clears this database instance's storage or throws if this
+ instance has been closed. Returns the number of
+ database blocks which were cleaned up.
+ */
+ jdb.prototype.clearStorage = function(){
+ return jdb.clearStorage(affirmDbOpen(this).filename);
+ };
+ /** Equivalent to sqlite3_js_kvvfs_size(). */
+ jdb.storageSize = capi.sqlite3_js_kvvfs_size;
+ /**
+ Returns the _approximate_ number of bytes this database takes
+ up in its storage or throws if this instance has been closed.
+ */
+ jdb.prototype.storageSize = function(){
+ return jdb.storageSize(affirmDbOpen(this).filename);
+ };
+ }/*main-window-only bits*/
+
+});
+//#else
+/* Built with the omit-oo1 flag. */
+//#endif ifnot omit-oo1
diff --git a/ext/wasm/api/sqlite3-api-prologue.js b/ext/wasm/api/sqlite3-api-prologue.js
new file mode 100644
index 0000000..ef1154f
--- /dev/null
+++ b/ext/wasm/api/sqlite3-api-prologue.js
@@ -0,0 +1,2154 @@
+/*
+ 2022-05-22
+
+ 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 is intended to be combined at build-time with other
+ related code, most notably a header and footer which wraps this
+ whole file into an Emscripten Module.postRun() handler. The sqlite3
+ JS API has no hard requirements on Emscripten and does not expose
+ any Emscripten APIs to clients. It is structured such that its build
+ can be tweaked to include it in arbitrary WASM environments which
+ can supply the necessary underlying features (e.g. a POSIX file I/O
+ layer).
+
+ Main project home page: https://sqlite.org
+
+ Documentation home page: https://sqlite.org/wasm
+*/
+
+/**
+ sqlite3ApiBootstrap() is the only global symbol persistently
+ exposed by this API. It is intended to be called one time at the
+ end of the API amalgamation process, passed configuration details
+ for the current environment, and then optionally be removed from
+ the global object using `delete globalThis.sqlite3ApiBootstrap`.
+
+ This function is not intended for client-level use. It is intended
+ for use in creating bundles configured for specific WASM
+ environments.
+
+ This function expects a configuration object, intended to abstract
+ away details specific to any given WASM environment, primarily so
+ that it can be used without any _direct_ dependency on
+ Emscripten. (Note the default values for the config object!) The
+ config object is only honored the first time this is
+ called. Subsequent calls ignore the argument and return the same
+ (configured) object which gets initialized by the first call. This
+ function will throw if any of the required config options are
+ missing.
+
+ The config object properties include:
+
+ - `exports`[^1]: the "exports" object for the current WASM
+ environment. In an Emscripten-based build, this should be set to
+ `Module['asm']`.
+
+ - `memory`[^1]: optional WebAssembly.Memory object, defaulting to
+ `exports.memory`. In Emscripten environments this should be set
+ to `Module.wasmMemory` if the build uses `-sIMPORTED_MEMORY`, or be
+ left undefined/falsy to default to `exports.memory` when using
+ WASM-exported memory.
+
+ - `bigIntEnabled`: true if BigInt support is enabled. Defaults to
+ true if `globalThis.BigInt64Array` is available, else false. Some APIs
+ will throw exceptions if called without BigInt support, as BigInt
+ is required for marshalling C-side int64 into and out of JS.
+ (Sidebar: it is technically possible to add int64 support via
+ marshalling of int32 pairs, but doing so is unduly invasive.)
+
+ - `allocExportName`: the name of the function, in `exports`, of the
+ `malloc(3)`-compatible routine for the WASM environment. Defaults
+ to `"sqlite3_malloc"`. Beware that using any allocator other than
+ sqlite3_malloc() may require care in certain client-side code
+ regarding which allocator is uses. Notably, sqlite3_deserialize()
+ and sqlite3_serialize() can only safely use memory from different
+ allocators under very specific conditions. The canonical builds
+ of this API guaranty that `sqlite3_malloc()` is the JS-side
+ allocator implementation.
+
+ - `deallocExportName`: the name of the function, in `exports`, of
+ the `free(3)`-compatible routine for the WASM
+ environment. Defaults to `"sqlite3_free"`.
+
+ - `reallocExportName`: the name of the function, in `exports`, of
+ the `realloc(3)`-compatible routine for the WASM
+ environment. Defaults to `"sqlite3_realloc"`.
+
+ - `debug`, `log`, `warn`, and `error` may be functions equivalent
+ to the like-named methods of the global `console` object. By
+ default, these map directly to their `console` counterparts, but
+ can be replaced with (e.g.) empty functions to squelch all such
+ output.
+
+ - `wasmfsOpfsDir`[^1]: Specifies the "mount point" of the OPFS-backed
+ filesystem in WASMFS-capable builds.
+
+
+ [^1] = This property may optionally be a function, in which case
+ this function calls that function to fetch the value,
+ enabling delayed evaluation.
+
+ The returned object is the top-level sqlite3 namespace object.
+
+*/
+'use strict';
+globalThis.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
+ apiConfig = (globalThis.sqlite3ApiConfig || sqlite3ApiBootstrap.defaultConfig)
+){
+ if(sqlite3ApiBootstrap.sqlite3){ /* already initalized */
+ console.warn("sqlite3ApiBootstrap() called multiple times.",
+ "Config and external initializers are ignored on calls after the first.");
+ return sqlite3ApiBootstrap.sqlite3;
+ }
+ const config = Object.assign(Object.create(null),{
+ exports: undefined,
+ memory: undefined,
+ bigIntEnabled: (()=>{
+ if('undefined'!==typeof Module){
+ /* Emscripten module will contain HEAPU64 when built with
+ -sWASM_BIGINT=1, else it will not. */
+ return !!Module.HEAPU64;
+ }
+ return !!globalThis.BigInt64Array;
+ })(),
+ debug: console.debug.bind(console),
+ warn: console.warn.bind(console),
+ error: console.error.bind(console),
+ log: console.log.bind(console),
+ wasmfsOpfsDir: '/opfs',
+ /**
+ useStdAlloc is just for testing allocator discrepancies. The
+ docs guarantee that this is false in the canonical builds. For
+ 99% of purposes it doesn't matter which allocators we use, but
+ it becomes significant with, e.g., sqlite3_deserialize() and
+ certain wasm.xWrap.resultAdapter()s.
+ */
+ useStdAlloc: false
+ }, apiConfig || {});
+
+ Object.assign(config, {
+ allocExportName: config.useStdAlloc ? 'malloc' : 'sqlite3_malloc',
+ deallocExportName: config.useStdAlloc ? 'free' : 'sqlite3_free',
+ reallocExportName: config.useStdAlloc ? 'realloc' : 'sqlite3_realloc'
+ }, config);
+
+ [
+ // If any of these config options are functions, replace them with
+ // the result of calling that function...
+ 'exports', 'memory', 'wasmfsOpfsDir'
+ ].forEach((k)=>{
+ if('function' === typeof config[k]){
+ config[k] = config[k]();
+ }
+ });
+ /**
+ The main sqlite3 binding API gets installed into this object,
+ mimicking the C API as closely as we can. The numerous members
+ names with prefixes 'sqlite3_' and 'SQLITE_' behave, insofar as
+ possible, identically to the C-native counterparts, as documented at:
+
+ https://www.sqlite.org/c3ref/intro.html
+
+ A very few exceptions require an additional level of proxy
+ function or may otherwise require special attention in the WASM
+ environment, and all such cases are documented somewhere below
+ in this file or in sqlite3-api-glue.js. capi members which are
+ not documented are installed as 1-to-1 proxies for their
+ C-side counterparts.
+ */
+ const capi = Object.create(null);
+ /**
+ Holds state which are specific to the WASM-related
+ infrastructure and glue code.
+
+ Note that a number of members of this object are injected
+ dynamically after the api object is fully constructed, so
+ not all are documented in this file.
+ */
+ const wasm = Object.create(null);
+
+ /** Internal helper for SQLite3Error ctor. */
+ const __rcStr = (rc)=>{
+ return (capi.sqlite3_js_rc_str && capi.sqlite3_js_rc_str(rc))
+ || ("Unknown result code #"+rc);
+ };
+
+ /** Internal helper for SQLite3Error ctor. */
+ const __isInt = (n)=>'number'===typeof n && n===(n | 0);
+
+ /**
+ An Error subclass specifically for reporting DB-level errors and
+ enabling clients to unambiguously identify such exceptions.
+ The C-level APIs never throw, but some of the higher-level
+ C-style APIs do and the object-oriented APIs use exceptions
+ exclusively to report errors.
+ */
+ class SQLite3Error extends Error {
+ /**
+ Constructs this object with a message depending on its arguments:
+
+ If its first argument is an integer, it is assumed to be
+ an SQLITE_... result code and it is passed to
+ sqlite3.capi.sqlite3_js_rc_str() to stringify it.
+
+ If called with exactly 2 arguments and the 2nd is an object,
+ that object is treated as the 2nd argument to the parent
+ constructor.
+
+ The exception's message is created by concatenating its
+ arguments with a space between each, except for the
+ two-args-with-an-objec form and that the first argument will
+ get coerced to a string, as described above, if it's an
+ integer.
+
+ If passed an integer first argument, the error object's
+ `resultCode` member will be set to the given integer value,
+ else it will be set to capi.SQLITE_ERROR.
+ */
+ constructor(...args){
+ let rc;
+ if(args.length){
+ if(__isInt(args[0])){
+ rc = args[0];
+ if(1===args.length){
+ super(__rcStr(args[0]));
+ }else{
+ const rcStr = __rcStr(rc);
+ if('object'===typeof args[1]){
+ super(rcStr,args[1]);
+ }else{
+ args[0] = rcStr+':';
+ super(args.join(' '));
+ }
+ }
+ }else{
+ if(2===args.length && 'object'===typeof args[1]){
+ super(...args);
+ }else{
+ super(args.join(' '));
+ }
+ }
+ }
+ this.resultCode = rc || capi.SQLITE_ERROR;
+ this.name = 'SQLite3Error';
+ }
+ };
+
+ /**
+ Functionally equivalent to the SQLite3Error constructor but may
+ be used as part of an expression, e.g.:
+
+ ```
+ return someFunction(x) || SQLite3Error.toss(...);
+ ```
+ */
+ SQLite3Error.toss = (...args)=>{
+ throw new SQLite3Error(...args);
+ };
+ const toss3 = SQLite3Error.toss;
+
+ if(config.wasmfsOpfsDir && !/^\/[^/]+$/.test(config.wasmfsOpfsDir)){
+ toss3("config.wasmfsOpfsDir must be falsy or in the form '/dir-name'.");
+ }
+
+ /**
+ Returns true if n is a 32-bit (signed) integer, else
+ false. This is used for determining when we need to switch to
+ double-type DB operations for integer values in order to keep
+ more precision.
+ */
+ const isInt32 = (n)=>{
+ return ('bigint'!==typeof n /*TypeError: can't convert BigInt to number*/)
+ && !!(n===(n|0) && n<=2147483647 && n>=-2147483648);
+ };
+ /**
+ Returns true if the given BigInt value is small enough to fit
+ into an int64 value, else false.
+ */
+ const bigIntFits64 = function f(b){
+ if(!f._max){
+ f._max = BigInt("0x7fffffffffffffff");
+ f._min = ~f._max;
+ }
+ return b >= f._min && b <= f._max;
+ };
+
+ /**
+ Returns true if the given BigInt value is small enough to fit
+ into an int32, else false.
+ */
+ const bigIntFits32 = (b)=>(b >= (-0x7fffffffn - 1n) && b <= 0x7fffffffn);
+
+ /**
+ Returns true if the given BigInt value is small enough to fit
+ into a double value without loss of precision, else false.
+ */
+ const bigIntFitsDouble = function f(b){
+ if(!f._min){
+ f._min = Number.MIN_SAFE_INTEGER;
+ f._max = Number.MAX_SAFE_INTEGER;
+ }
+ return b >= f._min && b <= f._max;
+ };
+
+ /** Returns v if v appears to be a TypedArray, else false. */
+ const isTypedArray = (v)=>{
+ return (v && v.constructor && isInt32(v.constructor.BYTES_PER_ELEMENT)) ? v : false;
+ };
+
+
+ /** Internal helper to use in operations which need to distinguish
+ between TypedArrays which are backed by a SharedArrayBuffer
+ from those which are not. */
+ const __SAB = ('undefined'===typeof SharedArrayBuffer)
+ ? function(){} : SharedArrayBuffer;
+ /** Returns true if the given TypedArray object is backed by a
+ SharedArrayBuffer, else false. */
+ const isSharedTypedArray = (aTypedArray)=>(aTypedArray.buffer instanceof __SAB);
+
+ /**
+ Returns either aTypedArray.slice(begin,end) (if
+ aTypedArray.buffer is a SharedArrayBuffer) or
+ aTypedArray.subarray(begin,end) (if it's not).
+
+ This distinction is important for APIs which don't like to
+ work on SABs, e.g. TextDecoder, and possibly for our
+ own APIs which work on memory ranges which "might" be
+ modified by other threads while they're working.
+ */
+ const typedArrayPart = (aTypedArray, begin, end)=>{
+ return isSharedTypedArray(aTypedArray)
+ ? aTypedArray.slice(begin, end)
+ : aTypedArray.subarray(begin, end);
+ };
+
+ /**
+ Returns true if v appears to be one of our bind()-able TypedArray
+ types: Uint8Array or Int8Array or ArrayBuffer. Support for
+ TypedArrays with element sizes >1 is a potential TODO just
+ waiting on a use case to justify them. Until then, their `buffer`
+ property can be used to pass them as an ArrayBuffer. If it's not
+ a bindable array type, a falsy value is returned.
+ */
+ const isBindableTypedArray = (v)=>{
+ return v && (v instanceof Uint8Array
+ || v instanceof Int8Array
+ || v instanceof ArrayBuffer);
+ };
+
+ /**
+ Returns true if v appears to be one of the TypedArray types
+ which is legal for holding SQL code (as opposed to binary blobs).
+
+ Currently this is the same as isBindableTypedArray() but it
+ seems likely that we'll eventually want to add Uint32Array
+ and friends to the isBindableTypedArray() list but not to the
+ isSQLableTypedArray() list.
+ */
+ const isSQLableTypedArray = (v)=>{
+ return v && (v instanceof Uint8Array
+ || v instanceof Int8Array
+ || v instanceof ArrayBuffer);
+ };
+
+ /** Returns true if isBindableTypedArray(v) does, else throws with a message
+ that v is not a supported TypedArray value. */
+ const affirmBindableTypedArray = (v)=>{
+ return isBindableTypedArray(v)
+ || toss3("Value is not of a supported TypedArray type.");
+ };
+
+ const utf8Decoder = new TextDecoder('utf-8');
+
+ /**
+ Uses TextDecoder to decode the given half-open range of the
+ given TypedArray to a string. This differs from a simple
+ call to TextDecoder in that it accounts for whether the
+ first argument is backed by a SharedArrayBuffer or not,
+ and can work more efficiently if it's not (TextDecoder
+ refuses to act upon an SAB).
+ */
+ const typedArrayToString = function(typedArray, begin, end){
+ return utf8Decoder.decode(typedArrayPart(typedArray, begin,end));
+ };
+
+ /**
+ If v is-a Array, its join("") result is returned. If
+ isSQLableTypedArray(v) is true then typedArrayToString(v) is
+ returned. If it looks like a WASM pointer, wasm.cstrToJs(v) is
+ returned. Else v is returned as-is.
+ */
+ const flexibleString = function(v){
+ if(isSQLableTypedArray(v)){
+ return typedArrayToString(
+ (v instanceof ArrayBuffer) ? new Uint8Array(v) : v
+ );
+ }
+ else if(Array.isArray(v)) return v.join("");
+ else if(wasm.isPtr(v)) v = wasm.cstrToJs(v);
+ return v;
+ };
+
+ /**
+ An Error subclass specifically for reporting Wasm-level malloc()
+ failure and enabling clients to unambiguously identify such
+ exceptions.
+ */
+ class WasmAllocError extends Error {
+ /**
+ If called with 2 arguments and the 2nd one is an object, it
+ behaves like the Error constructor, else it concatenates all
+ arguments together with a single space between each to
+ construct an error message string. As a special case, if
+ called with no arguments then it uses a default error
+ message.
+ */
+ constructor(...args){
+ if(2===args.length && 'object'===typeof args[1]){
+ super(...args);
+ }else if(args.length){
+ super(args.join(' '));
+ }else{
+ super("Allocation failed.");
+ }
+ this.resultCode = capi.SQLITE_NOMEM;
+ this.name = 'WasmAllocError';
+ }
+ };
+ /**
+ Functionally equivalent to the WasmAllocError constructor but may
+ be used as part of an expression, e.g.:
+
+ ```
+ return someAllocatingFunction(x) || WasmAllocError.toss(...);
+ ```
+ */
+ WasmAllocError.toss = (...args)=>{
+ throw new WasmAllocError(...args);
+ };
+
+ Object.assign(capi, {
+ /**
+ sqlite3_bind_blob() works exactly like its C counterpart unless
+ its 3rd argument is one of:
+
+ - JS string: the 3rd argument is converted to a C string, the
+ 4th argument is ignored, and the C-string's length is used
+ in its place.
+
+ - Array: converted to a string as defined for "flexible
+ strings" and then it's treated as a JS string.
+
+ - Int8Array or Uint8Array: wasm.allocFromTypedArray() is used to
+ conver the memory to the WASM heap. If the 4th argument is
+ 0 or greater, it is used as-is, otherwise the array's byteLength
+ value is used. This is an exception to the C API's undefined
+ behavior for a negative 4th argument, but results are undefined
+ if the given 4th argument value is greater than the byteLength
+ of the input array.
+
+ - If it's an ArrayBuffer, it gets wrapped in a Uint8Array and
+ treated as that type.
+
+ In all of those cases, the final argument (destructor) is
+ ignored and capi.SQLITE_WASM_DEALLOC is assumed.
+
+ A 3rd argument of `null` is treated as if it were a WASM pointer
+ of 0.
+
+ If the 3rd argument is neither a WASM pointer nor one of the
+ above-described types, capi.SQLITE_MISUSE is returned.
+
+ The first argument may be either an `sqlite3_stmt*` WASM
+ pointer or an sqlite3.oo1.Stmt instance.
+
+ For consistency with the C API, it requires the same number of
+ arguments. It returns capi.SQLITE_MISUSE if passed any other
+ argument count.
+ */
+ sqlite3_bind_blob: undefined/*installed later*/,
+
+ /**
+ sqlite3_bind_text() works exactly like its C counterpart unless
+ its 3rd argument is one of:
+
+ - JS string: the 3rd argument is converted to a C string, the
+ 4th argument is ignored, and the C-string's length is used
+ in its place.
+
+ - Array: converted to a string as defined for "flexible
+ strings". The 4th argument is ignored and a value of -1
+ is assumed.
+
+ - Int8Array or Uint8Array: is assumed to contain UTF-8 text, is
+ converted to a string. The 4th argument is ignored, replaced
+ by the array's byteLength value.
+
+ - If it's an ArrayBuffer, it gets wrapped in a Uint8Array and
+ treated as that type.
+
+ In each of those cases, the final argument (text destructor) is
+ ignored and capi.SQLITE_WASM_DEALLOC is assumed.
+
+ A 3rd argument of `null` is treated as if it were a WASM pointer
+ of 0.
+
+ If the 3rd argument is neither a WASM pointer nor one of the
+ above-described types, capi.SQLITE_MISUSE is returned.
+
+ The first argument may be either an `sqlite3_stmt*` WASM
+ pointer or an sqlite3.oo1.Stmt instance.
+
+ For consistency with the C API, it requires the same number of
+ arguments. It returns capi.SQLITE_MISUSE if passed any other
+ argument count.
+
+ If client code needs to bind partial strings, it needs to
+ either parcel the string up before passing it in here or it
+ must pass in a WASM pointer for the 3rd argument and a valid
+ 4th-argument value, taking care not to pass a value which
+ truncates a multi-byte UTF-8 character. When passing
+ WASM-format strings, it is important that the final argument be
+ valid or unexpected content can result can result, or even a
+ crash if the application reads past the WASM heap bounds.
+ */
+ sqlite3_bind_text: undefined/*installed later*/,
+
+ /**
+ sqlite3_create_function_v2() differs from its native
+ counterpart only in the following ways:
+
+ 1) The fourth argument (`eTextRep`) argument must not specify
+ any encoding other than sqlite3.SQLITE_UTF8. The JS API does not
+ currently support any other encoding and likely never
+ will. This function does not replace that argument on its own
+ because it may contain other flags. As a special case, if
+ the bottom 4 bits of that argument are 0, SQLITE_UTF8 is
+ assumed.
+
+ 2) Any of the four final arguments may be either WASM pointers
+ (assumed to be function pointers) or JS Functions. In the
+ latter case, each gets bound to WASM using
+ sqlite3.capi.wasm.installFunction() and that wrapper is passed
+ on to the native implementation.
+
+ For consistency with the C API, it requires the same number of
+ arguments. It returns capi.SQLITE_MISUSE if passed any other
+ argument count.
+
+ The semantics of JS functions are:
+
+ xFunc: is passed `(pCtx, ...values)`. Its return value becomes
+ the new SQL function's result.
+
+ xStep: is passed `(pCtx, ...values)`. Its return value is
+ ignored.
+
+ xFinal: is passed `(pCtx)`. Its return value becomes the new
+ aggregate SQL function's result.
+
+ xDestroy: is passed `(void*)`. Its return value is ignored. The
+ pointer passed to it is the one from the 5th argument to
+ sqlite3_create_function_v2().
+
+ Note that:
+
+ - `pCtx` in the above descriptions is a `sqlite3_context*`. At
+ least 99 times out of a hundred, that initial argument will
+ be irrelevant for JS UDF bindings, but it needs to be there
+ so that the cases where it _is_ relevant, in particular with
+ window and aggregate functions, have full access to the
+ lower-level sqlite3 APIs.
+
+ - When wrapping JS functions, the remaining arguments are passd
+ to them as positional arguments, not as an array of
+ arguments, because that allows callback definitions to be
+ more JS-idiomatic than C-like. For example `(pCtx,a,b)=>a+b`
+ is more intuitive and legible than
+ `(pCtx,args)=>args[0]+args[1]`. For cases where an array of
+ arguments would be more convenient, the callbacks simply need
+ to be declared like `(pCtx,...args)=>{...}`, in which case
+ `args` will be an array.
+
+ - If a JS wrapper throws, it gets translated to
+ sqlite3_result_error() or sqlite3_result_error_nomem(),
+ depending on whether the exception is an
+ sqlite3.WasmAllocError object or not.
+
+ - When passing on WASM function pointers, arguments are _not_
+ converted or reformulated. They are passed on as-is in raw
+ pointer form using their native C signatures. Only JS
+ functions passed in to this routine, and thus wrapped by this
+ routine, get automatic conversions of arguments and result
+ values. The routines which perform those conversions are
+ exposed for client-side use as
+ sqlite3_create_function_v2.convertUdfArgs() and
+ sqlite3_create_function_v2.setUdfResult(). sqlite3_create_function()
+ and sqlite3_create_window_function() have those same methods.
+
+ For xFunc(), xStep(), and xFinal():
+
+ - When called from SQL, arguments to the UDF, and its result,
+ will be converted between JS and SQL with as much fidelity as
+ is feasible, triggering an exception if a type conversion
+ cannot be determined. Some freedom is afforded to numeric
+ conversions due to friction between the JS and C worlds:
+ integers which are larger than 32 bits may be treated as
+ doubles or BigInts.
+
+ If any JS-side bound functions throw, those exceptions are
+ intercepted and converted to database-side errors with the
+ exception of xDestroy(): any exception from it is ignored,
+ possibly generating a console.error() message. Destructors
+ must not throw.
+
+ Once installed, there is currently no way to uninstall the
+ automatically-converted WASM-bound JS functions from WASM. They
+ can be uninstalled from the database as documented in the C
+ API, but this wrapper currently has no infrastructure in place
+ to also free the WASM-bound JS wrappers, effectively resulting
+ in a memory leak if the client uninstalls the UDF. Improving that
+ is a potential TODO, but removing client-installed UDFs is rare
+ in practice. If this factor is relevant for a given client,
+ they can create WASM-bound JS functions themselves, hold on to their
+ pointers, and pass the pointers in to here. Later on, they can
+ free those pointers (using `wasm.uninstallFunction()` or
+ equivalent).
+
+ C reference: https://www.sqlite.org/c3ref/create_function.html
+
+ Maintenance reminder: the ability to add new
+ WASM-accessible functions to the runtime requires that the
+ WASM build is compiled with emcc's `-sALLOW_TABLE_GROWTH`
+ flag.
+ */
+ sqlite3_create_function_v2: (
+ pDb, funcName, nArg, eTextRep, pApp,
+ xFunc, xStep, xFinal, xDestroy
+ )=>{/*installed later*/},
+ /**
+ Equivalent to passing the same arguments to
+ sqlite3_create_function_v2(), with 0 as the final argument.
+ */
+ sqlite3_create_function: (
+ pDb, funcName, nArg, eTextRep, pApp,
+ xFunc, xStep, xFinal
+ )=>{/*installed later*/},
+ /**
+ The sqlite3_create_window_function() JS wrapper differs from
+ its native implementation in the exact same way that
+ sqlite3_create_function_v2() does. The additional function,
+ xInverse(), is treated identically to xStep() by the wrapping
+ layer.
+ */
+ sqlite3_create_window_function: (
+ pDb, funcName, nArg, eTextRep, pApp,
+ xStep, xFinal, xValue, xInverse, xDestroy
+ )=>{/*installed later*/},
+ /**
+ The sqlite3_prepare_v3() binding handles two different uses
+ with differing JS/WASM semantics:
+
+ 1) sqlite3_prepare_v3(pDb, sqlString, -1, prepFlags, ppStmt , null)
+
+ 2) sqlite3_prepare_v3(pDb, sqlPointer, sqlByteLen, prepFlags, ppStmt, sqlPointerToPointer)
+
+ Note that the SQL length argument (the 3rd argument) must, for
+ usage (1), always be negative because it must be a byte length
+ and that value is expensive to calculate from JS (where only
+ the character length of strings is readily available). It is
+ retained in this API's interface for code/documentation
+ compatibility reasons but is currently _always_ ignored. With
+ usage (2), the 3rd argument is used as-is but is is still
+ critical that the C-style input string (2nd argument) be
+ terminated with a 0 byte.
+
+ In usage (1), the 2nd argument must be of type string,
+ Uint8Array, Int8Array, or ArrayBuffer (all of which are assumed
+ to hold SQL). If it is, this function assumes case (1) and
+ calls the underyling C function with the equivalent of:
+
+ (pDb, sqlAsString, -1, prepFlags, ppStmt, null)
+
+ The `pzTail` argument is ignored in this case because its
+ result is meaningless when a string-type value is passed
+ through: the string goes through another level of internal
+ conversion for WASM's sake and the result pointer would refer
+ to that transient conversion's memory, not the passed-in
+ string.
+
+ If the sql argument is not a string, it must be a _pointer_ to
+ a NUL-terminated string which was allocated in the WASM memory
+ (e.g. using capi.wasm.alloc() or equivalent). In that case,
+ the final argument may be 0/null/undefined or must be a pointer
+ to which the "tail" of the compiled SQL is written, as
+ documented for the C-side sqlite3_prepare_v3(). In case (2),
+ the underlying C function is called with the equivalent of:
+
+ (pDb, sqlAsPointer, sqlByteLen, prepFlags, ppStmt, pzTail)
+
+ It returns its result and compiled statement as documented in
+ the C API. Fetching the output pointers (5th and 6th
+ parameters) requires using `capi.wasm.peek()` (or
+ equivalent) and the `pzTail` will point to an address relative to
+ the `sqlAsPointer` value.
+
+ If passed an invalid 2nd argument type, this function will
+ return SQLITE_MISUSE and sqlite3_errmsg() will contain a string
+ describing the problem.
+
+ Side-note: if given an empty string, or one which contains only
+ comments or an empty SQL expression, 0 is returned but the result
+ output pointer will be NULL.
+ */
+ sqlite3_prepare_v3: (dbPtr, sql, sqlByteLen, prepFlags,
+ stmtPtrPtr, strPtrPtr)=>{}/*installed later*/,
+
+ /**
+ Equivalent to calling sqlite3_prapare_v3() with 0 as its 4th argument.
+ */
+ sqlite3_prepare_v2: (dbPtr, sql, sqlByteLen,
+ stmtPtrPtr,strPtrPtr)=>{}/*installed later*/,
+
+ /**
+ This binding enables the callback argument to be a JavaScript.
+
+ If the callback is a function, then for the duration of the
+ sqlite3_exec() call, it installs a WASM-bound function which
+ acts as a proxy for the given callback. That proxy will also
+ perform a conversion of the callback's arguments from
+ `(char**)` to JS arrays of strings. However, for API
+ consistency's sake it will still honor the C-level callback
+ parameter order and will call it like:
+
+ `callback(pVoid, colCount, listOfValues, listOfColNames)`
+
+ If the callback is not a JS function then this binding performs
+ no translation of the callback, but the sql argument is still
+ converted to a WASM string for the call using the
+ "string:flexible" argument converter.
+ */
+ sqlite3_exec: (pDb, sql, callback, pVoid, pErrMsg)=>{}/*installed later*/,
+
+ /**
+ If passed a single argument which appears to be a byte-oriented
+ TypedArray (Int8Array or Uint8Array), this function treats that
+ TypedArray as an output target, fetches `theArray.byteLength`
+ bytes of randomness, and populates the whole array with it. As
+ a special case, if the array's length is 0, this function
+ behaves as if it were passed (0,0). When called this way, it
+ returns its argument, else it returns the `undefined` value.
+
+ If called with any other arguments, they are passed on as-is
+ to the C API. Results are undefined if passed any incompatible
+ values.
+ */
+ sqlite3_randomness: (n, outPtr)=>{/*installed later*/},
+ }/*capi*/);
+
+ /**
+ Various internal-use utilities are added here as needed. They
+ are bound to an object only so that we have access to them in
+ the differently-scoped steps of the API bootstrapping
+ process. At the end of the API setup process, this object gets
+ removed. These are NOT part of the public API.
+ */
+ const util = {
+ affirmBindableTypedArray, flexibleString,
+ bigIntFits32, bigIntFits64, bigIntFitsDouble,
+ isBindableTypedArray,
+ isInt32, isSQLableTypedArray, isTypedArray,
+ typedArrayToString,
+ isUIThread: ()=>(globalThis.window===globalThis && !!globalThis.document),
+ // is this true for ESM?: 'undefined'===typeof WorkerGlobalScope
+ isSharedTypedArray,
+ toss: function(...args){throw new Error(args.join(' '))},
+ toss3,
+ typedArrayPart,
+ /**
+ Given a byte array or ArrayBuffer, this function throws if the
+ lead bytes of that buffer do not hold a SQLite3 database header,
+ else it returns without side effects.
+
+ Added in 3.44.
+ */
+ affirmDbHeader: function(bytes){
+ if(bytes instanceof ArrayBuffer) bytes = new Uint8Array(bytes);
+ const header = "SQLite format 3";
+ if( header.length > bytes.byteLength ){
+ toss3("Input does not contain an SQLite3 database header.");
+ }
+ for(let i = 0; i < header.length; ++i){
+ if( header.charCodeAt(i) !== bytes[i] ){
+ toss3("Input does not contain an SQLite3 database header.");
+ }
+ }
+ },
+ /**
+ Given a byte array or ArrayBuffer, this function throws if the
+ database does not, at a cursory glance, appear to be an SQLite3
+ database. It only examines the size and header, but further
+ checks may be added in the future.
+
+ Added in 3.44.
+ */
+ affirmIsDb: function(bytes){
+ if(bytes instanceof ArrayBuffer) bytes = new Uint8Array(bytes);
+ const n = bytes.byteLength;
+ if(n<512 || n%512!==0) {
+ toss3("Byte array size",n,"is invalid for an SQLite3 db.");
+ }
+ util.affirmDbHeader(bytes);
+ }
+ }/*util*/;
+
+ Object.assign(wasm, {
+ /**
+ Emscripten APIs have a deep-seated assumption that all pointers
+ are 32 bits. We'll remain optimistic that that won't always be
+ the case and will use this constant in places where we might
+ otherwise use a hard-coded 4.
+ */
+ ptrSizeof: config.wasmPtrSizeof || 4,
+ /**
+ The WASM IR (Intermediate Representation) value for
+ pointer-type values. It MUST refer to a value type of the
+ size described by this.ptrSizeof.
+ */
+ ptrIR: config.wasmPtrIR || "i32",
+ /**
+ True if BigInt support was enabled via (e.g.) the
+ Emscripten -sWASM_BIGINT flag, else false. When
+ enabled, certain 64-bit sqlite3 APIs are enabled which
+ are not otherwise enabled due to JS/WASM int64
+ impedence mismatches.
+ */
+ bigIntEnabled: !!config.bigIntEnabled,
+ /**
+ The symbols exported by the WASM environment.
+ */
+ exports: config.exports
+ || toss3("Missing API config.exports (WASM module exports)."),
+
+ /**
+ When Emscripten compiles with `-sIMPORTED_MEMORY`, it
+ initalizes the heap and imports it into wasm, as opposed to
+ the other way around. In this case, the memory is not
+ available via this.exports.memory.
+ */
+ memory: config.memory || config.exports['memory']
+ || toss3("API config object requires a WebAssembly.Memory object",
+ "in either config.exports.memory (exported)",
+ "or config.memory (imported)."),
+
+ /**
+ The API's primary point of access to the WASM-side memory
+ allocator. Works like sqlite3_malloc() but throws a
+ WasmAllocError if allocation fails. It is important that any
+ code which might pass through the sqlite3 C API NOT throw and
+ must instead return SQLITE_NOMEM (or equivalent, depending on
+ the context).
+
+ Very few cases in the sqlite3 JS APIs can result in
+ client-defined functions propagating exceptions via the C-style
+ API. Most notably, this applies to WASM-bound JS functions
+ which are created directly by clients and passed on _as WASM
+ function pointers_ to functions such as
+ sqlite3_create_function_v2(). Such bindings created
+ transparently by this API will automatically use wrappers which
+ catch exceptions and convert them to appropriate error codes.
+
+ For cases where non-throwing allocation is required, use
+ this.alloc.impl(), which is direct binding of the
+ underlying C-level allocator.
+
+ Design note: this function is not named "malloc" primarily
+ because Emscripten uses that name and we wanted to avoid any
+ confusion early on in this code's development, when it still
+ had close ties to Emscripten's glue code.
+ */
+ alloc: undefined/*installed later*/,
+
+ /**
+ Rarely necessary in JS code, this routine works like
+ sqlite3_realloc(M,N), where M is either NULL or a pointer
+ obtained from this function or this.alloc() and N is the number
+ of bytes to reallocate the block to. Returns a pointer to the
+ reallocated block or 0 if allocation fails.
+
+ If M is NULL and N is positive, this behaves like
+ this.alloc(N). If N is 0, it behaves like this.dealloc().
+ Results are undefined if N is negative (sqlite3_realloc()
+ treats that as 0, but if this code is built with a different
+ allocator it may misbehave with negative values).
+
+ Like this.alloc.impl(), this.realloc.impl() is a direct binding
+ to the underlying realloc() implementation which does not throw
+ exceptions, instead returning 0 on allocation error.
+ */
+ realloc: undefined/*installed later*/,
+
+ /**
+ The API's primary point of access to the WASM-side memory
+ deallocator. Works like sqlite3_free().
+
+ Design note: this function is not named "free" for the same
+ reason that this.alloc() is not called this.malloc().
+ */
+ dealloc: undefined/*installed later*/
+
+ /* Many more wasm-related APIs get installed later on. */
+ }/*wasm*/);
+
+ /**
+ wasm.alloc()'s srcTypedArray.byteLength bytes,
+ populates them with the values from the source
+ TypedArray, and returns the pointer to that memory. The
+ returned pointer must eventually be passed to
+ wasm.dealloc() to clean it up.
+
+ The argument may be a Uint8Array, Int8Array, or ArrayBuffer,
+ and it throws if passed any other type.
+
+ As a special case, to avoid further special cases where
+ this is used, if srcTypedArray.byteLength is 0, it
+ allocates a single byte and sets it to the value
+ 0. Even in such cases, calls must behave as if the
+ allocated memory has exactly srcTypedArray.byteLength
+ bytes.
+ */
+ wasm.allocFromTypedArray = function(srcTypedArray){
+ if(srcTypedArray instanceof ArrayBuffer){
+ srcTypedArray = new Uint8Array(srcTypedArray);
+ }
+ affirmBindableTypedArray(srcTypedArray);
+ const pRet = wasm.alloc(srcTypedArray.byteLength || 1);
+ wasm.heapForSize(srcTypedArray.constructor).set(
+ srcTypedArray.byteLength ? srcTypedArray : [0], pRet
+ );
+ return pRet;
+ };
+
+ {
+ // Set up allocators...
+ const keyAlloc = config.allocExportName,
+ keyDealloc = config.deallocExportName,
+ keyRealloc = config.reallocExportName;
+ for(const key of [keyAlloc, keyDealloc, keyRealloc]){
+ const f = wasm.exports[key];
+ if(!(f instanceof Function)) toss3("Missing required exports[",key,"] function.");
+ }
+
+ wasm.alloc = function f(n){
+ return f.impl(n) || WasmAllocError.toss("Failed to allocate",n," bytes.");
+ };
+ wasm.alloc.impl = wasm.exports[keyAlloc];
+ wasm.realloc = function f(m,n){
+ const m2 = f.impl(m,n);
+ return n ? (m2 || WasmAllocError.toss("Failed to reallocate",n," bytes.")) : 0;
+ };
+ wasm.realloc.impl = wasm.exports[keyRealloc];
+ wasm.dealloc = wasm.exports[keyDealloc];
+ }
+
+ /**
+ Reports info about compile-time options using
+ sqlite3_compileoption_get() and sqlite3_compileoption_used(). It
+ has several distinct uses:
+
+ If optName is an array then it is expected to be a list of
+ compilation options and this function returns an object
+ which maps each such option to true or false, indicating
+ whether or not the given option was included in this
+ build. That object is returned.
+
+ If optName is an object, its keys are expected to be compilation
+ options and this function sets each entry to true or false,
+ indicating whether the compilation option was used or not. That
+ object is returned.
+
+ If passed no arguments then it returns an object mapping
+ all known compilation options to their compile-time values,
+ or boolean true if they are defined with no value. This
+ result, which is relatively expensive to compute, is cached
+ and returned for future no-argument calls.
+
+ In all other cases it returns true if the given option was
+ active when when compiling the sqlite3 module, else false.
+
+ Compile-time option names may optionally include their
+ "SQLITE_" prefix. When it returns an object of all options,
+ the prefix is elided.
+ */
+ wasm.compileOptionUsed = function f(optName){
+ if(!arguments.length){
+ if(f._result) return f._result;
+ else if(!f._opt){
+ f._rx = /^([^=]+)=(.+)/;
+ f._rxInt = /^-?\d+$/;
+ f._opt = function(opt, rv){
+ const m = f._rx.exec(opt);
+ rv[0] = (m ? m[1] : opt);
+ rv[1] = m ? (f._rxInt.test(m[2]) ? +m[2] : m[2]) : true;
+ };
+ }
+ const rc = {}, ov = [0,0];
+ let i = 0, k;
+ while((k = capi.sqlite3_compileoption_get(i++))){
+ f._opt(k,ov);
+ rc[ov[0]] = ov[1];
+ }
+ return f._result = rc;
+ }else if(Array.isArray(optName)){
+ const rc = {};
+ optName.forEach((v)=>{
+ rc[v] = capi.sqlite3_compileoption_used(v);
+ });
+ return rc;
+ }else if('object' === typeof optName){
+ Object.keys(optName).forEach((k)=> {
+ optName[k] = capi.sqlite3_compileoption_used(k);
+ });
+ return optName;
+ }
+ return (
+ 'string'===typeof optName
+ ) ? !!capi.sqlite3_compileoption_used(optName) : false;
+ }/*compileOptionUsed()*/;
+
+ /**
+ sqlite3.wasm.pstack (pseudo-stack) holds a special-case
+ stack-style allocator intended only for use with _small_ data of
+ not more than (in total) a few kb in size, managed as if it were
+ stack-based.
+
+ It has only a single intended usage:
+
+ ```
+ const stackPos = pstack.pointer;
+ try{
+ const ptr = pstack.alloc(8);
+ // ==> pstack.pointer === ptr
+ const otherPtr = pstack.alloc(8);
+ // ==> pstack.pointer === otherPtr
+ ...
+ }finally{
+ pstack.restore(stackPos);
+ // ==> pstack.pointer === stackPos
+ }
+ ```
+
+ This allocator is much faster than a general-purpose one but is
+ limited to usage patterns like the one shown above.
+
+ It operates from a static range of memory which lives outside of
+ space managed by Emscripten's stack-management, so does not
+ collide with Emscripten-provided stack allocation APIs. The
+ memory lives in the WASM heap and can be used with routines such
+ as wasm.poke() and wasm.heap8u().slice().
+ */
+ wasm.pstack = Object.assign(Object.create(null),{
+ /**
+ Sets the current pstack position to the given pointer. Results
+ are undefined if the passed-in value did not come from
+ this.pointer.
+ */
+ restore: wasm.exports.sqlite3_wasm_pstack_restore,
+ /**
+ Attempts to allocate the given number of bytes from the
+ pstack. On success, it zeroes out a block of memory of the
+ given size, adjusts the pstack pointer, and returns a pointer
+ to the memory. On error, throws a WasmAllocError. The
+ memory must eventually be released using restore().
+
+ If n is a string, it must be a WASM "IR" value in the set
+ accepted by wasm.sizeofIR(), which is mapped to the size of
+ that data type. If passed a string not in that set, it throws a
+ WasmAllocError.
+
+ This method always adjusts the given value to be a multiple
+ of 8 bytes because failing to do so can lead to incorrect
+ results when reading and writing 64-bit values from/to the WASM
+ heap. Similarly, the returned address is always 8-byte aligned.
+ */
+ alloc: function(n){
+ if('string'===typeof n && !(n = wasm.sizeofIR(n))){
+ WasmAllocError.toss("Invalid value for pstack.alloc(",arguments[0],")");
+ }
+ return wasm.exports.sqlite3_wasm_pstack_alloc(n)
+ || WasmAllocError.toss("Could not allocate",n,
+ "bytes from the pstack.");
+ },
+ /**
+ alloc()'s n chunks, each sz bytes, as a single memory block and
+ returns the addresses as an array of n element, each holding
+ the address of one chunk.
+
+ sz may optionally be an IR string accepted by wasm.sizeofIR().
+
+ Throws a WasmAllocError if allocation fails.
+
+ Example:
+
+ ```
+ const [p1, p2, p3] = wasm.pstack.allocChunks(3,4);
+ ```
+ */
+ allocChunks: function(n,sz){
+ if('string'===typeof sz && !(sz = wasm.sizeofIR(sz))){
+ WasmAllocError.toss("Invalid size value for allocChunks(",arguments[1],")");
+ }
+ const mem = wasm.pstack.alloc(n * sz);
+ const rc = [];
+ let i = 0, offset = 0;
+ for(; i < n; ++i, offset += sz) rc.push(mem + offset);
+ return rc;
+ },
+ /**
+ A convenience wrapper for allocChunks() which sizes each chunk
+ as either 8 bytes (safePtrSize is truthy) or wasm.ptrSizeof (if
+ safePtrSize is falsy).
+
+ How it returns its result differs depending on its first
+ argument: if it's 1, it returns a single pointer value. If it's
+ more than 1, it returns the same as allocChunks().
+
+ When a returned pointers will refer to a 64-bit value, e.g. a
+ double or int64, and that value must be written or fetched,
+ e.g. using wasm.poke() or wasm.peek(), it is
+ important that the pointer in question be aligned to an 8-byte
+ boundary or else it will not be fetched or written properly and
+ will corrupt or read neighboring memory.
+
+ However, when all pointers involved point to "small" data, it
+ is safe to pass a falsy value to save a tiny bit of memory.
+ */
+ allocPtr: (n=1,safePtrSize=true)=>{
+ return 1===n
+ ? wasm.pstack.alloc(safePtrSize ? 8 : wasm.ptrSizeof)
+ : wasm.pstack.allocChunks(n, safePtrSize ? 8 : wasm.ptrSizeof);
+ },
+
+ /**
+ Records the current pstack position, calls the given function,
+ passing it the sqlite3 object, then restores the pstack
+ regardless of whether the function throws. Returns the result
+ of the call or propagates an exception on error.
+
+ Added in 3.44.
+ */
+ call: function(f){
+ const stackPos = wasm.pstack.pointer;
+ try{ return f(sqlite3) } finally{
+ wasm.pstack.restore(stackPos);
+ }
+ }
+
+ })/*wasm.pstack*/;
+ Object.defineProperties(wasm.pstack, {
+ /**
+ sqlite3.wasm.pstack.pointer resolves to the current pstack
+ position pointer. This value is intended _only_ to be saved
+ for passing to restore(). Writing to this memory, without
+ first reserving it via wasm.pstack.alloc() and friends, leads
+ to undefined results.
+ */
+ pointer: {
+ configurable: false, iterable: true, writeable: false,
+ get: wasm.exports.sqlite3_wasm_pstack_ptr
+ //Whether or not a setter as an alternative to restore() is
+ //clearer or would just lead to confusion is unclear.
+ //set: wasm.exports.sqlite3_wasm_pstack_restore
+ },
+ /**
+ sqlite3.wasm.pstack.quota to the total number of bytes
+ available in the pstack, including any space which is currently
+ allocated. This value is a compile-time constant.
+ */
+ quota: {
+ configurable: false, iterable: true, writeable: false,
+ get: wasm.exports.sqlite3_wasm_pstack_quota
+ },
+ /**
+ sqlite3.wasm.pstack.remaining resolves to the amount of space
+ remaining in the pstack.
+ */
+ remaining: {
+ configurable: false, iterable: true, writeable: false,
+ get: wasm.exports.sqlite3_wasm_pstack_remaining
+ }
+ })/*wasm.pstack properties*/;
+
+ capi.sqlite3_randomness = (...args)=>{
+ if(1===args.length && util.isTypedArray(args[0])
+ && 1===args[0].BYTES_PER_ELEMENT){
+ const ta = args[0];
+ if(0===ta.byteLength){
+ wasm.exports.sqlite3_randomness(0,0);
+ return ta;
+ }
+ const stack = wasm.pstack.pointer;
+ try {
+ let n = ta.byteLength, offset = 0;
+ const r = wasm.exports.sqlite3_randomness;
+ const heap = wasm.heap8u();
+ const nAlloc = n < 512 ? n : 512;
+ const ptr = wasm.pstack.alloc(nAlloc);
+ do{
+ const j = (n>nAlloc ? nAlloc : n);
+ r(j, ptr);
+ ta.set(typedArrayPart(heap, ptr, ptr+j), offset);
+ n -= j;
+ offset += j;
+ } while(n > 0);
+ }catch(e){
+ console.error("Highly unexpected (and ignored!) "+
+ "exception in sqlite3_randomness():",e);
+ }finally{
+ wasm.pstack.restore(stack);
+ }
+ return ta;
+ }
+ wasm.exports.sqlite3_randomness(...args);
+ };
+
+ /** State for sqlite3_wasmfs_opfs_dir(). */
+ let __wasmfsOpfsDir = undefined;
+ /**
+ If the wasm environment has a WASMFS/OPFS-backed persistent
+ storage directory, its path is returned by this function. If it
+ does not then it returns "" (noting that "" is a falsy value).
+
+ The first time this is called, this function inspects the current
+ environment to determine whether persistence support is available
+ and, if it is, enables it (if needed). After the first call it
+ always returns the cached result.
+
+ If the returned string is not empty, any files stored under the
+ given path (recursively) are housed in OPFS storage. If the
+ returned string is empty, this particular persistent storage
+ option is not available on the client.
+
+ Though the mount point name returned by this function is intended
+ to remain stable, clients should not hard-coded it anywhere. Always call this function to get the path.
+
+ Note that this function is a no-op in must builds of this
+ library, as the WASMFS capability requires a custom
+ build.
+ */
+ capi.sqlite3_wasmfs_opfs_dir = function(){
+ if(undefined !== __wasmfsOpfsDir) return __wasmfsOpfsDir;
+ // If we have no OPFS, there is no persistent dir
+ const pdir = config.wasmfsOpfsDir;
+ if(!pdir
+ || !globalThis.FileSystemHandle
+ || !globalThis.FileSystemDirectoryHandle
+ || !globalThis.FileSystemFileHandle){
+ return __wasmfsOpfsDir = "";
+ }
+ try{
+ if(pdir && 0===wasm.xCallWrapped(
+ 'sqlite3_wasm_init_wasmfs', 'i32', ['string'], pdir
+ )){
+ return __wasmfsOpfsDir = pdir;
+ }else{
+ return __wasmfsOpfsDir = "";
+ }
+ }catch(e){
+ // sqlite3_wasm_init_wasmfs() is not available
+ return __wasmfsOpfsDir = "";
+ }
+ };
+
+ /**
+ Returns true if sqlite3.capi.sqlite3_wasmfs_opfs_dir() is a
+ non-empty string and the given name starts with (that string +
+ '/'), else returns false.
+ */
+ capi.sqlite3_wasmfs_filename_is_persistent = function(name){
+ const p = capi.sqlite3_wasmfs_opfs_dir();
+ return (p && name) ? name.startsWith(p+'/') : false;
+ };
+
+ /**
+ Given an `sqlite3*`, an sqlite3_vfs name, and an optional db name
+ (defaulting to "main"), returns a truthy value (see below) if
+ that db uses that VFS, else returns false. If pDb is falsy then
+ the 3rd argument is ignored and this function returns a truthy
+ value if the default VFS name matches that of the 2nd
+ argument. Results are undefined if pDb is truthy but refers to an
+ invalid pointer. The 3rd argument specifies the database name of
+ the given database connection to check, defaulting to the main
+ db.
+
+ The 2nd and 3rd arguments may either be a JS string or a WASM
+ C-string. If the 2nd argument is a NULL WASM pointer, the default
+ VFS is assumed. If the 3rd is a NULL WASM pointer, "main" is
+ assumed.
+
+ The truthy value it returns is a pointer to the `sqlite3_vfs`
+ object.
+
+ To permit safe use of this function from APIs which may be called
+ via the C stack (like SQL UDFs), this function does not throw: if
+ bad arguments cause a conversion error when passing into
+ wasm-space, false is returned.
+ */
+ capi.sqlite3_js_db_uses_vfs = function(pDb,vfsName,dbName=0){
+ try{
+ const pK = capi.sqlite3_vfs_find(vfsName);
+ if(!pK) return false;
+ else if(!pDb){
+ return pK===capi.sqlite3_vfs_find(0) ? pK : false;
+ }else{
+ return pK===capi.sqlite3_js_db_vfs(pDb,dbName) ? pK : false;
+ }
+ }catch(e){
+ /* Ignore - probably bad args to a wasm-bound function. */
+ return false;
+ }
+ };
+
+ /**
+ Returns an array of the names of all currently-registered sqlite3
+ VFSes.
+ */
+ capi.sqlite3_js_vfs_list = function(){
+ const rc = [];
+ let pVfs = capi.sqlite3_vfs_find(0);
+ while(pVfs){
+ const oVfs = new capi.sqlite3_vfs(pVfs);
+ rc.push(wasm.cstrToJs(oVfs.$zName));
+ pVfs = oVfs.$pNext;
+ oVfs.dispose();
+ }
+ return rc;
+ };
+
+ /**
+ A convenience wrapper around sqlite3_serialize() which serializes
+ the given `sqlite3*` pointer to a Uint8Array. The first argument
+ may be either an `sqlite3*` or an sqlite3.oo1.DB instance.
+
+ On success it returns a Uint8Array. If the schema is empty, an
+ empty array is returned.
+
+ `schema` is the schema to serialize. It may be a WASM C-string
+ pointer or a JS string. If it is falsy, it defaults to `"main"`.
+
+ On error it throws with a description of the problem.
+ */
+ capi.sqlite3_js_db_export = function(pDb, schema=0){
+ pDb = wasm.xWrap.testConvertArg('sqlite3*', pDb);
+ if(!pDb) toss3('Invalid sqlite3* argument.');
+ if(!wasm.bigIntEnabled) toss3('BigInt64 support is not enabled.');
+ const scope = wasm.scopedAllocPush();
+ let pOut;
+ try{
+ const pSize = wasm.scopedAlloc(8/*i64*/ + wasm.ptrSizeof);
+ const ppOut = pSize + 8;
+ /**
+ Maintenance reminder, since this cost a full hour of grief
+ and confusion: if the order of pSize/ppOut are reversed in
+ that memory block, fetching the value of pSize after the
+ export reads a garbage size because it's not on an 8-byte
+ memory boundary!
+ */
+ const zSchema = schema
+ ? (wasm.isPtr(schema) ? schema : wasm.scopedAllocCString(''+schema))
+ : 0;
+ let rc = wasm.exports.sqlite3_wasm_db_serialize(
+ pDb, zSchema, ppOut, pSize, 0
+ );
+ if(rc){
+ toss3("Database serialization failed with code",
+ sqlite3.capi.sqlite3_js_rc_str(rc));
+ }
+ pOut = wasm.peekPtr(ppOut);
+ const nOut = wasm.peek(pSize, 'i64');
+ rc = nOut
+ ? wasm.heap8u().slice(pOut, pOut + Number(nOut))
+ : new Uint8Array();
+ return rc;
+ }finally{
+ if(pOut) wasm.exports.sqlite3_free(pOut);
+ wasm.scopedAllocPop(scope);
+ }
+ };
+
+ /**
+ Given a `sqlite3*` and a database name (JS string or WASM
+ C-string pointer, which may be 0), returns a pointer to the
+ sqlite3_vfs responsible for it. If the given db name is null/0,
+ or not provided, then "main" is assumed.
+ */
+ capi.sqlite3_js_db_vfs =
+ (dbPointer, dbName=0)=>wasm.sqlite3_wasm_db_vfs(dbPointer, dbName);
+
+ /**
+ A thin wrapper around capi.sqlite3_aggregate_context() which
+ behaves the same except that it throws a WasmAllocError if that
+ function returns 0. As a special case, if n is falsy it does
+ _not_ throw if that function returns 0. That special case is
+ intended for use with xFinal() implementations.
+ */
+ capi.sqlite3_js_aggregate_context = (pCtx, n)=>{
+ return capi.sqlite3_aggregate_context(pCtx, n)
+ || (n ? WasmAllocError.toss("Cannot allocate",n,
+ "bytes for sqlite3_aggregate_context()")
+ : 0);
+ };
+
+ /**
+ If the current environment supports the POSIX file APIs, this routine
+ creates (or overwrites) the given file using those APIs. This is
+ primarily intended for use in Emscripten-based builds where the POSIX
+ APIs are transparently proxied by an in-memory virtual filesystem.
+ It may behave diffrently in other environments.
+
+ The first argument must be either a JS string or WASM C-string
+ holding the filename. Note that this routine does _not_ create
+ intermediary directories if the filename has a directory part.
+
+ The 2nd argument may either a valid WASM memory pointer, an
+ ArrayBuffer, or a Uint8Array. The 3rd must be the length, in
+ bytes, of the data array to copy. If the 2nd argument is an
+ ArrayBuffer or Uint8Array and the 3rd is not a positive integer
+ then the 3rd defaults to the array's byteLength value.
+
+ Results are undefined if data is a WASM pointer and dataLen is
+ exceeds data's bounds.
+
+ Throws if any arguments are invalid or if creating or writing to
+ the file fails.
+
+ Added in 3.43 as an alternative for the deprecated
+ sqlite3_js_vfs_create_file().
+ */
+ capi.sqlite3_js_posix_create_file = function(filename, data, dataLen){
+ let pData;
+ if(data && wasm.isPtr(data)){
+ pData = data;
+ }else if(data instanceof ArrayBuffer || data instanceof Uint8Array){
+ pData = wasm.allocFromTypedArray(data);
+ if(arguments.length<3 || !util.isInt32(dataLen) || dataLen<0){
+ dataLen = data.byteLength;
+ }
+ }else{
+ SQLite3Error.toss("Invalid 2nd argument for sqlite3_js_posix_create_file().");
+ }
+ try{
+ if(!util.isInt32(dataLen) || dataLen<0){
+ SQLite3Error.toss("Invalid 3rd argument for sqlite3_js_posix_create_file().");
+ }
+ const rc = wasm.sqlite3_wasm_posix_create_file(filename, pData, dataLen);
+ if(rc) SQLite3Error.toss("Creation of file failed with sqlite3 result code",
+ capi.sqlite3_js_rc_str(rc));
+ }finally{
+ wasm.dealloc(pData);
+ }
+ };
+
+ /**
+ Deprecation warning: this function does not work properly in
+ debug builds of sqlite3 because its out-of-scope use of the
+ sqlite3_vfs API triggers assertions in the core library. That
+ was unfortunately not discovered until 2023-08-11. This function
+ is now deprecated and should not be used in new code.
+
+ Alternative options:
+
+ - "unix" VFS and its variants can get equivalent functionality
+ with sqlite3_js_posix_create_file().
+
+ - OPFS: use either sqlite3.oo1.OpfsDb.importDb(), for the "opfs"
+ VFS, or the importDb() method of the PoolUtil object provided
+ by the "opfs-sahpool" OPFS (noting that its VFS name may differ
+ depending on client-side configuration). We cannot proxy those
+ from here because the former is necessarily asynchronous and
+ the latter requires information not available to this function.
+
+ Creates a file using the storage appropriate for the given
+ sqlite3_vfs. The first argument may be a VFS name (JS string
+ only, NOT a WASM C-string), WASM-managed `sqlite3_vfs*`, or
+ a capi.sqlite3_vfs instance. Pass 0 (a NULL pointer) to use the
+ default VFS. If passed a string which does not resolve using
+ sqlite3_vfs_find(), an exception is thrown. (Note that a WASM
+ C-string is not accepted because it is impossible to
+ distinguish from a C-level `sqlite3_vfs*`.)
+
+ The second argument, the filename, must be a JS or WASM C-string.
+
+ The 3rd may either be falsy, a valid WASM memory pointer, an
+ ArrayBuffer, or a Uint8Array. The 4th must be the length, in
+ bytes, of the data array to copy. If the 3rd argument is an
+ ArrayBuffer or Uint8Array and the 4th is not a positive integer
+ then the 4th defaults to the array's byteLength value.
+
+ If data is falsy then a file is created with dataLen bytes filled
+ with uninitialized data (whatever truncate() leaves there). If
+ data is not falsy then a file is created or truncated and it is
+ filled with the first dataLen bytes of the data source.
+
+ Throws if any arguments are invalid or if creating or writing to
+ the file fails.
+
+ Note that most VFSes do _not_ automatically create directory
+ parts of filenames, nor do all VFSes have a concept of
+ directories. If the given filename is not valid for the given
+ VFS, an exception will be thrown. This function exists primarily
+ to assist in implementing file-upload capability, with the caveat
+ that clients must have some idea of the VFS into which they want
+ to upload and that VFS must support the operation.
+
+ VFS-specific notes:
+
+ - "memdb": results are undefined.
+
+ - "kvvfs": will fail with an I/O error due to strict internal
+ requirments of that VFS's xTruncate().
+
+ - "unix" and related: will use the WASM build's equivalent of the
+ POSIX I/O APIs. This will work so long as neither a specific
+ VFS nor the WASM environment imposes requirements which break it.
+
+ - "opfs": uses OPFS storage and creates directory parts of the
+ filename. It can only be used to import an SQLite3 database
+ file and will fail if given anything else.
+ */
+ capi.sqlite3_js_vfs_create_file = function(vfs, filename, data, dataLen){
+ config.warn("sqlite3_js_vfs_create_file() is deprecated and",
+ "should be avoided because it can lead to C-level crashes.",
+ "See its documentation for alternative options.");
+ let pData;
+ if(data){
+ if(wasm.isPtr(data)){
+ pData = data;
+ }else if(data instanceof ArrayBuffer){
+ data = new Uint8Array(data);
+ }
+ if(data instanceof Uint8Array){
+ pData = wasm.allocFromTypedArray(data);
+ if(arguments.length<4 || !util.isInt32(dataLen) || dataLen<0){
+ dataLen = data.byteLength;
+ }
+ }else{
+ SQLite3Error.toss("Invalid 3rd argument type for sqlite3_js_vfs_create_file().");
+ }
+ }else{
+ pData = 0;
+ }
+ if(!util.isInt32(dataLen) || dataLen<0){
+ wasm.dealloc(pData);
+ SQLite3Error.toss("Invalid 4th argument for sqlite3_js_vfs_create_file().");
+ }
+ try{
+ const rc = wasm.sqlite3_wasm_vfs_create_file(vfs, filename, pData, dataLen);
+ if(rc) SQLite3Error.toss("Creation of file failed with sqlite3 result code",
+ capi.sqlite3_js_rc_str(rc));
+ }finally{
+ wasm.dealloc(pData);
+ }
+ };
+
+ /**
+ Converts SQL input from a variety of convenient formats
+ to plain strings.
+
+ If v is a string, it is returned as-is. If it is-a Array, its
+ join("") result is returned. If is is a Uint8Array, Int8Array,
+ or ArrayBuffer, it is assumed to hold UTF-8-encoded text and is
+ decoded to a string. If it looks like a WASM pointer,
+ wasm.cstrToJs(sql) is returned. Else undefined is returned.
+
+ Added in 3.44
+ */
+ capi.sqlite3_js_sql_to_string = (sql)=>{
+ if('string' === typeof sql){
+ return sql;
+ }
+ const x = flexibleString(v);
+ return x===v ? undefined : x;
+ }
+
+ if( util.isUIThread() ){
+ /* Features specific to the main window thread... */
+
+ /**
+ Internal helper for sqlite3_js_kvvfs_clear() and friends.
+ Its argument should be one of ('local','session',"").
+ */
+ const __kvvfsInfo = function(which){
+ const rc = Object.create(null);
+ rc.prefix = 'kvvfs-'+which;
+ rc.stores = [];
+ if('session'===which || ""===which) rc.stores.push(globalThis.sessionStorage);
+ if('local'===which || ""===which) rc.stores.push(globalThis.localStorage);
+ return rc;
+ };
+
+ /**
+ Clears all storage used by the kvvfs DB backend, deleting any
+ DB(s) stored there. Its argument must be either 'session',
+ 'local', or "". In the first two cases, only sessionStorage
+ resp. localStorage is cleared. If it's an empty string (the
+ default) then both are cleared. Only storage keys which match
+ the pattern used by kvvfs are cleared: any other client-side
+ data are retained.
+
+ This function is only available in the main window thread.
+
+ Returns the number of entries cleared.
+ */
+ capi.sqlite3_js_kvvfs_clear = function(which=""){
+ let rc = 0;
+ const kvinfo = __kvvfsInfo(which);
+ kvinfo.stores.forEach((s)=>{
+ const toRm = [] /* keys to remove */;
+ let i;
+ for( i = 0; i < s.length; ++i ){
+ const k = s.key(i);
+ if(k.startsWith(kvinfo.prefix)) toRm.push(k);
+ }
+ toRm.forEach((kk)=>s.removeItem(kk));
+ rc += toRm.length;
+ });
+ return rc;
+ };
+
+ /**
+ This routine guesses the approximate amount of
+ window.localStorage and/or window.sessionStorage in use by the
+ kvvfs database backend. Its argument must be one of
+ ('session', 'local', ""). In the first two cases, only
+ sessionStorage resp. localStorage is counted. If it's an empty
+ string (the default) then both are counted. Only storage keys
+ which match the pattern used by kvvfs are counted. The returned
+ value is the "length" value of every matching key and value,
+ noting that JavaScript stores each character in 2 bytes.
+
+ Note that the returned size is not authoritative from the
+ perspective of how much data can fit into localStorage and
+ sessionStorage, as the precise algorithms for determining
+ those limits are unspecified and may include per-entry
+ overhead invisible to clients.
+ */
+ capi.sqlite3_js_kvvfs_size = function(which=""){
+ let sz = 0;
+ const kvinfo = __kvvfsInfo(which);
+ kvinfo.stores.forEach((s)=>{
+ let i;
+ for(i = 0; i < s.length; ++i){
+ const k = s.key(i);
+ if(k.startsWith(kvinfo.prefix)){
+ sz += k.length;
+ sz += s.getItem(k).length;
+ }
+ }
+ });
+ return sz * 2 /* because JS uses 2-byte char encoding */;
+ };
+
+ }/* main-window-only bits */
+
+ /**
+ Wraps all known variants of the C-side variadic
+ sqlite3_db_config().
+
+ Full docs: https://sqlite.org/c3ref/db_config.html
+
+ Returns capi.SQLITE_MISUSE if op is not a valid operation ID.
+
+ The variants which take `(int, int*)` arguments treat a
+ missing or falsy pointer argument as 0.
+ */
+ capi.sqlite3_db_config = function(pDb, op, ...args){
+ if(!this.s){
+ this.s = wasm.xWrap('sqlite3_wasm_db_config_s','int',
+ ['sqlite3*', 'int', 'string:static']
+ /* MAINDBNAME requires a static string */);
+ this.pii = wasm.xWrap('sqlite3_wasm_db_config_pii', 'int',
+ ['sqlite3*', 'int', '*','int', 'int']);
+ this.ip = wasm.xWrap('sqlite3_wasm_db_config_ip','int',
+ ['sqlite3*', 'int', 'int','*']);
+ }
+ switch(op){
+ case capi.SQLITE_DBCONFIG_ENABLE_FKEY:
+ case capi.SQLITE_DBCONFIG_ENABLE_TRIGGER:
+ case capi.SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER:
+ case capi.SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION:
+ case capi.SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE:
+ case capi.SQLITE_DBCONFIG_ENABLE_QPSG:
+ case capi.SQLITE_DBCONFIG_TRIGGER_EQP:
+ case capi.SQLITE_DBCONFIG_RESET_DATABASE:
+ case capi.SQLITE_DBCONFIG_DEFENSIVE:
+ case capi.SQLITE_DBCONFIG_WRITABLE_SCHEMA:
+ case capi.SQLITE_DBCONFIG_LEGACY_ALTER_TABLE:
+ case capi.SQLITE_DBCONFIG_DQS_DML:
+ case capi.SQLITE_DBCONFIG_DQS_DDL:
+ case capi.SQLITE_DBCONFIG_ENABLE_VIEW:
+ case capi.SQLITE_DBCONFIG_LEGACY_FILE_FORMAT:
+ case capi.SQLITE_DBCONFIG_TRUSTED_SCHEMA:
+ case capi.SQLITE_DBCONFIG_STMT_SCANSTATUS:
+ case capi.SQLITE_DBCONFIG_REVERSE_SCANORDER:
+ return this.ip(pDb, op, args[0], args[1] || 0);
+ case capi.SQLITE_DBCONFIG_LOOKASIDE:
+ return this.pii(pDb, op, args[0], args[1], args[2]);
+ case capi.SQLITE_DBCONFIG_MAINDBNAME:
+ return this.s(pDb, op, args[0]);
+ default:
+ return capi.SQLITE_MISUSE;
+ }
+ }.bind(Object.create(null));
+
+ /**
+ Given a (sqlite3_value*), this function attempts to convert it
+ to an equivalent JS value with as much fidelity as feasible and
+ return it.
+
+ By default it throws if it cannot determine any sensible
+ conversion. If passed a falsy second argument, it instead returns
+ `undefined` if no suitable conversion is found. Note that there
+ is no conversion from SQL to JS which results in the `undefined`
+ value, so `undefined` has an unambiguous meaning here. It will
+ always throw a WasmAllocError if allocating memory for a
+ conversion fails.
+
+ Caveats:
+
+ - It does not support sqlite3_value_to_pointer() conversions
+ because those require a type name string which this function
+ does not have and cannot sensibly be given at the level of the
+ API where this is used (e.g. automatically converting UDF
+ arguments). Clients using sqlite3_value_to_pointer(), and its
+ related APIs, will need to manage those themselves.
+ */
+ capi.sqlite3_value_to_js = function(pVal,throwIfCannotConvert=true){
+ let arg;
+ const valType = capi.sqlite3_value_type(pVal);
+ switch(valType){
+ case capi.SQLITE_INTEGER:
+ if(wasm.bigIntEnabled){
+ arg = capi.sqlite3_value_int64(pVal);
+ if(util.bigIntFitsDouble(arg)) arg = Number(arg);
+ }
+ else arg = capi.sqlite3_value_double(pVal)/*yes, double, for larger integers*/;
+ break;
+ case capi.SQLITE_FLOAT:
+ arg = capi.sqlite3_value_double(pVal);
+ break;
+ case capi.SQLITE_TEXT:
+ arg = capi.sqlite3_value_text(pVal);
+ break;
+ case capi.SQLITE_BLOB:{
+ const n = capi.sqlite3_value_bytes(pVal);
+ const pBlob = capi.sqlite3_value_blob(pVal);
+ if(n && !pBlob) sqlite3.WasmAllocError.toss(
+ "Cannot allocate memory for blob argument of",n,"byte(s)"
+ );
+ arg = n ? wasm.heap8u().slice(pBlob, pBlob + Number(n)) : null;
+ break;
+ }
+ case capi.SQLITE_NULL:
+ arg = null; break;
+ default:
+ if(throwIfCannotConvert){
+ toss3(capi.SQLITE_MISMATCH,
+ "Unhandled sqlite3_value_type():",valType);
+ }
+ arg = undefined;
+ }
+ return arg;
+ };
+
+ /**
+ Requires a C-style array of `sqlite3_value*` objects and the
+ number of entries in that array. Returns a JS array containing
+ the results of passing each C array entry to
+ sqlite3_value_to_js(). The 3rd argument to this function is
+ passed on as the 2nd argument to that one.
+ */
+ capi.sqlite3_values_to_js = function(argc,pArgv,throwIfCannotConvert=true){
+ let i;
+ const tgt = [];
+ for(i = 0; i < argc; ++i){
+ /**
+ Curiously: despite ostensibly requiring 8-byte
+ alignment, the pArgv array is parcelled into chunks of
+ 4 bytes (1 pointer each). The values those point to
+ have 8-byte alignment but the individual argv entries
+ do not.
+ */
+ tgt.push(capi.sqlite3_value_to_js(
+ wasm.peekPtr(pArgv + (wasm.ptrSizeof * i)),
+ throwIfCannotConvert
+ ));
+ }
+ return tgt;
+ };
+
+ /**
+ Calls either sqlite3_result_error_nomem(), if e is-a
+ WasmAllocError, or sqlite3_result_error(). In the latter case,
+ the second arugment is coerced to a string to create the error
+ message.
+
+ The first argument is a (sqlite3_context*). Returns void.
+ Does not throw.
+ */
+ capi.sqlite3_result_error_js = function(pCtx,e){
+ if(e instanceof WasmAllocError){
+ capi.sqlite3_result_error_nomem(pCtx);
+ }else{
+ /* Maintenance reminder: ''+e, rather than e.message,
+ will prefix e.message with e.name, so it includes
+ the exception's type name in the result. */;
+ capi.sqlite3_result_error(pCtx, ''+e, -1);
+ }
+ };
+
+ /**
+ This function passes its 2nd argument to one of the
+ sqlite3_result_xyz() routines, depending on the type of that
+ argument:
+
+ - If (val instanceof Error), this function passes it to
+ sqlite3_result_error_js().
+ - `null`: `sqlite3_result_null()`
+ - `boolean`: `sqlite3_result_int()` with a value of 0 or 1.
+ - `number`: `sqlite3_result_int()`, `sqlite3_result_int64()`, or
+ `sqlite3_result_double()`, depending on the range of the number
+ and whether or not int64 support is enabled.
+ - `bigint`: similar to `number` but will trigger an error if the
+ value is too big to store in an int64.
+ - `string`: `sqlite3_result_text()`
+ - Uint8Array or Int8Array or ArrayBuffer: `sqlite3_result_blob()`
+ - `undefined`: is a no-op provided to simplify certain use cases.
+
+ Anything else triggers `sqlite3_result_error()` with a
+ description of the problem.
+
+ The first argument to this function is a `(sqlite3_context*)`.
+ Returns void. Does not throw.
+ */
+ capi.sqlite3_result_js = function(pCtx,val){
+ if(val instanceof Error){
+ capi.sqlite3_result_error_js(pCtx, val);
+ return;
+ }
+ try{
+ switch(typeof val) {
+ case 'undefined':
+ /* This is a no-op. This routine originated in the create_function()
+ family of APIs and in that context, passing in undefined indicated
+ that the caller was responsible for calling sqlite3_result_xxx()
+ (if needed). */
+ break;
+ case 'boolean':
+ capi.sqlite3_result_int(pCtx, val ? 1 : 0);
+ break;
+ case 'bigint':
+ if(util.bigIntFits32(val)){
+ capi.sqlite3_result_int(pCtx, Number(val));
+ }else if(util.bigIntFitsDouble(val)){
+ capi.sqlite3_result_double(pCtx, Number(val));
+ }else if(wasm.bigIntEnabled){
+ if(util.bigIntFits64(val)) capi.sqlite3_result_int64(pCtx, val);
+ else toss3("BigInt value",val.toString(),"is too BigInt for int64.");
+ }else{
+ toss3("BigInt value",val.toString(),"is too BigInt.");
+ }
+ break;
+ case 'number': {
+ let f;
+ if(util.isInt32(val)){
+ f = capi.sqlite3_result_int;
+ }else if(wasm.bigIntEnabled
+ && Number.isInteger(val)
+ && util.bigIntFits64(BigInt(val))){
+ f = capi.sqlite3_result_int64;
+ }else{
+ f = capi.sqlite3_result_double;
+ }
+ f(pCtx, val);
+ break;
+ }
+ case 'string': {
+ const [p, n] = wasm.allocCString(val,true);
+ capi.sqlite3_result_text(pCtx, p, n, capi.SQLITE_WASM_DEALLOC);
+ break;
+ }
+ case 'object':
+ if(null===val/*yes, typeof null === 'object'*/) {
+ capi.sqlite3_result_null(pCtx);
+ break;
+ }else if(util.isBindableTypedArray(val)){
+ const pBlob = wasm.allocFromTypedArray(val);
+ capi.sqlite3_result_blob(
+ pCtx, pBlob, val.byteLength,
+ capi.SQLITE_WASM_DEALLOC
+ );
+ break;
+ }
+ // else fall through
+ default:
+ toss3("Don't not how to handle this UDF result value:",(typeof val), val);
+ }
+ }catch(e){
+ capi.sqlite3_result_error_js(pCtx, e);
+ }
+ };
+
+ /**
+ Returns the result sqlite3_column_value(pStmt,iCol) passed to
+ sqlite3_value_to_js(). The 3rd argument of this function is
+ ignored by this function except to pass it on as the second
+ argument of sqlite3_value_to_js(). If the sqlite3_column_value()
+ returns NULL (e.g. because the column index is out of range),
+ this function returns `undefined`, regardless of the 3rd
+ argument. If the 3rd argument is falsy and conversion fails,
+ `undefined` will be returned.
+
+ Note that sqlite3_column_value() returns an "unprotected" value
+ object, but in a single-threaded environment (like this one)
+ there is no distinction between protected and unprotected values.
+ */
+ capi.sqlite3_column_js = function(pStmt, iCol, throwIfCannotConvert=true){
+ const v = capi.sqlite3_column_value(pStmt, iCol);
+ return (0===v) ? undefined : capi.sqlite3_value_to_js(v, throwIfCannotConvert);
+ };
+
+ /**
+ Internal impl of sqlite3_preupdate_new/old_js() and
+ sqlite3changeset_new/old_js().
+ */
+ const __newOldValue = function(pObj, iCol, impl){
+ impl = capi[impl];
+ if(!this.ptr) this.ptr = wasm.allocPtr();
+ else wasm.pokePtr(this.ptr, 0);
+ const rc = impl(pObj, iCol, this.ptr);
+ if(rc) return SQLite3Error.toss(rc,arguments[2]+"() failed with code "+rc);
+ const pv = wasm.peekPtr(this.ptr);
+ return pv ? capi.sqlite3_value_to_js( pv, true ) : undefined;
+ }.bind(Object.create(null));
+
+ /**
+ A wrapper around sqlite3_preupdate_new() which fetches the
+ sqlite3_value at the given index and returns the result of
+ passing it to sqlite3_value_to_js(). Throws on error.
+ */
+ capi.sqlite3_preupdate_new_js =
+ (pDb, iCol)=>__newOldValue(pDb, iCol, 'sqlite3_preupdate_new');
+
+ /**
+ The sqlite3_preupdate_old() counterpart of
+ sqlite3_preupdate_new_js(), with an identical interface.
+ */
+ capi.sqlite3_preupdate_old_js =
+ (pDb, iCol)=>__newOldValue(pDb, iCol, 'sqlite3_preupdate_old');
+
+ /**
+ A wrapper around sqlite3changeset_new() which fetches the
+ sqlite3_value at the given index and returns the result of
+ passing it to sqlite3_value_to_js(). Throws on error.
+
+ If sqlite3changeset_new() succeeds but has no value to report,
+ this function returns the undefined value, noting that undefined
+ is a valid conversion from an `sqlite3_value`, so is unambiguous.
+ */
+ capi.sqlite3changeset_new_js =
+ (pChangesetIter, iCol) => __newOldValue(pChangesetIter, iCol,
+ 'sqlite3changeset_new');
+
+ /**
+ The sqlite3changeset_old() counterpart of
+ sqlite3changeset_new_js(), with an identical interface.
+ */
+ capi.sqlite3changeset_old_js =
+ (pChangesetIter, iCol)=>__newOldValue(pChangesetIter, iCol,
+ 'sqlite3changeset_old');
+
+ /* The remainder of the API will be set up in later steps. */
+ const sqlite3 = {
+ WasmAllocError: WasmAllocError,
+ SQLite3Error: SQLite3Error,
+ capi,
+ util,
+ wasm,
+ config,
+ /**
+ Holds the version info of the sqlite3 source tree from which
+ the generated sqlite3-api.js gets built. Note that its version
+ may well differ from that reported by sqlite3_libversion(), but
+ that should be considered a source file mismatch, as the JS and
+ WASM files are intended to be built and distributed together.
+
+ This object is initially a placeholder which gets replaced by a
+ build-generated object.
+ */
+ version: Object.create(null),
+
+ /**
+ The library reserves the 'client' property for client-side use
+ and promises to never define a property with this name nor to
+ ever rely on specific contents of it. It makes no such guarantees
+ for other properties.
+ */
+ client: undefined,
+
+ /**
+ This function is not part of the public interface, but a
+ piece of internal bootstrapping infrastructure.
+
+ Performs any optional asynchronous library-level initialization
+ which might be required. This function returns a Promise which
+ resolves to the sqlite3 namespace object. Any error in the
+ async init will be fatal to the init as a whole, but init
+ routines are themselves welcome to install dummy catch()
+ handlers which are not fatal if their failure should be
+ considered non-fatal. If called more than once, the second and
+ subsequent calls are no-ops which return a pre-resolved
+ Promise.
+
+ Ideally this function is called as part of the Promise chain
+ which handles the loading and bootstrapping of the API. If not
+ then it must be called by client-level code, which must not use
+ the library until the returned promise resolves.
+
+ If called multiple times it will return the same promise on
+ subsequent calls. The current build setup precludes that
+ possibility, so it's only a hypothetical problem if/when this
+ function ever needs to be invoked by clients.
+
+ In Emscripten-based builds, this function is called
+ automatically and deleted from this object.
+ */
+ asyncPostInit: async function ff(){
+ if(ff.isReady instanceof Promise) return ff.isReady;
+ let lia = sqlite3ApiBootstrap.initializersAsync;
+ delete sqlite3ApiBootstrap.initializersAsync;
+ const postInit = async ()=>{
+ if(!sqlite3.__isUnderTest){
+ /* Delete references to internal-only APIs which are used by
+ some initializers. Retain them when running in test mode
+ so that we can add tests for them. */
+ delete sqlite3.util;
+ /* It's conceivable that we might want to expose
+ StructBinder to client-side code, but it's only useful if
+ clients build their own sqlite3.wasm which contains their
+ own C struct types. */
+ delete sqlite3.StructBinder;
+ }
+ return sqlite3;
+ };
+ const catcher = (e)=>{
+ config.error("an async sqlite3 initializer failed:",e);
+ throw e;
+ };
+ if(!lia || !lia.length){
+ return ff.isReady = postInit().catch(catcher);
+ }
+ lia = lia.map((f)=>{
+ return (f instanceof Function) ? async x=>f(sqlite3) : f;
+ });
+ lia.push(postInit);
+ let p = Promise.resolve(sqlite3);
+ while(lia.length) p = p.then(lia.shift());
+ return ff.isReady = p.catch(catcher);
+ },
+ /**
+ scriptInfo ideally gets injected into this object by the
+ infrastructure which assembles the JS/WASM module. It contains
+ state which must be collected before sqlite3ApiBootstrap() can
+ be declared. It is not necessarily available to any
+ sqlite3ApiBootstrap.initializers but "should" be in place (if
+ it's added at all) by the time that
+ sqlite3ApiBootstrap.initializersAsync is processed.
+
+ This state is not part of the public API, only intended for use
+ with the sqlite3 API bootstrapping and wasm-loading process.
+ */
+ scriptInfo: undefined
+ };
+ try{
+ sqlite3ApiBootstrap.initializers.forEach((f)=>{
+ f(sqlite3);
+ });
+ }catch(e){
+ /* If we don't report this here, it can get completely swallowed
+ up and disappear into the abyss of Promises and Workers. */
+ console.error("sqlite3 bootstrap initializer threw:",e);
+ throw e;
+ }
+ delete sqlite3ApiBootstrap.initializers;
+ sqlite3ApiBootstrap.sqlite3 = sqlite3;
+ return sqlite3;
+}/*sqlite3ApiBootstrap()*/;
+/**
+ globalThis.sqlite3ApiBootstrap.initializers is an internal detail used by
+ the various pieces of the sqlite3 API's amalgamation process. It
+ must not be modified by client code except when plugging such code
+ into the amalgamation process.
+
+ Each component of the amalgamation is expected to append a function
+ to this array. When sqlite3ApiBootstrap() is called for the first
+ time, each such function will be called (in their appended order)
+ and passed the sqlite3 namespace object, into which they can install
+ their features (noting that most will also require that certain
+ features alread have been installed). At the end of that process,
+ this array is deleted.
+
+ Note that the order of insertion into this array is significant for
+ some pieces. e.g. sqlite3.capi and sqlite3.wasm cannot be fully
+ utilized until the whwasmutil.js part is plugged in via
+ sqlite3-api-glue.js.
+*/
+globalThis.sqlite3ApiBootstrap.initializers = [];
+/**
+ globalThis.sqlite3ApiBootstrap.initializersAsync is an internal detail
+ used by the sqlite3 API's amalgamation process. It must not be
+ modified by client code except when plugging such code into the
+ amalgamation process.
+
+ The counterpart of globalThis.sqlite3ApiBootstrap.initializers,
+ specifically for initializers which are asynchronous. All entries in
+ this list must be either async functions, non-async functions which
+ return a Promise, or a Promise. Each function in the list is called
+ with the sqlite3 object as its only argument.
+
+ The resolved value of any Promise is ignored and rejection will kill
+ the asyncPostInit() process (at an indeterminate point because all
+ of them are run asynchronously in parallel).
+
+ This list is not processed until the client calls
+ sqlite3.asyncPostInit(). This means, for example, that intializers
+ added to globalThis.sqlite3ApiBootstrap.initializers may push entries to
+ this list.
+*/
+globalThis.sqlite3ApiBootstrap.initializersAsync = [];
+/**
+ Client code may assign sqlite3ApiBootstrap.defaultConfig an
+ object-type value before calling sqlite3ApiBootstrap() (without
+ arguments) in order to tell that call to use this object as its
+ default config value. The intention of this is to provide
+ downstream clients with a reasonably flexible approach for plugging in
+ an environment-suitable configuration without having to define a new
+ global-scope symbol.
+*/
+globalThis.sqlite3ApiBootstrap.defaultConfig = Object.create(null);
+/**
+ Placeholder: gets installed by the first call to
+ globalThis.sqlite3ApiBootstrap(). However, it is recommended that the
+ caller of sqlite3ApiBootstrap() capture its return value and delete
+ globalThis.sqlite3ApiBootstrap after calling it. It returns the same
+ value which will be stored here.
+*/
+globalThis.sqlite3ApiBootstrap.sqlite3 = undefined;
diff --git a/ext/wasm/api/sqlite3-api-worker1.js b/ext/wasm/api/sqlite3-api-worker1.js
new file mode 100644
index 0000000..3099c19
--- /dev/null
+++ b/ext/wasm/api/sqlite3-api-worker1.js
@@ -0,0 +1,695 @@
+//#ifnot omit-oo1
+/**
+ 2022-07-22
+
+ 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 the initializer for SQLite's "Worker API #1", a
+ very basic DB access API intended to be scripted from a main window
+ thread via Worker-style messages. Because of limitations in that
+ type of communication, this API is minimalistic and only capable of
+ serving relatively basic DB requests (e.g. it cannot process nested
+ query loops concurrently).
+
+ This file requires that the core C-style sqlite3 API and OO API #1
+ have been loaded.
+*/
+
+/**
+ sqlite3.initWorker1API() implements a Worker-based wrapper around
+ SQLite3 OO API #1, colloquially known as "Worker API #1".
+
+ In order to permit this API to be loaded in worker threads without
+ automatically registering onmessage handlers, initializing the
+ worker API requires calling initWorker1API(). If this function is
+ called from a non-worker thread then it throws an exception. It
+ must only be called once per Worker.
+
+ When initialized, it installs message listeners to receive Worker
+ messages and then it posts a message in the form:
+
+ ```
+ {type:'sqlite3-api', result:'worker1-ready'}
+ ```
+
+ to let the client know that it has been initialized. Clients may
+ optionally depend on this function not returning until
+ initialization is complete, as the initialization is synchronous.
+ In some contexts, however, listening for the above message is
+ a better fit.
+
+ Note that the worker-based interface can be slightly quirky because
+ of its async nature. In particular, any number of messages may be posted
+ to the worker before it starts handling any of them. If, e.g., an
+ "open" operation fails, any subsequent messages will fail. The
+ Promise-based wrapper for this API (`sqlite3-worker1-promiser.js`)
+ is more comfortable to use in that regard.
+
+ The documentation for the input and output worker messages for
+ this API follows...
+
+ ====================================================================
+ Common message format...
+
+ Each message posted to the worker has an operation-independent
+ envelope and operation-dependent arguments:
+
+ ```
+ {
+ type: string, // one of: 'open', 'close', 'exec', 'export', 'config-get'
+
+ messageId: OPTIONAL arbitrary value. The worker will copy it as-is
+ into response messages to assist in client-side dispatching.
+
+ dbId: a db identifier string (returned by 'open') which tells the
+ operation which database instance to work on. If not provided, the
+ first-opened db is used. This is an "opaque" value, with no
+ inherently useful syntax or information. Its value is subject to
+ change with any given build of this API and cannot be used as a
+ basis for anything useful beyond its one intended purpose.
+
+ args: ...operation-dependent arguments...
+
+ // the framework may add other properties for testing or debugging
+ // purposes.
+
+ }
+ ```
+
+ Response messages, posted back to the main thread, look like:
+
+ ```
+ {
+ type: string. Same as above except for error responses, which have the type
+ 'error',
+
+ messageId: same value, if any, provided by the inbound message
+
+ dbId: the id of the db which was operated on, if any, as returned
+ by the corresponding 'open' operation.
+
+ result: ...operation-dependent result...
+
+ }
+ ```
+
+ ====================================================================
+ Error responses
+
+ Errors are reported messages in an operation-independent format:
+
+ ```
+ {
+ type: "error",
+
+ messageId: ...as above...,
+
+ dbId: ...as above...
+
+ result: {
+
+ operation: type of the triggering operation: 'open', 'close', ...
+
+ message: ...error message text...
+
+ errorClass: string. The ErrorClass.name property from the thrown exception.
+
+ input: the message object which triggered the error.
+
+ stack: _if available_, a stack trace array.
+
+ }
+
+ }
+ ```
+
+
+ ====================================================================
+ "config-get"
+
+ This operation fetches the serializable parts of the sqlite3 API
+ configuration.
+
+ Message format:
+
+ ```
+ {
+ type: "config-get",
+ messageId: ...as above...,
+ args: currently ignored and may be elided.
+ }
+ ```
+
+ Response:
+
+ ```
+ {
+ type: "config-get",
+ messageId: ...as above...,
+ result: {
+
+ version: sqlite3.version object
+
+ bigIntEnabled: bool. True if BigInt support is enabled.
+
+ vfsList: result of sqlite3.capi.sqlite3_js_vfs_list()
+ }
+ }
+ ```
+
+
+ ====================================================================
+ "open" a database
+
+ Message format:
+
+ ```
+ {
+ type: "open",
+ messageId: ...as above...,
+ args:{
+
+ filename [=":memory:" or "" (unspecified)]: the db filename.
+ See the sqlite3.oo1.DB constructor for peculiarities and
+ transformations,
+
+ vfs: sqlite3_vfs name. Ignored if filename is ":memory:" or "".
+ This may change how the given filename is resolved.
+ }
+ }
+ ```
+
+ Response:
+
+ ```
+ {
+ type: "open",
+ messageId: ...as above...,
+ result: {
+ filename: db filename, possibly differing from the input.
+
+ dbId: an opaque ID value which must be passed in the message
+ envelope to other calls in this API to tell them which db to
+ use. If it is not provided to future calls, they will default to
+ operating on the least-recently-opened db. This property is, for
+ API consistency's sake, also part of the containing message
+ envelope. Only the `open` operation includes it in the `result`
+ property.
+
+ persistent: true if the given filename resides in the
+ known-persistent storage, else false.
+
+ vfs: name of the VFS the "main" db is using.
+ }
+ }
+ ```
+
+ ====================================================================
+ "close" a database
+
+ Message format:
+
+ ```
+ {
+ type: "close",
+ messageId: ...as above...
+ dbId: ...as above...
+ args: OPTIONAL {unlink: boolean}
+ }
+ ```
+
+ If the `dbId` does not refer to an opened ID, this is a no-op. If
+ the `args` object contains a truthy `unlink` value then the database
+ will be unlinked (deleted) after closing it. The inability to close a
+ db (because it's not opened) or delete its file does not trigger an
+ error.
+
+ Response:
+
+ ```
+ {
+ type: "close",
+ messageId: ...as above...,
+ result: {
+
+ filename: filename of closed db, or undefined if no db was closed
+
+ }
+ }
+ ```
+
+ ====================================================================
+ "exec" SQL
+
+ All SQL execution is processed through the exec operation. It offers
+ most of the features of the oo1.DB.exec() method, with a few limitations
+ imposed by the state having to cross thread boundaries.
+
+ Message format:
+
+ ```
+ {
+ type: "exec",
+ messageId: ...as above...
+ dbId: ...as above...
+ args: string (SQL) or {... see below ...}
+ }
+ ```
+
+ Response:
+
+ ```
+ {
+ type: "exec",
+ messageId: ...as above...,
+ dbId: ...as above...
+ result: {
+ input arguments, possibly modified. See below.
+ }
+ }
+ ```
+
+ The arguments are in the same form accepted by oo1.DB.exec(), with
+ the exceptions noted below.
+
+ If the `countChanges` arguments property (added in version 3.43) is
+ truthy then the `result` property contained by the returned object
+ will have a `changeCount` property which holds the number of changes
+ made by the provided SQL. Because the SQL may contain an arbitrary
+ number of statements, the `changeCount` is calculated by calling
+ `sqlite3_total_changes()` before and after the SQL is evaluated. If
+ the value of `countChanges` is 64 then the `changeCount` property
+ will be returned as a 64-bit integer in the form of a BigInt (noting
+ that that will trigger an exception if used in a BigInt-incapable
+ build). In the latter case, the number of changes is calculated by
+ calling `sqlite3_total_changes64()` before and after the SQL is
+ evaluated.
+
+ A function-type args.callback property cannot cross
+ the window/Worker boundary, so is not useful here. If
+ args.callback is a string then it is assumed to be a
+ message type key, in which case a callback function will be
+ applied which posts each row result via:
+
+ postMessage({type: thatKeyType,
+ rowNumber: 1-based-#,
+ row: theRow,
+ columnNames: anArray
+ })
+
+ And, at the end of the result set (whether or not any result rows
+ were produced), it will post an identical message with
+ (row=undefined, rowNumber=null) to alert the caller than the result
+ set is completed. Note that a row value of `null` is a legal row
+ result for certain arg.rowMode values.
+
+ (Design note: we don't use (row=undefined, rowNumber=undefined) to
+ indicate end-of-results because fetching those would be
+ indistinguishable from fetching from an empty object unless the
+ client used hasOwnProperty() (or similar) to distinguish "missing
+ property" from "property with the undefined value". Similarly,
+ `null` is a legal value for `row` in some case , whereas the db
+ layer won't emit a result value of `undefined`.)
+
+ The callback proxy must not recurse into this interface. An exec()
+ call will tie up the Worker thread, causing any recursion attempt
+ to wait until the first exec() is completed.
+
+ The response is the input options object (or a synthesized one if
+ passed only a string), noting that options.resultRows and
+ options.columnNames may be populated by the call to db.exec().
+
+
+ ====================================================================
+ "export" the current db
+
+ To export the underlying database as a byte array...
+
+ Message format:
+
+ ```
+ {
+ type: "export",
+ messageId: ...as above...,
+ dbId: ...as above...
+ }
+ ```
+
+ Response:
+
+ ```
+ {
+ type: "export",
+ messageId: ...as above...,
+ dbId: ...as above...
+ result: {
+ byteArray: Uint8Array (as per sqlite3_js_db_export()),
+ filename: the db filename,
+ mimetype: "application/x-sqlite3"
+ }
+ }
+ ```
+
+*/
+globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
+sqlite3.initWorker1API = function(){
+ 'use strict';
+ const toss = (...args)=>{throw new Error(args.join(' '))};
+ if(!(globalThis.WorkerGlobalScope instanceof Function)){
+ toss("initWorker1API() must be run from a Worker thread.");
+ }
+ const sqlite3 = this.sqlite3 || toss("Missing this.sqlite3 object.");
+ const DB = sqlite3.oo1.DB;
+
+ /**
+ Returns the app-wide unique ID for the given db, creating one if
+ needed.
+ */
+ const getDbId = function(db){
+ let id = wState.idMap.get(db);
+ if(id) return id;
+ id = 'db#'+(++wState.idSeq)+'@'+db.pointer;
+ /** ^^^ can't simply use db.pointer b/c closing/opening may re-use
+ the same address, which could map pending messages to a wrong
+ instance. */
+ wState.idMap.set(db, id);
+ return id;
+ };
+
+ /**
+ Internal helper for managing Worker-level state.
+ */
+ const wState = {
+ /**
+ Each opened DB is added to this.dbList, and the first entry in
+ that list is the default db. As each db is closed, its entry is
+ removed from the list.
+ */
+ dbList: [],
+ /** Sequence number of dbId generation. */
+ idSeq: 0,
+ /** Map of DB instances to dbId. */
+ idMap: new WeakMap,
+ /** Temp holder for "transferable" postMessage() state. */
+ xfer: [],
+ open: function(opt){
+ const db = new DB(opt);
+ this.dbs[getDbId(db)] = db;
+ if(this.dbList.indexOf(db)<0) this.dbList.push(db);
+ return db;
+ },
+ close: function(db,alsoUnlink){
+ if(db){
+ delete this.dbs[getDbId(db)];
+ const filename = db.filename;
+ const pVfs = sqlite3.wasm.sqlite3_wasm_db_vfs(db.pointer, 0);
+ db.close();
+ const ddNdx = this.dbList.indexOf(db);
+ if(ddNdx>=0) this.dbList.splice(ddNdx, 1);
+ if(alsoUnlink && filename && pVfs){
+ sqlite3.wasm.sqlite3_wasm_vfs_unlink(pVfs, filename);
+ }
+ }
+ },
+ /**
+ Posts the given worker message value. If xferList is provided,
+ it must be an array, in which case a copy of it passed as
+ postMessage()'s second argument and xferList.length is set to
+ 0.
+ */
+ post: function(msg,xferList){
+ if(xferList && xferList.length){
+ globalThis.postMessage( msg, Array.from(xferList) );
+ xferList.length = 0;
+ }else{
+ globalThis.postMessage(msg);
+ }
+ },
+ /** Map of DB IDs to DBs. */
+ dbs: Object.create(null),
+ /** Fetch the DB for the given id. Throw if require=true and the
+ id is not valid, else return the db or undefined. */
+ getDb: function(id,require=true){
+ return this.dbs[id]
+ || (require ? toss("Unknown (or closed) DB ID:",id) : undefined);
+ }
+ };
+
+ /** Throws if the given db is falsy or not opened, else returns its
+ argument. */
+ const affirmDbOpen = function(db = wState.dbList[0]){
+ return (db && db.pointer) ? db : toss("DB is not opened.");
+ };
+
+ /** Extract dbId from the given message payload. */
+ const getMsgDb = function(msgData,affirmExists=true){
+ const db = wState.getDb(msgData.dbId,false) || wState.dbList[0];
+ return affirmExists ? affirmDbOpen(db) : db;
+ };
+
+ const getDefaultDbId = function(){
+ return wState.dbList[0] && getDbId(wState.dbList[0]);
+ };
+
+ const guessVfs = function(filename){
+ const m = /^file:.+(vfs=(\w+))/.exec(filename);
+ return sqlite3.capi.sqlite3_vfs_find(m ? m[2] : 0);
+ };
+
+ const isSpecialDbFilename = (n)=>{
+ return ""===n || ':'===n[0];
+ };
+
+ /**
+ A level of "organizational abstraction" for the Worker1
+ API. Each method in this object must map directly to a Worker1
+ message type key. The onmessage() dispatcher attempts to
+ dispatch all inbound messages to a method of this object,
+ passing it the event.data part of the inbound event object. All
+ methods must return a plain Object containing any result
+ state, which the dispatcher may amend. All methods must throw
+ on error.
+ */
+ const wMsgHandler = {
+ open: function(ev){
+ const oargs = Object.create(null), args = (ev.args || Object.create(null));
+ if(args.simulateError){ // undocumented internal testing option
+ toss("Throwing because of simulateError flag.");
+ }
+ const rc = Object.create(null);
+ let byteArray, pVfs;
+ oargs.vfs = args.vfs;
+ if(isSpecialDbFilename(args.filename)){
+ oargs.filename = args.filename || "";
+ }else{
+ oargs.filename = args.filename;
+ byteArray = args.byteArray;
+ if(byteArray) pVfs = guessVfs(args.filename);
+ }
+ if(pVfs){
+ /* 2022-11-02: this feature is as-yet untested except that
+ sqlite3_wasm_vfs_create_file() has been tested from the
+ browser dev console. */
+ let pMem;
+ try{
+ pMem = sqlite3.wasm.allocFromTypedArray(byteArray);
+ const rc = sqlite3.wasm.sqlite3_wasm_vfs_create_file(
+ pVfs, oargs.filename, pMem, byteArray.byteLength
+ );
+ if(rc) sqlite3.SQLite3Error.toss(rc);
+ }catch(e){
+ throw new sqlite3.SQLite3Error(
+ e.name+' creating '+args.filename+": "+e.message, {
+ cause: e
+ }
+ );
+ }finally{
+ if(pMem) sqlite3.wasm.dealloc(pMem);
+ }
+ }
+ const db = wState.open(oargs);
+ rc.filename = db.filename;
+ rc.persistent = !!sqlite3.capi.sqlite3_js_db_uses_vfs(db.pointer, "opfs");
+ rc.dbId = getDbId(db);
+ rc.vfs = db.dbVfsName();
+ return rc;
+ },
+
+ close: function(ev){
+ const db = getMsgDb(ev,false);
+ const response = {
+ filename: db && db.filename
+ };
+ if(db){
+ const doUnlink = ((ev.args && 'object'===typeof ev.args)
+ ? !!ev.args.unlink : false);
+ wState.close(db, doUnlink);
+ }
+ return response;
+ },
+
+ exec: function(ev){
+ const rc = (
+ 'string'===typeof ev.args
+ ) ? {sql: ev.args} : (ev.args || Object.create(null));
+ if('stmt'===rc.rowMode){
+ toss("Invalid rowMode for 'exec': stmt mode",
+ "does not work in the Worker API.");
+ }else if(!rc.sql){
+ toss("'exec' requires input SQL.");
+ }
+ const db = getMsgDb(ev);
+ if(rc.callback || Array.isArray(rc.resultRows)){
+ // Part of a copy-avoidance optimization for blobs
+ db._blobXfer = wState.xfer;
+ }
+ const theCallback = rc.callback;
+ let rowNumber = 0;
+ const hadColNames = !!rc.columnNames;
+ if('string' === typeof theCallback){
+ if(!hadColNames) rc.columnNames = [];
+ /* Treat this as a worker message type and post each
+ row as a message of that type. */
+ rc.callback = function(row,stmt){
+ wState.post({
+ type: theCallback,
+ columnNames: rc.columnNames,
+ rowNumber: ++rowNumber,
+ row: row
+ }, wState.xfer);
+ }
+ }
+ try {
+ const changeCount = !!rc.countChanges
+ ? db.changes(true,(64===rc.countChanges))
+ : undefined;
+ db.exec(rc);
+ if(undefined !== changeCount){
+ rc.changeCount = db.changes(true,64===rc.countChanges) - changeCount;
+ }
+ if(rc.callback instanceof Function){
+ rc.callback = theCallback;
+ /* Post a sentinel message to tell the client that the end
+ of the result set has been reached (possibly with zero
+ rows). */
+ wState.post({
+ type: theCallback,
+ columnNames: rc.columnNames,
+ rowNumber: null /*null to distinguish from "property not set"*/,
+ row: undefined /*undefined because null is a legal row value
+ for some rowType values, but undefined is not*/
+ });
+ }
+ }finally{
+ delete db._blobXfer;
+ if(rc.callback) rc.callback = theCallback;
+ }
+ return rc;
+ }/*exec()*/,
+
+ 'config-get': function(){
+ const rc = Object.create(null), src = sqlite3.config;
+ [
+ 'bigIntEnabled'
+ ].forEach(function(k){
+ if(Object.getOwnPropertyDescriptor(src, k)) rc[k] = src[k];
+ });
+ rc.version = sqlite3.version;
+ rc.vfsList = sqlite3.capi.sqlite3_js_vfs_list();
+ rc.opfsEnabled = !!sqlite3.opfs;
+ return rc;
+ },
+
+ /**
+ Exports the database to a byte array, as per
+ sqlite3_serialize(). Response is an object:
+
+ {
+ byteArray: Uint8Array (db file contents),
+ filename: the current db filename,
+ mimetype: 'application/x-sqlite3'
+ }
+ */
+ export: function(ev){
+ const db = getMsgDb(ev);
+ const response = {
+ byteArray: sqlite3.capi.sqlite3_js_db_export(db.pointer),
+ filename: db.filename,
+ mimetype: 'application/x-sqlite3'
+ };
+ wState.xfer.push(response.byteArray.buffer);
+ return response;
+ }/*export()*/,
+
+ toss: function(ev){
+ toss("Testing worker exception");
+ },
+
+ 'opfs-tree': async function(ev){
+ if(!sqlite3.opfs) toss("OPFS support is unavailable.");
+ const response = await sqlite3.opfs.treeList();
+ return response;
+ }
+ }/*wMsgHandler*/;
+
+ globalThis.onmessage = async function(ev){
+ ev = ev.data;
+ let result, dbId = ev.dbId, evType = ev.type;
+ const arrivalTime = performance.now();
+ try {
+ if(wMsgHandler.hasOwnProperty(evType) &&
+ wMsgHandler[evType] instanceof Function){
+ result = await wMsgHandler[evType](ev);
+ }else{
+ toss("Unknown db worker message type:",ev.type);
+ }
+ }catch(err){
+ evType = 'error';
+ result = {
+ operation: ev.type,
+ message: err.message,
+ errorClass: err.name,
+ input: ev
+ };
+ if(err.stack){
+ result.stack = ('string'===typeof err.stack)
+ ? err.stack.split(/\n\s*/) : err.stack;
+ }
+ if(0) sqlite3.config.warn("Worker is propagating an exception to main thread.",
+ "Reporting it _here_ for the stack trace:",err,result);
+ }
+ if(!dbId){
+ dbId = result.dbId/*from 'open' cmd*/
+ || getDefaultDbId();
+ }
+ // Timing info is primarily for use in testing this API. It's not part of
+ // the public API. arrivalTime = when the worker got the message.
+ wState.post({
+ type: evType,
+ dbId: dbId,
+ messageId: ev.messageId,
+ workerReceivedTime: arrivalTime,
+ workerRespondTime: performance.now(),
+ departureTime: ev.departureTime,
+ // TODO: move the timing bits into...
+ //timing:{
+ // departure: ev.departureTime,
+ // workerReceived: arrivalTime,
+ // workerResponse: performance.now();
+ //},
+ result: result
+ }, wState.xfer);
+ };
+ globalThis.postMessage({type:'sqlite3-api',result:'worker1-ready'});
+}.bind({sqlite3});
+});
+//#else
+/* Built with the omit-oo1 flag. */
+//#endif ifnot omit-oo1
diff --git a/ext/wasm/api/sqlite3-license-version-header.js b/ext/wasm/api/sqlite3-license-version-header.js
new file mode 100644
index 0000000..4829894
--- /dev/null
+++ b/ext/wasm/api/sqlite3-license-version-header.js
@@ -0,0 +1,25 @@
+/*
+** LICENSE for the sqlite3 WebAssembly/JavaScript APIs.
+**
+** This bundle (typically released as sqlite3.js or sqlite3.mjs)
+** is an amalgamation of JavaScript source code from two projects:
+**
+** 1) https://emscripten.org: the Emscripten "glue code" is covered by
+** the terms of the MIT license and University of Illinois/NCSA
+** Open Source License, as described at:
+**
+** https://emscripten.org/docs/introducing_emscripten/emscripten_license.html
+**
+** 2) https://sqlite.org: all code and documentation labeled as being
+** from this source are released under the same terms as the sqlite3
+** C library:
+**
+** 2022-10-16
+**
+** The author disclaims copyright to this source code. In place of a
+** legal notice, here is a blessing:
+**
+** * May you do good and not evil.
+** * May you find forgiveness for yourself and forgive others.
+** * May you share freely, never taking more than you give.
+*/
diff --git a/ext/wasm/api/sqlite3-opfs-async-proxy.js b/ext/wasm/api/sqlite3-opfs-async-proxy.js
new file mode 100644
index 0000000..cafd296
--- /dev/null
+++ b/ext/wasm/api/sqlite3-opfs-async-proxy.js
@@ -0,0 +1,915 @@
+/*
+ 2022-09-16
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ A Worker which manages asynchronous OPFS handles on behalf of a
+ synchronous API which controls it via a combination of Worker
+ messages, SharedArrayBuffer, and Atomics. It is the asynchronous
+ counterpart of the API defined in sqlite3-vfs-opfs.js.
+
+ Highly indebted to:
+
+ https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/OriginPrivateFileSystemVFS.js
+
+ for demonstrating how to use the OPFS APIs.
+
+ This file is to be loaded as a Worker. It does not have any direct
+ access to the sqlite3 JS/WASM bits, so any bits which it needs (most
+ notably SQLITE_xxx integer codes) have to be imported into it via an
+ initialization process.
+
+ This file represents an implementation detail of a larger piece of
+ code, and not a public interface. Its details may change at any time
+ and are not intended to be used by any client-level code.
+
+ 2022-11-27: Chrome v108 changes some async methods to synchronous, as
+ documented at:
+
+ https://developer.chrome.com/blog/sync-methods-for-accesshandles/
+
+ Firefox v111 and Safari 16.4, both released in March 2023, also
+ include this.
+
+ We cannot change to the sync forms at this point without breaking
+ clients who use Chrome v104-ish or higher. truncate(), getSize(),
+ flush(), and close() are now (as of v108) synchronous. Calling them
+ with an "await", as we have to for the async forms, is still legal
+ with the sync forms but is superfluous. Calling the async forms with
+ theFunc().then(...) is not compatible with the change to
+ synchronous, but we do do not use those APIs that way. i.e. we don't
+ _need_ to change anything for this, but at some point (after Chrome
+ versions (approximately) 104-107 are extinct) should change our
+ usage of those methods to remove the "await".
+*/
+"use strict";
+const wPost = (type,...args)=>postMessage({type, payload:args});
+const installAsyncProxy = function(self){
+ const toss = function(...args){throw new Error(args.join(' '))};
+ if(globalThis.window === globalThis){
+ toss("This code cannot run from the main thread.",
+ "Load it as a Worker from a separate Worker.");
+ }else if(!navigator?.storage?.getDirectory){
+ toss("This API requires navigator.storage.getDirectory.");
+ }
+
+ /**
+ Will hold state copied to this object from the syncronous side of
+ this API.
+ */
+ const state = Object.create(null);
+
+ /**
+ verbose:
+
+ 0 = no logging output
+ 1 = only errors
+ 2 = warnings and errors
+ 3 = debug, warnings, and errors
+ */
+ state.verbose = 1;
+
+ const loggers = {
+ 0:console.error.bind(console),
+ 1:console.warn.bind(console),
+ 2:console.log.bind(console)
+ };
+ const logImpl = (level,...args)=>{
+ if(state.verbose>level) loggers[level]("OPFS asyncer:",...args);
+ };
+ const log = (...args)=>logImpl(2, ...args);
+ const warn = (...args)=>logImpl(1, ...args);
+ const error = (...args)=>logImpl(0, ...args);
+ const metrics = Object.create(null);
+ metrics.reset = ()=>{
+ let k;
+ const r = (m)=>(m.count = m.time = m.wait = 0);
+ for(k in state.opIds){
+ r(metrics[k] = Object.create(null));
+ }
+ let s = metrics.s11n = Object.create(null);
+ s = s.serialize = Object.create(null);
+ s.count = s.time = 0;
+ s = metrics.s11n.deserialize = Object.create(null);
+ s.count = s.time = 0;
+ };
+ metrics.dump = ()=>{
+ let k, n = 0, t = 0, w = 0;
+ for(k in state.opIds){
+ const m = metrics[k];
+ n += m.count;
+ t += m.time;
+ w += m.wait;
+ m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
+ }
+ console.log(globalThis?.location?.href,
+ "metrics for",globalThis?.location?.href,":\n",
+ metrics,
+ "\nTotal of",n,"op(s) for",t,"ms",
+ "approx",w,"ms spent waiting on OPFS APIs.");
+ console.log("Serialization metrics:",metrics.s11n);
+ };
+
+ /**
+ __openFiles is a map of sqlite3_file pointers (integers) to
+ metadata related to a given OPFS file handles. The pointers are, in
+ this side of the interface, opaque file handle IDs provided by the
+ synchronous part of this constellation. Each value is an object
+ with a structure demonstrated in the xOpen() impl.
+ */
+ const __openFiles = Object.create(null);
+ /**
+ __implicitLocks is a Set of sqlite3_file pointers (integers) which were
+ "auto-locked". i.e. those for which we obtained a sync access
+ handle without an explicit xLock() call. Such locks will be
+ released during db connection idle time, whereas a sync access
+ handle obtained via xLock(), or subsequently xLock()'d after
+ auto-acquisition, will not be released until xUnlock() is called.
+
+ Maintenance reminder: if we relinquish auto-locks at the end of the
+ operation which acquires them, we pay a massive performance
+ penalty: speedtest1 benchmarks take up to 4x as long. By delaying
+ the lock release until idle time, the hit is negligible.
+ */
+ const __implicitLocks = new Set();
+
+ /**
+ Expects an OPFS file path. It gets resolved, such that ".."
+ components are properly expanded, and returned. If the 2nd arg is
+ true, the result is returned as an array of path elements, else an
+ absolute path string is returned.
+ */
+ const getResolvedPath = function(filename,splitIt){
+ const p = new URL(
+ filename, 'file://irrelevant'
+ ).pathname;
+ return splitIt ? p.split('/').filter((v)=>!!v) : p;
+ };
+
+ /**
+ Takes the absolute path to a filesystem element. Returns an array
+ of [handleOfContainingDir, filename]. If the 2nd argument is truthy
+ then each directory element leading to the file is created along
+ the way. Throws if any creation or resolution fails.
+ */
+ const getDirForFilename = async function f(absFilename, createDirs = false){
+ const path = getResolvedPath(absFilename, true);
+ const filename = path.pop();
+ let dh = state.rootDir;
+ for(const dirName of path){
+ if(dirName){
+ dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs});
+ }
+ }
+ return [dh, filename];
+ };
+
+ /**
+ If the given file-holding object has a sync handle attached to it,
+ that handle is remove and asynchronously closed. Though it may
+ sound sensible to continue work as soon as the close() returns
+ (noting that it's asynchronous), doing so can cause operations
+ performed soon afterwards, e.g. a call to getSyncHandle() to fail
+ because they may happen out of order from the close(). OPFS does
+ not guaranty that the actual order of operations is retained in
+ such cases. i.e. always "await" on the result of this function.
+ */
+ const closeSyncHandle = async (fh)=>{
+ if(fh.syncHandle){
+ log("Closing sync handle for",fh.filenameAbs);
+ const h = fh.syncHandle;
+ delete fh.syncHandle;
+ delete fh.xLock;
+ __implicitLocks.delete(fh.fid);
+ return h.close();
+ }
+ };
+
+ /**
+ A proxy for closeSyncHandle() which is guaranteed to not throw.
+
+ This function is part of a lock/unlock step in functions which
+ require a sync access handle but may be called without xLock()
+ having been called first. Such calls need to release that
+ handle to avoid locking the file for all of time. This is an
+ _attempt_ at reducing cross-tab contention but it may prove
+ to be more of a problem than a solution and may need to be
+ removed.
+ */
+ const closeSyncHandleNoThrow = async (fh)=>{
+ try{await closeSyncHandle(fh)}
+ catch(e){
+ warn("closeSyncHandleNoThrow() ignoring:",e,fh);
+ }
+ };
+
+ /* Release all auto-locks. */
+ const releaseImplicitLocks = async ()=>{
+ if(__implicitLocks.size){
+ /* Release all auto-locks. */
+ for(const fid of __implicitLocks){
+ const fh = __openFiles[fid];
+ await closeSyncHandleNoThrow(fh);
+ log("Auto-unlocked",fid,fh.filenameAbs);
+ }
+ }
+ };
+
+ /**
+ An experiment in improving concurrency by freeing up implicit locks
+ sooner. This is known to impact performance dramatically but it has
+ also shown to improve concurrency considerably.
+
+ If fh.releaseImplicitLocks is truthy and fh is in __implicitLocks,
+ this routine returns closeSyncHandleNoThrow(), else it is a no-op.
+ */
+ const releaseImplicitLock = async (fh)=>{
+ if(fh.releaseImplicitLocks && __implicitLocks.has(fh.fid)){
+ return closeSyncHandleNoThrow(fh);
+ }
+ };
+
+ /**
+ An error class specifically for use with getSyncHandle(), the goal
+ of which is to eventually be able to distinguish unambiguously
+ between locking-related failures and other types, noting that we
+ cannot currently do so because createSyncAccessHandle() does not
+ define its exceptions in the required level of detail.
+
+ 2022-11-29: according to:
+
+ https://github.com/whatwg/fs/pull/21
+
+ NoModificationAllowedError will be the standard exception thrown
+ when acquisition of a sync access handle fails due to a locking
+ error. As of this writing, that error type is not visible in the
+ dev console in Chrome v109, nor is it documented in MDN, but an
+ error with that "name" property is being thrown from the OPFS
+ layer.
+ */
+ class GetSyncHandleError extends Error {
+ constructor(errorObject, ...msg){
+ super([
+ ...msg, ': '+errorObject.name+':',
+ errorObject.message
+ ].join(' '), {
+ cause: errorObject
+ });
+ this.name = 'GetSyncHandleError';
+ }
+ };
+ GetSyncHandleError.convertRc = (e,rc)=>{
+ if(1){
+ return (
+ e instanceof GetSyncHandleError
+ && ((e.cause.name==='NoModificationAllowedError')
+ /* Inconsistent exception.name from Chrome/ium with the
+ same exception.message text: */
+ || (e.cause.name==='DOMException'
+ && 0===e.cause.message.indexOf('Access Handles cannot')))
+ ) ? (
+ /*console.warn("SQLITE_BUSY",e),*/
+ state.sq3Codes.SQLITE_BUSY
+ ) : rc;
+ }else{
+ return rc;
+ }
+ }
+ /**
+ Returns the sync access handle associated with the given file
+ handle object (which must be a valid handle object, as created by
+ xOpen()), lazily opening it if needed.
+
+ In order to help alleviate cross-tab contention for a dabase, if
+ an exception is thrown while acquiring the handle, this routine
+ will wait briefly and try again, up to some fixed number of
+ times. If acquisition still fails at that point it will give up
+ and propagate the exception. Client-level code will see that as
+ an I/O error.
+ */
+ const getSyncHandle = async (fh,opName)=>{
+ if(!fh.syncHandle){
+ const t = performance.now();
+ log("Acquiring sync handle for",fh.filenameAbs);
+ const maxTries = 6,
+ msBase = state.asyncIdleWaitTime * 2;
+ let i = 1, ms = msBase;
+ for(; true; ms = msBase * ++i){
+ try {
+ //if(i<3) toss("Just testing getSyncHandle() wait-and-retry.");
+ //TODO? A config option which tells it to throw here
+ //randomly every now and then, for testing purposes.
+ fh.syncHandle = await fh.fileHandle.createSyncAccessHandle();
+ break;
+ }catch(e){
+ if(i === maxTries){
+ throw new GetSyncHandleError(
+ e, "Error getting sync handle for",opName+"().",maxTries,
+ "attempts failed.",fh.filenameAbs
+ );
+ }
+ warn("Error getting sync handle for",opName+"(). Waiting",ms,
+ "ms and trying again.",fh.filenameAbs,e);
+ Atomics.wait(state.sabOPView, state.opIds.retry, 0, ms);
+ }
+ }
+ log("Got",opName+"() sync handle for",fh.filenameAbs,
+ 'in',performance.now() - t,'ms');
+ if(!fh.xLock){
+ __implicitLocks.add(fh.fid);
+ log("Acquired implicit lock for",opName+"()",fh.fid,fh.filenameAbs);
+ }
+ }
+ return fh.syncHandle;
+ };
+
+ /**
+ Stores the given value at state.sabOPView[state.opIds.rc] and then
+ Atomics.notify()'s it.
+ */
+ const storeAndNotify = (opName, value)=>{
+ log(opName+"() => notify(",value,")");
+ Atomics.store(state.sabOPView, state.opIds.rc, value);
+ Atomics.notify(state.sabOPView, state.opIds.rc);
+ };
+
+ /**
+ Throws if fh is a file-holding object which is flagged as read-only.
+ */
+ const affirmNotRO = function(opName,fh){
+ if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs);
+ };
+
+ /**
+ We track 2 different timers: the "metrics" timer records how much
+ time we spend performing work. The "wait" timer records how much
+ time we spend waiting on the underlying OPFS timer. See the calls
+ to mTimeStart(), mTimeEnd(), wTimeStart(), and wTimeEnd()
+ throughout this file to see how they're used.
+ */
+ const __mTimer = Object.create(null);
+ __mTimer.op = undefined;
+ __mTimer.start = undefined;
+ const mTimeStart = (op)=>{
+ __mTimer.start = performance.now();
+ __mTimer.op = op;
+ //metrics[op] || toss("Maintenance required: missing metrics for",op);
+ ++metrics[op].count;
+ };
+ const mTimeEnd = ()=>(
+ metrics[__mTimer.op].time += performance.now() - __mTimer.start
+ );
+ const __wTimer = Object.create(null);
+ __wTimer.op = undefined;
+ __wTimer.start = undefined;
+ const wTimeStart = (op)=>{
+ __wTimer.start = performance.now();
+ __wTimer.op = op;
+ //metrics[op] || toss("Maintenance required: missing metrics for",op);
+ };
+ const wTimeEnd = ()=>(
+ metrics[__wTimer.op].wait += performance.now() - __wTimer.start
+ );
+
+ /**
+ Gets set to true by the 'opfs-async-shutdown' command to quit the
+ wait loop. This is only intended for debugging purposes: we cannot
+ inspect this file's state while the tight waitLoop() is running and
+ need a way to stop that loop for introspection purposes.
+ */
+ let flagAsyncShutdown = false;
+
+ /**
+ Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods
+ methods, as well as helpers like mkdir(). Maintenance reminder:
+ members are in alphabetical order to simplify finding them.
+ */
+ const vfsAsyncImpls = {
+ 'opfs-async-metrics': async ()=>{
+ mTimeStart('opfs-async-metrics');
+ metrics.dump();
+ storeAndNotify('opfs-async-metrics', 0);
+ mTimeEnd();
+ },
+ 'opfs-async-shutdown': async ()=>{
+ flagAsyncShutdown = true;
+ storeAndNotify('opfs-async-shutdown', 0);
+ },
+ mkdir: async (dirname)=>{
+ mTimeStart('mkdir');
+ let rc = 0;
+ wTimeStart('mkdir');
+ try {
+ await getDirForFilename(dirname+"/filepart", true);
+ }catch(e){
+ state.s11n.storeException(2,e);
+ rc = state.sq3Codes.SQLITE_IOERR;
+ }finally{
+ wTimeEnd();
+ }
+ storeAndNotify('mkdir', rc);
+ mTimeEnd();
+ },
+ xAccess: async (filename)=>{
+ mTimeStart('xAccess');
+ /* OPFS cannot support the full range of xAccess() queries
+ sqlite3 calls for. We can essentially just tell if the file
+ is accessible, but if it is then it's automatically writable
+ (unless it's locked, which we cannot(?) know without trying
+ to open it). OPFS does not have the notion of read-only.
+
+ The return semantics of this function differ from sqlite3's
+ xAccess semantics because we are limited in what we can
+ communicate back to our synchronous communication partner: 0 =
+ accessible, non-0 means not accessible.
+ */
+ let rc = 0;
+ wTimeStart('xAccess');
+ try{
+ const [dh, fn] = await getDirForFilename(filename);
+ await dh.getFileHandle(fn);
+ }catch(e){
+ state.s11n.storeException(2,e);
+ rc = state.sq3Codes.SQLITE_IOERR;
+ }finally{
+ wTimeEnd();
+ }
+ storeAndNotify('xAccess', rc);
+ mTimeEnd();
+ },
+ xClose: async function(fid/*sqlite3_file pointer*/){
+ const opName = 'xClose';
+ mTimeStart(opName);
+ __implicitLocks.delete(fid);
+ const fh = __openFiles[fid];
+ let rc = 0;
+ wTimeStart(opName);
+ if(fh){
+ delete __openFiles[fid];
+ await closeSyncHandle(fh);
+ if(fh.deleteOnClose){
+ try{ await fh.dirHandle.removeEntry(fh.filenamePart) }
+ catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) }
+ }
+ }else{
+ state.s11n.serialize();
+ rc = state.sq3Codes.SQLITE_NOTFOUND;
+ }
+ wTimeEnd();
+ storeAndNotify(opName, rc);
+ mTimeEnd();
+ },
+ xDelete: async function(...args){
+ mTimeStart('xDelete');
+ const rc = await vfsAsyncImpls.xDeleteNoWait(...args);
+ storeAndNotify('xDelete', rc);
+ mTimeEnd();
+ },
+ xDeleteNoWait: async function(filename, syncDir = 0, recursive = false){
+ /* The syncDir flag is, for purposes of the VFS API's semantics,
+ ignored here. However, if it has the value 0x1234 then: after
+ deleting the given file, recursively try to delete any empty
+ directories left behind in its wake (ignoring any errors and
+ stopping at the first failure).
+
+ That said: we don't know for sure that removeEntry() fails if
+ the dir is not empty because the API is not documented. It has,
+ however, a "recursive" flag which defaults to false, so
+ presumably it will fail if the dir is not empty and that flag
+ is false.
+ */
+ let rc = 0;
+ wTimeStart('xDelete');
+ try {
+ while(filename){
+ const [hDir, filenamePart] = await getDirForFilename(filename, false);
+ if(!filenamePart) break;
+ await hDir.removeEntry(filenamePart, {recursive});
+ if(0x1234 !== syncDir) break;
+ recursive = false;
+ filename = getResolvedPath(filename, true);
+ filename.pop();
+ filename = filename.join('/');
+ }
+ }catch(e){
+ state.s11n.storeException(2,e);
+ rc = state.sq3Codes.SQLITE_IOERR_DELETE;
+ }
+ wTimeEnd();
+ return rc;
+ },
+ xFileSize: async function(fid/*sqlite3_file pointer*/){
+ mTimeStart('xFileSize');
+ const fh = __openFiles[fid];
+ let rc = 0;
+ wTimeStart('xFileSize');
+ try{
+ const sz = await (await getSyncHandle(fh,'xFileSize')).getSize();
+ state.s11n.serialize(Number(sz));
+ }catch(e){
+ state.s11n.storeException(1,e);
+ rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR);
+ }
+ await releaseImplicitLock(fh);
+ wTimeEnd();
+ storeAndNotify('xFileSize', rc);
+ mTimeEnd();
+ },
+ xLock: async function(fid/*sqlite3_file pointer*/,
+ lockType/*SQLITE_LOCK_...*/){
+ mTimeStart('xLock');
+ const fh = __openFiles[fid];
+ let rc = 0;
+ const oldLockType = fh.xLock;
+ fh.xLock = lockType;
+ if( !fh.syncHandle ){
+ wTimeStart('xLock');
+ try {
+ await getSyncHandle(fh,'xLock');
+ __implicitLocks.delete(fid);
+ }catch(e){
+ state.s11n.storeException(1,e);
+ rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_LOCK);
+ fh.xLock = oldLockType;
+ }
+ wTimeEnd();
+ }
+ storeAndNotify('xLock',rc);
+ mTimeEnd();
+ },
+ xOpen: async function(fid/*sqlite3_file pointer*/, filename,
+ flags/*SQLITE_OPEN_...*/,
+ opfsFlags/*OPFS_...*/){
+ const opName = 'xOpen';
+ mTimeStart(opName);
+ const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags);
+ wTimeStart('xOpen');
+ try{
+ let hDir, filenamePart;
+ try {
+ [hDir, filenamePart] = await getDirForFilename(filename, !!create);
+ }catch(e){
+ state.s11n.storeException(1,e);
+ storeAndNotify(opName, state.sq3Codes.SQLITE_NOTFOUND);
+ mTimeEnd();
+ wTimeEnd();
+ return;
+ }
+ const hFile = await hDir.getFileHandle(filenamePart, {create});
+ wTimeEnd();
+ const fh = Object.assign(Object.create(null),{
+ fid: fid,
+ filenameAbs: filename,
+ filenamePart: filenamePart,
+ dirHandle: hDir,
+ fileHandle: hFile,
+ sabView: state.sabFileBufView,
+ readOnly: create
+ ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags),
+ deleteOnClose: !!(state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags)
+ });
+ fh.releaseImplicitLocks =
+ (opfsFlags & state.opfsFlags.OPFS_UNLOCK_ASAP)
+ || state.opfsFlags.defaultUnlockAsap;
+ if(0 /* this block is modelled after something wa-sqlite
+ does but it leads to immediate contention on journal files.
+ Update: this approach reportedly only works for DELETE journal
+ mode. */
+ && (0===(flags & state.sq3Codes.SQLITE_OPEN_MAIN_DB))){
+ /* sqlite does not lock these files, so go ahead and grab an OPFS
+ lock. */
+ fh.xLock = "xOpen"/* Truthy value to keep entry from getting
+ flagged as auto-locked. String value so
+ that we can easily distinguish is later
+ if needed. */;
+ await getSyncHandle(fh,'xOpen');
+ }
+ __openFiles[fid] = fh;
+ storeAndNotify(opName, 0);
+ }catch(e){
+ wTimeEnd();
+ error(opName,e);
+ state.s11n.storeException(1,e);
+ storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
+ }
+ mTimeEnd();
+ },
+ xRead: async function(fid/*sqlite3_file pointer*/,n,offset64){
+ mTimeStart('xRead');
+ let rc = 0, nRead;
+ const fh = __openFiles[fid];
+ try{
+ wTimeStart('xRead');
+ nRead = (await getSyncHandle(fh,'xRead')).read(
+ fh.sabView.subarray(0, n),
+ {at: Number(offset64)}
+ );
+ wTimeEnd();
+ if(nRead < n){/* Zero-fill remaining bytes */
+ fh.sabView.fill(0, nRead, n);
+ rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
+ }
+ }catch(e){
+ if(undefined===nRead) wTimeEnd();
+ error("xRead() failed",e,fh);
+ state.s11n.storeException(1,e);
+ rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_READ);
+ }
+ await releaseImplicitLock(fh);
+ storeAndNotify('xRead',rc);
+ mTimeEnd();
+ },
+ xSync: async function(fid/*sqlite3_file pointer*/,flags/*ignored*/){
+ mTimeStart('xSync');
+ const fh = __openFiles[fid];
+ let rc = 0;
+ if(!fh.readOnly && fh.syncHandle){
+ try {
+ wTimeStart('xSync');
+ await fh.syncHandle.flush();
+ }catch(e){
+ state.s11n.storeException(2,e);
+ rc = state.sq3Codes.SQLITE_IOERR_FSYNC;
+ }
+ wTimeEnd();
+ }
+ storeAndNotify('xSync',rc);
+ mTimeEnd();
+ },
+ xTruncate: async function(fid/*sqlite3_file pointer*/,size){
+ mTimeStart('xTruncate');
+ let rc = 0;
+ const fh = __openFiles[fid];
+ wTimeStart('xTruncate');
+ try{
+ affirmNotRO('xTruncate', fh);
+ await (await getSyncHandle(fh,'xTruncate')).truncate(size);
+ }catch(e){
+ error("xTruncate():",e,fh);
+ state.s11n.storeException(2,e);
+ rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_TRUNCATE);
+ }
+ await releaseImplicitLock(fh);
+ wTimeEnd();
+ storeAndNotify('xTruncate',rc);
+ mTimeEnd();
+ },
+ xUnlock: async function(fid/*sqlite3_file pointer*/,
+ lockType/*SQLITE_LOCK_...*/){
+ mTimeStart('xUnlock');
+ let rc = 0;
+ const fh = __openFiles[fid];
+ if( state.sq3Codes.SQLITE_LOCK_NONE===lockType
+ && fh.syncHandle ){
+ wTimeStart('xUnlock');
+ try { await closeSyncHandle(fh) }
+ catch(e){
+ state.s11n.storeException(1,e);
+ rc = state.sq3Codes.SQLITE_IOERR_UNLOCK;
+ }
+ wTimeEnd();
+ }
+ storeAndNotify('xUnlock',rc);
+ mTimeEnd();
+ },
+ xWrite: async function(fid/*sqlite3_file pointer*/,n,offset64){
+ mTimeStart('xWrite');
+ let rc;
+ const fh = __openFiles[fid];
+ wTimeStart('xWrite');
+ try{
+ affirmNotRO('xWrite', fh);
+ rc = (
+ n === (await getSyncHandle(fh,'xWrite'))
+ .write(fh.sabView.subarray(0, n),
+ {at: Number(offset64)})
+ ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
+ }catch(e){
+ error("xWrite():",e,fh);
+ state.s11n.storeException(1,e);
+ rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_WRITE);
+ }
+ await releaseImplicitLock(fh);
+ wTimeEnd();
+ storeAndNotify('xWrite',rc);
+ mTimeEnd();
+ }
+ }/*vfsAsyncImpls*/;
+
+ const initS11n = ()=>{
+ /**
+ ACHTUNG: this code is 100% duplicated in the other half of this
+ proxy! The documentation is maintained in the "synchronous half".
+ */
+ if(state.s11n) return state.s11n;
+ const textDecoder = new TextDecoder(),
+ textEncoder = new TextEncoder('utf-8'),
+ viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize),
+ viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
+ state.s11n = Object.create(null);
+ const TypeIds = Object.create(null);
+ TypeIds.number = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' };
+ TypeIds.bigint = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' };
+ TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' };
+ TypeIds.string = { id: 4 };
+ const getTypeId = (v)=>(
+ TypeIds[typeof v]
+ || toss("Maintenance required: this value type cannot be serialized.",v)
+ );
+ const getTypeIdById = (tid)=>{
+ switch(tid){
+ case TypeIds.number.id: return TypeIds.number;
+ case TypeIds.bigint.id: return TypeIds.bigint;
+ case TypeIds.boolean.id: return TypeIds.boolean;
+ case TypeIds.string.id: return TypeIds.string;
+ default: toss("Invalid type ID:",tid);
+ }
+ };
+ state.s11n.deserialize = function(clear=false){
+ ++metrics.s11n.deserialize.count;
+ const t = performance.now();
+ const argc = viewU8[0];
+ const rc = argc ? [] : null;
+ if(argc){
+ const typeIds = [];
+ let offset = 1, i, n, v;
+ for(i = 0; i < argc; ++i, ++offset){
+ typeIds.push(getTypeIdById(viewU8[offset]));
+ }
+ for(i = 0; i < argc; ++i){
+ const t = typeIds[i];
+ if(t.getter){
+ v = viewDV[t.getter](offset, state.littleEndian);
+ offset += t.size;
+ }else{/*String*/
+ n = viewDV.getInt32(offset, state.littleEndian);
+ offset += 4;
+ v = textDecoder.decode(viewU8.slice(offset, offset+n));
+ offset += n;
+ }
+ rc.push(v);
+ }
+ }
+ if(clear) viewU8[0] = 0;
+ //log("deserialize:",argc, rc);
+ metrics.s11n.deserialize.time += performance.now() - t;
+ return rc;
+ };
+ state.s11n.serialize = function(...args){
+ const t = performance.now();
+ ++metrics.s11n.serialize.count;
+ if(args.length){
+ //log("serialize():",args);
+ const typeIds = [];
+ let i = 0, offset = 1;
+ viewU8[0] = args.length & 0xff /* header = # of args */;
+ for(; i < args.length; ++i, ++offset){
+ /* Write the TypeIds.id value into the next args.length
+ bytes. */
+ typeIds.push(getTypeId(args[i]));
+ viewU8[offset] = typeIds[i].id;
+ }
+ for(i = 0; i < args.length; ++i) {
+ /* Deserialize the following bytes based on their
+ corresponding TypeIds.id from the header. */
+ const t = typeIds[i];
+ if(t.setter){
+ viewDV[t.setter](offset, args[i], state.littleEndian);
+ offset += t.size;
+ }else{/*String*/
+ const s = textEncoder.encode(args[i]);
+ viewDV.setInt32(offset, s.byteLength, state.littleEndian);
+ offset += 4;
+ viewU8.set(s, offset);
+ offset += s.byteLength;
+ }
+ }
+ //log("serialize() result:",viewU8.slice(0,offset));
+ }else{
+ viewU8[0] = 0;
+ }
+ metrics.s11n.serialize.time += performance.now() - t;
+ };
+
+ state.s11n.storeException = state.asyncS11nExceptions
+ ? ((priority,e)=>{
+ if(priority<=state.asyncS11nExceptions){
+ state.s11n.serialize([e.name,': ',e.message].join(""));
+ }
+ })
+ : ()=>{};
+
+ return state.s11n;
+ }/*initS11n()*/;
+
+ const waitLoop = async function f(){
+ const opHandlers = Object.create(null);
+ for(let k of Object.keys(state.opIds)){
+ const vi = vfsAsyncImpls[k];
+ if(!vi) continue;
+ const o = Object.create(null);
+ opHandlers[state.opIds[k]] = o;
+ o.key = k;
+ o.f = vi;
+ }
+ while(!flagAsyncShutdown){
+ try {
+ if('not-equal'!==Atomics.wait(
+ state.sabOPView, state.opIds.whichOp, 0, state.asyncIdleWaitTime
+ )){
+ /* Maintenance note: we compare against 'not-equal' because
+
+ https://github.com/tomayac/sqlite-wasm/issues/12
+
+ is reporting that this occassionally, under high loads,
+ returns 'ok', which leads to the whichOp being 0 (which
+ isn't a valid operation ID and leads to an exception,
+ along with a corresponding ugly console log
+ message). Unfortunately, the conditions for that cannot
+ be reliably reproduced. The only place in our code which
+ writes a 0 to the state.opIds.whichOp SharedArrayBuffer
+ index is a few lines down from here, and that instance
+ is required in order for clear communication between
+ the sync half of this proxy and this half.
+ */
+ await releaseImplicitLocks();
+ continue;
+ }
+ const opId = Atomics.load(state.sabOPView, state.opIds.whichOp);
+ Atomics.store(state.sabOPView, state.opIds.whichOp, 0);
+ const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId);
+ const args = state.s11n.deserialize(
+ true /* clear s11n to keep the caller from confusing this with
+ an exception string written by the upcoming
+ operation */
+ ) || [];
+ //warn("waitLoop() whichOp =",opId, hnd, args);
+ if(hnd.f) await hnd.f(...args);
+ else error("Missing callback for opId",opId);
+ }catch(e){
+ error('in waitLoop():',e);
+ }
+ }
+ };
+
+ navigator.storage.getDirectory().then(function(d){
+ state.rootDir = d;
+ globalThis.onmessage = function({data}){
+ switch(data.type){
+ case 'opfs-async-init':{
+ /* Receive shared state from synchronous partner */
+ const opt = data.args;
+ for(const k in opt) state[k] = opt[k];
+ state.verbose = opt.verbose ?? 1;
+ state.sabOPView = new Int32Array(state.sabOP);
+ state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
+ state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
+ Object.keys(vfsAsyncImpls).forEach((k)=>{
+ if(!Number.isFinite(state.opIds[k])){
+ toss("Maintenance required: missing state.opIds[",k,"]");
+ }
+ });
+ initS11n();
+ metrics.reset();
+ log("init state",state);
+ wPost('opfs-async-inited');
+ waitLoop();
+ break;
+ }
+ case 'opfs-async-restart':
+ if(flagAsyncShutdown){
+ warn("Restarting after opfs-async-shutdown. Might or might not work.");
+ flagAsyncShutdown = false;
+ waitLoop();
+ }
+ break;
+ case 'opfs-async-metrics':
+ metrics.dump();
+ break;
+ }
+ };
+ wPost('opfs-async-loaded');
+ }).catch((e)=>error("error initializing OPFS asyncer:",e));
+}/*installAsyncProxy()*/;
+if(!globalThis.SharedArrayBuffer){
+ wPost('opfs-unavailable', "Missing SharedArrayBuffer API.",
+ "The server must emit the COOP/COEP response headers to enable that.");
+}else if(!globalThis.Atomics){
+ wPost('opfs-unavailable', "Missing Atomics API.",
+ "The server must emit the COOP/COEP response headers to enable that.");
+}else if(!globalThis.FileSystemHandle ||
+ !globalThis.FileSystemDirectoryHandle ||
+ !globalThis.FileSystemFileHandle ||
+ !globalThis.FileSystemFileHandle.prototype.createSyncAccessHandle ||
+ !navigator?.storage?.getDirectory){
+ wPost('opfs-unavailable',"Missing required OPFS APIs.");
+}else{
+ installAsyncProxy(self);
+}
diff --git a/ext/wasm/api/sqlite3-v-helper.js b/ext/wasm/api/sqlite3-v-helper.js
new file mode 100644
index 0000000..e63da8a
--- /dev/null
+++ b/ext/wasm/api/sqlite3-v-helper.js
@@ -0,0 +1,718 @@
+/*
+** 2022-11-30
+**
+** 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 installs sqlite3.vfs, and object which exists to assist
+ in the creation of JavaScript implementations of sqlite3_vfs, along
+ with its virtual table counterpart, sqlite3.vtab.
+*/
+'use strict';
+globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
+ const wasm = sqlite3.wasm, capi = sqlite3.capi, toss = sqlite3.util.toss3;
+ const vfs = Object.create(null), vtab = Object.create(null);
+
+ const StructBinder = sqlite3.StructBinder
+ /* we require a local alias b/c StructBinder is removed from the sqlite3
+ object during the final steps of the API cleanup. */;
+ sqlite3.vfs = vfs;
+ sqlite3.vtab = vtab;
+
+ const sii = capi.sqlite3_index_info;
+ /**
+ If n is >=0 and less than this.$nConstraint, this function
+ returns either a WASM pointer to the 0-based nth entry of
+ this.$aConstraint (if passed a truthy 2nd argument) or an
+ sqlite3_index_info.sqlite3_index_constraint object wrapping that
+ address (if passed a falsy value or no 2nd argument). Returns a
+ falsy value if n is out of range.
+ */
+ sii.prototype.nthConstraint = function(n, asPtr=false){
+ if(n<0 || n>=this.$nConstraint) return false;
+ const ptr = this.$aConstraint + (
+ sii.sqlite3_index_constraint.structInfo.sizeof * n
+ );
+ return asPtr ? ptr : new sii.sqlite3_index_constraint(ptr);
+ };
+
+ /**
+ Works identically to nthConstraint() but returns state from
+ this.$aConstraintUsage, so returns an
+ sqlite3_index_info.sqlite3_index_constraint_usage instance
+ if passed no 2nd argument or a falsy 2nd argument.
+ */
+ sii.prototype.nthConstraintUsage = function(n, asPtr=false){
+ if(n<0 || n>=this.$nConstraint) return false;
+ const ptr = this.$aConstraintUsage + (
+ sii.sqlite3_index_constraint_usage.structInfo.sizeof * n
+ );
+ return asPtr ? ptr : new sii.sqlite3_index_constraint_usage(ptr);
+ };
+
+ /**
+ If n is >=0 and less than this.$nOrderBy, this function
+ returns either a WASM pointer to the 0-based nth entry of
+ this.$aOrderBy (if passed a truthy 2nd argument) or an
+ sqlite3_index_info.sqlite3_index_orderby object wrapping that
+ address (if passed a falsy value or no 2nd argument). Returns a
+ falsy value if n is out of range.
+ */
+ sii.prototype.nthOrderBy = function(n, asPtr=false){
+ if(n<0 || n>=this.$nOrderBy) return false;
+ const ptr = this.$aOrderBy + (
+ sii.sqlite3_index_orderby.structInfo.sizeof * n
+ );
+ return asPtr ? ptr : new sii.sqlite3_index_orderby(ptr);
+ };
+
+ /**
+ Installs a StructBinder-bound function pointer member of the
+ given name and function in the given StructType target object.
+
+ It creates a WASM proxy for the given function and arranges for
+ that proxy to be cleaned up when tgt.dispose() is called. Throws
+ on the slightest hint of error, e.g. tgt is-not-a StructType,
+ name does not map to a struct-bound member, etc.
+
+ As a special case, if the given function is a pointer, then
+ `wasm.functionEntry()` is used to validate that it is a known
+ function. If so, it is used as-is with no extra level of proxying
+ or cleanup, else an exception is thrown. It is legal to pass a
+ value of 0, indicating a NULL pointer, with the caveat that 0
+ _is_ a legal function pointer in WASM but it will not be accepted
+ as such _here_. (Justification: the function at address zero must
+ be one which initially came from the WASM module, not a method we
+ want to bind to a virtual table or VFS.)
+
+ This function returns a proxy for itself which is bound to tgt
+ and takes 2 args (name,func). That function returns the same
+ thing as this one, permitting calls to be chained.
+
+ If called with only 1 arg, it has no side effects but returns a
+ func with the same signature as described above.
+
+ ACHTUNG: because we cannot generically know how to transform JS
+ exceptions into result codes, the installed functions do no
+ automatic catching of exceptions. It is critical, to avoid
+ undefined behavior in the C layer, that methods mapped via
+ this function do not throw. The exception, as it were, to that
+ rule is...
+
+ If applyArgcCheck is true then each JS function (as opposed to
+ function pointers) gets wrapped in a proxy which asserts that it
+ is passed the expected number of arguments, throwing if the
+ argument count does not match expectations. That is only intended
+ for dev-time usage for sanity checking, and will leave the C
+ environment in an undefined state.
+ */
+ const installMethod = function callee(
+ tgt, name, func, applyArgcCheck = callee.installMethodArgcCheck
+ ){
+ if(!(tgt instanceof StructBinder.StructType)){
+ toss("Usage error: target object is-not-a StructType.");
+ }else if(!(func instanceof Function) && !wasm.isPtr(func)){
+ toss("Usage errror: expecting a Function or WASM pointer to one.");
+ }
+ if(1===arguments.length){
+ return (n,f)=>callee(tgt, n, f, applyArgcCheck);
+ }
+ if(!callee.argcProxy){
+ callee.argcProxy = function(tgt, funcName, func,sig){
+ return function(...args){
+ if(func.length!==arguments.length){
+ toss("Argument mismatch for",
+ tgt.structInfo.name+"::"+funcName
+ +": Native signature is:",sig);
+ }
+ return func.apply(this, args);
+ }
+ };
+ /* An ondispose() callback for use with
+ StructBinder-created types. */
+ callee.removeFuncList = function(){
+ if(this.ondispose.__removeFuncList){
+ this.ondispose.__removeFuncList.forEach(
+ (v,ndx)=>{
+ if('number'===typeof v){
+ try{wasm.uninstallFunction(v)}
+ catch(e){/*ignore*/}
+ }
+ /* else it's a descriptive label for the next number in
+ the list. */
+ }
+ );
+ delete this.ondispose.__removeFuncList;
+ }
+ };
+ }/*static init*/
+ const sigN = tgt.memberSignature(name);
+ if(sigN.length<2){
+ toss("Member",name,"does not have a function pointer signature:",sigN);
+ }
+ const memKey = tgt.memberKey(name);
+ const fProxy = (applyArgcCheck && !wasm.isPtr(func))
+ /** This middle-man proxy is only for use during development, to
+ confirm that we always pass the proper number of
+ arguments. We know that the C-level code will always use the
+ correct argument count. */
+ ? callee.argcProxy(tgt, memKey, func, sigN)
+ : func;
+ if(wasm.isPtr(fProxy)){
+ if(fProxy && !wasm.functionEntry(fProxy)){
+ toss("Pointer",fProxy,"is not a WASM function table entry.");
+ }
+ tgt[memKey] = fProxy;
+ }else{
+ const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true));
+ tgt[memKey] = pFunc;
+ if(!tgt.ondispose || !tgt.ondispose.__removeFuncList){
+ tgt.addOnDispose('ondispose.__removeFuncList handler',
+ callee.removeFuncList);
+ tgt.ondispose.__removeFuncList = [];
+ }
+ tgt.ondispose.__removeFuncList.push(memKey, pFunc);
+ }
+ return (n,f)=>callee(tgt, n, f, applyArgcCheck);
+ }/*installMethod*/;
+ installMethod.installMethodArgcCheck = false;
+
+ /**
+ Installs methods into the given StructType-type instance. Each
+ entry in the given methods object must map to a known member of
+ the given StructType, else an exception will be triggered. See
+ installMethod() for more details, including the semantics of the
+ 3rd argument.
+
+ As an exception to the above, if any two or more methods in the
+ 2nd argument are the exact same function, installMethod() is
+ _not_ called for the 2nd and subsequent instances, and instead
+ those instances get assigned the same method pointer which is
+ created for the first instance. This optimization is primarily to
+ accommodate special handling of sqlite3_module::xConnect and
+ xCreate methods.
+
+ On success, returns its first argument. Throws on error.
+ */
+ const installMethods = function(
+ structInstance, methods, applyArgcCheck = installMethod.installMethodArgcCheck
+ ){
+ const seen = new Map /* map of <Function, memberName> */;
+ for(const k of Object.keys(methods)){
+ const m = methods[k];
+ const prior = seen.get(m);
+ if(prior){
+ const mkey = structInstance.memberKey(k);
+ structInstance[mkey] = structInstance[structInstance.memberKey(prior)];
+ }else{
+ installMethod(structInstance, k, m, applyArgcCheck);
+ seen.set(m, k);
+ }
+ }
+ return structInstance;
+ };
+
+ /**
+ Equivalent to calling installMethod(this,...arguments) with a
+ first argument of this object. If called with 1 or 2 arguments
+ and the first is an object, it's instead equivalent to calling
+ installMethods(this,...arguments).
+ */
+ StructBinder.StructType.prototype.installMethod = function callee(
+ name, func, applyArgcCheck = installMethod.installMethodArgcCheck
+ ){
+ return (arguments.length < 3 && name && 'object'===typeof name)
+ ? installMethods(this, ...arguments)
+ : installMethod(this, ...arguments);
+ };
+
+ /**
+ Equivalent to calling installMethods() with a first argument
+ of this object.
+ */
+ StructBinder.StructType.prototype.installMethods = function(
+ methods, applyArgcCheck = installMethod.installMethodArgcCheck
+ ){
+ return installMethods(this, methods, applyArgcCheck);
+ };
+
+ /**
+ Uses sqlite3_vfs_register() to register this
+ sqlite3.capi.sqlite3_vfs. This object must have already been
+ filled out properly. If the first argument is truthy, the VFS is
+ registered as the default VFS, else it is not.
+
+ On success, returns this object. Throws on error.
+ */
+ capi.sqlite3_vfs.prototype.registerVfs = function(asDefault=false){
+ if(!(this instanceof sqlite3.capi.sqlite3_vfs)){
+ toss("Expecting a sqlite3_vfs-type argument.");
+ }
+ const rc = capi.sqlite3_vfs_register(this, asDefault ? 1 : 0);
+ if(rc){
+ toss("sqlite3_vfs_register(",this,") failed with rc",rc);
+ }
+ if(this.pointer !== capi.sqlite3_vfs_find(this.$zName)){
+ toss("BUG: sqlite3_vfs_find(vfs.$zName) failed for just-installed VFS",
+ this);
+ }
+ return this;
+ };
+
+ /**
+ A wrapper for installMethods() or registerVfs() to reduce
+ installation of a VFS and/or its I/O methods to a single
+ call.
+
+ Accepts an object which contains the properties "io" and/or
+ "vfs", each of which is itself an object with following properties:
+
+ - `struct`: an sqlite3.StructType-type struct. This must be a
+ populated (except for the methods) object of type
+ sqlite3_io_methods (for the "io" entry) or sqlite3_vfs (for the
+ "vfs" entry).
+
+ - `methods`: an object mapping sqlite3_io_methods method names
+ (e.g. 'xClose') to JS implementations of those methods. The JS
+ implementations must be call-compatible with their native
+ counterparts.
+
+ For each of those object, this function passes its (`struct`,
+ `methods`, (optional) `applyArgcCheck`) properties to
+ installMethods().
+
+ If the `vfs` entry is set then:
+
+ - Its `struct` property's registerVfs() is called. The
+ `vfs` entry may optionally have an `asDefault` property, which
+ gets passed as the argument to registerVfs().
+
+ - If `struct.$zName` is falsy and the entry has a string-type
+ `name` property, `struct.$zName` is set to the C-string form of
+ that `name` value before registerVfs() is called. That string
+ gets added to the on-dispose state of the struct.
+
+ On success returns this object. Throws on error.
+ */
+ vfs.installVfs = function(opt){
+ let count = 0;
+ const propList = ['io','vfs'];
+ for(const key of propList){
+ const o = opt[key];
+ if(o){
+ ++count;
+ installMethods(o.struct, o.methods, !!o.applyArgcCheck);
+ if('vfs'===key){
+ if(!o.struct.$zName && 'string'===typeof o.name){
+ o.struct.addOnDispose(
+ o.struct.$zName = wasm.allocCString(o.name)
+ );
+ }
+ o.struct.registerVfs(!!o.asDefault);
+ }
+ }
+ }
+ if(!count) toss("Misuse: installVfs() options object requires at least",
+ "one of:", propList);
+ return this;
+ };
+
+ /**
+ Internal factory function for xVtab and xCursor impls.
+ */
+ const __xWrapFactory = function(methodName,StructType){
+ return function(ptr,removeMapping=false){
+ if(0===arguments.length) ptr = new StructType;
+ if(ptr instanceof StructType){
+ //T.assert(!this.has(ptr.pointer));
+ this.set(ptr.pointer, ptr);
+ return ptr;
+ }else if(!wasm.isPtr(ptr)){
+ sqlite3.SQLite3Error.toss("Invalid argument to",methodName+"()");
+ }
+ let rc = this.get(ptr);
+ if(removeMapping) this.delete(ptr);
+ return rc;
+ }.bind(new Map);
+ };
+
+ /**
+ A factory function which implements a simple lifetime manager for
+ mappings between C struct pointers and their JS-level wrappers.
+ The first argument must be the logical name of the manager
+ (e.g. 'xVtab' or 'xCursor'), which is only used for error
+ reporting. The second must be the capi.XYZ struct-type value,
+ e.g. capi.sqlite3_vtab or capi.sqlite3_vtab_cursor.
+
+ Returns an object with 4 methods: create(), get(), unget(), and
+ dispose(), plus a StructType member with the value of the 2nd
+ argument. The methods are documented in the body of this
+ function.
+ */
+ const StructPtrMapper = function(name, StructType){
+ const __xWrap = __xWrapFactory(name,StructType);
+ /**
+ This object houses a small API for managing mappings of (`T*`)
+ to StructType<T> objects, specifically within the lifetime
+ requirements of sqlite3_module methods.
+ */
+ return Object.assign(Object.create(null),{
+ /** The StructType object for this object's API. */
+ StructType,
+ /**
+ Creates a new StructType object, writes its `pointer`
+ value to the given output pointer, and returns that
+ object. Its intended usage depends on StructType:
+
+ sqlite3_vtab: to be called from sqlite3_module::xConnect()
+ or xCreate() implementations.
+
+ sqlite3_vtab_cursor: to be called from xOpen().
+
+ This will throw if allocation of the StructType instance
+ fails or if ppOut is not a pointer-type value.
+ */
+ create: (ppOut)=>{
+ const rc = __xWrap();
+ wasm.pokePtr(ppOut, rc.pointer);
+ return rc;
+ },
+ /**
+ Returns the StructType object previously mapped to the
+ given pointer using create(). Its intended usage depends
+ on StructType:
+
+ sqlite3_vtab: to be called from sqlite3_module methods which
+ take a (sqlite3_vtab*) pointer _except_ for
+ xDestroy()/xDisconnect(), in which case unget() or dispose().
+
+ sqlite3_vtab_cursor: to be called from any sqlite3_module methods
+ which take a `sqlite3_vtab_cursor*` argument except xClose(),
+ in which case use unget() or dispose().
+
+ Rule to remember: _never_ call dispose() on an instance
+ returned by this function.
+ */
+ get: (pCObj)=>__xWrap(pCObj),
+ /**
+ Identical to get() but also disconnects the mapping between the
+ given pointer and the returned StructType object, such that
+ future calls to this function or get() with the same pointer
+ will return the undefined value. Its intended usage depends
+ on StructType:
+
+ sqlite3_vtab: to be called from sqlite3_module::xDisconnect() or
+ xDestroy() implementations or in error handling of a failed
+ xCreate() or xConnect().
+
+ sqlite3_vtab_cursor: to be called from xClose() or during
+ cleanup in a failed xOpen().
+
+ Calling this method obligates the caller to call dispose() on
+ the returned object when they're done with it.
+ */
+ unget: (pCObj)=>__xWrap(pCObj,true),
+ /**
+ Works like unget() plus it calls dispose() on the
+ StructType object.
+ */
+ dispose: (pCObj)=>{
+ const o = __xWrap(pCObj,true);
+ if(o) o.dispose();
+ }
+ });
+ };
+
+ /**
+ A lifetime-management object for mapping `sqlite3_vtab*`
+ instances in sqlite3_module methods to capi.sqlite3_vtab
+ objects.
+
+ The API docs are in the API-internal StructPtrMapper().
+ */
+ vtab.xVtab = StructPtrMapper('xVtab', capi.sqlite3_vtab);
+
+ /**
+ A lifetime-management object for mapping `sqlite3_vtab_cursor*`
+ instances in sqlite3_module methods to capi.sqlite3_vtab_cursor
+ objects.
+
+ The API docs are in the API-internal StructPtrMapper().
+ */
+ vtab.xCursor = StructPtrMapper('xCursor', capi.sqlite3_vtab_cursor);
+
+ /**
+ Convenience form of creating an sqlite3_index_info wrapper,
+ intended for use in xBestIndex implementations. Note that the
+ caller is expected to call dispose() on the returned object
+ before returning. Though not _strictly_ required, as that object
+ does not own the pIdxInfo memory, it is nonetheless good form.
+ */
+ vtab.xIndexInfo = (pIdxInfo)=>new capi.sqlite3_index_info(pIdxInfo);
+
+ /**
+ Given an error object, this function returns
+ sqlite3.capi.SQLITE_NOMEM if (e instanceof
+ sqlite3.WasmAllocError), else it returns its
+ second argument. Its intended usage is in the methods
+ of a sqlite3_vfs or sqlite3_module:
+
+ ```
+ try{
+ let rc = ...
+ return rc;
+ }catch(e){
+ return sqlite3.vtab.exceptionToRc(e, sqlite3.capi.SQLITE_XYZ);
+ // where SQLITE_XYZ is some call-appropriate result code.
+ }
+ ```
+ */
+ /**vfs.exceptionToRc = vtab.exceptionToRc =
+ (e, defaultRc=capi.SQLITE_ERROR)=>(
+ (e instanceof sqlite3.WasmAllocError)
+ ? capi.SQLITE_NOMEM
+ : defaultRc
+ );*/
+
+ /**
+ Given an sqlite3_module method name and error object, this
+ function returns sqlite3.capi.SQLITE_NOMEM if (e instanceof
+ sqlite3.WasmAllocError), else it returns its second argument. Its
+ intended usage is in the methods of a sqlite3_vfs or
+ sqlite3_module:
+
+ ```
+ try{
+ let rc = ...
+ return rc;
+ }catch(e){
+ return sqlite3.vtab.xError(
+ 'xColumn', e, sqlite3.capi.SQLITE_XYZ);
+ // where SQLITE_XYZ is some call-appropriate result code.
+ }
+ ```
+
+ If no 3rd argument is provided, its default depends on
+ the error type:
+
+ - An sqlite3.WasmAllocError always resolves to capi.SQLITE_NOMEM.
+
+ - If err is an SQLite3Error then its `resultCode` property
+ is used.
+
+ - If all else fails, capi.SQLITE_ERROR is used.
+
+ If xError.errorReporter is a function, it is called in
+ order to report the error, else the error is not reported.
+ If that function throws, that exception is ignored.
+ */
+ vtab.xError = function f(methodName, err, defaultRc){
+ if(f.errorReporter instanceof Function){
+ try{f.errorReporter("sqlite3_module::"+methodName+"(): "+err.message);}
+ catch(e){/*ignored*/}
+ }
+ let rc;
+ if(err instanceof sqlite3.WasmAllocError) rc = capi.SQLITE_NOMEM;
+ else if(arguments.length>2) rc = defaultRc;
+ else if(err instanceof sqlite3.SQLite3Error) rc = err.resultCode;
+ return rc || capi.SQLITE_ERROR;
+ };
+ vtab.xError.errorReporter = 1 ? console.error.bind(console) : false;
+
+ /**
+ "The problem" with this is that it introduces an outer function with
+ a different arity than the passed-in method callback. That means we
+ cannot do argc validation on these. Additionally, some methods (namely
+ xConnect) may have call-specific error handling. It would be a shame to
+ hard-coded that per-method support in this function.
+ */
+ /** vtab.methodCatcher = function(methodName, method, defaultErrRc=capi.SQLITE_ERROR){
+ return function(...args){
+ try { method(...args); }
+ }catch(e){ return vtab.xError(methodName, e, defaultRc) }
+ };
+ */
+
+ /**
+ A helper for sqlite3_vtab::xRowid() and xUpdate()
+ implementations. It must be passed the final argument to one of
+ those methods (an output pointer to an int64 row ID) and the
+ value to store at the output pointer's address. Returns the same
+ as wasm.poke() and will throw if the 1st or 2nd arguments
+ are invalid for that function.
+
+ Example xRowid impl:
+
+ ```
+ const xRowid = (pCursor, ppRowid64)=>{
+ const c = vtab.xCursor(pCursor);
+ vtab.xRowid(ppRowid64, c.myRowId);
+ return 0;
+ };
+ ```
+ */
+ vtab.xRowid = (ppRowid64, value)=>wasm.poke(ppRowid64, value, 'i64');
+
+ /**
+ A helper to initialize and set up an sqlite3_module object for
+ later installation into individual databases using
+ sqlite3_create_module(). Requires an object with the following
+ properties:
+
+ - `methods`: an object containing a mapping of properties with
+ the C-side names of the sqlite3_module methods, e.g. xCreate,
+ xBestIndex, etc., to JS implementations for those functions.
+ Certain special-case handling is performed, as described below.
+
+ - `catchExceptions` (default=false): if truthy, the given methods
+ are not mapped as-is, but are instead wrapped inside wrappers
+ which translate exceptions into result codes of SQLITE_ERROR or
+ SQLITE_NOMEM, depending on whether the exception is an
+ sqlite3.WasmAllocError. In the case of the xConnect and xCreate
+ methods, the exception handler also sets the output error
+ string to the exception's error string.
+
+ - OPTIONAL `struct`: a sqlite3.capi.sqlite3_module() instance. If
+ not set, one will be created automatically. If the current
+ "this" is-a sqlite3_module then it is unconditionally used in
+ place of `struct`.
+
+ - OPTIONAL `iVersion`: if set, it must be an integer value and it
+ gets assigned to the `$iVersion` member of the struct object.
+ If it's _not_ set, and the passed-in `struct` object's `$iVersion`
+ is 0 (the default) then this function attempts to define a value
+ for that property based on the list of methods it has.
+
+ If `catchExceptions` is false, it is up to the client to ensure
+ that no exceptions escape the methods, as doing so would move
+ them through the C API, leading to undefined
+ behavior. (vtab.xError() is intended to assist in reporting
+ such exceptions.)
+
+ Certain methods may refer to the same implementation. To simplify
+ the definition of such methods:
+
+ - If `methods.xConnect` is `true` then the value of
+ `methods.xCreate` is used in its place, and vice versa. sqlite
+ treats xConnect/xCreate functions specially if they are exactly
+ the same function (same pointer value).
+
+ - If `methods.xDisconnect` is true then the value of
+ `methods.xDestroy` is used in its place, and vice versa.
+
+ This is to facilitate creation of those methods inline in the
+ passed-in object without requiring the client to explicitly get a
+ reference to one of them in order to assign it to the other
+ one.
+
+ The `catchExceptions`-installed handlers will account for
+ identical references to the above functions and will install the
+ same wrapper function for both.
+
+ The given methods are expected to return integer values, as
+ expected by the C API. If `catchExceptions` is truthy, the return
+ value of the wrapped function will be used as-is and will be
+ translated to 0 if the function returns a falsy value (e.g. if it
+ does not have an explicit return). If `catchExceptions` is _not_
+ active, the method implementations must explicitly return integer
+ values.
+
+ Throws on error. On success, returns the sqlite3_module object
+ (`this` or `opt.struct` or a new sqlite3_module instance,
+ depending on how it's called).
+ */
+ vtab.setupModule = function(opt){
+ let createdMod = false;
+ const mod = (this instanceof capi.sqlite3_module)
+ ? this : (opt.struct || (createdMod = new capi.sqlite3_module()));
+ try{
+ const methods = opt.methods || toss("Missing 'methods' object.");
+ for(const e of Object.entries({
+ // -----^ ==> [k,v] triggers a broken code transformation in
+ // some versions of the emsdk toolchain.
+ xConnect: 'xCreate', xDisconnect: 'xDestroy'
+ })){
+ // Remap X=true to X=Y for certain X/Y combinations
+ const k = e[0], v = e[1];
+ if(true === methods[k]) methods[k] = methods[v];
+ else if(true === methods[v]) methods[v] = methods[k];
+ }
+ if(opt.catchExceptions){
+ const fwrap = function(methodName, func){
+ if(['xConnect','xCreate'].indexOf(methodName) >= 0){
+ return function(pDb, pAux, argc, argv, ppVtab, pzErr){
+ try{return func(...arguments) || 0}
+ catch(e){
+ if(!(e instanceof sqlite3.WasmAllocError)){
+ wasm.dealloc(wasm.peekPtr(pzErr));
+ wasm.pokePtr(pzErr, wasm.allocCString(e.message));
+ }
+ return vtab.xError(methodName, e);
+ }
+ };
+ }else{
+ return function(...args){
+ try{return func(...args) || 0}
+ catch(e){
+ return vtab.xError(methodName, e);
+ }
+ };
+ }
+ };
+ const mnames = [
+ 'xCreate', 'xConnect', 'xBestIndex', 'xDisconnect',
+ 'xDestroy', 'xOpen', 'xClose', 'xFilter', 'xNext',
+ 'xEof', 'xColumn', 'xRowid', 'xUpdate',
+ 'xBegin', 'xSync', 'xCommit', 'xRollback',
+ 'xFindFunction', 'xRename', 'xSavepoint', 'xRelease',
+ 'xRollbackTo', 'xShadowName'
+ ];
+ const remethods = Object.create(null);
+ for(const k of mnames){
+ const m = methods[k];
+ if(!(m instanceof Function)) continue;
+ else if('xConnect'===k && methods.xCreate===m){
+ remethods[k] = methods.xCreate;
+ }else if('xCreate'===k && methods.xConnect===m){
+ remethods[k] = methods.xConnect;
+ }else{
+ remethods[k] = fwrap(k, m);
+ }
+ }
+ installMethods(mod, remethods, false);
+ }else{
+ // No automatic exception handling. Trust the client
+ // to not throw.
+ installMethods(
+ mod, methods, !!opt.applyArgcCheck/*undocumented option*/
+ );
+ }
+ if(0===mod.$iVersion){
+ let v;
+ if('number'===typeof opt.iVersion) v = opt.iVersion;
+ else if(mod.$xShadowName) v = 3;
+ else if(mod.$xSavePoint || mod.$xRelease || mod.$xRollbackTo) v = 2;
+ else v = 1;
+ mod.$iVersion = v;
+ }
+ }catch(e){
+ if(createdMod) createdMod.dispose();
+ throw e;
+ }
+ return mod;
+ }/*setupModule()*/;
+
+ /**
+ Equivalent to calling vtab.setupModule() with this sqlite3_module
+ object as the call's `this`.
+ */
+ capi.sqlite3_module.prototype.setupModule = function(opt){
+ return vtab.setupModule.call(this, opt);
+ };
+}/*sqlite3ApiBootstrap.initializers.push()*/);
diff --git a/ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js b/ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js
new file mode 100644
index 0000000..870073c
--- /dev/null
+++ b/ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js
@@ -0,0 +1,1287 @@
+//#ifnot target=node
+/*
+ 2023-07-14
+
+ 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 holds a sqlite3_vfs backed by OPFS storage which uses a
+ different implementation strategy than the "opfs" VFS. This one is a
+ port of Roy Hashimoto's OPFS SyncAccessHandle pool:
+
+ https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/AccessHandlePoolVFS.js
+
+ As described at:
+
+ https://github.com/rhashimoto/wa-sqlite/discussions/67
+
+ with Roy's explicit permission to permit us to port his to our
+ infrastructure rather than having to clean-room reverse-engineer it:
+
+ https://sqlite.org/forum/forumpost/e140d84e71
+
+ Primary differences from the "opfs" VFS include:
+
+ - This one avoids the need for a sub-worker to synchronize
+ communication between the synchronous C API and the
+ only-partly-synchronous OPFS API.
+
+ - It does so by opening a fixed number of OPFS files at
+ library-level initialization time, obtaining SyncAccessHandles to
+ each, and manipulating those handles via the synchronous sqlite3_vfs
+ interface. If it cannot open them (e.g. they are already opened by
+ another tab) then the VFS will not be installed.
+
+ - Because of that, this one lacks all library-level concurrency
+ support.
+
+ - Also because of that, it does not require the SharedArrayBuffer,
+ so can function without the COOP/COEP HTTP response headers.
+
+ - It can hypothetically support Safari 16.4+, whereas the "opfs" VFS
+ requires v17 due to a subworker/storage bug in 16.x which makes it
+ incompatible with that VFS.
+
+ - This VFS requires the "semi-fully-sync" FileSystemSyncAccessHandle
+ (hereafter "SAH") APIs released with Chrome v108 (and all other
+ major browsers released since March 2023). If that API is not
+ detected, the VFS is not registered.
+*/
+globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
+ 'use strict';
+ const toss = sqlite3.util.toss;
+ const toss3 = sqlite3.util.toss3;
+ const initPromises = Object.create(null);
+ const capi = sqlite3.capi;
+ const util = sqlite3.util;
+ const wasm = sqlite3.wasm;
+ // Config opts for the VFS...
+ const SECTOR_SIZE = 4096;
+ const HEADER_MAX_PATH_SIZE = 512;
+ const HEADER_FLAGS_SIZE = 4;
+ const HEADER_DIGEST_SIZE = 8;
+ const HEADER_CORPUS_SIZE = HEADER_MAX_PATH_SIZE + HEADER_FLAGS_SIZE;
+ const HEADER_OFFSET_FLAGS = HEADER_MAX_PATH_SIZE;
+ const HEADER_OFFSET_DIGEST = HEADER_CORPUS_SIZE;
+ const HEADER_OFFSET_DATA = SECTOR_SIZE;
+ /* Bitmask of file types which may persist across sessions.
+ SQLITE_OPEN_xyz types not listed here may be inadvertently
+ left in OPFS but are treated as transient by this VFS and
+ they will be cleaned up during VFS init. */
+ const PERSISTENT_FILE_TYPES =
+ capi.SQLITE_OPEN_MAIN_DB |
+ capi.SQLITE_OPEN_MAIN_JOURNAL |
+ capi.SQLITE_OPEN_SUPER_JOURNAL |
+ capi.SQLITE_OPEN_WAL /* noting that WAL support is
+ unavailable in the WASM build.*/;
+
+ /** Subdirectory of the VFS's space where "opaque" (randomly-named)
+ files are stored. Changing this effectively invalidates the data
+ stored under older names (orphaning it), so don't do that. */
+ const OPAQUE_DIR_NAME = ".opaque";
+
+ /**
+ Returns short a string of random alphanumeric characters
+ suitable for use as a random filename.
+ */
+ const getRandomName = ()=>Math.random().toString(36).slice(2);
+
+ const textDecoder = new TextDecoder();
+ const textEncoder = new TextEncoder();
+
+ const optionDefaults = Object.assign(Object.create(null),{
+ name: 'opfs-sahpool',
+ directory: undefined /* derived from .name */,
+ initialCapacity: 6,
+ clearOnInit: false,
+ /* Logging verbosity 3+ == everything, 2 == warnings+errors, 1 ==
+ errors only. */
+ verbosity: 2
+ });
+
+ /** Logging routines, from most to least serious. */
+ const loggers = [
+ sqlite3.config.error,
+ sqlite3.config.warn,
+ sqlite3.config.log
+ ];
+ const log = sqlite3.config.log;
+ const warn = sqlite3.config.warn;
+ const error = sqlite3.config.error;
+
+ /* Maps (sqlite3_vfs*) to OpfsSAHPool instances */
+ const __mapVfsToPool = new Map();
+ const getPoolForVfs = (pVfs)=>__mapVfsToPool.get(pVfs);
+ const setPoolForVfs = (pVfs,pool)=>{
+ if(pool) __mapVfsToPool.set(pVfs, pool);
+ else __mapVfsToPool.delete(pVfs);
+ };
+ /* Maps (sqlite3_file*) to OpfsSAHPool instances */
+ const __mapSqlite3File = new Map();
+ const getPoolForPFile = (pFile)=>__mapSqlite3File.get(pFile);
+ const setPoolForPFile = (pFile,pool)=>{
+ if(pool) __mapSqlite3File.set(pFile, pool);
+ else __mapSqlite3File.delete(pFile);
+ };
+
+ /**
+ Impls for the sqlite3_io_methods methods. Maintenance reminder:
+ members are in alphabetical order to simplify finding them.
+ */
+ const ioMethods = {
+ xCheckReservedLock: function(pFile,pOut){
+ const pool = getPoolForPFile(pFile);
+ pool.log('xCheckReservedLock');
+ pool.storeErr();
+ wasm.poke32(pOut, 1);
+ return 0;
+ },
+ xClose: function(pFile){
+ const pool = getPoolForPFile(pFile);
+ pool.storeErr();
+ const file = pool.getOFileForS3File(pFile);
+ if(file) {
+ try{
+ pool.log(`xClose ${file.path}`);
+ pool.mapS3FileToOFile(pFile, false);
+ file.sah.flush();
+ if(file.flags & capi.SQLITE_OPEN_DELETEONCLOSE){
+ pool.deletePath(file.path);
+ }
+ }catch(e){
+ return pool.storeErr(e, capi.SQLITE_IOERR);
+ }
+ }
+ return 0;
+ },
+ xDeviceCharacteristics: function(pFile){
+ return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
+ },
+ xFileControl: function(pFile, opId, pArg){
+ return capi.SQLITE_NOTFOUND;
+ },
+ xFileSize: function(pFile,pSz64){
+ const pool = getPoolForPFile(pFile);
+ pool.log(`xFileSize`);
+ const file = pool.getOFileForS3File(pFile);
+ const size = file.sah.getSize() - HEADER_OFFSET_DATA;
+ //log(`xFileSize ${file.path} ${size}`);
+ wasm.poke64(pSz64, BigInt(size));
+ return 0;
+ },
+ xLock: function(pFile,lockType){
+ const pool = getPoolForPFile(pFile);
+ pool.log(`xLock ${lockType}`);
+ pool.storeErr();
+ const file = pool.getOFileForS3File(pFile);
+ file.lockType = lockType;
+ return 0;
+ },
+ xRead: function(pFile,pDest,n,offset64){
+ const pool = getPoolForPFile(pFile);
+ pool.storeErr();
+ const file = pool.getOFileForS3File(pFile);
+ pool.log(`xRead ${file.path} ${n} @ ${offset64}`);
+ try {
+ const nRead = file.sah.read(
+ wasm.heap8u().subarray(pDest, pDest+n),
+ {at: HEADER_OFFSET_DATA + Number(offset64)}
+ );
+ if(nRead < n){
+ wasm.heap8u().fill(0, pDest + nRead, pDest + n);
+ return capi.SQLITE_IOERR_SHORT_READ;
+ }
+ return 0;
+ }catch(e){
+ return pool.storeErr(e, capi.SQLITE_IOERR);
+ }
+ },
+ xSectorSize: function(pFile){
+ return SECTOR_SIZE;
+ },
+ xSync: function(pFile,flags){
+ const pool = getPoolForPFile(pFile);
+ pool.log(`xSync ${flags}`);
+ pool.storeErr();
+ const file = pool.getOFileForS3File(pFile);
+ //log(`xSync ${file.path} ${flags}`);
+ try{
+ file.sah.flush();
+ return 0;
+ }catch(e){
+ return pool.storeErr(e, capi.SQLITE_IOERR);
+ }
+ },
+ xTruncate: function(pFile,sz64){
+ const pool = getPoolForPFile(pFile);
+ pool.log(`xTruncate ${sz64}`);
+ pool.storeErr();
+ const file = pool.getOFileForS3File(pFile);
+ //log(`xTruncate ${file.path} ${iSize}`);
+ try{
+ file.sah.truncate(HEADER_OFFSET_DATA + Number(sz64));
+ return 0;
+ }catch(e){
+ return pool.storeErr(e, capi.SQLITE_IOERR);
+ }
+ },
+ xUnlock: function(pFile,lockType){
+ const pool = getPoolForPFile(pFile);
+ pool.log('xUnlock');
+ const file = pool.getOFileForS3File(pFile);
+ file.lockType = lockType;
+ return 0;
+ },
+ xWrite: function(pFile,pSrc,n,offset64){
+ const pool = getPoolForPFile(pFile);
+ pool.storeErr();
+ const file = pool.getOFileForS3File(pFile);
+ pool.log(`xWrite ${file.path} ${n} ${offset64}`);
+ try{
+ const nBytes = file.sah.write(
+ wasm.heap8u().subarray(pSrc, pSrc+n),
+ { at: HEADER_OFFSET_DATA + Number(offset64) }
+ );
+ return n===nBytes ? 0 : toss("Unknown write() failure.");
+ }catch(e){
+ return pool.storeErr(e, capi.SQLITE_IOERR);
+ }
+ }
+ }/*ioMethods*/;
+
+ const opfsIoMethods = new capi.sqlite3_io_methods();
+ opfsIoMethods.$iVersion = 1;
+ sqlite3.vfs.installVfs({
+ io: {struct: opfsIoMethods, methods: ioMethods}
+ });
+
+ /**
+ Impls for the sqlite3_vfs methods. Maintenance reminder: members
+ are in alphabetical order to simplify finding them.
+ */
+ const vfsMethods = {
+ xAccess: function(pVfs,zName,flags,pOut){
+ //log(`xAccess ${wasm.cstrToJs(zName)}`);
+ const pool = getPoolForVfs(pVfs);
+ pool.storeErr();
+ try{
+ const name = pool.getPath(zName);
+ wasm.poke32(pOut, pool.hasFilename(name) ? 1 : 0);
+ }catch(e){
+ /*ignored*/
+ wasm.poke32(pOut, 0);
+ }
+ return 0;
+ },
+ xCurrentTime: function(pVfs,pOut){
+ wasm.poke(pOut, 2440587.5 + (new Date().getTime()/86400000),
+ 'double');
+ return 0;
+ },
+ xCurrentTimeInt64: function(pVfs,pOut){
+ wasm.poke(pOut, (2440587.5 * 86400000) + new Date().getTime(),
+ 'i64');
+ return 0;
+ },
+ xDelete: function(pVfs, zName, doSyncDir){
+ const pool = getPoolForVfs(pVfs);
+ pool.log(`xDelete ${wasm.cstrToJs(zName)}`);
+ pool.storeErr();
+ try{
+ pool.deletePath(pool.getPath(zName));
+ return 0;
+ }catch(e){
+ pool.storeErr(e);
+ return capi.SQLITE_IOERR_DELETE;
+ }
+ },
+ xFullPathname: function(pVfs,zName,nOut,pOut){
+ //const pool = getPoolForVfs(pVfs);
+ //pool.log(`xFullPathname ${wasm.cstrToJs(zName)}`);
+ const i = wasm.cstrncpy(pOut, zName, nOut);
+ return i<nOut ? 0 : capi.SQLITE_CANTOPEN;
+ },
+ xGetLastError: function(pVfs,nOut,pOut){
+ const pool = getPoolForVfs(pVfs);
+ const e = pool.popErr();
+ pool.log(`xGetLastError ${nOut} e =`,e);
+ if(e){
+ const scope = wasm.scopedAllocPush();
+ try{
+ const [cMsg, n] = wasm.scopedAllocCString(e.message, true);
+ wasm.cstrncpy(pOut, cMsg, nOut);
+ if(n > nOut) wasm.poke8(pOut + nOut - 1, 0);
+ }catch(e){
+ return capi.SQLITE_NOMEM;
+ }finally{
+ wasm.scopedAllocPop(scope);
+ }
+ }
+ return e ? (e.sqlite3Rc || capi.SQLITE_IOERR) : 0;
+ },
+ //xSleep is optionally defined below
+ xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){
+ const pool = getPoolForVfs(pVfs);
+ try{
+ pool.log(`xOpen ${wasm.cstrToJs(zName)} ${flags}`);
+ // First try to open a path that already exists in the file system.
+ const path = (zName && wasm.peek8(zName))
+ ? pool.getPath(zName)
+ : getRandomName();
+ let sah = pool.getSAHForPath(path);
+ if(!sah && (flags & capi.SQLITE_OPEN_CREATE)) {
+ // File not found so try to create it.
+ if(pool.getFileCount() < pool.getCapacity()) {
+ // Choose an unassociated OPFS file from the pool.
+ sah = pool.nextAvailableSAH();
+ pool.setAssociatedPath(sah, path, flags);
+ }else{
+ // File pool is full.
+ toss('SAH pool is full. Cannot create file',path);
+ }
+ }
+ if(!sah){
+ toss('file not found:',path);
+ }
+ // Subsequent I/O methods are only passed the sqlite3_file
+ // pointer, so map the relevant info we need to that pointer.
+ const file = {path, flags, sah};
+ pool.mapS3FileToOFile(pFile, file);
+ file.lockType = capi.SQLITE_LOCK_NONE;
+ const sq3File = new capi.sqlite3_file(pFile);
+ sq3File.$pMethods = opfsIoMethods.pointer;
+ sq3File.dispose();
+ wasm.poke32(pOutFlags, flags);
+ return 0;
+ }catch(e){
+ pool.storeErr(e);
+ return capi.SQLITE_CANTOPEN;
+ }
+ }/*xOpen()*/
+ }/*vfsMethods*/;
+
+ /**
+ Creates and initializes an sqlite3_vfs instance for an
+ OpfsSAHPool. The argument is the VFS's name (JS string).
+
+ Throws if the VFS name is already registered or if something
+ goes terribly wrong via sqlite3.vfs.installVfs().
+
+ Maintenance reminder: the only detail about the returned object
+ which is specific to any given OpfsSAHPool instance is the $zName
+ member. All other state is identical.
+ */
+ const createOpfsVfs = function(vfsName){
+ if( sqlite3.capi.sqlite3_vfs_find(vfsName)){
+ toss3("VFS name is already registered:", vfsName);
+ }
+ const opfsVfs = new capi.sqlite3_vfs();
+ /* We fetch the default VFS so that we can inherit some
+ methods from it. */
+ const pDVfs = capi.sqlite3_vfs_find(null);
+ const dVfs = pDVfs
+ ? new capi.sqlite3_vfs(pDVfs)
+ : null /* dVfs will be null when sqlite3 is built with
+ SQLITE_OS_OTHER. */;
+ opfsVfs.$iVersion = 2/*yes, two*/;
+ opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof;
+ opfsVfs.$mxPathname = HEADER_MAX_PATH_SIZE;
+ opfsVfs.addOnDispose(
+ opfsVfs.$zName = wasm.allocCString(vfsName),
+ ()=>setPoolForVfs(opfsVfs.pointer, 0)
+ );
+
+ if(dVfs){
+ /* Inherit certain VFS members from the default VFS,
+ if available. */
+ opfsVfs.$xRandomness = dVfs.$xRandomness;
+ opfsVfs.$xSleep = dVfs.$xSleep;
+ dVfs.dispose();
+ }
+ if(!opfsVfs.$xRandomness && !vfsMethods.xRandomness){
+ /* If the default VFS has no xRandomness(), add a basic JS impl... */
+ vfsMethods.xRandomness = function(pVfs, nOut, pOut){
+ const heap = wasm.heap8u();
+ let i = 0;
+ for(; i < nOut; ++i) heap[pOut + i] = (Math.random()*255000) & 0xFF;
+ return i;
+ };
+ }
+ if(!opfsVfs.$xSleep && !vfsMethods.xSleep){
+ vfsMethods.xSleep = (pVfs,ms)=>0;
+ }
+ sqlite3.vfs.installVfs({
+ vfs: {struct: opfsVfs, methods: vfsMethods}
+ });
+ return opfsVfs;
+ };
+
+ /**
+ Class for managing OPFS-related state for the
+ OPFS SharedAccessHandle Pool sqlite3_vfs.
+ */
+ class OpfsSAHPool {
+ /* OPFS dir in which VFS metadata is stored. */
+ vfsDir;
+ /* Directory handle to this.vfsDir. */
+ #dhVfsRoot;
+ /* Directory handle to the subdir of this.#dhVfsRoot which holds
+ the randomly-named "opaque" files. This subdir exists in the
+ hope that we can eventually support client-created files in
+ this.#dhVfsRoot. */
+ #dhOpaque;
+ /* Directory handle to this.dhVfsRoot's parent dir. Needed
+ for a VFS-wipe op. */
+ #dhVfsParent;
+ /* Maps SAHs to their opaque file names. */
+ #mapSAHToName = new Map();
+ /* Maps client-side file names to SAHs. */
+ #mapFilenameToSAH = new Map();
+ /* Set of currently-unused SAHs. */
+ #availableSAH = new Set();
+ /* Maps (sqlite3_file*) to xOpen's file objects. */
+ #mapS3FileToOFile_ = new Map();
+
+ /* Maps SAH to an abstract File Object which contains
+ various metadata about that handle. */
+ //#mapSAHToMeta = new Map();
+
+ /** Buffer used by [sg]etAssociatedPath(). */
+ #apBody = new Uint8Array(HEADER_CORPUS_SIZE);
+ // DataView for this.#apBody
+ #dvBody;
+
+ // associated sqlite3_vfs instance
+ #cVfs;
+
+ // Logging verbosity. See optionDefaults.verbosity.
+ #verbosity;
+
+ constructor(options = Object.create(null)){
+ this.#verbosity = options.verbosity ?? optionDefaults.verbosity;
+ this.vfsName = options.name || optionDefaults.name;
+ this.#cVfs = createOpfsVfs(this.vfsName);
+ setPoolForVfs(this.#cVfs.pointer, this);
+ this.vfsDir = options.directory || ("."+this.vfsName);
+ this.#dvBody =
+ new DataView(this.#apBody.buffer, this.#apBody.byteOffset);
+ this.isReady = this
+ .reset(!!(options.clearOnInit ?? optionDefaults.clearOnInit))
+ .then(()=>{
+ if(this.$error) throw this.$error;
+ return this.getCapacity()
+ ? Promise.resolve(undefined)
+ : this.addCapacity(options.initialCapacity
+ || optionDefaults.initialCapacity);
+ });
+ }
+
+ #logImpl(level,...args){
+ if(this.#verbosity>level) loggers[level](this.vfsName+":",...args);
+ };
+ log(...args){this.#logImpl(2, ...args)};
+ warn(...args){this.#logImpl(1, ...args)};
+ error(...args){this.#logImpl(0, ...args)};
+
+ getVfs(){return this.#cVfs}
+
+ /* Current pool capacity. */
+ getCapacity(){return this.#mapSAHToName.size}
+
+ /* Current number of in-use files from pool. */
+ getFileCount(){return this.#mapFilenameToSAH.size}
+
+ /* Returns an array of the names of all
+ currently-opened client-specified filenames. */
+ getFileNames(){
+ const rc = [];
+ const iter = this.#mapFilenameToSAH.keys();
+ for(const n of iter) rc.push(n);
+ return rc;
+ }
+
+// #createFileObject(sah,clientName,opaqueName){
+// const f = Object.assign(Object.create(null),{
+// clientName, opaqueName
+// });
+// this.#mapSAHToMeta.set(sah, f);
+// return f;
+// }
+// #unmapFileObject(sah){
+// this.#mapSAHToMeta.delete(sah);
+// }
+
+ /**
+ Adds n files to the pool's capacity. This change is
+ persistent across settings. Returns a Promise which resolves
+ to the new capacity.
+ */
+ async addCapacity(n){
+ for(let i = 0; i < n; ++i){
+ const name = getRandomName();
+ const h = await this.#dhOpaque.getFileHandle(name, {create:true});
+ const ah = await h.createSyncAccessHandle();
+ this.#mapSAHToName.set(ah,name);
+ this.setAssociatedPath(ah, '', 0);
+ //this.#createFileObject(ah,undefined,name);
+ }
+ return this.getCapacity();
+ }
+
+ /**
+ Reduce capacity by n, but can only reduce up to the limit
+ of currently-available SAHs. Returns a Promise which resolves
+ to the number of slots really removed.
+ */
+ async reduceCapacity(n){
+ let nRm = 0;
+ for(const ah of Array.from(this.#availableSAH)){
+ if(nRm === n || this.getFileCount() === this.getCapacity()){
+ break;
+ }
+ const name = this.#mapSAHToName.get(ah);
+ //this.#unmapFileObject(ah);
+ ah.close();
+ await this.#dhOpaque.removeEntry(name);
+ this.#mapSAHToName.delete(ah);
+ this.#availableSAH.delete(ah);
+ ++nRm;
+ }
+ return nRm;
+ }
+
+ /**
+ Releases all currently-opened SAHs. The only legal
+ operation after this is acquireAccessHandles().
+ */
+ releaseAccessHandles(){
+ for(const ah of this.#mapSAHToName.keys()) ah.close();
+ this.#mapSAHToName.clear();
+ this.#mapFilenameToSAH.clear();
+ this.#availableSAH.clear();
+ }
+
+ /**
+ Opens all files under this.vfsDir/this.#dhOpaque and acquires
+ a SAH for each. returns a Promise which resolves to no value
+ but completes once all SAHs are acquired. If acquiring an SAH
+ throws, SAHPool.$error will contain the corresponding
+ exception.
+
+ If clearFiles is true, the client-stored state of each file is
+ cleared when its handle is acquired, including its name, flags,
+ and any data stored after the metadata block.
+ */
+ async acquireAccessHandles(clearFiles){
+ const files = [];
+ for await (const [name,h] of this.#dhOpaque){
+ if('file'===h.kind){
+ files.push([name,h]);
+ }
+ }
+ return Promise.all(files.map(async([name,h])=>{
+ try{
+ const ah = await h.createSyncAccessHandle()
+ this.#mapSAHToName.set(ah, name);
+ if(clearFiles){
+ ah.truncate(HEADER_OFFSET_DATA);
+ this.setAssociatedPath(ah, '', 0);
+ }else{
+ const path = this.getAssociatedPath(ah);
+ if(path){
+ this.#mapFilenameToSAH.set(path, ah);
+ }else{
+ this.#availableSAH.add(ah);
+ }
+ }
+ }catch(e){
+ this.storeErr(e);
+ this.releaseAccessHandles();
+ throw e;
+ }
+ }));
+ }
+
+ /**
+ Given an SAH, returns the client-specified name of
+ that file by extracting it from the SAH's header.
+
+ On error, it disassociates SAH from the pool and
+ returns an empty string.
+ */
+ getAssociatedPath(sah){
+ sah.read(this.#apBody, {at: 0});
+ // Delete any unexpected files left over by previous
+ // untimely errors...
+ const flags = this.#dvBody.getUint32(HEADER_OFFSET_FLAGS);
+ if(this.#apBody[0] &&
+ ((flags & capi.SQLITE_OPEN_DELETEONCLOSE) ||
+ (flags & PERSISTENT_FILE_TYPES)===0)){
+ warn(`Removing file with unexpected flags ${flags.toString(16)}`,
+ this.#apBody);
+ this.setAssociatedPath(sah, '', 0);
+ return '';
+ }
+
+ const fileDigest = new Uint32Array(HEADER_DIGEST_SIZE / 4);
+ sah.read(fileDigest, {at: HEADER_OFFSET_DIGEST});
+ const compDigest = this.computeDigest(this.#apBody);
+ if(fileDigest.every((v,i) => v===compDigest[i])){
+ // Valid digest
+ const pathBytes = this.#apBody.findIndex((v)=>0===v);
+ if(0===pathBytes){
+ // This file is unassociated, so truncate it to avoid
+ // leaving stale db data laying around.
+ sah.truncate(HEADER_OFFSET_DATA);
+ }
+ return pathBytes
+ ? textDecoder.decode(this.#apBody.subarray(0,pathBytes))
+ : '';
+ }else{
+ // Invalid digest
+ warn('Disassociating file with bad digest.');
+ this.setAssociatedPath(sah, '', 0);
+ return '';
+ }
+ }
+
+ /**
+ Stores the given client-defined path and SQLITE_OPEN_xyz flags
+ into the given SAH. If path is an empty string then the file is
+ disassociated from the pool but its previous name is preserved
+ in the metadata.
+ */
+ setAssociatedPath(sah, path, flags){
+ const enc = textEncoder.encodeInto(path, this.#apBody);
+ if(HEADER_MAX_PATH_SIZE <= enc.written + 1/*NUL byte*/){
+ toss("Path too long:",path);
+ }
+ this.#apBody.fill(0, enc.written, HEADER_MAX_PATH_SIZE);
+ this.#dvBody.setUint32(HEADER_OFFSET_FLAGS, flags);
+
+ const digest = this.computeDigest(this.#apBody);
+ sah.write(this.#apBody, {at: 0});
+ sah.write(digest, {at: HEADER_OFFSET_DIGEST});
+ sah.flush();
+
+ if(path){
+ this.#mapFilenameToSAH.set(path, sah);
+ this.#availableSAH.delete(sah);
+ }else{
+ // This is not a persistent file, so eliminate the contents.
+ sah.truncate(HEADER_OFFSET_DATA);
+ this.#availableSAH.add(sah);
+ }
+ }
+
+ /**
+ Computes a digest for the given byte array and returns it as a
+ two-element Uint32Array. This digest gets stored in the
+ metadata for each file as a validation check. Changing this
+ algorithm invalidates all existing databases for this VFS, so
+ don't do that.
+ */
+ computeDigest(byteArray){
+ let h1 = 0xdeadbeef;
+ let h2 = 0x41c6ce57;
+ for(const v of byteArray){
+ h1 = 31 * h1 + (v * 307);
+ h2 = 31 * h2 + (v * 307);
+ }
+ return new Uint32Array([h1>>>0, h2>>>0]);
+ }
+
+ /**
+ Re-initializes the state of the SAH pool, releasing and
+ re-acquiring all handles.
+
+ See acquireAccessHandles() for the specifics of the clearFiles
+ argument.
+ */
+ async reset(clearFiles){
+ await this.isReady;
+ let h = await navigator.storage.getDirectory();
+ let prev, prevName;
+ for(const d of this.vfsDir.split('/')){
+ if(d){
+ prev = h;
+ h = await h.getDirectoryHandle(d,{create:true});
+ }
+ }
+ this.#dhVfsRoot = h;
+ this.#dhVfsParent = prev;
+ this.#dhOpaque = await this.#dhVfsRoot.getDirectoryHandle(
+ OPAQUE_DIR_NAME,{create:true}
+ );
+ this.releaseAccessHandles();
+ return this.acquireAccessHandles(clearFiles);
+ }
+
+ /**
+ Returns the pathname part of the given argument,
+ which may be any of:
+
+ - a URL object
+ - A JS string representing a file name
+ - Wasm C-string representing a file name
+
+ All "../" parts and duplicate slashes are resolve/removed from
+ the returned result.
+ */
+ getPath(arg) {
+ if(wasm.isPtr(arg)) arg = wasm.cstrToJs(arg);
+ return ((arg instanceof URL)
+ ? arg
+ : new URL(arg, 'file://localhost/')).pathname;
+ }
+
+ /**
+ Removes the association of the given client-specified file
+ name (JS string) from the pool. Returns true if a mapping
+ is found, else false.
+ */
+ deletePath(path) {
+ const sah = this.#mapFilenameToSAH.get(path);
+ if(sah) {
+ // Un-associate the name from the SAH.
+ this.#mapFilenameToSAH.delete(path);
+ this.setAssociatedPath(sah, '', 0);
+ }
+ return !!sah;
+ }
+
+ /**
+ Sets e (an Error object) as this object's current error. Pass a
+ falsy (or no) value to clear it. If code is truthy it is
+ assumed to be an SQLITE_xxx result code, defaulting to
+ SQLITE_IOERR if code is falsy.
+
+ Returns the 2nd argument.
+ */
+ storeErr(e,code){
+ if(e){
+ e.sqlite3Rc = code || capi.SQLITE_IOERR;
+ this.error(e);
+ }
+ this.$error = e;
+ return code;
+ }
+ /**
+ Pops this object's Error object and returns
+ it (a falsy value if no error is set).
+ */
+ popErr(){
+ const rc = this.$error;
+ this.$error = undefined;
+ return rc;
+ }
+
+ /**
+ Returns the next available SAH without removing
+ it from the set.
+ */
+ nextAvailableSAH(){
+ const [rc] = this.#availableSAH.keys();
+ return rc;
+ }
+
+ /**
+ Given an (sqlite3_file*), returns the mapped
+ xOpen file object.
+ */
+ getOFileForS3File(pFile){
+ return this.#mapS3FileToOFile_.get(pFile);
+ }
+ /**
+ Maps or unmaps (if file is falsy) the given (sqlite3_file*)
+ to an xOpen file object and to this pool object.
+ */
+ mapS3FileToOFile(pFile,file){
+ if(file){
+ this.#mapS3FileToOFile_.set(pFile, file);
+ setPoolForPFile(pFile, this);
+ }else{
+ this.#mapS3FileToOFile_.delete(pFile);
+ setPoolForPFile(pFile, false);
+ }
+ }
+
+ /**
+ Returns true if the given client-defined file name is in this
+ object's name-to-SAH map.
+ */
+ hasFilename(name){
+ return this.#mapFilenameToSAH.has(name)
+ }
+
+ /**
+ Returns the SAH associated with the given
+ client-defined file name.
+ */
+ getSAHForPath(path){
+ return this.#mapFilenameToSAH.get(path);
+ }
+
+ /**
+ Removes this object's sqlite3_vfs registration and shuts down
+ this object, releasing all handles, mappings, and whatnot,
+ including deleting its data directory. There is currently no
+ way to "revive" the object and reaquire its resources.
+
+ This function is intended primarily for testing.
+
+ Resolves to true if it did its job, false if the
+ VFS has already been shut down.
+ */
+ async removeVfs(){
+ if(!this.#cVfs.pointer || !this.#dhOpaque) return false;
+ capi.sqlite3_vfs_unregister(this.#cVfs.pointer);
+ this.#cVfs.dispose();
+ try{
+ this.releaseAccessHandles();
+ await this.#dhVfsRoot.removeEntry(OPAQUE_DIR_NAME, {recursive: true});
+ this.#dhOpaque = undefined;
+ await this.#dhVfsParent.removeEntry(
+ this.#dhVfsRoot.name, {recursive: true}
+ );
+ this.#dhVfsRoot = this.#dhVfsParent = undefined;
+ }catch(e){
+ sqlite3.config.error(this.vfsName,"removeVfs() failed:",e);
+ /*otherwise ignored - there is no recovery strategy*/
+ }
+ return true;
+ }
+
+
+ //! Documented elsewhere in this file.
+ exportFile(name){
+ const sah = this.#mapFilenameToSAH.get(name) || toss("File not found:",name);
+ const n = sah.getSize() - HEADER_OFFSET_DATA;
+ const b = new Uint8Array(n>0 ? n : 0);
+ if(n>0){
+ const nRead = sah.read(b, {at: HEADER_OFFSET_DATA});
+ if(nRead != n){
+ toss("Expected to read "+n+" bytes but read "+nRead+".");
+ }
+ }
+ return b;
+ }
+
+ //! Impl for importDb() when its 2nd arg is a function.
+ async importDbChunked(name, callback){
+ const sah = this.#mapFilenameToSAH.get(name)
+ || this.nextAvailableSAH()
+ || toss("No available handles to import to.");
+ sah.truncate(0);
+ let nWrote = 0, chunk, checkedHeader = false, err = false;
+ try{
+ while( undefined !== (chunk = await callback()) ){
+ if(chunk instanceof ArrayBuffer) chunk = new Uint8Array(chunk);
+ if( 0===nWrote && chunk.byteLength>=15 ){
+ util.affirmDbHeader(chunk);
+ checkedHeader = true;
+ }
+ sah.write(chunk, {at: HEADER_OFFSET_DATA + nWrote});
+ nWrote += chunk.byteLength;
+ }
+ if( nWrote < 512 || 0!==nWrote % 512 ){
+ toss("Input size",nWrote,"is not correct for an SQLite database.");
+ }
+ if( !checkedHeader ){
+ const header = new Uint8Array(20);
+ sah.read( header, {at: 0} );
+ util.affirmDbHeader( header );
+ }
+ sah.write(new Uint8Array([1,1]), {
+ at: HEADER_OFFSET_DATA + 18
+ }/*force db out of WAL mode*/);
+ }catch(e){
+ this.setAssociatedPath(sah, '', 0);
+ throw e;
+ }
+ this.setAssociatedPath(sah, name, capi.SQLITE_OPEN_MAIN_DB);
+ return nWrote;
+ }
+
+ //! Documented elsewhere in this file.
+ importDb(name, bytes){
+ if( bytes instanceof ArrayBuffer ) bytes = new Uint8Array(bytes);
+ else if( bytes instanceof Function ) return this.importDbChunked(name, bytes);
+ const sah = this.#mapFilenameToSAH.get(name)
+ || this.nextAvailableSAH()
+ || toss("No available handles to import to.");
+ const n = bytes.byteLength;
+ if(n<512 || n%512!=0){
+ toss("Byte array size is invalid for an SQLite db.");
+ }
+ const header = "SQLite format 3";
+ for(let i = 0; i < header.length; ++i){
+ if( header.charCodeAt(i) !== bytes[i] ){
+ toss("Input does not contain an SQLite database header.");
+ }
+ }
+ const nWrote = sah.write(bytes, {at: HEADER_OFFSET_DATA});
+ if(nWrote != n){
+ this.setAssociatedPath(sah, '', 0);
+ toss("Expected to write "+n+" bytes but wrote "+nWrote+".");
+ }else{
+ sah.write(new Uint8Array([1,1]), {at: HEADER_OFFSET_DATA+18}
+ /* force db out of WAL mode */);
+ this.setAssociatedPath(sah, name, capi.SQLITE_OPEN_MAIN_DB);
+ }
+ return nWrote;
+ }
+
+ }/*class OpfsSAHPool*/;
+
+
+ /**
+ A OpfsSAHPoolUtil instance is exposed to clients in order to
+ manipulate an OpfsSAHPool object without directly exposing that
+ object and allowing for some semantic changes compared to that
+ class.
+
+ Class docs are in the client-level docs for
+ installOpfsSAHPoolVfs().
+ */
+ class OpfsSAHPoolUtil {
+ /* This object's associated OpfsSAHPool. */
+ #p;
+
+ constructor(sahPool){
+ this.#p = sahPool;
+ this.vfsName = sahPool.vfsName;
+ }
+
+ async addCapacity(n){ return this.#p.addCapacity(n) }
+
+ async reduceCapacity(n){ return this.#p.reduceCapacity(n) }
+
+ getCapacity(){ return this.#p.getCapacity(this.#p) }
+
+ getFileCount(){ return this.#p.getFileCount() }
+ getFileNames(){ return this.#p.getFileNames() }
+
+ async reserveMinimumCapacity(min){
+ const c = this.#p.getCapacity();
+ return (c < min) ? this.#p.addCapacity(min - c) : c;
+ }
+
+ exportFile(name){ return this.#p.exportFile(name) }
+
+ importDb(name, bytes){ return this.#p.importDb(name,bytes) }
+
+ async wipeFiles(){ return this.#p.reset(true) }
+
+ unlink(filename){ return this.#p.deletePath(filename) }
+
+ async removeVfs(){ return this.#p.removeVfs() }
+
+ }/* class OpfsSAHPoolUtil */;
+
+ /**
+ Returns a resolved Promise if the current environment
+ has a "fully-sync" SAH impl, else a rejected Promise.
+ */
+ const apiVersionCheck = async ()=>{
+ const dh = await navigator.storage.getDirectory();
+ const fn = '.opfs-sahpool-sync-check-'+getRandomName();
+ const fh = await dh.getFileHandle(fn, { create: true });
+ const ah = await fh.createSyncAccessHandle();
+ const close = ah.close();
+ await close;
+ await dh.removeEntry(fn);
+ if(close?.then){
+ toss("The local OPFS API is too old for opfs-sahpool:",
+ "it has an async FileSystemSyncAccessHandle.close() method.");
+ }
+ return true;
+ };
+
+ /** Only for testing a rejection case. */
+ let instanceCounter = 0;
+
+ /**
+ installOpfsSAHPoolVfs() asynchronously initializes the OPFS
+ SyncAccessHandle (a.k.a. SAH) Pool VFS. It returns a Promise which
+ either resolves to a utility object described below or rejects with
+ an Error value.
+
+ Initialization of this VFS is not automatic because its
+ registration requires that it lock all resources it
+ will potentially use, even if client code does not want
+ to use them. That, in turn, can lead to locking errors
+ when, for example, one page in a given origin has loaded
+ this VFS but does not use it, then another page in that
+ origin tries to use the VFS. If the VFS were automatically
+ registered, the second page would fail to load the VFS
+ due to OPFS locking errors.
+
+ If this function is called more than once with a given "name"
+ option (see below), it will return the same Promise. Calls for
+ different names will return different Promises which resolve to
+ independent objects and refer to different VFS registrations.
+
+ On success, the resulting Promise resolves to a utility object
+ which can be used to query and manipulate the pool. Its API is
+ described at the end of these docs.
+
+ This function accepts an options object to configure certain
+ parts but it is only acknowledged for the very first call and
+ ignored for all subsequent calls.
+
+ The options, in alphabetical order:
+
+ - `clearOnInit`: (default=false) if truthy, contents and filename
+ mapping are removed from each SAH it is acquired during
+ initalization of the VFS, leaving the VFS's storage in a pristine
+ state. Use this only for databases which need not survive a page
+ reload.
+
+ - `initialCapacity`: (default=6) Specifies the default capacity of
+ the VFS. This should not be set unduly high because the VFS has
+ to open (and keep open) a file for each entry in the pool. This
+ setting only has an effect when the pool is initially empty. It
+ does not have any effect if a pool already exists.
+
+ - `directory`: (default="."+`name`) Specifies the OPFS directory
+ name in which to store metadata for the `"opfs-sahpool"`
+ sqlite3_vfs. Only one instance of this VFS can be installed per
+ JavaScript engine, and any two engines with the same storage
+ directory name will collide with each other, leading to locking
+ errors and the inability to register the VFS in the second and
+ subsequent engine. Using a different directory name for each
+ application enables different engines in the same HTTP origin to
+ co-exist, but their data are invisible to each other. Changing
+ this name will effectively orphan any databases stored under
+ previous names. The default is unspecified but descriptive. This
+ option may contain multiple path elements, e.g. "foo/bar/baz",
+ and they are created automatically. In practice there should be
+ no driving need to change this. ACHTUNG: all files in this
+ directory are assumed to be managed by the VFS. Do not place
+ other files in that directory, as they may be deleted or
+ otherwise modified by the VFS.
+
+ - `name`: (default="opfs-sahpool") sets the name to register this
+ VFS under. Normally this should not be changed, but it is
+ possible to register this VFS under multiple names so long as
+ each has its own separate directory to work from. The storage for
+ each is invisible to all others. The name must be a string
+ compatible with `sqlite3_vfs_register()` and friends and suitable
+ for use in URI-style database file names.
+
+ Achtung: if a custom `name` is provided, a custom `directory`
+ must also be provided if any other instance is registered with
+ the default directory. If no directory is explicitly provided
+ then a directory name is synthesized from the `name` option.
+
+ Peculiarities of this VFS:
+
+ - Paths given to it _must_ be absolute. Relative paths will not
+ be properly recognized. This is arguably a bug but correcting it
+ requires some hoop-jumping in routines which have no business
+ doing tricks.
+
+ - It is possible to install multiple instances under different
+ names, each sandboxed from one another inside their own private
+ directory. This feature exists primarily as a way for disparate
+ applications within a given HTTP origin to use this VFS without
+ introducing locking issues between them.
+
+
+ The API for the utility object passed on by this function's
+ Promise, in alphabetical order...
+
+ - [async] number addCapacity(n)
+
+ Adds `n` entries to the current pool. This change is persistent
+ across sessions so should not be called automatically at each app
+ startup (but see `reserveMinimumCapacity()`). Its returned Promise
+ resolves to the new capacity. Because this operation is necessarily
+ asynchronous, the C-level VFS API cannot call this on its own as
+ needed.
+
+ - byteArray exportFile(name)
+
+ Synchronously reads the contents of the given file into a Uint8Array
+ and returns it. This will throw if the given name is not currently
+ in active use or on I/O error. Note that the given name is _not_
+ visible directly in OPFS (or, if it is, it's not from this VFS).
+
+ - number getCapacity()
+
+ Returns the number of files currently contained
+ in the SAH pool. The default capacity is only large enough for one
+ or two databases and their associated temp files.
+
+ - number getFileCount()
+
+ Returns the number of files from the pool currently allocated to
+ slots. This is not the same as the files being "opened".
+
+ - array getFileNames()
+
+ Returns an array of the names of the files currently allocated to
+ slots. This list is the same length as getFileCount().
+
+ - void importDb(name, bytes)
+
+ Imports the contents of an SQLite database, provided as a byte
+ array or ArrayBuffer, under the given name, overwriting any
+ existing content. Throws if the pool has no available file slots,
+ on I/O error, or if the input does not appear to be a
+ database. In the latter case, only a cursory examination is made.
+ Note that this routine is _only_ for importing database files,
+ not arbitrary files, the reason being that this VFS will
+ automatically clean up any non-database files so importing them
+ is pointless.
+
+ If passed a function for its second argument, its behavior
+ changes to asynchronous and it imports its data in chunks fed to
+ it by the given callback function. It calls the callback (which
+ may be async) repeatedly, expecting either a Uint8Array or
+ ArrayBuffer (to denote new input) or undefined (to denote
+ EOF). For so long as the callback continues to return
+ non-undefined, it will append incoming data to the given
+ VFS-hosted database file. The result of the resolved Promise when
+ called this way is the size of the resulting database.
+
+ On succes this routine rewrites the database header bytes in the
+ output file (not the input array) to force disabling of WAL mode.
+
+ On a write error, the handle is removed from the pool and made
+ available for re-use.
+
+ - [async] number reduceCapacity(n)
+
+ Removes up to `n` entries from the pool, with the caveat that it can
+ only remove currently-unused entries. It returns a Promise which
+ resolves to the number of entries actually removed.
+
+ - [async] boolean removeVfs()
+
+ Unregisters the opfs-sahpool VFS and removes its directory from OPFS
+ (which means that _all client content_ is removed). After calling
+ this, the VFS may no longer be used and there is no way to re-add it
+ aside from reloading the current JavaScript context.
+
+ Results are undefined if a database is currently in use with this
+ VFS.
+
+ The returned Promise resolves to true if it performed the removal
+ and false if the VFS was not installed.
+
+ If the VFS has a multi-level directory, e.g. "/foo/bar/baz", _only_
+ the bottom-most directory is removed because this VFS cannot know for
+ certain whether the higher-level directories contain data which
+ should be removed.
+
+ - [async] number reserveMinimumCapacity(min)
+
+ If the current capacity is less than `min`, the capacity is
+ increased to `min`, else this returns with no side effects. The
+ resulting Promise resolves to the new capacity.
+
+ - boolean unlink(filename)
+
+ If a virtual file exists with the given name, disassociates it from
+ the pool and returns true, else returns false without side
+ effects. Results are undefined if the file is currently in active
+ use.
+
+ - string vfsName
+
+ The SQLite VFS name under which this pool's VFS is registered.
+
+ - [async] void wipeFiles()
+
+ Clears all client-defined state of all SAHs and makes all of them
+ available for re-use by the pool. Results are undefined if any such
+ handles are currently in use, e.g. by an sqlite3 db.
+ */
+ sqlite3.installOpfsSAHPoolVfs = async function(options=Object.create(null)){
+ const vfsName = options.name || optionDefaults.name;
+ if(0 && 2===++instanceCounter){
+ throw new Error("Just testing rejection.");
+ }
+ if(initPromises[vfsName]){
+ //console.warn("Returning same OpfsSAHPool result",options,vfsName,initPromises[vfsName]);
+ return initPromises[vfsName];
+ }
+ if(!globalThis.FileSystemHandle ||
+ !globalThis.FileSystemDirectoryHandle ||
+ !globalThis.FileSystemFileHandle ||
+ !globalThis.FileSystemFileHandle.prototype.createSyncAccessHandle ||
+ !navigator?.storage?.getDirectory){
+ return (initPromises[vfsName] = Promise.reject(new Error("Missing required OPFS APIs.")));
+ }
+
+ /**
+ Maintenance reminder: the order of ASYNC ops in this function
+ is significant. We need to have them all chained at the very
+ end in order to be able to catch a race condition where
+ installOpfsSAHPoolVfs() is called twice in rapid succession,
+ e.g.:
+
+ installOpfsSAHPoolVfs().then(console.warn.bind(console));
+ installOpfsSAHPoolVfs().then(console.warn.bind(console));
+
+ If the timing of the async calls is not "just right" then that
+ second call can end up triggering the init a second time and chaos
+ ensues.
+ */
+ return initPromises[vfsName] = apiVersionCheck().then(async function(){
+ if(options.$testThrowInInit){
+ throw options.$testThrowInInit;
+ }
+ const thePool = new OpfsSAHPool(options);
+ return thePool.isReady.then(async()=>{
+ /** The poolUtil object will be the result of the
+ resolved Promise. */
+ const poolUtil = new OpfsSAHPoolUtil(thePool);
+ if(sqlite3.oo1){
+ const oo1 = sqlite3.oo1;
+ const theVfs = thePool.getVfs();
+ const OpfsSAHPoolDb = function(...args){
+ const opt = oo1.DB.dbCtorHelper.normalizeArgs(...args);
+ opt.vfs = theVfs.$zName;
+ oo1.DB.dbCtorHelper.call(this, opt);
+ };
+ OpfsSAHPoolDb.prototype = Object.create(oo1.DB.prototype);
+ // yes or no? OpfsSAHPoolDb.PoolUtil = poolUtil;
+ poolUtil.OpfsSAHPoolDb = OpfsSAHPoolDb;
+ oo1.DB.dbCtorHelper.setVfsPostOpenSql(
+ theVfs.pointer,
+ function(oo1Db, sqlite3){
+ sqlite3.capi.sqlite3_exec(oo1Db, [
+ /* See notes in sqlite3-vfs-opfs.js */
+ "pragma journal_mode=DELETE;",
+ "pragma cache_size=-16384;"
+ ], 0, 0, 0);
+ }
+ );
+ }/*extend sqlite3.oo1*/
+ thePool.log("VFS initialized.");
+ return poolUtil;
+ }).catch(async (e)=>{
+ await thePool.removeVfs().catch(()=>{});
+ return e;
+ });
+ }).catch((err)=>{
+ //error("rejecting promise:",err);
+ return initPromises[vfsName] = Promise.reject(err);
+ });
+ }/*installOpfsSAHPoolVfs()*/;
+}/*sqlite3ApiBootstrap.initializers*/);
+//#else
+/*
+ The OPFS SAH Pool VFS parts are elided from builds targeting
+ node.js.
+*/
+//#endif target=node
diff --git a/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js b/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js
new file mode 100644
index 0000000..af89f21
--- /dev/null
+++ b/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js
@@ -0,0 +1,1469 @@
+//#ifnot target=node
+/*
+ 2022-09-18
+
+ 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 holds the synchronous half of an sqlite3_vfs
+ implementation which proxies, in a synchronous fashion, the
+ asynchronous Origin-Private FileSystem (OPFS) APIs using a second
+ Worker, implemented in sqlite3-opfs-async-proxy.js. This file is
+ intended to be appended to the main sqlite3 JS deliverable somewhere
+ after sqlite3-api-oo1.js and before sqlite3-api-cleanup.js.
+*/
+'use strict';
+globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
+/**
+ installOpfsVfs() returns a Promise which, on success, installs an
+ sqlite3_vfs named "opfs", suitable for use with all sqlite3 APIs
+ which accept a VFS. It is intended to be called via
+ sqlite3ApiBootstrap.initializers or an equivalent mechanism.
+
+ The installed VFS uses the Origin-Private FileSystem API for
+ all file storage. On error it is rejected with an exception
+ explaining the problem. Reasons for rejection include, but are
+ not limited to:
+
+ - The counterpart Worker (see below) could not be loaded.
+
+ - The environment does not support OPFS. That includes when
+ this function is called from the main window thread.
+
+ Significant notes and limitations:
+
+ - As of this writing, OPFS is still very much in flux and only
+ available in bleeding-edge versions of Chrome (v102+, noting that
+ that number will increase as the OPFS API matures).
+
+ - The OPFS features used here are only available in dedicated Worker
+ threads. This file tries to detect that case, resulting in a
+ rejected Promise if those features do not seem to be available.
+
+ - It requires the SharedArrayBuffer and Atomics classes, and the
+ former is only available if the HTTP server emits the so-called
+ COOP and COEP response headers. These features are required for
+ proxying OPFS's synchronous API via the synchronous interface
+ required by the sqlite3_vfs API.
+
+ - This function may only be called a single time. When called, this
+ function removes itself from the sqlite3 object.
+
+ All arguments to this function are for internal/development purposes
+ only. They do not constitute a public API and may change at any
+ time.
+
+ The argument may optionally be a plain object with the following
+ configuration options:
+
+ - proxyUri: as described above
+
+ - verbose (=2): an integer 0-3. 0 disables all logging, 1 enables
+ logging of errors. 2 enables logging of warnings and errors. 3
+ additionally enables debugging info.
+
+ - sanityChecks (=false): if true, some basic sanity tests are
+ run on the OPFS VFS API after it's initialized, before the
+ returned Promise resolves.
+
+ On success, the Promise resolves to the top-most sqlite3 namespace
+ object and that object gets a new object installed in its
+ `opfs` property, containing several OPFS-specific utilities.
+*/
+const installOpfsVfs = function callee(options){
+ if(!globalThis.SharedArrayBuffer
+ || !globalThis.Atomics){
+ return Promise.reject(
+ new Error("Cannot install OPFS: Missing SharedArrayBuffer and/or Atomics. "+
+ "The server must emit the COOP/COEP response headers to enable those. "+
+ "See https://sqlite.org/wasm/doc/trunk/persistence.md#coop-coep")
+ );
+ }else if('undefined'===typeof WorkerGlobalScope){
+ return Promise.reject(
+ new Error("The OPFS sqlite3_vfs cannot run in the main thread "+
+ "because it requires Atomics.wait().")
+ );
+ }else if(!globalThis.FileSystemHandle ||
+ !globalThis.FileSystemDirectoryHandle ||
+ !globalThis.FileSystemFileHandle ||
+ !globalThis.FileSystemFileHandle.prototype.createSyncAccessHandle ||
+ !navigator?.storage?.getDirectory){
+ return Promise.reject(
+ new Error("Missing required OPFS APIs.")
+ );
+ }
+ if(!options || 'object'!==typeof options){
+ options = Object.create(null);
+ }
+ const urlParams = new URL(globalThis.location.href).searchParams;
+ if(urlParams.has('opfs-disable')){
+ //sqlite3.config.warn('Explicitly not installing "opfs" VFS due to opfs-disable flag.');
+ return Promise.resolve(sqlite3);
+ }
+ if(undefined===options.verbose){
+ options.verbose = urlParams.has('opfs-verbose')
+ ? (+urlParams.get('opfs-verbose') || 2) : 1;
+ }
+ if(undefined===options.sanityChecks){
+ options.sanityChecks = urlParams.has('opfs-sanity-check');
+ }
+ if(undefined===options.proxyUri){
+ options.proxyUri = callee.defaultProxyUri;
+ }
+
+ //sqlite3.config.warn("OPFS options =",options,globalThis.location);
+
+ if('function' === typeof options.proxyUri){
+ options.proxyUri = options.proxyUri();
+ }
+ const thePromise = new Promise(function(promiseResolve_, promiseReject_){
+ const loggers = [
+ sqlite3.config.error,
+ sqlite3.config.warn,
+ sqlite3.config.log
+ ];
+ const logImpl = (level,...args)=>{
+ if(options.verbose>level) loggers[level]("OPFS syncer:",...args);
+ };
+ const log = (...args)=>logImpl(2, ...args);
+ const warn = (...args)=>logImpl(1, ...args);
+ const error = (...args)=>logImpl(0, ...args);
+ const toss = sqlite3.util.toss;
+ const capi = sqlite3.capi;
+ const util = sqlite3.util;
+ const wasm = sqlite3.wasm;
+ const sqlite3_vfs = capi.sqlite3_vfs;
+ const sqlite3_file = capi.sqlite3_file;
+ const sqlite3_io_methods = capi.sqlite3_io_methods;
+ /**
+ Generic utilities for working with OPFS. This will get filled out
+ by the Promise setup and, on success, installed as sqlite3.opfs.
+
+ ACHTUNG: do not rely on these APIs in client code. They are
+ experimental and subject to change or removal as the
+ OPFS-specific sqlite3_vfs evolves.
+ */
+ const opfsUtil = Object.create(null);
+
+ /**
+ Returns true if _this_ thread has access to the OPFS APIs.
+ */
+ const thisThreadHasOPFS = ()=>{
+ return globalThis.FileSystemHandle &&
+ globalThis.FileSystemDirectoryHandle &&
+ globalThis.FileSystemFileHandle &&
+ globalThis.FileSystemFileHandle.prototype.createSyncAccessHandle &&
+ navigator?.storage?.getDirectory;
+ };
+
+ /**
+ Not part of the public API. Solely for internal/development
+ use.
+ */
+ opfsUtil.metrics = {
+ dump: function(){
+ let k, n = 0, t = 0, w = 0;
+ for(k in state.opIds){
+ const m = metrics[k];
+ n += m.count;
+ t += m.time;
+ w += m.wait;
+ m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
+ m.avgWait = (m.count && m.wait) ? (m.wait / m.count) : 0;
+ }
+ sqlite3.config.log(globalThis.location.href,
+ "metrics for",globalThis.location.href,":",metrics,
+ "\nTotal of",n,"op(s) for",t,
+ "ms (incl. "+w+" ms of waiting on the async side)");
+ sqlite3.config.log("Serialization metrics:",metrics.s11n);
+ W.postMessage({type:'opfs-async-metrics'});
+ },
+ reset: function(){
+ let k;
+ const r = (m)=>(m.count = m.time = m.wait = 0);
+ for(k in state.opIds){
+ r(metrics[k] = Object.create(null));
+ }
+ let s = metrics.s11n = Object.create(null);
+ s = s.serialize = Object.create(null);
+ s.count = s.time = 0;
+ s = metrics.s11n.deserialize = Object.create(null);
+ s.count = s.time = 0;
+ }
+ }/*metrics*/;
+ const opfsIoMethods = new sqlite3_io_methods();
+ const opfsVfs = new sqlite3_vfs()
+ .addOnDispose( ()=>opfsIoMethods.dispose());
+ let promiseWasRejected = undefined;
+ const promiseReject = (err)=>{
+ promiseWasRejected = true;
+ opfsVfs.dispose();
+ return promiseReject_(err);
+ };
+ const promiseResolve = ()=>{
+ promiseWasRejected = false;
+ return promiseResolve_(sqlite3);
+ };
+ const W =
+//#if target=es6-bundler-friendly
+ new Worker(new URL("sqlite3-opfs-async-proxy.js", import.meta.url));
+//#elif target=es6-module
+ new Worker(new URL(options.proxyUri, import.meta.url));
+//#else
+ new Worker(options.proxyUri);
+//#endif
+ setTimeout(()=>{
+ /* At attempt to work around a browser-specific quirk in which
+ the Worker load is failing in such a way that we neither
+ resolve nor reject it. This workaround gives that resolve/reject
+ a time limit and rejects if that timer expires. Discussion:
+ https://sqlite.org/forum/forumpost/a708c98dcb3ef */
+ if(undefined===promiseWasRejected){
+ promiseReject(
+ new Error("Timeout while waiting for OPFS async proxy worker.")
+ );
+ }
+ }, 4000);
+ W._originalOnError = W.onerror /* will be restored later */;
+ W.onerror = function(err){
+ // The error object doesn't contain any useful info when the
+ // failure is, e.g., that the remote script is 404.
+ error("Error initializing OPFS asyncer:",err);
+ promiseReject(new Error("Loading OPFS async Worker failed for unknown reasons."));
+ };
+ const pDVfs = capi.sqlite3_vfs_find(null)/*pointer to default VFS*/;
+ const dVfs = pDVfs
+ ? new sqlite3_vfs(pDVfs)
+ : null /* dVfs will be null when sqlite3 is built with
+ SQLITE_OS_OTHER. */;
+ opfsIoMethods.$iVersion = 1;
+ opfsVfs.$iVersion = 2/*yes, two*/;
+ opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof;
+ opfsVfs.$mxPathname = 1024/*sure, why not?*/;
+ opfsVfs.$zName = wasm.allocCString("opfs");
+ // All C-side memory of opfsVfs is zeroed out, but just to be explicit:
+ opfsVfs.$xDlOpen = opfsVfs.$xDlError = opfsVfs.$xDlSym = opfsVfs.$xDlClose = null;
+ opfsVfs.addOnDispose(
+ '$zName', opfsVfs.$zName,
+ 'cleanup default VFS wrapper', ()=>(dVfs ? dVfs.dispose() : null)
+ );
+ /**
+ Pedantic sidebar about opfsVfs.ondispose: the entries in that array
+ are items to clean up when opfsVfs.dispose() is called, but in this
+ environment it will never be called. The VFS instance simply
+ hangs around until the WASM module instance is cleaned up. We
+ "could" _hypothetically_ clean it up by "importing" an
+ sqlite3_os_end() impl into the wasm build, but the shutdown order
+ of the wasm engine and the JS one are undefined so there is no
+ guaranty that the opfsVfs instance would be available in one
+ environment or the other when sqlite3_os_end() is called (_if_ it
+ gets called at all in a wasm build, which is undefined).
+ */
+ /**
+ State which we send to the async-api Worker or share with it.
+ This object must initially contain only cloneable or sharable
+ objects. After the worker's "inited" message arrives, other types
+ of data may be added to it.
+
+ For purposes of Atomics.wait() and Atomics.notify(), we use a
+ SharedArrayBuffer with one slot reserved for each of the API
+ proxy's methods. The sync side of the API uses Atomics.wait()
+ on the corresponding slot and the async side uses
+ Atomics.notify() on that slot.
+
+ The approach of using a single SAB to serialize comms for all
+ instances might(?) lead to deadlock situations in multi-db
+ cases. We should probably have one SAB here with a single slot
+ for locking a per-file initialization step and then allocate a
+ separate SAB like the above one for each file. That will
+ require a bit of acrobatics but should be feasible. The most
+ problematic part is that xOpen() would have to use
+ postMessage() to communicate its SharedArrayBuffer, and mixing
+ that approach with Atomics.wait/notify() gets a bit messy.
+ */
+ const state = Object.create(null);
+ state.verbose = options.verbose;
+ state.littleEndian = (()=>{
+ const buffer = new ArrayBuffer(2);
+ new DataView(buffer).setInt16(0, 256, true /* ==>littleEndian */);
+ // Int16Array uses the platform's endianness.
+ return new Int16Array(buffer)[0] === 256;
+ })();
+ /**
+ asyncIdleWaitTime is how long (ms) to wait, in the async proxy,
+ for each Atomics.wait() when waiting on inbound VFS API calls.
+ We need to wake up periodically to give the thread a chance to
+ do other things. If this is too high (e.g. 500ms) then even two
+ workers/tabs can easily run into locking errors. Some multiple
+ of this value is also used for determining how long to wait on
+ lock contention to free up.
+ */
+ state.asyncIdleWaitTime = 150;
+
+ /**
+ Whether the async counterpart should log exceptions to
+ the serialization channel. That produces a great deal of
+ noise for seemingly innocuous things like xAccess() checks
+ for missing files, so this option may have one of 3 values:
+
+ 0 = no exception logging.
+
+ 1 = only log exceptions for "significant" ops like xOpen(),
+ xRead(), and xWrite().
+
+ 2 = log all exceptions.
+ */
+ state.asyncS11nExceptions = 1;
+ /* Size of file I/O buffer block. 64k = max sqlite3 page size, and
+ xRead/xWrite() will never deal in blocks larger than that. */
+ state.fileBufferSize = 1024 * 64;
+ state.sabS11nOffset = state.fileBufferSize;
+ /**
+ The size of the block in our SAB for serializing arguments and
+ result values. Needs to be large enough to hold serialized
+ values of any of the proxied APIs. Filenames are the largest
+ part but are limited to opfsVfs.$mxPathname bytes. We also
+ store exceptions there, so it needs to be long enough to hold
+ a reasonably long exception string.
+ */
+ state.sabS11nSize = opfsVfs.$mxPathname * 2;
+ /**
+ The SAB used for all data I/O between the synchronous and
+ async halves (file i/o and arg/result s11n).
+ */
+ state.sabIO = new SharedArrayBuffer(
+ state.fileBufferSize/* file i/o block */
+ + state.sabS11nSize/* argument/result serialization block */
+ );
+ state.opIds = Object.create(null);
+ const metrics = Object.create(null);
+ {
+ /* Indexes for use in our SharedArrayBuffer... */
+ let i = 0;
+ /* SAB slot used to communicate which operation is desired
+ between both workers. This worker writes to it and the other
+ listens for changes. */
+ state.opIds.whichOp = i++;
+ /* Slot for storing return values. This worker listens to that
+ slot and the other worker writes to it. */
+ state.opIds.rc = i++;
+ /* Each function gets an ID which this worker writes to
+ the whichOp slot. The async-api worker uses Atomic.wait()
+ on the whichOp slot to figure out which operation to run
+ next. */
+ state.opIds.xAccess = i++;
+ state.opIds.xClose = i++;
+ state.opIds.xDelete = i++;
+ state.opIds.xDeleteNoWait = i++;
+ state.opIds.xFileSize = i++;
+ state.opIds.xLock = i++;
+ state.opIds.xOpen = i++;
+ state.opIds.xRead = i++;
+ state.opIds.xSleep = i++;
+ state.opIds.xSync = i++;
+ state.opIds.xTruncate = i++;
+ state.opIds.xUnlock = i++;
+ state.opIds.xWrite = i++;
+ state.opIds.mkdir = i++;
+ state.opIds['opfs-async-metrics'] = i++;
+ state.opIds['opfs-async-shutdown'] = i++;
+ /* The retry slot is used by the async part for wait-and-retry
+ semantics. Though we could hypothetically use the xSleep slot
+ for that, doing so might lead to undesired side effects. */
+ state.opIds.retry = i++;
+ state.sabOP = new SharedArrayBuffer(
+ i * 4/* ==sizeof int32, noting that Atomics.wait() and friends
+ can only function on Int32Array views of an SAB. */);
+ opfsUtil.metrics.reset();
+ }
+ /**
+ SQLITE_xxx constants to export to the async worker
+ counterpart...
+ */
+ state.sq3Codes = Object.create(null);
+ [
+ 'SQLITE_ACCESS_EXISTS',
+ 'SQLITE_ACCESS_READWRITE',
+ 'SQLITE_BUSY',
+ 'SQLITE_ERROR',
+ 'SQLITE_IOERR',
+ 'SQLITE_IOERR_ACCESS',
+ 'SQLITE_IOERR_CLOSE',
+ 'SQLITE_IOERR_DELETE',
+ 'SQLITE_IOERR_FSYNC',
+ 'SQLITE_IOERR_LOCK',
+ 'SQLITE_IOERR_READ',
+ 'SQLITE_IOERR_SHORT_READ',
+ 'SQLITE_IOERR_TRUNCATE',
+ 'SQLITE_IOERR_UNLOCK',
+ 'SQLITE_IOERR_WRITE',
+ 'SQLITE_LOCK_EXCLUSIVE',
+ 'SQLITE_LOCK_NONE',
+ 'SQLITE_LOCK_PENDING',
+ 'SQLITE_LOCK_RESERVED',
+ 'SQLITE_LOCK_SHARED',
+ 'SQLITE_LOCKED',
+ 'SQLITE_MISUSE',
+ 'SQLITE_NOTFOUND',
+ 'SQLITE_OPEN_CREATE',
+ 'SQLITE_OPEN_DELETEONCLOSE',
+ 'SQLITE_OPEN_MAIN_DB',
+ 'SQLITE_OPEN_READONLY'
+ ].forEach((k)=>{
+ if(undefined === (state.sq3Codes[k] = capi[k])){
+ toss("Maintenance required: not found:",k);
+ }
+ });
+ state.opfsFlags = Object.assign(Object.create(null),{
+ /**
+ Flag for use with xOpen(). "opfs-unlock-asap=1" enables
+ this. See defaultUnlockAsap, below.
+ */
+ OPFS_UNLOCK_ASAP: 0x01,
+ /**
+ If true, any async routine which implicitly acquires a sync
+ access handle (i.e. an OPFS lock) will release that locks at
+ the end of the call which acquires it. If false, such
+ "autolocks" are not released until the VFS is idle for some
+ brief amount of time.
+
+ The benefit of enabling this is much higher concurrency. The
+ down-side is much-reduced performance (as much as a 4x decrease
+ in speedtest1).
+ */
+ defaultUnlockAsap: false
+ });
+
+ /**
+ Runs the given operation (by name) in the async worker
+ counterpart, waits for its response, and returns the result
+ which the async worker writes to SAB[state.opIds.rc]. The
+ 2nd and subsequent arguments must be the aruguments for the
+ async op.
+ */
+ const opRun = (op,...args)=>{
+ const opNdx = state.opIds[op] || toss("Invalid op ID:",op);
+ state.s11n.serialize(...args);
+ Atomics.store(state.sabOPView, state.opIds.rc, -1);
+ Atomics.store(state.sabOPView, state.opIds.whichOp, opNdx);
+ Atomics.notify(state.sabOPView, state.opIds.whichOp)
+ /* async thread will take over here */;
+ const t = performance.now();
+ Atomics.wait(state.sabOPView, state.opIds.rc, -1)
+ /* When this wait() call returns, the async half will have
+ completed the operation and reported its results. */;
+ const rc = Atomics.load(state.sabOPView, state.opIds.rc);
+ metrics[op].wait += performance.now() - t;
+ if(rc && state.asyncS11nExceptions){
+ const err = state.s11n.deserialize();
+ if(err) error(op+"() async error:",...err);
+ }
+ return rc;
+ };
+
+ /**
+ Not part of the public API. Only for test/development use.
+ */
+ opfsUtil.debug = {
+ asyncShutdown: ()=>{
+ warn("Shutting down OPFS async listener. The OPFS VFS will no longer work.");
+ opRun('opfs-async-shutdown');
+ },
+ asyncRestart: ()=>{
+ warn("Attempting to restart OPFS VFS async listener. Might work, might not.");
+ W.postMessage({type: 'opfs-async-restart'});
+ }
+ };
+
+ const initS11n = ()=>{
+ /**
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ ACHTUNG: this code is 100% duplicated in the other half of
+ this proxy! The documentation is maintained in the
+ "synchronous half".
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+ This proxy de/serializes cross-thread function arguments and
+ output-pointer values via the state.sabIO SharedArrayBuffer,
+ using the region defined by (state.sabS11nOffset,
+ state.sabS11nOffset + state.sabS11nSize]. Only one dataset is
+ recorded at a time.
+
+ This is not a general-purpose format. It only supports the
+ range of operations, and data sizes, needed by the
+ sqlite3_vfs and sqlite3_io_methods operations. Serialized
+ data are transient and this serialization algorithm may
+ change at any time.
+
+ The data format can be succinctly summarized as:
+
+ Nt...Td...D
+
+ Where:
+
+ - N = number of entries (1 byte)
+
+ - t = type ID of first argument (1 byte)
+
+ - ...T = type IDs of the 2nd and subsequent arguments (1 byte
+ each).
+
+ - d = raw bytes of first argument (per-type size).
+
+ - ...D = raw bytes of the 2nd and subsequent arguments (per-type
+ size).
+
+ All types except strings have fixed sizes. Strings are stored
+ using their TextEncoder/TextDecoder representations. It would
+ arguably make more sense to store them as Int16Arrays of
+ their JS character values, but how best/fastest to get that
+ in and out of string form is an open point. Initial
+ experimentation with that approach did not gain us any speed.
+
+ Historical note: this impl was initially about 1% this size by
+ using using JSON.stringify/parse(), but using fit-to-purpose
+ serialization saves considerable runtime.
+ */
+ if(state.s11n) return state.s11n;
+ const textDecoder = new TextDecoder(),
+ textEncoder = new TextEncoder('utf-8'),
+ viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize),
+ viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
+ state.s11n = Object.create(null);
+ /* Only arguments and return values of these types may be
+ serialized. This covers the whole range of types needed by the
+ sqlite3_vfs API. */
+ const TypeIds = Object.create(null);
+ TypeIds.number = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' };
+ TypeIds.bigint = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' };
+ TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' };
+ TypeIds.string = { id: 4 };
+
+ const getTypeId = (v)=>(
+ TypeIds[typeof v]
+ || toss("Maintenance required: this value type cannot be serialized.",v)
+ );
+ const getTypeIdById = (tid)=>{
+ switch(tid){
+ case TypeIds.number.id: return TypeIds.number;
+ case TypeIds.bigint.id: return TypeIds.bigint;
+ case TypeIds.boolean.id: return TypeIds.boolean;
+ case TypeIds.string.id: return TypeIds.string;
+ default: toss("Invalid type ID:",tid);
+ }
+ };
+
+ /**
+ Returns an array of the deserialized state stored by the most
+ recent serialize() operation (from from this thread or the
+ counterpart thread), or null if the serialization buffer is
+ empty. If passed a truthy argument, the serialization buffer
+ is cleared after deserialization.
+ */
+ state.s11n.deserialize = function(clear=false){
+ ++metrics.s11n.deserialize.count;
+ const t = performance.now();
+ const argc = viewU8[0];
+ const rc = argc ? [] : null;
+ if(argc){
+ const typeIds = [];
+ let offset = 1, i, n, v;
+ for(i = 0; i < argc; ++i, ++offset){
+ typeIds.push(getTypeIdById(viewU8[offset]));
+ }
+ for(i = 0; i < argc; ++i){
+ const t = typeIds[i];
+ if(t.getter){
+ v = viewDV[t.getter](offset, state.littleEndian);
+ offset += t.size;
+ }else{/*String*/
+ n = viewDV.getInt32(offset, state.littleEndian);
+ offset += 4;
+ v = textDecoder.decode(viewU8.slice(offset, offset+n));
+ offset += n;
+ }
+ rc.push(v);
+ }
+ }
+ if(clear) viewU8[0] = 0;
+ //log("deserialize:",argc, rc);
+ metrics.s11n.deserialize.time += performance.now() - t;
+ return rc;
+ };
+
+ /**
+ Serializes all arguments to the shared buffer for consumption
+ by the counterpart thread.
+
+ This routine is only intended for serializing OPFS VFS
+ arguments and (in at least one special case) result values,
+ and the buffer is sized to be able to comfortably handle
+ those.
+
+ If passed no arguments then it zeroes out the serialization
+ state.
+ */
+ state.s11n.serialize = function(...args){
+ const t = performance.now();
+ ++metrics.s11n.serialize.count;
+ if(args.length){
+ //log("serialize():",args);
+ const typeIds = [];
+ let i = 0, offset = 1;
+ viewU8[0] = args.length & 0xff /* header = # of args */;
+ for(; i < args.length; ++i, ++offset){
+ /* Write the TypeIds.id value into the next args.length
+ bytes. */
+ typeIds.push(getTypeId(args[i]));
+ viewU8[offset] = typeIds[i].id;
+ }
+ for(i = 0; i < args.length; ++i) {
+ /* Deserialize the following bytes based on their
+ corresponding TypeIds.id from the header. */
+ const t = typeIds[i];
+ if(t.setter){
+ viewDV[t.setter](offset, args[i], state.littleEndian);
+ offset += t.size;
+ }else{/*String*/
+ const s = textEncoder.encode(args[i]);
+ viewDV.setInt32(offset, s.byteLength, state.littleEndian);
+ offset += 4;
+ viewU8.set(s, offset);
+ offset += s.byteLength;
+ }
+ }
+ //log("serialize() result:",viewU8.slice(0,offset));
+ }else{
+ viewU8[0] = 0;
+ }
+ metrics.s11n.serialize.time += performance.now() - t;
+ };
+ return state.s11n;
+ }/*initS11n()*/;
+
+ /**
+ Generates a random ASCII string len characters long, intended for
+ use as a temporary file name.
+ */
+ const randomFilename = function f(len=16){
+ if(!f._chars){
+ f._chars = "abcdefghijklmnopqrstuvwxyz"+
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"+
+ "012346789";
+ f._n = f._chars.length;
+ }
+ const a = [];
+ let i = 0;
+ for( ; i < len; ++i){
+ const ndx = Math.random() * (f._n * 64) % f._n | 0;
+ a[i] = f._chars[ndx];
+ }
+ return a.join("");
+ /*
+ An alternative impl. with an unpredictable length
+ but much simpler:
+
+ Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36)
+ */
+ };
+
+ /**
+ Map of sqlite3_file pointers to objects constructed by xOpen().
+ */
+ const __openFiles = Object.create(null);
+
+ const opTimer = Object.create(null);
+ opTimer.op = undefined;
+ opTimer.start = undefined;
+ const mTimeStart = (op)=>{
+ opTimer.start = performance.now();
+ opTimer.op = op;
+ ++metrics[op].count;
+ };
+ const mTimeEnd = ()=>(
+ metrics[opTimer.op].time += performance.now() - opTimer.start
+ );
+
+ /**
+ Impls for the sqlite3_io_methods methods. Maintenance reminder:
+ members are in alphabetical order to simplify finding them.
+ */
+ const ioSyncWrappers = {
+ xCheckReservedLock: function(pFile,pOut){
+ /**
+ As of late 2022, only a single lock can be held on an OPFS
+ file. We have no way of checking whether any _other_ db
+ connection has a lock except by trying to obtain and (on
+ success) release a sync-handle for it, but doing so would
+ involve an inherent race condition. For the time being,
+ pending a better solution, we simply report whether the
+ given pFile is open.
+ */
+ const f = __openFiles[pFile];
+ wasm.poke(pOut, f.lockType ? 1 : 0, 'i32');
+ return 0;
+ },
+ xClose: function(pFile){
+ mTimeStart('xClose');
+ let rc = 0;
+ const f = __openFiles[pFile];
+ if(f){
+ delete __openFiles[pFile];
+ rc = opRun('xClose', pFile);
+ if(f.sq3File) f.sq3File.dispose();
+ }
+ mTimeEnd();
+ return rc;
+ },
+ xDeviceCharacteristics: function(pFile){
+ //debug("xDeviceCharacteristics(",pFile,")");
+ return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
+ },
+ xFileControl: function(pFile, opId, pArg){
+ /*mTimeStart('xFileControl');
+ mTimeEnd();*/
+ return capi.SQLITE_NOTFOUND;
+ },
+ xFileSize: function(pFile,pSz64){
+ mTimeStart('xFileSize');
+ let rc = opRun('xFileSize', pFile);
+ if(0==rc){
+ try {
+ const sz = state.s11n.deserialize()[0];
+ wasm.poke(pSz64, sz, 'i64');
+ }catch(e){
+ error("Unexpected error reading xFileSize() result:",e);
+ rc = state.sq3Codes.SQLITE_IOERR;
+ }
+ }
+ mTimeEnd();
+ return rc;
+ },
+ xLock: function(pFile,lockType){
+ mTimeStart('xLock');
+ const f = __openFiles[pFile];
+ let rc = 0;
+ /* All OPFS locks are exclusive locks. If xLock() has
+ previously succeeded, do nothing except record the lock
+ type. If no lock is active, have the async counterpart
+ lock the file. */
+ if( !f.lockType ) {
+ rc = opRun('xLock', pFile, lockType);
+ if( 0===rc ) f.lockType = lockType;
+ }else{
+ f.lockType = lockType;
+ }
+ mTimeEnd();
+ return rc;
+ },
+ xRead: function(pFile,pDest,n,offset64){
+ mTimeStart('xRead');
+ const f = __openFiles[pFile];
+ let rc;
+ try {
+ rc = opRun('xRead',pFile, n, Number(offset64));
+ if(0===rc || capi.SQLITE_IOERR_SHORT_READ===rc){
+ /**
+ Results get written to the SharedArrayBuffer f.sabView.
+ Because the heap is _not_ a SharedArrayBuffer, we have
+ to copy the results. TypedArray.set() seems to be the
+ fastest way to copy this. */
+ wasm.heap8u().set(f.sabView.subarray(0, n), pDest);
+ }
+ }catch(e){
+ error("xRead(",arguments,") failed:",e,f);
+ rc = capi.SQLITE_IOERR_READ;
+ }
+ mTimeEnd();
+ return rc;
+ },
+ xSync: function(pFile,flags){
+ mTimeStart('xSync');
+ ++metrics.xSync.count;
+ const rc = opRun('xSync', pFile, flags);
+ mTimeEnd();
+ return rc;
+ },
+ xTruncate: function(pFile,sz64){
+ mTimeStart('xTruncate');
+ const rc = opRun('xTruncate', pFile, Number(sz64));
+ mTimeEnd();
+ return rc;
+ },
+ xUnlock: function(pFile,lockType){
+ mTimeStart('xUnlock');
+ const f = __openFiles[pFile];
+ let rc = 0;
+ if( capi.SQLITE_LOCK_NONE === lockType
+ && f.lockType ){
+ rc = opRun('xUnlock', pFile, lockType);
+ }
+ if( 0===rc ) f.lockType = lockType;
+ mTimeEnd();
+ return rc;
+ },
+ xWrite: function(pFile,pSrc,n,offset64){
+ mTimeStart('xWrite');
+ const f = __openFiles[pFile];
+ let rc;
+ try {
+ f.sabView.set(wasm.heap8u().subarray(pSrc, pSrc+n));
+ rc = opRun('xWrite', pFile, n, Number(offset64));
+ }catch(e){
+ error("xWrite(",arguments,") failed:",e,f);
+ rc = capi.SQLITE_IOERR_WRITE;
+ }
+ mTimeEnd();
+ return rc;
+ }
+ }/*ioSyncWrappers*/;
+
+ /**
+ Impls for the sqlite3_vfs methods. Maintenance reminder: members
+ are in alphabetical order to simplify finding them.
+ */
+ const vfsSyncWrappers = {
+ xAccess: function(pVfs,zName,flags,pOut){
+ mTimeStart('xAccess');
+ const rc = opRun('xAccess', wasm.cstrToJs(zName));
+ wasm.poke( pOut, (rc ? 0 : 1), 'i32' );
+ mTimeEnd();
+ return 0;
+ },
+ xCurrentTime: function(pVfs,pOut){
+ /* If it turns out that we need to adjust for timezone, see:
+ https://stackoverflow.com/a/11760121/1458521 */
+ wasm.poke(pOut, 2440587.5 + (new Date().getTime()/86400000),
+ 'double');
+ return 0;
+ },
+ xCurrentTimeInt64: function(pVfs,pOut){
+ wasm.poke(pOut, (2440587.5 * 86400000) + new Date().getTime(),
+ 'i64');
+ return 0;
+ },
+ xDelete: function(pVfs, zName, doSyncDir){
+ mTimeStart('xDelete');
+ const rc = opRun('xDelete', wasm.cstrToJs(zName), doSyncDir, false);
+ mTimeEnd();
+ return rc;
+ },
+ xFullPathname: function(pVfs,zName,nOut,pOut){
+ /* Until/unless we have some notion of "current dir"
+ in OPFS, simply copy zName to pOut... */
+ const i = wasm.cstrncpy(pOut, zName, nOut);
+ return i<nOut ? 0 : capi.SQLITE_CANTOPEN
+ /*CANTOPEN is required by the docs but SQLITE_RANGE would be a closer match*/;
+ },
+ xGetLastError: function(pVfs,nOut,pOut){
+ /* TODO: store exception.message values from the async
+ partner in a dedicated SharedArrayBuffer, noting that we'd have
+ to encode them... TextEncoder can do that for us. */
+ warn("OPFS xGetLastError() has nothing sensible to return.");
+ return 0;
+ },
+ //xSleep is optionally defined below
+ xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){
+ mTimeStart('xOpen');
+ let opfsFlags = 0;
+ if(0===zName){
+ zName = randomFilename();
+ }else if('number'===typeof zName){
+ if(capi.sqlite3_uri_boolean(zName, "opfs-unlock-asap", 0)){
+ /* -----------------------^^^^^ MUST pass the untranslated
+ C-string here. */
+ opfsFlags |= state.opfsFlags.OPFS_UNLOCK_ASAP;
+ }
+ zName = wasm.cstrToJs(zName);
+ }
+ const fh = Object.create(null);
+ fh.fid = pFile;
+ fh.filename = zName;
+ fh.sab = new SharedArrayBuffer(state.fileBufferSize);
+ fh.flags = flags;
+ const rc = opRun('xOpen', pFile, zName, flags, opfsFlags);
+ if(!rc){
+ /* Recall that sqlite3_vfs::xClose() will be called, even on
+ error, unless pFile->pMethods is NULL. */
+ if(fh.readOnly){
+ wasm.poke(pOutFlags, capi.SQLITE_OPEN_READONLY, 'i32');
+ }
+ __openFiles[pFile] = fh;
+ fh.sabView = state.sabFileBufView;
+ fh.sq3File = new sqlite3_file(pFile);
+ fh.sq3File.$pMethods = opfsIoMethods.pointer;
+ fh.lockType = capi.SQLITE_LOCK_NONE;
+ }
+ mTimeEnd();
+ return rc;
+ }/*xOpen()*/
+ }/*vfsSyncWrappers*/;
+
+ if(dVfs){
+ opfsVfs.$xRandomness = dVfs.$xRandomness;
+ opfsVfs.$xSleep = dVfs.$xSleep;
+ }
+ if(!opfsVfs.$xRandomness){
+ /* If the default VFS has no xRandomness(), add a basic JS impl... */
+ vfsSyncWrappers.xRandomness = function(pVfs, nOut, pOut){
+ const heap = wasm.heap8u();
+ let i = 0;
+ for(; i < nOut; ++i) heap[pOut + i] = (Math.random()*255000) & 0xFF;
+ return i;
+ };
+ }
+ if(!opfsVfs.$xSleep){
+ /* If we can inherit an xSleep() impl from the default VFS then
+ assume it's sane and use it, otherwise install a JS-based
+ one. */
+ vfsSyncWrappers.xSleep = function(pVfs,ms){
+ Atomics.wait(state.sabOPView, state.opIds.xSleep, 0, ms);
+ return 0;
+ };
+ }
+
+ /**
+ Expects an OPFS file path. It gets resolved, such that ".."
+ components are properly expanded, and returned. If the 2nd arg
+ is true, the result is returned as an array of path elements,
+ else an absolute path string is returned.
+ */
+ opfsUtil.getResolvedPath = function(filename,splitIt){
+ const p = new URL(filename, "file://irrelevant").pathname;
+ return splitIt ? p.split('/').filter((v)=>!!v) : p;
+ };
+
+ /**
+ Takes the absolute path to a filesystem element. Returns an
+ array of [handleOfContainingDir, filename]. If the 2nd argument
+ is truthy then each directory element leading to the file is
+ created along the way. Throws if any creation or resolution
+ fails.
+ */
+ opfsUtil.getDirForFilename = async function f(absFilename, createDirs = false){
+ const path = opfsUtil.getResolvedPath(absFilename, true);
+ const filename = path.pop();
+ let dh = opfsUtil.rootDirectory;
+ for(const dirName of path){
+ if(dirName){
+ dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs});
+ }
+ }
+ return [dh, filename];
+ };
+
+ /**
+ Creates the given directory name, recursively, in
+ the OPFS filesystem. Returns true if it succeeds or the
+ directory already exists, else false.
+ */
+ opfsUtil.mkdir = async function(absDirName){
+ try {
+ await opfsUtil.getDirForFilename(absDirName+"/filepart", true);
+ return true;
+ }catch(e){
+ //sqlite3.config.warn("mkdir(",absDirName,") failed:",e);
+ return false;
+ }
+ };
+ /**
+ Checks whether the given OPFS filesystem entry exists,
+ returning true if it does, false if it doesn't.
+ */
+ opfsUtil.entryExists = async function(fsEntryName){
+ try {
+ const [dh, fn] = await opfsUtil.getDirForFilename(fsEntryName);
+ await dh.getFileHandle(fn);
+ return true;
+ }catch(e){
+ return false;
+ }
+ };
+
+ /**
+ Generates a random ASCII string, intended for use as a
+ temporary file name. Its argument is the length of the string,
+ defaulting to 16.
+ */
+ opfsUtil.randomFilename = randomFilename;
+
+ /**
+ Re-registers the OPFS VFS. This is intended only for odd use
+ cases which have to call sqlite3_shutdown() as part of their
+ initialization process, which will unregister the VFS
+ registered by installOpfsVfs(). If passed a truthy value, the
+ OPFS VFS is registered as the default VFS, else it is not made
+ the default. Returns the result of the the
+ sqlite3_vfs_register() call.
+
+ Design note: the problem of having to re-register things after
+ a shutdown/initialize pair is more general. How to best plug
+ that in to the library is unclear. In particular, we cannot
+ hook in to any C-side calls to sqlite3_initialize(), so we
+ cannot add an after-initialize callback mechanism.
+ */
+ opfsUtil.registerVfs = (asDefault=false)=>{
+ return wasm.exports.sqlite3_vfs_register(
+ opfsVfs.pointer, asDefault ? 1 : 0
+ );
+ };
+
+ /**
+ Returns a promise which resolves to an object which represents
+ all files and directories in the OPFS tree. The top-most object
+ has two properties: `dirs` is an array of directory entries
+ (described below) and `files` is a list of file names for all
+ files in that directory.
+
+ Traversal starts at sqlite3.opfs.rootDirectory.
+
+ Each `dirs` entry is an object in this form:
+
+ ```
+ { name: directoryName,
+ dirs: [...subdirs],
+ files: [...file names]
+ }
+ ```
+
+ The `files` and `subdirs` entries are always set but may be
+ empty arrays.
+
+ The returned object has the same structure but its `name` is
+ an empty string. All returned objects are created with
+ Object.create(null), so have no prototype.
+
+ Design note: the entries do not contain more information,
+ e.g. file sizes, because getting such info is not only
+ expensive but is subject to locking-related errors.
+ */
+ opfsUtil.treeList = async function(){
+ const doDir = async function callee(dirHandle,tgt){
+ tgt.name = dirHandle.name;
+ tgt.dirs = [];
+ tgt.files = [];
+ for await (const handle of dirHandle.values()){
+ if('directory' === handle.kind){
+ const subDir = Object.create(null);
+ tgt.dirs.push(subDir);
+ await callee(handle, subDir);
+ }else{
+ tgt.files.push(handle.name);
+ }
+ }
+ };
+ const root = Object.create(null);
+ await doDir(opfsUtil.rootDirectory, root);
+ return root;
+ };
+
+ /**
+ Irrevocably deletes _all_ files in the current origin's OPFS.
+ Obviously, this must be used with great caution. It may throw
+ an exception if removal of anything fails (e.g. a file is
+ locked), but the precise conditions under which the underlying
+ APIs will throw are not documented (so we cannot tell you what
+ they are).
+ */
+ opfsUtil.rmfr = async function(){
+ const dir = opfsUtil.rootDirectory, opt = {recurse: true};
+ for await (const handle of dir.values()){
+ dir.removeEntry(handle.name, opt);
+ }
+ };
+
+ /**
+ Deletes the given OPFS filesystem entry. As this environment
+ has no notion of "current directory", the given name must be an
+ absolute path. If the 2nd argument is truthy, deletion is
+ recursive (use with caution!).
+
+ The returned Promise resolves to true if the deletion was
+ successful, else false (but...). The OPFS API reports the
+ reason for the failure only in human-readable form, not
+ exceptions which can be type-checked to determine the
+ failure. Because of that...
+
+ If the final argument is truthy then this function will
+ propagate any exception on error, rather than returning false.
+ */
+ opfsUtil.unlink = async function(fsEntryName, recursive = false,
+ throwOnError = false){
+ try {
+ const [hDir, filenamePart] =
+ await opfsUtil.getDirForFilename(fsEntryName, false);
+ await hDir.removeEntry(filenamePart, {recursive});
+ return true;
+ }catch(e){
+ if(throwOnError){
+ throw new Error("unlink(",arguments[0],") failed: "+e.message,{
+ cause: e
+ });
+ }
+ return false;
+ }
+ };
+
+ /**
+ Traverses the OPFS filesystem, calling a callback for each one.
+ The argument may be either a callback function or an options object
+ with any of the following properties:
+
+ - `callback`: function which gets called for each filesystem
+ entry. It gets passed 3 arguments: 1) the
+ FileSystemFileHandle or FileSystemDirectoryHandle of each
+ entry (noting that both are instanceof FileSystemHandle). 2)
+ the FileSystemDirectoryHandle of the parent directory. 3) the
+ current depth level, with 0 being at the top of the tree
+ relative to the starting directory. If the callback returns a
+ literal false, as opposed to any other falsy value, traversal
+ stops without an error. Any exceptions it throws are
+ propagated. Results are undefined if the callback manipulate
+ the filesystem (e.g. removing or adding entries) because the
+ how OPFS iterators behave in the face of such changes is
+ undocumented.
+
+ - `recursive` [bool=true]: specifies whether to recurse into
+ subdirectories or not. Whether recursion is depth-first or
+ breadth-first is unspecified!
+
+ - `directory` [FileSystemDirectoryEntry=sqlite3.opfs.rootDirectory]
+ specifies the starting directory.
+
+ If this function is passed a function, it is assumed to be the
+ callback.
+
+ Returns a promise because it has to (by virtue of being async)
+ but that promise has no specific meaning: the traversal it
+ performs is synchronous. The promise must be used to catch any
+ exceptions propagated by the callback, however.
+
+ TODO: add an option which specifies whether to traverse
+ depth-first or breadth-first. We currently do depth-first but
+ an incremental file browsing widget would benefit more from
+ breadth-first.
+ */
+ opfsUtil.traverse = async function(opt){
+ const defaultOpt = {
+ recursive: true,
+ directory: opfsUtil.rootDirectory
+ };
+ if('function'===typeof opt){
+ opt = {callback:opt};
+ }
+ opt = Object.assign(defaultOpt, opt||{});
+ const doDir = async function callee(dirHandle, depth){
+ for await (const handle of dirHandle.values()){
+ if(false === opt.callback(handle, dirHandle, depth)) return false;
+ else if(opt.recursive && 'directory' === handle.kind){
+ if(false === await callee(handle, depth + 1)) break;
+ }
+ }
+ };
+ doDir(opt.directory, 0);
+ };
+
+ /**
+ impl of importDb() when it's given a function as its second
+ argument.
+ */
+ const importDbChunked = async function(filename, callback){
+ const [hDir, fnamePart] = await opfsUtil.getDirForFilename(filename, true);
+ const hFile = await hDir.getFileHandle(fnamePart, {create:true});
+ let sah = await hFile.createSyncAccessHandle();
+ let nWrote = 0, chunk, checkedHeader = false, err = false;
+ try{
+ sah.truncate(0);
+ while( undefined !== (chunk = await callback()) ){
+ if(chunk instanceof ArrayBuffer) chunk = new Uint8Array(chunk);
+ if( 0===nWrote && chunk.byteLength>=15 ){
+ util.affirmDbHeader(chunk);
+ checkedHeader = true;
+ }
+ sah.write(chunk, {at: nWrote});
+ nWrote += chunk.byteLength;
+ }
+ if( nWrote < 512 || 0!==nWrote % 512 ){
+ toss("Input size",nWrote,"is not correct for an SQLite database.");
+ }
+ if( !checkedHeader ){
+ const header = new Uint8Array(20);
+ sah.read( header, {at: 0} );
+ util.affirmDbHeader( header );
+ }
+ sah.write(new Uint8Array([1,1]), {at: 18}/*force db out of WAL mode*/);
+ return nWrote;
+ }catch(e){
+ await sah.close();
+ sah = undefined;
+ await hDir.removeEntry( fnamePart ).catch(()=>{});
+ throw e;
+ }finally {
+ if( sah ) await sah.close();
+ }
+ };
+
+ /**
+ Asynchronously imports the given bytes (a byte array or
+ ArrayBuffer) into the given database file.
+
+ If passed a function for its second argument, its behaviour
+ changes to async and it imports its data in chunks fed to it by
+ the given callback function. It calls the callback (which may
+ be async) repeatedly, expecting either a Uint8Array or
+ ArrayBuffer (to denote new input) or undefined (to denote
+ EOF). For so long as the callback continues to return
+ non-undefined, it will append incoming data to the given
+ VFS-hosted database file. When called this way, the resolved
+ value of the returned Promise is the number of bytes written to
+ the target file.
+
+ It very specifically requires the input to be an SQLite3
+ database and throws if that's not the case. It does so in
+ order to prevent this function from taking on a larger scope
+ than it is specifically intended to. i.e. we do not want it to
+ become a convenience for importing arbitrary files into OPFS.
+
+ This routine rewrites the database header bytes in the output
+ file (not the input array) to force disabling of WAL mode.
+
+ On error this throws and the state of the input file is
+ undefined (it depends on where the exception was triggered).
+
+ On success, resolves to the number of bytes written.
+ */
+ opfsUtil.importDb = async function(filename, bytes){
+ if( bytes instanceof Function ){
+ return importDbChunked(filename, bytes);
+ }
+ if(bytes instanceof ArrayBuffer) bytes = new Uint8Array(bytes);
+ util.affirmIsDb(bytes);
+ const n = bytes.byteLength;
+ const [hDir, fnamePart] = await opfsUtil.getDirForFilename(filename, true);
+ let sah, err, nWrote = 0;
+ try {
+ const hFile = await hDir.getFileHandle(fnamePart, {create:true});
+ sah = await hFile.createSyncAccessHandle();
+ sah.truncate(0);
+ nWrote = sah.write(bytes, {at: 0});
+ if(nWrote != n){
+ toss("Expected to write "+n+" bytes but wrote "+nWrote+".");
+ }
+ sah.write(new Uint8Array([1,1]), {at: 18}) /* force db out of WAL mode */;
+ return nWrote;
+ }catch(e){
+ if( sah ){ await sah.close(); sah = undefined; }
+ await hDir.removeEntry( fnamePart ).catch(()=>{});
+ throw e;
+ }finally{
+ if( sah ) await sah.close();
+ }
+ };
+
+ if(sqlite3.oo1){
+ const OpfsDb = function(...args){
+ const opt = sqlite3.oo1.DB.dbCtorHelper.normalizeArgs(...args);
+ opt.vfs = opfsVfs.$zName;
+ sqlite3.oo1.DB.dbCtorHelper.call(this, opt);
+ };
+ OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype);
+ sqlite3.oo1.OpfsDb = OpfsDb;
+ OpfsDb.importDb = opfsUtil.importDb;
+ sqlite3.oo1.DB.dbCtorHelper.setVfsPostOpenSql(
+ opfsVfs.pointer,
+ function(oo1Db, sqlite3){
+ /* Set a relatively high default busy-timeout handler to
+ help OPFS dbs deal with multi-tab/multi-worker
+ contention. */
+ sqlite3.capi.sqlite3_busy_timeout(oo1Db, 10000);
+ sqlite3.capi.sqlite3_exec(oo1Db, [
+ /* As of July 2023, the PERSIST journal mode on OPFS is
+ somewhat slower than DELETE or TRUNCATE (it was faster
+ before Chrome version 108 or 109). TRUNCATE and DELETE
+ have very similar performance on OPFS.
+
+ Roy Hashimoto notes that TRUNCATE and PERSIST modes may
+ decrease OPFS concurrency because multiple connections
+ can open the journal file in those modes:
+
+ https://github.com/rhashimoto/wa-sqlite/issues/68
+
+ Given that, and the fact that testing has not revealed
+ any appreciable difference between performance of
+ TRUNCATE and DELETE modes on OPFS, we currently (as of
+ 2023-07-13) default to DELETE mode.
+ */
+ "pragma journal_mode=DELETE;",
+ /*
+ This vfs benefits hugely from cache on moderate/large
+ speedtest1 --size 50 and --size 100 workloads. We
+ currently rely on setting a non-default cache size when
+ building sqlite3.wasm. If that policy changes, the cache
+ can be set here.
+ */
+ "pragma cache_size=-16384;"
+ ], 0, 0, 0);
+ }
+ );
+ }/*extend sqlite3.oo1*/
+
+ const sanityCheck = function(){
+ const scope = wasm.scopedAllocPush();
+ const sq3File = new sqlite3_file();
+ try{
+ const fid = sq3File.pointer;
+ const openFlags = capi.SQLITE_OPEN_CREATE
+ | capi.SQLITE_OPEN_READWRITE
+ //| capi.SQLITE_OPEN_DELETEONCLOSE
+ | capi.SQLITE_OPEN_MAIN_DB;
+ const pOut = wasm.scopedAlloc(8);
+ const dbFile = "/sanity/check/file"+randomFilename(8);
+ const zDbFile = wasm.scopedAllocCString(dbFile);
+ let rc;
+ state.s11n.serialize("This is ä string.");
+ rc = state.s11n.deserialize();
+ log("deserialize() says:",rc);
+ if("This is ä string."!==rc[0]) toss("String d13n error.");
+ vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
+ rc = wasm.peek(pOut,'i32');
+ log("xAccess(",dbFile,") exists ?=",rc);
+ rc = vfsSyncWrappers.xOpen(opfsVfs.pointer, zDbFile,
+ fid, openFlags, pOut);
+ log("open rc =",rc,"state.sabOPView[xOpen] =",
+ state.sabOPView[state.opIds.xOpen]);
+ if(0!==rc){
+ error("open failed with code",rc);
+ return;
+ }
+ vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
+ rc = wasm.peek(pOut,'i32');
+ if(!rc) toss("xAccess() failed to detect file.");
+ rc = ioSyncWrappers.xSync(sq3File.pointer, 0);
+ if(rc) toss('sync failed w/ rc',rc);
+ rc = ioSyncWrappers.xTruncate(sq3File.pointer, 1024);
+ if(rc) toss('truncate failed w/ rc',rc);
+ wasm.poke(pOut,0,'i64');
+ rc = ioSyncWrappers.xFileSize(sq3File.pointer, pOut);
+ if(rc) toss('xFileSize failed w/ rc',rc);
+ log("xFileSize says:",wasm.peek(pOut, 'i64'));
+ rc = ioSyncWrappers.xWrite(sq3File.pointer, zDbFile, 10, 1);
+ if(rc) toss("xWrite() failed!");
+ const readBuf = wasm.scopedAlloc(16);
+ rc = ioSyncWrappers.xRead(sq3File.pointer, readBuf, 6, 2);
+ wasm.poke(readBuf+6,0);
+ let jRead = wasm.cstrToJs(readBuf);
+ log("xRead() got:",jRead);
+ if("sanity"!==jRead) toss("Unexpected xRead() value.");
+ if(vfsSyncWrappers.xSleep){
+ log("xSleep()ing before close()ing...");
+ vfsSyncWrappers.xSleep(opfsVfs.pointer,2000);
+ log("waking up from xSleep()");
+ }
+ rc = ioSyncWrappers.xClose(fid);
+ log("xClose rc =",rc,"sabOPView =",state.sabOPView);
+ log("Deleting file:",dbFile);
+ vfsSyncWrappers.xDelete(opfsVfs.pointer, zDbFile, 0x1234);
+ vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
+ rc = wasm.peek(pOut,'i32');
+ if(rc) toss("Expecting 0 from xAccess(",dbFile,") after xDelete().");
+ warn("End of OPFS sanity checks.");
+ }finally{
+ sq3File.dispose();
+ wasm.scopedAllocPop(scope);
+ }
+ }/*sanityCheck()*/;
+
+ W.onmessage = function({data}){
+ //log("Worker.onmessage:",data);
+ switch(data.type){
+ case 'opfs-unavailable':
+ /* Async proxy has determined that OPFS is unavailable. There's
+ nothing more for us to do here. */
+ promiseReject(new Error(data.payload.join(' ')));
+ break;
+ case 'opfs-async-loaded':
+ /* Arrives as soon as the asyc proxy finishes loading.
+ Pass our config and shared state on to the async
+ worker. */
+ W.postMessage({type: 'opfs-async-init',args: state});
+ break;
+ case 'opfs-async-inited': {
+ /* Indicates that the async partner has received the 'init'
+ and has finished initializing, so the real work can
+ begin... */
+ if(true===promiseWasRejected){
+ break /* promise was already rejected via timer */;
+ }
+ try {
+ sqlite3.vfs.installVfs({
+ io: {struct: opfsIoMethods, methods: ioSyncWrappers},
+ vfs: {struct: opfsVfs, methods: vfsSyncWrappers}
+ });
+ state.sabOPView = new Int32Array(state.sabOP);
+ state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
+ state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
+ initS11n();
+ if(options.sanityChecks){
+ warn("Running sanity checks because of opfs-sanity-check URL arg...");
+ sanityCheck();
+ }
+ if(thisThreadHasOPFS()){
+ navigator.storage.getDirectory().then((d)=>{
+ W.onerror = W._originalOnError;
+ delete W._originalOnError;
+ sqlite3.opfs = opfsUtil;
+ opfsUtil.rootDirectory = d;
+ log("End of OPFS sqlite3_vfs setup.", opfsVfs);
+ promiseResolve();
+ }).catch(promiseReject);
+ }else{
+ promiseResolve();
+ }
+ }catch(e){
+ error(e);
+ promiseReject(e);
+ }
+ break;
+ }
+ default: {
+ const errMsg = (
+ "Unexpected message from the OPFS async worker: " +
+ JSON.stringify(data)
+ );
+ error(errMsg);
+ promiseReject(new Error(errMsg));
+ break;
+ }
+ }/*switch(data.type)*/
+ }/*W.onmessage()*/;
+ })/*thePromise*/;
+ return thePromise;
+}/*installOpfsVfs()*/;
+installOpfsVfs.defaultProxyUri =
+ "sqlite3-opfs-async-proxy.js";
+globalThis.sqlite3ApiBootstrap.initializersAsync.push(async (sqlite3)=>{
+ try{
+ let proxyJs = installOpfsVfs.defaultProxyUri;
+ if(sqlite3.scriptInfo.sqlite3Dir){
+ installOpfsVfs.defaultProxyUri =
+ sqlite3.scriptInfo.sqlite3Dir + proxyJs;
+ //sqlite3.config.warn("installOpfsVfs.defaultProxyUri =",installOpfsVfs.defaultProxyUri);
+ }
+ return installOpfsVfs().catch((e)=>{
+ sqlite3.config.warn("Ignoring inability to install OPFS sqlite3_vfs:",e.message);
+ });
+ }catch(e){
+ sqlite3.config.error("installOpfsVfs() exception:",e);
+ return Promise.reject(e);
+ }
+});
+}/*sqlite3ApiBootstrap.initializers.push()*/);
+//#else
+/* The OPFS VFS parts are elided from builds targeting node.js. */
+//#endif target=node
diff --git a/ext/wasm/api/sqlite3-wasm.c b/ext/wasm/api/sqlite3-wasm.c
new file mode 100644
index 0000000..618d0f0
--- /dev/null
+++ b/ext/wasm/api/sqlite3-wasm.c
@@ -0,0 +1,1928 @@
+/*
+** This file requires access to sqlite3.c static state in order to
+** implement certain WASM-specific features, and thus directly
+** includes that file. Unlike the rest of sqlite3.c, this file
+** requires compiling with -std=c99 (or equivalent, or a later C
+** version) because it makes use of features not available in C89.
+**
+** At its simplest, to build sqlite3.wasm either place this file
+** in the same directory as sqlite3.c/h before compilation or use the
+** -I/path flag to tell the compiler where to find both of those
+** files, then compile this file. For example:
+**
+** emcc -o sqlite3.wasm ... -I/path/to/sqlite3-c-and-h sqlite3-wasm.c
+*/
+#define SQLITE_WASM
+#ifdef SQLITE_WASM_ENABLE_C_TESTS
+/*
+** Code blocked off by SQLITE_WASM_TESTS is intended solely for use in
+** unit/regression testing. They may be safely omitted from
+** client-side builds. The main unit test script, tester1.js, will
+** skip related tests if it doesn't find the corresponding functions
+** in the WASM exports.
+*/
+# define SQLITE_WASM_TESTS 1
+#else
+# define SQLITE_WASM_TESTS 0
+#endif
+
+/*
+** Threading and file locking: JS is single-threaded. Each Worker
+** thread is a separate instance of the JS engine so can never access
+** the same db handle as another thread, thus multi-threading support
+** is unnecessary in the library. Because the filesystems are virtual
+** and local to a given wasm runtime instance, two Workers can never
+** access the same db file at once, with the exception of OPFS.
+**
+** Summary: except for the case of OPFS, which supports locking using
+** its own API, threading and file locking support are unnecessary in
+** the wasm build.
+*/
+
+/*
+** Undefine any SQLITE_... config flags which we specifically do not
+** want defined. Please keep these alphabetized.
+*/
+#undef SQLITE_OMIT_DESERIALIZE
+#undef SQLITE_OMIT_MEMORYDB
+
+/*
+** Define any SQLITE_... config defaults we want if they aren't
+** overridden by the builder. Please keep these alphabetized.
+*/
+
+/**********************************************************************/
+/* SQLITE_D... */
+#ifndef SQLITE_DEFAULT_CACHE_SIZE
+/*
+** The OPFS impls benefit tremendously from an increased cache size
+** when working on large workloads, e.g. speedtest1 --size 50 or
+** higher. On smaller workloads, e.g. speedtest1 --size 25, they
+** clearly benefit from having 4mb of cache, but not as much as a
+** larger cache benefits the larger workloads. Speed differences
+** between 2x and nearly 3x have been measured with ample page cache.
+*/
+# define SQLITE_DEFAULT_CACHE_SIZE -16384
+#endif
+#if !defined(SQLITE_DEFAULT_PAGE_SIZE)
+/*
+** OPFS performance is improved by approx. 12% with a page size of 8kb
+** instead of 4kb. Performance with 16kb is equivalent to 8kb.
+**
+** Performance difference of kvvfs with a page size of 8kb compared to
+** 4kb, as measured by speedtest1 --size 4, is indeterminate:
+** measurements are all over the place either way and not
+** significantly different.
+*/
+# define SQLITE_DEFAULT_PAGE_SIZE 8192
+#endif
+#ifndef SQLITE_DEFAULT_UNIX_VFS
+# define SQLITE_DEFAULT_UNIX_VFS "unix-none"
+#endif
+#undef SQLITE_DQS
+#define SQLITE_DQS 0
+
+/**********************************************************************/
+/* SQLITE_ENABLE_... */
+/*
+** Unconditionally enable API_ARMOR in the WASM build. It ensures that
+** public APIs behave predictable in the face of passing illegal NULLs
+** or ranges which might otherwise invoke undefined behavior.
+*/
+#undef SQLITE_ENABLE_API_ARMOR
+#define SQLITE_ENABLE_API_ARMOR 1
+
+#ifndef SQLITE_ENABLE_BYTECODE_VTAB
+# define SQLITE_ENABLE_BYTECODE_VTAB 1
+#endif
+#ifndef SQLITE_ENABLE_DBPAGE_VTAB
+# define SQLITE_ENABLE_DBPAGE_VTAB 1
+#endif
+#ifndef SQLITE_ENABLE_DBSTAT_VTAB
+# define SQLITE_ENABLE_DBSTAT_VTAB 1
+#endif
+#ifndef SQLITE_ENABLE_EXPLAIN_COMMENTS
+# define SQLITE_ENABLE_EXPLAIN_COMMENTS 1
+#endif
+#ifndef SQLITE_ENABLE_FTS4
+# define SQLITE_ENABLE_FTS4 1
+#endif
+#ifndef SQLITE_ENABLE_MATH_FUNCTIONS
+# define SQLITE_ENABLE_MATH_FUNCTIONS 1
+#endif
+#ifndef SQLITE_ENABLE_OFFSET_SQL_FUNC
+# define SQLITE_ENABLE_OFFSET_SQL_FUNC 1
+#endif
+#ifndef SQLITE_ENABLE_PREUPDATE_HOOK
+# define SQLITE_ENABLE_PREUPDATE_HOOK 1 /*required by session extension*/
+#endif
+#ifndef SQLITE_ENABLE_RTREE
+# define SQLITE_ENABLE_RTREE 1
+#endif
+#ifndef SQLITE_ENABLE_SESSION
+# define SQLITE_ENABLE_SESSION 1
+#endif
+#ifndef SQLITE_ENABLE_STMTVTAB
+# define SQLITE_ENABLE_STMTVTAB 1
+#endif
+#ifndef SQLITE_ENABLE_UNKNOWN_SQL_FUNCTION
+# define SQLITE_ENABLE_UNKNOWN_SQL_FUNCTION
+#endif
+
+/**********************************************************************/
+/* SQLITE_O... */
+#ifndef SQLITE_OMIT_DEPRECATED
+# define SQLITE_OMIT_DEPRECATED 1
+#endif
+#ifndef SQLITE_OMIT_LOAD_EXTENSION
+# define SQLITE_OMIT_LOAD_EXTENSION 1
+#endif
+#ifndef SQLITE_OMIT_SHARED_CACHE
+# define SQLITE_OMIT_SHARED_CACHE 1
+#endif
+#ifndef SQLITE_OMIT_UTF16
+# define SQLITE_OMIT_UTF16 1
+#endif
+#ifndef SQLITE_OS_KV_OPTIONAL
+# define SQLITE_OS_KV_OPTIONAL 1
+#endif
+
+/**********************************************************************/
+/* SQLITE_S... */
+#ifndef SQLITE_STRICT_SUBTYPE
+# define SQLITE_STRICT_SUBTYPE 1
+#endif
+
+/**********************************************************************/
+/* SQLITE_T... */
+#ifndef SQLITE_TEMP_STORE
+# define SQLITE_TEMP_STORE 2
+#endif
+#ifndef SQLITE_THREADSAFE
+# define SQLITE_THREADSAFE 0
+#endif
+
+/**********************************************************************/
+/* SQLITE_USE_... */
+#ifndef SQLITE_USE_URI
+# define SQLITE_USE_URI 1
+#endif
+
+#ifdef SQLITE_WASM_EXTRA_INIT
+# define SQLITE_EXTRA_INIT sqlite3_wasm_extra_init
+#endif
+
+#include <assert.h>
+
+/*
+** SQLITE_WASM_EXPORT is functionally identical to EMSCRIPTEN_KEEPALIVE
+** but is not Emscripten-specific. It explicitly marks functions for
+** export into the target wasm file without requiring explicit listing
+** of those functions in Emscripten's -sEXPORTED_FUNCTIONS=... list
+** (or equivalent in other build platforms). Any function with neither
+** this attribute nor which is listed as an explicit export will not
+** be exported from the wasm file (but may still be used internally
+** within the wasm file).
+**
+** The functions in this file (sqlite3-wasm.c) which require exporting
+** are marked with this flag. They may also be added to any explicit
+** build-time export list but need not be. All of these APIs are
+** intended for use only within the project's own JS/WASM code, and
+** not by client code, so an argument can be made for reducing their
+** visibility by not including them in any build-time export lists.
+**
+** 2022-09-11: it's not yet _proven_ that this approach works in
+** non-Emscripten builds. If not, such builds will need to export
+** those using the --export=... wasm-ld flag (or equivalent). As of
+** this writing we are tied to Emscripten for various reasons
+** and cannot test the library with other build environments.
+*/
+#define SQLITE_WASM_EXPORT __attribute__((used,visibility("default")))
+// See also:
+//__attribute__((export_name("theExportedName"), used, visibility("default")))
+
+/*
+** Which sqlite3.c we're using needs to be configurable to enable
+** building against a custom copy, e.g. the SEE variant. Note that we
+** #include the .c file, rather than the header, so that the WASM
+** extensions have access to private API internals.
+**
+** The caveat here is that custom variants need to account for
+** exporting any necessary symbols (e.g. sqlite3_activate_see()). We
+** cannot export them from here using SQLITE_WASM_EXPORT because that
+** attribute (apparently) has to be part of the function definition.
+*/
+#ifndef SQLITE_C
+# define SQLITE_C sqlite3.c /* yes, .c instead of .h. */
+#endif
+#define INC__STRINGIFY_(f) #f
+#define INC__STRINGIFY(f) INC__STRINGIFY_(f)
+#include INC__STRINGIFY(SQLITE_C)
+#undef INC__STRINGIFY_
+#undef INC__STRINGIFY
+#undef SQLITE_C
+
+#if defined(__EMSCRIPTEN__)
+# include <emscripten/console.h>
+#endif
+
+#if 0
+/*
+** An EXPERIMENT in implementing a stack-based allocator analog to
+** Emscripten's stackSave(), stackAlloc(), stackRestore().
+** Unfortunately, this cannot work together with Emscripten because
+** Emscripten defines its own native one and we'd stomp on each
+** other's memory. Other than that complication, basic tests show it
+** to work just fine.
+**
+** Another option is to malloc() a chunk of our own and call that our
+** "stack".
+*/
+SQLITE_WASM_EXPORT void * sqlite3_wasm_stack_end(void){
+ extern void __heap_base
+ /* see https://stackoverflow.com/questions/10038964 */;
+ return &__heap_base;
+}
+SQLITE_WASM_EXPORT void * sqlite3_wasm_stack_begin(void){
+ extern void __data_end;
+ return &__data_end;
+}
+static void * pWasmStackPtr = 0;
+SQLITE_WASM_EXPORT void * sqlite3_wasm_stack_ptr(void){
+ if(!pWasmStackPtr) pWasmStackPtr = sqlite3_wasm_stack_end();
+ return pWasmStackPtr;
+}
+SQLITE_WASM_EXPORT void sqlite3_wasm_stack_restore(void * p){
+ pWasmStackPtr = p;
+}
+SQLITE_WASM_EXPORT void * sqlite3_wasm_stack_alloc(int n){
+ if(n<=0) return 0;
+ n = (n + 7) & ~7 /* align to 8-byte boundary */;
+ unsigned char * const p = (unsigned char *)sqlite3_wasm_stack_ptr();
+ unsigned const char * const b = (unsigned const char *)sqlite3_wasm_stack_begin();
+ if(b + n >= p || b + n < b/*overflow*/) return 0;
+ return pWasmStackPtr = p - n;
+}
+#endif /* stack allocator experiment */
+
+/*
+** State for the "pseudo-stack" allocator implemented in
+** sqlite3_wasm_pstack_xyz(). In order to avoid colliding with
+** Emscripten-controled stack space, it carves out a bit of stack
+** memory to use for that purpose. This memory ends up in the
+** WASM-managed memory, such that routines which manipulate the wasm
+** heap can also be used to manipulate this memory.
+**
+** This particular allocator is intended for small allocations such as
+** storage for output pointers. We cannot reasonably size it large
+** enough for general-purpose string conversions because some of our
+** tests use input files (strings) of 16MB+.
+*/
+static unsigned char PStack_mem[512 * 8] = {0};
+static struct {
+ unsigned const char * const pBegin;/* Start (inclusive) of memory */
+ unsigned const char * const pEnd; /* One-after-the-end of memory */
+ unsigned char * pPos; /* Current stack pointer */
+} PStack = {
+ &PStack_mem[0],
+ &PStack_mem[0] + sizeof(PStack_mem),
+ &PStack_mem[0] + sizeof(PStack_mem)
+};
+/*
+** Returns the current pstack position.
+*/
+SQLITE_WASM_EXPORT void * sqlite3_wasm_pstack_ptr(void){
+ return PStack.pPos;
+}
+/*
+** Sets the pstack position poitner to p. Results are undefined if the
+** given value did not come from sqlite3_wasm_pstack_ptr().
+*/
+SQLITE_WASM_EXPORT void sqlite3_wasm_pstack_restore(unsigned char * p){
+ assert(p>=PStack.pBegin && p<=PStack.pEnd && p>=PStack.pPos);
+ assert(0==((unsigned long long)p & 0x7));
+ if(p>=PStack.pBegin && p<=PStack.pEnd /*&& p>=PStack.pPos*/){
+ PStack.pPos = p;
+ }
+}
+/*
+** Allocate and zero out n bytes from the pstack. Returns a pointer to
+** the memory on success, 0 on error (including a negative n value). n
+** is always adjusted to be a multiple of 8 and returned memory is
+** always zeroed out before returning (because this keeps the client
+** JS code from having to do so, and most uses of the pstack will
+** call for doing so).
+*/
+SQLITE_WASM_EXPORT void * sqlite3_wasm_pstack_alloc(int n){
+ if( n<=0 ) return 0;
+ //if( n & 0x7 ) n += 8 - (n & 0x7) /* align to 8-byte boundary */;
+ n = (n + 7) & ~7 /* align to 8-byte boundary */;
+ if( PStack.pBegin + n > PStack.pPos /*not enough space left*/
+ || PStack.pBegin + n <= PStack.pBegin /*overflow*/ ) return 0;
+ memset((PStack.pPos = PStack.pPos - n), 0, (unsigned int)n);
+ return PStack.pPos;
+}
+/*
+** Return the number of bytes left which can be
+** sqlite3_wasm_pstack_alloc()'d.
+*/
+SQLITE_WASM_EXPORT int sqlite3_wasm_pstack_remaining(void){
+ assert(PStack.pPos >= PStack.pBegin);
+ assert(PStack.pPos <= PStack.pEnd);
+ return (int)(PStack.pPos - PStack.pBegin);
+}
+
+/*
+** Return the total number of bytes available in the pstack, including
+** any space which is currently allocated. This value is a
+** compile-time constant.
+*/
+SQLITE_WASM_EXPORT int sqlite3_wasm_pstack_quota(void){
+ return (int)(PStack.pEnd - PStack.pBegin);
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** For purposes of certain hand-crafted C/Wasm function bindings, we
+** need a way of reporting errors which is consistent with the rest of
+** the C API, as opposed to throwing JS exceptions. To that end, this
+** internal-use-only function is a thin proxy around
+** sqlite3ErrorWithMessage(). The intent is that it only be used from
+** Wasm bindings such as sqlite3_prepare_v2/v3(), and definitely not
+** from client code.
+**
+** Returns err_code.
+*/
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_db_error(sqlite3*db, int err_code, const char *zMsg){
+ if( db!=0 ){
+ if( 0!=zMsg ){
+ const int nMsg = sqlite3Strlen30(zMsg);
+ sqlite3_mutex_enter(sqlite3_db_mutex(db));
+ sqlite3ErrorWithMsg(db, err_code, "%.*s", nMsg, zMsg);
+ sqlite3_mutex_leave(sqlite3_db_mutex(db));
+ }else{
+ sqlite3ErrorWithMsg(db, err_code, NULL);
+ }
+ }
+ return err_code;
+}
+
+#if SQLITE_WASM_TESTS
+struct WasmTestStruct {
+ int v4;
+ void * ppV;
+ const char * cstr;
+ int64_t v8;
+ void (*xFunc)(void*);
+};
+typedef struct WasmTestStruct WasmTestStruct;
+SQLITE_WASM_EXPORT
+void sqlite3_wasm_test_struct(WasmTestStruct * s){
+ if(s){
+ s->v4 *= 2;
+ s->v8 = s->v4 * 2;
+ s->ppV = s;
+ s->cstr = __FILE__;
+ if(s->xFunc) s->xFunc(s);
+ }
+ return;
+}
+#endif /* SQLITE_WASM_TESTS */
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings. Unlike the
+** rest of the sqlite3 API, this part requires C99 for snprintf() and
+** variadic macros.
+**
+** Returns a string containing a JSON-format "enum" of C-level
+** constants and struct-related metadata intended to be imported into
+** the JS environment. The JSON is initialized the first time this
+** function is called and that result is reused for all future calls.
+**
+** If this function returns NULL then it means that the internal
+** buffer is not large enough for the generated JSON and needs to be
+** increased. In debug builds that will trigger an assert().
+*/
+SQLITE_WASM_EXPORT
+const char * sqlite3_wasm_enum_json(void){
+ static char aBuffer[1024 * 20] = {0} /* where the JSON goes */;
+ int n = 0, nChildren = 0, nStruct = 0
+ /* output counters for figuring out where commas go */;
+ char * zPos = &aBuffer[1] /* skip first byte for now to help protect
+ ** against a small race condition */;
+ char const * const zEnd = &aBuffer[0] + sizeof(aBuffer) /* one-past-the-end */;
+ if(aBuffer[0]) return aBuffer;
+ /* Leave aBuffer[0] at 0 until the end to help guard against a tiny
+ ** race condition. If this is called twice concurrently, they might
+ ** end up both writing to aBuffer, but they'll both write the same
+ ** thing, so that's okay. If we set byte 0 up front then the 2nd
+ ** instance might return and use the string before the 1st instance
+ ** is done filling it. */
+
+/* Core output macros... */
+#define lenCheck assert(zPos < zEnd - 128 \
+ && "sqlite3_wasm_enum_json() buffer is too small."); \
+ if( zPos >= zEnd - 128 ) return 0
+#define outf(format,...) \
+ zPos += snprintf(zPos, ((size_t)(zEnd - zPos)), format, __VA_ARGS__); \
+ lenCheck
+#define out(TXT) outf("%s",TXT)
+#define CloseBrace(LEVEL) \
+ assert(LEVEL<5); memset(zPos, '}', LEVEL); zPos+=LEVEL; lenCheck
+
+/* Macros for emitting maps of integer- and string-type macros to
+** their values. */
+#define DefGroup(KEY) n = 0; \
+ outf("%s\"" #KEY "\": {",(nChildren++ ? "," : ""));
+#define DefInt(KEY) \
+ outf("%s\"%s\": %d", (n++ ? ", " : ""), #KEY, (int)KEY)
+#define DefStr(KEY) \
+ outf("%s\"%s\": \"%s\"", (n++ ? ", " : ""), #KEY, KEY)
+#define _DefGroup CloseBrace(1)
+
+ /* The following groups are sorted alphabetic by group name. */
+ DefGroup(access){
+ DefInt(SQLITE_ACCESS_EXISTS);
+ DefInt(SQLITE_ACCESS_READWRITE);
+ DefInt(SQLITE_ACCESS_READ)/*docs say this is unused*/;
+ } _DefGroup;
+
+ DefGroup(authorizer){
+ DefInt(SQLITE_DENY);
+ DefInt(SQLITE_IGNORE);
+ DefInt(SQLITE_CREATE_INDEX);
+ DefInt(SQLITE_CREATE_TABLE);
+ DefInt(SQLITE_CREATE_TEMP_INDEX);
+ DefInt(SQLITE_CREATE_TEMP_TABLE);
+ DefInt(SQLITE_CREATE_TEMP_TRIGGER);
+ DefInt(SQLITE_CREATE_TEMP_VIEW);
+ DefInt(SQLITE_CREATE_TRIGGER);
+ DefInt(SQLITE_CREATE_VIEW);
+ DefInt(SQLITE_DELETE);
+ DefInt(SQLITE_DROP_INDEX);
+ DefInt(SQLITE_DROP_TABLE);
+ DefInt(SQLITE_DROP_TEMP_INDEX);
+ DefInt(SQLITE_DROP_TEMP_TABLE);
+ DefInt(SQLITE_DROP_TEMP_TRIGGER);
+ DefInt(SQLITE_DROP_TEMP_VIEW);
+ DefInt(SQLITE_DROP_TRIGGER);
+ DefInt(SQLITE_DROP_VIEW);
+ DefInt(SQLITE_INSERT);
+ DefInt(SQLITE_PRAGMA);
+ DefInt(SQLITE_READ);
+ DefInt(SQLITE_SELECT);
+ DefInt(SQLITE_TRANSACTION);
+ DefInt(SQLITE_UPDATE);
+ DefInt(SQLITE_ATTACH);
+ DefInt(SQLITE_DETACH);
+ DefInt(SQLITE_ALTER_TABLE);
+ DefInt(SQLITE_REINDEX);
+ DefInt(SQLITE_ANALYZE);
+ DefInt(SQLITE_CREATE_VTABLE);
+ DefInt(SQLITE_DROP_VTABLE);
+ DefInt(SQLITE_FUNCTION);
+ DefInt(SQLITE_SAVEPOINT);
+ //DefInt(SQLITE_COPY) /* No longer used */;
+ DefInt(SQLITE_RECURSIVE);
+ } _DefGroup;
+
+ DefGroup(blobFinalizers) {
+ /* SQLITE_STATIC/TRANSIENT need to be handled explicitly as
+ ** integers to avoid casting-related warnings. */
+ out("\"SQLITE_STATIC\":0, \"SQLITE_TRANSIENT\":-1");
+ outf(",\"SQLITE_WASM_DEALLOC\": %lld",
+ (sqlite3_int64)(sqlite3_free));
+ } _DefGroup;
+
+ DefGroup(changeset){
+ DefInt(SQLITE_CHANGESETSTART_INVERT);
+ DefInt(SQLITE_CHANGESETAPPLY_NOSAVEPOINT);
+ DefInt(SQLITE_CHANGESETAPPLY_INVERT);
+ DefInt(SQLITE_CHANGESETAPPLY_IGNORENOOP);
+
+ DefInt(SQLITE_CHANGESET_DATA);
+ DefInt(SQLITE_CHANGESET_NOTFOUND);
+ DefInt(SQLITE_CHANGESET_CONFLICT);
+ DefInt(SQLITE_CHANGESET_CONSTRAINT);
+ DefInt(SQLITE_CHANGESET_FOREIGN_KEY);
+
+ DefInt(SQLITE_CHANGESET_OMIT);
+ DefInt(SQLITE_CHANGESET_REPLACE);
+ DefInt(SQLITE_CHANGESET_ABORT);
+ } _DefGroup;
+
+ DefGroup(config){
+ DefInt(SQLITE_CONFIG_SINGLETHREAD);
+ DefInt(SQLITE_CONFIG_MULTITHREAD);
+ DefInt(SQLITE_CONFIG_SERIALIZED);
+ DefInt(SQLITE_CONFIG_MALLOC);
+ DefInt(SQLITE_CONFIG_GETMALLOC);
+ DefInt(SQLITE_CONFIG_SCRATCH);
+ DefInt(SQLITE_CONFIG_PAGECACHE);
+ DefInt(SQLITE_CONFIG_HEAP);
+ DefInt(SQLITE_CONFIG_MEMSTATUS);
+ DefInt(SQLITE_CONFIG_MUTEX);
+ DefInt(SQLITE_CONFIG_GETMUTEX);
+/* previously SQLITE_CONFIG_CHUNKALLOC 12 which is now unused. */
+ DefInt(SQLITE_CONFIG_LOOKASIDE);
+ DefInt(SQLITE_CONFIG_PCACHE);
+ DefInt(SQLITE_CONFIG_GETPCACHE);
+ DefInt(SQLITE_CONFIG_LOG);
+ DefInt(SQLITE_CONFIG_URI);
+ DefInt(SQLITE_CONFIG_PCACHE2);
+ DefInt(SQLITE_CONFIG_GETPCACHE2);
+ DefInt(SQLITE_CONFIG_COVERING_INDEX_SCAN);
+ DefInt(SQLITE_CONFIG_SQLLOG);
+ DefInt(SQLITE_CONFIG_MMAP_SIZE);
+ DefInt(SQLITE_CONFIG_WIN32_HEAPSIZE);
+ DefInt(SQLITE_CONFIG_PCACHE_HDRSZ);
+ DefInt(SQLITE_CONFIG_PMASZ);
+ DefInt(SQLITE_CONFIG_STMTJRNL_SPILL);
+ DefInt(SQLITE_CONFIG_SMALL_MALLOC);
+ DefInt(SQLITE_CONFIG_SORTERREF_SIZE);
+ DefInt(SQLITE_CONFIG_MEMDB_MAXSIZE);
+ } _DefGroup;
+
+ DefGroup(dataTypes) {
+ DefInt(SQLITE_INTEGER);
+ DefInt(SQLITE_FLOAT);
+ DefInt(SQLITE_TEXT);
+ DefInt(SQLITE_BLOB);
+ DefInt(SQLITE_NULL);
+ } _DefGroup;
+
+ DefGroup(dbConfig){
+ DefInt(SQLITE_DBCONFIG_MAINDBNAME);
+ DefInt(SQLITE_DBCONFIG_LOOKASIDE);
+ DefInt(SQLITE_DBCONFIG_ENABLE_FKEY);
+ DefInt(SQLITE_DBCONFIG_ENABLE_TRIGGER);
+ DefInt(SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER);
+ DefInt(SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION);
+ DefInt(SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE);
+ DefInt(SQLITE_DBCONFIG_ENABLE_QPSG);
+ DefInt(SQLITE_DBCONFIG_TRIGGER_EQP);
+ DefInt(SQLITE_DBCONFIG_RESET_DATABASE);
+ DefInt(SQLITE_DBCONFIG_DEFENSIVE);
+ DefInt(SQLITE_DBCONFIG_WRITABLE_SCHEMA);
+ DefInt(SQLITE_DBCONFIG_LEGACY_ALTER_TABLE);
+ DefInt(SQLITE_DBCONFIG_DQS_DML);
+ DefInt(SQLITE_DBCONFIG_DQS_DDL);
+ DefInt(SQLITE_DBCONFIG_ENABLE_VIEW);
+ DefInt(SQLITE_DBCONFIG_LEGACY_FILE_FORMAT);
+ DefInt(SQLITE_DBCONFIG_TRUSTED_SCHEMA);
+ DefInt(SQLITE_DBCONFIG_STMT_SCANSTATUS);
+ DefInt(SQLITE_DBCONFIG_REVERSE_SCANORDER);
+ DefInt(SQLITE_DBCONFIG_MAX);
+ } _DefGroup;
+
+ DefGroup(dbStatus){
+ DefInt(SQLITE_DBSTATUS_LOOKASIDE_USED);
+ DefInt(SQLITE_DBSTATUS_CACHE_USED);
+ DefInt(SQLITE_DBSTATUS_SCHEMA_USED);
+ DefInt(SQLITE_DBSTATUS_STMT_USED);
+ DefInt(SQLITE_DBSTATUS_LOOKASIDE_HIT);
+ DefInt(SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE);
+ DefInt(SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL);
+ DefInt(SQLITE_DBSTATUS_CACHE_HIT);
+ DefInt(SQLITE_DBSTATUS_CACHE_MISS);
+ DefInt(SQLITE_DBSTATUS_CACHE_WRITE);
+ DefInt(SQLITE_DBSTATUS_DEFERRED_FKS);
+ DefInt(SQLITE_DBSTATUS_CACHE_USED_SHARED);
+ DefInt(SQLITE_DBSTATUS_CACHE_SPILL);
+ DefInt(SQLITE_DBSTATUS_MAX);
+ } _DefGroup;
+
+ DefGroup(encodings) {
+ /* Noting that the wasm binding only aims to support UTF-8. */
+ DefInt(SQLITE_UTF8);
+ DefInt(SQLITE_UTF16LE);
+ DefInt(SQLITE_UTF16BE);
+ DefInt(SQLITE_UTF16);
+ /*deprecated DefInt(SQLITE_ANY); */
+ DefInt(SQLITE_UTF16_ALIGNED);
+ } _DefGroup;
+
+ DefGroup(fcntl) {
+ DefInt(SQLITE_FCNTL_LOCKSTATE);
+ DefInt(SQLITE_FCNTL_GET_LOCKPROXYFILE);
+ DefInt(SQLITE_FCNTL_SET_LOCKPROXYFILE);
+ DefInt(SQLITE_FCNTL_LAST_ERRNO);
+ DefInt(SQLITE_FCNTL_SIZE_HINT);
+ DefInt(SQLITE_FCNTL_CHUNK_SIZE);
+ DefInt(SQLITE_FCNTL_FILE_POINTER);
+ DefInt(SQLITE_FCNTL_SYNC_OMITTED);
+ DefInt(SQLITE_FCNTL_WIN32_AV_RETRY);
+ DefInt(SQLITE_FCNTL_PERSIST_WAL);
+ DefInt(SQLITE_FCNTL_OVERWRITE);
+ DefInt(SQLITE_FCNTL_VFSNAME);
+ DefInt(SQLITE_FCNTL_POWERSAFE_OVERWRITE);
+ DefInt(SQLITE_FCNTL_PRAGMA);
+ DefInt(SQLITE_FCNTL_BUSYHANDLER);
+ DefInt(SQLITE_FCNTL_TEMPFILENAME);
+ DefInt(SQLITE_FCNTL_MMAP_SIZE);
+ DefInt(SQLITE_FCNTL_TRACE);
+ DefInt(SQLITE_FCNTL_HAS_MOVED);
+ DefInt(SQLITE_FCNTL_SYNC);
+ DefInt(SQLITE_FCNTL_COMMIT_PHASETWO);
+ DefInt(SQLITE_FCNTL_WIN32_SET_HANDLE);
+ DefInt(SQLITE_FCNTL_WAL_BLOCK);
+ DefInt(SQLITE_FCNTL_ZIPVFS);
+ DefInt(SQLITE_FCNTL_RBU);
+ DefInt(SQLITE_FCNTL_VFS_POINTER);
+ DefInt(SQLITE_FCNTL_JOURNAL_POINTER);
+ DefInt(SQLITE_FCNTL_WIN32_GET_HANDLE);
+ DefInt(SQLITE_FCNTL_PDB);
+ DefInt(SQLITE_FCNTL_BEGIN_ATOMIC_WRITE);
+ DefInt(SQLITE_FCNTL_COMMIT_ATOMIC_WRITE);
+ DefInt(SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE);
+ DefInt(SQLITE_FCNTL_LOCK_TIMEOUT);
+ DefInt(SQLITE_FCNTL_DATA_VERSION);
+ DefInt(SQLITE_FCNTL_SIZE_LIMIT);
+ DefInt(SQLITE_FCNTL_CKPT_DONE);
+ DefInt(SQLITE_FCNTL_RESERVE_BYTES);
+ DefInt(SQLITE_FCNTL_CKPT_START);
+ DefInt(SQLITE_FCNTL_EXTERNAL_READER);
+ DefInt(SQLITE_FCNTL_CKSM_FILE);
+ DefInt(SQLITE_FCNTL_RESET_CACHE);
+ } _DefGroup;
+
+ DefGroup(flock) {
+ DefInt(SQLITE_LOCK_NONE);
+ DefInt(SQLITE_LOCK_SHARED);
+ DefInt(SQLITE_LOCK_RESERVED);
+ DefInt(SQLITE_LOCK_PENDING);
+ DefInt(SQLITE_LOCK_EXCLUSIVE);
+ } _DefGroup;
+
+ DefGroup(ioCap) {
+ DefInt(SQLITE_IOCAP_ATOMIC);
+ DefInt(SQLITE_IOCAP_ATOMIC512);
+ DefInt(SQLITE_IOCAP_ATOMIC1K);
+ DefInt(SQLITE_IOCAP_ATOMIC2K);
+ DefInt(SQLITE_IOCAP_ATOMIC4K);
+ DefInt(SQLITE_IOCAP_ATOMIC8K);
+ DefInt(SQLITE_IOCAP_ATOMIC16K);
+ DefInt(SQLITE_IOCAP_ATOMIC32K);
+ DefInt(SQLITE_IOCAP_ATOMIC64K);
+ DefInt(SQLITE_IOCAP_SAFE_APPEND);
+ DefInt(SQLITE_IOCAP_SEQUENTIAL);
+ DefInt(SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN);
+ DefInt(SQLITE_IOCAP_POWERSAFE_OVERWRITE);
+ DefInt(SQLITE_IOCAP_IMMUTABLE);
+ DefInt(SQLITE_IOCAP_BATCH_ATOMIC);
+ } _DefGroup;
+
+ DefGroup(limits) {
+ DefInt(SQLITE_MAX_ALLOCATION_SIZE);
+ DefInt(SQLITE_LIMIT_LENGTH);
+ DefInt(SQLITE_MAX_LENGTH);
+ DefInt(SQLITE_LIMIT_SQL_LENGTH);
+ DefInt(SQLITE_MAX_SQL_LENGTH);
+ DefInt(SQLITE_LIMIT_COLUMN);
+ DefInt(SQLITE_MAX_COLUMN);
+ DefInt(SQLITE_LIMIT_EXPR_DEPTH);
+ DefInt(SQLITE_MAX_EXPR_DEPTH);
+ DefInt(SQLITE_LIMIT_COMPOUND_SELECT);
+ DefInt(SQLITE_MAX_COMPOUND_SELECT);
+ DefInt(SQLITE_LIMIT_VDBE_OP);
+ DefInt(SQLITE_MAX_VDBE_OP);
+ DefInt(SQLITE_LIMIT_FUNCTION_ARG);
+ DefInt(SQLITE_MAX_FUNCTION_ARG);
+ DefInt(SQLITE_LIMIT_ATTACHED);
+ DefInt(SQLITE_MAX_ATTACHED);
+ DefInt(SQLITE_LIMIT_LIKE_PATTERN_LENGTH);
+ DefInt(SQLITE_MAX_LIKE_PATTERN_LENGTH);
+ DefInt(SQLITE_LIMIT_VARIABLE_NUMBER);
+ DefInt(SQLITE_MAX_VARIABLE_NUMBER);
+ DefInt(SQLITE_LIMIT_TRIGGER_DEPTH);
+ DefInt(SQLITE_MAX_TRIGGER_DEPTH);
+ DefInt(SQLITE_LIMIT_WORKER_THREADS);
+ DefInt(SQLITE_MAX_WORKER_THREADS);
+ } _DefGroup;
+
+ DefGroup(openFlags) {
+ /* Noting that not all of these will have any effect in
+ ** WASM-space. */
+ DefInt(SQLITE_OPEN_READONLY);
+ DefInt(SQLITE_OPEN_READWRITE);
+ DefInt(SQLITE_OPEN_CREATE);
+ DefInt(SQLITE_OPEN_URI);
+ DefInt(SQLITE_OPEN_MEMORY);
+ DefInt(SQLITE_OPEN_NOMUTEX);
+ DefInt(SQLITE_OPEN_FULLMUTEX);
+ DefInt(SQLITE_OPEN_SHAREDCACHE);
+ DefInt(SQLITE_OPEN_PRIVATECACHE);
+ DefInt(SQLITE_OPEN_EXRESCODE);
+ DefInt(SQLITE_OPEN_NOFOLLOW);
+ /* OPEN flags for use with VFSes... */
+ DefInt(SQLITE_OPEN_MAIN_DB);
+ DefInt(SQLITE_OPEN_MAIN_JOURNAL);
+ DefInt(SQLITE_OPEN_TEMP_DB);
+ DefInt(SQLITE_OPEN_TEMP_JOURNAL);
+ DefInt(SQLITE_OPEN_TRANSIENT_DB);
+ DefInt(SQLITE_OPEN_SUBJOURNAL);
+ DefInt(SQLITE_OPEN_SUPER_JOURNAL);
+ DefInt(SQLITE_OPEN_WAL);
+ DefInt(SQLITE_OPEN_DELETEONCLOSE);
+ DefInt(SQLITE_OPEN_EXCLUSIVE);
+ } _DefGroup;
+
+ DefGroup(prepareFlags) {
+ DefInt(SQLITE_PREPARE_PERSISTENT);
+ DefInt(SQLITE_PREPARE_NORMALIZE);
+ DefInt(SQLITE_PREPARE_NO_VTAB);
+ } _DefGroup;
+
+ DefGroup(resultCodes) {
+ DefInt(SQLITE_OK);
+ DefInt(SQLITE_ERROR);
+ DefInt(SQLITE_INTERNAL);
+ DefInt(SQLITE_PERM);
+ DefInt(SQLITE_ABORT);
+ DefInt(SQLITE_BUSY);
+ DefInt(SQLITE_LOCKED);
+ DefInt(SQLITE_NOMEM);
+ DefInt(SQLITE_READONLY);
+ DefInt(SQLITE_INTERRUPT);
+ DefInt(SQLITE_IOERR);
+ DefInt(SQLITE_CORRUPT);
+ DefInt(SQLITE_NOTFOUND);
+ DefInt(SQLITE_FULL);
+ DefInt(SQLITE_CANTOPEN);
+ DefInt(SQLITE_PROTOCOL);
+ DefInt(SQLITE_EMPTY);
+ DefInt(SQLITE_SCHEMA);
+ DefInt(SQLITE_TOOBIG);
+ DefInt(SQLITE_CONSTRAINT);
+ DefInt(SQLITE_MISMATCH);
+ DefInt(SQLITE_MISUSE);
+ DefInt(SQLITE_NOLFS);
+ DefInt(SQLITE_AUTH);
+ DefInt(SQLITE_FORMAT);
+ DefInt(SQLITE_RANGE);
+ DefInt(SQLITE_NOTADB);
+ DefInt(SQLITE_NOTICE);
+ DefInt(SQLITE_WARNING);
+ DefInt(SQLITE_ROW);
+ DefInt(SQLITE_DONE);
+ // Extended Result Codes
+ DefInt(SQLITE_ERROR_MISSING_COLLSEQ);
+ DefInt(SQLITE_ERROR_RETRY);
+ DefInt(SQLITE_ERROR_SNAPSHOT);
+ DefInt(SQLITE_IOERR_READ);
+ DefInt(SQLITE_IOERR_SHORT_READ);
+ DefInt(SQLITE_IOERR_WRITE);
+ DefInt(SQLITE_IOERR_FSYNC);
+ DefInt(SQLITE_IOERR_DIR_FSYNC);
+ DefInt(SQLITE_IOERR_TRUNCATE);
+ DefInt(SQLITE_IOERR_FSTAT);
+ DefInt(SQLITE_IOERR_UNLOCK);
+ DefInt(SQLITE_IOERR_RDLOCK);
+ DefInt(SQLITE_IOERR_DELETE);
+ DefInt(SQLITE_IOERR_BLOCKED);
+ DefInt(SQLITE_IOERR_NOMEM);
+ DefInt(SQLITE_IOERR_ACCESS);
+ DefInt(SQLITE_IOERR_CHECKRESERVEDLOCK);
+ DefInt(SQLITE_IOERR_LOCK);
+ DefInt(SQLITE_IOERR_CLOSE);
+ DefInt(SQLITE_IOERR_DIR_CLOSE);
+ DefInt(SQLITE_IOERR_SHMOPEN);
+ DefInt(SQLITE_IOERR_SHMSIZE);
+ DefInt(SQLITE_IOERR_SHMLOCK);
+ DefInt(SQLITE_IOERR_SHMMAP);
+ DefInt(SQLITE_IOERR_SEEK);
+ DefInt(SQLITE_IOERR_DELETE_NOENT);
+ DefInt(SQLITE_IOERR_MMAP);
+ DefInt(SQLITE_IOERR_GETTEMPPATH);
+ DefInt(SQLITE_IOERR_CONVPATH);
+ DefInt(SQLITE_IOERR_VNODE);
+ DefInt(SQLITE_IOERR_AUTH);
+ DefInt(SQLITE_IOERR_BEGIN_ATOMIC);
+ DefInt(SQLITE_IOERR_COMMIT_ATOMIC);
+ DefInt(SQLITE_IOERR_ROLLBACK_ATOMIC);
+ DefInt(SQLITE_IOERR_DATA);
+ DefInt(SQLITE_IOERR_CORRUPTFS);
+ DefInt(SQLITE_LOCKED_SHAREDCACHE);
+ DefInt(SQLITE_LOCKED_VTAB);
+ DefInt(SQLITE_BUSY_RECOVERY);
+ DefInt(SQLITE_BUSY_SNAPSHOT);
+ DefInt(SQLITE_BUSY_TIMEOUT);
+ DefInt(SQLITE_CANTOPEN_NOTEMPDIR);
+ DefInt(SQLITE_CANTOPEN_ISDIR);
+ DefInt(SQLITE_CANTOPEN_FULLPATH);
+ DefInt(SQLITE_CANTOPEN_CONVPATH);
+ //DefInt(SQLITE_CANTOPEN_DIRTYWAL)/*docs say not used*/;
+ DefInt(SQLITE_CANTOPEN_SYMLINK);
+ DefInt(SQLITE_CORRUPT_VTAB);
+ DefInt(SQLITE_CORRUPT_SEQUENCE);
+ DefInt(SQLITE_CORRUPT_INDEX);
+ DefInt(SQLITE_READONLY_RECOVERY);
+ DefInt(SQLITE_READONLY_CANTLOCK);
+ DefInt(SQLITE_READONLY_ROLLBACK);
+ DefInt(SQLITE_READONLY_DBMOVED);
+ DefInt(SQLITE_READONLY_CANTINIT);
+ DefInt(SQLITE_READONLY_DIRECTORY);
+ DefInt(SQLITE_ABORT_ROLLBACK);
+ DefInt(SQLITE_CONSTRAINT_CHECK);
+ DefInt(SQLITE_CONSTRAINT_COMMITHOOK);
+ DefInt(SQLITE_CONSTRAINT_FOREIGNKEY);
+ DefInt(SQLITE_CONSTRAINT_FUNCTION);
+ DefInt(SQLITE_CONSTRAINT_NOTNULL);
+ DefInt(SQLITE_CONSTRAINT_PRIMARYKEY);
+ DefInt(SQLITE_CONSTRAINT_TRIGGER);
+ DefInt(SQLITE_CONSTRAINT_UNIQUE);
+ DefInt(SQLITE_CONSTRAINT_VTAB);
+ DefInt(SQLITE_CONSTRAINT_ROWID);
+ DefInt(SQLITE_CONSTRAINT_PINNED);
+ DefInt(SQLITE_CONSTRAINT_DATATYPE);
+ DefInt(SQLITE_NOTICE_RECOVER_WAL);
+ DefInt(SQLITE_NOTICE_RECOVER_ROLLBACK);
+ DefInt(SQLITE_WARNING_AUTOINDEX);
+ DefInt(SQLITE_AUTH_USER);
+ DefInt(SQLITE_OK_LOAD_PERMANENTLY);
+ //DefInt(SQLITE_OK_SYMLINK) /* internal use only */;
+ } _DefGroup;
+
+ DefGroup(serialize){
+ DefInt(SQLITE_SERIALIZE_NOCOPY);
+ DefInt(SQLITE_DESERIALIZE_FREEONCLOSE);
+ DefInt(SQLITE_DESERIALIZE_READONLY);
+ DefInt(SQLITE_DESERIALIZE_RESIZEABLE);
+ } _DefGroup;
+
+ DefGroup(session){
+ DefInt(SQLITE_SESSION_CONFIG_STRMSIZE);
+ DefInt(SQLITE_SESSION_OBJCONFIG_SIZE);
+ } _DefGroup;
+
+ DefGroup(sqlite3Status){
+ DefInt(SQLITE_STATUS_MEMORY_USED);
+ DefInt(SQLITE_STATUS_PAGECACHE_USED);
+ DefInt(SQLITE_STATUS_PAGECACHE_OVERFLOW);
+ //DefInt(SQLITE_STATUS_SCRATCH_USED) /* NOT USED */;
+ //DefInt(SQLITE_STATUS_SCRATCH_OVERFLOW) /* NOT USED */;
+ DefInt(SQLITE_STATUS_MALLOC_SIZE);
+ DefInt(SQLITE_STATUS_PARSER_STACK);
+ DefInt(SQLITE_STATUS_PAGECACHE_SIZE);
+ //DefInt(SQLITE_STATUS_SCRATCH_SIZE) /* NOT USED */;
+ DefInt(SQLITE_STATUS_MALLOC_COUNT);
+ } _DefGroup;
+
+ DefGroup(stmtStatus){
+ DefInt(SQLITE_STMTSTATUS_FULLSCAN_STEP);
+ DefInt(SQLITE_STMTSTATUS_SORT);
+ DefInt(SQLITE_STMTSTATUS_AUTOINDEX);
+ DefInt(SQLITE_STMTSTATUS_VM_STEP);
+ DefInt(SQLITE_STMTSTATUS_REPREPARE);
+ DefInt(SQLITE_STMTSTATUS_RUN);
+ DefInt(SQLITE_STMTSTATUS_FILTER_MISS);
+ DefInt(SQLITE_STMTSTATUS_FILTER_HIT);
+ DefInt(SQLITE_STMTSTATUS_MEMUSED);
+ } _DefGroup;
+
+ DefGroup(syncFlags) {
+ DefInt(SQLITE_SYNC_NORMAL);
+ DefInt(SQLITE_SYNC_FULL);
+ DefInt(SQLITE_SYNC_DATAONLY);
+ } _DefGroup;
+
+ DefGroup(trace) {
+ DefInt(SQLITE_TRACE_STMT);
+ DefInt(SQLITE_TRACE_PROFILE);
+ DefInt(SQLITE_TRACE_ROW);
+ DefInt(SQLITE_TRACE_CLOSE);
+ } _DefGroup;
+
+ DefGroup(txnState){
+ DefInt(SQLITE_TXN_NONE);
+ DefInt(SQLITE_TXN_READ);
+ DefInt(SQLITE_TXN_WRITE);
+ } _DefGroup;
+
+ DefGroup(udfFlags) {
+ DefInt(SQLITE_DETERMINISTIC);
+ DefInt(SQLITE_DIRECTONLY);
+ DefInt(SQLITE_INNOCUOUS);
+ DefInt(SQLITE_SUBTYPE);
+ DefInt(SQLITE_RESULT_SUBTYPE);
+ } _DefGroup;
+
+ DefGroup(version) {
+ DefInt(SQLITE_VERSION_NUMBER);
+ DefStr(SQLITE_VERSION);
+ DefStr(SQLITE_SOURCE_ID);
+ } _DefGroup;
+
+ DefGroup(vtab) {
+ DefInt(SQLITE_INDEX_SCAN_UNIQUE);
+ DefInt(SQLITE_INDEX_CONSTRAINT_EQ);
+ DefInt(SQLITE_INDEX_CONSTRAINT_GT);
+ DefInt(SQLITE_INDEX_CONSTRAINT_LE);
+ DefInt(SQLITE_INDEX_CONSTRAINT_LT);
+ DefInt(SQLITE_INDEX_CONSTRAINT_GE);
+ DefInt(SQLITE_INDEX_CONSTRAINT_MATCH);
+ DefInt(SQLITE_INDEX_CONSTRAINT_LIKE);
+ DefInt(SQLITE_INDEX_CONSTRAINT_GLOB);
+ DefInt(SQLITE_INDEX_CONSTRAINT_REGEXP);
+ DefInt(SQLITE_INDEX_CONSTRAINT_NE);
+ DefInt(SQLITE_INDEX_CONSTRAINT_ISNOT);
+ DefInt(SQLITE_INDEX_CONSTRAINT_ISNOTNULL);
+ DefInt(SQLITE_INDEX_CONSTRAINT_ISNULL);
+ DefInt(SQLITE_INDEX_CONSTRAINT_IS);
+ DefInt(SQLITE_INDEX_CONSTRAINT_LIMIT);
+ DefInt(SQLITE_INDEX_CONSTRAINT_OFFSET);
+ DefInt(SQLITE_INDEX_CONSTRAINT_FUNCTION);
+ DefInt(SQLITE_VTAB_CONSTRAINT_SUPPORT);
+ DefInt(SQLITE_VTAB_INNOCUOUS);
+ DefInt(SQLITE_VTAB_DIRECTONLY);
+ DefInt(SQLITE_VTAB_USES_ALL_SCHEMAS);
+ DefInt(SQLITE_ROLLBACK);
+ //DefInt(SQLITE_IGNORE); // Also used by sqlite3_authorizer() callback
+ DefInt(SQLITE_FAIL);
+ //DefInt(SQLITE_ABORT); // Also an error code
+ DefInt(SQLITE_REPLACE);
+ } _DefGroup;
+
+#undef DefGroup
+#undef DefStr
+#undef DefInt
+#undef _DefGroup
+
+ /*
+ ** Emit an array of "StructBinder" struct descripions, which look
+ ** like:
+ **
+ ** {
+ ** "name": "MyStruct",
+ ** "sizeof": 16,
+ ** "members": {
+ ** "member1": {"offset": 0,"sizeof": 4,"signature": "i"},
+ ** "member2": {"offset": 4,"sizeof": 4,"signature": "p"},
+ ** "member3": {"offset": 8,"sizeof": 8,"signature": "j"}
+ ** }
+ ** }
+ **
+ ** Detailed documentation for those bits are in the docs for the
+ ** Jaccwabyt JS-side component.
+ */
+
+ /** Macros for emitting StructBinder description. */
+#define StructBinder__(TYPE) \
+ n = 0; \
+ outf("%s{", (nStruct++ ? ", " : "")); \
+ out("\"name\": \"" # TYPE "\","); \
+ outf("\"sizeof\": %d", (int)sizeof(TYPE)); \
+ out(",\"members\": {");
+#define StructBinder_(T) StructBinder__(T)
+ /** ^^^ indirection needed to expand CurrentStruct */
+#define StructBinder StructBinder_(CurrentStruct)
+#define _StructBinder CloseBrace(2)
+#define M(MEMBER,SIG) \
+ outf("%s\"%s\": " \
+ "{\"offset\":%d,\"sizeof\": %d,\"signature\":\"%s\"}", \
+ (n++ ? ", " : ""), #MEMBER, \
+ (int)offsetof(CurrentStruct,MEMBER), \
+ (int)sizeof(((CurrentStruct*)0)->MEMBER), \
+ SIG)
+
+ nStruct = 0;
+ out(", \"structs\": ["); {
+
+#define CurrentStruct sqlite3_vfs
+ StructBinder {
+ M(iVersion, "i");
+ M(szOsFile, "i");
+ M(mxPathname, "i");
+ M(pNext, "p");
+ M(zName, "s");
+ M(pAppData, "p");
+ M(xOpen, "i(pppip)");
+ M(xDelete, "i(ppi)");
+ M(xAccess, "i(ppip)");
+ M(xFullPathname, "i(ppip)");
+ M(xDlOpen, "p(pp)");
+ M(xDlError, "p(pip)");
+ M(xDlSym, "p()");
+ M(xDlClose, "v(pp)");
+ M(xRandomness, "i(pip)");
+ M(xSleep, "i(pi)");
+ M(xCurrentTime, "i(pp)");
+ M(xGetLastError, "i(pip)");
+ M(xCurrentTimeInt64, "i(pp)");
+ M(xSetSystemCall, "i(ppp)");
+ M(xGetSystemCall, "p(pp)");
+ M(xNextSystemCall, "p(pp)");
+ } _StructBinder;
+#undef CurrentStruct
+
+#define CurrentStruct sqlite3_io_methods
+ StructBinder {
+ M(iVersion, "i");
+ M(xClose, "i(p)");
+ M(xRead, "i(ppij)");
+ M(xWrite, "i(ppij)");
+ M(xTruncate, "i(pj)");
+ M(xSync, "i(pi)");
+ M(xFileSize, "i(pp)");
+ M(xLock, "i(pi)");
+ M(xUnlock, "i(pi)");
+ M(xCheckReservedLock, "i(pp)");
+ M(xFileControl, "i(pip)");
+ M(xSectorSize, "i(p)");
+ M(xDeviceCharacteristics, "i(p)");
+ M(xShmMap, "i(piiip)");
+ M(xShmLock, "i(piii)");
+ M(xShmBarrier, "v(p)");
+ M(xShmUnmap, "i(pi)");
+ M(xFetch, "i(pjip)");
+ M(xUnfetch, "i(pjp)");
+ } _StructBinder;
+#undef CurrentStruct
+
+#define CurrentStruct sqlite3_file
+ StructBinder {
+ M(pMethods, "p");
+ } _StructBinder;
+#undef CurrentStruct
+
+#define CurrentStruct sqlite3_kvvfs_methods
+ StructBinder {
+ M(xRead, "i(sspi)");
+ M(xWrite, "i(sss)");
+ M(xDelete, "i(ss)");
+ M(nKeySize, "i");
+ } _StructBinder;
+#undef CurrentStruct
+
+
+#define CurrentStruct sqlite3_vtab
+ StructBinder {
+ M(pModule, "p");
+ M(nRef, "i");
+ M(zErrMsg, "p");
+ } _StructBinder;
+#undef CurrentStruct
+
+#define CurrentStruct sqlite3_vtab_cursor
+ StructBinder {
+ M(pVtab, "p");
+ } _StructBinder;
+#undef CurrentStruct
+
+#define CurrentStruct sqlite3_module
+ StructBinder {
+ M(iVersion, "i");
+ M(xCreate, "i(ppippp)");
+ M(xConnect, "i(ppippp)");
+ M(xBestIndex, "i(pp)");
+ M(xDisconnect, "i(p)");
+ M(xDestroy, "i(p)");
+ M(xOpen, "i(pp)");
+ M(xClose, "i(p)");
+ M(xFilter, "i(pisip)");
+ M(xNext, "i(p)");
+ M(xEof, "i(p)");
+ M(xColumn, "i(ppi)");
+ M(xRowid, "i(pp)");
+ M(xUpdate, "i(pipp)");
+ M(xBegin, "i(p)");
+ M(xSync, "i(p)");
+ M(xCommit, "i(p)");
+ M(xRollback, "i(p)");
+ M(xFindFunction, "i(pispp)");
+ M(xRename, "i(ps)");
+ // ^^^ v1. v2+ follows...
+ M(xSavepoint, "i(pi)");
+ M(xRelease, "i(pi)");
+ M(xRollbackTo, "i(pi)");
+ // ^^^ v2. v3+ follows...
+ M(xShadowName, "i(s)");
+ } _StructBinder;
+#undef CurrentStruct
+
+ /**
+ ** Workaround: in order to map the various inner structs from
+ ** sqlite3_index_info, we have to uplift those into constructs we
+ ** can access by type name. These structs _must_ match their
+ ** in-sqlite3_index_info counterparts byte for byte.
+ */
+ typedef struct {
+ int iColumn;
+ unsigned char op;
+ unsigned char usable;
+ int iTermOffset;
+ } sqlite3_index_constraint;
+ typedef struct {
+ int iColumn;
+ unsigned char desc;
+ } sqlite3_index_orderby;
+ typedef struct {
+ int argvIndex;
+ unsigned char omit;
+ } sqlite3_index_constraint_usage;
+ { /* Validate that the above struct sizeof()s match
+ ** expectations. We could improve upon this by
+ ** checking the offsetof() for each member. */
+ const sqlite3_index_info siiCheck;
+#define IndexSzCheck(T,M) \
+ (sizeof(T) == sizeof(*siiCheck.M))
+ if(!IndexSzCheck(sqlite3_index_constraint,aConstraint)
+ || !IndexSzCheck(sqlite3_index_orderby,aOrderBy)
+ || !IndexSzCheck(sqlite3_index_constraint_usage,aConstraintUsage)){
+ assert(!"sizeof mismatch in sqlite3_index_... struct(s)");
+ return 0;
+ }
+#undef IndexSzCheck
+ }
+
+#define CurrentStruct sqlite3_index_constraint
+ StructBinder {
+ M(iColumn, "i");
+ M(op, "C");
+ M(usable, "C");
+ M(iTermOffset, "i");
+ } _StructBinder;
+#undef CurrentStruct
+
+#define CurrentStruct sqlite3_index_orderby
+ StructBinder {
+ M(iColumn, "i");
+ M(desc, "C");
+ } _StructBinder;
+#undef CurrentStruct
+
+#define CurrentStruct sqlite3_index_constraint_usage
+ StructBinder {
+ M(argvIndex, "i");
+ M(omit, "C");
+ } _StructBinder;
+#undef CurrentStruct
+
+#define CurrentStruct sqlite3_index_info
+ StructBinder {
+ M(nConstraint, "i");
+ M(aConstraint, "p");
+ M(nOrderBy, "i");
+ M(aOrderBy, "p");
+ M(aConstraintUsage, "p");
+ M(idxNum, "i");
+ M(idxStr, "p");
+ M(needToFreeIdxStr, "i");
+ M(orderByConsumed, "i");
+ M(estimatedCost, "d");
+ M(estimatedRows, "j");
+ M(idxFlags, "i");
+ M(colUsed, "j");
+ } _StructBinder;
+#undef CurrentStruct
+
+#if SQLITE_WASM_TESTS
+#define CurrentStruct WasmTestStruct
+ StructBinder {
+ M(v4, "i");
+ M(cstr, "s");
+ M(ppV, "p");
+ M(v8, "j");
+ M(xFunc, "v(p)");
+ } _StructBinder;
+#undef CurrentStruct
+#endif
+
+ } out( "]"/*structs*/);
+
+ out("}"/*top-level object*/);
+ *zPos = 0;
+ aBuffer[0] = '{'/*end of the race-condition workaround*/;
+ return aBuffer;
+#undef StructBinder
+#undef StructBinder_
+#undef StructBinder__
+#undef M
+#undef _StructBinder
+#undef CloseBrace
+#undef out
+#undef outf
+#undef lenCheck
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** This function invokes the xDelete method of the given VFS (or the
+** default VFS if pVfs is NULL), passing on the given filename. If
+** zName is NULL, no default VFS is found, or it has no xDelete
+** method, SQLITE_MISUSE is returned, else the result of the xDelete()
+** call is returned.
+*/
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_vfs_unlink(sqlite3_vfs *pVfs, const char *zName){
+ int rc = SQLITE_MISUSE /* ??? */;
+ if( 0==pVfs && 0!=zName ) pVfs = sqlite3_vfs_find(0);
+ if( zName && pVfs && pVfs->xDelete ){
+ rc = pVfs->xDelete(pVfs, zName, 1);
+ }
+ return rc;
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** Returns a pointer to the given DB's VFS for the given DB name,
+** defaulting to "main" if zDbName is 0. Returns 0 if no db with the
+** given name is open.
+*/
+SQLITE_WASM_EXPORT
+sqlite3_vfs * sqlite3_wasm_db_vfs(sqlite3 *pDb, const char *zDbName){
+ sqlite3_vfs * pVfs = 0;
+ sqlite3_file_control(pDb, zDbName ? zDbName : "main",
+ SQLITE_FCNTL_VFS_POINTER, &pVfs);
+ return pVfs;
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** This function resets the given db pointer's database as described at
+**
+** https://sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigresetdatabase
+**
+** But beware: virtual tables destroyed that way do not have their
+** xDestroy() called, so will leak if they require that function for
+** proper cleanup.
+**
+** Returns 0 on success, an SQLITE_xxx code on error. Returns
+** SQLITE_MISUSE if pDb is NULL.
+*/
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_db_reset(sqlite3 *pDb){
+ int rc = SQLITE_MISUSE;
+ if( pDb ){
+ sqlite3_table_column_metadata(pDb, "main", 0, 0, 0, 0, 0, 0, 0);
+ rc = sqlite3_db_config(pDb, SQLITE_DBCONFIG_RESET_DATABASE, 1, 0);
+ if( 0==rc ){
+ rc = sqlite3_exec(pDb, "VACUUM", 0, 0, 0);
+ sqlite3_db_config(pDb, SQLITE_DBCONFIG_RESET_DATABASE, 0, 0);
+ }
+ }
+ return rc;
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** Uses the given database's VFS xRead to stream the db file's
+** contents out to the given callback. The callback gets a single
+** chunk of size n (its 2nd argument) on each call and must return 0
+** on success, non-0 on error. This function returns 0 on success,
+** SQLITE_NOTFOUND if no db is open, or propagates any other non-0
+** code from the callback. Note that this is not thread-friendly: it
+** expects that it will be the only thread reading the db file and
+** takes no measures to ensure that is the case.
+**
+** This implementation appears to work fine, but
+** sqlite3_wasm_db_serialize() is arguably the better way to achieve
+** this.
+*/
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_db_export_chunked( sqlite3* pDb,
+ int (*xCallback)(unsigned const char *zOut, int n) ){
+ sqlite3_int64 nSize = 0;
+ sqlite3_int64 nPos = 0;
+ sqlite3_file * pFile = 0;
+ unsigned char buf[1024 * 8];
+ int nBuf = (int)sizeof(buf);
+ int rc = pDb
+ ? sqlite3_file_control(pDb, "main",
+ SQLITE_FCNTL_FILE_POINTER, &pFile)
+ : SQLITE_NOTFOUND;
+ if( rc ) return rc;
+ rc = pFile->pMethods->xFileSize(pFile, &nSize);
+ if( rc ) return rc;
+ if(nSize % nBuf){
+ /* DB size is not an even multiple of the buffer size. Reduce
+ ** buffer size so that we do not unduly inflate the db size
+ ** with zero-padding when exporting. */
+ if(0 == nSize % 4096) nBuf = 4096;
+ else if(0 == nSize % 2048) nBuf = 2048;
+ else if(0 == nSize % 1024) nBuf = 1024;
+ else nBuf = 512;
+ }
+ for( ; 0==rc && nPos<nSize; nPos += nBuf ){
+ rc = pFile->pMethods->xRead(pFile, buf, nBuf, nPos);
+ if( SQLITE_IOERR_SHORT_READ == rc ){
+ rc = (nPos + nBuf) < nSize ? rc : 0/*assume EOF*/;
+ }
+ if( 0==rc ) rc = xCallback(buf, nBuf);
+ }
+ return rc;
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** A proxy for sqlite3_serialize() which serializes the schema zSchema
+** of pDb, placing the serialized output in pOut and nOut. nOut may be
+** NULL. If zSchema is NULL then "main" is assumed. If pDb or pOut are
+** NULL then SQLITE_MISUSE is returned. If allocation of the
+** serialized copy fails, SQLITE_NOMEM is returned. On success, 0 is
+** returned and `*pOut` will contain a pointer to the memory unless
+** mFlags includes SQLITE_SERIALIZE_NOCOPY and the database has no
+** contiguous memory representation, in which case `*pOut` will be
+** NULL but 0 will be returned.
+**
+** If `*pOut` is not NULL, the caller is responsible for passing it to
+** sqlite3_free() to free it.
+*/
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_db_serialize( sqlite3 *pDb, const char *zSchema,
+ unsigned char **pOut,
+ sqlite3_int64 *nOut, unsigned int mFlags ){
+ unsigned char * z;
+ if( !pDb || !pOut ) return SQLITE_MISUSE;
+ if( nOut ) *nOut = 0;
+ z = sqlite3_serialize(pDb, zSchema ? zSchema : "main", nOut, mFlags);
+ if( z || (SQLITE_SERIALIZE_NOCOPY & mFlags) ){
+ *pOut = z;
+ return 0;
+ }else{
+ return SQLITE_NOMEM;
+ }
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** ACHTUNG: it was discovered on 2023-08-11 that, with SQLITE_DEBUG,
+** this function's out-of-scope use of the sqlite3_vfs/file/io_methods
+** APIs leads to triggering of assertions in the core library. Its use
+** is now deprecated and VFS-specific APIs for importing files need to
+** be found to replace it. sqlite3_wasm_posix_create_file() is
+** suitable for the "unix" family of VFSes.
+**
+** Creates a new file using the I/O API of the given VFS, containing
+** the given number of bytes of the given data. If the file exists, it
+** is truncated to the given length and populated with the given
+** data.
+**
+** This function exists so that we can implement the equivalent of
+** Emscripten's FS.createDataFile() in a VFS-agnostic way. This
+** functionality is intended for use in uploading database files.
+**
+** Not all VFSes support this functionality, e.g. the "kvvfs" does
+** not.
+**
+** If pVfs is NULL, sqlite3_vfs_find(0) is used.
+**
+** If zFile is NULL, pVfs is NULL (and sqlite3_vfs_find(0) returns
+** NULL), or nData is negative, SQLITE_MISUSE are returned.
+**
+** On success, it creates a new file with the given name, populated
+** with the fist nData bytes of pData. If pData is NULL, the file is
+** created and/or truncated to nData bytes.
+**
+** Whether or not directory components of zFilename are created
+** automatically or not is unspecified: that detail is left to the
+** VFS. The "opfs" VFS, for example, creates them.
+**
+** If an error happens while populating or truncating the file, the
+** target file will be deleted (if needed) if this function created
+** it. If this function did not create it, it is not deleted but may
+** be left in an undefined state.
+**
+** Returns 0 on success. On error, it returns a code described above
+** or propagates a code from one of the I/O methods.
+**
+** Design note: nData is an integer, instead of int64, for WASM
+** portability, so that the API can still work in builds where BigInt
+** support is disabled or unavailable.
+*/
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_vfs_create_file( sqlite3_vfs *pVfs,
+ const char *zFilename,
+ const unsigned char * pData,
+ int nData ){
+ int rc;
+ sqlite3_file *pFile = 0;
+ sqlite3_io_methods const *pIo;
+ const int openFlags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE
+#if 0 && defined(SQLITE_DEBUG)
+ | SQLITE_OPEN_MAIN_JOURNAL
+ /* ^^^^ This is for testing a horrible workaround to avoid
+ triggering a specific assert() in os_unix.c:unixOpen(). Please
+ do not enable this in real builds. */
+#endif
+ ;
+ int flagsOut = 0;
+ int fileExisted = 0;
+ int doUnlock = 0;
+ const unsigned char *pPos = pData;
+ const int blockSize = 512
+ /* Because we are using pFile->pMethods->xWrite() for writing, and
+ ** it may have a buffer limit related to sqlite3's pager size, we
+ ** conservatively write in 512-byte blocks (smallest page
+ ** size). */;
+ //fprintf(stderr, "pVfs=%p, zFilename=%s, nData=%d\n", pVfs, zFilename, nData);
+ if( !pVfs ) pVfs = sqlite3_vfs_find(0);
+ if( !pVfs || !zFilename || nData<0 ) return SQLITE_MISUSE;
+ pVfs->xAccess(pVfs, zFilename, SQLITE_ACCESS_EXISTS, &fileExisted);
+ rc = sqlite3OsOpenMalloc(pVfs, zFilename, &pFile, openFlags, &flagsOut);
+#if 0
+# define RC fprintf(stderr,"create_file(%s,%s) @%d rc=%d\n", \
+ pVfs->zName, zFilename, __LINE__, rc);
+#else
+# define RC
+#endif
+ RC;
+ if(rc) return rc;
+ pIo = pFile->pMethods;
+ if( pIo->xLock ) {
+ /* We need xLock() in order to accommodate the OPFS VFS, as it
+ ** obtains a writeable handle via the lock operation and releases
+ ** it in xUnlock(). If we don't do those here, we have to add code
+ ** to the VFS to account check whether it was locked before
+ ** xFileSize(), xTruncate(), and the like, and release the lock
+ ** only if it was unlocked when the op was started. */
+ rc = pIo->xLock(pFile, SQLITE_LOCK_EXCLUSIVE);
+ RC;
+ doUnlock = 0==rc;
+ }
+ if( 0==rc ){
+ rc = pIo->xTruncate(pFile, nData);
+ RC;
+ }
+ if( 0==rc && 0!=pData && nData>0 ){
+ while( 0==rc && nData>0 ){
+ const int n = nData>=blockSize ? blockSize : nData;
+ rc = pIo->xWrite(pFile, pPos, n, (sqlite3_int64)(pPos - pData));
+ RC;
+ nData -= n;
+ pPos += n;
+ }
+ if( 0==rc && nData>0 ){
+ assert( nData<blockSize );
+ rc = pIo->xWrite(pFile, pPos, nData,
+ (sqlite3_int64)(pPos - pData));
+ RC;
+ }
+ }
+ if( pIo->xUnlock && doUnlock!=0 ){
+ pIo->xUnlock(pFile, SQLITE_LOCK_NONE);
+ }
+ pIo->xClose(pFile);
+ if( rc!=0 && 0==fileExisted ){
+ pVfs->xDelete(pVfs, zFilename, 1);
+ }
+ RC;
+#undef RC
+ return rc;
+}
+
+/**
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** Creates or overwrites a file using the POSIX file API,
+** i.e. Emscripten's virtual filesystem. Creates or truncates
+** zFilename, appends pData bytes to it, and returns 0 on success or
+** SQLITE_IOERR on error.
+*/
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_posix_create_file( const char *zFilename,
+ const unsigned char * pData,
+ int nData ){
+ int rc;
+ FILE * pFile = 0;
+ int fileExisted = 0;
+ size_t nWrote = 1;
+
+ if( !zFilename || nData<0 || (pData==0 && nData>0) ) return SQLITE_MISUSE;
+ pFile = fopen(zFilename, "w");
+ if( 0==pFile ) return SQLITE_IOERR;
+ if( nData>0 ){
+ nWrote = fwrite(pData, (size_t)nData, 1, pFile);
+ }
+ fclose(pFile);
+ return 1==nWrote ? 0 : SQLITE_IOERR;
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** Allocates sqlite3KvvfsMethods.nKeySize bytes from
+** sqlite3_wasm_pstack_alloc() and returns 0 if that allocation fails,
+** else it passes that string to kvstorageMakeKey() and returns a
+** NUL-terminated pointer to that string. It is up to the caller to
+** use sqlite3_wasm_pstack_restore() to free the returned pointer.
+*/
+SQLITE_WASM_EXPORT
+char * sqlite3_wasm_kvvfsMakeKeyOnPstack(const char *zClass,
+ const char *zKeyIn){
+ assert(sqlite3KvvfsMethods.nKeySize>24);
+ char *zKeyOut =
+ (char *)sqlite3_wasm_pstack_alloc(sqlite3KvvfsMethods.nKeySize);
+ if(zKeyOut){
+ kvstorageMakeKey(zClass, zKeyIn, zKeyOut);
+ }
+ return zKeyOut;
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** Returns the pointer to the singleton object which holds the kvvfs
+** I/O methods and associated state.
+*/
+SQLITE_WASM_EXPORT
+sqlite3_kvvfs_methods * sqlite3_wasm_kvvfs_methods(void){
+ return &sqlite3KvvfsMethods;
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** This is a proxy for the variadic sqlite3_vtab_config() which passes
+** its argument on, or not, to sqlite3_vtab_config(), depending on the
+** value of its 2nd argument. Returns the result of
+** sqlite3_vtab_config(), or SQLITE_MISUSE if the 2nd arg is not a
+** valid value.
+*/
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_vtab_config(sqlite3 *pDb, int op, int arg){
+ switch(op){
+ case SQLITE_VTAB_DIRECTONLY:
+ case SQLITE_VTAB_INNOCUOUS:
+ return sqlite3_vtab_config(pDb, op);
+ case SQLITE_VTAB_CONSTRAINT_SUPPORT:
+ return sqlite3_vtab_config(pDb, op, arg);
+ default:
+ return SQLITE_MISUSE;
+ }
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** Wrapper for the variants of sqlite3_db_config() which take
+** (int,int*) variadic args.
+*/
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_db_config_ip(sqlite3 *pDb, int op, int arg1, int* pArg2){
+ switch(op){
+ case SQLITE_DBCONFIG_ENABLE_FKEY:
+ case SQLITE_DBCONFIG_ENABLE_TRIGGER:
+ case SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER:
+ case SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION:
+ case SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE:
+ case SQLITE_DBCONFIG_ENABLE_QPSG:
+ case SQLITE_DBCONFIG_TRIGGER_EQP:
+ case SQLITE_DBCONFIG_RESET_DATABASE:
+ case SQLITE_DBCONFIG_DEFENSIVE:
+ case SQLITE_DBCONFIG_WRITABLE_SCHEMA:
+ case SQLITE_DBCONFIG_LEGACY_ALTER_TABLE:
+ case SQLITE_DBCONFIG_DQS_DML:
+ case SQLITE_DBCONFIG_DQS_DDL:
+ case SQLITE_DBCONFIG_ENABLE_VIEW:
+ case SQLITE_DBCONFIG_LEGACY_FILE_FORMAT:
+ case SQLITE_DBCONFIG_TRUSTED_SCHEMA:
+ case SQLITE_DBCONFIG_STMT_SCANSTATUS:
+ case SQLITE_DBCONFIG_REVERSE_SCANORDER:
+ return sqlite3_db_config(pDb, op, arg1, pArg2);
+ default: return SQLITE_MISUSE;
+ }
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** Wrapper for the variants of sqlite3_db_config() which take
+** (void*,int,int) variadic args.
+*/
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_db_config_pii(sqlite3 *pDb, int op, void * pArg1, int arg2, int arg3){
+ switch(op){
+ case SQLITE_DBCONFIG_LOOKASIDE:
+ return sqlite3_db_config(pDb, op, pArg1, arg2, arg3);
+ default: return SQLITE_MISUSE;
+ }
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** Wrapper for the variants of sqlite3_db_config() which take
+** (const char *) variadic args.
+*/
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_db_config_s(sqlite3 *pDb, int op, const char *zArg){
+ switch(op){
+ case SQLITE_DBCONFIG_MAINDBNAME:
+ return sqlite3_db_config(pDb, op, zArg);
+ default: return SQLITE_MISUSE;
+ }
+}
+
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** Binding for combinations of sqlite3_config() arguments which take
+** a single integer argument.
+*/
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_config_i(int op, int arg){
+ return sqlite3_config(op, arg);
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** Binding for combinations of sqlite3_config() arguments which take
+** two int arguments.
+*/
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_config_ii(int op, int arg1, int arg2){
+ return sqlite3_config(op, arg1, arg2);
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** Binding for combinations of sqlite3_config() arguments which take
+** a single i64 argument.
+*/
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_config_j(int op, sqlite3_int64 arg){
+ return sqlite3_config(op, arg);
+}
+
+#if 0
+// Pending removal after verification of a workaround discussed in the
+// forum post linked to below.
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** Returns a pointer to sqlite3_free(). In compliant browsers the
+** return value, when passed to sqlite3.wasm.exports.functionEntry(),
+** must resolve to the same function as
+** sqlite3.wasm.exports.sqlite3_free. i.e. from a dev console where
+** sqlite3 is exported globally, the following must be true:
+**
+** ```
+** sqlite3.wasm.functionEntry(
+** sqlite3.wasm.exports.sqlite3_wasm_ptr_to_sqlite3_free()
+** ) === sqlite3.wasm.exports.sqlite3_free
+** ```
+**
+** Using a function to return this pointer, as opposed to exporting it
+** via sqlite3_wasm_enum_json(), is an attempt to work around a
+** Safari-specific quirk covered at
+** https://sqlite.org/forum/info/e5b20e1feb37a19a.
+**/
+SQLITE_WASM_EXPORT
+void * sqlite3_wasm_ptr_to_sqlite3_free(void){
+ return (void*)sqlite3_free;
+}
+#endif
+
+#if defined(__EMSCRIPTEN__) && defined(SQLITE_ENABLE_WASMFS)
+#include <emscripten/wasmfs.h>
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings, specifically
+** only when building with Emscripten's WASMFS support.
+**
+** This function should only be called if the JS side detects the
+** existence of the Origin-Private FileSystem (OPFS) APIs in the
+** client. The first time it is called, this function instantiates a
+** WASMFS backend impl for OPFS. On success, subsequent calls are
+** no-ops.
+**
+** This function may be passed a "mount point" name, which must have a
+** leading "/" and is currently restricted to a single path component,
+** e.g. "/foo" is legal but "/foo/" and "/foo/bar" are not. If it is
+** NULL or empty, it defaults to "/opfs".
+**
+** Returns 0 on success, SQLITE_NOMEM if instantiation of the backend
+** object fails, SQLITE_IOERR if mkdir() of the zMountPoint dir in
+** the virtual FS fails. In builds compiled without SQLITE_ENABLE_WASMFS
+** defined, SQLITE_NOTFOUND is returned without side effects.
+*/
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_init_wasmfs(const char *zMountPoint){
+ static backend_t pOpfs = 0;
+ if( !zMountPoint || !*zMountPoint ) zMountPoint = "/opfs";
+ if( !pOpfs ){
+ pOpfs = wasmfs_create_opfs_backend();
+ }
+ /** It's not enough to instantiate the backend. We have to create a
+ mountpoint in the VFS and attach the backend to it. */
+ if( pOpfs && 0!=access(zMountPoint, F_OK) ){
+ /* Note that this check and is not robust but it will
+ hypothetically suffice for the transient wasm-based virtual
+ filesystem we're currently running in. */
+ const int rc = wasmfs_create_directory(zMountPoint, 0777, pOpfs);
+ /*emscripten_console_logf("OPFS mkdir(%s) rc=%d", zMountPoint, rc);*/
+ if(rc) return SQLITE_IOERR;
+ }
+ return pOpfs ? 0 : SQLITE_NOMEM;
+}
+#else
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_init_wasmfs(const char *zUnused){
+ //emscripten_console_warn("WASMFS OPFS is not compiled in.");
+ if(zUnused){/*unused*/}
+ return SQLITE_NOTFOUND;
+}
+#endif /* __EMSCRIPTEN__ && SQLITE_ENABLE_WASMFS */
+
+#if SQLITE_WASM_TESTS
+
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_test_intptr(int * p){
+ return *p = *p * 2;
+}
+
+SQLITE_WASM_EXPORT
+void * sqlite3_wasm_test_voidptr(void * p){
+ return p;
+}
+
+SQLITE_WASM_EXPORT
+int64_t sqlite3_wasm_test_int64_max(void){
+ return (int64_t)0x7fffffffffffffff;
+}
+
+SQLITE_WASM_EXPORT
+int64_t sqlite3_wasm_test_int64_min(void){
+ return ~sqlite3_wasm_test_int64_max();
+}
+
+SQLITE_WASM_EXPORT
+int64_t sqlite3_wasm_test_int64_times2(int64_t x){
+ return x * 2;
+}
+
+SQLITE_WASM_EXPORT
+void sqlite3_wasm_test_int64_minmax(int64_t * min, int64_t *max){
+ *max = sqlite3_wasm_test_int64_max();
+ *min = sqlite3_wasm_test_int64_min();
+ /*printf("minmax: min=%lld, max=%lld\n", *min, *max);*/
+}
+
+SQLITE_WASM_EXPORT
+int64_t sqlite3_wasm_test_int64ptr(int64_t * p){
+ /*printf("sqlite3_wasm_test_int64ptr( @%lld = 0x%llx )\n", (int64_t)p, *p);*/
+ return *p = *p * 2;
+}
+
+SQLITE_WASM_EXPORT
+void sqlite3_wasm_test_stack_overflow(int recurse){
+ if(recurse) sqlite3_wasm_test_stack_overflow(recurse);
+}
+
+/* For testing the 'string:dealloc' whwasmutil.xWrap() conversion. */
+SQLITE_WASM_EXPORT
+char * sqlite3_wasm_test_str_hello(int fail){
+ char * s = fail ? 0 : (char *)sqlite3_malloc(6);
+ if(s){
+ memcpy(s, "hello", 5);
+ s[5] = 0;
+ }
+ return s;
+}
+
+/*
+** For testing using SQLTester scripts.
+**
+** Return non-zero if string z matches glob pattern zGlob and zero if the
+** pattern does not match.
+**
+** To repeat:
+**
+** zero == no match
+** non-zero == match
+**
+** Globbing rules:
+**
+** '*' Matches any sequence of zero or more characters.
+**
+** '?' Matches exactly one character.
+**
+** [...] Matches one character from the enclosed list of
+** characters.
+**
+** [^...] Matches one character not in the enclosed list.
+**
+** '#' Matches any sequence of one or more digits with an
+** optional + or - sign in front, or a hexadecimal
+** literal of the form 0x...
+*/
+static int sqlite3_wasm_SQLTester_strnotglob(const char *zGlob, const char *z){
+ int c, c2;
+ int invert;
+ int seen;
+ typedef int (*recurse_f)(const char *,const char *);
+ static const recurse_f recurse = sqlite3_wasm_SQLTester_strnotglob;
+
+ while( (c = (*(zGlob++)))!=0 ){
+ if( c=='*' ){
+ while( (c=(*(zGlob++))) == '*' || c=='?' ){
+ if( c=='?' && (*(z++))==0 ) return 0;
+ }
+ if( c==0 ){
+ return 1;
+ }else if( c=='[' ){
+ while( *z && recurse(zGlob-1,z)==0 ){
+ z++;
+ }
+ return (*z)!=0;
+ }
+ while( (c2 = (*(z++)))!=0 ){
+ while( c2!=c ){
+ c2 = *(z++);
+ if( c2==0 ) return 0;
+ }
+ if( recurse(zGlob,z) ) return 1;
+ }
+ return 0;
+ }else if( c=='?' ){
+ if( (*(z++))==0 ) return 0;
+ }else if( c=='[' ){
+ int prior_c = 0;
+ seen = 0;
+ invert = 0;
+ c = *(z++);
+ if( c==0 ) return 0;
+ c2 = *(zGlob++);
+ if( c2=='^' ){
+ invert = 1;
+ c2 = *(zGlob++);
+ }
+ if( c2==']' ){
+ if( c==']' ) seen = 1;
+ c2 = *(zGlob++);
+ }
+ while( c2 && c2!=']' ){
+ if( c2=='-' && zGlob[0]!=']' && zGlob[0]!=0 && prior_c>0 ){
+ c2 = *(zGlob++);
+ if( c>=prior_c && c<=c2 ) seen = 1;
+ prior_c = 0;
+ }else{
+ if( c==c2 ){
+ seen = 1;
+ }
+ prior_c = c2;
+ }
+ c2 = *(zGlob++);
+ }
+ if( c2==0 || (seen ^ invert)==0 ) return 0;
+ }else if( c=='#' ){
+ if( z[0]=='0'
+ && (z[1]=='x' || z[1]=='X')
+ && sqlite3Isxdigit(z[2])
+ ){
+ z += 3;
+ while( sqlite3Isxdigit(z[0]) ){ z++; }
+ }else{
+ if( (z[0]=='-' || z[0]=='+') && sqlite3Isdigit(z[1]) ) z++;
+ if( !sqlite3Isdigit(z[0]) ) return 0;
+ z++;
+ while( sqlite3Isdigit(z[0]) ){ z++; }
+ }
+ }else{
+ if( c!=(*(z++)) ) return 0;
+ }
+ }
+ return *z==0;
+}
+
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_SQLTester_strglob(const char *zGlob, const char *z){
+ return !sqlite3_wasm_SQLTester_strnotglob(zGlob, z);
+}
+
+
+#endif /* SQLITE_WASM_TESTS */
+
+#undef SQLITE_WASM_EXPORT
diff --git a/ext/wasm/api/sqlite3-worker1-promiser.c-pp.js b/ext/wasm/api/sqlite3-worker1-promiser.c-pp.js
new file mode 100644
index 0000000..cd78ed4
--- /dev/null
+++ b/ext/wasm/api/sqlite3-worker1-promiser.c-pp.js
@@ -0,0 +1,282 @@
+//#ifnot omit-oo1
+/*
+ 2022-08-24
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ This file implements a Promise-based proxy for the sqlite3 Worker
+ API #1. It is intended to be included either from the main thread or
+ a Worker, but only if (A) the environment supports nested Workers
+ and (B) it's _not_ a Worker which loads the sqlite3 WASM/JS
+ module. This file's features will load that module and provide a
+ slightly simpler client-side interface than the slightly-lower-level
+ Worker API does.
+
+ This script necessarily exposes one global symbol, but clients may
+ freely `delete` that symbol after calling it.
+*/
+'use strict';
+/**
+ Configures an sqlite3 Worker API #1 Worker such that it can be
+ manipulated via a Promise-based interface and returns a factory
+ function which returns Promises for communicating with the worker.
+ This proxy has an _almost_ identical interface to the normal
+ worker API, with any exceptions documented below.
+
+ It requires a configuration object with the following properties:
+
+ - `worker` (required): a Worker instance which loads
+ `sqlite3-worker1.js` or a functional equivalent. Note that the
+ promiser factory replaces the worker.onmessage property. This
+ config option may alternately be a function, in which case this
+ function re-assigns this property with the result of calling that
+ function, enabling delayed instantiation of a Worker.
+
+ - `onready` (optional, but...): this callback is called with no
+ arguments when the worker fires its initial
+ 'sqlite3-api'/'worker1-ready' message, which it does when
+ sqlite3.initWorker1API() completes its initialization. This is
+ the simplest way to tell the worker to kick off work at the
+ earliest opportunity.
+
+ - `onunhandled` (optional): a callback which gets passed the
+ message event object for any worker.onmessage() events which
+ are not handled by this proxy. Ideally that "should" never
+ happen, as this proxy aims to handle all known message types.
+
+ - `generateMessageId` (optional): a function which, when passed an
+ about-to-be-posted message object, generates a _unique_ message ID
+ for the message, which this API then assigns as the messageId
+ property of the message. It _must_ generate unique IDs on each call
+ so that dispatching can work. If not defined, a default generator
+ is used (which should be sufficient for most or all cases).
+
+ - `debug` (optional): a console.debug()-style function for logging
+ information about messages.
+
+ This function returns a stateful factory function with the
+ following interfaces:
+
+ - Promise function(messageType, messageArgs)
+ - Promise function({message object})
+
+ The first form expects the "type" and "args" values for a Worker
+ message. The second expects an object in the form {type:...,
+ args:...} plus any other properties the client cares to set. This
+ function will always set the `messageId` property on the object,
+ even if it's already set, and will set the `dbId` property to the
+ current database ID if it is _not_ set in the message object.
+
+ The function throws on error.
+
+ The function installs a temporary message listener, posts a
+ message to the configured Worker, and handles the message's
+ response via the temporary message listener. The then() callback
+ of the returned Promise is passed the `message.data` property from
+ the resulting message, i.e. the payload from the worker, stripped
+ of the lower-level event state which the onmessage() handler
+ receives.
+
+ Example usage:
+
+ ```
+ const config = {...};
+ const sq3Promiser = sqlite3Worker1Promiser(config);
+ sq3Promiser('open', {filename:"/foo.db"}).then(function(msg){
+ console.log("open response",msg); // => {type:'open', result: {filename:'/foo.db'}, ...}
+ });
+ sq3Promiser({type:'close'}).then((msg)=>{
+ console.log("close response",msg); // => {type:'close', result: {filename:'/foo.db'}, ...}
+ });
+ ```
+
+ Differences from Worker API #1:
+
+ - exec's {callback: STRING} option does not work via this
+ interface (it triggers an exception), but {callback: function}
+ does and works exactly like the STRING form does in the Worker:
+ 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,
+ row:VALUE,
+ rowNumber:1-based-#,
+ columnNames: array}
+
+ Where `typeString` is an internally-synthesized message type string
+ used temporarily for worker message dispatching. It can be ignored
+ by all client code except that which tests this API. The `row`
+ property contains the row result in the form implied by the
+ `rowMode` option (defaulting to `'array'`). The `rowNumber` is a
+ 1-based integer value incremented by 1 on each call into the
+ 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. Note that the rows
+ arrive via worker-posted messages, with all the implications
+ of that.
+
+ Notable shortcomings:
+
+ - 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
+ if(1===arguments.length && 'function'===typeof arguments[0]){
+ const f = config;
+ config = Object.assign(Object.create(null), callee.defaultConfig);
+ config.onready = f;
+ }else{
+ config = Object.assign(Object.create(null), callee.defaultConfig, config);
+ }
+ const handlerMap = Object.create(null);
+ const noop = function(){};
+ const err = config.onerror
+ || noop /* config.onerror is intentionally undocumented
+ pending finding a less ambiguous name */;
+ const debug = config.debug || noop;
+ const idTypeMap = config.generateMessageId ? undefined : Object.create(null);
+ const genMsgId = config.generateMessageId || function(msg){
+ return msg.type+'#'+(idTypeMap[msg.type] = (idTypeMap[msg.type]||0) + 1);
+ };
+ const toss = (...args)=>{throw new Error(args.join(' '))};
+ if(!config.worker) config.worker = callee.defaultConfig.worker;
+ if('function'===typeof config.worker) config.worker = config.worker();
+ let dbId;
+ config.worker.onmessage = function(ev){
+ ev = ev.data;
+ debug('worker1.onmessage',ev);
+ let msgHandler = handlerMap[ev.messageId];
+ if(!msgHandler){
+ if(ev && 'sqlite3-api'===ev.type && 'worker1-ready'===ev.result) {
+ /*fired one time when the Worker1 API initializes*/
+ if(config.onready) config.onready();
+ return;
+ }
+ msgHandler = handlerMap[ev.type] /* check for exec per-row callback */;
+ if(msgHandler && msgHandler.onrow){
+ msgHandler.onrow(ev);
+ return;
+ }
+ if(config.onunhandled) config.onunhandled(arguments[0]);
+ else err("sqlite3Worker1Promiser() unhandled worker message:",ev);
+ return;
+ }
+ delete handlerMap[ev.messageId];
+ switch(ev.type){
+ case 'error':
+ msgHandler.reject(ev);
+ return;
+ case 'open':
+ if(!dbId) dbId = ev.dbId;
+ break;
+ case 'close':
+ if(ev.dbId===dbId) dbId = undefined;
+ break;
+ default:
+ break;
+ }
+ try {msgHandler.resolve(ev)}
+ catch(e){msgHandler.reject(e)}
+ }/*worker.onmessage()*/;
+ return function(/*(msgType, msgArgs) || (msgEnvelope)*/){
+ let msg;
+ if(1===arguments.length){
+ msg = arguments[0];
+ }else if(2===arguments.length){
+ msg = Object.create(null);
+ msg.type = arguments[0];
+ msg.args = arguments[1];
+ msg.dbId = msg.args.dbId;
+ }else{
+ toss("Invalid arugments for sqlite3Worker1Promiser()-created factory.");
+ }
+ if(!msg.dbId && msg.type!=='open') msg.dbId = dbId;
+ msg.messageId = genMsgId(msg);
+ msg.departureTime = performance.now();
+ const proxy = Object.create(null);
+ proxy.message = msg;
+ let rowCallbackId /* message handler ID for exec on-row callback proxy */;
+ if('exec'===msg.type && msg.args){
+ if('function'===typeof msg.args.callback){
+ rowCallbackId = msg.messageId+':row';
+ proxy.onrow = msg.args.callback;
+ msg.args.callback = rowCallbackId;
+ handlerMap[rowCallbackId] = proxy;
+ }else if('string' === typeof msg.args.callback){
+ toss("exec callback may not be a string when using the Promise interface.");
+ /**
+ Design note: the reason for this limitation is that this
+ API takes over worker.onmessage() and the client has no way
+ of adding their own message-type handlers to it. Per-row
+ callbacks are implemented as short-lived message.type
+ mappings for worker.onmessage().
+
+ We "could" work around this by providing a new
+ config.fallbackMessageHandler (or some such) which contains
+ a map of event type names to callbacks. Seems like overkill
+ for now, seeing as the client can pass callback functions
+ to this interface (whereas the string-form "callback" is
+ needed for the over-the-Worker interface).
+ */
+ }
+ }
+ //debug("requestWork", msg);
+ let p = new Promise(function(resolve, reject){
+ proxy.resolve = resolve;
+ proxy.reject = reject;
+ handlerMap[msg.messageId] = proxy;
+ debug("Posting",msg.type,"message to Worker dbId="+(dbId||'default')+':',msg);
+ config.worker.postMessage(msg);
+ });
+ if(rowCallbackId) p = p.finally(()=>delete handlerMap[rowCallbackId]);
+ return p;
+ };
+}/*sqlite3Worker1Promiser()*/;
+globalThis.sqlite3Worker1Promiser.defaultConfig = {
+ worker: function(){
+//#if target=es6-bundler-friendly
+ return new Worker(new URL("sqlite3-worker1-bundler-friendly.mjs", import.meta.url),{
+ type: 'module'
+ });
+//#else
+ let theJs = "sqlite3-worker1.js";
+ if(this.currentScript){
+ const src = this.currentScript.src.split('/');
+ src.pop();
+ theJs = src.join('/')+'/' + theJs;
+ //sqlite3.config.warn("promiser currentScript, theJs =",this.currentScript,theJs);
+ }else if(globalThis.location){
+ //sqlite3.config.warn("promiser globalThis.location =",globalThis.location);
+ const urlParams = new URL(globalThis.location.href).searchParams;
+ if(urlParams.has('sqlite3.dir')){
+ theJs = urlParams.get('sqlite3.dir') + '/' + theJs;
+ }
+ }
+ return new Worker(theJs + globalThis.location.search);
+//#endif
+ }
+//#ifnot target=es6-bundler-friendly
+ .bind({
+ currentScript: globalThis?.document?.currentScript
+ })
+//#endif
+ ,
+ onerror: (...args)=>console.error('worker1 promiser error',...args)
+};
+//#else
+/* Built with the omit-oo1 flag. */
+//#endif ifnot omit-oo1
diff --git a/ext/wasm/api/sqlite3-worker1.c-pp.js b/ext/wasm/api/sqlite3-worker1.c-pp.js
new file mode 100644
index 0000000..74de9ec
--- /dev/null
+++ b/ext/wasm/api/sqlite3-worker1.c-pp.js
@@ -0,0 +1,54 @@
+//#ifnot omit-oo1
+/*
+ 2022-05-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 is a JS Worker file for the main sqlite3 api. It loads
+ sqlite3.js, initializes the module, and postMessage()'s a message
+ after the module is initialized:
+
+ {type: 'sqlite3-api', result: 'worker1-ready'}
+
+ This seemingly superfluous level of indirection is necessary when
+ loading sqlite3.js via a Worker. Instantiating a worker with new
+ Worker("sqlite.js") will not (cannot) call sqlite3InitModule() to
+ initialize the module due to a timing/order-of-operations conflict
+ (and that symbol is not exported in a way that a Worker loading it
+ that way can see it). Thus JS code wanting to load the sqlite3
+ Worker-specific API needs to pass _this_ file (or equivalent) to the
+ Worker constructor and then listen for an event in the form shown
+ above in order to know when the module has completed initialization.
+
+ This file accepts a URL arguments to adjust how it loads sqlite3.js:
+
+ - `sqlite3.dir`, if set, treats the given directory name as the
+ directory from which `sqlite3.js` will be loaded.
+*/
+//#if target=es6-bundler-friendly
+import {default as sqlite3InitModule} from './sqlite3-bundler-friendly.mjs';
+//#else
+"use strict";
+{
+ const urlParams = globalThis.location
+ ? new URL(globalThis.location.href).searchParams
+ : new URLSearchParams();
+ let theJs = 'sqlite3.js';
+ if(urlParams.has('sqlite3.dir')){
+ theJs = urlParams.get('sqlite3.dir') + '/' + theJs;
+ }
+ //console.warn("worker1 theJs =",theJs);
+ importScripts(theJs);
+}
+//#endif
+sqlite3InitModule().then(sqlite3 => sqlite3.initWorker1API());
+//#else
+/* Built with the omit-oo1 flag. */
+//#endif ifnot omit-oo1
diff --git a/ext/wasm/batch-runner-sahpool.html b/ext/wasm/batch-runner-sahpool.html
new file mode 100644
index 0000000..ad7e7b5
--- /dev/null
+++ b/ext/wasm/batch-runner-sahpool.html
@@ -0,0 +1,86 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>sqlite3-api batch SQL runner for the SAHPool VFS</title>
+ </head>
+ <body>
+ <header id='titlebar'><span>sqlite3-api batch SQL runner for the SAHPool VFS</span></header>
+ <div>
+ <span class='input-wrapper'>
+ <input type='checkbox' class='disable-during-eval' id='cb-reverse-log-order' checked></input>
+ <label for='cb-reverse-log-order' id='lbl-reverse-log-order'>Reverse log order</label>
+ </span>
+ </div>
+ <div id='test-output' class='reverse'></div>
+ <script>
+ (function(){
+ const E = (sel)=>document.querySelector(sel);
+
+ const eOut = E('#test-output');
+ const log2 = function(cssClass,...args){
+ let ln;
+ if(1 || cssClass){
+ ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ }else{
+ // This doesn't work with the "reverse order" option!
+ ln = document.createTextNode(args.join(' ')+'\n');
+ }
+ eOut.append(ln);
+ };
+ const log = (...args)=>{
+ //console.log(...args);
+ log2('', ...args);
+ };
+ const logErr = function(...args){
+ console.error(...args);
+ log2('error', ...args);
+ };
+ const logWarn = function(...args){
+ console.warn(...args);
+ log2('warning', ...args);
+ };
+
+ const cbReverseLog = E('#cb-reverse-log-order');
+ const lblReverseLog = E('#lbl-reverse-log-order');
+ if(cbReverseLog.checked){
+ lblReverseLog.classList.add('warning');
+ eOut.classList.add('reverse');
+ }
+ cbReverseLog.addEventListener('change', function(){
+ if(this.checked){
+ eOut.classList.add('reverse');
+ lblReverseLog.classList.add('warning');
+ }else{
+ eOut.classList.remove('reverse');
+ lblReverseLog.classList.remove('warning');
+ }
+ }, false);
+
+ const w = new Worker('batch-runner-sahpool.js?sqlite3.dir=jswasm');
+ w.onmessage = function(msg){
+ msg = msg.data;
+ switch(msg.type){
+ case 'stdout': log(...msg.data); break;
+ case 'warn': logWarn(...msg.data); break;
+ case 'error': logErr(...msg.data); break;
+ default:
+ logErr("Unhandled worker message type:",msg);
+ break;
+ }
+ };
+ })();
+ </script>
+ <style>
+ #test-output {
+ white-space: break-spaces;
+ overflow: auto;
+ }
+ </style>
+ </body>
+</html>
diff --git a/ext/wasm/batch-runner-sahpool.js b/ext/wasm/batch-runner-sahpool.js
new file mode 100644
index 0000000..dfa5044
--- /dev/null
+++ b/ext/wasm/batch-runner-sahpool.js
@@ -0,0 +1,341 @@
+/*
+ 2023-11-30
+
+ 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.
+
+ ***********************************************************************
+
+ A basic batch SQL runner for the SAHPool VFS. This file must be run in
+ a worker thread. This is not a full-featured app, just a way to get some
+ measurements for batch execution of SQL for the OPFS SAH Pool VFS.
+*/
+'use strict';
+
+const wMsg = function(msgType,...args){
+ postMessage({
+ type: msgType,
+ data: args
+ });
+};
+const toss = function(...args){throw new Error(args.join(' '))};
+const warn = (...args)=>{ wMsg('warn',...args); };
+const error = (...args)=>{ wMsg('error',...args); };
+const log = (...args)=>{ wMsg('stdout',...args); }
+let sqlite3;
+const urlParams = new URL(globalThis.location.href).searchParams;
+const cacheSize = (()=>{
+ if(urlParams.has('cachesize')) return +urlParams.get('cachesize');
+ return 200;
+})();
+
+
+/** Throws if the given sqlite3 result code is not 0. */
+const checkSqliteRc = (dbh,rc)=>{
+ if(rc) toss("Prepare failed:",sqlite3.capi.sqlite3_errmsg(dbh));
+};
+
+const sqlToDrop = [
+ "SELECT type,name FROM sqlite_schema ",
+ "WHERE name NOT LIKE 'sqlite\\_%' escape '\\' ",
+ "AND name NOT LIKE '\\_%' escape '\\'"
+].join('');
+
+const clearDbSqlite = function(db){
+ // This would be SO much easier with the oo1 API, but we specifically want to
+ // inject metrics we can't get via that API, and we cannot reliably (OPFS)
+ // open the same DB twice to clear it using that API, so...
+ const rc = sqlite3.wasm.exports.sqlite3_wasm_db_reset(db.handle);
+ log("reset db rc =",rc,db.id, db.filename);
+};
+
+const App = {
+ db: undefined,
+ cache:Object.create(null),
+ log: log,
+ warn: warn,
+ error: error,
+ metrics: {
+ fileCount: 0,
+ runTimeMs: 0,
+ prepareTimeMs: 0,
+ stepTimeMs: 0,
+ stmtCount: 0,
+ strcpyMs: 0,
+ sqlBytes: 0
+ },
+ fileList: undefined,
+ execSql: async function(name,sql){
+ const db = this.db;
+ const banner = "========================================";
+ this.log(banner,
+ "Running",name,'('+sql.length,'bytes)');
+ const capi = this.sqlite3.capi, wasm = this.sqlite3.wasm;
+ let pStmt = 0, pSqlBegin;
+ const metrics = db.metrics = Object.create(null);
+ metrics.prepTotal = metrics.stepTotal = 0;
+ metrics.stmtCount = 0;
+ metrics.malloc = 0;
+ metrics.strcpy = 0;
+ if(this.gotErr){
+ this.error("Cannot run SQL: error cleanup is pending.");
+ return;
+ }
+ // Run this async so that the UI can be updated for the above header...
+ const endRun = ()=>{
+ metrics.evalSqlEnd = performance.now();
+ metrics.evalTimeTotal = (metrics.evalSqlEnd - metrics.evalSqlStart);
+ this.log("metrics:",JSON.stringify(metrics, undefined, ' '));
+ this.log("prepare() count:",metrics.stmtCount);
+ this.log("Time in prepare_v2():",metrics.prepTotal,"ms",
+ "("+(metrics.prepTotal / metrics.stmtCount),"ms per prepare())");
+ this.log("Time in step():",metrics.stepTotal,"ms",
+ "("+(metrics.stepTotal / metrics.stmtCount),"ms per step())");
+ this.log("Total runtime:",metrics.evalTimeTotal,"ms");
+ this.log("Overhead (time - prep - step):",
+ (metrics.evalTimeTotal - metrics.prepTotal - metrics.stepTotal)+"ms");
+ this.log(banner,"End of",name);
+ this.metrics.prepareTimeMs += metrics.prepTotal;
+ this.metrics.stepTimeMs += metrics.stepTotal;
+ this.metrics.stmtCount += metrics.stmtCount;
+ this.metrics.strcpyMs += metrics.strcpy;
+ this.metrics.sqlBytes += sql.length;
+ };
+
+ const runner = function(resolve, reject){
+ ++this.metrics.fileCount;
+ metrics.evalSqlStart = performance.now();
+ const stack = wasm.scopedAllocPush();
+ try {
+ let t, rc;
+ let sqlByteLen = sql.byteLength;
+ const [ppStmt, pzTail] = wasm.scopedAllocPtr(2);
+ t = performance.now();
+ pSqlBegin = wasm.scopedAlloc( sqlByteLen + 1/*SQL + NUL*/) || toss("alloc(",sqlByteLen,") failed");
+ metrics.malloc = performance.now() - t;
+ metrics.byteLength = sqlByteLen;
+ let pSql = pSqlBegin;
+ const pSqlEnd = pSqlBegin + sqlByteLen;
+ t = performance.now();
+ wasm.heap8().set(sql, pSql);
+ wasm.poke(pSql + sqlByteLen, 0);
+ //log("SQL:",wasm.cstrToJs(pSql));
+ metrics.strcpy = performance.now() - t;
+ let breaker = 0;
+ while(pSql && wasm.peek8(pSql)){
+ wasm.pokePtr(ppStmt, 0);
+ wasm.pokePtr(pzTail, 0);
+ t = performance.now();
+ rc = capi.sqlite3_prepare_v2(
+ db.handle, pSql, sqlByteLen, ppStmt, pzTail
+ );
+ metrics.prepTotal += performance.now() - t;
+ checkSqliteRc(db.handle, rc);
+ pStmt = wasm.peekPtr(ppStmt);
+ pSql = wasm.peekPtr(pzTail);
+ sqlByteLen = pSqlEnd - pSql;
+ if(!pStmt) continue/*empty statement*/;
+ ++metrics.stmtCount;
+ t = performance.now();
+ rc = capi.sqlite3_step(pStmt);
+ capi.sqlite3_finalize(pStmt);
+ pStmt = 0;
+ metrics.stepTotal += performance.now() - t;
+ switch(rc){
+ case capi.SQLITE_ROW:
+ case capi.SQLITE_DONE: break;
+ default: checkSqliteRc(db.handle, rc); toss("Not reached.");
+ }
+ }
+ resolve(this);
+ }catch(e){
+ if(pStmt) capi.sqlite3_finalize(pStmt);
+ this.gotErr = e;
+ reject(e);
+ }finally{
+ capi.sqlite3_exec(db.handle,"rollback;",0,0,0);
+ wasm.scopedAllocPop(stack);
+ }
+ }.bind(this);
+ const p = new Promise(runner);
+ return p.catch(
+ (e)=>this.error("Error via execSql("+name+",...):",e.message)
+ ).finally(()=>{
+ endRun();
+ });
+ },
+
+ /**
+ Loads batch-runner.list and populates the selection list from
+ it. Returns a promise which resolves to nothing in particular
+ when it completes. Only intended to be run once at the start
+ of the app.
+ */
+ loadSqlList: async function(){
+ const infile = 'batch-runner.list';
+ this.log("Loading list of SQL files:", infile);
+ let txt;
+ try{
+ const r = await fetch(infile);
+ if(404 === r.status){
+ toss("Missing file '"+infile+"'.");
+ }
+ if(!r.ok) toss("Loading",infile,"failed:",r.statusText);
+ txt = await r.text();
+ }catch(e){
+ this.error(e.message);
+ throw e;
+ }
+ App.fileList = txt.split(/\n+/).filter(x=>!!x);
+ this.log("Loaded",infile);
+ },
+
+ /** Fetch ./fn and return its contents as a Uint8Array. */
+ fetchFile: async function(fn, cacheIt=false){
+ if(cacheIt && this.cache[fn]) return this.cache[fn];
+ this.log("Fetching",fn,"...");
+ let sql;
+ try {
+ const r = await fetch(fn);
+ if(!r.ok) toss("Fetch failed:",r.statusText);
+ sql = new Uint8Array(await r.arrayBuffer());
+ }catch(e){
+ this.error(e.message);
+ throw e;
+ }
+ this.log("Fetched",sql.length,"bytes from",fn);
+ if(cacheIt) this.cache[fn] = sql;
+ return sql;
+ }/*fetchFile()*/,
+
+ /**
+ Converts this.metrics() to a form which is suitable for easy conversion to
+ CSV. It returns an array of arrays. The first sub-array is the column names.
+ The 2nd and subsequent are the values, one per test file (only the most recent
+ metrics are kept for any given file).
+ */
+ metricsToArrays: function(){
+ const rc = [];
+ Object.keys(this.dbs).sort().forEach((k)=>{
+ const d = this.dbs[k];
+ const m = d.metrics;
+ delete m.evalSqlStart;
+ delete m.evalSqlEnd;
+ const mk = Object.keys(m).sort();
+ if(!rc.length){
+ rc.push(['db', ...mk]);
+ }
+ const row = [k.split('/').pop()/*remove dir prefix from filename*/];
+ rc.push(row);
+ row.push(...mk.map((kk)=>m[kk]));
+ });
+ return rc;
+ },
+
+ metricsToBlob: function(colSeparator='\t'){
+ const ar = [], ma = this.metricsToArrays();
+ if(!ma.length){
+ this.error("Metrics are empty. Run something.");
+ return;
+ }
+ ma.forEach(function(row){
+ ar.push(row.join(colSeparator),'\n');
+ });
+ return new Blob(ar);
+ },
+
+ /**
+ Fetch file fn and eval it as an SQL blob. This is an async
+ operation and returns a Promise which resolves to this
+ object on success.
+ */
+ evalFile: async function(fn){
+ const sql = await this.fetchFile(fn);
+ return this.execSql(fn,sql);
+ }/*evalFile()*/,
+
+ /**
+ Fetches the handle of the db associated with
+ this.e.selImpl.value, opening it if needed.
+ */
+ initDb: function(){
+ const capi = this.sqlite3.capi, wasm = this.sqlite3.wasm;
+ const stack = wasm.scopedAllocPush();
+ let pDb = 0;
+ const d = Object.create(null);
+ d.filename = "/batch.db";
+ try{
+ const oFlags = capi.SQLITE_OPEN_CREATE | capi.SQLITE_OPEN_READWRITE;
+ const ppDb = wasm.scopedAllocPtr();
+ const rc = capi.sqlite3_open_v2(d.filename, ppDb, oFlags, this.PoolUtil.vfsName);
+ pDb = wasm.peekPtr(ppDb)
+ if(rc) toss("sqlite3_open_v2() failed with code",rc);
+ capi.sqlite3_exec(pDb, "PRAGMA cache_size="+cacheSize, 0, 0, 0);
+ this.log("cache_size =",cacheSize);
+ }catch(e){
+ if(pDb) capi.sqlite3_close_v2(pDb);
+ throw e;
+ }finally{
+ wasm.scopedAllocPop(stack);
+ }
+ d.handle = pDb;
+ this.log("Opened db:",d.filename,'@',d.handle);
+ return d;
+ },
+
+ closeDb: function(){
+ if(this.db.handle){
+ this.sqlite3.capi.sqlite3_close_v2(this.db.handle);
+ this.db.handle = undefined;
+ }
+ },
+
+ run: async function(sqlite3){
+ delete this.run;
+ this.sqlite3 = sqlite3;
+ const capi = sqlite3.capi, wasm = sqlite3.wasm;
+ this.log("Loaded module:",capi.sqlite3_libversion(), capi.sqlite3_sourceid());
+ this.log("WASM heap size =",wasm.heap8().length);
+ let timeStart;
+ sqlite3.installOpfsSAHPoolVfs({
+ clearOnInit: true, initialCapacity: 4,
+ name: 'batch-sahpool',
+ verbosity: 2
+ }).then(PoolUtil=>{
+ App.PoolUtil = PoolUtil;
+ App.db = App.initDb();
+ })
+ .then(async ()=>this.loadSqlList())
+ .then(async ()=>{
+ timeStart = performance.now();
+ for(let i = 0; i < App.fileList.length; ++i){
+ const fn = App.fileList[i];
+ await App.evalFile(fn);
+ if(App.gotErr) throw App.gotErr;
+ }
+ })
+ .then(()=>{
+ App.metrics.runTimeMs = performance.now() - timeStart;
+ App.log("total metrics:",JSON.stringify(App.metrics, undefined, ' '));
+ App.log("Reload the page to run this again.");
+ App.closeDb();
+ App.PoolUtil.removeVfs();
+ })
+ .catch(e=>this.error("ERROR:",e));
+ }/*run()*/
+}/*App*/;
+
+let sqlite3Js = 'sqlite3.js';
+if(urlParams.has('sqlite3.dir')){
+ sqlite3Js = urlParams.get('sqlite3.dir') + '/' + sqlite3Js;
+}
+importScripts(sqlite3Js);
+globalThis.sqlite3InitModule().then(async function(sqlite3_){
+ log("Done initializing. Running batch runner...");
+ sqlite3 = sqlite3_;
+ App.run(sqlite3_);
+});
diff --git a/ext/wasm/batch-runner.html b/ext/wasm/batch-runner.html
new file mode 100644
index 0000000..5258f95
--- /dev/null
+++ b/ext/wasm/batch-runner.html
@@ -0,0 +1,90 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>sqlite3-api batch SQL runner</title>
+ </head>
+ <body>
+ <header id='titlebar'><span>sqlite3-api batch SQL runner</span></header>
+ <!-- emscripten bits -->
+ <figure id="module-spinner">
+ <div class="spinner"></div>
+ <div class='center'><strong>Initializing app...</strong></div>
+ <div class='center'>
+ On a slow internet connection this may take a moment. If this
+ message displays for "a long time", intialization may have
+ failed and the JavaScript console may contain clues as to why.
+ </div>
+ </figure>
+ <div class="emscripten" id="module-status">Downloading...</div>
+ <div class="emscripten">
+ <progress value="0" max="100" id="module-progress" hidden='1'></progress>
+ </div><!-- /emscripten bits -->
+ <p>
+ This page is for batch-running extracts from the output
+ of <tt>speedtest1 --script</tt>, as well as other standalone SQL
+ scripts.
+ </p>
+ <p id='warn-list' class='warning'>ACHTUNG: this file requires a generated input list
+ file. Run "make batch" from this directory to generate it.
+ </p>
+ <p id='warn-opfs' class='warning hidden'>WARNING: if the WASMFS/OPFS layer crashes, this page may
+ become completely unresponsive and need to be closed and reloaded to recover.
+ </p>
+ <p id='warn-websql' class='warning hidden'>WARNING: WebSQL's limited API requires that
+ this app split up SQL batches into separate statements for execution. That will
+ only work so long as semicolon characters are <em>only</em> used to terminate
+ SQL statements, and not used within string literals or the like.
+ </p>
+ <hr>
+ <fieldset id='toolbar'>
+ <div>
+ <select class='disable-during-eval' id='sql-select'>
+ <option disabled selected>Populated via script code</option>
+ </select>
+ <button class='disable-during-eval' id='sql-run'>Run selected SQL</button>
+ <button class='disable-during-eval' id='sql-run-next'>Run next...</button>
+ <button class='disable-during-eval' id='sql-run-remaining'>Run all remaining...</button>
+ <button class='disable-during-eval' id='export-metrics' disabled>Export metrics (WIP)<br>(broken by refactoring)</button>
+ <button class='disable-during-eval' id='db-reset'>Reset db</button>
+ <button id='output-clear'>Clear output</button>
+ <span class='input-wrapper flex-col'>
+ <label for='select-impl'>Storage impl:</label>
+ <select id='select-impl'>
+ <option value='virtualfs'>Virtual filesystem</option>
+ <option value='memdb'>:memory:</option>
+ <option value='wasmfs-opfs'>WASMFS OPFS</option>
+ <option value='websql'>WebSQL</option>
+ </select>
+ </span>
+ </fieldset>
+ </div>
+ <hr>
+ <span class='input-wrapper'>
+ <input type='checkbox' class='disable-during-eval' id='cb-reverse-log-order' checked></input>
+ <label for='cb-reverse-log-order'>Reverse log order (newest first)</label>
+ </span>
+ <div id='test-output'></div>
+ <script src="jswasm/sqlite3.js"></script>
+ <script src="common/SqliteTestUtil.js"></script>
+ <script src="batch-runner.js"></script>
+ <style>
+ .flex-col {
+ display: flex;
+ flex-direction: column;
+ }
+ #toolbar > div {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ }
+ #toolbar > div > * {
+ margin: 0.25em;
+ }
+ </style>
+ </body>
+</html>
diff --git a/ext/wasm/batch-runner.js b/ext/wasm/batch-runner.js
new file mode 100644
index 0000000..e7a322b
--- /dev/null
+++ b/ext/wasm/batch-runner.js
@@ -0,0 +1,604 @@
+/*
+ 2022-08-29
+
+ 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.
+
+ ***********************************************************************
+
+ A basic batch SQL runner for sqlite3-api.js. This file must be run in
+ main JS thread and sqlite3.js must have been loaded before it.
+*/
+'use strict';
+(function(){
+ const toss = function(...args){throw new Error(args.join(' '))};
+ const warn = console.warn.bind(console);
+ let sqlite3;
+ const urlParams = new URL(self.location.href).searchParams;
+ const cacheSize = (()=>{
+ if(urlParams.has('cachesize')) return +urlParams.get('cachesize');
+ return 200;
+ })();
+
+ /** Throws if the given sqlite3 result code is not 0. */
+ const checkSqliteRc = (dbh,rc)=>{
+ if(rc) toss("Prepare failed:",sqlite3.capi.sqlite3_errmsg(dbh));
+ };
+
+ const sqlToDrop = [
+ "SELECT type,name FROM sqlite_schema ",
+ "WHERE name NOT LIKE 'sqlite\\_%' escape '\\' ",
+ "AND name NOT LIKE '\\_%' escape '\\'"
+ ].join('');
+
+ const clearDbWebSQL = function(db){
+ db.handle.transaction(function(tx){
+ const onErr = (e)=>console.error(e);
+ const callback = function(tx, result){
+ const rows = result.rows;
+ let i, n;
+ i = n = rows.length;
+ while(i--){
+ const row = rows.item(i);
+ const name = JSON.stringify(row.name);
+ const type = row.type;
+ switch(type){
+ case 'index': case 'table':
+ case 'trigger': case 'view': {
+ const sql2 = 'DROP '+type+' '+name;
+ tx.executeSql(sql2, [], ()=>{}, onErr);
+ break;
+ }
+ default:
+ warn("Unhandled db entry type:",type,'name =',name);
+ break;
+ }
+ }
+ };
+ tx.executeSql(sqlToDrop, [], callback, onErr);
+ db.handle.changeVersion(db.handle.version, "", ()=>{}, onErr, ()=>{});
+ });
+ };
+
+ const clearDbSqlite = function(db){
+ // This would be SO much easier with the oo1 API, but we specifically want to
+ // inject metrics we can't get via that API, and we cannot reliably (OPFS)
+ // open the same DB twice to clear it using that API, so...
+ const rc = sqlite3.wasm.exports.sqlite3_wasm_db_reset(db.handle);
+ App.logHtml("reset db rc =",rc,db.id, db.filename);
+ };
+
+ const E = (s)=>document.querySelector(s);
+ const App = {
+ e: {
+ output: E('#test-output'),
+ selSql: E('#sql-select'),
+ btnRun: E('#sql-run'),
+ btnRunNext: E('#sql-run-next'),
+ btnRunRemaining: E('#sql-run-remaining'),
+ btnExportMetrics: E('#export-metrics'),
+ btnClear: E('#output-clear'),
+ btnReset: E('#db-reset'),
+ cbReverseLog: E('#cb-reverse-log-order'),
+ selImpl: E('#select-impl'),
+ fsToolbar: E('#toolbar')
+ },
+ db: Object.create(null),
+ dbs: Object.create(null),
+ cache:{},
+ metrics: {
+ fileCount: 0,
+ runTimeMs: 0,
+ prepareTimeMs: 0,
+ stepTimeMs: 0,
+ stmtCount: 0,
+ strcpyMs: 0,
+ sqlBytes: 0
+ },
+ log: console.log.bind(console),
+ warn: console.warn.bind(console),
+ cls: function(){this.e.output.innerHTML = ''},
+ logHtml2: function(cssClass,...args){
+ const ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ this.e.output.append(ln);
+ //this.e.output.lastElementChild.scrollIntoViewIfNeeded();
+ },
+ logHtml: function(...args){
+ console.log(...args);
+ if(1) this.logHtml2('', ...args);
+ },
+ logErr: function(...args){
+ console.error(...args);
+ if(1) this.logHtml2('error', ...args);
+ },
+
+ execSql: async function(name,sql){
+ const db = this.getSelectedDb();
+ const banner = "========================================";
+ this.logHtml(banner,
+ "Running",name,'('+sql.length,'bytes) using',db.id);
+ const capi = this.sqlite3.capi, wasm = this.sqlite3.wasm;
+ let pStmt = 0, pSqlBegin;
+ const metrics = db.metrics = Object.create(null);
+ metrics.prepTotal = metrics.stepTotal = 0;
+ metrics.stmtCount = 0;
+ metrics.malloc = 0;
+ metrics.strcpy = 0;
+ this.blockControls(true);
+ if(this.gotErr){
+ this.logErr("Cannot run SQL: error cleanup is pending.");
+ return;
+ }
+ // Run this async so that the UI can be updated for the above header...
+ const endRun = ()=>{
+ metrics.evalSqlEnd = performance.now();
+ metrics.evalTimeTotal = (metrics.evalSqlEnd - metrics.evalSqlStart);
+ this.logHtml(db.id,"metrics:",JSON.stringify(metrics, undefined, ' '));
+ this.logHtml("prepare() count:",metrics.stmtCount);
+ this.logHtml("Time in prepare_v2():",metrics.prepTotal,"ms",
+ "("+(metrics.prepTotal / metrics.stmtCount),"ms per prepare())");
+ this.logHtml("Time in step():",metrics.stepTotal,"ms",
+ "("+(metrics.stepTotal / metrics.stmtCount),"ms per step())");
+ this.logHtml("Total runtime:",metrics.evalTimeTotal,"ms");
+ this.logHtml("Overhead (time - prep - step):",
+ (metrics.evalTimeTotal - metrics.prepTotal - metrics.stepTotal)+"ms");
+ this.logHtml(banner,"End of",name);
+ this.metrics.prepareTimeMs += metrics.prepTotal;
+ this.metrics.stepTimeMs += metrics.stepTotal;
+ this.metrics.stmtCount += metrics.stmtCount;
+ this.metrics.strcpyMs += metrics.strcpy;
+ this.metrics.sqlBytes += sql.length;
+ };
+
+ let runner;
+ if('websql'===db.id){
+ const who = this;
+ runner = function(resolve, reject){
+ /* WebSQL cannot execute multiple statements, nor can it execute SQL without
+ an explicit transaction. Thus we have to do some fragile surgery on the
+ input SQL. Since we're only expecting carefully curated inputs, the hope is
+ that this will suffice. PS: it also can't run most SQL functions, e.g. even
+ instr() results in "not authorized". */
+ if('string'!==typeof sql){ // assume TypedArray
+ sql = new TextDecoder().decode(sql);
+ }
+ sql = sql.replace(/-- [^\n]+\n/g,''); // comment lines interfere with our split()
+ const sqls = sql.split(/;+\n/);
+ const rxBegin = /^BEGIN/i, rxCommit = /^COMMIT/i;
+ try {
+ const nextSql = ()=>{
+ let x = sqls.shift();
+ while(sqls.length && !x) x = sqls.shift();
+ return x && x.trim();
+ };
+ const who = this;
+ const transaction = function(tx){
+ try {
+ let s;
+ /* Try to approximate the spirit of the input scripts
+ by running batches bound by BEGIN/COMMIT statements. */
+ for(s = nextSql(); !!s; s = nextSql()){
+ if(rxBegin.test(s)) continue;
+ else if(rxCommit.test(s)) break;
+ //console.log("websql sql again",sqls.length, s);
+ ++metrics.stmtCount;
+ const t = performance.now();
+ tx.executeSql(s,[], ()=>{}, (t,e)=>{
+ console.error("WebSQL error",e,"SQL =",s);
+ who.logErr(e.message);
+ //throw e;
+ return false;
+ });
+ metrics.stepTotal += performance.now() - t;
+ }
+ }catch(e){
+ who.logErr("transaction():",e.message);
+ throw e;
+ }
+ };
+ const n = sqls.length;
+ const nextBatch = function(){
+ if(sqls.length){
+ console.log("websql sqls.length",sqls.length,'of',n);
+ db.handle.transaction(transaction, (e)=>{
+ who.logErr("Ignoring and contiuing:",e.message)
+ //reject(e);
+ return false;
+ }, nextBatch);
+ }else{
+ resolve(who);
+ }
+ };
+ metrics.evalSqlStart = performance.now();
+ nextBatch();
+ }catch(e){
+ //this.gotErr = e;
+ console.error("websql error:",e);
+ who.logErr(e.message);
+ //reject(e);
+ }
+ }.bind(this);
+ }else{/*sqlite3 db...*/
+ runner = function(resolve, reject){
+ ++this.metrics.fileCount;
+ metrics.evalSqlStart = performance.now();
+ const stack = wasm.scopedAllocPush();
+ try {
+ let t;
+ let sqlByteLen = sql.byteLength;
+ const [ppStmt, pzTail] = wasm.scopedAllocPtr(2);
+ t = performance.now();
+ pSqlBegin = wasm.scopedAlloc( sqlByteLen + 1/*SQL + NUL*/) || toss("alloc(",sqlByteLen,") failed");
+ metrics.malloc = performance.now() - t;
+ metrics.byteLength = sqlByteLen;
+ let pSql = pSqlBegin;
+ const pSqlEnd = pSqlBegin + sqlByteLen;
+ t = performance.now();
+ wasm.heap8().set(sql, pSql);
+ wasm.poke(pSql + sqlByteLen, 0);
+ metrics.strcpy = performance.now() - t;
+ let breaker = 0;
+ while(pSql && wasm.peek(pSql,'i8')){
+ wasm.pokePtr(ppStmt, 0);
+ wasm.pokePtr(pzTail, 0);
+ t = performance.now();
+ let rc = capi.sqlite3_prepare_v3(
+ db.handle, pSql, sqlByteLen, 0, ppStmt, pzTail
+ );
+ metrics.prepTotal += performance.now() - t;
+ checkSqliteRc(db.handle, rc);
+ pStmt = wasm.peekPtr(ppStmt);
+ pSql = wasm.peekPtr(pzTail);
+ sqlByteLen = pSqlEnd - pSql;
+ if(!pStmt) continue/*empty statement*/;
+ ++metrics.stmtCount;
+ t = performance.now();
+ rc = capi.sqlite3_step(pStmt);
+ capi.sqlite3_finalize(pStmt);
+ pStmt = 0;
+ metrics.stepTotal += performance.now() - t;
+ switch(rc){
+ case capi.SQLITE_ROW:
+ case capi.SQLITE_DONE: break;
+ default: checkSqliteRc(db.handle, rc); toss("Not reached.");
+ }
+ }
+ resolve(this);
+ }catch(e){
+ if(pStmt) capi.sqlite3_finalize(pStmt);
+ //this.gotErr = e;
+ reject(e);
+ }finally{
+ capi.sqlite3_exec(db.handle,"rollback;",0,0,0);
+ wasm.scopedAllocPop(stack);
+ }
+ }.bind(this);
+ }
+ let p;
+ if(1){
+ p = new Promise(function(res,rej){
+ setTimeout(()=>runner(res, rej), 0)/*give UI a chance to output the "running" banner*/;
+ });
+ }else{
+ p = new Promise(runner);
+ }
+ return p.catch(
+ (e)=>this.logErr("Error via execSql("+name+",...):",e.message)
+ ).finally(()=>{
+ endRun();
+ this.blockControls(false);
+ });
+ },
+
+ clearDb: function(){
+ const db = this.getSelectedDb();
+ if('websql'===db.id){
+ this.logErr("TODO: clear websql db.");
+ return;
+ }
+ if(!db.handle) return;
+ const capi = this.sqlite3, wasm = this.sqlite3.wasm;
+ //const scope = wasm.scopedAllocPush(
+ this.logErr("TODO: clear db");
+ },
+
+ /**
+ Loads batch-runner.list and populates the selection list from
+ it. Returns a promise which resolves to nothing in particular
+ when it completes. Only intended to be run once at the start
+ of the app.
+ */
+ loadSqlList: async function(){
+ const sel = this.e.selSql;
+ sel.innerHTML = '';
+ this.blockControls(true);
+ const infile = 'batch-runner.list';
+ this.logHtml("Loading list of SQL files:", infile);
+ let txt;
+ try{
+ const r = await fetch(infile);
+ if(404 === r.status){
+ toss("Missing file '"+infile+"'.");
+ }
+ if(!r.ok) toss("Loading",infile,"failed:",r.statusText);
+ txt = await r.text();
+ const warning = E('#warn-list');
+ if(warning) warning.remove();
+ }catch(e){
+ this.logErr(e.message);
+ throw e;
+ }finally{
+ this.blockControls(false);
+ }
+ const list = txt.split(/\n+/);
+ let opt;
+ if(0){
+ opt = document.createElement('option');
+ opt.innerText = "Select file to evaluate...";
+ opt.value = '';
+ opt.disabled = true;
+ opt.selected = true;
+ sel.appendChild(opt);
+ }
+ list.forEach(function(fn){
+ if(!fn) return;
+ opt = document.createElement('option');
+ opt.value = fn;
+ opt.innerText = fn.split('/').pop();
+ sel.appendChild(opt);
+ });
+ this.logHtml("Loaded",infile);
+ },
+
+ /** Fetch ./fn and return its contents as a Uint8Array. */
+ fetchFile: async function(fn, cacheIt=false){
+ if(cacheIt && this.cache[fn]) return this.cache[fn];
+ this.logHtml("Fetching",fn,"...");
+ let sql;
+ try {
+ const r = await fetch(fn);
+ if(!r.ok) toss("Fetch failed:",r.statusText);
+ sql = new Uint8Array(await r.arrayBuffer());
+ }catch(e){
+ this.logErr(e.message);
+ throw e;
+ }
+ this.logHtml("Fetched",sql.length,"bytes from",fn);
+ if(cacheIt) this.cache[fn] = sql;
+ return sql;
+ }/*fetchFile()*/,
+
+ /** Disable or enable certain UI controls. */
+ blockControls: function(disable){
+ //document.querySelectorAll('.disable-during-eval').forEach((e)=>e.disabled = disable);
+ this.e.fsToolbar.disabled = disable;
+ },
+
+ /**
+ Converts this.metrics() to a form which is suitable for easy conversion to
+ CSV. It returns an array of arrays. The first sub-array is the column names.
+ The 2nd and subsequent are the values, one per test file (only the most recent
+ metrics are kept for any given file).
+ */
+ metricsToArrays: function(){
+ const rc = [];
+ Object.keys(this.dbs).sort().forEach((k)=>{
+ const d = this.dbs[k];
+ const m = d.metrics;
+ delete m.evalSqlStart;
+ delete m.evalSqlEnd;
+ const mk = Object.keys(m).sort();
+ if(!rc.length){
+ rc.push(['db', ...mk]);
+ }
+ const row = [k.split('/').pop()/*remove dir prefix from filename*/];
+ rc.push(row);
+ row.push(...mk.map((kk)=>m[kk]));
+ });
+ return rc;
+ },
+
+ metricsToBlob: function(colSeparator='\t'){
+ const ar = [], ma = this.metricsToArrays();
+ if(!ma.length){
+ this.logErr("Metrics are empty. Run something.");
+ return;
+ }
+ ma.forEach(function(row){
+ ar.push(row.join(colSeparator),'\n');
+ });
+ return new Blob(ar);
+ },
+
+ downloadMetrics: function(){
+ const b = this.metricsToBlob();
+ if(!b) return;
+ const url = URL.createObjectURL(b);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'batch-runner-js-'+((new Date().getTime()/1000) | 0)+'.csv';
+ this.logHtml("Triggering download of",a.download);
+ document.body.appendChild(a);
+ a.click();
+ setTimeout(()=>{
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }, 500);
+ },
+
+ /**
+ Fetch file fn and eval it as an SQL blob. This is an async
+ operation and returns a Promise which resolves to this
+ object on success.
+ */
+ evalFile: async function(fn){
+ const sql = await this.fetchFile(fn);
+ return this.execSql(fn,sql);
+ }/*evalFile()*/,
+
+ /**
+ Clears all DB tables in all _opened_ databases. Because of
+ disparities between backends, we cannot simply "unlink" the
+ databases to clean them up.
+ */
+ clearStorage: function(onlySelectedDb=false){
+ const list = onlySelectedDb
+ ? [('boolean'===typeof onlySelectedDb)
+ ? this.dbs[this.e.selImpl.value]
+ : onlySelectedDb]
+ : Object.values(this.dbs);
+ for(let db of list){
+ if(db && db.handle){
+ this.logHtml("Clearing db",db.id);
+ db.clear();
+ }
+ }
+ },
+
+ /**
+ Fetches the handle of the db associated with
+ this.e.selImpl.value, opening it if needed.
+ */
+ getSelectedDb: function(){
+ if(!this.dbs.memdb){
+ for(let opt of this.e.selImpl.options){
+ const d = this.dbs[opt.value] = Object.create(null);
+ d.id = opt.value;
+ switch(d.id){
+ case 'virtualfs':
+ d.filename = 'file:/virtualfs.sqlite3?vfs=unix-none';
+ break;
+ case 'memdb':
+ d.filename = ':memory:';
+ break;
+ case 'wasmfs-opfs':
+ d.filename = 'file:'+(
+ this.sqlite3.capi.sqlite3_wasmfs_opfs_dir()
+ )+'/wasmfs-opfs.sqlite3b';
+ break;
+ case 'websql':
+ d.filename = 'websql.db';
+ break;
+ default:
+ this.logErr("Unhandled db selection option (see details in the console).",opt);
+ toss("Unhandled db init option");
+ }
+ }
+ }/*first-time init*/
+ const dbId = this.e.selImpl.value;
+ const d = this.dbs[dbId];
+ if(d.handle) return d;
+ if('websql' === dbId){
+ d.handle = self.openDatabase('batch-runner', '0.1', 'foo', 1024 * 1024 * 50);
+ d.clear = ()=>clearDbWebSQL(d);
+ d.handle.transaction(function(tx){
+ tx.executeSql("PRAGMA cache_size="+cacheSize);
+ App.logHtml(dbId,"cache_size =",cacheSize);
+ });
+ }else{
+ const capi = this.sqlite3.capi, wasm = this.sqlite3.wasm;
+ const stack = wasm.scopedAllocPush();
+ let pDb = 0;
+ try{
+ const oFlags = capi.SQLITE_OPEN_CREATE | capi.SQLITE_OPEN_READWRITE;
+ const ppDb = wasm.scopedAllocPtr();
+ const rc = capi.sqlite3_open_v2(d.filename, ppDb, oFlags, null);
+ pDb = wasm.peekPtr(ppDb)
+ if(rc) toss("sqlite3_open_v2() failed with code",rc);
+ capi.sqlite3_exec(pDb, "PRAGMA cache_size="+cacheSize, 0, 0, 0);
+ this.logHtml(dbId,"cache_size =",cacheSize);
+ }catch(e){
+ if(pDb) capi.sqlite3_close_v2(pDb);
+ }finally{
+ wasm.scopedAllocPop(stack);
+ }
+ d.handle = pDb;
+ d.clear = ()=>clearDbSqlite(d);
+ }
+ d.clear();
+ this.logHtml("Opened db:",dbId,d.filename);
+ console.log("db =",d);
+ return d;
+ },
+
+ run: function(sqlite3){
+ delete this.run;
+ this.sqlite3 = sqlite3;
+ const capi = sqlite3.capi, wasm = sqlite3.wasm;
+ this.logHtml("Loaded module:",capi.sqlite3_libversion(), capi.sqlite3_sourceid());
+ this.logHtml("WASM heap size =",wasm.heap8().length);
+ this.loadSqlList();
+ if(capi.sqlite3_wasmfs_opfs_dir()){
+ E('#warn-opfs').classList.remove('hidden');
+ }else{
+ E('#warn-opfs').remove();
+ E('option[value=wasmfs-opfs]').disabled = true;
+ }
+ if('function' === typeof self.openDatabase){
+ E('#warn-websql').classList.remove('hidden');
+ }else{
+ E('option[value=websql]').disabled = true;
+ E('#warn-websql').remove();
+ }
+ const who = this;
+ if(this.e.cbReverseLog.checked){
+ this.e.output.classList.add('reverse');
+ }
+ this.e.cbReverseLog.addEventListener('change', function(){
+ who.e.output.classList[this.checked ? 'add' : 'remove']('reverse');
+ }, false);
+ this.e.btnClear.addEventListener('click', ()=>this.cls(), false);
+ this.e.btnRun.addEventListener('click', function(){
+ if(!who.e.selSql.value) return;
+ who.evalFile(who.e.selSql.value);
+ }, false);
+ this.e.btnRunNext.addEventListener('click', function(){
+ ++who.e.selSql.selectedIndex;
+ if(!who.e.selSql.value) return;
+ who.evalFile(who.e.selSql.value);
+ }, false);
+ this.e.btnReset.addEventListener('click', function(){
+ who.clearStorage(true);
+ }, false);
+ this.e.btnExportMetrics.addEventListener('click', function(){
+ who.logHtml2('warning',"Triggering download of metrics CSV. Check your downloads folder.");
+ who.downloadMetrics();
+ //const m = who.metricsToArrays();
+ //console.log("Metrics:",who.metrics, m);
+ });
+ this.e.selImpl.addEventListener('change', function(){
+ who.getSelectedDb();
+ });
+ this.e.btnRunRemaining.addEventListener('click', async function(){
+ let v = who.e.selSql.value;
+ const timeStart = performance.now();
+ while(v){
+ await who.evalFile(v);
+ if(who.gotError){
+ who.logErr("Error handling script",v,":",who.gotError.message);
+ break;
+ }
+ ++who.e.selSql.selectedIndex;
+ v = who.e.selSql.value;
+ }
+ const timeTotal = performance.now() - timeStart;
+ who.logHtml("Run-remaining time:",timeTotal,"ms ("+(timeTotal/1000/60)+" minute(s))");
+ who.clearStorage();
+ App.metrics.runTimeMs = timeTotal;
+ who.logHtml("Total metrics:",JSON.stringify(App.metrics,undefined,' '));
+ }, false);
+ }/*run()*/
+ }/*App*/;
+
+ self.sqlite3TestModule.initSqlite3().then(function(sqlite3_){
+ sqlite3 = sqlite3_;
+ self.App = App /* only to facilitate dev console access */;
+ App.run(sqlite3);
+ });
+})();
diff --git a/ext/wasm/c-pp.c b/ext/wasm/c-pp.c
new file mode 100644
index 0000000..c439a0d
--- /dev/null
+++ b/ext/wasm/c-pp.c
@@ -0,0 +1,1530 @@
+/*
+** 2022-11-12:
+**
+** 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 C-minus Preprocessor: a truly minimal C-like preprocessor.
+** Why? Because C preprocessors _can_ process non-C code but generally make
+** quite a mess of it. The purpose of this application is an extremely
+** minimal preprocessor with only the most basic functionality of a C
+** preprocessor, namely:
+**
+** - Limited `#if`, where its one argument is a macro name which
+** resolves to true if it's defined, false if it's not. Likewise,
+** `#ifnot` is the inverse. Includes `#else` and `#elif` and
+** `#elifnot`. Such chains are terminated with `#endif`.
+**
+** - `#define` accepts one or more arguments, the names of
+** macros. Each one is implicitly true.
+**
+** - `#undef` undefine one or more macros.
+**
+** - `#error` treats the rest of the line as a fatal error message.
+**
+** - `#include` treats its argument as a filename token (NOT quoted,
+** though support for quoting may be added later). Some effort is
+** made to prevent recursive inclusion, but that support is both
+** somewhat fragile and possibly completely unnecessary.
+**
+** - `#pragma` is in place for adding "meta-commands", but it does not
+** yet have any concrete list of documented commands.
+**
+* - `#stderr` outputs its file name, line number, and the remaininder
+** of that line to stderr.
+**
+** - `#//` acts as a single-line comment, noting that there must be as
+** space after the `//` part because `//` is (despite appearances)
+** parsed like a keyword.
+**
+** Note that "#" above is symbolic. The keyword delimiter is
+** configurable and defaults to "##". Define CMPP_DEFAULT_DELIM to a
+** string when compiling to define the default at build-time.
+**
+** This preprocessor does no expansion of content except within the
+** bounds of its `#keywords`.
+**
+** Design note: this code makes use of sqlite3. Though not _strictly_
+** needed in order to implement it, this tool was specifically created
+** for use with the sqlite3 project's own JavaScript code, so there's
+** no reason not to make use of it to do some of the heavy lifting. It
+** does not require any cutting-edge sqlite3 features and should be
+** usable with any version which supports `WITHOUT ROWID`.
+**
+** Author(s):
+**
+** - Stephan Beal <https://wanderinghorse.net/home/stephan/>
+*/
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <stdarg.h>
+#include <assert.h>
+#include <ctype.h>
+
+#include "sqlite3.h"
+
+#if defined(_WIN32) || defined(WIN32)
+# include <io.h>
+# include <fcntl.h>
+# ifndef access
+# define access(f,m) _access((f),(m))
+# endif
+#else
+# include <unistd.h>
+#endif
+
+#ifndef CMPP_DEFAULT_DELIM
+#define CMPP_DEFAULT_DELIM "##"
+#endif
+
+#if 1
+# define CMPP_NORETURN __attribute__((noreturn))
+#else
+# define CMPP_NORETURN
+#endif
+
+/* Fatally exits the app with the given printf-style message. */
+static CMPP_NORETURN void fatalv(char const *zFmt, va_list);
+static CMPP_NORETURN void fatal(char const *zFmt, ...);
+
+/** Proxy for free(), for symmetry with cmpp_realloc(). */
+static void cmpp_free(void *p);
+/** A realloc() proxy which dies fatally on allocation error. */
+static void * cmpp_realloc(void * p, unsigned n);
+#if 0
+/** A malloc() proxy which dies fatally on allocation error. */
+static void * cmpp_malloc(unsigned n);
+#endif
+
+/*
+** If p is stdin or stderr then this is a no-op, else it is a
+** proxy for fclose(). This is a no-op if p is NULL.
+*/
+static void FILE_close(FILE *p);
+/*
+** Works like fopen() but accepts the special name "-" to mean either
+** stdin (if zMode indicates a real-only mode) or stdout. Fails
+** fatally on error.
+*/
+static FILE * FILE_open(char const *zName, const char * zMode);
+/*
+** Reads the entire contents of the given file, allocating it in a
+** buffer which gets assigned to `*pOut`. `*nOut` gets assigned the
+** length of the output buffer. Fails fatally on error.
+*/
+static void FILE_slurp(FILE *pFile, unsigned char **pOut,
+ unsigned * nOut);
+
+/*
+** Intended to be passed an sqlite3 result code. If it's non-0
+** then it emits a fatal error message which contains both the
+** given string and the sqlite3_errmsg() from the application's
+** database instance.
+*/
+static void db_affirm_rc(int rc, const char * zMsg);
+
+/*
+** Proxy for sqlite3_str_finish() which fails fatally if that
+** routine returns NULL.
+*/
+static char * db_str_finish(sqlite3_str *s, int * n);
+/*
+** Proxy for sqlite3_str_new() which fails fatally if that
+** routine returns NULL.
+*/
+static sqlite3_str * db_str_new(void);
+
+/* Proxy for sqlite3_finalize(). */
+static void db_finalize(sqlite3_stmt *pStmt);
+/*
+** Proxy for sqlite3_step() which fails fatally if the result
+** is anything other than SQLITE_ROW or SQLITE_DONE.
+*/
+static int db_step(sqlite3_stmt *pStmt);
+/*
+** Proxy for sqlite3_bind_int() which fails fatally on error.
+*/
+static void db_bind_int(sqlite3_stmt *pStmt, int col, int val);
+#if 0
+/*
+** Proxy for sqlite3_bind_null() which fails fatally on error.
+*/
+static void db_bind_null(sqlite3_stmt *pStmt, int col);
+#endif
+/*
+** Proxy for sqlite3_bind_text() which fails fatally on error.
+*/
+static void db_bind_text(sqlite3_stmt *pStmt, int col, const char * zStr);
+/*
+** Proxy for sqlite3_bind_text() which fails fatally on error.
+*/
+static void db_bind_textn(sqlite3_stmt *pStmt, int col, const char * zStr, int len);
+#if 0
+/*
+** Proxy for sqlite3_bind_text() which fails fatally on error. It uses
+** sqlite3_str_vappendf() so supports all of its formatting options.
+*/
+static void db_bind_textv(sqlite3_stmt *pStmt, int col, const char * zFmt, ...);
+#endif
+/*
+** Proxy for sqlite3_free(), to be passed any memory which is allocated
+** by sqlite3_malloc().
+*/
+static void db_free(void *m);
+/*
+** Adds the given `#define` macro name to the list of macros, ignoring
+** any duplicates. Fails fatally on error.
+*/
+static void db_define_add(const char * zKey);
+/*
+** Returns true if the given key is already in the `#define` list,
+** else false. Fails fatally on db error.
+*/
+static int db_define_has(const char * zName);
+/*
+** Removes the given `#define` macro name from the list of
+** macros. Fails fatally on error.
+*/
+static void db_define_rm(const char * zKey);
+/*
+** Adds the given filename to the list of being-`#include`d files,
+** using the given source file name and line number of error reporting
+** purposes. If recursion is later detected.
+*/
+static void db_including_add(const char * zKey, const char * zSrc, int srcLine);
+/*
+** Adds the given dir to the list of includes. They are checked in the
+** order they are added.
+*/
+static void db_include_dir_add(const char * zKey);
+/*
+** Returns a resolved path of PREFIX+'/'+zKey, where PREFIX is one of
+** the `#include` dirs (db_include_dir_add()). If no file match is
+** found, NULL is returned. Memory must eventually be passed to
+** db_free() to free it.
+*/
+static char * db_include_search(const char * zKey);
+/*
+** Removes the given key from the `#include` list.
+*/
+static void db_include_rm(const char * zKey);
+/*
+** A proxy for sqlite3_prepare() which fails fatally on error.
+*/
+static void db_prepare(sqlite3_stmt **pStmt, const char * zSql, ...);
+
+/*
+** Opens the given file and processes its contents as c-pp, sending
+** all output to the global c-pp output channel. Fails fatally on
+** error.
+*/
+static void cmpp_process_file(const char * zName);
+
+/*
+** Returns the number newline characters between the given starting
+** point and inclusive ending point. Results are undefined if zFrom is
+** greater than zTo.
+*/
+static unsigned count_lines(unsigned char const * zFrom,
+ unsigned char const *zTo);
+
+/*
+** Wrapper around a FILE handle.
+*/
+struct FileWrapper {
+ /* File's name. */
+ char const *zName;
+ /* FILE handle. */
+ FILE * pFile;
+ /* Where FileWrapper_slurp() stores the file's contents. */
+ unsigned char * zContent;
+ /* Size of this->zContent, as set by FileWrapper_slurp(). */
+ unsigned nContent;
+};
+typedef struct FileWrapper FileWrapper;
+#define FileWrapper_empty_m {0,0,0,0}
+static const FileWrapper FileWrapper_empty = FileWrapper_empty_m;
+
+/* Proxy for FILE_close(). */
+static void FileWrapper_close(FileWrapper * p);
+/* Proxy for FILE_open(). */
+static void FileWrapper_open(FileWrapper * p, const char * zName, const char *zMode);
+/* Proxy for FILE_slurp(). */
+static void FileWrapper_slurp(FileWrapper * p);
+
+/*
+** Outputs a printf()-formatted message to stderr.
+*/
+static void g_stderr(char const *zFmt, ...);
+/*
+** Outputs a printf()-formatted message to stderr.
+*/
+static void g_stderrv(char const *zFmt, va_list);
+#define g_debug(lvl,pfexpr) \
+ if(lvl<=g.doDebug) g_stderr("%s @ %s:%d: ",g.zArgv0,__FILE__,__LINE__); \
+ if(lvl<=g.doDebug) g_stderr pfexpr
+
+void fatalv(char const *zFmt, va_list va){
+ if(zFmt && *zFmt){
+ vfprintf(stderr, zFmt, va);
+ }
+ fputc('\n', stderr);
+ exit(1);
+}
+
+void fatal(char const *zFmt, ...){
+ va_list va;
+ va_start(va, zFmt);
+ fatalv(zFmt, va);
+ va_end(va);
+}
+
+void cmpp_free(void *p){
+ free(p);
+}
+
+void * cmpp_realloc(void * p, unsigned n){
+ void * const rc = realloc(p, n);
+ if(!rc) fatal("realloc(P,%u) failed", n);
+ return rc;
+}
+
+#if 0
+void * cmpp_malloc(unsigned n){
+ void * const rc = malloc(n);
+ if(!rc) fatal("malloc(%u) failed", n);
+ return rc;
+}
+#endif
+
+FILE * FILE_open(char const *zName, const char * zMode){
+ FILE * p;
+ if('-'==zName[0] && 0==zName[1]){
+ p = strstr(zMode,"w") ? stdout : stdin;
+ }else{
+ p = fopen(zName, zMode);
+ if(!p) fatal("Cannot open file [%s] with mode [%s]", zName, zMode);
+ }
+ return p;
+}
+
+void FILE_close(FILE *p){
+ if(p && p!=stdout && p!=stderr){
+ fclose(p);
+ }
+}
+
+void FILE_slurp(FILE *pFile, unsigned char **pOut,
+ unsigned * nOut){
+ unsigned char zBuf[1024 * 8];
+ unsigned char * pDest = 0;
+ unsigned nAlloc = 0;
+ unsigned nOff = 0;
+ /* Note that this needs to be able to work on non-seekable streams,
+ ** thus we read in chunks instead of doing a single alloc and
+ ** filling it in one go. */
+ while( !feof(pFile) ){
+ size_t const n = fread(zBuf, 1, sizeof(zBuf), pFile);
+ if(n>0){
+ if(nAlloc < nOff + n + 1){
+ nAlloc = nOff + n + 1;
+ pDest = cmpp_realloc(pDest, nAlloc);
+ }
+ memcpy(pDest + nOff, zBuf, n);
+ nOff += n;
+ }
+ }
+ if(pDest) pDest[nOff] = 0;
+ *pOut = pDest;
+ *nOut = nOff;
+}
+
+void FileWrapper_close(FileWrapper * p){
+ if(p->pFile) FILE_close(p->pFile);
+ if(p->zContent) cmpp_free(p->zContent);
+ *p = FileWrapper_empty;
+}
+
+void FileWrapper_open(FileWrapper * p, const char * zName,
+ const char * zMode){
+ FileWrapper_close(p);
+ p->pFile = FILE_open(zName, zMode);
+ p->zName = zName;
+}
+
+void FileWrapper_slurp(FileWrapper * p){
+ assert(!p->zContent);
+ assert(p->pFile);
+ FILE_slurp(p->pFile, &p->zContent, &p->nContent);
+}
+
+unsigned count_lines(unsigned char const * zFrom, unsigned char const *zTo){
+ unsigned ln = 0;
+ unsigned char const *zPos = zFrom;
+ assert(zFrom && zTo);
+ assert(zFrom <= zTo);
+ for(; zPos < zTo; ++zPos){
+ switch(*zPos){
+ case (unsigned)'\n': ++ln; break;
+ default: break;
+ }
+ }
+ return ln;
+}
+
+enum CmppParseState {
+TS_Start = 1,
+TS_If,
+TS_IfPassed,
+TS_Else,
+TS_Error
+};
+typedef enum CmppParseState CmppParseState;
+enum CmppTokenType {
+TT_Invalid = 0,
+TT_Comment,
+TT_Define,
+TT_Elif,
+TT_ElifNot,
+TT_Else,
+TT_EndIf,
+TT_Error,
+TT_If,
+TT_IfNot,
+TT_Include,
+TT_Line,
+TT_Pragma,
+TT_Stderr,
+TT_Undef
+};
+typedef enum CmppTokenType CmppTokenType;
+
+struct CmppToken {
+ CmppTokenType ttype;
+ /* Line number of this token in the source file. */
+ unsigned lineNo;
+ /* Start of the token. */
+ unsigned char const * zBegin;
+ /* One-past-the-end byte of the token. */
+ unsigned char const * zEnd;
+};
+typedef struct CmppToken CmppToken;
+#define CmppToken_empty_m {TT_Invalid,0,0,0}
+static const CmppToken CmppToken_empty = CmppToken_empty_m;
+
+/*
+** CmppLevel represents one "level" of tokenization, starting at the
+** top of the main input, incrementing once for each level of `#if`,
+** and decrementing for each `#endif`.
+*/
+typedef struct CmppLevel CmppLevel;
+struct CmppLevel {
+ unsigned short flags;
+ /*
+ ** Used for controlling which parts of an if/elif/...endif chain
+ ** should get output.
+ */
+ unsigned short skipLevel;
+ /* The token which started this level (an 'if' or 'ifnot'). */
+ CmppToken token;
+ CmppParseState pstate;
+};
+#define CmppLevel_empty_m {0U,0U,CmppToken_empty_m,TS_Start}
+static const CmppLevel CmppLevel_empty = CmppLevel_empty_m;
+enum CmppLevel_Flags {
+/* Max depth of nested `#if` constructs in a single tokenizer. */
+CmppLevel_Max = 10,
+/* Max number of keyword arguments. */
+CmppArgs_Max = 10,
+/* Flag indicating that output for a CmpLevel should be elided. */
+CmppLevel_F_ELIDE = 0x01,
+/*
+** Mask of CmppLevel::flags which are inherited when CmppLevel_push()
+** is used.
+*/
+CmppLevel_F_INHERIT_MASK = 0x01
+};
+
+typedef struct CmppTokenizer CmppTokenizer;
+typedef struct CmppKeyword CmppKeyword;
+typedef void (*cmpp_keyword_f)(CmppKeyword const * pKw, CmppTokenizer * t);
+struct CmppKeyword {
+ const char *zName;
+ unsigned nName;
+ int bTokenize;
+ CmppTokenType ttype;
+ cmpp_keyword_f xCall;
+};
+
+static CmppKeyword const * CmppKeyword_search(const char *zName);
+static void cmpp_process_keyword(CmppTokenizer * const t);
+
+/*
+** Tokenizer for c-pp input files.
+*/
+struct CmppTokenizer {
+ const char * zName; /* Input (file) name for error reporting */
+ unsigned const char * zBegin; /* start of input */
+ unsigned const char * zEnd; /* one-after-the-end of input */
+ unsigned const char * zAnchor; /* start of input or end point of
+ previous token */
+ unsigned const char * zPos; /* current position */
+ unsigned int lineNo; /* line # of current pos */
+ CmppParseState pstate;
+ CmppToken token; /* current token result */
+ struct {
+ unsigned ndx;
+ CmppLevel stack[CmppLevel_Max];
+ } level;
+ /* Args for use in cmpp_keyword_f() impls. */
+ struct {
+ CmppKeyword const * pKw;
+ int argc;
+ const unsigned char * argv[CmppArgs_Max];
+ unsigned char lineBuf[1024];
+ } args;
+};
+#define CT_level(t) (t)->level.stack[(t)->level.ndx]
+#define CT_pstate(t) CT_level(t).pstate
+#define CT_skipLevel(t) CT_level(t).skipLevel
+#define CLvl_skip(lvl) ((lvl)->skipLevel || ((lvl)->flags & CmppLevel_F_ELIDE))
+#define CT_skip(t) CLvl_skip(&CT_level(t))
+#define CmppTokenizer_empty_m { \
+ 0,0,0,0,0,1U/*lineNo*/, \
+ TS_Start, \
+ CmppToken_empty_m, \
+ {/*level*/0U,{CmppLevel_empty_m}}, \
+ {/*args*/0,0,{0},{0}} \
+ }
+static const CmppTokenizer CmppTokenizer_empty = CmppTokenizer_empty_m;
+
+static void cmpp_t_out(CmppTokenizer * t, void const *z, unsigned int n);
+/*static void cmpp_t_outf(CmppTokenizer * t, char const *zFmt, ...);*/
+
+/*
+** Pushes a new level into the given tokenizer. Fails fatally if
+** it's too deep.
+*/
+static void CmppLevel_push(CmppTokenizer * const t);
+/*
+** Pops a level from the tokenizer. Fails fatally if the top
+** level is popped.
+*/
+static void CmppLevel_pop(CmppTokenizer * const t);
+/*
+** Returns the current level object.
+*/
+static CmppLevel * CmppLevel_get(CmppTokenizer * const t);
+
+/*
+** Global app state singleton. */
+static struct Global {
+ /* main()'s argv[0]. */
+ const char * zArgv0;
+ /*
+ ** Bytes of the keyword delimiter/prefix. Owned
+ ** elsewhere.
+ */
+ const char * zDelim;
+ /* Byte length of this->zDelim. */
+ unsigned short nDelim;
+ /* If true, enables certain debugging output. */
+ int doDebug;
+ /* App's db instance. */
+ sqlite3 * db;
+ /* Output channel. */
+ FileWrapper out;
+ struct {
+ sqlite3_stmt * defIns;
+ sqlite3_stmt * defDel;
+ sqlite3_stmt * defHas;
+ sqlite3_stmt * inclIns;
+ sqlite3_stmt * inclDel;
+ sqlite3_stmt * inclHas;
+ sqlite3_stmt * inclPathAdd;
+ sqlite3_stmt * inclSearch;
+ } stmt;
+} g = {
+"?",
+CMPP_DEFAULT_DELIM/*zDelim*/,
+(unsigned short) sizeof(CMPP_DEFAULT_DELIM)-1/*nDelim*/,
+0/*doDebug*/,
+0/*db*/,
+FileWrapper_empty_m/*out*/,
+{/*stmt*/
+ 0/*defIns*/, 0/*defDel*/, 0/*defHas*/,
+ 0/*inclIns*/, 0/*inclDel*/, 0/*inclHas*/,
+ 0/*inclPathAdd*/
+}
+};
+
+
+#if 0
+/*
+** Outputs a printf()-formatted message to c-pp's global output
+** channel.
+*/
+static void g_outf(char const *zFmt, ...);
+void g_outf(char const *zFmt, ...){
+ va_list va;
+ va_start(va, zFmt);
+ vfprintf(g.out.pFile, zFmt, va);
+ va_end(va);
+}
+#endif
+
+#if 0
+/* Outputs n bytes from z to c-pp's global output channel. */
+static void g_out(void const *z, unsigned int n);
+void g_out(void const *z, unsigned int n){
+ if(1!=fwrite(z, n, 1, g.out.pFile)){
+ int const err = errno;
+ fatal("fwrite() output failed with errno #%d", err);
+ }
+}
+#endif
+
+void g_stderrv(char const *zFmt, va_list va){
+ vfprintf(stderr, zFmt, va);
+}
+
+void g_stderr(char const *zFmt, ...){
+ va_list va;
+ va_start(va, zFmt);
+ g_stderrv(zFmt, va);
+ va_end(va);
+}
+
+void cmpp_t_out(CmppTokenizer * t, void const *z, unsigned int n){
+ g_debug(3,("CT_skipLevel() ?= %d\n",CT_skipLevel(t)));
+ g_debug(3,("CT_skip() ?= %d\n",CT_skip(t)));
+ if(!CT_skip(t)){
+ if(1!=fwrite(z, n, 1, g.out.pFile)){
+ int const err = errno;
+ fatal("fwrite() output failed with errno #%d", err);
+ }
+ }
+}
+
+void CmppLevel_push(CmppTokenizer * const t){
+ CmppLevel * pPrev;
+ CmppLevel * p;
+ if(t->level.ndx+1 == (unsigned)CmppLevel_Max){
+ fatal("%sif nesting level is too deep. Max=%d\n",
+ g.zDelim, CmppLevel_Max);
+ }
+ pPrev = &CT_level(t);
+ g_debug(3,("push from tokenizer level=%u flags=%04x\n", t->level.ndx, pPrev->flags));
+ p = &t->level.stack[++t->level.ndx];
+ *p = CmppLevel_empty;
+ p->token = t->token;
+ p->flags = (CmppLevel_F_INHERIT_MASK & pPrev->flags);
+ if(CLvl_skip(pPrev)) p->flags |= CmppLevel_F_ELIDE;
+ g_debug(3,("push to tokenizer level=%u flags=%04x\n", t->level.ndx, p->flags));
+}
+
+void CmppLevel_pop(CmppTokenizer * const t){
+ if(!t->level.ndx){
+ fatal("Internal error: CmppLevel_pop() at the top of the stack");
+ }
+ g_debug(3,("pop from tokenizer level=%u, flags=%04x skipLevel?=%d\n", t->level.ndx,
+ t->level.stack[t->level.ndx].flags, CT_skipLevel(t)));
+ g_debug(3,("CT_skipLevel() ?= %d\n",CT_skipLevel(t)));
+ g_debug(3,("CT_skip() ?= %d\n",CT_skip(t)));
+ t->level.stack[t->level.ndx--] = CmppLevel_empty;
+ g_debug(3,("pop to tokenizer level=%u, flags=%04x\n", t->level.ndx,
+ t->level.stack[t->level.ndx].flags));
+ g_debug(3,("CT_skipLevel() ?= %d\n",CT_skipLevel(t)));
+ g_debug(3,("CT_skip() ?= %d\n",CT_skip(t)));
+}
+
+CmppLevel * CmppLevel_get(CmppTokenizer * const t){
+ return &t->level.stack[t->level.ndx];
+}
+
+
+void db_affirm_rc(int rc, const char * zMsg){
+ if(rc){
+ fatal("Db error #%d %s: %s", rc, zMsg, sqlite3_errmsg(g.db));
+ }
+}
+
+void db_finalize(sqlite3_stmt *pStmt){
+ sqlite3_finalize(pStmt);
+}
+
+int db_step(sqlite3_stmt *pStmt){
+ int const rc = sqlite3_step(pStmt);
+ if(SQLITE_ROW!=rc && SQLITE_DONE!=rc){
+ db_affirm_rc(rc, "from db_step()");
+ }
+ return rc;
+}
+
+static sqlite3_str * db_str_new(void){
+ sqlite3_str * rc = sqlite3_str_new(g.db);
+ if(!rc) fatal("Alloc failed for sqlite3_str_new()");
+ return rc;
+}
+
+static char * db_str_finish(sqlite3_str *s, int * n){
+ int const rc = sqlite3_str_errcode(s);
+ if(rc) fatal("Error #%d from sqlite3_str_errcode()", rc);
+ if(n) *n = sqlite3_str_length(s);
+ char * z = sqlite3_str_finish(s);
+ if(!z) fatal("Alloc failed for sqlite3_str_new()");
+ return z;
+}
+
+void db_prepare(sqlite3_stmt **pStmt, const char * zSql, ...){
+ int rc;
+ sqlite3_str * str = db_str_new();
+ char * z = 0;
+ int n = 0;
+ va_list va;
+ if(!str) fatal("sqlite3_str_new() failed");
+ va_start(va, zSql);
+ sqlite3_str_vappendf(str, zSql, va);
+ va_end(va);
+ rc = sqlite3_str_errcode(str);
+ if(rc) fatal("sqlite3_str_errcode() = %d", rc);
+ z = db_str_finish(str, &n);
+ rc = sqlite3_prepare_v2(g.db, z, n, pStmt, 0);
+ if(rc) fatal("Error #%d (%s) preparing: %s",
+ rc, sqlite3_errmsg(g.db), z);
+ sqlite3_free(z);
+}
+
+void db_bind_int(sqlite3_stmt *pStmt, int col, int val){
+ int const rc = sqlite3_bind_int(pStmt, col, val);
+ db_affirm_rc(rc,"from db_bind_int()");
+}
+
+#if 0
+void db_bind_null(sqlite3_stmt *pStmt, int col){
+ int const rc = sqlite3_bind_null(pStmt, col);
+ db_affirm_rc(rc,"from db_bind_null()");
+}
+#endif
+
+void db_bind_textn(sqlite3_stmt *pStmt, int col,
+ const char * zStr, int n){
+ int const rc = zStr
+ ? sqlite3_bind_text(pStmt, col, zStr, n, SQLITE_TRANSIENT)
+ : sqlite3_bind_null(pStmt, col);
+ db_affirm_rc(rc,"from db_bind_textn()");
+}
+
+void db_bind_text(sqlite3_stmt *pStmt, int col,
+ const char * zStr){
+ db_bind_textn(pStmt, col, zStr, -1);
+}
+
+#if 0
+void db_bind_textv(sqlite3_stmt *pStmt, int col,
+ const char * zFmt, ...){
+ int rc;
+ sqlite3_str * str = db_str_new();
+ int n = 0;
+ char * z;
+ va_list va;
+ va_start(va,zFmt);
+ sqlite3_str_vappendf(str, zFmt, va);
+ va_end(va);
+ z = db_str_finish(str, &n);
+ rc = sqlite3_bind_text(pStmt, col, z, n, sqlite3_free);
+ db_affirm_rc(rc,"from db_bind_textv()");
+}
+#endif
+
+void db_free(void *m){
+ sqlite3_free(m);
+}
+
+void db_define_add(const char * zKey){
+ int rc;
+ if(!g.stmt.defIns){
+ db_prepare(&g.stmt.defIns,
+ "INSERT OR REPLACE INTO def(k) VALUES(?)");
+ }
+ db_bind_text(g.stmt.defIns, 1, zKey);
+ rc = db_step(g.stmt.defIns);
+ if(SQLITE_DONE != rc){
+ db_affirm_rc(rc, "Stepping INSERT on def");
+ }
+ g_debug(2,("define: %s\n",zKey));
+ sqlite3_reset(g.stmt.defIns);
+}
+
+int db_define_has(const char * zName){
+ int rc;
+ if(!g.stmt.defHas){
+ db_prepare(&g.stmt.defHas, "SELECT 1 FROM def WHERE k=?");
+ }
+ db_bind_text(g.stmt.defHas, 1, zName);
+ rc = db_step(g.stmt.defHas);
+ if(SQLITE_ROW == rc){
+ rc = 1;
+ }else{
+ assert(SQLITE_DONE==rc);
+ rc = 0;
+ }
+ g_debug(1,("defined [%s] ?= %d\n",zName, rc));
+ sqlite3_clear_bindings(g.stmt.defHas);
+ sqlite3_reset(g.stmt.defHas);
+ return rc;
+}
+
+
+void db_define_rm(const char * zKey){
+ int rc;
+ int n = 0;
+ const char *zPos = zKey;
+ if(!g.stmt.defDel){
+ db_prepare(&g.stmt.defDel, "DELETE FROM def WHERE k=?");
+ }
+ for( ; *zPos && '='!=*zPos; ++n, ++zPos) {}
+ db_bind_text(g.stmt.defDel, 1, zKey);
+ rc = db_step(g.stmt.defDel);
+ if(SQLITE_DONE != rc){
+ db_affirm_rc(rc, "Stepping DELETE on def");
+ }
+ g_debug(2,("undefine: %.*s\n",n, zKey));
+ sqlite3_clear_bindings(g.stmt.defDel);
+ sqlite3_reset(g.stmt.defDel);
+}
+
+void db_including_add(const char * zKey, const char * zSrc, int srcLine){
+ int rc;
+ if(!g.stmt.inclIns){
+ db_prepare(&g.stmt.inclIns,
+ "INSERT OR FAIL INTO incl(file,srcFile,srcLine) VALUES(?,?,?)");
+ }
+ db_bind_text(g.stmt.inclIns, 1, zKey);
+ db_bind_text(g.stmt.inclIns, 2, zSrc);
+ db_bind_int(g.stmt.inclIns, 3, srcLine);
+ rc = db_step(g.stmt.inclIns);
+ if(SQLITE_DONE != rc){
+ db_affirm_rc(rc, "Stepping INSERT on incl");
+ }
+ g_debug(2,("inclpath add [%s] from [%s]:%d\n", zKey, zSrc, srcLine));
+ sqlite3_clear_bindings(g.stmt.inclIns);
+ sqlite3_reset(g.stmt.inclIns);
+}
+
+void db_include_rm(const char * zKey){
+ int rc;
+ if(!g.stmt.inclDel){
+ db_prepare(&g.stmt.inclDel, "DELETE FROM incl WHERE file=?");
+ }
+ db_bind_text(g.stmt.inclDel, 1, zKey);
+ rc = db_step(g.stmt.inclDel);
+ if(SQLITE_DONE != rc){
+ db_affirm_rc(rc, "Stepping DELETE on incl");
+ }
+ g_debug(2,("inclpath rm [%s]\n", zKey));
+ sqlite3_clear_bindings(g.stmt.inclDel);
+ sqlite3_reset(g.stmt.inclDel);
+}
+
+char * db_include_search(const char * zKey){
+ char * zName = 0;
+ if(!g.stmt.inclSearch){
+ db_prepare(&g.stmt.inclSearch,
+ "SELECT ?1 fn WHERE fileExists(fn) "
+ "UNION ALL SELECT * FROM ("
+ "SELECT replace(dir||'/'||?1, '//','/') AS fn "
+ "FROM inclpath WHERE fileExists(fn) ORDER BY seq"
+ ")");
+ }
+ db_bind_text(g.stmt.inclSearch, 1, zKey);
+ if(SQLITE_ROW==db_step(g.stmt.inclSearch)){
+ const unsigned char * z = sqlite3_column_text(g.stmt.inclSearch, 0);
+ zName = z ? sqlite3_mprintf("%s", z) : 0;
+ if(!zName) fatal("Alloc failed");
+ }
+ sqlite3_clear_bindings(g.stmt.inclSearch);
+ sqlite3_reset(g.stmt.inclSearch);
+ return zName;
+}
+
+static int db_including_has(const char * zName){
+ int rc;
+ if(!g.stmt.inclHas){
+ db_prepare(&g.stmt.inclHas, "SELECT 1 FROM incl WHERE file=?");
+ }
+ db_bind_text(g.stmt.inclHas, 1, zName);
+ rc = db_step(g.stmt.inclHas);
+ if(SQLITE_ROW == rc){
+ rc = 1;
+ }else{
+ assert(SQLITE_DONE==rc);
+ rc = 0;
+ }
+ g_debug(2,("inclpath has [%s] = %d\n",zName, rc));
+ sqlite3_clear_bindings(g.stmt.inclHas);
+ sqlite3_reset(g.stmt.inclHas);
+ return rc;
+}
+
+#if 0
+/*
+** Fails fatally if the `#include` list contains the given key.
+*/
+static void db_including_check(const char * zKey);
+void db_including_check(const char * zName){
+ if(db_including_has(zName)){
+ fatal("Recursive include detected: %s\n", zName);
+ }
+}
+#endif
+
+void db_include_dir_add(const char * zDir){
+ static int seq = 0;
+ int rc;
+ if(!g.stmt.inclPathAdd){
+ db_prepare(&g.stmt.inclPathAdd,
+ "INSERT OR FAIL INTO inclpath(seq,dir) VALUES(?,?)");
+ }
+ db_bind_int(g.stmt.inclPathAdd, 1, ++seq);
+ db_bind_text(g.stmt.inclPathAdd, 2, zDir);
+ rc = db_step(g.stmt.inclPathAdd);
+ if(SQLITE_DONE != rc){
+ db_affirm_rc(rc, "Stepping INSERT on inclpath");
+ }
+ g_debug(2,("inclpath add #%d: %s\n",seq, zDir));
+ sqlite3_clear_bindings(g.stmt.inclPathAdd);
+ sqlite3_reset(g.stmt.inclPathAdd);
+}
+
+static void cmpp_atexit(void){
+#define FINI(M) if(g.stmt.M) sqlite3_finalize(g.stmt.M)
+ FINI(defIns); FINI(defDel); FINI(defHas);
+ FINI(inclIns); FINI(inclDel); FINI(inclHas);
+ FINI(inclPathAdd); FINI(inclSearch);
+#undef FINI
+ FileWrapper_close(&g.out);
+ if(g.db) sqlite3_close(g.db);
+}
+
+/*
+** sqlite3 UDF which returns true if its argument refers to an
+** accessible file, else false.
+*/
+static void udf_file_exists(
+ sqlite3_context *context,
+ int argc,
+ sqlite3_value **argv
+){
+ const char *zName;
+ (void)(argc); /* Unused parameter */
+ zName = (const char*)sqlite3_value_text(argv[0]);
+ if( zName==0 ) return;
+ sqlite3_result_int(context, 0==access(zName, 0));
+}
+
+/* Initialize g.db, failing fatally on error. */
+static void cmpp_initdb(void){
+ int rc;
+ char * zErr = 0;
+ const char * zSchema =
+ "CREATE TABLE def("
+ "k TEXT PRIMARY KEY NOT NULL"
+ /*"v INTEGER DEFAULT 1"*/
+ ") WITHOUT ROWID;"
+ /* ^^^ defines */
+ "CREATE TABLE incl("
+ "file TEXT PRIMARY KEY NOT NULL,"
+ "srcFile TEXT DEFAULT NULL,"
+ "srcLine INTEGER DEFAULT 0"
+ ") WITHOUT ROWID;"
+ /* ^^^ files currently being included */
+ "CREATE TABLE inclpath("
+ "seq INTEGER UNIQUE, "
+ "dir TEXT PRIMARY KEY NOT NULL ON CONFLICT IGNORE"
+ ")"
+ /* ^^^ include path */
+ ;
+ assert(0==g.db);
+ if(g.db) return;
+ rc = sqlite3_open_v2(":memory:", &g.db, SQLITE_OPEN_READWRITE, 0);
+ if(rc) fatal("Error opening :memory: db.");
+ rc = sqlite3_exec(g.db, zSchema, 0, 0, &zErr);
+ if(rc) fatal("Error initializing database: %s", zErr);
+ rc = sqlite3_create_function(g.db, "fileExists", 1,
+ SQLITE_UTF8|SQLITE_DIRECTONLY, 0,
+ udf_file_exists, 0, 0);
+ db_affirm_rc(rc, "UDF registration failed.");
+}
+
+/*
+** For position zPos, which must be in the half-open range
+** [zBegin,zEnd), returns g.nDelim if it is at the start of a line and
+** starts with g.zDelim, else returns 0.
+*/
+static unsigned short cmpp_is_delim(unsigned char const *zBegin,
+ unsigned char const *zEnd,
+ unsigned char const *zPos){
+ assert(zEnd>zBegin);
+ assert(zPos<zEnd);
+ assert(zPos>=zBegin);
+ if(zPos>zBegin &&
+ ('\n'!=*(zPos - 1)
+ || ((unsigned)(zEnd - zPos) <= g.nDelim))){
+ return 0;
+ }else if(0==memcmp(zPos, g.zDelim, g.nDelim)){
+ return g.nDelim;
+ }else{
+ return 0;
+ }
+}
+
+/*
+** Scans t to the next keyword line, emitting all input before that
+** which is _not_ a keyword line unless it's elided due to being
+** inside a block which elides its content. Returns 0 if no keyword
+** line was found, in which case the end of the input has been
+** reached, else returns a truthy value and sets up t's state for use
+** with cmpp_process_keyword(), which should then be called.
+*/
+static int cmpp_next_keyword_line(CmppTokenizer * const t){
+ unsigned char const * zStart;
+ unsigned char const * z;
+ CmppToken * const tok = &t->token;
+ unsigned short isDelim = 0;
+
+ assert(t->zBegin);
+ assert(t->zEnd > t->zBegin);
+ if(!t->zPos) t->zPos = t->zBegin;
+ t->zAnchor = t->zPos;
+ zStart = z = t->zPos;
+ *tok = CmppToken_empty;
+ while(z<t->zEnd
+ && 0==(isDelim = cmpp_is_delim(t->zBegin, t->zEnd, z))){
+ ++z;
+ }
+ if(z>zStart){
+ /* We passed up content */
+ cmpp_t_out(t, zStart, (unsigned)(z - zStart));
+ }
+ assert(isDelim==0 || isDelim==g.nDelim);
+ tok->lineNo = t->lineNo += count_lines(zStart, z);
+ if(isDelim){
+ /* Handle backslash-escaped newlines */
+ int isEsc = 0, atEol = 0;
+ tok->zBegin = z+isDelim;
+ for( ++z ; z<t->zEnd && 0==atEol; ++z ){
+ switch((int)*z){
+ case (int)'\\':
+ isEsc = 0==isEsc; break;
+ case (int)'\n':
+ atEol = 0==isEsc;
+ isEsc = 0;
+ ++t->lineNo;
+ break;
+ default:
+ break;
+ }
+ }
+ tok->zEnd = atEol ? z-1 : z;
+ /* Strip leading spaces */
+ while(tok->zBegin < tok->zEnd && isspace((char)(*tok->zBegin))){
+ ++tok->zBegin;
+ }
+ tok->ttype = TT_Line;
+ g_debug(2,("Keyword @ line %u: [[[%.*s]]]\n",
+ tok->lineNo,
+ (int)(tok->zEnd-tok->zBegin), tok->zBegin));
+ }
+ t->zPos = z;
+ if(isDelim){
+ /* Split t->token into arguments for the line's keyword */
+ int i, argc = 0, prevChar = 0;
+ const unsigned tokLen = (unsigned)(tok->zEnd - tok->zBegin);
+ unsigned char * zKwd;
+ unsigned char * zEsc;
+ unsigned char * zz;
+
+ assert(TT_Line==tok->ttype);
+ if((unsigned)sizeof(t->args.lineBuf) < tokLen + 1){
+ fatal("Keyword line is unreasonably long: %.*s",
+ tokLen, tok->zBegin);
+ }else if(!tokLen){
+ fatal("Line #%u has no keyword after delimiter", tok->lineNo);
+ }
+ g_debug(2,("token @ line %u len=%u [[[%.*s]]]\n",
+ tok->lineNo, tokLen, tokLen, tok->zBegin));
+ zKwd = &t->args.lineBuf[0];
+ memcpy(zKwd, tok->zBegin, tokLen);
+ memset(zKwd + tokLen, 0, sizeof(t->args.lineBuf) - tokLen);
+ for( zEsc = 0, zz = zKwd; *zz; ++zz ){
+ /* Convert backslash-escaped newlines to whitespace */
+ switch((int)*zz){
+ case (int)'\\':
+ if(zEsc) zEsc = 0;
+ else zEsc = zz;
+ break;
+ case (int)'\n':
+ assert(zEsc && "Should not have an unescaped newline?");
+ if(zEsc==zz-1){
+ *zEsc = (unsigned char)' ';
+ /* FIXME?: memmove() lnBuf content one byte to the left here
+ ** to collapse backslash and newline into a single
+ ** byte. Also consider collapsing all leading space on the
+ ** next line. */
+ }
+ zEsc = 0;
+ *zz = (unsigned char)' ';
+ break;
+ default:
+ zEsc = 0;
+ break;
+ }
+ }
+ t->args.argv[argc++] = zKwd;
+ for( zz = zKwd; *zz; ++zz ){
+ if(isspace(*zz)){
+ *zz = 0;
+ break;
+ }
+ }
+ t->args.pKw = CmppKeyword_search((char const *)zKwd);
+ if(!t->args.pKw){
+ fatal("Unknown keyword '%s' at line %u\n", (char const *)zKwd,
+ tok->lineNo);
+ }
+ for( ++zz ; *zz && isspace(*zz); ++zz ){}
+ if(t->args.pKw->bTokenize){
+ for( ; *zz; prevChar = *zz, ++zz ){
+ /* Split string into word-shaped tokens.
+ ** TODO ?= quoted strings, for the sake of the
+ ** #error keyword. */
+ if(isspace(*zz)){
+ assert(zz!=zKwd && "Leading space was stripped earlier.");
+ *zz = 0;
+ }else{
+ if(argc == (int)CmppArgs_Max){
+ fatal("Too many arguments @ line %u: %.*s",
+ tok->lineNo, tokLen, tok->zBegin);
+ }else if(zz>zKwd && !prevChar){
+ t->args.argv[argc++] = zz;
+ }
+ }
+ }
+ }else{
+ /* Treat rest of line as one token */
+ if(*zz) t->args.argv[argc++] = zz;
+ }
+ tok->ttype = t->args.pKw->ttype;
+ if(g.doDebug>1){
+ for(i = 0; i < argc; ++i){
+ g_debug(0,("line %u arg #%d=%s\n",
+ tok->lineNo, i,
+ (char const *)t->args.argv[i]));
+ }
+ }
+ t->args.argc = argc;
+ }else{
+ t->args.pKw = 0;
+ t->args.argc = 0;
+ }
+ return isDelim;
+}
+
+static void cmpp_kwd__err_prefix(CmppKeyword const * pKw, CmppTokenizer *t,
+ char const *zPrefix){
+ g_stderr("%s%s%s @ %s line %u: ",
+ zPrefix ? zPrefix : "",
+ zPrefix ? ": " : "",
+ pKw->zName, t->zName, t->token.lineNo);
+}
+
+/* Internal error reporting helper for cmpp_keyword_f() impls. */
+static CMPP_NORETURN void cmpp_kwd__misuse(CmppKeyword const * pKw,
+ CmppTokenizer *t,
+ char const *zFmt, ...){
+ va_list va;
+ cmpp_kwd__err_prefix(pKw, t, "Fatal error");
+ va_start(va, zFmt);
+ fatalv(zFmt, va);
+ va_end(va);
+}
+
+/* No-op cmpp_keyword_f() impl. */
+static void cmpp_kwd_noop(CmppKeyword const * pKw, CmppTokenizer *t){
+ if(t || pKw){/*unused*/}
+}
+
+/* #error impl. */
+static void cmpp_kwd_error(CmppKeyword const * pKw, CmppTokenizer *t){
+ if(CT_skip(t)) return;
+ else{
+ assert(t->args.argc < 3);
+ const char *zBegin = t->args.argc>1
+ ? (const char *)t->args.argv[1] : 0;
+ cmpp_kwd__err_prefix(pKw, t, NULL);
+ fatal("%s", zBegin ? zBegin : "(no additional info)");
+ }
+}
+
+/* Impl. for #define, #undef */
+static void cmpp_kwd_define(CmppKeyword const * pKw, CmppTokenizer *t){
+ if(CT_skip(t)) return;
+ if(t->args.argc<2){
+ cmpp_kwd__misuse(pKw, t, "Expecting one or more arguments");
+ }else{
+ int i = 1;
+ void (*func)(const char *) = TT_Define==pKw->ttype
+ ? db_define_add : db_define_rm;
+ for( ; i < t->args.argc; ++i){
+ func( (char const *)t->args.argv[i] );
+ }
+ }
+}
+
+/* Impl. for #if, #ifnot, #elif, #elifnot. */
+static void cmpp_kwd_if(CmppKeyword const * pKw, CmppTokenizer *t){
+ int buul;
+ CmppParseState tmpState = TS_Start;
+ if(t->args.argc!=2){
+ cmpp_kwd__misuse(pKw, t, "Expecting exactly 1 argument");
+ }
+ /*g_debug(0,("%s %s level %u pstate=%d\n", pKw->zName,
+ (char const *)t->args.argv[1],
+ t->level.ndx, (int)CT_pstate(t)));*/
+ switch(pKw->ttype){
+ case TT_Elif:
+ case TT_ElifNot:
+ switch(CT_pstate(t)){
+ case TS_If: break;
+ case TS_IfPassed: CT_level(t).flags |= CmppLevel_F_ELIDE; return;
+ default: goto misuse;
+ }
+ break;
+ case TT_If:
+ case TT_IfNot:
+ CmppLevel_push(t);
+ break;
+ default:
+ cmpp_kwd__misuse(pKw, t, "Unpexected keyword token type");
+ break;
+ }
+ buul = db_define_has((char const *)t->args.argv[1]);
+ if(TT_IfNot==pKw->ttype || TT_ElifNot==pKw->ttype) buul = !buul;
+ if(buul){
+ CT_pstate(t) = tmpState = TS_IfPassed;
+ CT_skipLevel(t) = 0;
+ }else{
+ CT_pstate(t) = TS_If /* also for TT_IfNot, TT_Elif, TT_ElifNot */;
+ CT_skipLevel(t) = 1;
+ g_debug(3,("setting CT_skipLevel = 1 @ level %d\n", t->level.ndx));
+ }
+ if(TT_If==pKw->ttype || TT_IfNot==pKw->ttype){
+ unsigned const lvlIf = t->level.ndx;
+ CmppToken const lvlToken = CT_level(t).token;
+ while(cmpp_next_keyword_line(t)){
+ cmpp_process_keyword(t);
+ if(lvlIf > t->level.ndx){
+ assert(TT_EndIf == t->token.ttype);
+ break;
+ }
+#if 0
+ if(TS_IfPassed==tmpState){
+ tmpState = TS_Start;
+ t->level.stack[lvlIf].flags |= CmppLevel_F_ELIDE;
+ g_debug(1,("Setting ELIDE for TS_IfPassed @ lv %d (lvlIf=%d)\n", t->level.ndx, lvlIf));
+ }
+#endif
+ }
+ if(lvlIf <= t->level.ndx){
+ cmpp_kwd__err_prefix(pKw, t, NULL);
+ fatal("Input ended inside an unterminated %sif "
+ "opened at [%s] line %u",
+ g.zDelim, t->zName, lvlToken.lineNo);
+ }
+ }
+ return;
+ misuse:
+ cmpp_kwd__misuse(pKw, t, "'%s' used out of context",
+ pKw->zName);
+}
+
+/* Impl. for #else. */
+static void cmpp_kwd_else(CmppKeyword const * pKw, CmppTokenizer *t){
+ if(t->args.argc>1){
+ cmpp_kwd__misuse(pKw, t, "Expecting no arguments");
+ }
+ switch(CT_pstate(t)){
+ case TS_IfPassed: CT_skipLevel(t) = 1; break;
+ case TS_If: CT_skipLevel(t) = 0; break;
+ default:
+ cmpp_kwd__misuse(pKw, t, "'%s' with no matching 'if'",
+ pKw->zName);
+ }
+ /*g_debug(0,("else flags=0x%02x skipLevel=%u\n",
+ CT_level(t).flags, CT_level(t).skipLevel));*/
+ CT_pstate(t) = TS_Else;
+}
+
+/* Impl. for #endif. */
+static void cmpp_kwd_endif(CmppKeyword const * pKw, CmppTokenizer *t){
+ /* Maintenance reminder: we ignore all arguments after the endif
+ ** to allow for constructs like:
+ **
+ ** #endif // foo
+ **
+ ** in a manner which does not require a specific comment style */
+ switch(CT_pstate(t)){
+ case TS_Else:
+ case TS_If:
+ case TS_IfPassed:
+ break;
+ default:
+ cmpp_kwd__misuse(pKw, t, "'%s' with no matching 'if'",
+ pKw->zName);
+ }
+ CmppLevel_pop(t);
+}
+
+/* Impl. for #include. */
+static void cmpp_kwd_include(CmppKeyword const * pKw, CmppTokenizer *t){
+ char const * zFile;
+ char * zResolved;
+ if(CT_skip(t)) return;
+ else if(t->args.argc!=2){
+ cmpp_kwd__misuse(pKw, t, "Expecting exactly 1 filename argument");
+ }
+ zFile = (const char *)t->args.argv[1];
+ if(db_including_has(zFile)){
+ /* Note that different spellings of the same filename
+ ** will elude this check, but that seems okay, as different
+ ** spellings means that we're not re-running the exact same
+ ** invocation. We might want some other form of multi-include
+ ** protection, rather than this, however. There may well be
+ ** sensible uses for recursion. */
+ cmpp_kwd__err_prefix(pKw, t, NULL);
+ fatal("Recursive include of file: %s", zFile);
+ }
+ zResolved = db_include_search(zFile);
+ if(zResolved){
+ db_including_add(zFile, t->zName, t->token.lineNo);
+ cmpp_process_file(zResolved);
+ db_include_rm(zFile);
+ db_free(zResolved);
+ }else{
+ cmpp_kwd__err_prefix(pKw, t, NULL);
+ fatal("file not found: %s", zFile);
+ }
+}
+
+/* Impl. for #pragma. */
+static void cmpp_kwd_pragma(CmppKeyword const * pKw, CmppTokenizer *t){
+ const char * zArg;
+ if(CT_skip(t)) return;
+ else if(t->args.argc!=2){
+ cmpp_kwd__misuse(pKw, t, "Expecting one argument");
+ }
+ zArg = (const char *)t->args.argv[1];
+#define M(X) 0==strcmp(zArg,X)
+ if(M("defines")){
+ sqlite3_stmt * q = 0;
+ db_prepare(&q, "SELECT k FROM def ORDER BY k");
+ g_stderr("cmpp defines:\n");
+ while(SQLITE_ROW==db_step(q)){
+ int const n = sqlite3_column_bytes(q, 0);
+ const char * z = (const char *)sqlite3_column_text(q, 0);
+ g_stderr("\t%.*s\n", n, z);
+ }
+ db_finalize(q);
+ }else{
+ cmpp_kwd__misuse(pKw, t, "Unknown pragma");
+ }
+#undef M
+}
+
+/* #stder impl. */
+static void cmpp_kwd_stderr(CmppKeyword const * pKw, CmppTokenizer *t){
+ if(CT_skip(t)) return;
+ else{
+ const char *zBegin = t->args.argc>1
+ ? (const char *)t->args.argv[1] : 0;
+ if(zBegin){
+ g_stderr("%s:%u: %s\n", t->zName, t->token.lineNo, zBegin);
+ }else{
+ g_stderr("%s:%u: (no %.*s%s argument)\n",
+ t->zName, t->token.lineNo,
+ g.nDelim, g.zDelim, pKw->zName);
+ }
+ }
+}
+
+#if 0
+/* Impl. for dummy placeholder. */
+static void cmpp_kwd_todo(CmppKeyword const * pKw, CmppTokenizer *t){
+ if(t){/*unused*/}
+ g_debug(0,("TODO: keyword handler for %s\n", pKw->zName));
+}
+#endif
+
+CmppKeyword aKeywords[] = {
+/* Keep these sorted by zName */
+ {"//", 2, 0, TT_Comment, cmpp_kwd_noop},
+ {"define", 6, 1, TT_Define, cmpp_kwd_define},
+ {"elif", 4, 1, TT_Elif, cmpp_kwd_if},
+ {"elifnot", 7, 1, TT_ElifNot, cmpp_kwd_if},
+ {"else", 4, 1, TT_Else, cmpp_kwd_else},
+ {"endif", 5, 0, TT_EndIf, cmpp_kwd_endif},
+ {"error", 4, 0, TT_Error, cmpp_kwd_error},
+ {"if", 2, 1, TT_If, cmpp_kwd_if},
+ {"ifnot", 5, 1, TT_IfNot, cmpp_kwd_if},
+ {"include", 7, 0, TT_Include, cmpp_kwd_include},
+ {"pragma", 6, 1, TT_Pragma, cmpp_kwd_pragma},
+ {"stderr", 6, 0, TT_Stderr, cmpp_kwd_stderr},
+ {"undef", 5, 1, TT_Undef, cmpp_kwd_define},
+ {0,0,TT_Invalid, 0}
+};
+
+static int cmp_CmppKeyword(const void *p1, const void *p2){
+ char const * zName = (const char *)p1;
+ CmppKeyword const * kw = (CmppKeyword const *)p2;
+ return strcmp(zName, kw->zName);
+}
+
+CmppKeyword const * CmppKeyword_search(const char *zName){
+ return (CmppKeyword const *)bsearch(zName, &aKeywords[0],
+ sizeof(aKeywords)/sizeof(aKeywords[0]) - 1,
+ sizeof(aKeywords[0]),
+ cmp_CmppKeyword);
+}
+
+void cmpp_process_keyword(CmppTokenizer * const t){
+ assert(t->args.pKw);
+ assert(t->args.argc);
+ t->args.pKw->xCall(t->args.pKw, t);
+ t->args.pKw = 0;
+ t->args.argc = 0;
+}
+
+void cmpp_process_file(const char * zName){
+ FileWrapper fw = FileWrapper_empty;
+ CmppTokenizer ct = CmppTokenizer_empty;
+
+ FileWrapper_open(&fw, zName, "r");
+ FileWrapper_slurp(&fw);
+ g_debug(1,("Read %u byte(s) from [%s]\n", fw.nContent, fw.zName));
+ ct.zName = zName;
+ ct.zBegin = fw.zContent;
+ ct.zEnd = fw.zContent + fw.nContent;
+ while(cmpp_next_keyword_line(&ct)){
+ cmpp_process_keyword(&ct);
+ }
+ FileWrapper_close(&fw);
+ if(0!=ct.level.ndx){
+ CmppLevel * const lv = CmppLevel_get(&ct);
+ fatal("Input ended inside an unterminated nested construct"
+ "opened at [%s] line %u", zName, lv->token.lineNo);
+ }
+}
+
+static void usage(int isErr){
+ FILE * const fOut = isErr ? stderr : stdout;
+ fprintf(fOut,
+ "Usage: %s [flags] [infile]\n"
+ "Flags:\n",
+ g.zArgv0);
+#define arg(F,D) fprintf(fOut," %s\n %s\n",F, D)
+ arg("-f|--file FILE","Read input from FILE (default=- (stdin)).\n"
+ " Alternately, the first non-flag argument is assumed to "
+ "be the input file.");
+ arg("-o|--outfile FILE","Send output to FILE (default=- (stdout))");
+ arg("-DXYZ","Define XYZ to true");
+ arg("-UXYZ","Undefine XYZ (equivalent to false)");
+ arg("-IXYZ","Add dir XYZ to include path");
+ arg("-d|--delimiter VALUE", "Set keyword delimiter to VALUE "
+ "(default=" CMPP_DEFAULT_DELIM ")");
+#undef arg
+ fputs("",fOut);
+}
+
+int main(int argc, char const * const * argv){
+ int rc = 0;
+ int i;
+ int inclCount = 0;
+ const char * zInfile = 0;
+#define M(X) (0==strcmp(X,zArg))
+#define ISFLAG(X) else if(M(X))
+#define ISFLAG2(X,Y) else if(M(X) || M(Y))
+#define ARGVAL \
+ if(i+1>=argc) fatal("Missing value for flag '%s'", zArg); \
+ zArg = argv[++i]
+ g.zArgv0 = argv[0];
+ atexit(cmpp_atexit);
+ cmpp_initdb();
+ for(i = 1; i < argc; ++i){
+ char const * zArg = argv[i];
+ while('-'==*zArg) ++zArg;
+ if(M("?") || M("help")) {
+ usage(0);
+ goto end;
+ }else if('D'==*zArg){
+ ++zArg;
+ if(!*zArg) fatal("Missing key for -D");
+ db_define_add(zArg);
+ }else if('U'==*zArg){
+ ++zArg;
+ if(!*zArg) fatal("Missing key for -U");
+ db_define_rm(zArg);
+ }else if('I'==*zArg){
+ ++zArg;
+ if(!*zArg) fatal("Missing directory for -I");
+ db_include_dir_add(zArg);
+ ++inclCount;
+ }
+ ISFLAG2("o","outfile"){
+ ARGVAL;
+ if(g.out.zName) fatal("Cannot use -o more than once.");
+ g.out.zName = zArg;
+ }
+ ISFLAG2("f","file"){
+ ARGVAL;
+ do_infile:
+ if(zInfile) fatal("Cannot use -i more than once.");
+ zInfile = zArg;
+ }
+ ISFLAG2("d","delimiter"){
+ ARGVAL;
+ g.zDelim = zArg;
+ g.nDelim = (unsigned short)strlen(zArg);
+ if(!g.nDelim) fatal("Keyword delimiter may not be empty.");
+ }
+ ISFLAG("debug"){
+ ++g.doDebug;
+ }else if(!zInfile){
+ goto do_infile;
+ }else{
+ fatal("Unhandled flag: %s", argv[i]);
+ }
+ }
+ if(!zInfile) zInfile = "-";
+ if(!g.out.zName) g.out.zName = "-";
+ if(!inclCount) db_include_dir_add(".");
+ FileWrapper_open(&g.out, g.out.zName, "w");
+ cmpp_process_file(zInfile);
+ FileWrapper_close(&g.out);
+ end:
+ return rc ? EXIT_FAILURE : EXIT_SUCCESS;
+}
+
+#undef CT_level
+#undef CT_pstate
+#undef CT_skipLevel
+#undef CT_skip
+#undef CLvl_skip
diff --git a/ext/wasm/common/SqliteTestUtil.js b/ext/wasm/common/SqliteTestUtil.js
new file mode 100644
index 0000000..2c17824
--- /dev/null
+++ b/ext/wasm/common/SqliteTestUtil.js
@@ -0,0 +1,235 @@
+/*
+ 2022-05-22
+
+ 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 contains bootstrapping code used by various test scripts
+ which live in this file's directory.
+*/
+'use strict';
+(function(self){
+ /* querySelectorAll() proxy */
+ const EAll = function(/*[element=document,] cssSelector*/){
+ return (arguments.length>1 ? arguments[0] : document)
+ .querySelectorAll(arguments[arguments.length-1]);
+ };
+ /* querySelector() proxy */
+ const E = function(/*[element=document,] cssSelector*/){
+ return (arguments.length>1 ? arguments[0] : document)
+ .querySelector(arguments[arguments.length-1]);
+ };
+
+ /**
+ Helpers for writing sqlite3-specific tests.
+ */
+ self.SqliteTestUtil = {
+ /** Running total of the number of tests run via
+ this API. */
+ counter: 0,
+ /**
+ If expr is a function, it is called and its result
+ is returned, coerced to a bool, else expr, coerced to
+ a bool, is returned.
+ */
+ toBool: function(expr){
+ return (expr instanceof Function) ? !!expr() : !!expr;
+ },
+ /** abort() if expr is false. If expr is a function, it
+ is called and its result is evaluated.
+ */
+ assert: function f(expr, msg){
+ if(!f._){
+ f._ = ('undefined'===typeof abort
+ ? (msg)=>{throw new Error(msg)}
+ : abort);
+ }
+ ++this.counter;
+ if(!this.toBool(expr)){
+ f._(msg || "Assertion failed.");
+ }
+ return this;
+ },
+ /** Identical to assert() but throws instead of calling
+ abort(). */
+ affirm: function(expr, msg){
+ ++this.counter;
+ if(!this.toBool(expr)) throw new Error(msg || "Affirmation failed.");
+ return this;
+ },
+ /** Calls f() and squelches any exception it throws. If it
+ does not throw, this function throws. */
+ mustThrow: function(f, msg){
+ ++this.counter;
+ let err;
+ try{ f(); } catch(e){err=e;}
+ if(!err) throw new Error(msg || "Expected exception.");
+ return this;
+ },
+ /**
+ Works like mustThrow() but expects filter to be a regex,
+ function, or string to match/filter the resulting exception
+ against. If f() does not throw, this test fails and an Error is
+ thrown. If filter is a regex, the test passes if
+ filter.test(error.message) passes. If it's a function, the test
+ passes if filter(error) returns truthy. If it's a string, the
+ test passes if the filter matches the exception message
+ precisely. In all other cases the test fails, throwing an
+ Error.
+
+ If it throws, msg is used as the error report unless it's falsy,
+ in which case a default is used.
+ */
+ mustThrowMatching: function(f, filter, msg){
+ ++this.counter;
+ let err;
+ try{ f(); } catch(e){err=e;}
+ if(!err) throw new Error(msg || "Expected exception.");
+ let pass = false;
+ if(filter instanceof RegExp) pass = filter.test(err.message);
+ else if(filter instanceof Function) pass = filter(err);
+ else if('string' === typeof filter) pass = (err.message === filter);
+ if(!pass){
+ throw new Error(msg || ("Filter rejected this exception: "+err.message));
+ }
+ return this;
+ },
+ /** Throws if expr is truthy or expr is a function and expr()
+ returns truthy. */
+ throwIf: function(expr, msg){
+ ++this.counter;
+ if(this.toBool(expr)) throw new Error(msg || "throwIf() failed");
+ return this;
+ },
+ /** Throws if expr is falsy or expr is a function and expr()
+ returns falsy. */
+ throwUnless: function(expr, msg){
+ ++this.counter;
+ if(!this.toBool(expr)) throw new Error(msg || "throwUnless() failed");
+ return this;
+ },
+
+ /**
+ Parses window.location.search-style string into an object
+ containing key/value pairs of URL arguments (already
+ urldecoded). The object is created using Object.create(null),
+ so contains only parsed-out properties and has no prototype
+ (and thus no inherited properties).
+
+ If the str argument is not passed (arguments.length==0) then
+ window.location.search.substring(1) is used by default. If
+ neither str is passed in nor window exists then false is returned.
+
+ On success it returns an Object containing the key/value pairs
+ parsed from the string. Keys which have no value are treated
+ has having the boolean true value.
+
+ Pedantic licensing note: this code has appeared in other source
+ trees, but was originally written by the same person who pasted
+ it into those trees.
+ */
+ processUrlArgs: function(str) {
+ if( 0 === arguments.length ) {
+ if( ('undefined' === typeof window) ||
+ !window.location ||
+ !window.location.search ) return false;
+ else str = (''+window.location.search).substring(1);
+ }
+ if( ! str ) return false;
+ str = (''+str).split(/#/,2)[0]; // remove #... to avoid it being added as part of the last value.
+ const args = Object.create(null);
+ const sp = str.split(/&+/);
+ const rx = /^([^=]+)(=(.+))?/;
+ var i, m;
+ for( i in sp ) {
+ m = rx.exec( sp[i] );
+ if( ! m ) continue;
+ args[decodeURIComponent(m[1])] = (m[3] ? decodeURIComponent(m[3]) : true);
+ }
+ return args;
+ }
+ };
+
+ /**
+ This is a module object for use with the emscripten-installed
+ sqlite3InitModule() factory function.
+ */
+ self.sqlite3TestModule = {
+ /**
+ Array of functions to call after Emscripten has initialized the
+ wasm module. Each gets passed the Emscripten module object
+ (which is _this_ object).
+ */
+ postRun: [
+ /* function(theModule){...} */
+ ],
+ //onRuntimeInitialized: function(){},
+ /* Proxy for C-side stdout output. */
+ print: (...args)=>{console.log(...args)},
+ /* Proxy for C-side stderr output. */
+ printErr: (...args)=>{console.error(...args)},
+ /**
+ Called by the Emscripten module init bits to report loading
+ progress. It gets passed an empty argument when loading is done
+ (after onRuntimeInitialized() and any this.postRun callbacks
+ have been run).
+ */
+ setStatus: function f(text){
+ if(!f.last){
+ f.last = { text: '', step: 0 };
+ f.ui = {
+ status: E('#module-status'),
+ progress: E('#module-progress'),
+ spinner: E('#module-spinner')
+ };
+ }
+ if(text === f.last.text) return;
+ f.last.text = text;
+ if(f.ui.progress){
+ f.ui.progress.value = f.last.step;
+ f.ui.progress.max = f.last.step + 1;
+ }
+ ++f.last.step;
+ if(text) {
+ f.ui.status.classList.remove('hidden');
+ f.ui.status.innerText = text;
+ }else{
+ if(f.ui.progress){
+ f.ui.progress.remove();
+ f.ui.spinner.remove();
+ delete f.ui.progress;
+ delete f.ui.spinner;
+ }
+ f.ui.status.classList.add('hidden');
+ }
+ },
+ /**
+ Config options used by the Emscripten-dependent initialization
+ which happens via this.initSqlite3(). This object gets
+ (indirectly) passed to sqlite3ApiBootstrap() to configure the
+ sqlite3 API.
+ */
+ sqlite3ApiConfig: {
+ wasmfsOpfsDir: "/opfs"
+ },
+ /**
+ Intended to be called by apps which need to call the
+ Emscripten-installed sqlite3InitModule() routine. This function
+ temporarily installs this.sqlite3ApiConfig into the self
+ object, calls it sqlite3InitModule(), and removes
+ self.sqlite3ApiConfig after initialization is done. Returns the
+ promise from sqlite3InitModule(), and the next then() handler
+ will get the sqlite3 API object as its argument.
+ */
+ initSqlite3: function(){
+ self.sqlite3ApiConfig = this.sqlite3ApiConfig;
+ return self.sqlite3InitModule(this).finally(()=>delete self.sqlite3ApiConfig);
+ }
+ };
+})(self/*window or worker*/);
diff --git a/ext/wasm/common/emscripten.css b/ext/wasm/common/emscripten.css
new file mode 100644
index 0000000..d8f82c7
--- /dev/null
+++ b/ext/wasm/common/emscripten.css
@@ -0,0 +1,24 @@
+/* emscripten-related styling, used during the module load/intialization processes... */
+.emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
+div.emscripten { text-align: center; }
+div.emscripten_border { border: 1px solid black; }
+#module-spinner { overflow: visible; }
+#module-spinner > * {
+ margin-top: 1em;
+}
+.spinner {
+ height: 50px;
+ width: 50px;
+ margin: 0px auto;
+ animation: rotation 0.8s linear infinite;
+ border-left: 10px solid rgb(0,150,240);
+ border-right: 10px solid rgb(0,150,240);
+ border-bottom: 10px solid rgb(0,150,240);
+ border-top: 10px solid rgb(100,0,200);
+ border-radius: 100%;
+ background-color: rgb(200,100,250);
+}
+@keyframes rotation {
+ from {transform: rotate(0deg);}
+ to {transform: rotate(360deg);}
+}
diff --git a/ext/wasm/common/testing.css b/ext/wasm/common/testing.css
new file mode 100644
index 0000000..a9be767
--- /dev/null
+++ b/ext/wasm/common/testing.css
@@ -0,0 +1,102 @@
+body {
+ display: flex;
+ flex-direction: column;
+ flex-wrap: wrap;
+}
+textarea {
+ font-family: monospace;
+}
+header {
+ font-size: 130%;
+ font-weight: bold;
+}
+.hidden, .initially-hidden {
+ position: absolute !important;
+ opacity: 0 !important;
+ pointer-events: none !important;
+ display: none !important;
+}
+fieldset.options {
+ font-size: 75%;
+}
+fieldset > legend {
+ padding: 0 0.5em;
+}
+span.labeled-input {
+ padding: 0.25em;
+ margin: 0.25em 0.5em;
+ border-radius: 0.25em;
+ white-space: nowrap;
+ background: #0002;
+}
+.center { text-align: center; }
+.error {
+ color: red;
+ background-color: yellow;
+}
+.strong { font-weight: 700 }
+.warning { color: firebrick; }
+.green { color: darkgreen; }
+.tests-pass { background-color: green; color: white }
+.tests-fail { background-color: red; color: yellow }
+.faded { opacity: 0.5; }
+.group-start {
+ color: blue;
+ background-color: skyblue;
+ font-weight: bold;
+ border-top: 1px dotted blue;
+ padding: 0.5em;
+ margin-top: 0.5em;
+}
+.group-end {
+ padding: 0.5em;
+ margin-bottom: 0.25em;
+ /*border-bottom: 1px dotted blue;*/
+}
+.group-end.green {
+ background: lightgreen;
+ border-bottom: 1px dotted green;
+}
+.one-test-line, .skipping-group {
+ margin-left: 3em;
+}
+.skipping-test, .skipping-group {
+ padding: 0.25em 0.5em;
+ background-color: #ffff73;
+}
+.skipping-test {
+ margin-left: 6em;
+}
+.one-test-summary {
+ margin-left: 6em;
+}
+.full-test-summary {
+ padding-bottom: 0.5em;
+ padding-top: 0.5em;
+ border-top: 1px solid black;
+}
+.input-wrapper {
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+}
+#test-output {
+ border: 1px inset;
+ border-radius: 0.25em;
+ padding: 0.25em;
+ /*max-height: 30em;*/
+ overflow: auto;
+ white-space: break-spaces;
+ display: flex; flex-direction: column;
+ font-family: monospace;
+}
+#test-output.reverse {
+ flex-direction: column-reverse;
+}
+label[for] { cursor: pointer }
+
+h1 {
+ border-radius: 0.25em;
+ padding: 0.15em 0.25em;
+}
+h1:first-of-type {margin: 0 0 0.5em 0;}
diff --git a/ext/wasm/common/whwasmutil.js b/ext/wasm/common/whwasmutil.js
new file mode 100644
index 0000000..0437ef3
--- /dev/null
+++ b/ext/wasm/common/whwasmutil.js
@@ -0,0 +1,2263 @@
+/**
+ 2022-07-08
+
+ 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 whwasmutil is developed in conjunction with the Jaccwabyt
+ project:
+
+ https://fossil.wanderinghorse.net/r/jaccwabyt
+
+ and sqlite3:
+
+ https://sqlite.org
+
+ This file is kept in sync between both of those trees.
+
+ Maintenance reminder: If you're reading this in a tree other than
+ one of those listed above, note that this copy may be replaced with
+ upstream copies of that one from time to time. Thus the code
+ installed by this function "should not" be edited outside of those
+ projects, else it risks getting overwritten.
+*/
+/**
+ This function is intended to simplify porting around various bits
+ of WASM-related utility code from project to project.
+
+ The primary goal of this code is to replace, where possible,
+ Emscripten-generated glue code with equivalent utility code which
+ can be used in arbitrary WASM environments built with toolchains
+ other than Emscripten. As of this writing, this code is capable of
+ acting as a replacement for Emscripten's generated glue code
+ _except_ that the latter installs handlers for Emscripten-provided
+ APIs such as its "FS" (virtual filesystem) API. Loading of such
+ things still requires using Emscripten's glue, but the post-load
+ utility APIs provided by this code are still usable as replacements
+ for their sub-optimally-documented Emscripten counterparts.
+
+ Intended usage:
+
+ ```
+ globalThis.WhWasmUtilInstaller(appObject);
+ delete globalThis.WhWasmUtilInstaller;
+ ```
+
+ Its global-scope symbol is intended only to provide an easy way to
+ make it available to 3rd-party scripts and "should" be deleted
+ after calling it. That symbols is _not_ used within the library.
+
+ Forewarning: this API explicitly targets only browser
+ environments. If a given non-browser environment has the
+ capabilities needed for a given feature (e.g. TextEncoder), great,
+ but it does not go out of its way to account for them and does not
+ provide compatibility crutches for them.
+
+ It currently offers alternatives to the following
+ Emscripten-generated APIs:
+
+ - OPTIONALLY memory allocation, but how this gets imported is
+ environment-specific. Most of the following features only work
+ if allocation is available.
+
+ - WASM-exported "indirect function table" access and
+ manipulation. e.g. creating new WASM-side functions using JS
+ functions, analog to Emscripten's addFunction() and
+ uninstallFunction() but slightly different.
+
+ - Get/set specific heap memory values, analog to Emscripten's
+ getValue() and setValue().
+
+ - String length counting in UTF-8 bytes (C-style and JS strings).
+
+ - JS string to C-string conversion and vice versa, analog to
+ Emscripten's stringToUTF8Array() and friends, but with slighter
+ different interfaces.
+
+ - JS string to Uint8Array conversion, noting that browsers actually
+ already have this built in via TextEncoder.
+
+ - "Scoped" allocation, such that allocations made inside of a given
+ explicit scope will be automatically cleaned up when the scope is
+ closed. This is fundamentally similar to Emscripten's
+ stackAlloc() and friends but uses the heap instead of the stack
+ because access to the stack requires C code.
+
+ - Create JS wrappers for WASM functions, analog to Emscripten's
+ ccall() and cwrap() functions, except that the automatic
+ conversions for function arguments and return values can be
+ easily customized by the client by assigning custom function
+ signature type names to conversion functions. Essentially,
+ it's ccall() and cwrap() on steroids.
+
+ How to install...
+
+ Passing an object to this function will install the functionality
+ into that object. Afterwards, client code "should" delete the global
+ symbol.
+
+ This code requires that the target object have the following
+ properties, noting that they needn't be available until the first
+ time one of the installed APIs is used (as opposed to when this
+ function is called) except where explicitly noted:
+
+ - `exports` must be a property of the target object OR a property
+ of `target.instance` (a WebAssembly.Module instance) and it must
+ contain the symbols exported by the WASM module associated with
+ this code. In an Enscripten environment it must be set to
+ `Module['asm']` (versions <=3.1.43) or `wasmExports` (versions
+ >=3.1.44). The exports object must contain a minimum of the
+ following symbols:
+
+ - `memory`: a WebAssembly.Memory object representing the WASM
+ memory. _Alternately_, the `memory` property can be set as
+ `target.memory`, in particular if the WASM heap memory is
+ initialized in JS an _imported_ into WASM, as opposed to being
+ initialized in WASM and exported to JS.
+
+ - `__indirect_function_table`: the WebAssembly.Table object which
+ holds WASM-exported functions. This API does not strictly
+ require that the table be able to grow but it will throw if its
+ `installFunction()` is called and the table cannot grow.
+
+ In order to simplify downstream usage, if `target.exports` is not
+ set when this is called then a property access interceptor
+ (read-only, configurable, enumerable) gets installed as `exports`
+ which resolves to `target.instance.exports`, noting that the latter
+ property need not exist until the first time `target.exports` is
+ accessed.
+
+ Some APIs _optionally_ make use of the `bigIntEnabled` property of
+ the target object. It "should" be set to true if the WASM
+ environment is compiled with BigInt support, else it must be
+ false. If it is false, certain BigInt-related features will trigger
+ an exception if invoked. This property, if not set when this is
+ called, will get a default value of true only if the BigInt64Array
+ constructor is available, else it will default to false. Note that
+ having the BigInt type is not sufficient for full int64 integration
+ with WASM: the target WASM file must also have been built with
+ that support. In Emscripten that's done using the `-sWASM_BIGINT`
+ flag.
+
+ Some optional APIs require that the target have the following
+ methods:
+
+ - 'alloc()` must behave like C's `malloc()`, allocating N bytes of
+ memory and returning its pointer. In Emscripten this is
+ conventionally made available via `Module['_malloc']`. This API
+ requires that the alloc routine throw on allocation error, as
+ opposed to returning null or 0.
+
+ - 'dealloc()` must behave like C's `free()`, accepting either a
+ pointer returned from its allocation counterpart or the values
+ null/0 (for which it must be a no-op). allocating N bytes of
+ memory and returning its pointer. In Emscripten this is
+ conventionally made available via `Module['_free']`.
+
+ APIs which require allocation routines are explicitly documented as
+ such and/or have "alloc" in their names.
+
+ This code is developed and maintained in conjunction with the
+ Jaccwabyt project:
+
+ https://fossil.wanderinghorse.net/r/jaccwabbyt
+
+ More specifically:
+
+ https://fossil.wanderinghorse.net/r/jaccwabbyt/file/common/whwasmutil.js
+*/
+globalThis.WhWasmUtilInstaller = function(target){
+ 'use strict';
+ if(undefined===target.bigIntEnabled){
+ target.bigIntEnabled = !!globalThis['BigInt64Array'];
+ }
+
+ /** Throws a new Error, the message of which is the concatenation of
+ all args with a space between each. */
+ const toss = (...args)=>{throw new Error(args.join(' '))};
+
+ if(!target.exports){
+ Object.defineProperty(target, 'exports', {
+ enumerable: true, configurable: true,
+ get: ()=>(target.instance && target.instance.exports)
+ });
+ }
+
+ /*********
+ alloc()/dealloc() auto-install...
+
+ This would be convenient but it can also cause us to pick up
+ malloc() even when the client code is using a different exported
+ allocator (who, me?), which is bad. malloc() may be exported even
+ if we're not explicitly using it and overriding the malloc()
+ function, linking ours first, is not always feasible when using a
+ malloc() proxy, as it can lead to recursion and stack overflow
+ (who, me?). So... we really need the downstream code to set up
+ target.alloc/dealloc() itself.
+ ******/
+ /******
+ if(target.exports){
+ //Maybe auto-install alloc()/dealloc()...
+ if(!target.alloc && target.exports.malloc){
+ target.alloc = function(n){
+ const m = this(n);
+ return m || toss("Allocation of",n,"byte(s) failed.");
+ }.bind(target.exports.malloc);
+ }
+
+ if(!target.dealloc && target.exports.free){
+ target.dealloc = function(ptr){
+ if(ptr) this(ptr);
+ }.bind(target.exports.free);
+ }
+ }*******/
+
+ /**
+ Pointers in WASM are currently assumed to be 32-bit, but someday
+ that will certainly change.
+ */
+ const ptrIR = target.pointerIR || 'i32';
+ const ptrSizeof = target.ptrSizeof =
+ ('i32'===ptrIR ? 4
+ : ('i64'===ptrIR
+ ? 8 : toss("Unhandled ptrSizeof:",ptrIR)));
+ /** Stores various cached state. */
+ const cache = Object.create(null);
+ /** Previously-recorded size of cache.memory.buffer, noted so that
+ we can recreate the view objects if the heap grows. */
+ cache.heapSize = 0;
+ /** WebAssembly.Memory object extracted from target.memory or
+ target.exports.memory the first time heapWrappers() is
+ called. */
+ cache.memory = null;
+ /** uninstallFunction() puts table indexes in here for reuse and
+ installFunction() extracts them. */
+ cache.freeFuncIndexes = [];
+ /**
+ Used by scopedAlloc() and friends.
+ */
+ cache.scopedAlloc = [];
+
+ cache.utf8Decoder = new TextDecoder();
+ cache.utf8Encoder = new TextEncoder('utf-8');
+
+ /**
+ For the given IR-like string in the set ('i8', 'i16', 'i32',
+ 'f32', 'float', 'i64', 'f64', 'double', '*'), or any string value
+ ending in '*', returns the sizeof for that value
+ (target.ptrSizeof in the latter case). For any other value, it
+ returns the undefined value.
+ */
+ target.sizeofIR = (n)=>{
+ switch(n){
+ case 'i8': return 1;
+ case 'i16': return 2;
+ case 'i32': case 'f32': case 'float': return 4;
+ case 'i64': case 'f64': case 'double': return 8;
+ case '*': return ptrSizeof;
+ default:
+ return (''+n).endsWith('*') ? ptrSizeof : undefined;
+ }
+ };
+
+ /**
+ If (cache.heapSize !== cache.memory.buffer.byteLength), i.e. if
+ the heap has grown since the last call, updates cache.HEAPxyz.
+ Returns the cache object.
+ */
+ const heapWrappers = function(){
+ if(!cache.memory){
+ cache.memory = (target.memory instanceof WebAssembly.Memory)
+ ? target.memory : target.exports.memory;
+ }else if(cache.heapSize === cache.memory.buffer.byteLength){
+ return cache;
+ }
+ // heap is newly-acquired or has been resized....
+ const b = cache.memory.buffer;
+ cache.HEAP8 = new Int8Array(b); cache.HEAP8U = new Uint8Array(b);
+ cache.HEAP16 = new Int16Array(b); cache.HEAP16U = new Uint16Array(b);
+ cache.HEAP32 = new Int32Array(b); cache.HEAP32U = new Uint32Array(b);
+ if(target.bigIntEnabled){
+ cache.HEAP64 = new BigInt64Array(b); cache.HEAP64U = new BigUint64Array(b);
+ }
+ cache.HEAP32F = new Float32Array(b); cache.HEAP64F = new Float64Array(b);
+ cache.heapSize = b.byteLength;
+ return cache;
+ };
+
+ /** Convenience equivalent of this.heapForSize(8,false). */
+ target.heap8 = ()=>heapWrappers().HEAP8;
+
+ /** Convenience equivalent of this.heapForSize(8,true). */
+ target.heap8u = ()=>heapWrappers().HEAP8U;
+
+ /** Convenience equivalent of this.heapForSize(16,false). */
+ target.heap16 = ()=>heapWrappers().HEAP16;
+
+ /** Convenience equivalent of this.heapForSize(16,true). */
+ target.heap16u = ()=>heapWrappers().HEAP16U;
+
+ /** Convenience equivalent of this.heapForSize(32,false). */
+ target.heap32 = ()=>heapWrappers().HEAP32;
+
+ /** Convenience equivalent of this.heapForSize(32,true). */
+ target.heap32u = ()=>heapWrappers().HEAP32U;
+
+ /**
+ Requires n to be one of:
+
+ - integer 8, 16, or 32.
+ - A integer-type TypedArray constructor: Int8Array, Int16Array,
+ Int32Array, or their Uint counterparts.
+
+ If this.bigIntEnabled is true, it also accepts the value 64 or a
+ BigInt64Array/BigUint64Array, else it throws if passed 64 or one
+ of those constructors.
+
+ Returns an integer-based TypedArray view of the WASM heap
+ memory buffer associated with the given block size. If passed
+ an integer as the first argument and unsigned is truthy then
+ the "U" (unsigned) variant of that view is returned, else the
+ signed variant is returned. If passed a TypedArray value, the
+ 2nd argument is ignored. Note that Float32Array and
+ Float64Array views are not supported by this function.
+
+ Note that growth of the heap will invalidate any references to
+ this heap, so do not hold a reference longer than needed and do
+ not use a reference after any operation which may
+ allocate. Instead, re-fetch the reference by calling this
+ function again.
+
+ Throws if passed an invalid n.
+
+ Pedantic side note: the name "heap" is a bit of a misnomer. In a
+ WASM environment, the stack and heap memory are all accessed via
+ the same view(s) of the memory.
+ */
+ target.heapForSize = function(n,unsigned = true){
+ let ctor;
+ const c = (cache.memory && cache.heapSize === cache.memory.buffer.byteLength)
+ ? cache : heapWrappers();
+ switch(n){
+ case Int8Array: return c.HEAP8; case Uint8Array: return c.HEAP8U;
+ case Int16Array: return c.HEAP16; case Uint16Array: return c.HEAP16U;
+ case Int32Array: return c.HEAP32; case Uint32Array: return c.HEAP32U;
+ case 8: return unsigned ? c.HEAP8U : c.HEAP8;
+ case 16: return unsigned ? c.HEAP16U : c.HEAP16;
+ case 32: return unsigned ? c.HEAP32U : c.HEAP32;
+ case 64:
+ if(c.HEAP64) return unsigned ? c.HEAP64U : c.HEAP64;
+ break;
+ default:
+ if(target.bigIntEnabled){
+ if(n===globalThis['BigUint64Array']) return c.HEAP64U;
+ else if(n===globalThis['BigInt64Array']) return c.HEAP64;
+ break;
+ }
+ }
+ toss("Invalid heapForSize() size: expecting 8, 16, 32,",
+ "or (if BigInt is enabled) 64.");
+ };
+
+ /**
+ Returns the WASM-exported "indirect function table."
+ */
+ target.functionTable = function(){
+ return target.exports.__indirect_function_table;
+ /** -----------------^^^^^ "seems" to be a standardized export name.
+ From Emscripten release notes from 2020-09-10:
+ - Use `__indirect_function_table` as the import name for the
+ table, which is what LLVM does.
+ */
+ };
+
+ /**
+ Given a function pointer, returns the WASM function table entry
+ if found, else returns a falsy value: undefined if fptr is out of
+ range or null if it's in range but the table entry is empty.
+ */
+ target.functionEntry = function(fptr){
+ const ft = target.functionTable();
+ return fptr < ft.length ? ft.get(fptr) : undefined;
+ };
+
+ /**
+ Creates a WASM function which wraps the given JS function and
+ returns the JS binding of that WASM function. The signature
+ string must be the Jaccwabyt-format or Emscripten
+ addFunction()-format function signature string. In short: in may
+ have one of the following formats:
+
+ - Emscripten: `"x..."`, where the first x is a letter representing
+ the result type and subsequent letters represent the argument
+ types. Functions with no arguments have only a single
+ letter. See below.
+
+ - Jaccwabyt: `"x(...)"` where `x` is the letter representing the
+ result type and letters in the parens (if any) represent the
+ argument types. Functions with no arguments use `x()`. See
+ below.
+
+ Supported letters:
+
+ - `i` = int32
+ - `p` = int32 ("pointer")
+ - `j` = int64
+ - `f` = float32
+ - `d` = float64
+ - `v` = void, only legal for use as the result type
+
+ It throws if an invalid signature letter is used.
+
+ Jaccwabyt-format signatures support some additional letters which
+ have no special meaning here but (in this context) act as aliases
+ for other letters:
+
+ - `s`, `P`: same as `p`
+
+ Sidebar: this code is developed together with Jaccwabyt, thus the
+ support for its signature format.
+
+ The arguments may be supplied in either order: (func,sig) or
+ (sig,func).
+ */
+ target.jsFuncToWasm = function f(func, sig){
+ /** Attribution: adapted up from Emscripten-generated glue code,
+ refactored primarily for efficiency's sake, eliminating
+ call-local functions and superfluous temporary arrays. */
+ if(!f._){/*static init...*/
+ f._ = {
+ // Map of signature letters to type IR values
+ sigTypes: Object.assign(Object.create(null),{
+ i: 'i32', p: 'i32', P: 'i32', s: 'i32',
+ j: 'i64', f: 'f32', d: 'f64'
+ }),
+ // Map of type IR values to WASM type code values
+ typeCodes: Object.assign(Object.create(null),{
+ f64: 0x7c, f32: 0x7d, i64: 0x7e, i32: 0x7f
+ }),
+ /** Encodes n, which must be <2^14 (16384), into target array
+ tgt, as a little-endian value, using the given method
+ ('push' or 'unshift'). */
+ uleb128Encode: function(tgt, method, n){
+ if(n<128) tgt[method](n);
+ else tgt[method]( (n % 128) | 128, n>>7);
+ },
+ /** Intentionally-lax pattern for Jaccwabyt-format function
+ pointer signatures, the intent of which is simply to
+ distinguish them from Emscripten-format signatures. The
+ downstream checks are less lax. */
+ rxJSig: /^(\w)\((\w*)\)$/,
+ /** Returns the parameter-value part of the given signature
+ string. */
+ sigParams: function(sig){
+ const m = f._.rxJSig.exec(sig);
+ return m ? m[2] : sig.substr(1);
+ },
+ /** Returns the IR value for the given letter or throws
+ if the letter is invalid. */
+ letterType: (x)=>f._.sigTypes[x] || toss("Invalid signature letter:",x),
+ /** Returns an object describing the result type and parameter
+ type(s) of the given function signature, or throws if the
+ signature is invalid. */
+ /******** // only valid for use with the WebAssembly.Function ctor, which
+ // is not yet documented on MDN.
+ sigToWasm: function(sig){
+ const rc = {parameters:[], results: []};
+ if('v'!==sig[0]) rc.results.push(f.sigTypes(sig[0]));
+ for(const x of f._.sigParams(sig)){
+ rc.parameters.push(f._.typeCodes(x));
+ }
+ return rc;
+ },************/
+ /** Pushes the WASM data type code for the given signature
+ letter to the given target array. Throws if letter is
+ invalid. */
+ pushSigType: (dest, letter)=>dest.push(f._.typeCodes[f._.letterType(letter)])
+ };
+ }/*static init*/
+ if('string'===typeof func){
+ const x = sig;
+ sig = func;
+ func = x;
+ }
+ const sigParams = f._.sigParams(sig);
+ const wasmCode = [0x01/*count: 1*/, 0x60/*function*/];
+ f._.uleb128Encode(wasmCode, 'push', sigParams.length);
+ for(const x of sigParams) f._.pushSigType(wasmCode, x);
+ if('v'===sig[0]) wasmCode.push(0);
+ else{
+ wasmCode.push(1);
+ f._.pushSigType(wasmCode, sig[0]);
+ }
+ f._.uleb128Encode(wasmCode, 'unshift', wasmCode.length)/* type section length */;
+ wasmCode.unshift(
+ 0x00, 0x61, 0x73, 0x6d, /* magic: "\0asm" */
+ 0x01, 0x00, 0x00, 0x00, /* version: 1 */
+ 0x01 /* type section code */
+ );
+ wasmCode.push(
+ /* import section: */ 0x02, 0x07,
+ /* (import "e" "f" (func 0 (type 0))): */
+ 0x01, 0x01, 0x65, 0x01, 0x66, 0x00, 0x00,
+ /* export section: */ 0x07, 0x05,
+ /* (export "f" (func 0 (type 0))): */
+ 0x01, 0x01, 0x66, 0x00, 0x00
+ );
+ return (new WebAssembly.Instance(
+ new WebAssembly.Module(new Uint8Array(wasmCode)), {
+ e: { f: func }
+ })).exports['f'];
+ }/*jsFuncToWasm()*/;
+
+ /**
+ Documented as target.installFunction() except for the 3rd
+ argument: if truthy, the newly-created function pointer
+ is stashed in the current scoped-alloc scope and will be
+ cleaned up at the matching scopedAllocPop(), else it
+ is not stashed there.
+ */
+ const __installFunction = function f(func, sig, scoped){
+ if(scoped && !cache.scopedAlloc.length){
+ toss("No scopedAllocPush() scope is active.");
+ }
+ if('string'===typeof func){
+ const x = sig;
+ sig = func;
+ func = x;
+ }
+ if('string'!==typeof sig || !(func instanceof Function)){
+ toss("Invalid arguments: expecting (function,signature) "+
+ "or (signature,function).");
+ }
+ const ft = target.functionTable();
+ const oldLen = ft.length;
+ let ptr;
+ while(cache.freeFuncIndexes.length){
+ ptr = cache.freeFuncIndexes.pop();
+ if(ft.get(ptr)){ /* Table was modified via a different API */
+ ptr = null;
+ continue;
+ }else{
+ break;
+ }
+ }
+ if(!ptr){
+ ptr = oldLen;
+ ft.grow(1);
+ }
+ try{
+ /*this will only work if func is a WASM-exported function*/
+ ft.set(ptr, func);
+ if(scoped){
+ cache.scopedAlloc[cache.scopedAlloc.length-1].push(ptr);
+ }
+ return ptr;
+ }catch(e){
+ if(!(e instanceof TypeError)){
+ if(ptr===oldLen) cache.freeFuncIndexes.push(oldLen);
+ throw e;
+ }
+ }
+ // It's not a WASM-exported function, so compile one...
+ try {
+ const fptr = target.jsFuncToWasm(func, sig);
+ ft.set(ptr, fptr);
+ if(scoped){
+ cache.scopedAlloc[cache.scopedAlloc.length-1].push(ptr);
+ }
+ }catch(e){
+ if(ptr===oldLen) cache.freeFuncIndexes.push(oldLen);
+ throw e;
+ }
+ return ptr;
+ };
+
+ /**
+ Expects a JS function and signature, exactly as for
+ this.jsFuncToWasm(). It uses that function to create a
+ WASM-exported function, installs that function to the next
+ available slot of this.functionTable(), and returns the
+ function's index in that table (which acts as a pointer to that
+ function). The returned pointer can be passed to
+ uninstallFunction() to uninstall it and free up the table slot for
+ reuse.
+
+ If passed (string,function) arguments then it treats the first
+ argument as the signature and second as the function.
+
+ As a special case, if the passed-in function is a WASM-exported
+ function then the signature argument is ignored and func is
+ installed as-is, without requiring re-compilation/re-wrapping.
+
+ This function will propagate an exception if
+ WebAssembly.Table.grow() throws or this.jsFuncToWasm() throws.
+ The former case can happen in an Emscripten-compiled
+ environment when building without Emscripten's
+ `-sALLOW_TABLE_GROWTH` flag.
+
+ Sidebar: this function differs from Emscripten's addFunction()
+ _primarily_ in that it does not share that function's
+ undocumented behavior of reusing a function if it's passed to
+ addFunction() more than once, which leads to uninstallFunction()
+ breaking clients which do not take care to avoid that case:
+
+ https://github.com/emscripten-core/emscripten/issues/17323
+ */
+ target.installFunction = (func, sig)=>__installFunction(func, sig, false);
+
+ /**
+ Works exactly like installFunction() but requires that a
+ scopedAllocPush() is active and uninstalls the given function
+ when that alloc scope is popped via scopedAllocPop().
+ This is used for implementing JS/WASM function bindings which
+ should only persist for the life of a call into a single
+ C-side function.
+ */
+ target.scopedInstallFunction = (func, sig)=>__installFunction(func, sig, true);
+
+ /**
+ Requires a pointer value previously returned from
+ this.installFunction(). Removes that function from the WASM
+ function table, marks its table slot as free for re-use, and
+ returns that function. It is illegal to call this before
+ installFunction() has been called and results are undefined if
+ ptr was not returned by that function. The returned function
+ may be passed back to installFunction() to reinstall it.
+
+ To simplify certain use cases, if passed a falsy non-0 value
+ (noting that 0 is a valid function table index), this function
+ has no side effects and returns undefined.
+ */
+ target.uninstallFunction = function(ptr){
+ if(!ptr && 0!==ptr) return undefined;
+ const fi = cache.freeFuncIndexes;
+ const ft = target.functionTable();
+ fi.push(ptr);
+ const rc = ft.get(ptr);
+ ft.set(ptr, null);
+ return rc;
+ };
+
+ /**
+ Given a WASM heap memory address and a data type name in the form
+ (i8, i16, i32, i64, float (or f32), double (or f64)), this
+ fetches the numeric value from that address and returns it as a
+ number or, for the case of type='i64', a BigInt (noting that that
+ type triggers an exception if this.bigIntEnabled is
+ falsy). Throws if given an invalid type.
+
+ If the first argument is an array, it is treated as an array of
+ addresses and the result is an array of the values from each of
+ those address, using the same 2nd argument for determining the
+ value type to fetch.
+
+ As a special case, if type ends with a `*`, it is considered to
+ be a pointer type and is treated as the WASM numeric type
+ appropriate for the pointer size (`i32`).
+
+ While likely not obvious, this routine and its poke()
+ counterpart are how pointer-to-value _output_ parameters
+ in WASM-compiled C code can be interacted with:
+
+ ```
+ const ptr = alloc(4);
+ poke(ptr, 0, 'i32'); // clear the ptr's value
+ aCFuncWithOutputPtrToInt32Arg( ptr ); // e.g. void foo(int *x);
+ const result = peek(ptr, 'i32'); // fetch ptr's value
+ dealloc(ptr);
+ ```
+
+ scopedAlloc() and friends can be used to make handling of
+ `ptr` safe against leaks in the case of an exception:
+
+ ```
+ let result;
+ const scope = scopedAllocPush();
+ try{
+ const ptr = scopedAlloc(4);
+ poke(ptr, 0, 'i32');
+ aCFuncWithOutputPtrArg( ptr );
+ result = peek(ptr, 'i32');
+ }finally{
+ scopedAllocPop(scope);
+ }
+ ```
+
+ As a rule poke() must be called to set (typically zero
+ out) the pointer's value, else it will contain an essentially
+ random value.
+
+ ACHTUNG: calling this often, e.g. in a loop, can have a noticably
+ painful impact on performance. Rather than doing so, use
+ heapForSize() to fetch the heap object and read directly from it.
+
+ See: poke()
+ */
+ target.peek = function f(ptr, type='i8'){
+ if(type.endsWith('*')) type = ptrIR;
+ const c = (cache.memory && cache.heapSize === cache.memory.buffer.byteLength)
+ ? cache : heapWrappers();
+ const list = Array.isArray(ptr) ? [] : undefined;
+ let rc;
+ do{
+ if(list) ptr = arguments[0].shift();
+ switch(type){
+ case 'i1':
+ case 'i8': rc = c.HEAP8[ptr>>0]; break;
+ case 'i16': rc = c.HEAP16[ptr>>1]; break;
+ case 'i32': rc = c.HEAP32[ptr>>2]; break;
+ case 'float': case 'f32': rc = c.HEAP32F[ptr>>2]; break;
+ case 'double': case 'f64': rc = Number(c.HEAP64F[ptr>>3]); break;
+ case 'i64':
+ if(target.bigIntEnabled){
+ rc = BigInt(c.HEAP64[ptr>>3]);
+ break;
+ }
+ /* fallthru */
+ default:
+ toss('Invalid type for peek():',type);
+ }
+ if(list) list.push(rc);
+ }while(list && arguments[0].length);
+ return list || rc;
+ };
+
+ /**
+ The counterpart of peek(), this sets a numeric value at
+ the given WASM heap address, using the type to define how many
+ bytes are written. Throws if given an invalid type. See
+ peek() for details about the type argument. If the 3rd
+ argument ends with `*` then it is treated as a pointer type and
+ this function behaves as if the 3rd argument were `i32`.
+
+ If the first argument is an array, it is treated like a list
+ of pointers and the given value is written to each one.
+
+ Returns `this`. (Prior to 2022-12-09 it returns this function.)
+
+ ACHTUNG: calling this often, e.g. in a loop, can have a noticably
+ painful impact on performance. Rather than doing so, use
+ heapForSize() to fetch the heap object and assign directly to it
+ or use the heap's set() method.
+ */
+ target.poke = function(ptr, value, type='i8'){
+ if (type.endsWith('*')) type = ptrIR;
+ const c = (cache.memory && cache.heapSize === cache.memory.buffer.byteLength)
+ ? cache : heapWrappers();
+ for(const p of (Array.isArray(ptr) ? ptr : [ptr])){
+ switch (type) {
+ case 'i1':
+ case 'i8': c.HEAP8[p>>0] = value; continue;
+ case 'i16': c.HEAP16[p>>1] = value; continue;
+ case 'i32': c.HEAP32[p>>2] = value; continue;
+ case 'float': case 'f32': c.HEAP32F[p>>2] = value; continue;
+ case 'double': case 'f64': c.HEAP64F[p>>3] = value; continue;
+ case 'i64':
+ if(c.HEAP64){
+ c.HEAP64[p>>3] = BigInt(value);
+ continue;
+ }
+ /* fallthru */
+ default:
+ toss('Invalid type for poke(): ' + type);
+ }
+ }
+ return this;
+ };
+
+ /**
+ Convenience form of peek() intended for fetching
+ pointer-to-pointer values. If passed a single non-array argument
+ it returns the value of that one pointer address. If passed
+ multiple arguments, or a single array of arguments, it returns an
+ array of their values.
+ */
+ target.peekPtr = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), ptrIR );
+
+ /**
+ A variant of poke() intended for setting pointer-to-pointer
+ values. Its differences from poke() are that (1) it defaults to a
+ value of 0 and (2) it always writes to the pointer-sized heap
+ view.
+ */
+ target.pokePtr = (ptr, value=0)=>target.poke(ptr, value, ptrIR);
+
+ /**
+ Convenience form of peek() intended for fetching i8 values. If
+ passed a single non-array argument it returns the value of that
+ one pointer address. If passed multiple arguments, or a single
+ array of arguments, it returns an array of their values.
+ */
+ target.peek8 = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), 'i8' );
+ /**
+ Convience form of poke() intended for setting individual bytes.
+ Its difference from poke() is that it always writes to the
+ i8-sized heap view.
+ */
+ target.poke8 = (ptr, value)=>target.poke(ptr, value, 'i8');
+ /** i16 variant of peek8(). */
+ target.peek16 = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), 'i16' );
+ /** i16 variant of poke8(). */
+ target.poke16 = (ptr, value)=>target.poke(ptr, value, 'i16');
+ /** i32 variant of peek8(). */
+ target.peek32 = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), 'i32' );
+ /** i32 variant of poke8(). */
+ target.poke32 = (ptr, value)=>target.poke(ptr, value, 'i32');
+ /** i64 variant of peek8(). Will throw if this build is not
+ configured for BigInt support. */
+ target.peek64 = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), 'i64' );
+ /** i64 variant of poke8(). Will throw if this build is not
+ configured for BigInt support. Note that this returns
+ a BigInt-type value, not a Number-type value. */
+ target.poke64 = (ptr, value)=>target.poke(ptr, value, 'i64');
+ /** f32 variant of peek8(). */
+ target.peek32f = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), 'f32' );
+ /** f32 variant of poke8(). */
+ target.poke32f = (ptr, value)=>target.poke(ptr, value, 'f32');
+ /** f64 variant of peek8(). */
+ target.peek64f = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), 'f64' );
+ /** f64 variant of poke8(). */
+ target.poke64f = (ptr, value)=>target.poke(ptr, value, 'f64');
+
+ /** Deprecated alias for getMemValue() */
+ target.getMemValue = target.peek;
+ /** Deprecated alias for peekPtr() */
+ target.getPtrValue = target.peekPtr;
+ /** Deprecated alias for poke() */
+ target.setMemValue = target.poke;
+ /** Deprecated alias for pokePtr() */
+ target.setPtrValue = target.pokePtr;
+
+ /**
+ Returns true if the given value appears to be legal for use as
+ a WASM pointer value. Its _range_ of values is not (cannot be)
+ validated except to ensure that it is a 32-bit integer with a
+ value of 0 or greater. Likewise, it cannot verify whether the
+ value actually refers to allocated memory in the WASM heap.
+ */
+ target.isPtr32 = (ptr)=>('number'===typeof ptr && (ptr===(ptr|0)) && ptr>=0);
+
+ /**
+ isPtr() is an alias for isPtr32(). If/when 64-bit WASM pointer
+ support becomes widespread, it will become an alias for either
+ isPtr32() or the as-yet-hypothetical isPtr64(), depending on a
+ configuration option.
+ */
+ target.isPtr = target.isPtr32;
+
+ /**
+ Expects ptr to be a pointer into the WASM heap memory which
+ refers to a NUL-terminated C-style string encoded as UTF-8.
+ Returns the length, in bytes, of the string, as for `strlen(3)`.
+ As a special case, if !ptr or if it's not a pointer then it
+ returns `null`. Throws if ptr is out of range for
+ target.heap8u().
+ */
+ target.cstrlen = function(ptr){
+ if(!ptr || !target.isPtr(ptr)) return null;
+ const h = heapWrappers().HEAP8U;
+ let pos = ptr;
+ for( ; h[pos] !== 0; ++pos ){}
+ return pos - ptr;
+ };
+
+ /** Internal helper to use in operations which need to distinguish
+ between SharedArrayBuffer heap memory and non-shared heap. */
+ const __SAB = ('undefined'===typeof SharedArrayBuffer)
+ ? function(){} : SharedArrayBuffer;
+ const __utf8Decode = function(arrayBuffer, begin, end){
+ return cache.utf8Decoder.decode(
+ (arrayBuffer.buffer instanceof __SAB)
+ ? arrayBuffer.slice(begin, end)
+ : arrayBuffer.subarray(begin, end)
+ );
+ };
+
+ /**
+ Expects ptr to be a pointer into the WASM heap memory which
+ refers to a NUL-terminated C-style string encoded as UTF-8. This
+ function counts its byte length using cstrlen() then returns a
+ JS-format string representing its contents. As a special case, if
+ ptr is falsy or not a pointer, `null` is returned.
+ */
+ target.cstrToJs = function(ptr){
+ const n = target.cstrlen(ptr);
+ return n ? __utf8Decode(heapWrappers().HEAP8U, ptr, ptr+n) : (null===n ? n : "");
+ };
+
+ /**
+ Given a JS string, this function returns its UTF-8 length in
+ bytes. Returns null if str is not a string.
+ */
+ target.jstrlen = function(str){
+ /** Attribution: derived from Emscripten's lengthBytesUTF8() */
+ if('string'!==typeof str) return null;
+ const n = str.length;
+ let len = 0;
+ for(let i = 0; i < n; ++i){
+ let u = str.charCodeAt(i);
+ if(u>=0xd800 && u<=0xdfff){
+ u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF);
+ }
+ if(u<=0x7f) ++len;
+ else if(u<=0x7ff) len += 2;
+ else if(u<=0xffff) len += 3;
+ else len += 4;
+ }
+ return len;
+ };
+
+ /**
+ Encodes the given JS string as UTF8 into the given TypedArray
+ tgt, starting at the given offset and writing, at most, maxBytes
+ bytes (including the NUL terminator if addNul is true, else no
+ NUL is added). If it writes any bytes at all and addNul is true,
+ it always NUL-terminates the output, even if doing so means that
+ the NUL byte is all that it writes.
+
+ If maxBytes is negative (the default) then it is treated as the
+ remaining length of tgt, starting at the given offset.
+
+ If writing the last character would surpass the maxBytes count
+ because the character is multi-byte, that character will not be
+ written (as opposed to writing a truncated multi-byte character).
+ This can lead to it writing as many as 3 fewer bytes than
+ maxBytes specifies.
+
+ Returns the number of bytes written to the target, _including_
+ the NUL terminator (if any). If it returns 0, it wrote nothing at
+ all, which can happen if:
+
+ - str is empty and addNul is false.
+ - offset < 0.
+ - maxBytes == 0.
+ - maxBytes is less than the byte length of a multi-byte str[0].
+
+ Throws if tgt is not an Int8Array or Uint8Array.
+
+ Design notes:
+
+ - In C's strcpy(), the destination pointer is the first
+ argument. That is not the case here primarily because the 3rd+
+ arguments are all referring to the destination, so it seems to
+ make sense to have them grouped with it.
+
+ - Emscripten's counterpart of this function (stringToUTF8Array())
+ returns the number of bytes written sans NUL terminator. That
+ is, however, ambiguous: str.length===0 or maxBytes===(0 or 1)
+ all cause 0 to be returned.
+ */
+ target.jstrcpy = function(jstr, tgt, offset = 0, maxBytes = -1, addNul = true){
+ /** Attribution: the encoding bits are taken from Emscripten's
+ stringToUTF8Array(). */
+ if(!tgt || (!(tgt instanceof Int8Array) && !(tgt instanceof Uint8Array))){
+ toss("jstrcpy() target must be an Int8Array or Uint8Array.");
+ }
+ if(maxBytes<0) maxBytes = tgt.length - offset;
+ if(!(maxBytes>0) || !(offset>=0)) return 0;
+ let i = 0, max = jstr.length;
+ const begin = offset, end = offset + maxBytes - (addNul ? 1 : 0);
+ for(; i < max && offset < end; ++i){
+ let u = jstr.charCodeAt(i);
+ if(u>=0xd800 && u<=0xdfff){
+ u = 0x10000 + ((u & 0x3FF) << 10) | (jstr.charCodeAt(++i) & 0x3FF);
+ }
+ if(u<=0x7f){
+ if(offset >= end) break;
+ tgt[offset++] = u;
+ }else if(u<=0x7ff){
+ if(offset + 1 >= end) break;
+ tgt[offset++] = 0xC0 | (u >> 6);
+ tgt[offset++] = 0x80 | (u & 0x3f);
+ }else if(u<=0xffff){
+ if(offset + 2 >= end) break;
+ tgt[offset++] = 0xe0 | (u >> 12);
+ tgt[offset++] = 0x80 | ((u >> 6) & 0x3f);
+ tgt[offset++] = 0x80 | (u & 0x3f);
+ }else{
+ if(offset + 3 >= end) break;
+ tgt[offset++] = 0xf0 | (u >> 18);
+ tgt[offset++] = 0x80 | ((u >> 12) & 0x3f);
+ tgt[offset++] = 0x80 | ((u >> 6) & 0x3f);
+ tgt[offset++] = 0x80 | (u & 0x3f);
+ }
+ }
+ if(addNul) tgt[offset++] = 0;
+ return offset - begin;
+ };
+
+ /**
+ Works similarly to C's strncpy(), copying, at most, n bytes (not
+ characters) from srcPtr to tgtPtr. It copies until n bytes have
+ been copied or a 0 byte is reached in src. _Unlike_ strncpy(), it
+ returns the number of bytes it assigns in tgtPtr, _including_ the
+ NUL byte (if any). If n is reached before a NUL byte in srcPtr,
+ tgtPtr will _not_ be NULL-terminated. If a NUL byte is reached
+ before n bytes are copied, tgtPtr will be NUL-terminated.
+
+ If n is negative, cstrlen(srcPtr)+1 is used to calculate it, the
+ +1 being for the NUL byte.
+
+ Throws if tgtPtr or srcPtr are falsy. Results are undefined if:
+
+ - either is not a pointer into the WASM heap or
+
+ - srcPtr is not NUL-terminated AND n is less than srcPtr's
+ logical length.
+
+ ACHTUNG: it is possible to copy partial multi-byte characters
+ this way, and converting such strings back to JS strings will
+ have undefined results.
+ */
+ target.cstrncpy = function(tgtPtr, srcPtr, n){
+ if(!tgtPtr || !srcPtr) toss("cstrncpy() does not accept NULL strings.");
+ if(n<0) n = target.cstrlen(strPtr)+1;
+ else if(!(n>0)) return 0;
+ const heap = target.heap8u();
+ let i = 0, ch;
+ for(; i < n && (ch = heap[srcPtr+i]); ++i){
+ heap[tgtPtr+i] = ch;
+ }
+ if(i<n) heap[tgtPtr + i++] = 0;
+ return i;
+ };
+
+ /**
+ For the given JS string, returns a Uint8Array of its contents
+ encoded as UTF-8. If addNul is true, the returned array will have
+ a trailing 0 entry, else it will not.
+ */
+ target.jstrToUintArray = (str, addNul=false)=>{
+ return cache.utf8Encoder.encode(addNul ? (str+"\0") : str);
+ // Or the hard way...
+ /** Attribution: derived from Emscripten's stringToUTF8Array() */
+ //const a = [], max = str.length;
+ //let i = 0, pos = 0;
+ //for(; i < max; ++i){
+ // let u = str.charCodeAt(i);
+ // if(u>=0xd800 && u<=0xdfff){
+ // u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF);
+ // }
+ // if(u<=0x7f) a[pos++] = u;
+ // else if(u<=0x7ff){
+ // a[pos++] = 0xC0 | (u >> 6);
+ // a[pos++] = 0x80 | (u & 63);
+ // }else if(u<=0xffff){
+ // a[pos++] = 0xe0 | (u >> 12);
+ // a[pos++] = 0x80 | ((u >> 6) & 63);
+ // a[pos++] = 0x80 | (u & 63);
+ // }else{
+ // a[pos++] = 0xf0 | (u >> 18);
+ // a[pos++] = 0x80 | ((u >> 12) & 63);
+ // a[pos++] = 0x80 | ((u >> 6) & 63);
+ // a[pos++] = 0x80 | (u & 63);
+ // }
+ // }
+ // return new Uint8Array(a);
+ };
+
+ const __affirmAlloc = (obj,funcName)=>{
+ if(!(obj.alloc instanceof Function) ||
+ !(obj.dealloc instanceof Function)){
+ toss("Object is missing alloc() and/or dealloc() function(s)",
+ "required by",funcName+"().");
+ }
+ };
+
+ const __allocCStr = function(jstr, returnWithLength, allocator, funcName){
+ __affirmAlloc(target, funcName);
+ if('string'!==typeof jstr) return null;
+ if(0){/* older impl, possibly more widely compatible? */
+ const n = target.jstrlen(jstr),
+ ptr = allocator(n+1);
+ target.jstrcpy(jstr, target.heap8u(), ptr, n+1, true);
+ return returnWithLength ? [ptr, n] : ptr;
+ }else{/* newer, (probably) faster and (certainly) simpler impl */
+ const u = cache.utf8Encoder.encode(jstr),
+ ptr = allocator(u.length+1),
+ heap = heapWrappers().HEAP8U;
+ heap.set(u, ptr);
+ heap[ptr + u.length] = 0;
+ return returnWithLength ? [ptr, u.length] : ptr;
+ }
+ };
+
+ /**
+ Uses target.alloc() to allocate enough memory for jstrlen(jstr)+1
+ bytes of memory, copies jstr to that memory using jstrcpy(),
+ NUL-terminates it, and returns the pointer to that C-string.
+ Ownership of the pointer is transfered to the caller, who must
+ eventually pass the pointer to dealloc() to free it.
+
+ If passed a truthy 2nd argument then its return semantics change:
+ it returns [ptr,n], where ptr is the C-string's pointer and n is
+ its cstrlen().
+
+ Throws if `target.alloc` or `target.dealloc` are not functions.
+ */
+ target.allocCString =
+ (jstr, returnWithLength=false)=>__allocCStr(jstr, returnWithLength,
+ target.alloc, 'allocCString()');
+
+ /**
+ Starts an "allocation scope." All allocations made using
+ scopedAlloc() are recorded in this scope and are freed when the
+ value returned from this function is passed to
+ scopedAllocPop().
+
+ This family of functions requires that the API's object have both
+ `alloc()` and `dealloc()` methods, else this function will throw.
+
+ Intended usage:
+
+ ```
+ const scope = scopedAllocPush();
+ try {
+ const ptr1 = scopedAlloc(100);
+ const ptr2 = scopedAlloc(200);
+ const ptr3 = scopedAlloc(300);
+ ...
+ // Note that only allocations made via scopedAlloc()
+ // are managed by this allocation scope.
+ }finally{
+ scopedAllocPop(scope);
+ }
+ ```
+
+ The value returned by this function must be treated as opaque by
+ the caller, suitable _only_ for passing to scopedAllocPop().
+ Its type and value are not part of this function's API and may
+ change in any given version of this code.
+
+ `scopedAlloc.level` can be used to determine how many scoped
+ alloc levels are currently active.
+ */
+ target.scopedAllocPush = function(){
+ __affirmAlloc(target, 'scopedAllocPush');
+ const a = [];
+ cache.scopedAlloc.push(a);
+ return a;
+ };
+
+ /**
+ Cleans up all allocations made using scopedAlloc() in the context
+ of the given opaque state object, which must be a value returned
+ by scopedAllocPush(). See that function for an example of how to
+ use this function.
+
+ Though scoped allocations are managed like a stack, this API
+ behaves properly if allocation scopes are popped in an order
+ other than the order they were pushed.
+
+ If called with no arguments, it pops the most recent
+ scopedAllocPush() result:
+
+ ```
+ scopedAllocPush();
+ try{ ... } finally { scopedAllocPop(); }
+ ```
+
+ It's generally recommended that it be passed an explicit argument
+ to help ensure that push/push are used in matching pairs, but in
+ trivial code that may be a non-issue.
+ */
+ target.scopedAllocPop = function(state){
+ __affirmAlloc(target, 'scopedAllocPop');
+ const n = arguments.length
+ ? cache.scopedAlloc.indexOf(state)
+ : cache.scopedAlloc.length-1;
+ if(n<0) toss("Invalid state object for scopedAllocPop().");
+ if(0===arguments.length) state = cache.scopedAlloc[n];
+ cache.scopedAlloc.splice(n,1);
+ for(let p; (p = state.pop()); ){
+ if(target.functionEntry(p)){
+ //console.warn("scopedAllocPop() uninstalling function",p);
+ target.uninstallFunction(p);
+ }
+ else target.dealloc(p);
+ }
+ };
+
+ /**
+ Allocates n bytes of memory using this.alloc() and records that
+ fact in the state for the most recent call of scopedAllocPush().
+ Ownership of the memory is given to scopedAllocPop(), which
+ will clean it up when it is called. The memory _must not_ be
+ passed to this.dealloc(). Throws if this API object is missing
+ the required `alloc()` or `dealloc()` functions or no scoped
+ alloc is active.
+
+ See scopedAllocPush() for an example of how to use this function.
+
+ The `level` property of this function can be queried to query how
+ many scoped allocation levels are currently active.
+
+ See also: scopedAllocPtr(), scopedAllocCString()
+ */
+ target.scopedAlloc = function(n){
+ if(!cache.scopedAlloc.length){
+ toss("No scopedAllocPush() scope is active.");
+ }
+ const p = target.alloc(n);
+ cache.scopedAlloc[cache.scopedAlloc.length-1].push(p);
+ return p;
+ };
+
+ Object.defineProperty(target.scopedAlloc, 'level', {
+ configurable: false, enumerable: false,
+ get: ()=>cache.scopedAlloc.length,
+ set: ()=>toss("The 'active' property is read-only.")
+ });
+
+ /**
+ Works identically to allocCString() except that it allocates the
+ memory using scopedAlloc().
+
+ Will throw if no scopedAllocPush() call is active.
+ */
+ target.scopedAllocCString =
+ (jstr, returnWithLength=false)=>__allocCStr(jstr, returnWithLength,
+ target.scopedAlloc, 'scopedAllocCString()');
+
+ // impl for allocMainArgv() and scopedAllocMainArgv().
+ const __allocMainArgv = function(isScoped, list){
+ const pList = target[
+ isScoped ? 'scopedAlloc' : 'alloc'
+ ]((list.length + 1) * target.ptrSizeof);
+ let i = 0;
+ list.forEach((e)=>{
+ target.pokePtr(pList + (target.ptrSizeof * i++),
+ target[
+ isScoped ? 'scopedAllocCString' : 'allocCString'
+ ](""+e));
+ });
+ target.pokePtr(pList + (target.ptrSizeof * i), 0);
+ return pList;
+ };
+
+ /**
+ Creates an array, using scopedAlloc(), suitable for passing to a
+ C-level main() routine. The input is a collection with a length
+ property and a forEach() method. A block of memory
+ (list.length+1) entries long is allocated and each pointer-sized
+ block of that memory is populated with a scopedAllocCString()
+ conversion of the (""+value) of each element, with the exception
+ that the final entry is a NULL pointer. Returns a pointer to the
+ start of the list, suitable for passing as the 2nd argument to a
+ C-style main() function.
+
+ Throws if scopedAllocPush() is not active.
+
+ Design note: the returned array is allocated with an extra NULL
+ pointer entry to accommodate certain APIs, but client code which
+ does not need that functionality should treat the returned array
+ as list.length entries long.
+ */
+ target.scopedAllocMainArgv = (list)=>__allocMainArgv(true, list);
+
+ /**
+ Identical to scopedAllocMainArgv() but uses alloc() instead of
+ scopedAlloc().
+ */
+ target.allocMainArgv = (list)=>__allocMainArgv(false, list);
+
+ /**
+ Expects to be given a C-style string array and its length. It
+ returns a JS array of strings and/or nulls: any entry in the
+ pArgv array which is NULL results in a null entry in the result
+ array. If argc is 0 then an empty array is returned.
+
+ Results are undefined if any entry in the first argc entries of
+ pArgv are neither 0 (NULL) nor legal UTF-format C strings.
+
+ To be clear, the expected C-style arguments to be passed to this
+ function are `(int, char **)` (optionally const-qualified).
+ */
+ target.cArgvToJs = (argc, pArgv)=>{
+ const list = [];
+ for(let i = 0; i < argc; ++i){
+ const arg = target.peekPtr(pArgv + (target.ptrSizeof * i));
+ list.push( arg ? target.cstrToJs(arg) : null );
+ }
+ return list;
+ };
+
+ /**
+ Wraps function call func() in a scopedAllocPush() and
+ scopedAllocPop() block, such that all calls to scopedAlloc() and
+ friends from within that call will have their memory freed
+ automatically when func() returns. If func throws or propagates
+ an exception, the scope is still popped, otherwise it returns the
+ result of calling func().
+ */
+ target.scopedAllocCall = function(func){
+ target.scopedAllocPush();
+ try{ return func() } finally{ target.scopedAllocPop() }
+ };
+
+ /** Internal impl for allocPtr() and scopedAllocPtr(). */
+ const __allocPtr = function(howMany, safePtrSize, method){
+ __affirmAlloc(target, method);
+ const pIr = safePtrSize ? 'i64' : ptrIR;
+ let m = target[method](howMany * (safePtrSize ? 8 : ptrSizeof));
+ target.poke(m, 0, pIr)
+ if(1===howMany){
+ return m;
+ }
+ const a = [m];
+ for(let i = 1; i < howMany; ++i){
+ m += (safePtrSize ? 8 : ptrSizeof);
+ a[i] = m;
+ target.poke(m, 0, pIr);
+ }
+ return a;
+ };
+
+ /**
+ Allocates one or more pointers as a single chunk of memory and
+ zeroes them out.
+
+ The first argument is the number of pointers to allocate. The
+ second specifies whether they should use a "safe" pointer size (8
+ bytes) or whether they may use the default pointer size
+ (typically 4 but also possibly 8).
+
+ How the result is returned depends on its first argument: if
+ passed 1, it returns the allocated memory address. If passed more
+ than one then an array of pointer addresses is returned, which
+ can optionally be used with "destructuring assignment" like this:
+
+ ```
+ const [p1, p2, p3] = allocPtr(3);
+ ```
+
+ ACHTUNG: when freeing the memory, pass only the _first_ result
+ value to dealloc(). The others are part of the same memory chunk
+ and must not be freed separately.
+
+ The reason for the 2nd argument is..
+
+ When one of the returned pointers will refer to a 64-bit value,
+ e.g. a double or int64, an that value must be written or fetched,
+ e.g. using poke() or peek(), it is important that
+ the pointer in question be aligned to an 8-byte boundary or else
+ it will not be fetched or written properly and will corrupt or
+ read neighboring memory. It is only safe to pass false when the
+ client code is certain that it will only get/fetch 4-byte values
+ (or smaller).
+ */
+ target.allocPtr =
+ (howMany=1, safePtrSize=true)=>__allocPtr(howMany, safePtrSize, 'alloc');
+
+ /**
+ Identical to allocPtr() except that it allocates using scopedAlloc()
+ instead of alloc().
+ */
+ target.scopedAllocPtr =
+ (howMany=1, safePtrSize=true)=>__allocPtr(howMany, safePtrSize, 'scopedAlloc');
+
+ /**
+ If target.exports[name] exists, it is returned, else an
+ exception is thrown.
+ */
+ target.xGet = function(name){
+ return target.exports[name] || toss("Cannot find exported symbol:",name);
+ };
+
+ const __argcMismatch =
+ (f,n)=>toss(f+"() requires",n,"argument(s).");
+
+ /**
+ Looks up a WASM-exported function named fname from
+ target.exports. If found, it is called, passed all remaining
+ arguments, and its return value is returned to xCall's caller. If
+ not found, an exception is thrown. This function does no
+ conversion of argument or return types, but see xWrap() and
+ xCallWrapped() for variants which do.
+
+ As a special case, if passed only 1 argument after the name and
+ that argument in an Array, that array's entries become the
+ function arguments. (This is not an ambiguous case because it's
+ not legal to pass an Array object to a WASM function.)
+ */
+ target.xCall = function(fname, ...args){
+ const f = target.xGet(fname);
+ if(!(f instanceof Function)) toss("Exported symbol",fname,"is not a function.");
+ if(f.length!==args.length) __argcMismatch(fname,f.length)
+ /* This is arguably over-pedantic but we want to help clients keep
+ from shooting themselves in the foot when calling C APIs. */;
+ return (2===arguments.length && Array.isArray(arguments[1]))
+ ? f.apply(null, arguments[1])
+ : f.apply(null, args);
+ };
+
+ /**
+ State for use with xWrap()
+ */
+ cache.xWrap = Object.create(null);
+ cache.xWrap.convert = Object.create(null);
+ /** Map of type names to argument conversion functions. */
+ cache.xWrap.convert.arg = new Map;
+ /** Map of type names to return result conversion functions. */
+ cache.xWrap.convert.result = new Map;
+ const xArg = cache.xWrap.convert.arg, xResult = cache.xWrap.convert.result;
+
+ if(target.bigIntEnabled){
+ xArg.set('i64', (i)=>BigInt(i));
+ }
+ const __xArgPtr = 'i32' === ptrIR
+ ? ((i)=>(i | 0)) : ((i)=>(BigInt(i) | BigInt(0)));
+ xArg.set('i32', __xArgPtr )
+ .set('i16', (i)=>((i | 0) & 0xFFFF))
+ .set('i8', (i)=>((i | 0) & 0xFF))
+ .set('f32', (i)=>Number(i).valueOf())
+ .set('float', xArg.get('f32'))
+ .set('f64', xArg.get('f32'))
+ .set('double', xArg.get('f64'))
+ .set('int', xArg.get('i32'))
+ .set('null', (i)=>i)
+ .set(null, xArg.get('null'))
+ .set('**', __xArgPtr)
+ .set('*', __xArgPtr);
+ xResult.set('*', __xArgPtr)
+ .set('pointer', __xArgPtr)
+ .set('number', (v)=>Number(v))
+ .set('void', (v)=>undefined)
+ .set('null', (v)=>v)
+ .set(null, xResult.get('null'));
+
+ { /* Copy certain xArg[...] handlers to xResult[...] and
+ add pointer-style variants of them. */
+ const copyToResult = ['i8', 'i16', 'i32', 'int',
+ 'f32', 'float', 'f64', 'double'];
+ if(target.bigIntEnabled) copyToResult.push('i64');
+ const adaptPtr = xArg.get(ptrIR);
+ for(const t of copyToResult){
+ xArg.set(t+'*', adaptPtr);
+ xResult.set(t+'*', adaptPtr);
+ xResult.set(t, (xArg.get(t) || toss("Missing arg converter:",t)));
+ }
+ }
+
+ /**
+ In order for args of type string to work in various contexts in
+ the sqlite3 API, we need to pass them on as, variably, a C-string
+ or a pointer value. Thus for ARGs of type 'string' and
+ '*'/'pointer' we behave differently depending on whether the
+ argument is a string or not:
+
+ - If v is a string, scopeAlloc() a new C-string from it and return
+ that temp string's pointer.
+
+ - Else return the value from the arg adapter defined for ptrIR.
+
+ TODO? Permit an Int8Array/Uint8Array and convert it to a string?
+ Would that be too much magic concentrated in one place, ready to
+ backfire? We handle that at the client level in sqlite3 with a
+ custom argument converter.
+ */
+ const __xArgString = function(v){
+ if('string'===typeof v) return target.scopedAllocCString(v);
+ return v ? __xArgPtr(v) : null;
+ };
+ xArg.set('string', __xArgString)
+ .set('utf8', __xArgString)
+ .set('pointer', __xArgString);
+ //xArg.set('*', __xArgString);
+
+ xResult.set('string', (i)=>target.cstrToJs(i))
+ .set('utf8', xResult.get('string'))
+ .set('string:dealloc', (i)=>{
+ try { return i ? target.cstrToJs(i) : null }
+ finally{ target.dealloc(i) }
+ })
+ .set('utf8:dealloc', xResult.get('string:dealloc'))
+ .set('json', (i)=>JSON.parse(target.cstrToJs(i)))
+ .set('json:dealloc', (i)=>{
+ try{ return i ? JSON.parse(target.cstrToJs(i)) : null }
+ finally{ target.dealloc(i) }
+ });
+
+ /**
+ Internal-use-only base class for FuncPtrAdapter and potentially
+ additional stateful argument adapter classes.
+
+ Note that its main interface (convertArg()) is strictly
+ internal, not to be exposed to client code, as it may still
+ need re-shaping. Only the constructors of concrete subclasses
+ should be exposed to clients, and those in such a way that
+ does not hinder internal redesign of the convertArg()
+ interface.
+ */
+ const AbstractArgAdapter = class {
+ constructor(opt){
+ this.name = opt.name || 'unnamed adapter';
+ }
+ /**
+ Gets called via xWrap() to "convert" v to whatever type
+ this specific class supports.
+
+ argIndex is the argv index of _this_ argument in the
+ being-xWrap()'d call. argv is the current argument list
+ undergoing xWrap() argument conversion. argv entries to the
+ left of argIndex will have already undergone transformation and
+ those to the right will not have (they will have the values the
+ client-level code passed in, awaiting conversion). The RHS
+ indexes must never be relied upon for anything because their
+ types are indeterminate, whereas the LHS values will be
+ WASM-compatible values by the time this is called.
+ */
+ convertArg(v,argv,argIndex){
+ toss("AbstractArgAdapter must be subclassed.");
+ }
+ };
+
+ /**
+ An attempt at adding function pointer conversion support to
+ xWrap(). This type is recognized by xWrap() as a proxy for
+ converting a JS function to a C-side function, either
+ permanently, for the duration of a single call into the C layer,
+ or semi-contextual, where it may keep track of a single binding
+ for a given context and uninstall the binding if it's replaced.
+
+ The constructor requires an options object with these properties:
+
+ - name (optional): string describing the function binding. This
+ is solely for debugging and error-reporting purposes. If not
+ provided, an empty string is assumed.
+
+ - signature: a function signature string compatible with
+ jsFuncToWasm().
+
+ - bindScope (string): one of ('transient', 'context',
+ 'singleton'). Bind scopes are:
+
+ - 'transient': it will convert JS functions to WASM only for
+ the duration of the xWrap()'d function call, using
+ scopedInstallFunction(). Before that call returns, the
+ WASM-side binding will be uninstalled.
+
+ - 'singleton': holds one function-pointer binding for this
+ instance. If it's called with a different function pointer,
+ it uninstalls the previous one after converting the new
+ value. This is only useful for use with "global" functions
+ which do not rely on any state other than this function
+ pointer. If the being-converted function pointer is intended
+ to be mapped to some sort of state object (e.g. an
+ `sqlite3*`) then "context" (see below) is the proper mode.
+
+ - 'context': similar to singleton mode but for a given
+ "context", where the context is a key provided by the user
+ and possibly dependent on a small amount of call-time
+ context. This mode is the default if bindScope is _not_ set
+ but a property named contextKey (described below) is.
+
+ - 'permanent': the function is installed and left there
+ forever. There is no way to recover its pointer address
+ later on.
+
+ - callProxy (function): if set, this must be a function which
+ will act as a proxy for any "converted" JS function. It is
+ passed the being-converted function value and must return
+ either that function or a function which acts on its
+ behalf. The returned function will be the one which gets
+ installed into the WASM function table. The proxy must perform
+ any required argument conversion (noting that it will be called
+ from C code, so will receive C-format arguments) before passing
+ them on to the being-converted function. Whether or not the
+ proxy itself must return a value depends on the context. If it
+ does, it must be a WASM-friendly value, as it will be returning
+ from a call made from native code.
+
+ - contextKey (function): is only used if bindScope is 'context'
+ or if bindScope is not set and this function is, in which case
+ 'context' is assumed. This function gets bound to this object,
+ so its "this" is this object. It gets passed (argv,argIndex),
+ where argIndex is the index of _this_ function pointer in its
+ _wrapping_ function's arguments and argv is the _current_
+ still-being-xWrap()-processed args array. All arguments to the
+ left of argIndex will have been processed by xWrap() by the
+ time this is called. argv[argIndex] will be the value the user
+ passed in to the xWrap()'d function for the argument this
+ FuncPtrAdapter is mapped to. Arguments to the right of
+ argv[argIndex] will not yet have been converted before this is
+ called. The function must return a key which uniquely
+ identifies this function mapping context for _this_
+ FuncPtrAdapter instance (other instances are not considered),
+ taking into account that C functions often take some sort of
+ state object as one or more of their arguments. As an example,
+ if the xWrap()'d function takes `(int,T*,functionPtr,X*)` and
+ this FuncPtrAdapter is the argv[2]nd arg, contextKey(argv,2)
+ might return 'T@'+argv[1], or even just argv[1]. Note,
+ however, that the (X*) argument will not yet have been
+ processed by the time this is called and should not be used as
+ part of that key because its pre-conversion data type might be
+ unpredictable. Similarly, care must be taken with C-string-type
+ arguments: those to the left in argv will, when this is called,
+ be WASM pointers, whereas those to the right might (and likely
+ do) have another data type. When using C-strings in keys, never
+ use their pointers in the key because most C-strings in this
+ constellation are transient.
+
+ Yes, that ^^^ is quite awkward, but it's what we have.
+
+ The constructor only saves the above state for later, and does
+ not actually bind any functions. Its convertArg() method is
+ called via xWrap() to perform any bindings.
+
+ Shortcomings:
+
+ - These "reverse" bindings, i.e. calling into a JS-defined
+ function from a WASM-defined function (the generated proxy
+ wrapper), lack all type conversion support. That means, for
+ example, that...
+
+ - Function pointers which include C-string arguments may still
+ need a level of hand-written wrappers around them, depending on
+ how they're used, in order to provide the client with JS
+ strings. Alternately, clients will need to perform such conversions
+ on their own, e.g. using cstrtojs(). Or maybe we can find a way
+ to perform such conversions here, via addition of an xWrap()-style
+ function signature to the options argument.
+ */
+ xArg.FuncPtrAdapter = class FuncPtrAdapter extends AbstractArgAdapter {
+ constructor(opt) {
+ super(opt);
+ if(xArg.FuncPtrAdapter.warnOnUse){
+ console.warn('xArg.FuncPtrAdapter is an internal-only API',
+ 'and is not intended to be invoked from',
+ 'client-level code. Invoked with:',opt);
+ }
+ this.name = opt.name || "unnamed";
+ this.signature = opt.signature;
+ if(opt.contextKey instanceof Function){
+ this.contextKey = opt.contextKey;
+ if(!opt.bindScope) opt.bindScope = 'context';
+ }
+ this.bindScope = opt.bindScope
+ || toss("FuncPtrAdapter options requires a bindScope (explicit or implied).");
+ if(FuncPtrAdapter.bindScopes.indexOf(opt.bindScope)<0){
+ toss("Invalid options.bindScope ("+opt.bindMod+") for FuncPtrAdapter. "+
+ "Expecting one of: ("+FuncPtrAdapter.bindScopes.join(', ')+')');
+ }
+ this.isTransient = 'transient'===this.bindScope;
+ this.isContext = 'context'===this.bindScope;
+ this.isPermanent = 'permanent'===this.bindScope;
+ this.singleton = ('singleton'===this.bindScope) ? [] : undefined;
+ //console.warn("FuncPtrAdapter()",opt,this);
+ this.callProxy = (opt.callProxy instanceof Function)
+ ? opt.callProxy : undefined;
+ }
+
+ /**
+ Note that static class members are defined outside of the class
+ to work around an emcc toolchain build problem: one of the
+ tools in emsdk v3.1.42 does not support the static keyword.
+ */
+
+ /* Dummy impl. Overwritten per-instance as needed. */
+ contextKey(argv,argIndex){
+ return this;
+ }
+
+ /* Returns this objects mapping for the given context key, in the
+ form of an an array, creating the mapping if needed. The key
+ may be anything suitable for use in a Map. */
+ contextMap(key){
+ const cm = (this.__cmap || (this.__cmap = new Map));
+ let rc = cm.get(key);
+ if(undefined===rc) cm.set(key, (rc = []));
+ return rc;
+ }
+
+ /**
+ Gets called via xWrap() to "convert" v to a WASM-bound function
+ pointer. If v is one of (a pointer, null, undefined) then
+ (v||0) is returned and any earlier function installed by this
+ mapping _might_, depending on how it's bound, be uninstalled.
+ If v is not one of those types, it must be a Function, for
+ which it creates (if needed) a WASM function binding and
+ returns the WASM pointer to that binding. If this instance is
+ not in 'transient' mode, it will remember the binding for at
+ least the next call, to avoid recreating the function binding
+ unnecessarily.
+
+ If it's passed a pointer(ish) value for v, it does _not_
+ perform any function binding, so this object's bindMode is
+ irrelevant for such cases.
+
+ See the parent class's convertArg() docs for details on what
+ exactly the 2nd and 3rd arguments are.
+ */
+ convertArg(v,argv,argIndex){
+ //FuncPtrAdapter.debugOut("FuncPtrAdapter.convertArg()",this.name,this.signature,this.transient,v);
+ let pair = this.singleton;
+ if(!pair && this.isContext){
+ pair = this.contextMap(this.contextKey(argv,argIndex));
+ //FuncPtrAdapter.debugOut(this.name, this.signature, "contextKey() =",this.contextKey(argv,argIndex), pair);
+ }
+ if(pair && pair[0]===v) return pair[1];
+ if(v instanceof Function){
+ /* Install a WASM binding and return its pointer. */
+ //FuncPtrAdapter.debugOut("FuncPtrAdapter.convertArg()",this.name,this.signature,this.transient,v,pair);
+ if(this.callProxy) v = this.callProxy(v);
+ const fp = __installFunction(v, this.signature, this.isTransient);
+ if(FuncPtrAdapter.debugFuncInstall){
+ FuncPtrAdapter.debugOut("FuncPtrAdapter installed", this,
+ this.contextKey(argv,argIndex), '@'+fp, v);
+ }
+ if(pair){
+ /* Replace existing stashed mapping */
+ if(pair[1]){
+ if(FuncPtrAdapter.debugFuncInstall){
+ FuncPtrAdapter.debugOut("FuncPtrAdapter uninstalling", this,
+ this.contextKey(argv,argIndex), '@'+pair[1], v);
+ }
+ try{
+ /* Because the pending native call might rely on the
+ pointer we're replacing, e.g. as is normally the case
+ with sqlite3's xDestroy() methods, we don't
+ immediately uninstall but instead add its pointer to
+ the scopedAlloc stack, which will be cleared when the
+ xWrap() mechanism is done calling the native
+ function. We're relying very much here on xWrap()
+ having pushed an alloc scope.
+ */
+ cache.scopedAlloc[cache.scopedAlloc.length-1].push(pair[1]);
+ }
+ catch(e){/*ignored*/}
+ }
+ pair[0] = v;
+ pair[1] = fp;
+ }
+ return fp;
+ }else if(target.isPtr(v) || null===v || undefined===v){
+ //FuncPtrAdapter.debugOut("FuncPtrAdapter.convertArg()",this.name,this.signature,this.transient,v,pair);
+ if(pair && pair[1] && pair[1]!==v){
+ /* uninstall stashed mapping and replace stashed mapping with v. */
+ if(FuncPtrAdapter.debugFuncInstall){
+ FuncPtrAdapter.debugOut("FuncPtrAdapter uninstalling", this,
+ this.contextKey(argv,argIndex), '@'+pair[1], v);
+ }
+ try{ cache.scopedAlloc[cache.scopedAlloc.length-1].push(pair[1]) }
+ catch(e){/*ignored*/}
+ pair[0] = pair[1] = (v | 0);
+ }
+ return v || 0;
+ }else{
+ throw new TypeError("Invalid FuncPtrAdapter argument type. "+
+ "Expecting a function pointer or a "+
+ (this.name ? this.name+' ' : '')+
+ "function matching signature "+
+ this.signature+".");
+ }
+ }/*convertArg()*/
+ }/*FuncPtrAdapter*/;
+
+ /** If true, the constructor emits a warning. The intent is that
+ this be set to true after bootstrapping of the higher-level
+ client library is complete, to warn downstream clients that
+ they shouldn't be relying on this implemenation detail which
+ does not have a stable interface. */
+ xArg.FuncPtrAdapter.warnOnUse = false;
+
+ /** If true, convertArg() will FuncPtrAdapter.debugOut() when it
+ (un)installs a function binding to/from WASM. Note that
+ deinstallation of bindScope=transient bindings happens
+ via scopedAllocPop() so will not be output. */
+ xArg.FuncPtrAdapter.debugFuncInstall = false;
+
+ /** Function used for debug output. */
+ xArg.FuncPtrAdapter.debugOut = console.debug.bind(console);
+
+ xArg.FuncPtrAdapter.bindScopes = [
+ 'transient', 'context', 'singleton', 'permanent'
+ ];
+
+ const __xArgAdapterCheck =
+ (t)=>xArg.get(t) || toss("Argument adapter not found:",t);
+
+ const __xResultAdapterCheck =
+ (t)=>xResult.get(t) || toss("Result adapter not found:",t);
+
+ cache.xWrap.convertArg = (t,...args)=>__xArgAdapterCheck(t)(...args);
+ cache.xWrap.convertArgNoCheck = (t,...args)=>xArg.get(t)(...args);
+
+ cache.xWrap.convertResult =
+ (t,v)=>(null===t ? v : (t ? __xResultAdapterCheck(t)(v) : undefined));
+ cache.xWrap.convertResultNoCheck =
+ (t,v)=>(null===t ? v : (t ? xResult.get(t)(v) : undefined));
+
+ /**
+ Creates a wrapper for another function which converts the arguments
+ of the wrapper to argument types accepted by the wrapped function,
+ then converts the wrapped function's result to another form
+ for the wrapper.
+
+ The first argument must be one of:
+
+ - A JavaScript function.
+ - The name of a WASM-exported function. In the latter case xGet()
+ is used to fetch the exported function, which throws if it's not
+ found.
+ - A pointer into the indirect function table. e.g. a pointer
+ returned from target.installFunction().
+
+ It returns either the passed-in function or a wrapper for that
+ function which converts the JS-side argument types into WASM-side
+ types and converts the result type.
+
+ The second argument, `resultType`, describes the conversion for
+ the wrapped functions result. A literal `null` or the string
+ `'null'` both mean to return the original function's value as-is
+ (mnemonic: there is "null" conversion going on). Literal
+ `undefined` or the string `"void"` both mean to ignore the
+ function's result and return `undefined`. Aside from those two
+ special cases, the `resultType` value may be one of the values
+ described below or any mapping installed by the client using
+ xWrap.resultAdapter().
+
+ If passed 3 arguments and the final one is an array, that array
+ must contain a list of type names (see below) for adapting the
+ arguments from JS to WASM. If passed 2 arguments, more than 3,
+ or the 3rd is not an array, all arguments after the 2nd (if any)
+ are treated as type names. i.e.:
+
+ ```
+ xWrap('funcname', 'i32', 'string', 'f64');
+ // is equivalent to:
+ xWrap('funcname', 'i32', ['string', 'f64']);
+ ```
+
+ This function enforces that the given list of arguments has the
+ same arity as the being-wrapped function (as defined by its
+ `length` property) and it will throw if that is not the case.
+ Similarly, the created wrapper will throw if passed a differing
+ argument count.
+
+ Type names are symbolic names which map the arguments to an
+ adapter function to convert, if needed, the value before passing
+ it on to WASM or to convert a return result from WASM. The list
+ of built-in names:
+
+ - `i8`, `i16`, `i32` (args and results): all integer conversions
+ which convert their argument to an integer and truncate it to
+ the given bit length.
+
+ - `N*` (args): a type name in the form `N*`, where N is a numeric
+ type name, is treated the same as WASM pointer.
+
+ - `*` and `pointer` (args): are assumed to be WASM pointer values
+ and are returned coerced to an appropriately-sized pointer
+ value (i32 or i64). Non-numeric values will coerce to 0 and
+ out-of-range values will have undefined results (just as with
+ any pointer misuse).
+
+ - `*` and `pointer` (results): aliases for the current
+ WASM pointer numeric type.
+
+ - `**` (args): is simply a descriptive alias for the WASM pointer
+ type. It's primarily intended to mark output-pointer arguments.
+
+ - `i64` (args and results): passes the value to BigInt() to
+ convert it to an int64. Only available if bigIntEnabled is
+ true.
+
+ - `f32` (`float`), `f64` (`double`) (args and results): pass
+ their argument to Number(). i.e. the adapter does not currently
+ distinguish between the two types of floating-point numbers.
+
+ - `number` (results): converts the result to a JS Number using
+ Number(theValue).valueOf(). Note that this is for result
+ conversions only, as it's not possible to generically know
+ which type of number to convert arguments to.
+
+ Non-numeric conversions include:
+
+ - `null` literal or `"null"` string (args and results): perform
+ no translation and pass the arg on as-is. This is primarily
+ useful for results but may have a use or two for arguments.
+
+ - `string` or `utf8` (args): has two different semantics in order
+ to accommodate various uses of certain C APIs
+ (e.g. output-style strings)...
+
+ - If the arg is a string, it creates a _temporary_
+ UTF-8-encoded C-string to pass to the exported function,
+ cleaning it up before the wrapper returns. If a long-lived
+ C-string pointer is required, that requires client-side code
+ to create the string, then pass its pointer to the function.
+
+ - Else the arg is assumed to be a pointer to a string the
+ client has already allocated and it's passed on as
+ a WASM pointer.
+
+ - `string` or `utf8` (results): treats the result value as a
+ const C-string, encoded as UTF-8, copies it to a JS string,
+ and returns that JS string.
+
+ - `string:dealloc` or `utf8:dealloc) (results): treats the result value
+ as a non-const UTF-8 C-string, ownership of which has just been
+ transfered to the caller. It copies the C-string to a JS
+ string, frees the C-string, and returns the JS string. If such
+ a result value is NULL, the JS result is `null`. Achtung: when
+ using an API which returns results from a specific allocator,
+ e.g. `my_malloc()`, this conversion _is not legal_. Instead, an
+ equivalent conversion which uses the appropriate deallocator is
+ required. For example:
+
+```js
+ target.xWrap.resultAdapter('string:my_free',(i)=>{
+ try { return i ? target.cstrToJs(i) : null }
+ finally{ target.exports.my_free(i) }
+ };
+```
+
+ - `json` (results): treats the result as a const C-string and
+ returns the result of passing the converted-to-JS string to
+ JSON.parse(). Returns `null` if the C-string is a NULL pointer.
+
+ - `json:dealloc` (results): works exactly like `string:dealloc` but
+ returns the same thing as the `json` adapter. Note the
+ warning in `string:dealloc` regarding maching allocators and
+ deallocators.
+
+ The type names for results and arguments are validated when
+ xWrap() is called and any unknown names will trigger an
+ exception.
+
+ Clients may map their own result and argument adapters using
+ xWrap.resultAdapter() and xWrap.argAdapter(), noting that not all
+ type conversions are valid for both arguments _and_ result types
+ as they often have different memory ownership requirements.
+
+ Design note: the ability to pass in a JS function as the first
+ argument is of relatively limited use, primarily for testing
+ argument and result converters. JS functions, by and large, will
+ not want to deal with C-type arguments.
+
+ TODOs:
+
+ - Figure out how/whether we can (semi-)transparently handle
+ pointer-type _output_ arguments. Those currently require
+ explicit handling by allocating pointers, assigning them before
+ the call using poke(), and fetching them with
+ peek() after the call. We may be able to automate some
+ or all of that.
+
+ - Figure out whether it makes sense to extend the arg adapter
+ interface such that each arg adapter gets an array containing
+ the results of the previous arguments in the current call. That
+ might allow some interesting type-conversion feature. Use case:
+ handling of the final argument to sqlite3_prepare_v2() depends
+ on the type (pointer vs JS string) of its 2nd
+ argument. Currently that distinction requires hand-writing a
+ wrapper for that function. That case is unusual enough that
+ abstracting it into this API (and taking on the associated
+ costs) may well not make good sense.
+ */
+ target.xWrap = function(fArg, resultType, ...argTypes){
+ if(3===arguments.length && Array.isArray(arguments[2])){
+ argTypes = arguments[2];
+ }
+ if(target.isPtr(fArg)){
+ fArg = target.functionEntry(fArg)
+ || toss("Function pointer not found in WASM function table.");
+ }
+ const fIsFunc = (fArg instanceof Function);
+ const xf = fIsFunc ? fArg : target.xGet(fArg);
+ if(fIsFunc) fArg = xf.name || 'unnamed function';
+ if(argTypes.length!==xf.length) __argcMismatch(fArg, xf.length);
+ if((null===resultType) && 0===xf.length){
+ /* Func taking no args with an as-is return. We don't need a wrapper.
+ We forego the argc check here, though. */
+ return xf;
+ }
+ /*Verify the arg type conversions are valid...*/;
+ if(undefined!==resultType && null!==resultType) __xResultAdapterCheck(resultType);
+ for(const t of argTypes){
+ if(t instanceof AbstractArgAdapter) xArg.set(t, (...args)=>t.convertArg(...args));
+ else __xArgAdapterCheck(t);
+ }
+ const cxw = cache.xWrap;
+ if(0===xf.length){
+ // No args to convert, so we can create a simpler wrapper...
+ return (...args)=>(args.length
+ ? __argcMismatch(fArg, xf.length)
+ : cxw.convertResult(resultType, xf.call(null)));
+ }
+ return function(...args){
+ if(args.length!==xf.length) __argcMismatch(fArg, xf.length);
+ const scope = target.scopedAllocPush();
+ try{
+ /*
+ Maintenance reminder re. arguments passed to convertArg():
+ The public interface of argument adapters is that they take
+ ONE argument and return a (possibly) converted result for
+ it. The passing-on of arguments after the first is an
+ internal implementation detail for the sake of
+ AbstractArgAdapter, and not to be relied on or documented
+ for other cases. The fact that this is how
+ AbstractArgAdapter.convertArgs() gets its 2nd+ arguments,
+ and how FuncPtrAdapter.contextKey() gets its args, is also
+ an implementation detail and subject to change. i.e. the
+ public interface of 1 argument is stable. The fact that any
+ arguments may be passed in after that one, and what those
+ arguments are, is _not_ part of the public interface and is
+ _not_ stable.
+ */
+ for(const i in args) args[i] = cxw.convertArgNoCheck(
+ argTypes[i], args[i], args, i
+ );
+ return cxw.convertResultNoCheck(resultType, xf.apply(null,args));
+ }finally{
+ target.scopedAllocPop(scope);
+ }
+ };
+ }/*xWrap()*/;
+
+ /** Internal impl for xWrap.resultAdapter() and argAdapter(). */
+ const __xAdapter = function(func, argc, typeName, adapter, modeName, xcvPart){
+ if('string'===typeof typeName){
+ if(1===argc) return xcvPart.get(typeName);
+ else if(2===argc){
+ if(!adapter){
+ delete xcvPart.get(typeName);
+ return func;
+ }else if(!(adapter instanceof Function)){
+ toss(modeName,"requires a function argument.");
+ }
+ xcvPart.set(typeName, adapter);
+ return func;
+ }
+ }
+ toss("Invalid arguments to",modeName);
+ };
+
+ /**
+ Gets, sets, or removes a result value adapter for use with
+ xWrap(). If passed only 1 argument, the adapter function for the
+ given type name is returned. If the second argument is explicit
+ falsy (as opposed to defaulted), the adapter named by the first
+ argument is removed. If the 2nd argument is not falsy, it must be
+ a function which takes one value and returns a value appropriate
+ for the given type name. The adapter may throw if its argument is
+ not of a type it can work with. This function throws for invalid
+ arguments.
+
+ Example:
+
+ ```
+ xWrap.resultAdapter('twice',(v)=>v+v);
+ ```
+
+ xWrap.resultAdapter() MUST NOT use the scopedAlloc() family of
+ APIs to allocate a result value. xWrap()-generated wrappers run
+ in the context of scopedAllocPush() so that argument adapters can
+ easily convert, e.g., to C-strings, and have them cleaned up
+ automatically before the wrapper returns to the caller. Likewise,
+ if a _result_ adapter uses scoped allocation, the result will be
+ freed before because they would be freed before the wrapper
+ returns, leading to chaos and undefined behavior.
+
+ Except when called as a getter, this function returns itself.
+ */
+ target.xWrap.resultAdapter = function f(typeName, adapter){
+ return __xAdapter(f, arguments.length, typeName, adapter,
+ 'resultAdapter()', xResult);
+ };
+
+ /**
+ Functions identically to xWrap.resultAdapter() but applies to
+ call argument conversions instead of result value conversions.
+
+ xWrap()-generated wrappers perform argument conversion in the
+ context of a scopedAllocPush(), so any memory allocation
+ performed by argument adapters really, really, really should be
+ made using the scopedAlloc() family of functions unless
+ specifically necessary. For example:
+
+ ```
+ xWrap.argAdapter('my-string', function(v){
+ return ('string'===typeof v)
+ ? myWasmObj.scopedAllocCString(v) : null;
+ };
+ ```
+
+ Contrariwise, xWrap.resultAdapter() must _not_ use scopedAlloc()
+ to allocate its results because they would be freed before the
+ xWrap()-created wrapper returns.
+
+ Note that it is perfectly legitimate to use these adapters to
+ perform argument validation, as opposed (or in addition) to
+ conversion.
+ */
+ target.xWrap.argAdapter = function f(typeName, adapter){
+ return __xAdapter(f, arguments.length, typeName, adapter,
+ 'argAdapter()', xArg);
+ };
+
+ target.xWrap.FuncPtrAdapter = xArg.FuncPtrAdapter;
+
+ /**
+ Functions like xCall() but performs argument and result type
+ conversions as for xWrap(). The first, second, and third
+ arguments are as documented for xWrap(), except that the 3rd
+ argument may be either a falsy value or empty array to represent
+ nullary functions. The 4th+ arguments are arguments for the call,
+ with the special case that if the 4th argument is an array, it is
+ used as the arguments for the call. Returns the converted result
+ of the call.
+
+ This is just a thin wrapper around xWrap(). If the given function
+ is to be called more than once, it's more efficient to use
+ xWrap() to create a wrapper, then to call that wrapper as many
+ times as needed. For one-shot calls, however, this variant is
+ arguably more efficient because it will hypothetically free the
+ wrapper function quickly.
+ */
+ target.xCallWrapped = function(fArg, resultType, argTypes, ...args){
+ if(Array.isArray(arguments[3])) args = arguments[3];
+ return target.xWrap(fArg, resultType, argTypes||[]).apply(null, args||[]);
+ };
+
+ /**
+ This function is ONLY exposed in the public API to facilitate
+ testing. It should not be used in application-level code, only
+ in test code.
+
+ Expects to be given (typeName, value) and returns a conversion
+ of that value as has been registered using argAdapter().
+ It throws if no adapter is found.
+
+ ACHTUNG: the adapter may require that a scopedAllocPush() is
+ active and it may allocate memory within that scope. It may also
+ require additional arguments, depending on the type of
+ conversion.
+ */
+ target.xWrap.testConvertArg = cache.xWrap.convertArg;
+
+ /**
+ This function is ONLY exposed in the public API to facilitate
+ testing. It should not be used in application-level code, only
+ in test code.
+
+ Expects to be given (typeName, value) and returns a conversion
+ of that value as has been registered using resultAdapter().
+ It throws if no adapter is found.
+
+ ACHTUNG: the adapter may allocate memory which the caller may need
+ to know how to free.
+ */
+ target.xWrap.testConvertResult = cache.xWrap.convertResult;
+
+ return target;
+};
+
+/**
+ yawl (Yet Another Wasm Loader) provides very basic wasm loader.
+ It requires a config object:
+
+ - `uri`: required URI of the WASM file to load.
+
+ - `onload(loadResult,config)`: optional callback. The first
+ argument is the result object from
+ WebAssembly.instantiate[Streaming](). The 2nd is the config
+ object passed to this function. Described in more detail below.
+
+ - `imports`: optional imports object for
+ WebAssembly.instantiate[Streaming](). The default is an empty set
+ of imports. If the module requires any imports, this object
+ must include them.
+
+ - `wasmUtilTarget`: optional object suitable for passing to
+ WhWasmUtilInstaller(). If set, it gets passed to that function
+ after the promise resolves. This function sets several properties
+ on it before passing it on to that function (which sets many
+ more):
+
+ - `module`, `instance`: the properties from the
+ instantiate[Streaming]() result.
+
+ - If `instance.exports.memory` is _not_ set then it requires that
+ `config.imports.env.memory` be set (else it throws), and
+ assigns that to `target.memory`.
+
+ - If `wasmUtilTarget.alloc` is not set and
+ `instance.exports.malloc` is, it installs
+ `wasmUtilTarget.alloc()` and `wasmUtilTarget.dealloc()`
+ wrappers for the exports `malloc` and `free` functions.
+
+ It returns a function which, when called, initiates loading of the
+ module and returns a Promise. When that Promise resolves, it calls
+ the `config.onload` callback (if set) and passes it
+ `(loadResult,config)`, where `loadResult` is the result of
+ WebAssembly.instantiate[Streaming](): an object in the form:
+
+ ```
+ {
+ module: a WebAssembly.Module,
+ instance: a WebAssembly.Instance
+ }
+ ```
+
+ (Note that the initial `then()` attached to the promise gets only
+ that object, and not the `config` one.)
+
+ Error handling is up to the caller, who may attach a `catch()` call
+ to the promise.
+*/
+globalThis.WhWasmUtilInstaller.yawl = function(config){
+ const wfetch = ()=>fetch(config.uri, {credentials: 'same-origin'});
+ const wui = this;
+ const finalThen = function(arg){
+ //log("finalThen()",arg);
+ if(config.wasmUtilTarget){
+ const toss = (...args)=>{throw new Error(args.join(' '))};
+ const tgt = config.wasmUtilTarget;
+ tgt.module = arg.module;
+ tgt.instance = arg.instance;
+ //tgt.exports = tgt.instance.exports;
+ if(!tgt.instance.exports.memory){
+ /**
+ WhWasmUtilInstaller requires either tgt.exports.memory
+ (exported from WASM) or tgt.memory (JS-provided memory
+ imported into WASM).
+ */
+ tgt.memory = (config.imports && config.imports.env
+ && config.imports.env.memory)
+ || toss("Missing 'memory' object!");
+ }
+ if(!tgt.alloc && arg.instance.exports.malloc){
+ const exports = arg.instance.exports;
+ tgt.alloc = function(n){
+ return exports.malloc(n) || toss("Allocation of",n,"bytes failed.");
+ };
+ tgt.dealloc = function(m){exports.free(m)};
+ }
+ wui(tgt);
+ }
+ if(config.onload) config.onload(arg,config);
+ return arg /* for any then() handler attached to
+ yetAnotherWasmLoader()'s return value */;
+ };
+ const loadWasm = WebAssembly.instantiateStreaming
+ ? function loadWasmStreaming(){
+ return WebAssembly.instantiateStreaming(wfetch(), config.imports||{})
+ .then(finalThen);
+ }
+ : function loadWasmOldSchool(){ // Safari < v15
+ return wfetch()
+ .then(response => response.arrayBuffer())
+ .then(bytes => WebAssembly.instantiate(bytes, config.imports||{}))
+ .then(finalThen);
+ };
+ return loadWasm;
+}.bind(globalThis.WhWasmUtilInstaller)/*yawl()*/;
diff --git a/ext/wasm/demo-123-worker.html b/ext/wasm/demo-123-worker.html
new file mode 100644
index 0000000..692203d
--- /dev/null
+++ b/ext/wasm/demo-123-worker.html
@@ -0,0 +1,44 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <title>Hello, sqlite3</title>
+ <style>
+ .warning, .error {color: red}
+ .error {background-color: yellow}
+ body {
+ display: flex;
+ flex-direction: column;
+ font-family: monospace;
+ white-space: break-spaces;
+ }
+ </style>
+ </head>
+ <body>
+ <h1>1-2-sqlite3 worker demo</h1>
+ <script>(function(){
+ const logHtml = function(cssClass,...args){
+ const ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ document.body.append(ln);
+ };
+ const w = new Worker("demo-123.js?sqlite3.dir=jswasm"
+ /* Note the URL argument on that name. See
+ the notes in demo-123.js (search for
+ "importScripts") for why we need
+ that. */);
+ w.onmessage = function({data}){
+ switch(data.type){
+ case 'log':
+ logHtml(data.payload.cssClass, ...data.payload.args);
+ break;
+ default:
+ logHtml('error',"Unhandled message:",data.type);
+ };
+ };
+ })();</script>
+ </body>
+</html>
diff --git a/ext/wasm/demo-123.html b/ext/wasm/demo-123.html
new file mode 100644
index 0000000..2046b07
--- /dev/null
+++ b/ext/wasm/demo-123.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <title>Hello, sqlite3</title>
+ <style>
+ .warning, .error {color: red}
+ .error {background-color: yellow}
+ body {
+ display: flex;
+ flex-direction: column;
+ font-family: monospace;
+ white-space: break-spaces;
+ }
+ </style>
+ </head>
+ <body>
+ <h1>1-2-sqlite3 demo</h1>
+ <script src="jswasm/sqlite3.js"></script>
+ <script src="demo-123.js"></script>
+ </body>
+</html>
diff --git a/ext/wasm/demo-123.js b/ext/wasm/demo-123.js
new file mode 100644
index 0000000..9f90ca7
--- /dev/null
+++ b/ext/wasm/demo-123.js
@@ -0,0 +1,290 @@
+/*
+ 2022-09-19
+
+ 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.
+
+ ***********************************************************************
+
+ A basic demonstration of the SQLite3 "OO#1" API.
+*/
+'use strict';
+(function(){
+ /**
+ Set up our output channel differently depending
+ on whether we are running in a worker thread or
+ the main (UI) thread.
+ */
+ let logHtml;
+ if(globalThis.window === globalThis /* UI thread */){
+ console.log("Running demo from main UI thread.");
+ logHtml = function(cssClass,...args){
+ const ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ document.body.append(ln);
+ };
+ }else{ /* Worker thread */
+ console.log("Running demo from Worker thread.");
+ logHtml = function(cssClass,...args){
+ postMessage({
+ type:'log',
+ payload:{cssClass, args}
+ });
+ };
+ }
+ const log = (...args)=>logHtml('',...args);
+ const warn = (...args)=>logHtml('warning',...args);
+ const error = (...args)=>logHtml('error',...args);
+
+ const demo1 = function(sqlite3){
+ const capi = sqlite3.capi/*C-style API*/,
+ oo = sqlite3.oo1/*high-level OO API*/;
+ log("sqlite3 version",capi.sqlite3_libversion(), capi.sqlite3_sourceid());
+ const db = new oo.DB("/mydb.sqlite3",'ct');
+ log("transient db =",db.filename);
+ /**
+ Never(!) rely on garbage collection to clean up DBs and
+ (especially) prepared statements. Always wrap their lifetimes
+ in a try/finally construct, as demonstrated below. By and
+ large, client code can entirely avoid lifetime-related
+ complications of prepared statement objects by using the
+ DB.exec() method for SQL execution.
+ */
+ try {
+ log("Create a table...");
+ db.exec("CREATE TABLE IF NOT EXISTS t(a,b)");
+ //Equivalent:
+ db.exec({
+ sql:"CREATE TABLE IF NOT EXISTS t(a,b)"
+ // ... numerous other options ...
+ });
+ // SQL can be either a string or a byte array
+ // or an array of strings which get concatenated
+ // together as-is (so be sure to end each statement
+ // with a semicolon).
+
+ log("Insert some data using exec()...");
+ let i;
+ for( i = 20; i <= 25; ++i ){
+ db.exec({
+ sql: "insert into t(a,b) values (?,?)",
+ // bind by parameter index...
+ bind: [i, i*2]
+ });
+ db.exec({
+ sql: "insert into t(a,b) values ($a,$b)",
+ // bind by parameter name...
+ bind: {$a: i * 10, $b: i * 20}
+ });
+ }
+
+ log("Insert using a prepared statement...");
+ let q = db.prepare([
+ // SQL may be a string or array of strings
+ // (concatenated w/o separators).
+ "insert into t(a,b) ",
+ "values(?,?)"
+ ]);
+ try {
+ for( i = 100; i < 103; ++i ){
+ q.bind( [i, i*2] ).step();
+ q.reset();
+ }
+ // Equivalent...
+ for( i = 103; i <= 105; ++i ){
+ q.bind(1, i).bind(2, i*2).stepReset();
+ }
+ }finally{
+ q.finalize();
+ }
+
+ log("Query data with exec() using rowMode 'array'...");
+ db.exec({
+ sql: "select a from t order by a limit 3",
+ rowMode: 'array', // 'array' (default), 'object', or 'stmt'
+ callback: function(row){
+ log("row ",++this.counter,"=",row);
+ }.bind({counter: 0})
+ });
+
+ log("Query data with exec() using rowMode 'object'...");
+ db.exec({
+ sql: "select a as aa, b as bb from t order by aa limit 3",
+ rowMode: 'object',
+ callback: function(row){
+ log("row ",++this.counter,"=",JSON.stringify(row));
+ }.bind({counter: 0})
+ });
+
+ log("Query data with exec() using rowMode 'stmt'...");
+ db.exec({
+ sql: "select a from t order by a limit 3",
+ rowMode: 'stmt',
+ callback: function(row){
+ log("row ",++this.counter,"get(0) =",row.get(0));
+ }.bind({counter: 0})
+ });
+
+ log("Query data with exec() using rowMode INTEGER (result column index)...");
+ db.exec({
+ sql: "select a, b from t order by a limit 3",
+ rowMode: 1, // === result column 1
+ callback: function(row){
+ log("row ",++this.counter,"b =",row);
+ }.bind({counter: 0})
+ });
+
+ log("Query data with exec() using rowMode $COLNAME (result column name)...");
+ db.exec({
+ sql: "select a a, b from t order by a limit 3",
+ rowMode: '$a',
+ callback: function(value){
+ log("row ",++this.counter,"a =",value);
+ }.bind({counter: 0})
+ });
+
+ log("Query data with exec() without a callback...");
+ let resultRows = [];
+ db.exec({
+ sql: "select a, b from t order by a limit 3",
+ rowMode: 'object',
+ resultRows: resultRows
+ });
+ log("Result rows:",JSON.stringify(resultRows,undefined,2));
+
+ log("Create a scalar UDF...");
+ db.createFunction({
+ name: 'twice',
+ xFunc: function(pCx, arg){ // note the call arg count
+ return arg + arg;
+ }
+ });
+ log("Run scalar UDF and collect result column names...");
+ let columnNames = [];
+ db.exec({
+ sql: "select a, twice(a), twice(''||a) from t order by a desc limit 3",
+ columnNames: columnNames,
+ rowMode: 'stmt',
+ callback: function(row){
+ log("a =",row.get(0), "twice(a) =", row.get(1),
+ "twice(''||a) =",row.get(2));
+ }
+ });
+ log("Result column names:",columnNames);
+
+ try{
+ log("The following use of the twice() UDF will",
+ "fail because of incorrect arg count...");
+ db.exec("select twice(1,2,3)");
+ }catch(e){
+ warn("Got expected exception:",e.message);
+ }
+
+ try {
+ db.transaction( function(D) {
+ D.exec("delete from t");
+ log("In transaction: count(*) from t =",db.selectValue("select count(*) from t"));
+ throw new sqlite3.SQLite3Error("Demonstrating transaction() rollback");
+ });
+ }catch(e){
+ if(e instanceof sqlite3.SQLite3Error){
+ log("Got expected exception from db.transaction():",e.message);
+ log("count(*) from t =",db.selectValue("select count(*) from t"));
+ }else{
+ throw e;
+ }
+ }
+
+ try {
+ db.savepoint( function(D) {
+ D.exec("delete from t");
+ log("In savepoint: count(*) from t =",db.selectValue("select count(*) from t"));
+ D.savepoint(function(DD){
+ const rows = [];
+ DD.exec({
+ sql: ["insert into t(a,b) values(99,100);",
+ "select count(*) from t"],
+ rowMode: 0,
+ resultRows: rows
+ });
+ log("In nested savepoint. Row count =",rows[0]);
+ throw new sqlite3.SQLite3Error("Demonstrating nested savepoint() rollback");
+ })
+ });
+ }catch(e){
+ if(e instanceof sqlite3.SQLite3Error){
+ log("Got expected exception from nested db.savepoint():",e.message);
+ log("count(*) from t =",db.selectValue("select count(*) from t"));
+ }else{
+ throw e;
+ }
+ }
+ }finally{
+ db.close();
+ }
+
+ log("That's all, folks!");
+
+ /**
+ Some of the features of the OO API not demonstrated above...
+
+ - get change count (total or statement-local, 32- or 64-bit)
+ - get a DB's file name
+
+ Misc. Stmt features:
+
+ - Various forms of bind()
+ - clearBindings()
+ - reset()
+ - Various forms of step()
+ - Variants of get() for explicit type treatment/conversion,
+ e.g. getInt(), getFloat(), getBlob(), getJSON()
+ - getColumnName(ndx), getColumnNames()
+ - getParamIndex(name)
+ */
+ }/*demo1()*/;
+
+ log("Loading and initializing sqlite3 module...");
+ if(globalThis.window!==globalThis) /*worker thread*/{
+ /*
+ If sqlite3.js is in a directory other than this script, in order
+ to get sqlite3.js to resolve sqlite3.wasm properly, we have to
+ explicitly tell it where sqlite3.js is being loaded from. We do
+ that by passing the `sqlite3.dir=theDirName` URL argument to
+ _this_ script. That URL argument will be seen by the JS/WASM
+ loader and it will adjust the sqlite3.wasm path accordingly. If
+ sqlite3.js/.wasm are in the same directory as this script then
+ that's not needed.
+
+ URL arguments passed as part of the filename via importScripts()
+ are simply lost, and such scripts see the globalThis.location of
+ _this_ script.
+ */
+ let sqlite3Js = 'sqlite3.js';
+ const urlParams = new URL(globalThis.location.href).searchParams;
+ if(urlParams.has('sqlite3.dir')){
+ sqlite3Js = urlParams.get('sqlite3.dir') + '/' + sqlite3Js;
+ }
+ importScripts(sqlite3Js);
+ }
+ globalThis.sqlite3InitModule({
+ /* We can redirect any stdout/stderr from the module like so, but
+ note that doing so makes use of Emscripten-isms, not
+ well-defined sqlite APIs. */
+ print: log,
+ printErr: error
+ }).then(function(sqlite3){
+ //console.log('sqlite3 =',sqlite3);
+ log("Done initializing. Running demo...");
+ try {
+ demo1(sqlite3);
+ }catch(e){
+ error("Exception:",e.message);
+ }
+ });
+})();
diff --git a/ext/wasm/demo-jsstorage.html b/ext/wasm/demo-jsstorage.html
new file mode 100644
index 0000000..79f4a3b
--- /dev/null
+++ b/ext/wasm/demo-jsstorage.html
@@ -0,0 +1,49 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>sqlite3-kvvfs.js tests</title>
+ </head>
+ <body>
+ <header id='titlebar'><span>sqlite3-kvvfs.js tests</span></header>
+ <!-- emscripten bits -->
+ <figure id="module-spinner">
+ <div class="spinner"></div>
+ <div class='center'><strong>Initializing app...</strong></div>
+ <div class='center'>
+ On a slow internet connection this may take a moment. If this
+ message displays for "a long time", intialization may have
+ failed and the JavaScript console may contain clues as to why.
+ </div>
+ </figure>
+ <div class="emscripten" id="module-status">Downloading...</div>
+ <div class="emscripten">
+ <progress value="0" max="100" id="module-progress" hidden='1'></progress>
+ </div><!-- /emscripten bits -->
+ <fieldset>
+ <legend>Options</legend>
+ <div class='toolbar'>
+ <button id='btn-clear-log'>Clear log</button>
+ <button id='btn-clear-storage'>Clear storage</button>
+ <button id='btn-init-db'>(Re)init db</button>
+ <button id='btn-select1'>Select db rows</button>
+ <button id='btn-storage-size'>Approx. storage size</button>
+ </div>
+ </fieldset>
+ <div id='test-output'></div>
+ <style>
+ .toolbar {
+ display: flex;
+ }
+ .toolbar > * { margin: 0.25em; }
+ fieldset { border-radius: 0.5em; }
+ </style>
+ <script src="jswasm/sqlite3.js"></script>
+ <script src="common/SqliteTestUtil.js"></script>
+ <script src="demo-jsstorage.js"></script>
+ </body>
+</html>
diff --git a/ext/wasm/demo-jsstorage.js b/ext/wasm/demo-jsstorage.js
new file mode 100644
index 0000000..cf820e4
--- /dev/null
+++ b/ext/wasm/demo-jsstorage.js
@@ -0,0 +1,114 @@
+/*
+ 2022-09-12
+
+ 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.
+
+ ***********************************************************************
+
+ A basic test script for sqlite3.wasm with kvvfs support. This file
+ must be run in main JS thread and sqlite3.js must have been loaded
+ before it.
+*/
+'use strict';
+(function(){
+ const T = self.SqliteTestUtil;
+ const toss = function(...args){throw new Error(args.join(' '))};
+ const debug = console.debug.bind(console);
+ const eOutput = document.querySelector('#test-output');
+ const logC = console.log.bind(console)
+ const logE = function(domElement){
+ eOutput.append(domElement);
+ };
+ const logHtml = function(cssClass,...args){
+ const ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ logE(ln);
+ }
+ const log = function(...args){
+ logC(...args);
+ logHtml('',...args);
+ };
+ const warn = function(...args){
+ logHtml('warning',...args);
+ };
+ const error = function(...args){
+ logHtml('error',...args);
+ };
+
+ const runTests = function(sqlite3){
+ const capi = sqlite3.capi,
+ oo = sqlite3.oo1,
+ wasm = sqlite3.wasm;
+ log("Loaded module:",capi.sqlite3_libversion(), capi.sqlite3_sourceid());
+ T.assert( 0 !== capi.sqlite3_vfs_find(null) );
+ if(!capi.sqlite3_vfs_find('kvvfs')){
+ error("This build is not kvvfs-capable.");
+ return;
+ }
+
+ const dbStorage = 0 ? 'session' : 'local';
+ const theStore = 's'===dbStorage[0] ? sessionStorage : localStorage;
+ const db = new oo.JsStorageDb( dbStorage );
+ // Or: oo.DB(dbStorage, 'c', 'kvvfs')
+ log("db.storageSize():",db.storageSize());
+ document.querySelector('#btn-clear-storage').addEventListener('click',function(){
+ const sz = db.clearStorage();
+ log("kvvfs",db.filename+"Storage cleared:",sz,"entries.");
+ });
+ document.querySelector('#btn-clear-log').addEventListener('click',function(){
+ eOutput.innerText = '';
+ });
+ document.querySelector('#btn-init-db').addEventListener('click',function(){
+ try{
+ const saveSql = [];
+ db.exec({
+ sql: ["drop table if exists t;",
+ "create table if not exists t(a);",
+ "insert into t(a) values(?),(?),(?)"],
+ bind: [performance.now() >> 0,
+ (performance.now() * 2) >> 0,
+ (performance.now() / 2) >> 0],
+ saveSql
+ });
+ console.log("saveSql =",saveSql,theStore);
+ log("DB (re)initialized.");
+ }catch(e){
+ error(e.message);
+ }
+ });
+ const btnSelect = document.querySelector('#btn-select1');
+ btnSelect.addEventListener('click',function(){
+ log("DB rows:");
+ try{
+ db.exec({
+ sql: "select * from t order by a",
+ rowMode: 0,
+ callback: (v)=>log(v)
+ });
+ }catch(e){
+ error(e.message);
+ }
+ });
+ document.querySelector('#btn-storage-size').addEventListener('click',function(){
+ log("size.storageSize(",dbStorage,") says", db.storageSize(),
+ "bytes");
+ });
+ log("Storage backend:",db.filename);
+ if(0===db.selectValue('select count(*) from sqlite_master')){
+ log("DB is empty. Use the init button to populate it.");
+ }else{
+ log("DB contains data from a previous session. Use the Clear Ctorage button to delete it.");
+ btnSelect.click();
+ }
+ };
+
+ sqlite3InitModule(self.sqlite3TestModule).then((sqlite3)=>{
+ runTests(sqlite3);
+ });
+})();
diff --git a/ext/wasm/demo-worker1-promiser.html b/ext/wasm/demo-worker1-promiser.html
new file mode 100644
index 0000000..e99131e
--- /dev/null
+++ b/ext/wasm/demo-worker1-promiser.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>worker-promise tests</title>
+ </head>
+ <body>
+ <header id='titlebar'><span>worker-promise tests</span></header>
+ <!-- emscripten bits -->
+ <figure id="module-spinner">
+ <div class="spinner"></div>
+ <div class='center'><strong>Initializing app...</strong></div>
+ <div class='center'>
+ On a slow internet connection this may take a moment. If this
+ message displays for "a long time", intialization may have
+ failed and the JavaScript console may contain clues as to why.
+ </div>
+ </figure>
+ <div class="emscripten" id="module-status">Downloading...</div>
+ <div class="emscripten">
+ <progress value="0" max="100" id="module-progress" hidden='1'></progress>
+ </div><!-- /emscripten bits -->
+ <div>Most stuff on this page happens in the dev console.</div>
+ <hr>
+ <div id='test-output'></div>
+ <script src="common/SqliteTestUtil.js"></script>
+ <script src="jswasm/sqlite3-worker1-promiser.js"></script>
+ <script src="demo-worker1-promiser.js"></script>
+ </body>
+</html>
diff --git a/ext/wasm/demo-worker1-promiser.js b/ext/wasm/demo-worker1-promiser.js
new file mode 100644
index 0000000..c2d2462
--- /dev/null
+++ b/ext/wasm/demo-worker1-promiser.js
@@ -0,0 +1,275 @@
+/*
+ 2022-08-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.
+
+ ***********************************************************************
+
+ Demonstration of the sqlite3 Worker API #1 Promiser: a Promise-based
+ proxy for for the sqlite3 Worker #1 API.
+*/
+'use strict';
+(function(){
+ const T = self.SqliteTestUtil;
+ const eOutput = document.querySelector('#test-output');
+ const warn = console.warn.bind(console);
+ const error = console.error.bind(console);
+ const log = console.log.bind(console);
+ const logHtml = async function(cssClass,...args){
+ log.apply(this, args);
+ const ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ eOutput.append(ln);
+ };
+
+ let startTime;
+ const testCount = async ()=>{
+ logHtml("","Total test count:",T.counter+". Total time =",(performance.now() - startTime),"ms");
+ };
+
+ //why is this triggered even when we catch() a Promise?
+ //window.addEventListener('unhandledrejection', function(event) {
+ // warn('unhandledrejection',event);
+ //});
+
+ const promiserConfig = {
+ worker: ()=>{
+ const w = new Worker("jswasm/sqlite3-worker1.js");
+ w.onerror = (event)=>error("worker.onerror",event);
+ return w;
+ },
+ debug: 1 ? undefined : (...args)=>console.debug('worker debug',...args),
+ onunhandled: function(ev){
+ error("Unhandled worker message:",ev.data);
+ },
+ onready: function(){
+ self.sqlite3TestModule.setStatus(null)/*hide the HTML-side is-loading spinner*/;
+ runTests();
+ },
+ onerror: function(ev){
+ error("worker1 error:",ev);
+ }
+ };
+ const workerPromise = self.sqlite3Worker1Promiser(promiserConfig);
+ delete self.sqlite3Worker1Promiser;
+
+ const wtest = async function(msgType, msgArgs, callback){
+ if(2===arguments.length && 'function'===typeof msgArgs){
+ callback = msgArgs;
+ msgArgs = undefined;
+ }
+ const p = 1
+ ? workerPromise({type: msgType, args:msgArgs})
+ : workerPromise(msgType, msgArgs);
+ return callback ? p.then(callback).finally(testCount) : p;
+ };
+
+ let sqConfig;
+ const runTests = async function(){
+ const dbFilename = '/testing2.sqlite3';
+ startTime = performance.now();
+
+ await wtest('config-get', (ev)=>{
+ const r = ev.result;
+ log('sqlite3.config subset:', r);
+ T.assert('boolean' === typeof r.bigIntEnabled);
+ sqConfig = r;
+ });
+ logHtml('',
+ "Sending 'open' message and waiting for its response before continuing...");
+
+ await wtest('open', {
+ filename: dbFilename,
+ simulateError: 0 /* if true, fail the 'open' */,
+ }, function(ev){
+ const r = ev.result;
+ log("then open result",r);
+ T.assert(ev.dbId === r.dbId)
+ .assert(ev.messageId)
+ .assert('string' === typeof r.vfs);
+ promiserConfig.dbId = ev.dbId;
+ }).then(runTests2);
+ };
+
+ const runTests2 = async function(){
+ const mustNotReach = ()=>toss("This is not supposed to be reached.");
+
+ await wtest('exec',{
+ sql: ["create table t(a,b)",
+ "insert into t(a,b) values(1,2),(3,4),(5,6)"
+ ].join(';'),
+ resultRows: [], columnNames: [],
+ countChanges: sqConfig.bigIntEnabled ? 64 : true
+ }, function(ev){
+ ev = ev.result;
+ T.assert(0===ev.resultRows.length)
+ .assert(0===ev.columnNames.length)
+ .assert(sqConfig.bigIntEnabled
+ ? (3n===ev.changeCount)
+ : (3===ev.changeCount));
+ });
+
+ await wtest('exec',{
+ sql: 'select a a, b b from t order by a',
+ resultRows: [], columnNames: [],
+ }, function(ev){
+ ev = ev.result;
+ T.assert(3===ev.resultRows.length)
+ .assert(1===ev.resultRows[0][0])
+ .assert(6===ev.resultRows[2][1])
+ .assert(2===ev.columnNames.length)
+ .assert('b'===ev.columnNames[1]);
+ });
+
+ await wtest('exec',{
+ sql: 'select a a, b b from t order by a',
+ resultRows: [], columnNames: [],
+ rowMode: 'object',
+ countChanges: true
+ }, function(ev){
+ ev = ev.result;
+ T.assert(3===ev.resultRows.length)
+ .assert(1===ev.resultRows[0].a)
+ .assert(6===ev.resultRows[2].b)
+ .assert(0===ev.changeCount);
+ });
+
+ await wtest(
+ 'exec',
+ {sql:'intentional_error'},
+ mustNotReach
+ ).catch((e)=>{
+ warn("Intentional error:",e);
+ });
+
+ await wtest('exec',{
+ sql:'select 1 union all select 3',
+ resultRows: []
+ }, function(ev){
+ ev = ev.result;
+ T.assert(2 === ev.resultRows.length)
+ .assert(1 === ev.resultRows[0][0])
+ .assert(3 === ev.resultRows[1][0])
+ .assert(undefined === ev.changeCount);
+ });
+
+ const resultRowTest1 = function f(ev){
+ if(undefined === f.counter) f.counter = 0;
+ if(null === ev.rowNumber){
+ /* End of result set. */
+ T.assert(undefined === ev.row)
+ .assert(2===ev.columnNames.length)
+ .assert('a'===ev.columnNames[0])
+ .assert('B'===ev.columnNames[1]);
+ }else{
+ T.assert(ev.rowNumber > 0);
+ ++f.counter;
+ }
+ log("exec() result row:",ev);
+ T.assert(null === ev.rowNumber || 'number' === typeof ev.row.B);
+ };
+ await wtest('exec',{
+ sql: 'select a a, b B from t order by a limit 3',
+ callback: resultRowTest1,
+ rowMode: 'object'
+ }, function(ev){
+ T.assert(3===resultRowTest1.counter);
+ resultRowTest1.counter = 0;
+ });
+
+ const resultRowTest2 = function f(ev){
+ if(null === ev.rowNumber){
+ /* End of result set. */
+ T.assert(undefined === ev.row)
+ .assert(1===ev.columnNames.length)
+ .assert('a'===ev.columnNames[0])
+ }else{
+ T.assert(ev.rowNumber > 0);
+ f.counter = ev.rowNumber;
+ }
+ log("exec() result row:",ev);
+ T.assert(null === ev.rowNumber || 'number' === typeof ev.row);
+ };
+ await wtest('exec',{
+ sql: 'select a a from t limit 3',
+ callback: resultRowTest2,
+ rowMode: 0
+ }, function(ev){
+ T.assert(3===resultRowTest2.counter);
+ });
+
+ const resultRowTest3 = function f(ev){
+ if(null === ev.rowNumber){
+ T.assert(3===ev.columnNames.length)
+ .assert('foo'===ev.columnNames[0])
+ .assert('bar'===ev.columnNames[1])
+ .assert('baz'===ev.columnNames[2]);
+ }else{
+ f.counter = ev.rowNumber;
+ T.assert('number' === typeof ev.row);
+ }
+ };
+ await wtest('exec',{
+ sql: "select 'foo' foo, a bar, 'baz' baz from t limit 2",
+ callback: resultRowTest3,
+ columnNames: [],
+ rowMode: '$bar'
+ }, function(ev){
+ log("exec() result row:",ev);
+ T.assert(2===resultRowTest3.counter);
+ });
+
+ await wtest('exec',{
+ sql:[
+ 'pragma foreign_keys=0;',
+ // ^^^ arbitrary query with no result columns
+ 'select a, b from t order by a desc; select a from t;'
+ // exec() only honors SELECT results from the first
+ // statement with result columns (regardless of whether
+ // it has any rows).
+ ],
+ rowMode: 1,
+ resultRows: []
+ },function(ev){
+ const rows = ev.result.resultRows;
+ T.assert(3===rows.length).
+ assert(6===rows[0]);
+ });
+
+ await wtest('exec',{sql: 'delete from t where a>3'});
+
+ await wtest('exec',{
+ sql: 'select count(a) from t',
+ resultRows: []
+ },function(ev){
+ ev = ev.result;
+ T.assert(1===ev.resultRows.length)
+ .assert(2===ev.resultRows[0][0]);
+ });
+
+ await wtest('export', function(ev){
+ ev = ev.result;
+ T.assert('string' === typeof ev.filename)
+ .assert(ev.byteArray instanceof Uint8Array)
+ .assert(ev.byteArray.length > 1024)
+ .assert('application/x-sqlite3' === ev.mimetype);
+ });
+
+ /***** close() tests must come last. *****/
+ await wtest('close',{},function(ev){
+ T.assert('string' === typeof ev.result.filename);
+ });
+
+ await wtest('close', (ev)=>{
+ T.assert(undefined === ev.result.filename);
+ }).finally(()=>logHtml('',"That's all, folks!"));
+ }/*runTests2()*/;
+
+ log("Init complete, but async init bits may still be running.");
+})();
diff --git a/ext/wasm/demo-worker1.html b/ext/wasm/demo-worker1.html
new file mode 100644
index 0000000..c766ffd
--- /dev/null
+++ b/ext/wasm/demo-worker1.html
@@ -0,0 +1,34 @@
+
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>sqlite3-worker1.js tests</title>
+ </head>
+ <body>
+ <header id='titlebar'><span>sqlite3-worker1.js tests</span></header>
+ <!-- emscripten bits -->
+ <figure id="module-spinner">
+ <div class="spinner"></div>
+ <div class='center'><strong>Initializing app...</strong></div>
+ <div class='center'>
+ On a slow internet connection this may take a moment. If this
+ message displays for "a long time", intialization may have
+ failed and the JavaScript console may contain clues as to why.
+ </div>
+ </figure>
+ <div class="emscripten" id="module-status">Downloading...</div>
+ <div class="emscripten">
+ <progress value="0" max="100" id="module-progress" hidden='1'></progress>
+ </div><!-- /emscripten bits -->
+ <div>Most stuff on this page happens in the dev console.</div>
+ <hr>
+ <div id='test-output'></div>
+ <script src="common/SqliteTestUtil.js"></script>
+ <script src="demo-worker1.js"></script>
+ </body>
+</html>
diff --git a/ext/wasm/demo-worker1.js b/ext/wasm/demo-worker1.js
new file mode 100644
index 0000000..60f5e8d
--- /dev/null
+++ b/ext/wasm/demo-worker1.js
@@ -0,0 +1,345 @@
+/*
+ 2022-05-22
+
+ 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.
+
+ ***********************************************************************
+
+ A basic test script for sqlite3-worker1.js.
+
+ Note that the wrapper interface demonstrated in
+ demo-worker1-promiser.js is much easier to use from client code, as it
+ lacks the message-passing acrobatics demonstrated in this file.
+*/
+'use strict';
+(function(){
+ const T = self.SqliteTestUtil;
+ const SW = new Worker("jswasm/sqlite3-worker1.js");
+ const DbState = {
+ id: undefined
+ };
+ const eOutput = document.querySelector('#test-output');
+ const log = console.log.bind(console);
+ const logHtml = function(cssClass,...args){
+ log.apply(this, args);
+ const ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ eOutput.append(ln);
+ };
+ const warn = console.warn.bind(console);
+ const error = console.error.bind(console);
+ const toss = (...args)=>{throw new Error(args.join(' '))};
+
+ SW.onerror = function(event){
+ error("onerror",event);
+ };
+
+ let startTime;
+
+ /**
+ A queue for callbacks which are to be run in response to async
+ DB commands. See the notes in runTests() for why we need
+ this. The event-handling plumbing of this file requires that
+ any DB command which includes a `messageId` property also have
+ a queued callback entry, as the existence of that property in
+ response payloads is how it knows whether or not to shift an
+ entry off of the queue.
+ */
+ const MsgHandlerQueue = {
+ queue: [],
+ id: 0,
+ push: function(type,callback){
+ this.queue.push(callback);
+ return type + '-' + (++this.id);
+ },
+ shift: function(){
+ return this.queue.shift();
+ }
+ };
+
+ const testCount = ()=>{
+ logHtml("","Total test count:",T.counter+". Total time =",(performance.now() - startTime),"ms");
+ };
+
+ const logEventResult = function(ev){
+ const evd = ev.result;
+ logHtml(evd.errorClass ? 'error' : '',
+ "runOneTest",ev.messageId,"Worker time =",
+ (ev.workerRespondTime - ev.workerReceivedTime),"ms.",
+ "Round-trip event time =",
+ (performance.now() - ev.departureTime),"ms.",
+ (evd.errorClass ? evd.message : "")//, JSON.stringify(evd)
+ );
+ };
+
+ const runOneTest = function(eventType, eventArgs, callback){
+ T.assert(eventArgs && 'object'===typeof eventArgs);
+ /* ^^^ that is for the testing and messageId-related code, not
+ a hard requirement of all of the Worker-exposed APIs. */
+ const messageId = MsgHandlerQueue.push(eventType,function(ev){
+ logEventResult(ev);
+ if(callback instanceof Function){
+ callback(ev);
+ testCount();
+ }
+ });
+ const msg = {
+ type: eventType,
+ args: eventArgs,
+ dbId: DbState.id,
+ messageId: messageId,
+ departureTime: performance.now()
+ };
+ log("Posting",eventType,"message to worker dbId="+(DbState.id||'default')+':',msg);
+ SW.postMessage(msg);
+ };
+
+ /** Methods which map directly to onmessage() event.type keys.
+ They get passed the inbound event.data. */
+ const dbMsgHandler = {
+ open: function(ev){
+ DbState.id = ev.dbId;
+ log("open result",ev);
+ },
+ exec: function(ev){
+ log("exec result",ev);
+ },
+ export: function(ev){
+ log("export result",ev);
+ },
+ error: function(ev){
+ error("ERROR from the worker:",ev);
+ logEventResult(ev);
+ },
+ resultRowTest1: function f(ev){
+ if(undefined === f.counter) f.counter = 0;
+ if(null === ev.rowNumber){
+ /* End of result set. */
+ T.assert(undefined === ev.row)
+ .assert(Array.isArray(ev.columnNames))
+ .assert(ev.columnNames.length);
+ }else{
+ T.assert(ev.rowNumber > 0);
+ ++f.counter;
+ }
+ //log("exec() result row:",ev);
+ T.assert(null === ev.rowNumber || 'number' === typeof ev.row.b);
+ }
+ };
+
+ /**
+ "The problem" now is that the test results are async. We
+ know, however, that the messages posted to the worker will
+ be processed in the order they are passed to it, so we can
+ create a queue of callbacks to handle them. The problem
+ with that approach is that it's not error-handling
+ friendly, in that an error can cause us to bypass a result
+ handler queue entry. We have to perform some extra
+ acrobatics to account for that.
+
+ Problem #2 is that we cannot simply start posting events: we
+ first have to post an 'open' event, wait for it to respond, and
+ collect its db ID before continuing. If we don't wait, we may
+ well fire off 10+ messages before the open actually responds.
+ */
+ const runTests2 = function(){
+ const mustNotReach = ()=>{
+ throw new Error("This is not supposed to be reached.");
+ };
+ runOneTest('exec',{
+ sql: ["create table t(a,b);",
+ "insert into t(a,b) values(1,2),(3,4),(5,6)"
+ ],
+ resultRows: [], columnNames: []
+ }, function(ev){
+ ev = ev.result;
+ T.assert(0===ev.resultRows.length)
+ .assert(0===ev.columnNames.length);
+ });
+ runOneTest('exec',{
+ sql: 'select a a, b b from t order by a',
+ resultRows: [], columnNames: [], saveSql:[]
+ }, function(ev){
+ ev = ev.result;
+ T.assert(3===ev.resultRows.length)
+ .assert(1===ev.resultRows[0][0])
+ .assert(6===ev.resultRows[2][1])
+ .assert(2===ev.columnNames.length)
+ .assert('b'===ev.columnNames[1]);
+ });
+ //if(1){ error("Returning prematurely for testing."); return; }
+ runOneTest('exec',{
+ sql: 'select a a, b b from t order by a',
+ resultRows: [], columnNames: [],
+ rowMode: 'object'
+ }, function(ev){
+ ev = ev.result;
+ T.assert(3===ev.resultRows.length)
+ .assert(1===ev.resultRows[0].a)
+ .assert(6===ev.resultRows[2].b)
+ });
+ runOneTest('exec',{sql:'intentional_error'}, mustNotReach);
+ // Ensure that the message-handler queue survives ^^^ that error...
+ runOneTest('exec',{
+ sql:'select 1',
+ resultRows: [],
+ //rowMode: 'array', // array is the default in the Worker interface
+ }, function(ev){
+ ev = ev.result;
+ T.assert(1 === ev.resultRows.length)
+ .assert(1 === ev.resultRows[0][0]);
+ });
+ runOneTest('exec',{
+ sql: 'select a a, b b from t order by a',
+ callback: 'resultRowTest1',
+ rowMode: 'object'
+ }, function(ev){
+ T.assert(3===dbMsgHandler.resultRowTest1.counter);
+ dbMsgHandler.resultRowTest1.counter = 0;
+ });
+ runOneTest('exec',{
+ sql:[
+ "pragma foreign_keys=0;",
+ // ^^^ arbitrary query with no result columns
+ "select a, b from t order by a desc;",
+ "select a from t;"
+ // exec() only honors SELECT results from the first
+ // statement with result columns (regardless of whether
+ // it has any rows).
+ ],
+ rowMode: 1,
+ resultRows: []
+ },function(ev){
+ const rows = ev.result.resultRows;
+ T.assert(3===rows.length).
+ assert(6===rows[0]);
+ });
+ runOneTest('exec',{sql: 'delete from t where a>3'});
+ runOneTest('exec',{
+ sql: 'select count(a) from t',
+ resultRows: []
+ },function(ev){
+ ev = ev.result;
+ T.assert(1===ev.resultRows.length)
+ .assert(2===ev.resultRows[0][0]);
+ });
+ runOneTest('export',{}, function(ev){
+ ev = ev.result;
+ log("export result:",ev);
+ T.assert('string' === typeof ev.filename)
+ .assert(ev.byteArray instanceof Uint8Array)
+ .assert(ev.byteArray.length > 1024)
+ .assert('application/x-sqlite3' === ev.mimetype);
+ });
+ /***** close() tests must come last. *****/
+ runOneTest('close',{unlink:true},function(ev){
+ ev = ev.result;
+ T.assert('string' === typeof ev.filename);
+ });
+ runOneTest('close',{unlink:true},function(ev){
+ ev = ev.result;
+ T.assert(undefined === ev.filename);
+ logHtml('warning',"This is the final test.");
+ });
+ logHtml('warning',"Finished posting tests. Waiting on async results.");
+ };
+
+ const runTests = function(){
+ /**
+ Design decision time: all remaining tests depend on the 'open'
+ command having succeeded. In order to support multiple DBs, the
+ upcoming commands ostensibly have to know the ID of the DB they
+ want to talk to. We have two choices:
+
+ 1) We run 'open' and wait for its response, which contains the
+ db id.
+
+ 2) We have the Worker automatically use the current "default
+ db" (the one which was most recently opened) if no db id is
+ provided in the message. When we do this, the main thread may
+ well fire off _all_ of the test messages before the 'open'
+ actually responds, but because the messages are handled on a
+ FIFO basis, those after the initial 'open' will pick up the
+ "default" db. However, if the open fails, then all pending
+ messages (until next next 'open', at least) except for 'close'
+ will fail and we have no way of cancelling them once they've
+ been posted to the worker.
+
+ Which approach we use below depends on the boolean value of
+ waitForOpen.
+ */
+ const waitForOpen = 1,
+ simulateOpenError = 0 /* if true, the remaining tests will
+ all barf if waitForOpen is
+ false. */;
+ logHtml('',
+ "Sending 'open' message and",(waitForOpen ? "" : "NOT ")+
+ "waiting for its response before continuing.");
+ startTime = performance.now();
+ runOneTest('open', {
+ filename:'testing2.sqlite3',
+ simulateError: simulateOpenError
+ }, function(ev){
+ log("open result",ev);
+ T.assert('testing2.sqlite3'===ev.result.filename)
+ .assert(ev.dbId)
+ .assert(ev.messageId)
+ .assert('string' === typeof ev.result.vfs);
+ DbState.id = ev.dbId;
+ if(waitForOpen) setTimeout(runTests2, 0);
+ });
+ if(!waitForOpen) runTests2();
+ };
+
+ SW.onmessage = function(ev){
+ if(!ev.data || 'object'!==typeof ev.data){
+ warn("Unknown sqlite3-worker message type:",ev);
+ return;
+ }
+ ev = ev.data/*expecting a nested object*/;
+ //log("main window onmessage:",ev);
+ if(ev.result && ev.messageId){
+ /* We're expecting a queued-up callback handler. */
+ const f = MsgHandlerQueue.shift();
+ if('error'===ev.type){
+ dbMsgHandler.error(ev);
+ return;
+ }
+ T.assert(f instanceof Function);
+ f(ev);
+ return;
+ }
+ switch(ev.type){
+ case 'sqlite3-api':
+ switch(ev.result){
+ case 'worker1-ready':
+ log("Message:",ev);
+ self.sqlite3TestModule.setStatus(null);
+ runTests();
+ return;
+ default:
+ warn("Unknown sqlite3-api message type:",ev);
+ return;
+ }
+ default:
+ if(dbMsgHandler.hasOwnProperty(ev.type)){
+ try{dbMsgHandler[ev.type](ev);}
+ catch(err){
+ error("Exception while handling db result message",
+ ev,":",err);
+ }
+ return;
+ }
+ warn("Unknown sqlite3-api message type:",ev);
+ }
+ };
+ log("Init complete, but async init bits may still be running.");
+ log("Installing Worker into global scope SW for dev purposes.");
+ self.SW = SW;
+})();
diff --git a/ext/wasm/dist.make b/ext/wasm/dist.make
new file mode 100644
index 0000000..5d610e3
--- /dev/null
+++ b/ext/wasm/dist.make
@@ -0,0 +1,131 @@
+#!/do/not/make
+#^^^ help emacs select edit mode
+#
+# Intended to include'd by ./GNUmakefile.
+#
+# 'make dist' rules for creating a distribution archive of the WASM/JS
+# pieces, noting that we only build a dist of the built files, not the
+# numerous pieces required to build them.
+#
+# Use 'make snapshot' to create "snapshot" releases. They use a
+# distinctly different zip file and top directory name to distinguish
+# them from release builds.
+#######################################################################
+MAKEFILE.dist := $(lastword $(MAKEFILE_LIST))
+
+########################################################################
+# Chicken/egg situation: we need $(bin.version-info) to get the
+# version info for the archive name, but that binary may not yet be
+# built, and won't be built until we expand the dependencies. Thus we
+# have to use a temporary name for the archive until we can get
+# that binary built.
+ifeq (,$(filter snapshot,$(MAKECMDGOALS)))
+dist-name-prefix := sqlite-wasm
+else
+dist-name-prefix := sqlite-wasm-snapshot-$(shell /usr/bin/date +%Y%m%d)
+endif
+dist-name := $(dist-name-prefix)-TEMP
+
+########################################################################
+# dist.build must be the name of a target which triggers the build of
+# the files to be packed into the dist archive. The intention is that
+# it be one of (o0, o1, o2, o3, os, oz), each of which uses like-named
+# -Ox optimization level flags. The o2 target provides the best
+# overall runtime speeds. The oz target provides slightly slower
+# speeds (roughly 10%) with significantly smaller WASM file
+# sizes. Note that -O2 (the o2 target) results in faster binaries than
+# both -O3 and -Os (the o3 and os targets) in all tests run to
+# date. Our general policy is that we want the smallest binaries for
+# dist zip files, so use the oz build unless there is a compelling
+# reason not to.
+dist.build ?= oz
+
+dist-dir.top := $(dist-name)
+dist-dir.jswasm := $(dist-dir.top)/$(notdir $(dir.dout))
+dist-dir.common := $(dist-dir.top)/common
+dist.top.extras := \
+ demo-123.html demo-123-worker.html demo-123.js \
+ tester1.html tester1-worker.html tester1-esm.html \
+ tester1.js tester1.mjs \
+ demo-jsstorage.html demo-jsstorage.js \
+ demo-worker1.html demo-worker1.js \
+ demo-worker1-promiser.html demo-worker1-promiser.js
+dist.jswasm.extras := $(sqlite3-api.ext.jses) $(sqlite3.wasm)
+dist.common.extras := \
+ $(wildcard $(dir.common)/*.css) \
+ $(dir.common)/SqliteTestUtil.js
+
+.PHONY: dist snapshot
+# DIST_STRIP_COMMENTS $(call)able to be used in stripping C-style
+# from the dist copies of certain files.
+#
+# $1 = source js file
+# $2 = flags for $(bin.stripcomments)
+define DIST_STRIP_COMMENTS
+$(bin.stripccomments) $(2) < $(1) > $(dist-dir.jswasm)/$(notdir $(1)) || exit;
+endef
+# STRIP_K1.js = list of JS files which need to be passed through
+# $(bin.stripcomments) with a single -k flag.
+STRIP_K1.js := $(sqlite3-worker1.js) $(sqlite3-worker1-promiser.js) \
+ $(sqlite3-worker1-bundler-friendly.js) $(sqlite3-worker1-promiser-bundler-friendly.js)
+# STRIP_K2.js = list of JS files which need to be passed through
+# $(bin.stripcomments) with two -k flags.
+STRIP_K2.js := $(sqlite3.js) $(sqlite3.mjs) \
+ $(sqlite3-bundler-friendly.mjs) $(sqlite3-node.mjs)
+########################################################################
+# dist: create the end-user deliverable archive.
+#
+# Maintenance reminder: because dist depends on $(dist.build), and
+# $(dist.build) will depend on clean, having any deps on
+# $(dist-archive) which themselves may be cleaned up by the clean
+# target will lead to grief in parallel builds (-j #). Thus
+# dist's deps must be trimmed to non-generated files or
+# files which are _not_ cleaned up by the clean target.
+#
+# Note that we require $(bin.version-info) in order to figure out the
+# dist file's name, so cannot (without a recursive make) have the
+# target name equal to the archive name.
+dist: \
+ $(bin.stripccomments) $(bin.version-info) \
+ $(dist.build) $(STRIP_K1.js) $(STRIP_K2.js) \
+ $(MAKEFILE) $(MAKEFILE.dist)
+ @echo "Making end-user deliverables..."
+ @rm -fr $(dist-dir.top)
+ @mkdir -p $(dist-dir.jswasm) $(dist-dir.common)
+ @cp -p $(dist.top.extras) $(dist-dir.top)
+ @cp -p README-dist.txt $(dist-dir.top)/README.txt
+ @cp -p index-dist.html $(dist-dir.top)/index.html
+ @cp -p $(dist.jswasm.extras) $(dist-dir.jswasm)
+ @$(foreach JS,$(STRIP_K1.js),$(call DIST_STRIP_COMMENTS,$(JS),-k))
+ @$(foreach JS,$(STRIP_K2.js),$(call DIST_STRIP_COMMENTS,$(JS),-k -k))
+ @cp -p $(dist.common.extras) $(dist-dir.common)
+ @set -e; \
+ vnum=$$($(bin.version-info) --download-version); \
+ vdir=$(dist-name-prefix)-$$vnum; \
+ arczip=$$vdir.zip; \
+ echo "Making $$arczip ..."; \
+ rm -fr $$arczip $$vdir; \
+ mv $(dist-dir.top) $$vdir; \
+ zip -qr $$arczip $$vdir; \
+ rm -fr $$vdir; \
+ ls -la $$arczip; \
+ set +e; \
+ unzip -lv $$arczip || echo "Missing unzip app? Not fatal."
+ifeq (,$(wasm.docs.found))
+snapshot: dist
+ @echo "To upload the snapshot build to the wasm docs server:"; \
+ echo "1) move $(dist-name-prefix)*.zip to the top of a wasm docs checkout."; \
+ echo "2) run 'make uv-sync'"
+else
+snapshot: dist
+ @echo "Moving snapshot to [$(wasm.docs.found)]..."; \
+ mv $(dist-name-prefix)*.zip $(wasm.docs.found)/.
+ @echo "Run 'make uv-sync' from $(wasm.docs.found) to upload it."
+endif
+# We need a separate `clean` rule to account for weirdness in
+# a sub-make, where we get a copy of the $(dist-name) dir
+# copied into the new $(dist-name) dir.
+.PHONY: dist-clean
+clean: dist-clean
+dist-clean:
+ rm -fr $(dist-name) $(wildcard sqlite-wasm-*.zip)
diff --git a/ext/wasm/example_extra_init.c b/ext/wasm/example_extra_init.c
new file mode 100644
index 0000000..b91d757
--- /dev/null
+++ b/ext/wasm/example_extra_init.c
@@ -0,0 +1,23 @@
+/*
+** If the canonical build process finds the file
+** sqlite3_wasm_extra_init.c in the main wasm build directory, it
+** arranges to include that file in the build of sqlite3.wasm and
+** defines SQLITE_EXTRA_INIT=sqlite3_wasm_extra_init.
+**
+** The C file must define the function sqlite3_wasm_extra_init() with
+** this signature:
+**
+** int sqlite3_wasm_extra_init(const char *)
+**
+** and the sqlite3 library will call it with an argument of NULL one
+** time during sqlite3_initialize(). If it returns non-0,
+** initialization of the library will fail.
+*/
+
+#include "sqlite3.h"
+#include <stdio.h>
+
+int sqlite3_wasm_extra_init(const char *z){
+ fprintf(stderr,"%s: %s()\n", __FILE__, __func__);
+ return 0;
+}
diff --git a/ext/wasm/fiddle.make b/ext/wasm/fiddle.make
new file mode 100644
index 0000000..57141d7
--- /dev/null
+++ b/ext/wasm/fiddle.make
@@ -0,0 +1,194 @@
+#!/do/not/make
+#^^^ help emacs select edit mode
+#
+# Intended to include'd by ./GNUmakefile.
+#######################################################################
+MAKEFILE.fiddle := $(lastword $(MAKEFILE_LIST))
+
+########################################################################
+# shell.c and its build flags...
+make-np-0 := make -C $(dir.top) -n -p
+make-np-1 := sed -e 's/(TOP)/(dir.top)/g'
+$(eval $(shell $(make-np-0) | grep -e '^SHELL_OPT ' | $(make-np-1)))
+$(eval $(shell $(make-np-0) | grep -e '^SHELL_SRC ' | $(make-np-1)))
+# ^^^ can't do that in 1 invocation b/c newlines get stripped
+ifeq (,$(SHELL_OPT))
+$(error Could not parse SHELL_OPT from $(dir.top)/Makefile.)
+endif
+ifeq (,$(SHELL_SRC))
+$(error Could not parse SHELL_SRC from $(dir.top)/Makefile.)
+endif
+$(dir.top)/shell.c: $(SHELL_SRC) $(dir.top)/tool/mkshellc.tcl $(sqlite3.c)
+ $(MAKE) -C $(dir.top) shell.c
+# /shell.c
+########################################################################
+
+EXPORTED_FUNCTIONS.fiddle := $(dir.tmp)/EXPORTED_FUNCTIONS.fiddle
+fiddle.emcc-flags = \
+ $(emcc.cflags) $(emcc_opt_full) \
+ --minify 0 \
+ -sALLOW_TABLE_GROWTH \
+ -sABORTING_MALLOC \
+ -sSTRICT_JS=0 \
+ -sENVIRONMENT=web,worker \
+ -sMODULARIZE \
+ -sDYNAMIC_EXECUTION=0 \
+ -sWASM_BIGINT=$(emcc.WASM_BIGINT) \
+ -sEXPORT_NAME=$(sqlite3.js.init-func) \
+ -Wno-limited-postlink-optimizations \
+ $(emcc.exportedRuntimeMethods) \
+ -sEXPORTED_FUNCTIONS=@$(abspath $(EXPORTED_FUNCTIONS.fiddle)) \
+ -sEXPORTED_RUNTIME_METHODS=FS,wasmMemory \
+ $(SQLITE_OPT) $(SHELL_OPT) \
+ -DSQLITE_SHELL_FIDDLE
+# -D_POSIX_C_SOURCE is needed for strdup() with emcc
+
+fiddle.EXPORTED_FUNCTIONS.in := \
+ EXPORTED_FUNCTIONS.fiddle.in \
+ $(EXPORTED_FUNCTIONS.api)
+
+$(EXPORTED_FUNCTIONS.fiddle): $(fiddle.EXPORTED_FUNCTIONS.in) $(MAKEFILE.fiddle)
+ sort -u $(fiddle.EXPORTED_FUNCTIONS.in) > $@
+
+fiddle-module.js := $(dir.fiddle)/fiddle-module.js
+fiddle-module.wasm := $(subst .js,.wasm,$(fiddle-module.js))
+fiddle.cses := $(dir.top)/shell.c $(sqlite3-wasm.c)
+
+fiddle.SOAP.js := $(dir.fiddle)/$(notdir $(SOAP.js))
+$(fiddle.SOAP.js): $(SOAP.js)
+ cp $< $@
+
+$(eval $(call call-make-pre-post,fiddle-module,vanilla))
+$(fiddle-module.js): $(MAKEFILE) $(MAKEFILE.fiddle) \
+ $(EXPORTED_FUNCTIONS.fiddle) \
+ $(fiddle.cses) $(pre-post-fiddle-module-vanilla.deps) $(fiddle.SOAP.js)
+ $(emcc.bin) -o $@ $(fiddle.emcc-flags) \
+ $(pre-post-fiddle-module-vanilla.flags) \
+ $(fiddle.cses)
+ $(maybe-wasm-strip) $(fiddle-module.wasm)
+ gzip < $@ > $@.gz
+ gzip < $(fiddle-module.wasm) > $(fiddle-module.wasm).gz
+
+$(dir.fiddle)/fiddle.js.gz: $(dir.fiddle)/fiddle.js
+ gzip < $< > $@
+
+clean: clean-fiddle
+clean-fiddle:
+ rm -f $(fiddle-module.js) $(fiddle-module.js).gz \
+ $(fiddle-module.wasm) $(fiddle-module.wasm).gz \
+ $(dir.fiddle)/$(SOAP.js) \
+ $(dir.fiddle)/fiddle-module.worker.js \
+ EXPORTED_FUNCTIONS.fiddle
+.PHONY: fiddle
+fiddle: $(fiddle-module.js) $(dir.fiddle)/fiddle.js.gz
+all: fiddle
+
+########################################################################
+# fiddle_remote is the remote destination for the fiddle app. It
+# must be a [user@]HOST:/path for rsync.
+# Note that the target "should probably" contain a symlink of
+# index.html -> fiddle.html.
+fiddle_remote ?=
+ifeq (,$(fiddle_remote))
+ifneq (,$(wildcard /home/stephan))
+ fiddle_remote = wh:www/wh/sqlite3/.
+else ifneq (,$(wildcard /home/drh))
+ #fiddle_remote = if appropriate, add that user@host:/path here
+endif
+endif
+push-fiddle: fiddle
+ @if [ x = "x$(fiddle_remote)" ]; then \
+ echo "fiddle_remote must be a [user@]HOST:/path for rsync"; \
+ exit 1; \
+ fi
+ rsync -va fiddle/ $(fiddle_remote)
+# end fiddle remote push
+########################################################################
+
+
+########################################################################
+# Explanation of the emcc build flags follows. Full docs for these can
+# be found at:
+#
+# https://github.com/emscripten-core/emscripten/blob/main/src/settings.js
+#
+# -sENVIRONMENT=web: elides bootstrap code related to non-web JS
+# environments like node.js. Removing this makes the output a tiny
+# tick larger but hypothetically makes it more portable to
+# non-browser JS environments.
+#
+# -sMODULARIZE: changes how the generated code is structured to avoid
+# declaring a global Module object and instead installing a function
+# which loads and initializes the module. The function is named...
+#
+# -sEXPORT_NAME=jsFunctionName (see -sMODULARIZE)
+#
+# -sEXPORTED_RUNTIME_METHODS=@/absolute/path/to/file: a file
+# containing a list of emscripten-supplied APIs, one per line, which
+# must be exported into the generated JS. Must be an absolute path!
+#
+# -sEXPORTED_FUNCTIONS=@/absolute/path/to/file: a file containing a
+# list of C functions, one per line, which must be exported via wasm
+# so they're visible to JS. C symbols names in that file must all
+# start with an underscore for reasons known only to the emcc
+# developers. e.g., _sqlite3_open_v2 and _sqlite3_finalize. Must be
+# an absolute path!
+#
+# -sSTRICT_JS ensures that the emitted JS code includes the 'use
+# strict' option. Note that -sSTRICT is more broadly-scoped and
+# results in build errors.
+#
+# -sALLOW_TABLE_GROWTH is required for (at a minimum) the UDF-binding
+# feature. Without it, JS functions cannot be made to proxy C-side
+# callbacks.
+#
+# -sABORTING_MALLOC causes the JS-bound _malloc() to abort rather than
+# return 0 on OOM. If set to 0 then all code which uses _malloc()
+# must, just like in C, check the result before using it, else
+# they're likely to corrupt the JS/WASM heap by writing to its
+# address of 0. It is, as of this writing, enabled in Emscripten by
+# default but we enable it explicitly in case that default changes.
+#
+# -sDYNAMIC_EXECUTION=0 disables eval() and the Function constructor.
+# If the build runs without these, it's preferable to use this flag
+# because certain execution environments disallow those constructs.
+# This flag is not strictly necessary, however.
+#
+# -sWASM_BIGINT is UNTESTED but "should" allow the int64-using C APIs
+# to work with JS/wasm, insofar as the JS environment supports the
+# BigInt type. That support requires an extremely recent browser:
+# Safari didn't get that support until late 2020.
+#
+# --no-entry: for compiling library code with no main(). If this is
+# not supplied and the code has a main(), it is called as part of the
+# module init process. Note that main() is #if'd out of shell.c
+# (renamed) when building in wasm mode.
+#
+# --pre-js/--post-js=FILE relative or absolute paths to JS files to
+# prepend/append to the emcc-generated bootstrapping JS. It's
+# easier/faster to develop with separate JS files (reduces rebuilding
+# requirements) but certain configurations, namely -sMODULARIZE, may
+# require using at least a --pre-js file. They can be used
+# individually and need not be paired.
+#
+# -O0..-O3 and -Oz: optimization levels affect not only C-style
+# optimization but whether or not the resulting generated JS code
+# gets minified. -O0 compiles _much_ more quickly than -O3 or -Oz,
+# and doesn't minimize any JS code, so is recommended for
+# development. -O3 or -Oz are recommended for deployment, but
+# primarily because -Oz will shrink the wasm file notably. JS-side
+# minification makes little difference in terms of overall
+# distributable size.
+#
+# --minify 0: disables minification of the generated JS code,
+# regardless of optimization level. Minification of the JS has
+# minimal overall effect in the larger scheme of things and results
+# in JS files which can neither be edited nor viewed as text files in
+# Fossil (which flags them as binary because of their extreme line
+# lengths). Interestingly, whether or not the comments in the
+# generated JS file get stripped is unaffected by this setting and
+# depends entirely on the optimization level. Higher optimization
+# levels reduce the size of the JS considerably even without
+# minification.
+#
+########################################################################
diff --git a/ext/wasm/fiddle/emscripten.css b/ext/wasm/fiddle/emscripten.css
new file mode 100644
index 0000000..7e3dc81
--- /dev/null
+++ b/ext/wasm/fiddle/emscripten.css
@@ -0,0 +1,24 @@
+/* emcscript-related styling, used during the module load/intialization processes... */
+.emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
+div.emscripten { text-align: center; }
+div.emscripten_border { border: 1px solid black; }
+#module-spinner { overflow: visible; }
+#module-spinner > * {
+ margin-top: 1em;
+}
+.spinner {
+ height: 50px;
+ width: 50px;
+ margin: 0px auto;
+ animation: rotation 0.8s linear infinite;
+ border-left: 10px solid rgb(0,150,240);
+ border-right: 10px solid rgb(0,150,240);
+ border-bottom: 10px solid rgb(0,150,240);
+ border-top: 10px solid rgb(100,0,200);
+ border-radius: 100%;
+ background-color: rgb(200,100,250);
+}
+@keyframes rotation {
+ from {transform: rotate(0deg);}
+ to {transform: rotate(360deg);}
+}
diff --git a/ext/wasm/fiddle/fiddle-worker.js b/ext/wasm/fiddle/fiddle-worker.js
new file mode 100644
index 0000000..67f6100
--- /dev/null
+++ b/ext/wasm/fiddle/fiddle-worker.js
@@ -0,0 +1,384 @@
+/*
+ 2022-05-20
+
+ 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 is the JS Worker file for the sqlite3 fiddle app. It loads the
+ sqlite3 wasm module and offers access to the db via the Worker
+ message-passing interface.
+
+ Forewarning: this API is still very much Under Construction and
+ subject to any number of changes as experience reveals what those
+ need to be.
+
+ Because we can have only a single message handler, as opposed to an
+ arbitrary number of discrete event listeners like with DOM elements,
+ we have to define a lower-level message API. Messages abstractly
+ look like:
+
+ { type: string, data: type-specific value }
+
+ Where 'type' is used for dispatching and 'data' is a
+ 'type'-dependent value.
+
+ The 'type' values expected by each side of the main/worker
+ connection vary. The types are described below but subject to
+ change at any time as this experiment evolves.
+
+ Workers-to-Main types
+
+ - stdout, stderr: indicate stdout/stderr output from the wasm
+ layer. The data property is the string of the output, noting
+ that the emscripten binding emits these one line at a time. Thus,
+ if a C-side puts() emits multiple lines in a single call, the JS
+ side will see that as multiple calls. Example:
+
+ {type:'stdout', data: 'Hi, world.'}
+
+ - module: Status text. This is intended to alert the main thread
+ about module loading status so that, e.g., the main thread can
+ update a progress widget and DTRT when the module is finished
+ loading and available for work. Status messages come in the form
+
+ {type:'module', data:{
+ type:'status',
+ data: {text:string|null, step:1-based-integer}
+ }
+
+ with an incrementing step value for each subsequent message. When
+ the module loading is complete, a message with a text value of
+ null is posted.
+
+ - working: data='start'|'end'. Indicates that work is about to be
+ sent to the module or has just completed. This can be used, e.g.,
+ to disable UI elements which should not be activated while work
+ is pending. Example:
+
+ {type:'working', data:'start'}
+
+ Main-to-Worker types:
+
+ - shellExec: data=text to execute as if it had been entered in the
+ sqlite3 CLI shell app (as opposed to sqlite3_exec()). This event
+ causes the worker to emit a 'working' event (data='start') before
+ it starts and a 'working' event (data='end') when it finished. If
+ called while work is currently being executed it emits stderr
+ message instead of doing actual work, as the underlying db cannot
+ handle concurrent tasks. Example:
+
+ {type:'shellExec', data: 'select * from sqlite_master'}
+
+ - More TBD as the higher-level db layer develops.
+*/
+
+/*
+ Apparent browser(s) bug: console messages emitted may be duplicated
+ in the console, even though they're provably only run once. See:
+
+ https://stackoverflow.com/questions/49659464
+
+ Noting that it happens in Firefox as well as Chrome. Harmless but
+ annoying.
+*/
+"use strict";
+(function(){
+ /**
+ Posts a message in the form {type,data}. If passed more than 2
+ args, the 3rd must be an array of "transferable" values to pass
+ as the 2nd argument to postMessage(). */
+ const wMsg =
+ (type,data,transferables)=>{
+ postMessage({type, data}, transferables || []);
+ };
+ const stdout = (...args)=>wMsg('stdout', args);
+ const stderr = (...args)=>wMsg('stderr', args);
+ const toss = (...args)=>{
+ throw new Error(args.join(' '));
+ };
+ const fixmeOPFS = "(FIXME: won't work with OPFS-over-sqlite3_vfs.)";
+ let sqlite3 /* gets assigned when the wasm module is loaded */;
+
+ self.onerror = function(/*message, source, lineno, colno, error*/) {
+ const err = arguments[4];
+ if(err && 'ExitStatus'==err.name){
+ /* This is relevant for the sqlite3 shell binding but not the
+ lower-level binding. */
+ fiddleModule.isDead = true;
+ stderr("FATAL ERROR:", err.message);
+ stderr("Restarting the app requires reloading the page.");
+ wMsg('error', err);
+ }
+ console.error(err);
+ fiddleModule.setStatus('Exception thrown, see JavaScript console: '+err);
+ };
+
+ const Sqlite3Shell = {
+ /** Returns the name of the currently-opened db. */
+ dbFilename: function f(){
+ if(!f._) f._ = sqlite3.wasm.xWrap('fiddle_db_filename', "string", ['string']);
+ return f._(0);
+ },
+ dbHandle: function f(){
+ if(!f._) f._ = sqlite3.wasm.xWrap("fiddle_db_handle", "sqlite3*");
+ return f._();
+ },
+ dbIsOpfs: function f(){
+ return sqlite3.opfs && sqlite3.capi.sqlite3_js_db_uses_vfs(
+ this.dbHandle(), "opfs"
+ );
+ },
+ runMain: function f(){
+ if(f.argv) return 0===f.argv.rc;
+ const dbName = "/fiddle.sqlite3";
+ f.argv = [
+ 'sqlite3-fiddle.wasm',
+ '-bail', '-safe',
+ dbName
+ /* Reminder: because of how we run fiddle, we have to ensure
+ that any argv strings passed to its main() are valid until
+ the wasm environment shuts down. */
+ ];
+ const capi = sqlite3.capi, wasm = sqlite3.wasm;
+ /* We need to call sqlite3_shutdown() in order to avoid numerous
+ legitimate warnings from the shell about it being initialized
+ after sqlite3_initialize() has been called. This means,
+ however, that any initialization done by the JS code may need
+ to be re-done (e.g. re-registration of dynamically-loaded
+ VFSes). We need a more generic approach to running such
+ init-level code. */
+ capi.sqlite3_shutdown();
+ f.argv.pArgv = wasm.allocMainArgv(f.argv);
+ f.argv.rc = wasm.exports.fiddle_main(
+ f.argv.length, f.argv.pArgv
+ );
+ if(f.argv.rc){
+ stderr("Fatal error initializing sqlite3 shell.");
+ fiddleModule.isDead = true;
+ return false;
+ }
+ stdout("SQLite version", capi.sqlite3_libversion(),
+ capi.sqlite3_sourceid().substr(0,19));
+ stdout('Welcome to the "fiddle" shell.');
+ if(sqlite3.opfs){
+ stdout("\nOPFS is available. To open a persistent db, use:\n\n",
+ " .open file:name?vfs=opfs\n\nbut note that some",
+ "features (e.g. upload) do not yet work with OPFS.");
+ sqlite3.opfs.registerVfs();
+ }
+ stdout('\nEnter ".help" for usage hints.');
+ this.exec([ // initialization commands...
+ '.nullvalue NULL',
+ '.headers on'
+ ].join('\n'));
+ return true;
+ },
+ /**
+ Runs the given text through the shell as if it had been typed
+ in by a user. Fires a working/start event before it starts and
+ working/end event when it finishes.
+ */
+ exec: function f(sql){
+ if(!f._){
+ if(!this.runMain()) return;
+ f._ = sqlite3.wasm.xWrap('fiddle_exec', null, ['string']);
+ }
+ if(fiddleModule.isDead){
+ stderr("shell module has exit()ed. Cannot run SQL.");
+ return;
+ }
+ wMsg('working','start');
+ try {
+ if(f._running){
+ stderr('Cannot run multiple commands concurrently.');
+ }else if(sql){
+ if(Array.isArray(sql)) sql = sql.join('');
+ f._running = true;
+ f._(sql);
+ }
+ }finally{
+ delete f._running;
+ wMsg('working','end');
+ }
+ },
+ resetDb: function f(){
+ if(!f._) f._ = sqlite3.wasm.xWrap('fiddle_reset_db', null);
+ stdout("Resetting database.");
+ f._();
+ stdout("Reset",this.dbFilename());
+ },
+ /* Interrupt can't work: this Worker is tied up working, so won't get the
+ interrupt event which would be needed to perform the interrupt. */
+ interrupt: function f(){
+ if(!f._) f._ = sqlite3.wasm.xWrap('fiddle_interrupt', null);
+ stdout("Requesting interrupt.");
+ f._();
+ }
+ };
+
+ self.onmessage = function f(ev){
+ ev = ev.data;
+ if(!f.cache){
+ f.cache = {
+ prevFilename: null
+ };
+ }
+ //console.debug("worker: onmessage.data",ev);
+ switch(ev.type){
+ case 'shellExec': Sqlite3Shell.exec(ev.data); return;
+ case 'db-reset': Sqlite3Shell.resetDb(); return;
+ case 'interrupt': Sqlite3Shell.interrupt(); return;
+ /** Triggers the export of the current db. Fires an
+ event in the form:
+
+ {type:'db-export',
+ data:{
+ filename: name of db,
+ buffer: contents of the db file (Uint8Array),
+ error: on error, a message string and no buffer property.
+ }
+ }
+ */
+ case 'db-export': {
+ const fn = Sqlite3Shell.dbFilename();
+ stdout("Exporting",fn+".");
+ const fn2 = fn ? fn.split(/[/\\]/).pop() : null;
+ try{
+ if(!fn2) toss("DB appears to be closed.");
+ const buffer = sqlite3.capi.sqlite3_js_db_export(
+ Sqlite3Shell.dbHandle()
+ );
+ wMsg('db-export',{filename: fn2, buffer: buffer.buffer}, [buffer.buffer]);
+ }catch(e){
+ console.error("Export failed:",e);
+ /* Post a failure message so that UI elements disabled
+ during the export can be re-enabled. */
+ wMsg('db-export',{
+ filename: fn,
+ error: e.message
+ });
+ }
+ return;
+ }
+ case 'open': {
+ /* Expects: {
+ buffer: ArrayBuffer | Uint8Array,
+ filename: the filename for the db. Any dir part is
+ stripped.
+ }
+ */
+ const opt = ev.data;
+ let buffer = opt.buffer;
+ stderr('open():',fixmeOPFS);
+ if(buffer instanceof ArrayBuffer){
+ buffer = new Uint8Array(buffer);
+ }else if(!(buffer instanceof Uint8Array)){
+ stderr("'open' expects {buffer:Uint8Array} containing an uploaded db.");
+ return;
+ }
+ const fn = (
+ opt.filename
+ ? opt.filename.split(/[/\\]/).pop().replace('"','_')
+ : ("db-"+((Math.random() * 10000000) | 0)+
+ "-"+((Math.random() * 10000000) | 0)+".sqlite3")
+ );
+ try {
+ /* We cannot delete the existing db file until the new one
+ is installed, which means that we risk overflowing our
+ quota (if any) by having both the previous and current
+ db briefly installed in the virtual filesystem. */
+ const fnAbs = '/'+fn;
+ const oldName = Sqlite3Shell.dbFilename();
+ if(oldName && oldName===fnAbs){
+ /* We cannot create the replacement file while the current file
+ is opened, nor does the shell have a .close command, so we
+ must temporarily switch to another db... */
+ Sqlite3Shell.exec('.open :memory:');
+ fiddleModule.FS.unlink(fnAbs);
+ }
+ fiddleModule.FS.createDataFile("/", fn, buffer, true, true);
+ Sqlite3Shell.exec('.open "'+fnAbs+'"');
+ if(oldName && oldName!==fnAbs){
+ try{fiddleModule.fsUnlink(oldName)}
+ catch(e){/*ignored*/}
+ }
+ stdout("Replaced DB with",fn+".");
+ }catch(e){
+ stderr("Error installing db",fn+":",e.message);
+ }
+ return;
+ }
+ };
+ console.warn("Unknown fiddle-worker message type:",ev);
+ };
+
+ /**
+ emscripten module for use with build mode -sMODULARIZE.
+ */
+ const fiddleModule = {
+ print: stdout,
+ printErr: stderr,
+ /**
+ Intercepts status updates from the emscripting module init
+ and fires worker events with a type of 'status' and a
+ payload of:
+
+ {
+ text: string | null, // null at end of load process
+ step: integer // starts at 1, increments 1 per call
+ }
+
+ We have no way of knowing in advance how many steps will
+ be processed/posted, so creating a "percentage done" view is
+ not really practical. One can be approximated by giving it a
+ current value of message.step and max value of message.step+1,
+ though.
+
+ When work is finished, a message with a text value of null is
+ submitted.
+
+ After a message with text==null is posted, the module may later
+ post messages about fatal problems, e.g. an exit() being
+ triggered, so it is recommended that UI elements for posting
+ status messages not be outright removed from the DOM when
+ text==null, and that they instead be hidden until/unless
+ text!=null.
+ */
+ setStatus: function f(text){
+ if(!f.last) f.last = { step: 0, text: '' };
+ else if(text === f.last.text) return;
+ f.last.text = text;
+ wMsg('module',{
+ type:'status',
+ data:{step: ++f.last.step, text: text||null}
+ });
+ }
+ };
+
+ importScripts('fiddle-module.js'+self.location.search);
+ /**
+ initFiddleModule() is installed via fiddle-module.js due to
+ building with:
+
+ emcc ... -sMODULARIZE=1 -sEXPORT_NAME=initFiddleModule
+ */
+ sqlite3InitModule(fiddleModule).then((_sqlite3)=>{
+ sqlite3 = _sqlite3;
+ console.warn("Installing sqlite3 module globally (in Worker)",
+ "for use in the dev console.", sqlite3);
+ globalThis.sqlite3 = sqlite3;
+ const dbVfs = sqlite3.wasm.xWrap('fiddle_db_vfs', "*", ['string']);
+ fiddleModule.fsUnlink = (fn)=>{
+ return sqlite3.wasm.sqlite3_wasm_vfs_unlink(dbVfs(0), fn);
+ };
+ wMsg('fiddle-ready');
+ }).catch(e=>{
+ console.error("Fiddle worker init failed:",e);
+ });
+})();
diff --git a/ext/wasm/fiddle/fiddle.js b/ext/wasm/fiddle/fiddle.js
new file mode 100644
index 0000000..2a3d174
--- /dev/null
+++ b/ext/wasm/fiddle/fiddle.js
@@ -0,0 +1,815 @@
+/*
+ 2022-05-20
+
+ 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 is the main entry point for the sqlite3 fiddle app. It sets up the
+ various UI bits, loads a Worker for the db connection, and manages the
+ communication between the UI and worker.
+*/
+(function(){
+ 'use strict';
+ /* Recall that the 'self' symbol, except where locally
+ overwritten, refers to the global window or worker object. */
+
+ const storage = (function(NS/*namespace object in which to store this module*/){
+ /* Pedantic licensing note: this code originated in the Fossil SCM
+ source tree, where it has a different license, but the person who
+ ported it into sqlite is the same one who wrote it for fossil. */
+ 'use strict';
+ NS = NS||{};
+
+ /**
+ This module provides a basic wrapper around localStorage
+ or sessionStorage or a dummy proxy object if neither
+ of those are available.
+ */
+ const tryStorage = function f(obj){
+ if(!f.key) f.key = 'storage.access.check';
+ try{
+ obj.setItem(f.key, 'f');
+ const x = obj.getItem(f.key);
+ obj.removeItem(f.key);
+ if(x!=='f') throw new Error(f.key+" failed")
+ return obj;
+ }catch(e){
+ return undefined;
+ }
+ };
+
+ /** Internal storage impl for this module. */
+ const $storage =
+ tryStorage(window.localStorage)
+ || tryStorage(window.sessionStorage)
+ || tryStorage({
+ // A basic dummy xyzStorage stand-in
+ $$$:{},
+ setItem: function(k,v){this.$$$[k]=v},
+ getItem: function(k){
+ return this.$$$.hasOwnProperty(k) ? this.$$$[k] : undefined;
+ },
+ removeItem: function(k){delete this.$$$[k]},
+ clear: function(){this.$$$={}}
+ });
+
+ /**
+ For the dummy storage we need to differentiate between
+ $storage and its real property storage for hasOwnProperty()
+ to work properly...
+ */
+ const $storageHolder = $storage.hasOwnProperty('$$$') ? $storage.$$$ : $storage;
+
+ /**
+ A prefix which gets internally applied to all storage module
+ property keys so that localStorage and sessionStorage across the
+ same browser profile instance do not "leak" across multiple apps
+ being hosted by the same origin server. Such cross-polination is
+ still there but, with this key prefix applied, it won't be
+ immediately visible via the storage API.
+
+ With this in place we can justify using localStorage instead of
+ sessionStorage.
+
+ One implication of using localStorage and sessionStorage is that
+ their scope (the same "origin" and client application/profile)
+ allows multiple apps on the same origin to use the same
+ storage. Thus /appA/foo could then see changes made via
+ /appB/foo. The data do not cross user- or browser boundaries,
+ though, so it "might" arguably be called a
+ feature. storageKeyPrefix was added so that we can sandbox that
+ state for each separate app which shares an origin.
+
+ See: https://fossil-scm.org/forum/forumpost/4afc4d34de
+
+ Sidebar: it might seem odd to provide a key prefix and stick all
+ properties in the topmost level of the storage object. We do that
+ because adding a layer of object to sandbox each app would mean
+ (de)serializing that whole tree on every storage property change.
+ e.g. instead of storageObject.projectName.foo we have
+ storageObject[storageKeyPrefix+'foo']. That's soley for
+ efficiency's sake (in terms of battery life and
+ environment-internal storage-level effort).
+ */
+ const storageKeyPrefix = (
+ $storageHolder===$storage/*localStorage or sessionStorage*/
+ ? (
+ (NS.config ?
+ (NS.config.projectCode || NS.config.projectName
+ || NS.config.shortProjectName)
+ : false)
+ || window.location.pathname
+ )+'::' : (
+ '' /* transient storage */
+ )
+ );
+
+ /**
+ A proxy for localStorage or sessionStorage or a
+ page-instance-local proxy, if neither one is availble.
+
+ Which exact storage implementation is uses is unspecified, and
+ apps must not rely on it.
+ */
+ NS.storage = {
+ storageKeyPrefix: storageKeyPrefix,
+ /** Sets the storage key k to value v, implicitly converting
+ it to a string. */
+ set: (k,v)=>$storage.setItem(storageKeyPrefix+k,v),
+ /** Sets storage key k to JSON.stringify(v). */
+ setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)),
+ /** Returns the value for the given storage key, or
+ dflt if the key is not found in the storage. */
+ get: (k,dflt)=>$storageHolder.hasOwnProperty(
+ storageKeyPrefix+k
+ ) ? $storage.getItem(storageKeyPrefix+k) : dflt,
+ /** Returns true if the given key has a value of "true". If the
+ key is not found, it returns true if the boolean value of dflt
+ is "true". (Remember that JS persistent storage values are all
+ strings.) */
+ getBool: function(k,dflt){
+ return 'true'===this.get(k,''+(!!dflt));
+ },
+ /** Returns the JSON.parse()'d value of the given
+ storage key's value, or dflt is the key is not
+ found or JSON.parse() fails. */
+ getJSON: function f(k,dflt){
+ try {
+ const x = this.get(k,f);
+ return x===f ? dflt : JSON.parse(x);
+ }
+ catch(e){return dflt}
+ },
+ /** Returns true if the storage contains the given key,
+ else false. */
+ contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k),
+ /** Removes the given key from the storage. Returns this. */
+ remove: function(k){
+ $storage.removeItem(storageKeyPrefix+k);
+ return this;
+ },
+ /** Clears ALL keys from the storage. Returns this. */
+ clear: function(){
+ this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k));
+ return this;
+ },
+ /** Returns an array of all keys currently in the storage. */
+ keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)),
+ /** Returns true if this storage is transient (only available
+ until the page is reloaded), indicating that fileStorage
+ and sessionStorage are unavailable. */
+ isTransient: ()=>$storageHolder!==$storage,
+ /** Returns a symbolic name for the current storage mechanism. */
+ storageImplName: function(){
+ if($storage===window.localStorage) return 'localStorage';
+ else if($storage===window.sessionStorage) return 'sessionStorage';
+ else return 'transient';
+ },
+
+ /**
+ Returns a brief help text string for the currently-selected
+ storage type.
+ */
+ storageHelpDescription: function(){
+ return {
+ localStorage: "Browser-local persistent storage with an "+
+ "unspecified long-term lifetime (survives closing the browser, "+
+ "but maybe not a browser upgrade).",
+ sessionStorage: "Storage local to this browser tab, "+
+ "lost if this tab is closed.",
+ "transient": "Transient storage local to this invocation of this page."
+ }[this.storageImplName()];
+ }
+ };
+ return NS.storage;
+ })({})/*storage API setup*/;
+
+
+ /** Name of the stored copy of SqliteFiddle.config. */
+ const configStorageKey = 'sqlite3-fiddle-config';
+
+ /**
+ The SqliteFiddle object is intended to be the primary
+ app-level object for the main-thread side of the sqlite
+ fiddle application. It uses a worker thread to load the
+ sqlite WASM module and communicate with it.
+ */
+ const SF/*local convenience alias*/
+ = window.SqliteFiddle/*canonical name*/ = {
+ /* Config options. */
+ config: {
+ /* If true, SqliteFiddle.echo() will auto-scroll the
+ output widget to the bottom when it receives output,
+ else it won't. */
+ autoScrollOutput: true,
+ /* If true, the output area will be cleared before each
+ command is run, else it will not. */
+ autoClearOutput: false,
+ /* If true, SqliteFiddle.echo() will echo its output to
+ the console, in addition to its normal output widget.
+ That slows it down but is useful for testing. */
+ echoToConsole: false,
+ /* If true, display input/output areas side-by-side. */
+ sideBySide: true,
+ /* If true, swap positions of the input/output areas. */
+ swapInOut: false
+ },
+ /**
+ Emits the given text, followed by a line break, to the
+ output widget. If given more than one argument, they are
+ join()'d together with a space between each. As a special
+ case, if passed a single array, that array is used in place
+ of the arguments array (this is to facilitate receiving
+ lists of arguments via worker events).
+ */
+ echo: function f(text) {
+ /* Maintenance reminder: we currently require/expect a textarea
+ output element. It might be nice to extend this to behave
+ differently if the output element is a non-textarea element,
+ in which case it would need to append the given text as a TEXT
+ node and add a line break. */
+ if(!f._){
+ f._ = document.getElementById('output');
+ f._.value = ''; // clear browser cache
+ }
+ if(arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
+ else if(1===arguments.length && Array.isArray(text)) text = text.join(' ');
+ // These replacements are necessary if you render to raw HTML
+ //text = text.replace(/&/g, "&amp;");
+ //text = text.replace(/</g, "&lt;");
+ //text = text.replace(/>/g, "&gt;");
+ //text = text.replace('\n', '<br>', 'g');
+ if(null===text){/*special case: clear output*/
+ f._.value = '';
+ return;
+ }else if(this.echo._clearPending){
+ delete this.echo._clearPending;
+ f._.value = '';
+ }
+ if(this.config.echoToConsole) console.log(text);
+ if(this.jqTerm) this.jqTerm.echo(text);
+ f._.value += text + "\n";
+ if(this.config.autoScrollOutput){
+ f._.scrollTop = f._.scrollHeight;
+ }
+ },
+ _msgMap: {},
+ /** Adds a worker message handler for messages of the given
+ type. */
+ addMsgHandler: function f(type,callback){
+ if(Array.isArray(type)){
+ type.forEach((t)=>this.addMsgHandler(t, callback));
+ return this;
+ }
+ (this._msgMap.hasOwnProperty(type)
+ ? this._msgMap[type]
+ : (this._msgMap[type] = [])).push(callback);
+ return this;
+ },
+ /** Given a worker message, runs all handlers for msg.type. */
+ runMsgHandlers: function(msg){
+ const list = (this._msgMap.hasOwnProperty(msg.type)
+ ? this._msgMap[msg.type] : false);
+ if(!list){
+ console.warn("No handlers found for message type:",msg);
+ return false;
+ }
+ //console.debug("runMsgHandlers",msg);
+ list.forEach((f)=>f(msg));
+ return true;
+ },
+ /** Removes all message handlers for the given message type. */
+ clearMsgHandlers: function(type){
+ delete this._msgMap[type];
+ return this;
+ },
+ /* Posts a message in the form {type, data} to the db worker. Returns this. */
+ wMsg: function(type,data,transferables){
+ this.worker.postMessage({type, data}, transferables || []);
+ return this;
+ },
+ /**
+ Prompts for confirmation and, if accepted, deletes
+ all content and tables in the (transient) database.
+ */
+ resetDb: function(){
+ if(window.confirm("Really destroy all content and tables "
+ +"in the (transient) db?")){
+ this.wMsg('db-reset');
+ }
+ return this;
+ },
+ /** Stores this object's config in the browser's storage. */
+ storeConfig: function(){
+ storage.setJSON(configStorageKey,this.config);
+ }
+ };
+
+ if(1){ /* Restore SF.config */
+ const storedConfig = storage.getJSON(configStorageKey);
+ if(storedConfig){
+ /* Copy all properties to SF.config which are currently in
+ storedConfig. We don't bother copying any other
+ properties: those have been removed from the app in the
+ meantime. */
+ Object.keys(SF.config).forEach(function(k){
+ if(storedConfig.hasOwnProperty(k)){
+ SF.config[k] = storedConfig[k];
+ }
+ });
+ }
+ }
+
+ SF.worker = new Worker('fiddle-worker.js'+self.location.search);
+ SF.worker.onmessage = (ev)=>SF.runMsgHandlers(ev.data);
+ SF.addMsgHandler(['stdout', 'stderr'], (ev)=>SF.echo(ev.data));
+
+ /* querySelectorAll() proxy */
+ const EAll = function(/*[element=document,] cssSelector*/){
+ return (arguments.length>1 ? arguments[0] : document)
+ .querySelectorAll(arguments[arguments.length-1]);
+ };
+ /* querySelector() proxy */
+ const E = function(/*[element=document,] cssSelector*/){
+ return (arguments.length>1 ? arguments[0] : document)
+ .querySelector(arguments[arguments.length-1]);
+ };
+
+ /** Handles status updates from the Emscripten Module object. */
+ SF.addMsgHandler('module', function f(ev){
+ ev = ev.data;
+ if('status'!==ev.type){
+ console.warn("Unexpected module-type message:",ev);
+ return;
+ }
+ if(!f.ui){
+ f.ui = {
+ status: E('#module-status'),
+ progress: E('#module-progress'),
+ spinner: E('#module-spinner')
+ };
+ }
+ const msg = ev.data;
+ if(f.ui.progres){
+ progress.value = msg.step;
+ progress.max = msg.step + 1/*we don't know how many steps to expect*/;
+ }
+ if(1==msg.step){
+ f.ui.progress.classList.remove('hidden');
+ f.ui.spinner.classList.remove('hidden');
+ }
+ if(msg.text){
+ f.ui.status.classList.remove('hidden');
+ f.ui.status.innerText = msg.text;
+ }else{
+ if(f.ui.progress){
+ f.ui.progress.remove();
+ f.ui.spinner.remove();
+ delete f.ui.progress;
+ delete f.ui.spinner;
+ }
+ f.ui.status.classList.add('hidden');
+ /* The module can post messages about fatal problems,
+ e.g. an exit() being triggered or assertion failure,
+ after the last "load" message has arrived, so
+ leave f.ui.status and message listener intact. */
+ }
+ });
+
+ /**
+ The 'fiddle-ready' event is fired (with no payload) when the
+ wasm module has finished loading. Interestingly, that happens
+ _before_ the final module:status event */
+ SF.addMsgHandler('fiddle-ready', function(){
+ SF.clearMsgHandlers('fiddle-ready');
+ self.onSFLoaded();
+ });
+
+ /**
+ Performs all app initialization which must wait until after the
+ worker module is loaded. This function removes itself when it's
+ called.
+ */
+ self.onSFLoaded = function(){
+ delete this.onSFLoaded;
+ // Unhide all elements which start out hidden
+ EAll('.initially-hidden').forEach((e)=>e.classList.remove('initially-hidden'));
+ E('#btn-reset').addEventListener('click',()=>SF.resetDb());
+ const taInput = E('#input');
+ const btnClearIn = E('#btn-clear');
+ btnClearIn.addEventListener('click',function(){
+ taInput.value = '';
+ },false);
+ // Ctrl-enter and shift-enter both run the current SQL.
+ taInput.addEventListener('keydown',function(ev){
+ if((ev.ctrlKey || ev.shiftKey) && 13 === ev.keyCode){
+ ev.preventDefault();
+ ev.stopPropagation();
+ btnShellExec.click();
+ }
+ }, false);
+ const taOutput = E('#output');
+ const btnClearOut = E('#btn-clear-output');
+ btnClearOut.addEventListener('click',function(){
+ taOutput.value = '';
+ if(SF.jqTerm) SF.jqTerm.clear();
+ },false);
+ const btnShellExec = E('#btn-shell-exec');
+ btnShellExec.addEventListener('click',function(ev){
+ let sql;
+ ev.preventDefault();
+ if(taInput.selectionStart<taInput.selectionEnd){
+ sql = taInput.value.substring(taInput.selectionStart,taInput.selectionEnd).trim();
+ }else{
+ sql = taInput.value.trim();
+ }
+ if(sql) SF.dbExec(sql);
+ },false);
+
+ const btnInterrupt = E("#btn-interrupt");
+ //btnInterrupt.classList.add('hidden');
+ /** To be called immediately before work is sent to the
+ worker. Updates some UI elements. The 'working'/'end'
+ event will apply the inverse, undoing the bits this
+ function does. This impl is not in the 'working'/'start'
+ event handler because that event is given to us
+ asynchronously _after_ we need to have performed this
+ work.
+ */
+ const preStartWork = function f(){
+ if(!f._){
+ const title = E('title');
+ f._ = {
+ btnLabel: btnShellExec.innerText,
+ pageTitle: title,
+ pageTitleOrig: title.innerText
+ };
+ }
+ f._.pageTitle.innerText = "[working...] "+f._.pageTitleOrig;
+ btnShellExec.setAttribute('disabled','disabled');
+ btnInterrupt.removeAttribute('disabled','disabled');
+ };
+
+ /* Sends the given text to the db module to evaluate as if it
+ had been entered in the sqlite3 CLI shell. If it's null or
+ empty, this is a no-op. */
+ SF.dbExec = function f(sql){
+ if(null!==sql && this.config.autoClearOutput){
+ this.echo._clearPending = true;
+ }
+ preStartWork();
+ this.wMsg('shellExec',sql);
+ };
+
+ SF.addMsgHandler('working',function f(ev){
+ switch(ev.data){
+ case 'start': /* See notes in preStartWork(). */; return;
+ case 'end':
+ preStartWork._.pageTitle.innerText = preStartWork._.pageTitleOrig;
+ btnShellExec.innerText = preStartWork._.btnLabel;
+ btnShellExec.removeAttribute('disabled');
+ btnInterrupt.setAttribute('disabled','disabled');
+ return;
+ }
+ console.warn("Unhandled 'working' event:",ev.data);
+ });
+
+ /* For each checkbox with data-csstgt, set up a handler which
+ toggles the given CSS class on the element matching
+ E(data-csstgt). */
+ EAll('input[type=checkbox][data-csstgt]')
+ .forEach(function(e){
+ const tgt = E(e.dataset.csstgt);
+ const cssClass = e.dataset.cssclass || 'error';
+ e.checked = tgt.classList.contains(cssClass);
+ e.addEventListener('change', function(){
+ tgt.classList[
+ this.checked ? 'add' : 'remove'
+ ](cssClass)
+ }, false);
+ });
+ /* For each checkbox with data-config=X, set up a binding to
+ SF.config[X]. These must be set up AFTER data-csstgt
+ checkboxes so that those two states can be synced properly. */
+ EAll('input[type=checkbox][data-config]')
+ .forEach(function(e){
+ const confVal = !!SF.config[e.dataset.config];
+ if(e.checked !== confVal){
+ /* Ensure that data-csstgt mappings (if any) get
+ synced properly. */
+ e.checked = confVal;
+ e.dispatchEvent(new Event('change'));
+ }
+ e.addEventListener('change', function(){
+ SF.config[this.dataset.config] = this.checked;
+ SF.storeConfig();
+ }, false);
+ });
+ /* For each button with data-cmd=X, map a click handler which
+ calls SF.dbExec(X). */
+ const cmdClick = function(){SF.dbExec(this.dataset.cmd);};
+ EAll('button[data-cmd]').forEach(
+ e => e.addEventListener('click', cmdClick, false)
+ );
+
+ btnInterrupt.addEventListener('click',function(){
+ SF.wMsg('interrupt');
+ });
+
+ /** Initiate a download of the db. */
+ const btnExport = E('#btn-export');
+ const eLoadDb = E('#load-db');
+ const btnLoadDb = E('#btn-load-db');
+ btnLoadDb.addEventListener('click', ()=>eLoadDb.click());
+ /**
+ Enables (if passed true) or disables all UI elements which
+ "might," if timed "just right," interfere with an
+ in-progress db import/export/exec operation.
+ */
+ const enableMutatingElements = function f(enable){
+ if(!f._elems){
+ f._elems = [
+ /* UI elements to disable while import/export are
+ running. Normally the export is fast enough
+ that this won't matter, but we really don't
+ want to be reading (from outside of sqlite) the
+ db when the user taps btnShellExec. */
+ btnShellExec, btnExport, eLoadDb
+ ];
+ }
+ f._elems.forEach( enable
+ ? (e)=>e.removeAttribute('disabled')
+ : (e)=>e.setAttribute('disabled','disabled') );
+ };
+ btnExport.addEventListener('click',function(){
+ enableMutatingElements(false);
+ SF.wMsg('db-export');
+ });
+ SF.addMsgHandler('db-export', function(ev){
+ enableMutatingElements(true);
+ ev = ev.data;
+ if(ev.error){
+ SF.echo("Export failed:",ev.error);
+ return;
+ }
+ const blob = new Blob([ev.buffer],
+ {type:"application/x-sqlite3"});
+ const a = document.createElement('a');
+ document.body.appendChild(a);
+ a.href = window.URL.createObjectURL(blob);
+ a.download = ev.filename;
+ a.addEventListener('click',function(){
+ setTimeout(function(){
+ SF.echo("Exported (possibly auto-downloaded):",ev.filename);
+ window.URL.revokeObjectURL(a.href);
+ a.remove();
+ },500);
+ });
+ a.click();
+ });
+ /**
+ Handle load/import of an external db file.
+ */
+ eLoadDb.addEventListener('change',function(){
+ const f = this.files[0];
+ const r = new FileReader();
+ const status = {loaded: 0, total: 0};
+ enableMutatingElements(false);
+ r.addEventListener('loadstart', function(){
+ SF.echo("Loading",f.name,"...");
+ });
+ r.addEventListener('progress', function(ev){
+ SF.echo("Loading progress:",ev.loaded,"of",ev.total,"bytes.");
+ });
+ const that = this;
+ r.addEventListener('load', function(){
+ enableMutatingElements(true);
+ SF.echo("Loaded",f.name+". Opening db...");
+ SF.wMsg('open',{
+ filename: f.name,
+ buffer: this.result
+ }, [this.result]);
+ });
+ r.addEventListener('error',function(){
+ enableMutatingElements(true);
+ SF.echo("Loading",f.name,"failed for unknown reasons.");
+ });
+ r.addEventListener('abort',function(){
+ enableMutatingElements(true);
+ SF.echo("Cancelled loading of",f.name+".");
+ });
+ r.readAsArrayBuffer(f);
+ });
+
+ EAll('fieldset.collapsible').forEach(function(fs){
+ const btnToggle = E(fs,'legend > .fieldset-toggle'),
+ content = EAll(fs,':scope > div');
+ btnToggle.addEventListener('click', function(){
+ fs.classList.toggle('collapsed');
+ content.forEach((d)=>d.classList.toggle('hidden'));
+ }, false);
+ });
+
+ /**
+ Given a DOM element, this routine measures its "effective
+ height", which is the bounding top/bottom range of this element
+ and all of its children, recursively. For some DOM structure
+ cases, a parent may have a reported height of 0 even though
+ children have non-0 sizes.
+
+ Returns 0 if !e or if the element really has no height.
+ */
+ const effectiveHeight = function f(e){
+ if(!e) return 0;
+ if(!f.measure){
+ f.measure = function callee(e, depth){
+ if(!e) return;
+ const m = e.getBoundingClientRect();
+ if(0===depth){
+ callee.top = m.top;
+ callee.bottom = m.bottom;
+ }else{
+ callee.top = m.top ? Math.min(callee.top, m.top) : callee.top;
+ callee.bottom = Math.max(callee.bottom, m.bottom);
+ }
+ Array.prototype.forEach.call(e.children,(e)=>callee(e,depth+1));
+ if(0===depth){
+ //console.debug("measure() height:",e.className, callee.top, callee.bottom, (callee.bottom - callee.top));
+ f.extra += callee.bottom - callee.top;
+ }
+ return f.extra;
+ };
+ }
+ f.extra = 0;
+ f.measure(e,0);
+ return f.extra;
+ };
+
+ /**
+ Returns a function, that, as long as it continues to be invoked,
+ will not be triggered. The function will be called after it stops
+ being called for N milliseconds. If `immediate` is passed, call
+ the callback immediately and hinder future invocations until at
+ least the given time has passed.
+
+ If passed only 1 argument, or passed a falsy 2nd argument,
+ the default wait time set in this function's $defaultDelay
+ property is used.
+
+ Source: underscore.js, by way of https://davidwalsh.name/javascript-debounce-function
+ */
+ const debounce = function f(func, wait, immediate) {
+ var timeout;
+ if(!wait) wait = f.$defaultDelay;
+ return function() {
+ const context = this, args = Array.prototype.slice.call(arguments);
+ const later = function() {
+ timeout = undefined;
+ if(!immediate) func.apply(context, args);
+ };
+ const callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if(callNow) func.apply(context, args);
+ };
+ };
+ debounce.$defaultDelay = 500 /*arbitrary*/;
+
+ const ForceResizeKludge = (function(){
+ /* Workaround for Safari mayhem regarding use of vh CSS
+ units.... We cannot use vh units to set the main view
+ size because Safari chokes on that, so we calculate
+ that height here. Larger than ~95% is too big for
+ Firefox on Android, causing the input area to move
+ off-screen. */
+ const appViews = EAll('.app-view');
+ const elemsToCount = [
+ /* Elements which we need to always count in the
+ visible body size. */
+ E('body > header'),
+ E('body > footer')
+ ];
+ const resized = function f(){
+ if(f.$disabled) return;
+ const wh = window.innerHeight;
+ var ht;
+ var extra = 0;
+ elemsToCount.forEach((e)=>e ? extra += effectiveHeight(e) : false);
+ ht = wh - extra;
+ appViews.forEach(function(e){
+ e.style.height =
+ e.style.maxHeight = [
+ "calc(", (ht>=100 ? ht : 100), "px",
+ " - 2em"/*fudge value*/,")"
+ /* ^^^^ hypothetically not needed, but both
+ Chrome/FF on Linux will force scrollbars on the
+ body if this value is too small. */
+ ].join('');
+ });
+ };
+ resized.$disabled = true/*gets deleted when setup is finished*/;
+ window.addEventListener('resize', debounce(resized, 250), false);
+ return resized;
+ })();
+
+ /** Set up a selection list of examples */
+ (function(){
+ const xElem = E('#select-examples');
+ const examples = [
+ {name: "Help", sql: [
+ "-- ================================================\n",
+ "-- Use ctrl-enter or shift-enter to execute sqlite3\n",
+ "-- shell commands and SQL.\n",
+ "-- If a subset of the text is currently selected,\n",
+ "-- only that part is executed.\n",
+ "-- ================================================\n",
+ ".help\n"
+ ]},
+ //{name: "Timer on", sql: ".timer on"},
+ // ^^^ re-enable if emscripten re-enables getrusage()
+ {name: "Setup table T", sql:[
+ ".nullvalue NULL\n",
+ "CREATE TABLE t(a,b);\n",
+ "INSERT INTO t(a,b) VALUES('abc',123),('def',456),(NULL,789),('ghi',012);\n",
+ "SELECT * FROM t;\n"
+ ]},
+ {name: "Table list", sql: ".tables"},
+ {name: "Box Mode", sql: ".mode box"},
+ {name: "JSON Mode", sql: ".mode json"},
+ {name: "Mandlebrot", sql:[
+ "WITH RECURSIVE",
+ " xaxis(x) AS (VALUES(-2.0) UNION ALL SELECT x+0.05 FROM xaxis WHERE x<1.2),\n",
+ " yaxis(y) AS (VALUES(-1.0) UNION ALL SELECT y+0.1 FROM yaxis WHERE y<1.0),\n",
+ " m(iter, cx, cy, x, y) AS (\n",
+ " SELECT 0, x, y, 0.0, 0.0 FROM xaxis, yaxis\n",
+ " UNION ALL\n",
+ " SELECT iter+1, cx, cy, x*x-y*y + cx, 2.0*x*y + cy FROM m \n",
+ " WHERE (x*x + y*y) < 4.0 AND iter<28\n",
+ " ),\n",
+ " m2(iter, cx, cy) AS (\n",
+ " SELECT max(iter), cx, cy FROM m GROUP BY cx, cy\n",
+ " ),\n",
+ " a(t) AS (\n",
+ " SELECT group_concat( substr(' .+*#', 1+min(iter/7,4), 1), '') \n",
+ " FROM m2 GROUP BY cy\n",
+ " )\n",
+ "SELECT group_concat(rtrim(t),x'0a') as Mandelbrot FROM a;\n",
+ ]}
+ ];
+ const newOpt = function(lbl,val){
+ const o = document.createElement('option');
+ if(Array.isArray(val)) val = val.join('');
+ o.value = val;
+ if(!val) o.setAttribute('disabled',true);
+ o.appendChild(document.createTextNode(lbl));
+ xElem.appendChild(o);
+ };
+ newOpt("Examples (replaces input!)");
+ examples.forEach((o)=>newOpt(o.name, o.sql));
+ //xElem.setAttribute('disabled',true);
+ xElem.selectedIndex = 0;
+ xElem.addEventListener('change', function(){
+ taInput.value = '-- ' +
+ this.selectedOptions[0].innerText +
+ '\n' + this.value;
+ SF.dbExec(this.value);
+ });
+ })()/* example queries */;
+
+ //SF.echo(null/*clear any output generated by the init process*/);
+ if(window.jQuery && window.jQuery.terminal){
+ /* Set up the terminal-style view... */
+ const eTerm = window.jQuery('#view-terminal').empty();
+ SF.jqTerm = eTerm.terminal(SF.dbExec.bind(SF),{
+ prompt: 'sqlite> ',
+ greetings: false /* note that the docs incorrectly call this 'greeting' */
+ });
+ /* Set up a button to toggle the views... */
+ const head = E('header#titlebar');
+ const btnToggleView = document.createElement('button');
+ btnToggleView.appendChild(document.createTextNode("Toggle View"));
+ head.appendChild(btnToggleView);
+ btnToggleView.addEventListener('click',function f(){
+ EAll('.app-view').forEach(e=>e.classList.toggle('hidden'));
+ if(document.body.classList.toggle('terminal-mode')){
+ ForceResizeKludge();
+ }
+ }, false);
+ btnToggleView.click()/*default to terminal view*/;
+ }
+ SF.echo('This experimental app is provided in the hope that it',
+ 'may prove interesting or useful but is not an officially',
+ 'supported deliverable of the sqlite project. It is subject to',
+ 'any number of changes or outright removal at any time.\n');
+ const urlParams = new URL(self.location.href).searchParams;
+ SF.dbExec(urlParams.get('sql') || null);
+ delete ForceResizeKludge.$disabled;
+ ForceResizeKludge();
+ }/*onSFLoaded()*/;
+})();
diff --git a/ext/wasm/fiddle/index.html b/ext/wasm/fiddle/index.html
new file mode 100644
index 0000000..272f1ac
--- /dev/null
+++ b/ext/wasm/fiddle/index.html
@@ -0,0 +1,278 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>SQLite3 Fiddle</title>
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <!-- to add a togglable terminal-style view, uncomment the following
+ two lines and ensure that these files are on the web server. -->
+ <!--script src="jqterm/jqterm-bundle.min.js"></script>
+ <link rel="stylesheet" href="jqterm/jquery.terminal.min.css"/-->
+ <link rel="stylesheet" href="emscripten.css"/>
+ <style>
+ /* The following styles are for app-level use. */
+ :root {
+ --sqlite-blue: #044a64;
+ --textarea-color1: #044a64;
+ --textarea-color2: white;
+ }
+ textarea {
+ font-family: monospace;
+ flex: 1 1 auto;
+ background-color: var(--textarea-color1);
+ color: var(--textarea-color2);
+ }
+ textarea#input {
+ color: var(--textarea-color1);
+ background-color: var(--textarea-color2);
+ }
+ header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: var(--sqlite-blue);
+ color: white;
+ font-size: 120%;
+ font-weight: bold;
+ border-radius: 0.25em;
+ padding: 0.2em 0.5em;
+ }
+ header > .powered-by {
+ font-size: 80%;
+ }
+ header a, header a:visited, header a:hover {
+ color: inherit;
+ }
+ #main-wrapper {
+ display: flex;
+ flex-direction: column-reverse;
+ flex: 1 1 auto;
+ margin: 0.5em 0;
+ overflow: hidden;
+ }
+ #main-wrapper.side-by-side {
+ flex-direction: row;
+ }
+ #main-wrapper.side-by-side > fieldset {
+ margin-left: 0.25em;
+ margin-right: 0.25em;
+ }
+ #main-wrapper:not(.side-by-side) > fieldset {
+ margin-bottom: 0.25em;
+ }
+ #main-wrapper.swapio {
+ flex-direction: column;
+ }
+ #main-wrapper.side-by-side.swapio {
+ flex-direction: row-reverse;
+ }
+ .zone-wrapper{
+ display: flex;
+ margin: 0;
+ flex: 1 1 0%;
+ border-radius: 0.5em;
+ min-width: inherit/*important: resolves inability to scroll fieldset child element!*/;
+ padding: 0.35em 0 0 0;
+ }
+ .zone-wrapper textarea {
+ border-radius: 0.5em;
+ flex: 1 1 auto;
+ /*min/max width resolve an inexplicable margin on the RHS. The -1em
+ is for the padding, else we overlap the parent boundaries.*/
+ /*min-width: calc(100% - 1em);
+ max-width: calc(100% - 1em);
+ padding: 0 0.5em;*/
+ }
+
+ .zone-wrapper.input { flex: 10 1 auto; }
+ .zone-wrapper.output { flex: 20 1 auto; }
+ .zone-wrapper > div {
+ display:flex;
+ flex: 1 1 0%;
+ }
+ .zone-wrapper.output {}
+ .button-bar {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ align-content: space-between;
+ justify-content: flex-start;
+ }
+ .button-bar > * {
+ margin: 0.05em 0.5em 0.05em 0;
+ flex: 0 1 auto;
+ align-self: auto;
+ }
+ label[for] {
+ cursor: pointer;
+ }
+ .error {
+ color: red;
+ background-color: yellow;
+ }
+ .hidden, .initially-hidden {
+ position: absolute !important;
+ opacity: 0 !important;
+ pointer-events: none !important;
+ display: none !important;
+ }
+ fieldset {
+ border-radius: 0.5em;
+ border: 1px inset;
+ padding: 0.25em;
+ }
+ fieldset.options {
+ font-size: 80%;
+ margin-top: 0.5em;
+ }
+ fieldset:not(.options) > legend {
+ font-size: 80%;
+ }
+ fieldset.options > div {
+ display: flex;
+ flex-wrap: wrap;
+ }
+ fieldset button {
+ font-size: inherit;
+ }
+ fieldset.collapsible > legend > .fieldset-toggle::after {
+ content: " [hide]";
+ position: relative;
+ }
+ fieldset.collapsible.collapsed > legend > .fieldset-toggle::after {
+ content: " [show]";
+ position: relative;
+ }
+ span.labeled-input {
+ padding: 0.25em;
+ margin: 0.05em 0.25em;
+ border-radius: 0.25em;
+ white-space: nowrap;
+ background: #0002;
+ display: flex;
+ align-items: center;
+ }
+ span.labeled-input > *:nth-child(2) {
+ margin-left: 0.3em;
+ }
+ .center { text-align: center; }
+ body.terminal-mode {
+ max-height: calc(100% - 2em);
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ }
+ #view-terminal {}
+ .app-view {
+ flex: 20 1 auto;
+ }
+ #view-split {
+ display: flex;
+ flex-direction: column-reverse;
+ }
+ </style>
+ </head>
+ <body>
+ <header id='titlebar'>
+ <span>SQLite3 Fiddle</span>
+ <span class='powered-by'>Powered by
+ <a href='https://sqlite.org'>SQLite3</a></span>
+ </header>
+ <!-- emscripten bits -->
+ <figure id="module-spinner">
+ <div class="spinner"></div>
+ <div class='center'><strong>Initializing app...</strong></div>
+ <div class='center'>
+ On a slow internet connection this may take a moment. If this
+ message displays for "a long time", intialization may have
+ failed and the JavaScript console may contain clues as to why.
+ </div>
+ </figure>
+ <div class="emscripten" id="module-status">Downloading...</div>
+ <div class="emscripten">
+ <progress value="0" max="100" id="module-progress" hidden='1'></progress>
+ </div><!-- /emscripten bits -->
+
+ <div id='view-terminal' class='app-view hidden initially-hidden'>
+ This is a placeholder for a terminal-like view which is not in
+ the default build.
+ </div>
+
+ <div id='view-split' class='app-view initially-hidden'>
+ <fieldset class='options collapsible'>
+ <legend><button class='fieldset-toggle'>Options</button></legend>
+ <div class=''>
+ <span class='labeled-input'>
+ <input type='checkbox' id='opt-cb-sbs'
+ data-csstgt='#main-wrapper'
+ data-cssclass='side-by-side'
+ data-config='sideBySide'>
+ <label for='opt-cb-sbs'>Side-by-side</label>
+ </span>
+ <span class='labeled-input'>
+ <input type='checkbox' id='opt-cb-swapio'
+ data-csstgt='#main-wrapper'
+ data-cssclass='swapio'
+ data-config='swapInOut'>
+ <label for='opt-cb-swapio'>Swap in/out</label>
+ </span>
+ <span class='labeled-input'>
+ <input type='checkbox' id='opt-cb-autoscroll'
+ data-config='autoScrollOutput'>
+ <label for='opt-cb-autoscroll'>Auto-scroll output</label>
+ </span>
+ <span class='labeled-input'>
+ <input type='checkbox' id='opt-cb-autoclear'
+ data-config='autoClearOutput'>
+ <label for='opt-cb-autoclear'>Auto-clear output</label>
+ </span>
+ <span class='labeled-input'>
+ <input type='file' id='load-db' class='hidden'/>
+ <button id='btn-load-db'>Load DB...</button>
+ </span>
+ <span class='labeled-input'>
+ <button id='btn-export'>Download DB</button>
+ </span>
+ <span class='labeled-input'>
+ <button id='btn-reset'>Reset DB</button>
+ </span>
+ </div>
+ </fieldset><!-- .options -->
+ <div id='main-wrapper' class=''>
+ <fieldset class='zone-wrapper input'>
+ <legend><div class='button-bar'>
+ <button id='btn-shell-exec'>Run</button>
+ <button id='btn-clear'>Clear Input</button>
+ <!--button data-cmd='.help'>Help</button-->
+ <select id='select-examples'></select>
+ </div></legend>
+ <div><textarea id="input"
+ placeholder="Shell input. Ctrl-enter/shift-enter runs it.">
+-- ==================================================
+-- Use ctrl-enter or shift-enter to execute sqlite3
+-- shell commands and SQL.
+-- If a subset of the text is currently selected,
+-- only that part is executed.
+-- ==================================================
+.nullvalue NULL
+.headers on
+</textarea></div>
+ </fieldset>
+ <fieldset class='zone-wrapper output'>
+ <legend><div class='button-bar'>
+ <button id='btn-clear-output'>Clear Output</button>
+ <button id='btn-interrupt' class='hidden' disabled>Interrupt</button>
+ <!-- interruption cannot work in the current configuration
+ because we cannot send an interrupt message when work
+ is currently underway. At that point the Worker is
+ tied up and will not receive the message. -->
+ </div></legend>
+ <div><textarea id="output" readonly
+ placeholder="Shell output."></textarea></div>
+ </fieldset>
+ </div>
+ </div> <!-- #view-split -->
+ <script src="fiddle.js"></script>
+ </body>
+</html>
diff --git a/ext/wasm/index-dist.html b/ext/wasm/index-dist.html
new file mode 100644
index 0000000..f5bcdc1
--- /dev/null
+++ b/ext/wasm/index-dist.html
@@ -0,0 +1,113 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <title>sqlite3 WASM Demo Page Index</title>
+ </head>
+ <body>
+ <style>
+ body {
+ display: flex;
+ flex-direction: column;
+ flex-wrap: wrap;
+ }
+ textarea {
+ font-family: monospace;
+ }
+ header {
+ font-size: 130%;
+ font-weight: bold;
+ background: #044a64;
+ color: white;
+ padding: 0.5em;
+ border-radius: 0.25em;
+ }
+ .hidden, .initially-hidden {
+ position: absolute !important;
+ opacity: 0 !important;
+ pointer-events: none !important;
+ display: none !important;
+ }
+ .warning { color: firebrick; }
+ </style>
+ <header id='titlebar'><span>sqlite3 WASM demo pages</span></header>
+ <hr>
+ <div>Below is the list of demo pages for the sqlite3 WASM
+ builds. The intent is that <em>this</em> page be run
+ using the functional equivalent of:</div>
+ <blockquote><pre><a href='https://sqlite.org/althttpd'>althttpd</a> -enable-sab -page index.html</pre></blockquote>
+ <div>and the individual pages be started in their own tab.
+ Warnings and Caveats:
+ <ul class='warning'>
+ <li>All of these pages must be served via an HTTP
+ server. Browsers do not support loading WASM files via
+ file:// URLs.</li>
+ <li>Any OPFS-related pages or tests require:
+ <ul>
+ <li>An OPFS-capable browser released after February
+ 2023. Some tests will work with Chromium-based browsers
+ going back to around v102.</li>
+ <li>That the web server emit the so-called
+ <a href='https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy'>COOP</a>
+ and
+ <a href='https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy'>COEP</a>
+ headers. <a href='https://sqlite.org/althttpd'>althttpd</a> requires the
+ <code>-enable-sab</code> flag for that.
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <div>The tests and demos...
+ <ul id='test-list'>
+ <li>Core-most tests
+ <ul>
+ <li><a href='tester1.html'>tester1</a>: Core unit and
+ regression tests for the various APIs and surrounding
+ utility code.</li>
+ <li><a href='tester1-worker.html'>tester1-worker</a>: same thing
+ but running in a Worker.</li>
+ <li><a href='tester1-esm.html'>tester1-esm</a>: same as
+ <code>tester1</code> but loads sqlite3 in the main thread via
+ an ES6 module.
+ </li>
+ <li><a href='tester1-worker.html?esm'>tester1-worker?esm</a>:
+ same as <code>tester1-esm</code> but loads a Worker Module which
+ then loads the sqlite3 API via an ES6 module. Note that
+ not all browsers permit loading modules in Worker
+ threads.
+ </li>
+ </ul>
+ </li>
+ <li>Higher-level apps and demos...
+ <ul>
+ <li><a href='demo-123.html'>demo-123</a> provides a
+ no-nonsense example of adding sqlite3 support to a web
+ page in the UI thread.</li>
+ <li><a href='demo-123-worker.html'>demo-123-worker</a> is
+ the same as <code>demo-123</code> but loads and runs
+ sqlite3 from a Worker thread.</li>
+ <li><a href='demo-jsstorage.html'>demo-jsstorage</a>: very basic
+ demo of using the key-value VFS for storing a persistent db
+ in JS <code>localStorage</code> or <code>sessionStorage</code>.</li>
+ <li><a href='demo-worker1.html'>demo-worker1</a>:
+ Worker-based wrapper of the OO API #1. Its Promise-based
+ wrapper is significantly easier to use, however.</li>
+ <li><a href='demo-worker1-promiser.html'>demo-worker1-promiser</a>:
+ a demo of the Promise-based wrapper of the Worker1 API.</li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <style>
+ #test-list { font-size: 120%; }
+ </style>
+ <script>//Assign a distinct target tab name for each test page...
+ document.querySelectorAll('a').forEach(function(e){
+ e.target = e.href;
+ });
+ </script>
+ </body>
+</html>
diff --git a/ext/wasm/index.html b/ext/wasm/index.html
new file mode 100644
index 0000000..ebbfd67
--- /dev/null
+++ b/ext/wasm/index.html
@@ -0,0 +1,147 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>sqlite3 WASM Testing Page Index</title>
+ </head>
+ <body>
+ <style>
+ header {
+ background: #044a64;
+ color: white;
+ padding: 0.5em;
+ border-radius: 0.25em;
+ }
+ </style>
+ <header id='titlebar'><span>sqlite3 WASM test pages</span></header>
+ <hr>
+ <div>Below is the list of test pages for the sqlite3 WASM
+ builds. All of them require that this directory have been
+ "make"d first. The intent is that <em>this</em> page be run
+ using:</div>
+ <blockquote><pre>althttpd -enable-sab -page index.html</pre></blockquote>
+ <div>and the individual tests be started in their own tab.
+ Warnings and Caveats:
+ <ul class='warning'>
+ <li>All of these pages must be served via an HTTP
+ server. Browsers do not support loading WASM files via
+ file:// URLs.</li>
+ <li>Any OPFS-related pages or tests require:
+ <ul>
+ <li>An OPFS-capable browser released after February
+ 2023. Some tests will work with Chromium-based browsers
+ going back to around v102.</li>
+ <li>That the web server emit the so-called
+ <a href='https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy'>COOP</a>
+ and
+ <a href='https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy'>COEP</a>
+ headers. <a href='https://sqlite.org/althttpd'>althttpd</a> requires the
+ <code>-enable-sab</code> flag for that.
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <div>The tests and demos...
+ <ul id='test-list'>
+ <li>Core-most tests
+ <ul>
+ <li><a href='tester1.html'>tester1</a>: Core unit and
+ regression tests for the various APIs and surrounding
+ utility code.</li>
+ <li><a href='tester1-worker.html'>tester1-worker</a>: same thing
+ but running in a Worker.</li>
+ <li><a href='tester1-esm.html'>tester1-esm</a>: same as
+ <code>tester1</code> but loads sqlite3 in the main thread via
+ an ES6 module.
+ </li>
+ <li><a href='tester1-worker.html?esm'>tester1-worker?esm</a>:
+ same as <code>tester1-esm</code> but loads a Worker Module which
+ then loads the sqlite3 API via an ES6 module. Note that
+ not all browsers permit loading modules in Worker
+ threads.
+ </li>
+ </ul>
+ </li>
+ <li>High-level apps and demos...
+ <ul>
+ <li><a href='fiddle/index.html'>fiddle</a> is an HTML front-end
+ to a wasm build of the sqlite3 shell.</li>
+ <li><a href='demo-123.html'>demo-123</a> provides a
+ no-nonsense example of adding sqlite3 support to a web
+ page in the UI thread.</li>
+ <li><a href='demo-123-worker.html'>demo-123-worker</a> is
+ the same as <code>demo-123</code> but loads and runs
+ sqlite3 from a Worker thread.</li>
+ <li><a href='demo-jsstorage.html'>demo-jsstorage</a>: very basic
+ demo of using the key-value VFS for storing a persistent db
+ in JS <code>localStorage</code> or <code>sessionStorage</code>.</li>
+ <li><a href='demo-worker1.html'>demo-worker1</a>:
+ Worker-based wrapper of the OO API #1. Its Promise-based
+ wrapper is significantly easier to use, however.</li>
+ <li><a href='demo-worker1-promiser.html'>demo-worker1-promiser</a>:
+ a demo of the Promise-based wrapper of the Worker1 API.</li>
+ </ul>
+ </li>
+ <li>speedtest1 ports (sqlite3's primary benchmarking tool)...
+ <ul>
+ <li><a href='speedtest1.html'>speedtest1</a>: a main-thread WASM build of speedtest1.</li>
+ <li><a href='speedtest1.html?vfs=kvvfs'>speedtest1?vfs=kvvfs</a>: speedtest1 with the kvvfs.</li>
+ <li><a href='speedtest1-worker.html?size=15'>speedtest1-worker</a>: an interactive Worker-thread variant of speedtest1.</li>
+ <li><a href='speedtest1-worker.html?vfs=opfs&size=10'>speedtest1-worker?vfs=opfs</a>: speedtest1-worker with the
+ OPFS VFS preselected and configured for a moderate workload.</li>
+ <li><a href='speedtest1-worker.html?vfs=opfs-sahpool&size=10'>speedtest1-worker?vfs=opfs-sahpool</a>:
+ speedtest1-worker with the OPFS-SAHPOOL VFS preselected
+ and configured for a moderate workload.
+ </li>
+ </ul>
+ </li>
+ <li>The obligatory "misc." category...
+ <ul>
+ <li><a href='module-symbols.html'>module-symbols</a> gives
+ a high-level overview of the symbols exposed by the JS
+ module.</li>
+ <!--li><a href='batch-runner.html'>batch-runner</a>: runs batches of
+ SQL exported from speedtest1.</li-->
+ <li><a href='test-opfs-vfs.html'>test-opfs-vfs</a>
+ (<a href='test-opfs-vfs.html?opfs-sanity-check&opfs-verbose'>same
+ with verbose output and sanity-checking tests</a>) is an
+ sqlite3_vfs OPFS proxy using SharedArrayBuffer and the
+ Atomics APIs to regulate communication between the
+ synchronous sqlite3_vfs interface and the async OPFS
+ impl.
+ </li>
+ <li><a href='tests/opfs/concurrency/index.html'>OPFS concurrency</a>
+ tests using multiple workers.
+ </li>
+ </ul>
+ </li>
+ <li><strong>WASMFS</strong>-specific tests which require that
+ the WASMFS build is available on this server (it is not by
+ default) and that this server emits the COOP/COEP headers.
+ <ul>
+ <li><a href='scratchpad-wasmfs.html'>scratchpad-wasmfs</a>:
+ experimenting with WASMFS/OPFS-based persistence.
+ </li>
+ <li><a href='speedtest1-wasmfs.html?flags=--size,15'>speedtest1-wasmfs</a>:
+ a variant of speedtest1 built solely for the wasmfs/opfs
+ feature.
+ </li>
+ </ul>
+ </li>
+ <!--li><a href='x.html'></a></li-->
+ </ul>
+ </div>
+ <style>
+ #test-list { font-size: 120%; }
+ </style>
+ <script>//Assign a distinct target tab name for each test page...
+ document.querySelectorAll('a').forEach(function(e){
+ e.target = e.href.replace(/^http*:\/\/[^/]+\//, '');
+ });
+ </script>
+ </body>
+</html>
diff --git a/ext/wasm/jaccwabyt/jaccwabyt.js b/ext/wasm/jaccwabyt/jaccwabyt.js
new file mode 100644
index 0000000..1846441
--- /dev/null
+++ b/ext/wasm/jaccwabyt/jaccwabyt.js
@@ -0,0 +1,696 @@
+/**
+ 2022-06-30
+
+ 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 Jaccwabyt API is documented in detail in an external file,
+ _possibly_ called jaccwabyt.md in the same directory as this file.
+
+ Project homes:
+ - https://fossil.wanderinghorse.net/r/jaccwabyt
+ - https://sqlite.org/src/dir/ext/wasm/jaccwabyt
+
+*/
+'use strict';
+globalThis.Jaccwabyt = function StructBinderFactory(config){
+/* ^^^^ it is recommended that clients move that object into wherever
+ they'd like to have it and delete the self-held copy ("self" being
+ the global window or worker object). This API does not require the
+ global reference - it is simply installed as a convenience for
+ connecting these bits to other co-developed code before it gets
+ removed from the global namespace.
+*/
+
+ /** Throws a new Error, the message of which is the concatenation
+ all args with a space between each. */
+ const toss = (...args)=>{throw new Error(args.join(' '))};
+
+ /**
+ Implementing function bindings revealed significant
+ shortcomings in Emscripten's addFunction()/removeFunction()
+ interfaces:
+
+ https://github.com/emscripten-core/emscripten/issues/17323
+
+ Until those are resolved, or a suitable replacement can be
+ implemented, our function-binding API will be more limited
+ and/or clumsier to use than initially hoped.
+ */
+ if(!(config.heap instanceof WebAssembly.Memory)
+ && !(config.heap instanceof Function)){
+ toss("config.heap must be WebAssembly.Memory instance or a function.");
+ }
+ ['alloc','dealloc'].forEach(function(k){
+ (config[k] instanceof Function) ||
+ toss("Config option '"+k+"' must be a function.");
+ });
+ const SBF = StructBinderFactory;
+ const heap = (config.heap instanceof Function)
+ ? config.heap : (()=>new Uint8Array(config.heap.buffer)),
+ alloc = config.alloc,
+ dealloc = config.dealloc,
+ log = config.log || console.log.bind(console),
+ memberPrefix = (config.memberPrefix || ""),
+ memberSuffix = (config.memberSuffix || ""),
+ bigIntEnabled = (undefined===config.bigIntEnabled
+ ? !!globalThis['BigInt64Array'] : !!config.bigIntEnabled),
+ BigInt = globalThis['BigInt'],
+ BigInt64Array = globalThis['BigInt64Array'],
+ /* Undocumented (on purpose) config options: */
+ ptrSizeof = config.ptrSizeof || 4,
+ ptrIR = config.ptrIR || 'i32'
+ ;
+
+ if(!SBF.debugFlags){
+ SBF.__makeDebugFlags = function(deriveFrom=null){
+ /* This is disgustingly overengineered. :/ */
+ if(deriveFrom && deriveFrom.__flags) deriveFrom = deriveFrom.__flags;
+ const f = function f(flags){
+ if(0===arguments.length){
+ return f.__flags;
+ }
+ if(flags<0){
+ delete f.__flags.getter; delete f.__flags.setter;
+ delete f.__flags.alloc; delete f.__flags.dealloc;
+ }else{
+ f.__flags.getter = 0!==(0x01 & flags);
+ f.__flags.setter = 0!==(0x02 & flags);
+ f.__flags.alloc = 0!==(0x04 & flags);
+ f.__flags.dealloc = 0!==(0x08 & flags);
+ }
+ return f._flags;
+ };
+ Object.defineProperty(f,'__flags', {
+ iterable: false, writable: false,
+ value: Object.create(deriveFrom)
+ });
+ if(!deriveFrom) f(0);
+ return f;
+ };
+ SBF.debugFlags = SBF.__makeDebugFlags();
+ }/*static init*/
+
+ const isLittleEndian = (function() {
+ const buffer = new ArrayBuffer(2);
+ new DataView(buffer).setInt16(0, 256, true /* littleEndian */);
+ // Int16Array uses the platform's endianness.
+ return new Int16Array(buffer)[0] === 256;
+ })();
+ /**
+ Some terms used in the internal docs:
+
+ StructType: a struct-wrapping class generated by this
+ framework.
+ DEF: struct description object.
+ SIG: struct member signature string.
+ */
+
+ /** True if SIG s looks like a function signature, else
+ false. */
+ const isFuncSig = (s)=>'('===s[1];
+ /** True if SIG s is-a pointer signature. */
+ const isPtrSig = (s)=>'p'===s || 'P'===s;
+ const isAutoPtrSig = (s)=>'P'===s /*EXPERIMENTAL*/;
+ const sigLetter = (s)=>isFuncSig(s) ? 'p' : s[0];
+ /** Returns the WASM IR form of the Emscripten-conventional letter
+ at SIG s[0]. Throws for an unknown SIG. */
+ const sigIR = function(s){
+ switch(sigLetter(s)){
+ case 'c': case 'C': return 'i8';
+ case 'i': return 'i32';
+ case 'p': case 'P': case 's': return ptrIR;
+ case 'j': return 'i64';
+ case 'f': return 'float';
+ case 'd': return 'double';
+ }
+ toss("Unhandled signature IR:",s);
+ };
+
+ const affirmBigIntArray = BigInt64Array
+ ? ()=>true : ()=>toss('BigInt64Array is not available.');
+ /** Returns the name of a DataView getter method corresponding
+ to the given SIG. */
+ const sigDVGetter = function(s){
+ switch(sigLetter(s)) {
+ case 'p': case 'P': case 's': {
+ switch(ptrSizeof){
+ case 4: return 'getInt32';
+ case 8: return affirmBigIntArray() && 'getBigInt64';
+ }
+ break;
+ }
+ case 'i': return 'getInt32';
+ case 'c': return 'getInt8';
+ case 'C': return 'getUint8';
+ case 'j': return affirmBigIntArray() && 'getBigInt64';
+ case 'f': return 'getFloat32';
+ case 'd': return 'getFloat64';
+ }
+ toss("Unhandled DataView getter for signature:",s);
+ };
+ /** Returns the name of a DataView setter method corresponding
+ to the given SIG. */
+ const sigDVSetter = function(s){
+ switch(sigLetter(s)){
+ case 'p': case 'P': case 's': {
+ switch(ptrSizeof){
+ case 4: return 'setInt32';
+ case 8: return affirmBigIntArray() && 'setBigInt64';
+ }
+ break;
+ }
+ case 'i': return 'setInt32';
+ case 'c': return 'setInt8';
+ case 'C': return 'setUint8';
+ case 'j': return affirmBigIntArray() && 'setBigInt64';
+ case 'f': return 'setFloat32';
+ case 'd': return 'setFloat64';
+ }
+ toss("Unhandled DataView setter for signature:",s);
+ };
+ /**
+ Returns either Number of BigInt, depending on the given
+ SIG. This constructor is used in property setters to coerce
+ the being-set value to the correct size.
+ */
+ const sigDVSetWrapper = function(s){
+ switch(sigLetter(s)) {
+ case 'i': case 'f': case 'c': case 'C': case 'd': return Number;
+ case 'j': return affirmBigIntArray() && BigInt;
+ case 'p': case 'P': case 's':
+ switch(ptrSizeof){
+ case 4: return Number;
+ case 8: return affirmBigIntArray() && BigInt;
+ }
+ break;
+ }
+ toss("Unhandled DataView set wrapper for signature:",s);
+ };
+
+ /** Returns the given struct and member name in a form suitable for
+ debugging and error output. */
+ const sPropName = (s,k)=>s+'::'+k;
+
+ const __propThrowOnSet = function(structName,propName){
+ return ()=>toss(sPropName(structName,propName),"is read-only.");
+ };
+
+ /**
+ In order to completely hide StructBinder-bound struct
+ pointers from JS code, we store them in a scope-local
+ WeakMap which maps the struct-bound objects to their WASM
+ pointers. The pointers are accessible via
+ boundObject.pointer, which is gated behind an accessor
+ function, but are not exposed anywhere else in the
+ object. The main intention of that is to make it impossible
+ for stale copies to be made.
+ */
+ const __instancePointerMap = new WeakMap();
+
+ /** Property name for the pointer-is-external marker. */
+ const xPtrPropName = '(pointer-is-external)';
+
+ /** Frees the obj.pointer memory and clears the pointer
+ property. */
+ const __freeStruct = function(ctor, obj, m){
+ if(!m) m = __instancePointerMap.get(obj);
+ if(m) {
+ __instancePointerMap.delete(obj);
+ if(Array.isArray(obj.ondispose)){
+ let x;
+ while((x = obj.ondispose.shift())){
+ try{
+ if(x instanceof Function) x.call(obj);
+ else if(x instanceof StructType) x.dispose();
+ else if('number' === typeof x) dealloc(x);
+ // else ignore. Strings are permitted to annotate entries
+ // to assist in debugging.
+ }catch(e){
+ console.warn("ondispose() for",ctor.structName,'@',
+ m,'threw. NOT propagating it.',e);
+ }
+ }
+ }else if(obj.ondispose instanceof Function){
+ try{obj.ondispose()}
+ catch(e){
+ /*do not rethrow: destructors must not throw*/
+ console.warn("ondispose() for",ctor.structName,'@',
+ m,'threw. NOT propagating it.',e);
+ }
+ }
+ delete obj.ondispose;
+ if(ctor.debugFlags.__flags.dealloc){
+ log("debug.dealloc:",(obj[xPtrPropName]?"EXTERNAL":""),
+ ctor.structName,"instance:",
+ ctor.structInfo.sizeof,"bytes @"+m);
+ }
+ if(!obj[xPtrPropName]) dealloc(m);
+ }
+ };
+
+ /** Returns a skeleton for a read-only property accessor wrapping
+ value v. */
+ const rop = (v)=>{return {configurable: false, writable: false,
+ iterable: false, value: v}};
+
+ /** Allocates obj's memory buffer based on the size defined in
+ ctor.structInfo.sizeof. */
+ const __allocStruct = function(ctor, obj, m){
+ let fill = !m;
+ if(m) Object.defineProperty(obj, xPtrPropName, rop(m));
+ else{
+ m = alloc(ctor.structInfo.sizeof);
+ if(!m) toss("Allocation of",ctor.structName,"structure failed.");
+ }
+ try {
+ if(ctor.debugFlags.__flags.alloc){
+ log("debug.alloc:",(fill?"":"EXTERNAL"),
+ ctor.structName,"instance:",
+ ctor.structInfo.sizeof,"bytes @"+m);
+ }
+ if(fill) heap().fill(0, m, m + ctor.structInfo.sizeof);
+ __instancePointerMap.set(obj, m);
+ }catch(e){
+ __freeStruct(ctor, obj, m);
+ throw e;
+ }
+ };
+ /** Gets installed as the memoryDump() method of all structs. */
+ const __memoryDump = function(){
+ const p = this.pointer;
+ return p
+ ? new Uint8Array(heap().slice(p, p+this.structInfo.sizeof))
+ : null;
+ };
+
+ const __memberKey = (k)=>memberPrefix + k + memberSuffix;
+ const __memberKeyProp = rop(__memberKey);
+
+ /**
+ Looks up a struct member in structInfo.members. Throws if found
+ if tossIfNotFound is true, else returns undefined if not
+ found. The given name may be either the name of the
+ structInfo.members key (faster) or the key as modified by the
+ memberPrefix and memberSuffix settings.
+ */
+ const __lookupMember = function(structInfo, memberName, tossIfNotFound=true){
+ let m = structInfo.members[memberName];
+ if(!m && (memberPrefix || memberSuffix)){
+ // Check for a match on members[X].key
+ for(const v of Object.values(structInfo.members)){
+ if(v.key===memberName){ m = v; break; }
+ }
+ if(!m && tossIfNotFound){
+ toss(sPropName(structInfo.name,memberName),'is not a mapped struct member.');
+ }
+ }
+ return m;
+ };
+
+ /**
+ Uses __lookupMember(obj.structInfo,memberName) to find a member,
+ throwing if not found. Returns its signature, either in this
+ framework's native format or in Emscripten format.
+ */
+ const __memberSignature = function f(obj,memberName,emscriptenFormat=false){
+ if(!f._) f._ = (x)=>x.replace(/[^vipPsjrdcC]/g,"").replace(/[pPscC]/g,'i');
+ const m = __lookupMember(obj.structInfo, memberName, true);
+ return emscriptenFormat ? f._(m.signature) : m.signature;
+ };
+
+ const __ptrPropDescriptor = {
+ configurable: false, enumerable: false,
+ get: function(){return __instancePointerMap.get(this)},
+ set: ()=>toss("Cannot assign the 'pointer' property of a struct.")
+ // Reminder: leaving `set` undefined makes assignments
+ // to the property _silently_ do nothing. Current unit tests
+ // rely on it throwing, though.
+ };
+
+ /** Impl of X.memberKeys() for StructType and struct ctors. */
+ const __structMemberKeys = rop(function(){
+ const a = [];
+ for(const k of Object.keys(this.structInfo.members)){
+ a.push(this.memberKey(k));
+ }
+ return a;
+ });
+
+ const __utf8Decoder = new TextDecoder('utf-8');
+ const __utf8Encoder = new TextEncoder();
+ /** Internal helper to use in operations which need to distinguish
+ between SharedArrayBuffer heap memory and non-shared heap. */
+ const __SAB = ('undefined'===typeof SharedArrayBuffer)
+ ? function(){} : SharedArrayBuffer;
+ const __utf8Decode = function(arrayBuffer, begin, end){
+ return __utf8Decoder.decode(
+ (arrayBuffer.buffer instanceof __SAB)
+ ? arrayBuffer.slice(begin, end)
+ : arrayBuffer.subarray(begin, end)
+ );
+ };
+ /**
+ Uses __lookupMember() to find the given obj.structInfo key.
+ Returns that member if it is a string, else returns false. If the
+ member is not found, throws if tossIfNotFound is true, else
+ returns false.
+ */
+ const __memberIsString = function(obj,memberName, tossIfNotFound=false){
+ const m = __lookupMember(obj.structInfo, memberName, tossIfNotFound);
+ return (m && 1===m.signature.length && 's'===m.signature[0]) ? m : false;
+ };
+
+ /**
+ Given a member description object, throws if member.signature is
+ not valid for assigning to or interpretation as a C-style string.
+ It optimistically assumes that any signature of (i,p,s) is
+ C-string compatible.
+ */
+ const __affirmCStringSignature = function(member){
+ if('s'===member.signature) return;
+ toss("Invalid member type signature for C-string value:",
+ JSON.stringify(member));
+ };
+
+ /**
+ Looks up the given member in obj.structInfo. If it has a
+ signature of 's' then it is assumed to be a C-style UTF-8 string
+ and a decoded copy of the string at its address is returned. If
+ the signature is of any other type, it throws. If an s-type
+ member's address is 0, `null` is returned.
+ */
+ const __memberToJsString = function f(obj,memberName){
+ const m = __lookupMember(obj.structInfo, memberName, true);
+ __affirmCStringSignature(m);
+ const addr = obj[m.key];
+ //log("addr =",addr,memberName,"m =",m);
+ if(!addr) return null;
+ let pos = addr;
+ const mem = heap();
+ for( ; mem[pos]!==0; ++pos ) {
+ //log("mem[",pos,"]",mem[pos]);
+ };
+ //log("addr =",addr,"pos =",pos);
+ return (addr===pos) ? "" : __utf8Decode(mem, addr, pos);
+ };
+
+ /**
+ Adds value v to obj.ondispose, creating ondispose,
+ or converting it to an array, if needed.
+ */
+ const __addOnDispose = function(obj, ...v){
+ if(obj.ondispose){
+ if(!Array.isArray(obj.ondispose)){
+ obj.ondispose = [obj.ondispose];
+ }
+ }else{
+ obj.ondispose = [];
+ }
+ obj.ondispose.push(...v);
+ };
+
+ /**
+ Allocates a new UTF-8-encoded, NUL-terminated copy of the given
+ JS string and returns its address relative to heap(). If
+ allocation returns 0 this function throws. Ownership of the
+ memory is transfered to the caller, who must eventually pass it
+ to the configured dealloc() function.
+ */
+ const __allocCString = function(str){
+ const u = __utf8Encoder.encode(str);
+ const mem = alloc(u.length+1);
+ if(!mem) toss("Allocation error while duplicating string:",str);
+ const h = heap();
+ //let i = 0;
+ //for( ; i < u.length; ++i ) h[mem + i] = u[i];
+ h.set(u, mem);
+ h[mem + u.length] = 0;
+ //log("allocCString @",mem," =",u);
+ return mem;
+ };
+
+ /**
+ Sets the given struct member of obj to a dynamically-allocated,
+ UTF-8-encoded, NUL-terminated copy of str. It is up to the caller
+ to free any prior memory, if appropriate. The newly-allocated
+ string is added to obj.ondispose so will be freed when the object
+ is disposed.
+
+ The given name may be either the name of the structInfo.members
+ key (faster) or the key as modified by the memberPrefix and
+ memberSuffix settings.
+ */
+ const __setMemberCString = function(obj, memberName, str){
+ const m = __lookupMember(obj.structInfo, memberName, true);
+ __affirmCStringSignature(m);
+ /* Potential TODO: if obj.ondispose contains obj[m.key] then
+ dealloc that value and clear that ondispose entry */
+ const mem = __allocCString(str);
+ obj[m.key] = mem;
+ __addOnDispose(obj, mem);
+ return obj;
+ };
+
+ /**
+ Prototype for all StructFactory instances (the constructors
+ returned from StructBinder).
+ */
+ const StructType = function ctor(structName, structInfo){
+ if(arguments[2]!==rop){
+ toss("Do not call the StructType constructor",
+ "from client-level code.");
+ }
+ Object.defineProperties(this,{
+ //isA: rop((v)=>v instanceof ctor),
+ structName: rop(structName),
+ structInfo: rop(structInfo)
+ });
+ };
+
+ /**
+ Properties inherited by struct-type-specific StructType instances
+ and (indirectly) concrete struct-type instances.
+ */
+ StructType.prototype = Object.create(null, {
+ dispose: rop(function(){__freeStruct(this.constructor, this)}),
+ lookupMember: rop(function(memberName, tossIfNotFound=true){
+ return __lookupMember(this.structInfo, memberName, tossIfNotFound);
+ }),
+ memberToJsString: rop(function(memberName){
+ return __memberToJsString(this, memberName);
+ }),
+ memberIsString: rop(function(memberName, tossIfNotFound=true){
+ return __memberIsString(this, memberName, tossIfNotFound);
+ }),
+ memberKey: __memberKeyProp,
+ memberKeys: __structMemberKeys,
+ memberSignature: rop(function(memberName, emscriptenFormat=false){
+ return __memberSignature(this, memberName, emscriptenFormat);
+ }),
+ memoryDump: rop(__memoryDump),
+ pointer: __ptrPropDescriptor,
+ setMemberCString: rop(function(memberName, str){
+ return __setMemberCString(this, memberName, str);
+ })
+ });
+ // Function-type non-Property inherited members
+ Object.assign(StructType.prototype,{
+ addOnDispose: function(...v){
+ __addOnDispose(this,...v);
+ return this;
+ }
+ });
+
+ /**
+ "Static" properties for StructType.
+ */
+ Object.defineProperties(StructType, {
+ allocCString: rop(__allocCString),
+ isA: rop((v)=>v instanceof StructType),
+ hasExternalPointer: rop((v)=>(v instanceof StructType) && !!v[xPtrPropName]),
+ memberKey: __memberKeyProp
+ });
+
+ const isNumericValue = (v)=>Number.isFinite(v) || (v instanceof (BigInt || Number));
+
+ /**
+ Pass this a StructBinder-generated prototype, and the struct
+ member description object. It will define property accessors for
+ proto[memberKey] which read from/write to memory in
+ this.pointer. It modifies descr to make certain downstream
+ operations much simpler.
+ */
+ const makeMemberWrapper = function f(ctor,name, descr){
+ if(!f._){
+ /*cache all available getters/setters/set-wrappers for
+ direct reuse in each accessor function. */
+ f._ = {getters: {}, setters: {}, sw:{}};
+ const a = ['i','c','C','p','P','s','f','d','v()'];
+ if(bigIntEnabled) a.push('j');
+ a.forEach(function(v){
+ //const ir = sigIR(v);
+ f._.getters[v] = sigDVGetter(v) /* DataView[MethodName] values for GETTERS */;
+ f._.setters[v] = sigDVSetter(v) /* DataView[MethodName] values for SETTERS */;
+ f._.sw[v] = sigDVSetWrapper(v) /* BigInt or Number ctor to wrap around values
+ for conversion */;
+ });
+ const rxSig1 = /^[ipPsjfdcC]$/,
+ rxSig2 = /^[vipPsjfdcC]\([ipPsjfdcC]*\)$/;
+ f.sigCheck = function(obj, name, key,sig){
+ if(Object.prototype.hasOwnProperty.call(obj, key)){
+ toss(obj.structName,'already has a property named',key+'.');
+ }
+ rxSig1.test(sig) || rxSig2.test(sig)
+ || toss("Malformed signature for",
+ sPropName(obj.structName,name)+":",sig);
+ };
+ }
+ const key = ctor.memberKey(name);
+ f.sigCheck(ctor.prototype, name, key, descr.signature);
+ descr.key = key;
+ descr.name = name;
+ const sigGlyph = sigLetter(descr.signature);
+ const xPropName = sPropName(ctor.prototype.structName,key);
+ const dbg = ctor.prototype.debugFlags.__flags;
+ /*
+ TODO?: set prototype of descr to an object which can set/fetch
+ its prefered representation, e.g. conversion to string or mapped
+ function. Advantage: we can avoid doing that via if/else if/else
+ in the get/set methods.
+ */
+ const prop = Object.create(null);
+ prop.configurable = false;
+ prop.enumerable = false;
+ prop.get = function(){
+ if(dbg.getter){
+ log("debug.getter:",f._.getters[sigGlyph],"for", sigIR(sigGlyph),
+ xPropName,'@', this.pointer,'+',descr.offset,'sz',descr.sizeof);
+ }
+ let rc = (
+ new DataView(heap().buffer, this.pointer + descr.offset, descr.sizeof)
+ )[f._.getters[sigGlyph]](0, isLittleEndian);
+ if(dbg.getter) log("debug.getter:",xPropName,"result =",rc);
+ return rc;
+ };
+ if(descr.readOnly){
+ prop.set = __propThrowOnSet(ctor.prototype.structName,key);
+ }else{
+ prop.set = function(v){
+ if(dbg.setter){
+ log("debug.setter:",f._.setters[sigGlyph],"for", sigIR(sigGlyph),
+ xPropName,'@', this.pointer,'+',descr.offset,'sz',descr.sizeof, v);
+ }
+ if(!this.pointer){
+ toss("Cannot set struct property on disposed instance.");
+ }
+ if(null===v) v = 0;
+ else while(!isNumericValue(v)){
+ if(isAutoPtrSig(descr.signature) && (v instanceof StructType)){
+ // It's a struct instance: let's store its pointer value!
+ v = v.pointer || 0;
+ if(dbg.setter) log("debug.setter:",xPropName,"resolved to",v);
+ break;
+ }
+ toss("Invalid value for pointer-type",xPropName+'.');
+ }
+ (
+ new DataView(heap().buffer, this.pointer + descr.offset, descr.sizeof)
+ )[f._.setters[sigGlyph]](0, f._.sw[sigGlyph](v), isLittleEndian);
+ };
+ }
+ Object.defineProperty(ctor.prototype, key, prop);
+ }/*makeMemberWrapper*/;
+
+ /**
+ The main factory function which will be returned to the
+ caller.
+ */
+ const StructBinder = function StructBinder(structName, structInfo){
+ if(1===arguments.length){
+ structInfo = structName;
+ structName = structInfo.name;
+ }else if(!structInfo.name){
+ structInfo.name = structName;
+ }
+ if(!structName) toss("Struct name is required.");
+ let lastMember = false;
+ Object.keys(structInfo.members).forEach((k)=>{
+ // Sanity checks of sizeof/offset info...
+ const m = structInfo.members[k];
+ if(!m.sizeof) toss(structName,"member",k,"is missing sizeof.");
+ else if(m.sizeof===1){
+ (m.signature === 'c' || m.signature === 'C') ||
+ toss("Unexpected sizeof==1 member",
+ sPropName(structInfo.name,k),
+ "with signature",m.signature);
+ }else{
+ // sizes and offsets of size-1 members may be odd values, but
+ // others may not.
+ if(0!==(m.sizeof%4)){
+ console.warn("Invalid struct member description =",m,"from",structInfo);
+ toss(structName,"member",k,"sizeof is not aligned. sizeof="+m.sizeof);
+ }
+ if(0!==(m.offset%4)){
+ console.warn("Invalid struct member description =",m,"from",structInfo);
+ toss(structName,"member",k,"offset is not aligned. offset="+m.offset);
+ }
+ }
+ if(!lastMember || lastMember.offset < m.offset) lastMember = m;
+ });
+ if(!lastMember) toss("No member property descriptions found.");
+ else if(structInfo.sizeof < lastMember.offset+lastMember.sizeof){
+ toss("Invalid struct config:",structName,
+ "max member offset ("+lastMember.offset+") ",
+ "extends past end of struct (sizeof="+structInfo.sizeof+").");
+ }
+ const debugFlags = rop(SBF.__makeDebugFlags(StructBinder.debugFlags));
+ /** Constructor for the StructCtor. */
+ const StructCtor = function StructCtor(externalMemory){
+ if(!(this instanceof StructCtor)){
+ toss("The",structName,"constructor may only be called via 'new'.");
+ }else if(arguments.length){
+ if(externalMemory!==(externalMemory|0) || externalMemory<=0){
+ toss("Invalid pointer value for",structName,"constructor.");
+ }
+ __allocStruct(StructCtor, this, externalMemory);
+ }else{
+ __allocStruct(StructCtor, this);
+ }
+ };
+ Object.defineProperties(StructCtor,{
+ debugFlags: debugFlags,
+ isA: rop((v)=>v instanceof StructCtor),
+ memberKey: __memberKeyProp,
+ memberKeys: __structMemberKeys,
+ methodInfoForKey: rop(function(mKey){
+ }),
+ structInfo: rop(structInfo),
+ structName: rop(structName)
+ });
+ StructCtor.prototype = new StructType(structName, structInfo, rop);
+ Object.defineProperties(StructCtor.prototype,{
+ debugFlags: debugFlags,
+ constructor: rop(StructCtor)
+ /*if we assign StructCtor.prototype and don't do
+ this then StructCtor!==instance.constructor!*/
+ });
+ Object.keys(structInfo.members).forEach(
+ (name)=>makeMemberWrapper(StructCtor, name, structInfo.members[name])
+ );
+ return StructCtor;
+ };
+ StructBinder.StructType = StructType;
+ StructBinder.config = config;
+ StructBinder.allocCString = __allocCString;
+ if(!StructBinder.debugFlags){
+ StructBinder.debugFlags = SBF.__makeDebugFlags(SBF.debugFlags);
+ }
+ return StructBinder;
+}/*StructBinderFactory*/;
diff --git a/ext/wasm/jaccwabyt/jaccwabyt.md b/ext/wasm/jaccwabyt/jaccwabyt.md
new file mode 100644
index 0000000..431741e
--- /dev/null
+++ b/ext/wasm/jaccwabyt/jaccwabyt.md
@@ -0,0 +1,1065 @@
+Jaccwabyt 🐇
+============================================================
+
+**Jaccwabyt**: _JavaScript ⇄ C Struct Communication via WASM Byte
+Arrays_
+
+Welcome to Jaccwabyt, a JavaScript API which creates bindings for
+WASM-compiled C structs, defining them in such a way that changes to
+their state in JS are visible in C/WASM, and vice versa, permitting
+two-way interchange of struct state with very little user-side
+friction.
+
+(If that means nothing to you, neither will the rest of this page!)
+
+**Browser compatibility**: this library requires a _recent_ browser
+and makes no attempt whatsoever to accommodate "older" or
+lesser-capable ones, where "recent," _very roughly_, means released in
+mid-2018 or later, with late 2021 releases required for some optional
+features in some browsers (e.g. [BigInt64Array][] in Safari). It also
+relies on a couple non-standard, but widespread, features, namely
+[TextEncoder][] and [TextDecoder][]. It is developed primarily on
+Firefox and Chrome on Linux and all claims of Safari compatibility
+are based solely on feature compatibility tables provided at
+[MDN][].
+
+**Formalities:**
+
+- Author: [Stephan Beal][sgb]
+- Project Homes:
+ - <https://fossil.wanderinghorse.net/r/jaccwabyt>\
+ Is the primary home but...
+ - <https://sqlite.org/src/dir/ext/wasm/jaccwabyt>\
+ ... most development happens here.
+
+The license for both this documentation and the software it documents
+is the same as [sqlite3][], the project from which this spinoff
+project was spawned:
+
+-----
+
+> 2022-06-30:
+>
+> 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.
+
+-----
+
+<a name='overview'></a>
+Table of Contents
+============================================================
+
+- [Overview](#overview)
+ - [Architecture](#architecture)
+- [Creating and Binding Structs](#creating-binding)
+ - [Step 1: Configure Jaccwabyt](#step-1)
+ - [Step 2: Struct Description](#step-2)
+ - [`P` vs `p`](#step-2-pvsp)
+ - [Step 3: Binding a Struct](#step-3)
+ - [Step 4: Creating, Using, and Destroying Instances](#step-4)
+- APIs
+ - [Struct Binder Factory](#api-binderfactory)
+ - [Struct Binder](#api-structbinder)
+ - [Struct Type](#api-structtype)
+ - [Struct Constructors](#api-structctor)
+ - [Struct Protypes](#api-structprototype)
+ - [Struct Instances](#api-structinstance)
+- Appendices
+ - [Appendix A: Limitations, TODOs, etc.](#appendix-a)
+ - [Appendix D: Debug Info](#appendix-d)
+ - [Appendix G: Generating Struct Descriptions](#appendix-g)
+
+<a name='overview'></a>
+Overview
+============================================================
+
+Management summary: this JavaScript-only framework provides limited
+two-way bindings between C structs and JavaScript objects, such that
+changes to the struct in one environment are visible in the other.
+
+Details...
+
+It works by creating JavaScript proxies for C structs. Reads and
+writes of the JS-side members are marshaled through a flat byte array
+allocated from the WASM heap. As that heap is shared with the C-side
+code, and the memory block is written using the same approach C does,
+that byte array can be used to access and manipulate a given struct
+instance from both JS and C.
+
+Motivating use case: this API was initially developed as an
+experiment to determine whether it would be feasible to implement,
+completely in JS, custom "VFS" and "virtual table" objects for the
+WASM build of [sqlite3][]. Doing so was going to require some form of
+two-way binding of several structs. Once the proof of concept was
+demonstrated, a rabbit hole appeared and _down we went_... It has
+since grown beyond its humble proof-of-concept origins and is believed
+to be a useful (or at least interesting) tool for mixed JS/C
+applications.
+
+Portability notes:
+
+- These docs sometimes use [Emscripten][] as a point of reference
+ because it is the most widespread WASM toolchain, but this code is
+ specifically designed to be usable in arbitrary WASM environments.
+ It abstracts away a few Emscripten-specific features into
+ configurable options. Similarly, the build tree requires Emscripten
+ but Jaccwabyt does not have any hard Emscripten dependencies.
+- This code is encapsulated into a single JavaScript function. It
+ should be trivial to copy/paste into arbitrary WASM/JS-using
+ projects.
+- The source tree includes C code, but only for testing and
+ demonstration purposes. It is not part of the core distributable.
+
+<a name='architecture'></a>
+Architecture
+------------------------------------------------------------
+
+<!--
+bug(?) (fossil): using "center" shrinks pikchr too much.
+-->
+
+```pikchr
+BSBF: box rad 0.3*boxht "StructBinderFactory" fit fill lightblue
+BSB: box same "StructBinder" fit at 0.75 e of 0.7 s of BSBF.c
+BST: box same "StructType<T>" fit at 1.5 e of BSBF
+BSC: box same "Struct<T>" "Ctor" fit at 1.5 s of BST
+BSI: box same "Struct<T>" "Instances" fit at 1 right of BSB.e
+BC: box same at 0.25 right of 1.6 e of BST "C Structs" fit fill lightgrey
+
+arrow -> from BSBF.s to BSB.w "Generates" aligned above
+arrow -> from BSB.n to BST.sw "Contains" aligned above
+arrow -> from BSB.s to BSC.nw "Generates" aligned below
+arrow -> from BSC.ne to BSI.s "Constructs" aligned below
+arrow <- from BST.se to BSI.n "Inherits" aligned above
+arrow <-> from BSI.e to BC.s dotted "Shared" aligned above "Memory" aligned below
+arrow -> from BST.e to BC.w dotted "Mirrors Struct" aligned above "Model From" aligned below
+arrow -> from BST.s to BSC.n "Prototype of" aligned above
+```
+
+Its major classes and functions are:
+
+- **[StructBinderFactory][StructBinderFactory]** is a factory function which
+ accepts a configuration object to customize it for a given WASM
+ environment. A client will typically call this only one time, with
+ an appropriate configuration, to generate a single...
+- **[StructBinder][]** is a factory function which converts an
+ arbitrary number struct descriptions into...
+- **[StructTypes][StructCtors]** are constructors, one per struct
+ description, which inherit from
+ **[`StructBinder.StructType`][StructType]** and are used to instantiate...
+- **[Struct instances][StructInstance]** are objects representing
+ individual instances of generated struct types.
+
+An app may have any number of StructBinders, but will typically
+need only one. Each StructBinder is effectively a separate
+namespace for struct creation.
+
+
+<a name='creating-binding'></a>
+Creating and Binding Structs
+============================================================
+
+From the amount of documentation provided, it may seem that
+creating and using struct bindings is a daunting task, but it
+essentially boils down to:
+
+1. [Confire Jaccwabyt for your WASM environment](#step-1). This is a
+ one-time task per project and results is a factory function which
+ can create new struct bindings.
+2. [Create a JSON-format description of your C structs](#step-2). This is
+ required once for each struct and required updating if the C
+ structs change.
+3. [Feed (2) to the function generated by (1)](#step-3) to create JS
+ constuctor functions for each struct. This is done at runtime, as
+ opposed to during a build-process step, and can be set up in such a
+ way that it does not require any maintenace after its initial
+ setup.
+4. [Create and use instances of those structs](#step-4).
+
+Detailed instructions for each of those steps follows...
+
+<a name='step-1'></a>
+Step 1: Configure Jaccwabyt for the Environment
+------------------------------------------------------------
+
+Jaccwabyt's highest-level API is a single function. It creates a
+factory for processing struct descriptions, but does not process any
+descriptions itself. This level of abstraction exist primarily so that
+the struct-specific factories can be configured for a given WASM
+environment. Its usage looks like:
+
+>
+```javascript
+const MyBinder = StructBinderFactory({
+ // These config options are all required:
+ heap: WebAssembly.Memory instance or a function which returns
+ a Uint8Array or Int8Array view of the WASM memory,
+ alloc: function(howMuchMemory){...},
+ dealloc: function(pointerToFree){...}
+});
+```
+
+It also offers a number of other settings, but all are optional except
+for the ones shown above. Those three config options abstract away
+details which are specific to a given WASM environment. They provide
+the WASM "heap" memory, the memory allocator, and the deallocator. In
+a conventional Emscripten setup, that config might simply look like:
+
+>
+```javascript
+{
+ heap: Module['asm']['memory'],
+ //Or:
+ // heap: ()=>Module['HEAP8'],
+ alloc: (n)=>Module['_malloc'](n),
+ dealloc: (m)=>Module['_free'](m)
+}
+```
+
+The StructBinder factory function returns a function which can then be
+used to create bindings for our structs.
+
+<a name='step-2'></a>
+Step 2: Create a Struct Description
+------------------------------------------------------------
+
+The primary input for this framework is a JSON-compatible construct
+which describes a struct we want to bind. For example, given this C
+struct:
+
+>
+```c
+// C-side:
+struct Foo {
+ int member1;
+ void * member2;
+ int64_t member3;
+};
+```
+
+Its JSON description looks like:
+
+>
+```json
+{
+ "name": "Foo",
+ "sizeof": 16,
+ "members": {
+ "member1": {"offset": 0,"sizeof": 4,"signature": "i"},
+ "member2": {"offset": 4,"sizeof": 4,"signature": "p"},
+ "member3": {"offset": 8,"sizeof": 8,"signature": "j"}
+ }
+}
+```
+
+These data _must_ match up with the C-side definition of the struct
+(if any). See [Appendix G][appendix-g] for one way to easily generate
+these from C code.
+
+Each entry in the `members` object maps the member's name to
+its low-level layout:
+
+- `offset`: the byte offset from the start of the struct, as reported
+ by C's `offsetof()` feature.
+- `sizeof`: as reported by C's `sizeof()`.
+- `signature`: described below.
+- `readOnly`: optional. If set to true, the binding layer will
+ throw if JS code tries to set that property.
+
+The order of the `members` entries is not important: their memory
+layout is determined by their `offset` and `sizeof` members. The
+`name` property is technically optional, but one of the steps in the
+binding process requires that either it be passed an explicit name or
+there be one in the struct description. The names of the `members`
+entries need not match their C counterparts. Project conventions may
+call for giving them different names in the JS side and the
+[StructBinderFactory][] can be configured to automatically add a
+prefix and/or suffix to their names.
+
+Nested structs are as-yet unsupported by this tool.
+
+Struct member "signatures" describe the data types of the members and
+are an extended variant of the format used by Emscripten's
+`addFunction()`. A signature for a non-function-pointer member, or
+function pointer member which is to be modelled as an opaque pointer,
+is a single letter. A signature for a function pointer may also be
+modelled as a series of letters describing the call signature. The
+supported letters are:
+
+- **`v`** = `void` (only used as return type for function pointer members)
+- **`i`** = `int32` (4 bytes)
+- **`j`** = `int64` (8 bytes) is only really usable if this code is built
+ with BigInt support (e.g. using the Emscripten `-sWASM_BIGINT` build
+ flag). Without that, this API may throw when encountering the `j`
+ signature entry.
+- **`f`** = `float` (4 bytes)
+- **`d`** = `double` (8 bytes)
+- **`c`** = `int8` (1 byte) char - see notes below!
+- **`C`** = `uint8` (1 byte) unsigned char - see notes below!
+- **`p`** = `int32` (see notes below!)
+- **`P`** = Like `p` but with extra handling. Described below.
+- **`s`** = like `int32` but is a _hint_ that it's a pointer to a
+ string so that _some_ (very limited) contexts may treat it as such,
+ noting that such algorithms must, for lack of information to the
+ contrary, assume both that the encoding is UTF-8 and that the
+ pointer's member is NUL-terminated. If that is _not_ the case for a
+ given string member, do not use `s`: use `i` or `p` instead and do
+ any string handling yourself.
+
+Noting that:
+
+- **All of these types are numeric**. Attempting to set any
+ struct-bound property to a non-numeric value will trigger an
+ exception except in cases explicitly noted otherwise.
+- **"Char" types**: WASM does not define an `int8` type, nor does it
+ distinguish between signed and unsigned. This API treats `c` as
+ `int8` and `C` as `uint8` for purposes of getting and setting values
+ when using the `DataView` class. It is _not_ recommended that client
+ code use these types in new WASM-capable code, but they were added
+ for the sake of binding some immutable legacy code to WASM.
+
+> Sidebar: Emscripten's public docs do not mention `p`, but their
+generated code includes `p` as an alias for `i`, presumably to mean
+"pointer". Though `i` is legal for pointer types in the signature, `p`
+is more descriptive, so this framework encourages the use of `p` for
+pointer-type members. Using `p` for pointers also helps future-proof
+the signatures against the eventuality that WASM eventually supports
+64-bit pointers. Note that sometimes `p` really means
+pointer-to-pointer, but the Emscripten JS/WASM glue does not offer
+that level of expressiveness in these signatures. We simply have to be
+aware of when we need to deal with pointers and pointers-to-pointers
+in JS code.
+
+> Trivia: this API treates `p` as distinctly different from `i` in
+some contexts, so its use is encouraged for pointer types.
+
+Signatures in the form `x(...)` denote function-pointer members and
+`x` denotes non-function members. Functions with no arguments use the
+form `x()`. For function-type signatures, the strings are formulated
+such that they can be passed to Emscripten's `addFunction()` after
+stripping out the `(` and `)` characters. For good measure, to match
+the public Emscripten docs, `p`, `c`, and `C`, should also be replaced
+with `i`. In JavaScript that might look like:
+
+>
+```
+signature.replace(/[^vipPsjfdcC]/g,'').replace(/[pPscC]/g,'i');
+```
+
+<a name='step-2-pvsp'></a>
+### `P` vs `p` in Method Signatures
+
+*This support is experimental and subject to change.*
+
+The method signature letter `p` means "pointer," which, in WASM, means
+"integer." `p` is treated as an integer for most contexts, while still
+also being a separate type (analog to how pointers in C are just a
+special use of unsigned numbers). A capital `P` changes the semantics
+of plain member pointers (but not, as of this writing, function
+pointer members) as follows:
+
+- When a `P`-type member is **set** via `myStruct.x=y`, if
+ [`(y instanceof StructType)`][StructType] then the value of `y.pointer` is
+ stored in `myStruct.x`. If `y` is neither a number nor
+ a [StructType][], an exception is triggered (regardless of whether
+ `p` or `P` is used).
+
+
+<a name='step-3'></a>
+Step 3: Binding the Struct
+------------------------------------------------------------
+
+We can now use the results of steps 1 and 2:
+
+>
+```javascript
+const MyStruct = MyBinder(myStructDescription);
+```
+
+That creates a new constructor function, `MyStruct`, which can be used
+to instantiate new instances. The binder will throw if it encounters
+any problems.
+
+That's all there is to it.
+
+> Sidebar: that function may modify the struct description object
+and/or its sub-objects, or may even replace sub-objects, in order to
+simplify certain later operations. If that is not desired, then feed
+it a copy of the original, e.g. by passing it
+`JSON.parse(JSON.stringify(structDefinition))`.
+
+<a name='step-4'></a>
+Step 4: Creating, Using, and Destroying Struct Instances
+------------------------------------------------------------
+
+Now that we have our constructor...
+
+>
+```javascript
+const my = new MyStruct();
+```
+
+It is important to understand that creating a new instance allocates
+memory on the WASM heap. We must not simply rely on garbage collection
+to clean up the instances because doing so will not free up the WASM
+heap memory. The correct way to free up that memory is to use the
+object's `dispose()` method.
+
+The following usage pattern offers one way to easily ensure proper
+cleanup of struct instances:
+
+>
+```javascript
+const my = new MyStruct();
+try {
+ console.log(my.member1, my.member2, my.member3);
+ my.member1 = 12;
+ assert(12 === my.member1);
+ /* ^^^ it may seem silly to test that, but recall that assigning that
+ property encodes the value into a byte array in heap memory, not
+ a normal JS property. Similarly, fetching the property decodes it
+ from the byte array. */
+ // Pass the struct to C code which takes a MyStruct pointer:
+ aCFunction( my.pointer );
+} finally {
+ my.dispose();
+}
+```
+
+> Sidebar: the `finally` block will be run no matter how the `try`
+exits, whether it runs to completion, propagates an exception, or uses
+flow-control keywords like `return` or `break`. It is perfectly legal
+to use `try`/`finally` without a `catch`, and doing so is an ideal
+match for the memory management requirements of Jaccwaby-bound struct
+instances.
+
+It is often useful to wrap an existing instance of a C-side struct
+without taking over ownership of its memory. That can be achieved by
+simply passing a pointer to the constructor. For example:
+
+```js
+const m = new MyStruct( functionReturningASharedPtr() );
+// calling m.dispose() will _not_ free the wrapped C-side instance
+// but will trigger any ondispose handler.
+```
+
+Now that we have struct instances, there are a number of things we
+can do with them, as covered in the rest of this document.
+
+
+<a name='api'></a>
+API Reference
+============================================================
+
+<a name='api-binderfactory'></a>
+API: Binder Factory
+------------------------------------------------------------
+
+This is the top-most function of the API, from which all other
+functions and types are generated. The binder factory's signature is:
+
+>
+```
+Function StructBinderFactory(object configOptions);
+```
+
+It returns a function which these docs refer to as a [StructBinder][]
+(covered in the next section). It throws on error.
+
+The binder factory supports the following options in its
+configuration object argument:
+
+
+- `heap`
+ Must be either a `WebAssembly.Memory` instance representing the WASM
+ heap memory OR a function which returns an Int8Array or Uint8Array
+ view of the WASM heap. In the latter case the function should, if
+ appropriate for the environment, account for the heap being able to
+ grow. Jaccwabyt uses this property in such a way that it "should" be
+ okay for the WASM heap to grow at runtime (that case is, however,
+ untested).
+
+- `alloc`
+ Must be a function semantically compatible with Emscripten's
+ `Module._malloc()`. That is, it is passed the number of bytes to
+ allocate and it returns a pointer. On allocation failure it may
+ either return 0 or throw an exception. This API will throw an
+ exception if allocation fails or will propagate whatever exception
+ the allocator throws. The allocator _must_ use the same heap as the
+ `heap` config option.
+
+- `dealloc`
+ Must be a function semantically compatible with Emscripten's
+ `Module._free()`. That is, it takes a pointer returned from
+ `alloc()` and releases that memory. It must never throw and must
+ accept a value of 0/null to mean "do nothing" (noting that 0 is
+ _technically_ a legal memory address in WASM, but that seems like a
+ design flaw).
+
+- `bigIntEnabled` (bool=true if BigInt64Array is available, else false)
+ If true, the WASM bits this code is used with must have been
+ compiled with int64 support (e.g. using Emscripten's `-sWASM_BIGINT`
+ flag). If that's not the case, this flag should be set to false. If
+ it's enabled, BigInt support is assumed to work and certain extra
+ features are enabled. Trying to use features which requires BigInt
+ when it is disabled (e.g. using 64-bit integer types) will trigger
+ an exception.
+
+- `memberPrefix` and `memberSuffix` (string="")
+ If set, struct-defined properties get bound to JS with this string
+ as a prefix resp. suffix. This can be used to avoid symbol name
+ collisions between the struct-side members and the JS-side ones
+ and/or to make more explicit which object-level properties belong to
+ the struct mapping and which to the JS side. This does not modify
+ the values in the struct description objects, just the property
+ names through which they are accessed via property access operations
+ and the various a [StructInstance][] APIs (noting that the latter
+ tend to permit both the original names and the names as modified by
+ these settings).
+
+- `log`
+ Optional function used for debugging output. By default
+ `console.log` is used but by default no debug output is generated.
+ This API assumes that the function will space-separate each argument
+ (like `console.log` does). See [Appendix D](#appendix-d) for info
+ about enabling debugging output.
+
+
+<a name='api-structbinder'></a>
+API: Struct Binder
+------------------------------------------------------------
+
+Struct Binders are factories which are created by the
+[StructBinderFactory][]. A given Struct Binder can process any number
+of distinct structs. In a typical setup, an app will have ony one
+shared Binder Factory and one Struct Binder. Struct Binders which are
+created via different [StructBinderFactory][] calls are unrelated to each
+other, sharing no state except, perhaps, indirectly via
+[StructBinderFactory][] configuration (e.g. the memory heap).
+
+These factories have two call signatures:
+
+>
+```javascript
+Function StructBinder([string structName,] object structDescription)
+```
+
+If the struct description argument has a `name` property then the name
+argument is optional, otherwise it is required.
+
+The returned object is a constructor for instances of the struct
+described by its argument(s), each of which derives from
+a separate [StructType][] instance.
+
+The Struct Binder has the following members:
+
+- `allocCString(str)`
+ Allocates a new UTF-8-encoded, NUL-terminated copy of the given JS
+ string and returns its address relative to `config.heap()`. If
+ allocation returns 0 this function throws. Ownership of the memory
+ is transfered to the caller, who must eventually pass it to the
+ configured `config.dealloc()` function.
+
+- `config`
+ The configuration object passed to the [StructBinderFactory][],
+ primarily for accessing the memory (de)allocator and memory. Modifying
+ any of its "significant" configuration values may have undefined
+ results.
+
+<a name='api-structtype'></a>
+API: Struct Type
+------------------------------------------------------------
+
+The StructType class is a property of the [StructBinder][] function.
+
+Each constructor created by a [StructBinder][] inherits from _its own
+instance_ of the StructType class, which contains state specific to
+that struct type (e.g. the struct name and description metadata).
+StructTypes which are created via different [StructBinder][] instances
+are unrelated to each other, sharing no state except [StructBinderFactory][]
+config options.
+
+The StructType constructor cannot be called from client code. It is
+only called by the [StructBinder][]-generated
+[constructors][StructCtors]. The `StructBinder.StructType` object
+has the following "static" properties (^Which are accessible from
+individual instances via `theInstance.constructor`.):
+
+- `addOnDispose(...value)`\
+ If this object has no `ondispose` property, this function creates it
+ as an array and pushes the given value(s) onto it. If the object has
+ a function-typed `ondispose` property, this call replaces it with an
+ array and moves that function into the array. In all other cases,
+ `ondispose` is assumed to be an array and the argument(s) is/are
+ appended to it. Returns `this`.
+
+- `allocCString(str)`
+ Identical to the [StructBinder][] method of the same name.
+
+- `hasExternalPointer(object)`
+ Returns true if the given object's `pointer` member refers to an
+ "external" object. That is the case when a pointer is passed to a
+ [struct's constructor][StructCtors]. If true, the memory is owned by
+ someone other than the object and must outlive the object.
+
+- `isA(value)`
+ Returns true if its argument is a StructType instance _from the same
+ [StructBinder][]_ as this StructType.
+
+- `memberKey(string)`
+ Returns the given string wrapped in the configured `memberPrefix`
+ and `memberSuffix` values. e.g. if passed `"x"` and `memberPrefix`
+ is `"$"` then it returns `"$x"`. This does not verify that the
+ property is actually a struct a member, it simply transforms the
+ given string. TODO(?): add a 2nd parameter indicating whether it
+ should validate that it's a known member name.
+
+The base StructType prototype has the following members, all of which
+are inherited by [struct instances](#api-structinstance) and may only
+legally be called on concrete struct instances unless noted otherwise:
+
+- `dispose()`
+ Frees, if appropriate, the WASM-allocated memory which is allocated
+ by the constructor. If this is not called before the JS engine
+ cleans up the object, a leak in the WASM heap memory pool will result.
+ When `dispose()` is called, if the object has a property named `ondispose`
+ then it is treated as follows:
+ - If it is a function, it is called with the struct object as its `this`.
+ That method must not throw - if it does, the exception will be
+ ignored.
+ - If it is an array, it may contain functions, pointers, other
+ [StructType] instances, and/or JS strings. If an entry is a
+ function, it is called as described above. If it's a number, it's
+ assumed to be a pointer and is passed to the `dealloc()` function
+ configured for the parent [StructBinder][]. If it's a
+ [StructType][] instance then its `dispose()` method is called. If
+ it's a JS string, it's assumed to be a helpful description of the
+ next entry in the list and is simply ignored. Strings are
+ supported primarily for use as debugging information.
+ - Some struct APIs will manipulate the `ondispose` member, creating
+ it as an array or converting it from a function to array as
+ needed.
+
+- `lookupMember(memberName,throwIfNotFound=true)`
+ Given the name of a mapped struct member, it returns the member
+ description object. If not found, it either throws (if the 2nd
+ argument is true) or returns `undefined` (if the second argument is
+ false). The first argument may be either the member name as it is
+ mapped in the struct description or that same name with the
+ configured `memberPrefix` and `memberSuffix` applied, noting that
+ the lookup in the former case is faster.\
+ This method may be called directly on the prototype, without a
+ struct instance.
+
+- `memberToJsString(memberName)`
+ Uses `this.lookupMember(memberName,true)` to look up the given
+ member. If its signature is `s` then it is assumed to refer to a
+ NUL-terminated, UTF-8-encoded string and its memory is decoded as
+ such. If its signature is not one of those then an exception is
+ thrown. If its address is 0, `null` is returned. See also:
+ `setMemberCString()`.
+
+- `memberIsString(memberName [,throwIfNotFound=true])`
+ Uses `this.lookupMember(memberName,throwIfNotFound)` to look up the
+ given member. Returns the member description object if the member
+ has a signature of `s`, else returns false. If the given member is
+ not found, it throws if the 2nd argument is true, else it returns
+ false.
+
+- `memberKey(string)`
+ Works identically to `StructBinder.StructType.memberKey()`.
+
+- `memberKeys()`
+ Returns an array of the names of the properties of this object
+ which refer to C-side struct counterparts.
+
+- `memberSignature(memberName [,emscriptenFormat=false])`
+ Returns the signature for a given a member property, either in this
+ framework's format or, if passed a truthy 2nd argument, in a format
+ suitable for the 2nd argument to Emscripten's `addFunction()`.
+ Throws if the first argument does not resolve to a struct-bound
+ member name. The member name is resolved using `this.lookupMember()`
+ and throws if the member is found mapped.
+
+- `memoryDump()`
+ Returns a Uint8Array which contains the current state of this
+ object's raw memory buffer. Potentially useful for debugging, but
+ not much else. Note that the memory is necessarily, for
+ compatibility with C, written in the host platform's endianness and
+ is thus not useful as a persistent/portable serialization format.
+
+- `setMemberCString(memberName,str)`
+ Uses `StructType.allocCString()` to allocate a new C-style string,
+ assign it to the given member, and add the new string to this
+ object's `ondispose` list for cleanup when `this.dispose()` is
+ called. This function throws if `lookupMember()` fails for the given
+ member name, if allocation of the string fails, or if the member has
+ a signature value of anything other than `s`. Returns `this`.
+ *Achtung*: calling this repeatedly will not immediately free the
+ previous values because this code cannot know whether they are in
+ use in other places, namely C. Instead, each time this is called,
+ the prior value is retained in the `ondispose` list for cleanup when
+ the struct is disposed of. Because of the complexities and general
+ uncertainties of memory ownership and lifetime in such
+ constellations, it is recommended that the use of C-string members
+ from JS be kept to a minimum or that the relationship be one-way:
+ let C manage the strings and only fetch them from JS using, e.g.,
+ `memberToJsString()`.
+
+
+<a name='api-structctor'></a>
+API: Struct Constructors
+------------------------------------------------------------
+
+Struct constructors (the functions returned from [StructBinder][])
+are used for, intuitively enough, creating new instances of a given
+struct type:
+
+>
+```
+const x = new MyStruct;
+```
+
+Normally they should be passed no arguments, but they optionally
+accept a single argument: a WASM heap pointer address of memory
+which the object will use for storage. It does _not_ take over
+ownership of that memory and that memory must be valid at
+for least as long as this struct instance. This is used, for example,
+to proxy static/shared C-side instances:
+
+>
+```
+const x = new MyStruct( someCFuncWhichReturnsAMyStructPointer() );
+...
+x.dispose(); // does NOT free the memory
+```
+
+The JS-side construct does not own the memory in that case and has no
+way of knowing when the C-side struct is destroyed. Results are
+specifically undefined if the JS-side struct is used after the C-side
+struct's member is freed.
+
+> Potential TODO: add a way of passing ownership of the C-side struct
+to the JS-side object. e.g. maybe simply pass `true` as the second
+argument to tell the constructor to take over ownership. Currently the
+pointer can be taken over using something like
+`myStruct.ondispose=[myStruct.pointer]` immediately after creation.
+
+These constructors have the following "static" members:
+
+- `isA(value)`
+ Returns true if its argument was created by this constructor.
+
+- `memberKey(string)`
+ Works exactly as documented for [StructType][].
+
+- `memberKeys(string)`
+ Works exactly as documented for [StructType][].
+
+- `structInfo`
+ The structure description passed to [StructBinder][] when this
+ constructor was generated.
+
+- `structName`
+ The structure name passed to [StructBinder][] when this constructor
+ was generated.
+
+
+<a name='api-structprototype'></a>
+API: Struct Prototypes
+------------------------------------------------------------
+
+The prototypes of structs created via [the constructors described in
+the previous section][StructCtors] are each a struct-type-specific
+instance of [StructType][] and add the following struct-type-specific
+properties to the mix:
+
+- `structInfo`
+ The struct description metadata, as it was given to the
+ [StructBinder][] which created this class.
+
+- `structName`
+ The name of the struct, as it was given to the [StructBinder][] which
+ created this class.
+
+<a name='api-structinstance'></a>
+API: Struct Instances
+------------------------------------------------------------------------
+
+Instances of structs created via [the constructors described
+above][StructCtors] each have the following instance-specific state in
+common:
+
+- `pointer`
+ A read-only numeric property which is the "pointer" returned by the
+ configured allocator when this object is constructed. After
+ `dispose()` (inherited from [StructType][]) is called, this property
+ has the `undefined` value. When calling C-side code which takes a
+ pointer to a struct of this type, simply pass it `myStruct.pointer`.
+
+<a name='appendices'></a>
+Appendices
+============================================================
+
+<a name='appendix-a'></a>
+Appendix A: Limitations, TODOs, and Non-TODOs
+------------------------------------------------------------
+
+- This library only supports the basic set of member types supported
+ by WASM: numbers (which includes pointers). Nested structs are not
+ handled except that a member may be a _pointer_ to such a
+ struct. Whether or not it ever will depends entirely on whether its
+ developer ever needs that support. Conversion of strings between
+ JS and C requires infrastructure specific to each WASM environment
+ and is not directly supported by this library.
+
+- Binding functions to struct instances, such that C can see and call
+ JS-defined functions, is not as transparent as it really could be,
+ due to [shortcomings in the Emscripten
+ `addFunction()`/`removeFunction()`
+ interfaces](https://github.com/emscripten-core/emscripten/issues/17323). Until
+ a replacement for that API can be written, this support will be
+ quite limited. It _is_ possible to bind a JS-defined function to a
+ C-side function pointer and call that function from C. What's
+ missing is easier-to-use/more transparent support for doing so.
+ - In the meantime, a [standalone
+ subproject](/file/common/whwasmutil.js) of Jaccwabyt provides such a
+ binding mechanism, but integrating it directly with Jaccwabyt would
+ not only more than double its size but somehow feels inappropriate, so
+ experimentation is in order for how to offer that capability via
+ completely optional [StructBinderFactory][] config options.
+
+- It "might be interesting" to move access of the C-bound members into
+ a sub-object. e.g., from JS they might be accessed via
+ `myStructInstance.s.structMember`. The main advantage is that it would
+ eliminate any potential confusion about which members are part of
+ the C struct and which exist purely in JS. "The problem" with that
+ is that it requires internally mapping the `s` member back to the
+ object which contains it, which makes the whole thing more costly
+ and adds one more moving part which can break. Even so, it's
+ something to try out one rainy day. Maybe even make it optional and
+ make the `s` name configurable via the [StructBinderFactory][]
+ options. (Over-engineering is an arguably bad habit of mine.)
+
+- It "might be interesting" to offer (de)serialization support. It
+ would be very limited, e.g. we can't serialize arbitrary pointers in
+ any meaningful way, but "might" be useful for structs which contain
+ only numeric or C-string state. As it is, it's easy enough for
+ client code to write wrappers for that and handle the members in
+ ways appropriate to their apps. Any impl provided in this library
+ would have the shortcoming that it may inadvertently serialize
+ pointers (since they're just integers), resulting in potential chaos
+ after deserialization. Perhaps the struct description can be
+ extended to tag specific members as serializable and how to
+ serialize them.
+
+<a name='appendix-d'></a>
+Appendix D: Debug Info
+------------------------------------------------------------
+
+The [StructBinderFactory][], [StructBinder][], and [StructType][] classes
+all have the following "unsupported" method intended primarily
+to assist in their own development, as opposed to being for use in
+client code:
+
+- `debugFlags(flags)` (integer)
+ An "unsupported" debugging option which may change or be removed at
+ any time. Its argument is a set of flags to enable/disable certain
+ debug/tracing output for property accessors: 0x01 for getters, 0x02
+ for setters, 0x04 for allocations, 0x08 for deallocations. Pass 0 to
+ disable all flags and pass a negative value to _completely_ clear
+ all flags. The latter has the side effect of telling the flags to be
+ inherited from the next-higher-up class in the hierarchy, with
+ [StructBinderFactory][] being top-most, followed by [StructBinder][], then
+ [StructType][].
+
+
+<a name='appendix-g'></a>
+Appendix G: Generating Struct Descriptions From C
+------------------------------------------------------------
+
+Struct definitions are _ideally_ generated from WASM-compiled C, as
+opposed to simply guessing the sizeofs and offsets, so that the sizeof
+and offset information can be collected using C's `sizeof()` and
+`offsetof()` features (noting that struct padding may impact offsets
+in ways which might not be immediately obvious, so writing them by
+hand is _most certainly not recommended_).
+
+How exactly the desciption is generated is necessarily
+project-dependent. It's tempting say, "oh, that's easy! We'll just
+write it by hand!" but that would be folly. The struct sizes and byte
+offsets into the struct _must_ be precisely how C-side code sees the
+struct or the runtime results are completely undefined.
+
+The approach used in developing and testing _this_ software is...
+
+Below is a complete copy/pastable example of how we can use a small
+set of macros to generate struct descriptions from C99 or later into
+static string memory. Simply add such a file to your WASM build,
+arrange for its function to be exported[^export-func], and call it
+from JS (noting that it requires environment-specific JS glue to
+convert the returned pointer to a JS-side string). Use `JSON.parse()`
+to process it, then feed the included struct descriptions into the
+binder factory at your leisure.
+
+------------------------------------------------------------
+
+```c
+#include <string.h> /* memset() */
+#include <stddef.h> /* offsetof() */
+#include <stdio.h> /* snprintf() */
+#include <stdint.h> /* int64_t */
+#include <assert.h>
+
+struct ExampleStruct {
+ int v4;
+ void * ppV;
+ int64_t v8;
+ void (*xFunc)(void*);
+};
+typedef struct ExampleStruct ExampleStruct;
+
+const char * wasm__ctype_json(void){
+ static char strBuf[512 * 8] = {0}
+ /* Static buffer which must be sized large enough for
+ our JSON. The string-generation macros try very
+ hard to assert() if this buffer is too small. */;
+ int n = 0, structCount = 0 /* counters for the macros */;
+ char * pos = &strBuf[1]
+ /* Write-position cursor. Skip the first byte for now to help
+ protect against a small race condition */;
+ char const * const zEnd = pos + sizeof(strBuf)
+ /* one-past-the-end cursor (virtual EOF) */;
+ if(strBuf[0]) return strBuf; // Was set up in a previous call.
+
+ ////////////////////////////////////////////////////////////////////
+ // First we need to build up our macro framework...
+
+ ////////////////////////////////////////////////////////////////////
+ // Core output-generating macros...
+#define lenCheck assert(pos < zEnd - 100)
+#define outf(format,...) \
+ pos += snprintf(pos, ((size_t)(zEnd - pos)), format, __VA_ARGS__); \
+ lenCheck
+#define out(TXT) outf("%s",TXT)
+#define CloseBrace(LEVEL) \
+ assert(LEVEL<5); memset(pos, '}', LEVEL); pos+=LEVEL; lenCheck
+
+ ////////////////////////////////////////////////////////////////////
+ // Macros for emiting StructBinders...
+#define StructBinder__(TYPE) \
+ n = 0; \
+ outf("%s{", (structCount++ ? ", " : "")); \
+ out("\"name\": \"" # TYPE "\","); \
+ outf("\"sizeof\": %d", (int)sizeof(TYPE)); \
+ out(",\"members\": {");
+#define StructBinder_(T) StructBinder__(T)
+// ^^^ extra indirection needed to expand CurrentStruct
+#define StructBinder StructBinder_(CurrentStruct)
+#define _StructBinder CloseBrace(2)
+#define M(MEMBER,SIG) \
+ outf("%s\"%s\": " \
+ "{\"offset\":%d,\"sizeof\": %d,\"signature\":\"%s\"}", \
+ (n++ ? ", " : ""), #MEMBER, \
+ (int)offsetof(CurrentStruct,MEMBER), \
+ (int)sizeof(((CurrentStruct*)0)->MEMBER), \
+ SIG)
+ // End of macros.
+ ////////////////////////////////////////////////////////////////////
+
+ ////////////////////////////////////////////////////////////////////
+ // With that out of the way, we can do what we came here to do.
+ out("\"structs\": ["); {
+
+// For each struct description, do...
+#define CurrentStruct ExampleStruct
+ StructBinder {
+ M(v4,"i");
+ M(ppV,"p");
+ M(v8,"j");
+ M(xFunc,"v(p)");
+ } _StructBinder;
+#undef CurrentStruct
+
+ } out( "]"/*structs*/);
+ ////////////////////////////////////////////////////////////////////
+ // Done! Finalize the output...
+ out("}"/*top-level wrapper*/);
+ *pos = 0;
+ strBuf[0] = '{'/*end of the race-condition workaround*/;
+ return strBuf;
+
+// If this file will ever be concatenated or #included with others,
+// it's good practice to clean up our macros:
+#undef StructBinder
+#undef StructBinder_
+#undef StructBinder__
+#undef M
+#undef _StructBinder
+#undef CloseBrace
+#undef out
+#undef outf
+#undef lenCheck
+}
+```
+
+------------------------------------------------------------
+
+<style>
+div.content {
+ counter-reset: h1 -1;
+}
+div.content h1, div.content h2, div.content h3 {
+ border-radius: 0.25em;
+ border-bottom: 1px solid #70707070;
+}
+div.content h1 {
+ counter-reset: h2;
+}
+div.content h1::before, div.content h2::before, div.content h3::before {
+ background-color: #a5a5a570;
+ margin-right: 0.5em;
+ border-radius: 0.25em;
+}
+div.content h1::before {
+ counter-increment: h1;
+ content: counter(h1) ;
+ padding: 0 0.5em;
+ border-radius: 0.25em;
+}
+div.content h2::before {
+ counter-increment: h2;
+ content: counter(h1) "." counter(h2);
+ padding: 0 0.5em 0 1.75em;
+ border-radius: 0.25em;
+}
+div.content h2 {
+ counter-reset: h3;
+}
+div.content h3::before {
+ counter-increment: h3;
+ content: counter(h1) "." counter(h2) "." counter(h3);
+ padding: 0 0.5em 0 2.5em;
+}
+div.content h3 {border-left-width: 2.5em}
+</style>
+
+[sqlite3]: https://sqlite.org
+[emscripten]: https://emscripten.org
+[sgb]: https://wanderinghorse.net/home/stephan/
+[appendix-g]: #appendix-g
+[StructBinderFactory]: #api-binderfactory
+[StructCtors]: #api-structctor
+[StructType]: #api-structtype
+[StructBinder]: #api-structbinder
+[StructInstance]: #api-structinstance
+[^export-func]: In Emscripten, add its name, prefixed with `_`, to the
+ project's `EXPORT_FUNCTIONS` list.
+[BigInt64Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array
+[TextDecoder]: https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
+[TextEncoder]: https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
+[MDN]: https://developer.mozilla.org/docs/Web/API
diff --git a/ext/wasm/module-symbols.html b/ext/wasm/module-symbols.html
new file mode 100644
index 0000000..c5c6bc1
--- /dev/null
+++ b/ext/wasm/module-symbols.html
@@ -0,0 +1,531 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <title>sqlite3 Module Symbols</title>
+ <style>
+ body {
+ font-size: 12.5pt;
+ padding-bottom: 1em;
+ }
+ </style>
+ </head>
+ <body>
+<div class="fossil-doc" data-title="sqlite3 Module Symbols"><!-- EXTRACT_BEGIN -->
+<!--
+ The part of this doc wrapped in div.fossil-doc gets snipped out
+ from the canonical copy in the main tree (ext/wasm/module-symbols.html)
+ and added to the wasm docs repository, where it's served from
+ fossil.
+-->
+ <style>
+ .pseudolist {
+ column-count: auto;
+ column-width: 12rem;
+ column-gap: 1.5em;
+ width: 90%;
+ margin: auto;
+ }
+ .pseudolist.wide {
+ column-width: 21rem;
+ }
+ .pseudolist.wide2 {
+ column-width: 25rem;
+ }
+ .pseudolist > span {
+ font-family: monospace;
+ margin: 0.25em 0;
+ display: block;
+ }
+ .pseudolist.wrap-anywhere {
+ overflow-wrap: anywhere;
+ }
+ .warning { color: firebrick }
+ .error { color: firebrick; background-color: yellow}
+ .hidden, .initially-hidden {
+ position: absolute !important;
+ opacity: 0 !important;
+ pointer-events: none !important;
+ display: none !important;
+ }
+ h1::before, h2::before, h3::before, h4::before {
+ /* Remove automatic numbering */
+ content: "" !important;
+ background-color: transparent !important;
+ margin: 0 !important;
+ border: 0 !important;
+ padding: 0 !important;
+ }
+ .func-wasm {
+
+ }
+ .func-wasm::after {
+ content: "WASM";
+ color: saddlebrown;
+ /* ^^^^ the color must be legible in both "bright" and "dark"
+ s ite themes. */
+ font-size: 0.65em;
+ /* baseline-shift: super; */
+ vertical-align: super;
+ }
+ </style>
+ <p id='module-load-status'><strong>Loading WASM module...</strong>
+ If this takes "a long time" it may have failed and the browser's
+ dev console may contain hints as to why.
+ </p>
+
+ <p>
+ This page lists the SQLite3 APIs exported
+ by <code>sqlite3.wasm</code> and exposed to clients
+ by <code>sqlite3.js</code>. These lists are generated dynamically
+ by loading the JS/WASM module and introspecting it, with the following
+ caveats:
+ </p>
+
+ <ul>
+ <li>Some APIs are explicitly filtered out of these lists because
+ they are strictly for internal use within the JS/WASM APIs and
+ its own test code.
+ </li>
+ <li>This page runs in the main UI thread so cannot see features
+ which are only available in a Worker thread. If this page were
+ to function via a Worker, it would not be able to see
+ functionality only available in the main thread. Either way, it
+ would be missing certain APIs.
+ </li>
+ </ul>
+
+ <div class='initially-hidden'>
+
+ <p>This page exposes a global symbol named <code>sqlite3</code>
+ which can be inspected using the browser's dev tools.
+ </p>
+
+ <p>Jump to...</p>
+ <ul>
+ <li><a href='#sqlite3-namespace'><code>sqlite3</code> namespace</a></li>
+ <li><a href='#sqlite3-version'><code>sqlite3.version</code> object</a></li>
+ <li><a href='#sqlite3-functions'><code>sqlite3_...()</code> functions</a></li>
+ <li><a href='#sqlite3-constants'><code>SQLITE_...</code> constants</a></li>
+ <li><a href='#sqlite3.oo1'><code>sqlite3.oo1</code> namespace</a>
+ <!--ul>
+ <li><a href='#sqlite3.oo1.DB'><code>sqlite3.oo1.DB</code></a></li>
+ <li><a href='#sqlite3.oo1.Stmt'><code>sqlite3.oo1.Stmt</code></a></li>
+ </ul-->
+ </li>
+ <li><a href='#sqlite3.wasm'><code>sqlite3.wasm</code> namespace</a></li>
+ <li><a href='#sqlite3.wasm.pstack'><code>sqlite3.wasm.pstack</code> namespace</a></li>
+ <li><a href='#compile-options'>Compilation options used in this module build</a></li>
+ </ul>
+
+ <a id="sqlite3-namespace"></a>
+ <h1><code>sqlite3</code> Namespace</h1>
+ <p>
+ The <code>sqlite3</code> namespace object exposes the following...
+ </p>
+ <div id='list-namespace' class='pseudolist'></div>
+
+ <a id="sqlite3-version"></a>
+ <h1><code>sqlite3.version</code> Object</h1>
+ <p>
+ The <code>sqlite3.version</code> object exposes the following...
+ </p>
+ <div id='list-version' class='pseudolist wide wrap-anywhere'></div>
+
+ <a id="sqlite3-functions"></a>
+ <h1><code>sqlite3_...()</code> Function List</h1>
+
+ <p>The <code>sqlite3.capi</code> namespace exposes the following
+ <a href='https://sqlite.org/c3ref/funclist.html'><code>sqlite3_...()</code>
+ functions</a>...
+ </p>
+ <div id='list-functions' class='pseudolist wide'></div>
+ <p>
+ <code class='func-wasm'></code> = function is specific to the JS/WASM
+ bindings, not part of the C API.
+ </p>
+
+ <a id="sqlite3-constants"></a>
+ <h1><code>SQLITE_...</code> Constants</h1>
+
+ <p>The <code>sqlite3.capi</code> namespace exposes the following
+ <a href='https://sqlite.org/c3ref/constlist.html'><code>SQLITE_...</code>
+ constants</a>...
+ </p>
+ <div id='list-constants' class='pseudolist wide'></div>
+
+ <a id="sqlite3.oo1"></a>
+ <h1><code>sqlite3.oo1</code> Namespace</h1>
+ <p>
+ The <code>sqlite3.oo1</code> namespace exposes the following...
+ </p>
+ <div id='list-oo1' class='pseudolist'></div>
+
+ <a id="sqlite3.wasm"></a>
+ <h1><code>sqlite3.wasm</code> Namespace</h1>
+ <p>
+ The <code>sqlite3.wasm</code> namespace exposes the
+ following...
+ </p>
+ <div id='list-wasm' class='pseudolist'></div>
+
+ <a id="sqlite3.wasm.pstack"></a>
+ <h1><code>sqlite3.wasm.pstack</code> Namespace</h1>
+ <p>
+ The <code>sqlite3.wasm.pstack</code> namespace exposes the
+ following...
+ </p>
+ <div id='list-wasm-pstack' class='pseudolist'></div>
+
+ <a id="compile-options"></a>
+ <h1>Compilation Options</h1>
+ <p>
+ <code>SQLITE_...</code> compilation options used in this build
+ of <code>sqlite3.wasm</code>...
+ </p>
+ <div id='list-compile-options' class='pseudolist wide2'></div>
+
+ </div><!-- .initially-hidden -->
+ <script src="jswasm/sqlite3.js">/* This tag MUST be inside the
+ fossil-doc block so that this part can work without modification in
+ the wasm docs repo. */</script>
+ <script>(async function(){
+ const apiLinks = Object.assign(Object.create(null),{
+ sqlite3_aggregate_context: 'www:/c3ref/aggregate_context.html',
+ sqlite3_auto_extension: 'wasm:/api-c-style.md#auto-extension',
+ sqlite3_bind_blob: 'www:/c3ref/bind_blob.html',
+ sqlite3_bind_double: 'www:/c3ref/bind_blob.html',
+ sqlite3_bind_int: 'www:/c3ref/bind_blob.html',
+ sqlite3_bind_int64: 'www:/c3ref/bind_blob.html',
+ sqlite3_bind_null: 'www:/c3ref/bind_blob.html',
+ sqlite3_bind_parameter_count: 'www:/c3ref/bind_parameter_count.html',
+ sqlite3_bind_parameter_index: 'www:/c3ref/bind_parameter_index.html',
+ sqlite3_bind_pointer: 'www:/c3ref/bind_blob.html',
+ sqlite3_bind_text: 'www:/c3ref/bind_blob.html',
+ sqlite3_busy_handler: 'www:/c3ref/busy_handler.html',
+ sqlite3_busy_timeout: 'www:/c3ref/busy_timeout.html',
+ sqlite3_cancel_auto_extension: 'wasm:/api-c-style.md#auto-extension',
+ sqlite3_changes: 'www:/c3ref/changes.html',
+ sqlite3_changes64: 'www:/c3ref/changes.html',
+ sqlite3_clear_bindings: 'www:/c3ref/clear_bindings.html',
+ sqlite3_close_v2: 'www:/c3ref/close.html',
+ sqlite3_collation_needed: 'www:/c3ref/collation_needed.html',
+ sqlite3_column_blob: 'www:/c3ref/column_blob.html',
+ sqlite3_column_bytes: 'www:/c3ref/column.html',
+ sqlite3_column_count: 'www:/c3ref/column_count.html',
+ sqlite3_column_double: 'www:/c3ref/column_blob.html',
+ sqlite3_column_int: 'www:/c3ref/column_blob.html',
+ sqlite3_column_int64: 'www:/c3ref/column_blob.html',
+ sqlite3_column_name: 'www:/c3ref/column_name.html',
+ sqlite3_column_text: 'www:/c3ref/column_blob.html',
+ sqlite3_column_type: 'www:/c3ref/column_blob.html',
+ sqlite3_column_value: 'www:/c3ref/column_blob.html',
+ sqlite3_commit_hook: 'wasm:/api-c-style.md#hook-api',
+ sqlite3_compileoption_get: 'www:/c3ref/compileoption_get.html',
+ sqlite3_compileoption_used: 'www:/c3ref/compileoption_get.html',
+ sqlite3_complete: 'www:/c3ref/complete.html',
+ sqlite3_config: 'www:/c3ref/config.html',
+ sqlite3_context_db_handle: 'www:/c3ref/context_db_handle.html',
+ sqlite3_create_collation: 'www:/c3ref/create_collation.html',
+ sqlite3_create_collation_v2: 'www:/c3ref/create_collation.html',
+ sqlite3_create_function: 'wasm:/api-c-style.md#sqlite3_create_function',
+ sqlite3_create_function_v2: 'wasm:/api-c-style.md#sqlite3_create_function',
+ sqlite3_create_module: 'www:/c3ref/create_module.html',
+ sqlite3_create_module_v2: 'www:/c3ref/create_module.html',
+ sqlite3_create_window_function: 'wasm:/api-c-style.md#sqlite3_create_function',
+ sqlite3_db_config: 'wasm:/api-c-style.md#sqlite3_db_config',
+ sqlite3_data_count: 'www:/c3ref/data_count.html',
+ sqlite3_db_filename: 'www:/c3ref/db_filename.html',
+ sqlite3_db_handle: 'www:/c3ref/db_handle.html',
+ sqlite3_db_name: 'www:/c3ref/db_name.html',
+ sqlite3_db_status: 'www:/c3ref/db_status.html',
+ sqlite3_declare_vtab: 'www:/c3ref/declare_vtab.html',
+ sqlite3_deserialize: 'wasm:/api-c-style.md#sqlite3_deserialize',
+ sqlite3_drop_modules: 'www:/c3ref/drop_modules.html',
+ sqlite3_errcode: 'www:/c3ref/errcode.html',
+ sqlite3_errmsg: 'www:/c3ref/errcode.html',
+ sqlite3_error_offset: 'www:/c3ref/error_offset.html',
+ sqlite3_errstr: 'www:/c3ref/errcode.html',
+ sqlite3_exec: 'wasm:/api-c-style.md#sqlite3_exec',
+ sqlite3_expanded_sql: 'www:/c3ref/expanded_sql.html',
+ sqlite3_extended_errcode: 'www:/c3ref/errcode.html',
+ sqlite3_extended_result_codes: 'www:/c3ref/extended_result_codes.html',
+ sqlite3_file_control: 'www:/c3ref/file_control.html',
+ sqlite3_finalize: 'www:/c3ref/finalize.html',
+ sqlite3_free: 'www:/c3ref/free.html',
+ sqlite3_get_auxdata: 'www:/c3ref/get_auxdata.html',
+ sqlite3_initialize: 'www:/c3ref/initialize.html',
+ sqlite3_keyword_check: 'www:/c3ref/keyword_check.html',
+ sqlite3_keyword_count: 'www:/c3ref/keyword_check.html',
+ sqlite3_keyword_name: 'www:/c3ref/keyword_check.html',
+ sqlite3_last_insert_rowid: 'www:/c3ref/last_insert_rowid.html',
+ sqlite3_libversion: 'www:/c3ref/libversion.html',
+ sqlite3_libversion_number: 'www:/c3ref/libversion.html',
+ sqlite3_limit: 'www:/c3ref/limit.html',
+ sqlite3_malloc: 'www:/c3ref/free.html',
+ sqlite3_malloc64: 'www:/c3ref/free.html',
+ sqlite3_msize: 'www:/c3ref/free.html',
+ sqlite3_open: 'www:/c3ref/open.html',
+ sqlite3_open_v2: 'www:/c3ref/open.html',
+ sqlite3_overload_function: 'www:/c3ref/overload_function.html',
+ sqlite3_prepare_v2: 'wasm:/api-c-style.md#sqlite3_prepare_v2',
+ sqlite3_prepare_v3: 'wasm:/api-c-style.md#sqlite3_prepare_v2',
+ sqlite3_progress_handler: 'www:/c3ref/progress_handler.html',
+ sqlite3_randomness: 'wasm:/api-c-style.md#sqlite3_randomness',
+ sqlite3_realloc: 'www:/c3ref/free.html',
+ sqlite3_realloc64: 'www:/c3ref/free.html',
+ sqlite3_reset: 'www:/c3ref/reset.html',
+ sqlite3_reset_auto_extension: 'wasm:/api-c-style.md#auto-extension',
+ sqlite3_result_blob: 'www:/c3ref/result_blob.html',
+ sqlite3_result_double: 'www:/c3ref/result_blob.html',
+ sqlite3_result_error: 'www:/c3ref/result_blob.html',
+ sqlite3_result_error_code: 'www:/c3ref/result_blob.html',
+ sqlite3_result_error_nomem: 'www:/c3ref/result_blob.html',
+ sqlite3_result_error_toobig: 'www:/c3ref/result_blob.html',
+ sqlite3_result_int: 'www:/c3ref/result_blob.html',
+ sqlite3_result_int64: 'www:/c3ref/result_blob.html',
+ sqlite3_result_null: 'www:/c3ref/result_blob.html',
+ sqlite3_result_pointer: 'www:/c3ref/result_blob.html',
+ sqlite3_result_subtype: 'www:/c3ref/result_subtype.html',
+ sqlite3_result_text: 'www:/c3ref/result_blob.html',
+ sqlite3_result_zeroblob: 'www:/c3ref/result_blob.html',
+ sqlite3_result_zeroblob64: 'www:/c3ref/result_blob.html',
+ sqlite3_rollback_hook: 'wasm:/api-c-style.md#hook-api',
+ sqlite3_serialize: 'www:/c3ref/serialize.html',
+ sqlite3_set_authorizer: 'wasm:/api-c-style.md#sqlite3_set_authorizer',
+ sqlite3_set_auxdata: 'www:/c3ref/get_auxdata.html',
+ sqlite3_set_last_insert_rowid: 'www:/c3ref/set_last_insert_rowid',
+ sqlite3_shutdown: 'www:/c3ref/initialize.html',
+ sqlite3_sourceid: 'www:/c3ref/libversion.html',
+ sqlite3_sql: 'www:/c3ref/expanded_sql.html',
+ sqlite3_status: 'www:/c3ref/status.html',
+ sqlite3_status64: 'www:/c3ref/status.html',
+ sqlite3_step: 'www:/c3ref/step.html',
+ sqlite3_stmt_isexplain: 'www:/c3ref/stmt_isexplain.html',
+ sqlite3_stmt_readonly: 'www:/c3ref/stmt_readonly.html',
+ sqlite3_stmt_status: 'www:/c3ref/stmt_status.html',
+ sqlite3_strglob: 'www:/c3ref/strglob.html',
+ sqlite3_stricmp: 'www:/c3ref/stricmp.html',
+ sqlite3_strlike: 'www:/c3ref/strlike.html',
+ sqlite3_strnicmp: 'www:/c3ref/strnicmp.html',
+ sqlite3_table_column_metadata: 'www:/c3ref/table_column_metadata.html',
+ sqlite3_total_changes: 'www:/c3ref/total_changes.html',
+ sqlite3_total_changes64: 'www:/c3ref/total_changes.html',
+ sqlite3_trace_v2: 'www:/c3ref/trace_v2.html',
+ sqlite3_txn_state: 'www:/c3ref/txn_state.html',
+ sqlite3_update_hook: 'wasm:/api-c-style.md#hook-api',
+ sqlite3_uri_boolean: 'www:/c3ref/uri_boolean.html',
+ sqlite3_uri_int64: 'www:/c3ref/uri_boolean.html',
+ sqlite3_uri_key: 'www:/c3ref/uri_boolean.html',
+ sqlite3_uri_parameter: 'www:/c3ref/uri_boolean.html',
+ sqlite3_user_data: 'www:/c3ref/user_data.html',
+ sqlite3_value_blob: 'www:/c3ref/value_blob.html',
+ sqlite3_value_bytes: 'www:/c3ref/value_blob.html',
+ sqlite3_value_double: 'www:/c3ref/value_blob.html',
+ sqlite3_value_dup: 'www:/c3ref/value_dup.html',
+ sqlite3_value_free: 'www:/c3ref/value_dup.html',
+ sqlite3_value_frombind: 'www:/c3ref/value_blob.html',
+ sqlite3_value_int: 'www:/c3ref/value_blob.html',
+ sqlite3_value_int64: 'www:/c3ref/value_blob.html',
+ sqlite3_value_nochange: 'www:/c3ref/value_blob.html',
+ sqlite3_value_numeric_type: 'www:/c3ref/value_blob.html',
+ sqlite3_value_pointer: 'www:/c3ref/value_blob.html',
+ sqlite3_value_subtype: 'www:/c3ref/value_subtype.html',
+ sqlite3_value_text: 'www:/c3ref/value_blob.html',
+ sqlite3_value_type: 'www:/c3ref/value_blob.html',
+ sqlite3_vfs_find: 'www:/c3ref/vfs_find.html',
+ sqlite3_vfs_register: 'www:/c3ref/vfs_find.html',
+ sqlite3_vfs_unregister: 'www:/c3ref/vfs_find.html',
+ sqlite3_vtab_collation: 'www:/c3ref/vtab_collation.html',
+ sqlite3_vtab_config: 'www:/c3ref/vtab_config.html',
+ sqlite3_vtab_distinct: 'www:/c3ref/vtab_distinct.html',
+ sqlite3_vtab_in: 'www:/c3ref/vtab_in.html',
+ sqlite3_vtab_in_first: 'www:/c3ref/vtab_in_first.html',
+ sqlite3_vtab_in_next: 'www:/c3ref/vtab_in_next.html',
+ sqlite3_vtab_nochange: 'www:/c3ref/vtab_nochange.html',
+ sqlite3_vtab_on_conflict: 'www:/c3ref/vtab_on_conflict.html',
+ sqlite3_vtab_rhs_value: 'www:/c3ref/vtab_rhs_value.html',
+
+ sqlite3_column_js: 'wasm:/api-c-style.md#sqlite3_column_js',
+ sqlite3_js_aggregate_context: 'wasm:/api-c-style.md#sqlite3_js_aggregate_context',
+ sqlite3_js_db_export: 'wasm:/api-c-style.md#sqlite3_js_db_export',
+ sqlite3_js_db_uses_vfs: 'wasm:/api-c-style.md#sqlite3_js_db_uses_vfs',
+ sqlite3_js_db_vfs: 'wasm:/api-c-style.md#sqlite3_js_db_vfs',
+ sqlite3_js_kvvfs_clear: 'wasm:/api-c-style.md#sqlite3_js_kvvfs',
+ sqlite3_js_kvvfs_size: 'wasm:/api-c-style.md#sqlite3_js_kvvfs',
+ sqlite3_js_posix_create_file: 'wasm:/api-c-style.md#sqlite3_js_posix_create_file',
+ sqlite3_js_rc_str: 'wasm:/api-c-style.md#sqlite3_js_rc_str',
+ sqlite3_js_vfs_create_file: 'wasm:/api-c-style.md#sqlite3_js_vfs_create_file',
+ sqlite3_js_vfs_list: 'wasm:/api-c-style.md#sqlite3_js_vfs_list',
+ sqlite3_result_error_js: 'wasm:/api-c-style.md#sqlite3_result_error_js',
+ sqlite3_result_js: 'wasm:/api-c-style.md#sqlite3_result_js',
+ sqlite3_value_to_js: 'wasm:/api-c-style.md#sqlite3_value_to_js',
+ sqlite3_values_to_js: 'wasm:/api-c-style.md#sqlite3_values_to_js',
+
+ xform: (v)=>{
+ if(v){
+ return v.replace('www:','https://sqlite.org')
+ .replace('wasm:','https://sqlite.org/wasm/doc/trunk');
+ }else{
+ return undefined;
+ }
+ }
+ });
+ const eNew = (tag,parent)=>{
+ const e = document.createElement(tag);
+ if(parent) parent.appendChild(e);
+ return e;
+ };
+ const eLi = (label,parent)=>{
+ const e = eNew('span',parent);
+ e.innerText = label;
+ return e;
+ };
+ const eLink = (label,url,parent)=>{
+ const w = eNew('span',parent);
+ const e = eNew('a',w);
+ if(url){
+ e.href = url;
+ e.target = 'sqlite3-api-docs';
+ }
+ e.innerText = label;
+ return w;
+ };
+ const E = (sel)=>document.querySelector(sel);
+ const EAll = (sel)=>document.querySelectorAll(sel);
+ const eFuncs = E('#list-functions'),
+ eConst = E('#list-constants');
+ const renderConst = function(name){
+ eLi(name, eConst);
+ };
+ const renderFunc = function(name){
+ let lbl = name+'()';
+ const e = eLink(lbl, apiLinks.xform(apiLinks[name]), eFuncs);
+ if(name.indexOf('_js')>0
+ || name.indexOf('_wasm')>0){
+ e.classList.add('func-wasm');
+ }
+ };
+ const renderGeneric = function(name,value,eParent){
+ let lbl;
+ if(value instanceof Function) lbl = name+'()';
+ else{
+ switch(typeof value){
+ case 'number': case 'boolean': case 'string':
+ lbl = name+' = '+JSON.stringify(value);
+ break;
+ default:
+ lbl = name + ' ['+(typeof value)+']';
+ }
+ }
+ const e = eLi(lbl, eParent);
+ if(name.startsWith('sqlite3_wasm')){
+ e.classList.add('func-wasm');
+ }
+ };
+ const renderIt = async function(sqlite3){
+ self.sqlite3 = sqlite3;
+ console.warn("sqlite3 installed as global symbol self.sqlite3.");
+ const capi = sqlite3.capi, wasm = sqlite3.wasm;
+ const cmpIcase = (a,b)=>a.toLowerCase().localeCompare(b.toLowerCase());
+ const renderX = function(tgtElem, container, keys){
+ for(const k of keys.sort(cmpIcase)){
+ renderGeneric(k, container[k], tgtElem);
+ }
+ };
+
+ const excludeNamespace = ['scriptInfo','StructBinder'];
+ renderX(
+ E('#list-namespace'), sqlite3,
+ Object.keys(sqlite3)
+ .filter((v)=>excludeNamespace.indexOf(v)<0)
+ );
+ renderX(
+ E('#list-version'), sqlite3.version,
+ Object.keys(sqlite3.version)
+ );
+
+ /* sqlite3_...() and SQLITE_... */
+ const lists = {c: [/*constants*/], f: [/*functions*/],
+ s: [/*structs*/]};
+ /* Exclude these from the function list... */
+ const excludeCapi = [
+ // WASMFS stuff:
+ 'sqlite3_wasmfs_filename_is_persistent',
+ 'sqlite3_wasmfs_opfs_dir'
+ ];
+ for(const [k,v] of Object.entries(capi)){
+ if(k.startsWith('SQLITE_')){
+ lists.c.push(k);
+ }else if(k.startsWith('sqlite3_')){
+ if(excludeCapi.indexOf(k)>=0) continue;
+ if(v.structInfo){
+ // assume this is a StructType-type.
+ continue;
+ }
+ lists.f.push(k);
+ }
+ }
+ lists.c.sort().forEach(renderConst);
+ lists.f.sort().forEach(renderFunc);
+ lists.c = lists.f = null;
+
+ renderX(E('#list-oo1'), sqlite3.oo1,
+ Object.keys(sqlite3.oo1) );
+
+ const excludeWasm = ['ctype'];
+ renderX(E('#list-wasm'),
+ wasm, Object.keys(wasm).filter((v)=>{
+ return !v.startsWith('sqlite3_wasm_')
+ && excludeWasm.indexOf(v)<0;
+ }));
+ const psKeys = Object.keys(wasm.pstack);
+ psKeys.push('pointer','quota','remaining');
+ renderX(E('#list-wasm-pstack'), wasm.pstack, psKeys);
+
+ const cou = wasm.compileOptionUsed();
+ //const cou2 = Object.create(null);
+ //Object.entries(cou).forEach((e)=>cou2['SQLITE_'+e[0]] = e[1]);
+ renderX(E('#list-compile-options'), cou, Object.keys(cou));
+ };
+
+ /**
+ This is a module object for use with the emscripten-installed
+ sqlite3InitModule() factory function.
+ */
+ const myModule = {
+ print: (...args)=>{console.log(...args)},
+ printErr: (...args)=>{console.error(...args)},
+ /**
+ Called by the Emscripten module init bits to report loading
+ progress. It gets passed an empty argument when loading is done
+ (after onRuntimeInitialized() and any this.postRun callbacks
+ have been run).
+ */
+ setStatus: function f(text){
+ if(!f.last){
+ f.last = { text: '', step: 0 };
+ f.ui = {
+ status: E('#module-load-status')
+ };
+ }
+ if(text === f.last.text) return;
+ f.last.text = text;
+ ++f.last.step;
+ if(text) {
+ f.ui.status.classList.remove('hidden');
+ f.ui.status.innerText = text;
+ }else{
+ f.ui.status.classList.add('hidden');
+ EAll('.initially-hidden').forEach((e)=>{
+ e.classList.remove('initially-hidden');
+ });
+ }
+ }
+ }/*myModule*/;
+ self.sqlite3InitModule(myModule).then(renderIt);
+})();</script>
+</div><!-- .fossil-doc EXTRACT_END -->
+</body></html>
diff --git a/ext/wasm/scratchpad-wasmfs.html b/ext/wasm/scratchpad-wasmfs.html
new file mode 100644
index 0000000..c37febf
--- /dev/null
+++ b/ext/wasm/scratchpad-wasmfs.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>sqlite3 WASMFS/OPFS Main-thread Scratchpad</title>
+ </head>
+ <body>
+ <header id='titlebar'><span>sqlite3 WASMFS/OPFS Main-thread Scratchpad</span></header>
+ <p>Scratchpad/test app for the WASMF/OPFS integration in the
+ main window thread. This page requires that the sqlite3 API have
+ been built with WASMFS support. If OPFS support is available then
+ it "should" persist a database across reloads (watch the dev console
+ output), otherwise it will not.
+ </p>
+ <p>All stuff on this page happens in the dev console.</p>
+ <hr>
+ <div id='test-output'></div>
+ <script>
+ (function(){
+ const W = new Worker('scratchpad-wasmfs.mjs',{
+ type: 'module'
+ });
+ })();
+ </script>
+ </body>
+</html>
diff --git a/ext/wasm/scratchpad-wasmfs.mjs b/ext/wasm/scratchpad-wasmfs.mjs
new file mode 100644
index 0000000..d6b69a1
--- /dev/null
+++ b/ext/wasm/scratchpad-wasmfs.mjs
@@ -0,0 +1,70 @@
+/*
+ 2022-05-22
+
+ 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.
+
+ ***********************************************************************
+
+ A basic test script for sqlite3-api.js. This file must be run in
+ main JS thread and sqlite3.js must have been loaded before it.
+*/
+import sqlite3InitModule from './jswasm/sqlite3-wasmfs.mjs';
+//console.log('sqlite3InitModule =',sqlite3InitModule);
+const toss = function(...args){throw new Error(args.join(' '))};
+const log = console.log.bind(console),
+ warn = console.warn.bind(console),
+ error = console.error.bind(console);
+
+const stdout = log;
+const stderr = error;
+
+const test1 = function(db){
+ db.exec("create table if not exists t(a);")
+ .transaction(function(db){
+ db.prepare("insert into t(a) values(?)")
+ .bind(new Date().getTime())
+ .stepFinalize();
+ stdout("Number of values in table t:",
+ db.selectValue("select count(*) from t"));
+ });
+};
+
+const runTests = function(sqlite3){
+ const capi = sqlite3.capi,
+ oo = sqlite3.oo1,
+ wasm = sqlite3.wasm;
+ stdout("Loaded module:",sqlite3);
+ stdout("Loaded sqlite3:",capi.sqlite3_libversion(), capi.sqlite3_sourceid());
+ const persistentDir = capi.sqlite3_wasmfs_opfs_dir();
+ if(persistentDir){
+ stdout("Persistent storage dir:",persistentDir);
+ }else{
+ stderr("No persistent storage available.");
+ }
+ const startTime = performance.now();
+ let db;
+ try {
+ db = new oo.DB(persistentDir+'/foo.db');
+ stdout("DB filename:",db.filename);
+ const banner1 = '>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>',
+ banner2 = '<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<';
+ [
+ test1
+ ].forEach((f)=>{
+ const n = performance.now();
+ stdout(banner1,"Running",f.name+"()...");
+ f(db, sqlite3);
+ stdout(banner2,f.name+"() took ",(performance.now() - n),"ms");
+ });
+ }finally{
+ if(db) db.close();
+ }
+ stdout("Total test time:",(performance.now() - startTime),"ms");
+};
+
+sqlite3InitModule().then(runTests);
diff --git a/ext/wasm/speedtest1-wasmfs.html b/ext/wasm/speedtest1-wasmfs.html
new file mode 100644
index 0000000..c018583
--- /dev/null
+++ b/ext/wasm/speedtest1-wasmfs.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>speedtest1-wasmfs.wasm</title>
+ </head>
+ <body>
+ <header id='titlebar'><span>speedtest1-wasmfs.wasm</span></header>
+ <div>See also: <a href='speedtest1-worker.html'>speedtest1-worker</a></div>
+ <div class='warning'>Achtung: running it with the dev tools open may
+ <em>drastically</em> slow it down. For faster results, keep the dev
+ tools closed when running it!
+ </div>
+ <div id='test-output'></div>
+ <script>
+ (function(){
+ const eOut = document.querySelector('#test-output');
+ const log2 = function(cssClass,...args){
+ const ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ eOut.append(ln);
+ //this.e.output.lastElementChild.scrollIntoViewIfNeeded();
+ };
+ /* can't update DOM while speedtest is running unless we run
+ speedtest in a worker thread. */;
+ const log = (...args)=>{
+ console.log(...args);
+ log2('',...args);
+ };
+ const logErr = function(...args){
+ console.error(...args);
+ log2('error',...args);
+ };
+ const W = new Worker(
+ 'speedtest1-wasmfs.mjs'+globalThis.location.search,{
+ type: 'module'
+ });
+ log("Starting up...");
+ W.onmessage = function({data}){
+ switch(data.type){
+ case 'log': log(...data.args); break;
+ case 'logErr': logErr(...data.args); break;
+ default:
+ break;
+ }
+ };
+ })();
+ </script>
+ </body>
+</html>
diff --git a/ext/wasm/speedtest1-wasmfs.mjs b/ext/wasm/speedtest1-wasmfs.mjs
new file mode 100644
index 0000000..2d5ae32
--- /dev/null
+++ b/ext/wasm/speedtest1-wasmfs.mjs
@@ -0,0 +1,90 @@
+import sqlite3InitModule from './jswasm/speedtest1-wasmfs.mjs';
+const wMsg = (type,...args)=>{
+ postMessage({type, args});
+};
+wMsg('log',"speedtest1-wasmfs starting...");
+/**
+ If this environment contains OPFS, this function initializes it and
+ returns the name of the dir on which OPFS is mounted, else it returns
+ an empty string.
+*/
+const wasmfsDir = function f(wasmUtil,dirName="/opfs"){
+ if(undefined !== f._) return f._;
+ if( !self.FileSystemHandle
+ || !self.FileSystemDirectoryHandle
+ || !self.FileSystemFileHandle){
+ return f._ = "";
+ }
+ try{
+ if(0===wasmUtil.xCallWrapped(
+ 'sqlite3_wasm_init_wasmfs', 'i32', ['string'], dirName
+ )){
+ return f._ = dirName;
+ }else{
+ return f._ = "";
+ }
+ }catch(e){
+ // sqlite3_wasm_init_wasmfs() is not available
+ return f._ = "";
+ }
+};
+wasmfsDir._ = undefined;
+
+const log = (...args)=>wMsg('log',...args);
+const logErr = (...args)=>wMsg('logErr',...args);
+
+const runTests = function(sqlite3){
+ console.log("Module inited.",sqlite3);
+ const wasm = sqlite3.wasm;
+ const __unlink = wasm.xWrap("sqlite3_wasm_vfs_unlink", "int", ["*","string"]);
+ const unlink = (fn)=>__unlink(0,fn);
+ const pDir = wasmfsDir(wasm);
+ if(pDir) log("Persistent storage:",pDir);
+ else{
+ logErr("Expecting persistent storage in this build.");
+ return;
+ }
+ const scope = wasm.scopedAllocPush();
+ const dbFile = pDir+"/speedtest1.db";
+ const urlParams = new URL(self.location.href).searchParams;
+ const argv = ["speedtest1"];
+ if(urlParams.has('flags')){
+ argv.push(...(urlParams.get('flags').split(',')));
+ let i = argv.indexOf('--vfs');
+ if(i>=0) argv.splice(i,2);
+ }else{
+ argv.push(
+ "--singlethread",
+ "--nomutex",
+ //"--nosync",
+ "--nomemstat",
+ "--size", "10"
+ );
+ }
+
+ if(argv.indexOf('--memdb')>=0){
+ logErr("WARNING: --memdb flag trumps db filename.");
+ }
+ argv.push("--big-transactions"/*important for tests 410 and 510!*/,
+ dbFile);
+ //log("argv =",argv);
+ // These log messages are not emitted to the UI until after main() returns. Fixing that
+ // requires moving the main() call and related cleanup into a timeout handler.
+ if(pDir) unlink(dbFile);
+ log("Starting native app:\n ",argv.join(' '));
+ log("This will take a while and the browser might warn about the runaway JS.",
+ "Give it time...");
+ setTimeout(function(){
+ if(pDir) unlink(dbFile);
+ wasm.xCall('wasm_main', argv.length,
+ wasm.scopedAllocMainArgv(argv));
+ wasm.scopedAllocPop(scope);
+ if(pDir) unlink(dbFile);
+ log("Done running native main()");
+ }, 25);
+}/*runTests()*/;
+
+sqlite3InitModule({
+ print: log,
+ printErr: logErr
+}).then(runTests);
diff --git a/ext/wasm/speedtest1-worker.html b/ext/wasm/speedtest1-worker.html
new file mode 100644
index 0000000..8c9a77d
--- /dev/null
+++ b/ext/wasm/speedtest1-worker.html
@@ -0,0 +1,372 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>speedtest1.wasm Worker</title>
+ </head>
+ <body>
+ <header id='titlebar'>speedtest1.wasm Worker</header>
+ <div>See also: <a href='speedtest1.html'>A main-thread variant of this page.</a></div>
+ <!-- emscripten bits -->
+ <figure id="module-spinner">
+ <div class="spinner"></div>
+ <div class='center'><strong>Initializing app...</strong></div>
+ <div class='center'>
+ On a slow internet connection this may take a moment. If this
+ message displays for "a long time", intialization may have
+ failed and the JavaScript console may contain clues as to why.
+ </div>
+ </figure>
+ <div class="emscripten" id="module-status">Downloading...</div>
+ <div class="emscripten">
+ <progress value="0" max="100" id="module-progress" hidden='1'></progress>
+ </div><!-- /emscripten bits -->
+ <fieldset id='ui-controls' class='hidden'>
+ <legend>Options</legend>
+ <div id='toolbar'>
+ <div id='toolbar-select'>
+ <select id='select-flags' size='10' multiple></select>
+ <div>The following flags can be passed as URL parameters:
+ vfs=NAME, size=N, journal=MODE, cachesize=SIZE
+ </div>
+ </div>
+ <div class='toolbar-inner-vertical'>
+ <div id='toolbar-selected-flags'></div>
+ <div class='toolbar-inner-vertical'>
+ <span>&rarr; <a id='link-main-thread' href='#' target='speedtest-main'
+ title='Start speedtest1.html with the selected flags'>speedtest1</a>
+ </span>
+ <span class='hidden'>&rarr; <a id='link-wasmfs' href='#' target='speedtest-wasmfs'
+ title='Start speedtest1-wasmfs.html with the selected flags'>speedtest1-wasmfs</a>
+ </span>
+ <span>&rarr; <a id='link-kvvfs' href='#' target='speedtest-kvvfs'
+ title='Start kvvfs speedtest1 with the selected flags'>speedtest1-kvvfs</a>
+ </span>
+ </div>
+ </div>
+ <div class='toolbar-inner-vertical' id='toolbar-runner-controls'>
+ <button id='btn-reset-flags'>Reset Flags</button>
+ <button id='btn-output-clear'>Clear output</button>
+ <button id='btn-run'>Run</button>
+ </div>
+ </div>
+ </fieldset>
+ <div>
+ <span class='input-wrapper'>
+ <input type='checkbox' class='disable-during-eval' id='cb-reverse-log-order' checked></input>
+ <label for='cb-reverse-log-order' id='lbl-reverse-log-order'>Reverse log order</label>
+ </span>
+ </div>
+ <div id='test-output'>
+ </div>
+ <div id='tips'>
+ <strong>Tips:</strong>
+ <ul>
+ <li>Control-click the flags to (de)select multiple flags.</li>
+ <li>The <tt>--big-transactions</tt> flag is important for two
+ of the bigger tests. Without it, those tests create many
+ thousands of implicit transactions, reducing the affected
+ tests to an absolute crawl, in particular with OPFS.
+ </li>
+ <li>The easiest way to try different optimization levels is,
+ from this directory:
+ <pre>$ rm -f jswasm/speedtest1.js; make -e emcc_opt='-O2' speedtest1</pre>
+ Then reload this page. -O2 seems to consistently produce the fastest results.
+ </li>
+ </ul>
+ </div>
+ <style>
+ #test-output {
+ white-space: break-spaces;
+ overflow: auto;
+ }
+ div#tips { margin-top: 1em; }
+ #toolbar {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ }
+ #toolbar > * {
+ margin: 0 0.5em;
+ }
+ .toolbar-inner-vertical {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ }
+ #toolbar-select {
+ display: flex;
+ flex-direction: column;
+ }
+ .toolbar-inner-vertical > *, #toolbar-select > * {
+ margin: 0.2em 0;
+ }
+ #select-flags > option {
+ white-space: pre;
+ font-family: monospace;
+ }
+ fieldset {
+ border-radius: 0.5em;
+ }
+ #toolbar-runner-controls { flex-grow: 1 }
+ #toolbar-runner-controls > * { flex: 1 0 auto }
+ #toolbar-selected-flags::before {
+ font-family: initial;
+ content:"Selected flags: ";
+ }
+ #toolbar-selected-flags {
+ display: flex;
+ flex-direction: column;
+ font-family: monospace;
+ justify-content: flex-start;
+ }
+ </style>
+ <script>(function(){
+ 'use strict';
+ const E = (sel)=>document.querySelector(sel);
+ const eOut = E('#test-output');
+ const log2 = function(cssClass,...args){
+ let ln;
+ if(1 || cssClass){
+ ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ }else{
+ // This doesn't work with the "reverse order" option!
+ ln = document.createTextNode(args.join(' ')+'\n');
+ }
+ eOut.append(ln);
+ };
+ const log = (...args)=>{
+ //console.log(...args);
+ log2('', ...args);
+ };
+ const logErr = function(...args){
+ console.error(...args);
+ log2('error', ...args);
+ };
+ const logWarn = function(...args){
+ console.warn(...args);
+ log2('warning', ...args);
+ };
+
+ const spacePad = function(str,len=21){
+ if(str.length===len) return str;
+ else if(str.length>len) return str.substr(0,len);
+ const a = []; a.length = len - str.length;
+ return str+a.join(' ');
+ };
+ // OPTION elements seem to ignore white-space:pre, so do this the hard way...
+ const nbspPad = function(str,len=21){
+ if(str.length===len) return str;
+ else if(str.length>len) return str.substr(0,len);
+ const a = []; a.length = len - str.length;
+ return str+a.join('&nbsp;');
+ };
+
+ const urlParams = new URL(self.location.href).searchParams;
+ const W = new Worker(
+ "speedtest1-worker.js?sqlite3.dir=jswasm"+
+ (urlParams.has('opfs-verbose') ? '&opfs-verbose' : '')+
+ (urlParams.has('opfs-disable') ? '&opfs-disable' : '')
+ );
+ const mPost = function(msgType,payload){
+ W.postMessage({type: msgType, data: payload});
+ };
+
+ const eFlags = E('#select-flags');
+ const eSelectedFlags = E('#toolbar-selected-flags');
+ const eLinkMainThread = E('#link-main-thread');
+ const eLinkWasmfs = E('#link-wasmfs');
+ const eLinkKvvfs = E('#link-kvvfs');
+ const getSelectedFlags = ()=>{
+ const f = Array.prototype.map.call(eFlags.selectedOptions, (v)=>v.value);
+ [
+ 'size', 'vfs', 'journal', 'cachesize'
+ ].forEach(function(k){
+ if(urlParams.has(k)) f.push('--'+k, urlParams.get(k));
+ });
+ return f;
+ };
+ const updateSelectedFlags = function(){
+ eSelectedFlags.innerText = '';
+ const flags = getSelectedFlags();
+ flags.forEach(function(f){
+ const e = document.createElement('span');
+ e.innerText = f;
+ eSelectedFlags.appendChild(e);
+ });
+ const rxStripDash = /^(-+)?/;
+ const comma = flags.join(',');
+ eLinkMainThread.setAttribute('target', 'speedtest1-main-'+comma);
+ eLinkMainThread.href = 'speedtest1.html?flags='+comma;
+ eLinkWasmfs.setAttribute('target', 'speedtest1-wasmfs-'+comma);
+ eLinkWasmfs.href = 'speedtest1-wasmfs.html?flags='+comma;
+ eLinkKvvfs.setAttribute('target', 'speedtest1-kvvfs-'+comma);
+ eLinkKvvfs.href = 'speedtest1.html?vfs=kvvfs&flags='+comma;
+ };
+ eFlags.addEventListener('change', updateSelectedFlags );
+ {
+ const flags = Object.create(null);
+ /* TODO? Flags which require values need custom UI
+ controls and some of them make little sense here
+ (e.g. --script FILE). */
+ flags["--autovacuum"] = "Enable AUTOVACUUM mode";
+ flags["--big-transactions"] = "Important for tests 410 and 510!";
+ //flags["--cachesize"] = "N Set the cache size to N pages";
+ flags["--checkpoint"] = "Run PRAGMA wal_checkpoint after each test case";
+ flags["--exclusive"] = "Enable locking_mode=EXCLUSIVE";
+ flags["--explain"] = "Like --sqlonly but with added EXPLAIN keywords";
+ //flags["--heap"] = "SZ MIN Memory allocator uses SZ bytes & min allocation MIN";
+ flags["--incrvacuum"] = "Enable incremenatal vacuum mode";
+ //flags["--journal"] = "M Set the journal_mode to M";
+ //flags["--key"] = "KEY Set the encryption key to KEY";
+ //flags["--lookaside"] = "N SZ Configure lookaside for N slots of SZ bytes each";
+ flags["--memdb"] = "Use an in-memory database";
+ //flags["--mmap"] = "SZ MMAP the first SZ bytes of the database file";
+ flags["--multithread"] = "Set multithreaded mode";
+ flags["--nomemstat"] = "Disable memory statistics";
+ flags["--nomutex"] = "Open db with SQLITE_OPEN_NOMUTEX";
+ flags["--nosync"] = "Set PRAGMA synchronous=OFF";
+ flags["--notnull"] = "Add NOT NULL constraints to table columns";
+ //flags["--output"] = "FILE Store SQL output in FILE";
+ //flags["--pagesize"] = "N Set the page size to N";
+ //flags["--pcache"] = "N SZ Configure N pages of pagecache each of size SZ bytes";
+ //flags["--primarykey"] = "Use PRIMARY KEY instead of UNIQUE where appropriate";
+ //flags["--repeat"] = "N Repeat each SELECT N times (default: 1)";
+ flags["--reprepare"] = "Reprepare each statement upon every invocation";
+ //flags["--reserve"] = "N Reserve N bytes on each database page";
+ //flags["--script"] = "FILE Write an SQL script for the test into FILE";
+ flags["--serialized"] = "Set serialized threading mode";
+ flags["--singlethread"] = "Set single-threaded mode - disables all mutexing";
+ flags["--sqlonly"] = "No-op. Only show the SQL that would have been run.";
+ flags["--shrink-memory"] = "Invoke sqlite3_db_release_memory() frequently.";
+ //flags["--size"] = "N Relative test size. Default=100";
+ flags["--strict"] = "Use STRICT table where appropriate";
+ flags["--stats"] = "Show statistics at the end";
+ //flags["--temp"] = "N N from 0 to 9. 0: no temp table. 9: all temp tables";
+ //flags["--testset"] = "T Run test-set T (main, cte, rtree, orm, fp, debug)";
+ flags["--trace"] = "Turn on SQL tracing";
+ //flags["--threads"] = "N Use up to N threads for sorting";
+ /*
+ The core API's WASM build does not support UTF16, but in
+ this app it's not an issue because the data are not crossing
+ JS/WASM boundaries.
+ */
+ flags["--utf16be"] = "Set text encoding to UTF-16BE";
+ flags["--utf16le"] = "Set text encoding to UTF-16LE";
+ flags["--verify"] = "Run additional verification steps.";
+ flags["--without-rowid"] = "Use WITHOUT ROWID where appropriate";
+ const preselectedFlags = [
+ '--big-transactions',
+ '--singlethread'
+ ];
+ if(urlParams.has('flags')){
+ preselectedFlags.push(...urlParams.get('flags').split(','));
+ }
+ if(!urlParams.get('vfs')){
+ preselectedFlags.push('--memdb');
+ }
+ Object.keys(flags).sort().forEach(function(f){
+ const opt = document.createElement('option');
+ eFlags.appendChild(opt);
+ const lbl = nbspPad(f)+flags[f];
+ //opt.innerText = lbl;
+ opt.innerHTML = lbl;
+ opt.value = f;
+ if(preselectedFlags.indexOf(f) >= 0) opt.selected = true;
+ });
+ const cbReverseLog = E('#cb-reverse-log-order');
+ const lblReverseLog = E('#lbl-reverse-log-order');
+ if(cbReverseLog.checked){
+ lblReverseLog.classList.add('warning');
+ eOut.classList.add('reverse');
+ }
+ cbReverseLog.addEventListener('change', function(){
+ if(this.checked){
+ eOut.classList.add('reverse');
+ lblReverseLog.classList.add('warning');
+ }else{
+ eOut.classList.remove('reverse');
+ lblReverseLog.classList.remove('warning');
+ }
+ }, false);
+ updateSelectedFlags();
+ }
+ E('#btn-output-clear').addEventListener('click', ()=>{
+ eOut.innerText = '';
+ });
+ E('#btn-reset-flags').addEventListener('click',()=>{
+ eFlags.value = '';
+ updateSelectedFlags();
+ });
+ E('#btn-run').addEventListener('click',function(){
+ log("Running speedtest1. UI controls will be disabled until it completes.");
+ mPost('run', getSelectedFlags());
+ });
+
+ const eControls = E('#ui-controls');
+ /** Update Emscripten-related UI elements while loading the module. */
+ const updateLoadStatus = function f(text){
+ if(!f.last){
+ f.last = { text: '', step: 0 };
+ const E = (cssSelector)=>document.querySelector(cssSelector);
+ f.ui = {
+ status: E('#module-status'),
+ progress: E('#module-progress'),
+ spinner: E('#module-spinner')
+ };
+ }
+ if(text === f.last.text) return;
+ f.last.text = text;
+ if(f.ui.progress){
+ f.ui.progress.value = f.last.step;
+ f.ui.progress.max = f.last.step + 1;
+ }
+ ++f.last.step;
+ if(text) {
+ f.ui.status.classList.remove('hidden');
+ f.ui.status.innerText = text;
+ }else{
+ if(f.ui.progress){
+ f.ui.progress.remove();
+ f.ui.spinner.remove();
+ delete f.ui.progress;
+ delete f.ui.spinner;
+ }
+ f.ui.status.classList.add('hidden');
+ }
+ };
+
+ W.onmessage = function(msg){
+ msg = msg.data;
+ switch(msg.type){
+ case 'ready':
+ log("Worker is ready.");
+ eControls.classList.remove('hidden');
+ break;
+ case 'stdout': log(msg.data); break;
+ case 'stderr': logErr(msg.data); break;
+ case 'run-start':
+ eControls.disabled = true;
+ log("Running speedtest1 with argv =",msg.data.join(' '));
+ break;
+ case 'run-end':
+ log("speedtest1 finished.");
+ eControls.disabled = false;
+ // app output is in msg.data
+ break;
+ case 'error': logErr(msg.data); break;
+ case 'load-status': updateLoadStatus(msg.data); break;
+ default:
+ logErr("Unhandled worker message type:",msg);
+ break;
+ }
+ };
+})();</script>
+ </body>
+</html>
diff --git a/ext/wasm/speedtest1-worker.js b/ext/wasm/speedtest1-worker.js
new file mode 100644
index 0000000..3abc589
--- /dev/null
+++ b/ext/wasm/speedtest1-worker.js
@@ -0,0 +1,127 @@
+'use strict';
+(function(){
+ let speedtestJs = 'speedtest1.js';
+ const urlParams = new URL(self.location.href).searchParams;
+ if(urlParams.has('sqlite3.dir')){
+ speedtestJs = urlParams.get('sqlite3.dir') + '/' + speedtestJs;
+ }
+ importScripts(speedtestJs);
+ /**
+ If this build includes WASMFS, this function initializes it and
+ returns the name of the dir on which OPFS is mounted, else it
+ returns an empty string.
+ */
+ const wasmfsDir = function f(wasmUtil){
+ if(undefined !== f._) return f._;
+ const pdir = '/opfs';
+ if( !self.FileSystemHandle
+ || !self.FileSystemDirectoryHandle
+ || !self.FileSystemFileHandle){
+ return f._ = "";
+ }
+ try{
+ if(0===wasmUtil.xCallWrapped(
+ 'sqlite3_wasm_init_wasmfs', 'i32', ['string'], pdir
+ )){
+ return f._ = pdir;
+ }else{
+ return f._ = "";
+ }
+ }catch(e){
+ // sqlite3_wasm_init_wasmfs() is not available
+ return f._ = "";
+ }
+ };
+ wasmfsDir._ = undefined;
+
+ const mPost = function(msgType,payload){
+ postMessage({type: msgType, data: payload});
+ };
+
+ const App = Object.create(null);
+ App.logBuffer = [];
+ const logMsg = (type,msgArgs)=>{
+ const msg = msgArgs.join(' ');
+ App.logBuffer.push(msg);
+ mPost(type,msg);
+ };
+ const log = (...args)=>logMsg('stdout',args);
+ const logErr = (...args)=>logMsg('stderr',args);
+ const realSahName = 'opfs-sahpool-speedtest1';
+
+ const runSpeedtest = async function(cliFlagsArray){
+ const scope = App.wasm.scopedAllocPush();
+ const dbFile = App.pDir+"/speedtest1.sqlite3";
+ try{
+ const argv = [
+ "speedtest1.wasm", ...cliFlagsArray, dbFile
+ ];
+ App.logBuffer.length = 0;
+ const ndxSahPool = argv.indexOf('opfs-sahpool');
+ if(ndxSahPool>0){
+ argv[ndxSahPool] = realSahName;
+ log("Updated argv for opfs-sahpool: --vfs",realSahName);
+ }
+ mPost('run-start', [...argv]);
+ if(App.sqlite3.installOpfsSAHPoolVfs
+ && !App.sqlite3.$SAHPoolUtil
+ && ndxSahPool>0){
+ log("Installing opfs-sahpool as",realSahName,"...");
+ await App.sqlite3.installOpfsSAHPoolVfs({
+ name: realSahName,
+ initialCapacity: 3,
+ clearOnInit: true,
+ verbosity: 2
+ }).then(PoolUtil=>{
+ log("opfs-sahpool successfully installed as",PoolUtil.vfsName);
+ App.sqlite3.$SAHPoolUtil = PoolUtil;
+ //console.log("sqlite3.oo1.OpfsSAHPoolDb =", App.sqlite3.oo1.OpfsSAHPoolDb);
+ });
+ }
+ App.wasm.xCall('wasm_main', argv.length,
+ App.wasm.scopedAllocMainArgv(argv));
+ }catch(e){
+ mPost('error',e.message);
+ }finally{
+ App.wasm.scopedAllocPop(scope);
+ mPost('run-end', App.logBuffer.join('\n'));
+ App.logBuffer.length = 0;
+ }
+ };
+
+ self.onmessage = function(msg){
+ msg = msg.data;
+ switch(msg.type){
+ case 'run':
+ runSpeedtest(msg.data || [])
+ .catch(e=>mPost('error',e));
+ break;
+ default:
+ logErr("Unhandled worker message type:",msg.type);
+ break;
+ }
+ };
+
+ const EmscriptenModule = {
+ print: log,
+ printErr: logErr,
+ setStatus: (text)=>mPost('load-status',text)
+ };
+ log("Initializing speedtest1 module...");
+ self.sqlite3InitModule(EmscriptenModule).then(async (sqlite3)=>{
+ const S = globalThis.S = App.sqlite3 = sqlite3;
+ log("Loaded speedtest1 module. Setting up...");
+ App.vfsUnlink = function(pDb, fname){
+ const pVfs = S.wasm.sqlite3_wasm_db_vfs(pDb, 0);
+ if(pVfs) S.wasm.sqlite3_wasm_vfs_unlink(pVfs, fname||0);
+ };
+ App.pDir = wasmfsDir(S.wasm);
+ App.wasm = S.wasm;
+ //if(App.pDir) log("Persistent storage:",pDir);
+ //else log("Using transient storage.");
+ mPost('ready',true);
+ log("Registered VFSes:", ...S.capi.sqlite3_js_vfs_list());
+ }).catch(e=>{
+ logErr(e);
+ });
+})();
diff --git a/ext/wasm/speedtest1.html b/ext/wasm/speedtest1.html
new file mode 100644
index 0000000..9cc2092
--- /dev/null
+++ b/ext/wasm/speedtest1.html
@@ -0,0 +1,174 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>speedtest1.wasm</title>
+ </head>
+ <body>
+ <header id='titlebar'><span>speedtest1.wasm</span></header>
+ <div>See also: <a href='speedtest1-worker.html'>A Worker-thread variant of this page.</a></div>
+ <!-- emscripten bits -->
+ <figure id="module-spinner">
+ <div class="spinner"></div>
+ <div class='center'><strong>Initializing app...</strong></div>
+ <div class='center'>
+ On a slow internet connection this may take a moment. If this
+ message displays for "a long time", intialization may have
+ failed and the JavaScript console may contain clues as to why.
+ </div>
+ </figure>
+ <div class="emscripten" id="module-status">Downloading...</div>
+ <div class="emscripten">
+ <progress value="0" max="100" id="module-progress" hidden='1'></progress>
+ </div><!-- /emscripten bits -->
+ <div class='warning'>This page starts running the main exe when it loads, which will
+ block the UI until it finishes! Adding UI controls to manually configure and start it
+ are TODO.</div>
+ </div>
+ <div class='warning'>Achtung: running it with the dev tools open may
+ <em>drastically</em> slow it down. For faster results, keep the dev
+ tools closed when running it!
+ </div>
+ <div>Output is delayed/buffered because we cannot update the UI while the
+ speedtest is running. Output will appear below when ready...
+ <div id='test-output'></div>
+ <script src="common/SqliteTestUtil.js"></script>
+ <script src="jswasm/speedtest1.js"></script>
+ <script>(function(){
+ /**
+ If this environment contains WASMFS with OPFS, this function
+ initializes it and returns the name of the dir on which OPFS is
+ mounted, else it returns an empty string.
+ */
+ const wasmfsDir = function f(wasmUtil){
+ if(undefined !== f._) return f._;
+ const pdir = '/persistent';
+ if( !self.FileSystemHandle
+ || !self.FileSystemDirectoryHandle
+ || !self.FileSystemFileHandle){
+ return f._ = "";
+ }
+ try{
+ if(0===wasmUtil.xCallWrapped(
+ 'sqlite3_wasm_init_wasmfs', 'i32', ['string'], pdir
+ )){
+ return f._ = pdir;
+ }else{
+ return f._ = "";
+ }
+ }catch(e){
+ // sqlite3_wasm_init_wasmfs() is not available
+ return f._ = "";
+ }
+ };
+ wasmfsDir._ = undefined;
+
+ const eOut = document.querySelector('#test-output');
+ const log2 = function(cssClass,...args){
+ const ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ eOut.append(ln);
+ //this.e.output.lastElementChild.scrollIntoViewIfNeeded();
+ };
+ const logList = [];
+ const dumpLogList = function(){
+ logList.forEach((v)=>log2('',v));
+ logList.length = 0;
+ };
+ /* can't update DOM while speedtest is running unless we run
+ speedtest in a worker thread. */;
+ const log = (...args)=>{
+ console.log(...args);
+ logList.push(args.join(' '));
+ };
+ const logErr = function(...args){
+ console.error(...args);
+ logList.push('ERROR: '+args.join(' '));
+ };
+
+ const runTests = function(sqlite3){
+ const capi = sqlite3.capi, wasm = sqlite3.wasm;
+ //console.debug('sqlite3 =',sqlite3);
+ const pDir = wasmfsDir(wasm);
+ if(pDir){
+ console.warn("Persistent storage:",pDir);
+ }
+ const scope = wasm.scopedAllocPush();
+ let dbFile = pDir+"/speedtest1.db";
+ const urlParams = new URL(self.location.href).searchParams;
+ const argv = ["speedtest1"];
+ if(urlParams.has('flags')){
+ argv.push(...(urlParams.get('flags').split(',')));
+ }
+
+ let forceSize = 0;
+ let vfs, pVfs = 0;
+ if(urlParams.has('vfs')){
+ vfs = urlParams.get('vfs');
+ pVfs = capi.sqlite3_vfs_find(vfs);
+ if(!pVfs){
+ log2('error',"Unknown VFS:",vfs);
+ return;
+ }
+ argv.push("--vfs", vfs);
+ log2('',"Using VFS:",vfs);
+ if('kvvfs' === vfs){
+ forceSize = 4 /* 5 uses approx. 4.96mb */;
+ dbFile = 'session';
+ log2('warning',"kvvfs VFS: forcing --size",forceSize,
+ "and filename '"+dbFile+"'.");
+ capi.sqlite3_js_kvvfs_clear(dbFile);
+ }
+ }
+ if(forceSize){
+ argv.push('--size',forceSize);
+ }else{
+ [
+ 'size'
+ ].forEach(function(k){
+ const v = urlParams.get(k);
+ if(v) argv.push('--'+k, urlParams[k]);
+ });
+ }
+ argv.push(
+ "--singlethread",
+ //"--nomutex",
+ //"--nosync",
+ //"--memdb", // note that memdb trumps the filename arg
+ "--nomemstat"
+ );
+ argv.push("--big-transactions"/*important for tests 410 and 510!*/,
+ dbFile);
+ console.log("argv =",argv);
+ // These log messages are not emitted to the UI until after main() returns. Fixing that
+ // requires moving the main() call and related cleanup into a timeout handler.
+ if(pDir) wasm.sqlite3_wasm_vfs_unlink(pVfs,dbFile);
+ log2('',"Starting native app:\n ",argv.join(' '));
+ log2('',"This will take a while and the browser might warn about the runaway JS.",
+ "Give it time...");
+ logList.length = 0;
+ setTimeout(function(){
+ wasm.xCall('wasm_main', argv.length,
+ wasm.scopedAllocMainArgv(argv));
+ wasm.scopedAllocPop(scope);
+ if('kvvfs'===vfs){
+ logList.unshift("KVVFS "+dbFile+" size = "+
+ capi.sqlite3_js_kvvfs_size(dbFile));
+ }
+ if(pDir) wasm.sqlite3_wasm_vfs_unlink(pVfs,dbFile);
+ logList.unshift("Done running native main(). Output:");
+ dumpLogList();
+ }, 50);
+ }/*runTests()*/;
+
+ self.sqlite3TestModule.print = log;
+ self.sqlite3TestModule.printErr = logErr;
+ sqlite3InitModule(self.sqlite3TestModule).then(runTests);
+})();</script>
+</body>
+</html>
diff --git a/ext/wasm/split-speedtest1-script.sh b/ext/wasm/split-speedtest1-script.sh
new file mode 100755
index 0000000..e072d08
--- /dev/null
+++ b/ext/wasm/split-speedtest1-script.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+# Expects $1 to be a (speedtest1 --script) output file. Output is a
+# series of SQL files extracted from that file.
+infile=${1:?arg = speedtest1 --script output file}
+testnums=$(grep -e '^-- begin test' "$infile" | cut -d' ' -f4)
+if [ x = "x${testnums}" ]; then
+ echo "Could not parse any begin/end blocks out of $infile" 1>&2
+ exit 1
+fi
+odir=${infile%%/*}
+if [ "$odir" = "$infile" ]; then odir="."; fi
+#echo testnums=$testnums
+for n in $testnums; do
+ ofile=$odir/$(printf "speedtest1-%03d.sql" $n)
+ sed -n -e "/^-- begin test $n /,/^-- end test $n\$/p" $infile > $ofile
+ echo -e "$n\t$ofile"
+done
diff --git a/ext/wasm/sql/000-mandelbrot.sql b/ext/wasm/sql/000-mandelbrot.sql
new file mode 100644
index 0000000..3aa5f57
--- /dev/null
+++ b/ext/wasm/sql/000-mandelbrot.sql
@@ -0,0 +1,17 @@
+WITH RECURSIVE
+ xaxis(x) AS (VALUES(-2.0) UNION ALL SELECT x+0.05 FROM xaxis WHERE x<1.2),
+ yaxis(y) AS (VALUES(-1.0) UNION ALL SELECT y+0.1 FROM yaxis WHERE y<1.0),
+ m(iter, cx, cy, x, y) AS (
+ SELECT 0, x, y, 0.0, 0.0 FROM xaxis, yaxis
+ UNION ALL
+ SELECT iter+1, cx, cy, x*x-y*y + cx, 2.0*x*y + cy FROM m
+ WHERE (x*x + y*y) < 4.0 AND iter<28
+ ),
+ m2(iter, cx, cy) AS (
+ SELECT max(iter), cx, cy FROM m GROUP BY cx, cy
+ ),
+ a(t) AS (
+ SELECT group_concat( substr(' .+*#', 1+min(iter/7,4), 1), '')
+ FROM m2 GROUP BY cy
+ )
+SELECT group_concat(rtrim(t),x'0a') as Mandelbrot FROM a;
diff --git a/ext/wasm/sql/001-sudoku.sql b/ext/wasm/sql/001-sudoku.sql
new file mode 100644
index 0000000..53661b1
--- /dev/null
+++ b/ext/wasm/sql/001-sudoku.sql
@@ -0,0 +1,28 @@
+WITH RECURSIVE
+ input(sud) AS (
+ VALUES('53..7....6..195....98....6.8...6...34..8.3..17...2...6.6....28....419..5....8..79')
+ ),
+ digits(z, lp) AS (
+ VALUES('1', 1)
+ UNION ALL SELECT
+ CAST(lp+1 AS TEXT), lp+1 FROM digits WHERE lp<9
+ ),
+ x(s, ind) AS (
+ SELECT sud, instr(sud, '.') FROM input
+ UNION ALL
+ SELECT
+ substr(s, 1, ind-1) || z || substr(s, ind+1),
+ instr( substr(s, 1, ind-1) || z || substr(s, ind+1), '.' )
+ FROM x, digits AS z
+ WHERE ind>0
+ AND NOT EXISTS (
+ SELECT 1
+ FROM digits AS lp
+ WHERE z.z = substr(s, ((ind-1)/9)*9 + lp, 1)
+ OR z.z = substr(s, ((ind-1)%9) + (lp-1)*9 + 1, 1)
+ OR z.z = substr(s, (((ind-1)/3) % 3) * 3
+ + ((ind-1)/27) * 27 + lp
+ + ((lp-1) / 3) * 6, 1)
+ )
+ )
+SELECT s FROM x WHERE ind=0;
diff --git a/ext/wasm/test-opfs-vfs.html b/ext/wasm/test-opfs-vfs.html
new file mode 100644
index 0000000..235ef51
--- /dev/null
+++ b/ext/wasm/test-opfs-vfs.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>Async-behind-Sync experiment</title>
+ </head>
+ <body>
+ <header id='titlebar'><span>Async-behind-Sync sqlite3_vfs</span></header>
+ <div>This performs a sanity test of the "opfs" sqlite3_vfs.
+ <strong>See the dev console for all output.</strong>
+ </div>
+ <div>
+ <a href='?delete'>Use this link</a> to delete the persistent OPFS-side db (if any).
+ </div>
+ <div id='test-output'></div>
+ <script>
+ new Worker(
+ "test-opfs-vfs.js?sqlite3.dir=jswasm&"+self.location.search.substr(1)
+ );
+ </script>
+ </body>
+</html>
diff --git a/ext/wasm/test-opfs-vfs.js b/ext/wasm/test-opfs-vfs.js
new file mode 100644
index 0000000..96d0eac
--- /dev/null
+++ b/ext/wasm/test-opfs-vfs.js
@@ -0,0 +1,85 @@
+/*
+ 2022-09-17
+
+ 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.
+
+ ***********************************************************************
+
+ A testing ground for the OPFS VFS.
+*/
+'use strict';
+const tryOpfsVfs = async function(sqlite3){
+ const toss = function(...args){throw new Error(args.join(' '))};
+ const logPrefix = "OPFS tester:";
+ const log = (...args)=>console.log(logPrefix,...args);
+ const warn = (...args)=>console.warn(logPrefix,...args);
+ const error = (...args)=>console.error(logPrefix,...args);
+ const opfs = sqlite3.opfs;
+ log("tryOpfsVfs()");
+ if(!sqlite3.opfs){
+ const e = new Error("OPFS is not available.");
+ error(e);
+ throw e;
+ }
+ const capi = sqlite3.capi;
+ const pVfs = capi.sqlite3_vfs_find("opfs") || toss("Missing 'opfs' VFS.");
+ const oVfs = new capi.sqlite3_vfs(pVfs);
+ log("OPFS VFS:",pVfs, oVfs);
+
+ const wait = async (ms)=>{
+ return new Promise((resolve)=>setTimeout(resolve, ms));
+ };
+
+ const urlArgs = new URL(self.location.href).searchParams;
+ const dbFile = "my-persistent.db";
+ if(urlArgs.has('delete')) sqlite3.opfs.unlink(dbFile);
+
+ const db = new sqlite3.oo1.OpfsDb(dbFile,'ct');
+ log("db file:",db.filename);
+ try{
+ if(opfs.entryExists(dbFile)){
+ let n = db.selectValue("select count(*) from sqlite_schema");
+ log("Persistent data found. sqlite_schema entry count =",n);
+ }
+ db.transaction((db)=>{
+ db.exec({
+ sql:[
+ "create table if not exists t(a);",
+ "insert into t(a) values(?),(?),(?);",
+ ],
+ bind: [performance.now() | 0,
+ (performance.now() |0) / 2,
+ (performance.now() |0) / 4]
+ });
+ });
+ log("count(*) from t =",db.selectValue("select count(*) from t"));
+
+ // Some sanity checks of the opfs utility functions...
+ const testDir = '/sqlite3-opfs-'+opfs.randomFilename(12);
+ const aDir = testDir+'/test/dir';
+ await opfs.mkdir(aDir) || toss("mkdir failed");
+ await opfs.mkdir(aDir) || toss("mkdir must pass if the dir exists");
+ await opfs.unlink(testDir+'/test') && toss("delete 1 should have failed (dir not empty)");
+ //await opfs.entryExists(testDir)
+ await opfs.unlink(testDir+'/test/dir') || toss("delete 2 failed");
+ await opfs.unlink(testDir+'/test/dir') && toss("delete 2b should have failed (dir already deleted)");
+ await opfs.unlink(testDir, true) || toss("delete 3 failed");
+ await opfs.entryExists(testDir) && toss("entryExists(",testDir,") should have failed");
+ }finally{
+ db.close();
+ }
+
+ log("Done!");
+}/*tryOpfsVfs()*/;
+
+importScripts('jswasm/sqlite3.js');
+self.sqlite3InitModule()
+ .then((sqlite3)=>tryOpfsVfs(sqlite3))
+ .catch((e)=>{
+ console.error("Error initializing module:",e);
+ });
diff --git a/ext/wasm/tester1-worker.html b/ext/wasm/tester1-worker.html
new file mode 100644
index 0000000..e768c3d
--- /dev/null
+++ b/ext/wasm/tester1-worker.html
@@ -0,0 +1,81 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="../common/emscripten.css"/>
+ <link rel="stylesheet" href="../common/testing.css"/>
+ <title>sqlite3 tester #1: Worker thread</title>
+ <style></style>
+ </head>
+ <body>
+ <h1 id='color-target'>sqlite3 tester #1: Worker thread</h1>
+ <div>Variants:
+ <a href='tester1.html' target='tester1.html'>conventional UI thread</a>,
+ <a href='tester1-worker.html' target='tester1-worker.html'>conventional worker</a>,
+ <a href='tester1-esm.html' target='tester1-esm.html'>ESM in UI thread</a>,
+ <a href='tester1-worker.html?esm' target='tester1-worker.html?esm'>ESM worker</a>
+ </div>
+ <div class='input-wrapper'>
+ <input type='checkbox' id='cb-log-reverse'>
+ <label for='cb-log-reverse'>Reverse log order?</label>
+ </div>
+ <div id='test-output'></div>
+ <script>(function(){
+ const logTarget = document.querySelector('#test-output');
+ const logHtml = function(cssClass,...args){
+ const ln = document.createElement('div');
+ if(cssClass){
+ for(const c of (Array.isArray(cssClass) ? cssClass : [cssClass])){
+ ln.classList.add(c);
+ }
+ }
+ ln.append(document.createTextNode(args.join(' ')));
+ logTarget.append(ln);
+ };
+ const cbReverse = document.querySelector('#cb-log-reverse');
+ const cbReverseIt = ()=>{
+ logTarget.classList[cbReverse.checked ? 'add' : 'remove']('reverse');
+ };
+ cbReverse.addEventListener('change',cbReverseIt,true);
+ cbReverseIt();
+ const urlParams = new URL(self.location.href).searchParams;
+ const workerArgs = [];
+ if(urlParams.has('esm')){
+ logHtml('warning',"Attempting to run an ES6 Worker Module, "+
+ "which is not supported by all browsers! "+
+ "e.g. Firefox (as of 2023-05) cannot do this.");
+ workerArgs.push("tester1.mjs",{type:"module"});
+ document.querySelectorAll('title,#color-target').forEach((e)=>{
+ e.innerText = "sqlite3 tester #1: ES6 Worker Module";
+ });
+ }else{
+ workerArgs.push("tester1.js?sqlite3.dir=jswasm");
+ }
+ const w = new Worker(...workerArgs);
+ w.onmessage = function({data}){
+ switch(data.type){
+ case 'log':
+ logHtml(data.payload.cssClass, ...data.payload.args);
+ break;
+ case 'error':
+ logHtml('error', ...data.payload.args);
+ break;
+ case 'test-result':{
+ document.querySelector('#color-target').classList.add(
+ data.payload.pass ? 'tests-pass' : 'tests-fail'
+ );
+ const e = document.querySelector('title');
+ e.innerText = (
+ data.payload.pass ? 'PASS' : 'FAIL'
+ ) + ': ' + e.innerText;
+ break;
+ }
+ default:
+ logHtml('error',"Unhandled message:",data.type);
+ };
+ };
+ })();</script>
+ </body>
+</html>
diff --git a/ext/wasm/tester1.c-pp.html b/ext/wasm/tester1.c-pp.html
new file mode 100644
index 0000000..bbdd8b2
--- /dev/null
+++ b/ext/wasm/tester1.c-pp.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>sqlite3 tester #1:
+//#if target=es6-module
+ES6 Module in UI thread
+//#else
+UI thread
+//#endif
+ </title>
+ <style></style>
+ </head>
+ <body><h1 id='color-target'></h1>
+ <div>Variants:
+ <a href='tester1.html' target='tester1.html'>conventional UI thread</a>,
+ <a href='tester1-worker.html' target='tester1-worker.html'>conventional worker</a>,
+ <a href='tester1-esm.html' target='tester1-esm.html'>ESM in UI thread</a>,
+ <a href='tester1-worker.html?esm' target='tester1-worker.html?esm'>ESM worker</a>
+ </div>
+ <div class='input-wrapper'>
+ <input type='checkbox' id='cb-log-reverse'>
+ <label for='cb-log-reverse'>Reverse log order?</label>
+ </div>
+ <div id='test-output'></div>
+ <script>(function(){
+ document.querySelector('h1').innerHTML =
+ document.querySelector('title').innerHTML;
+ })();</script>
+//#if target=es6-module
+ <script src="tester1.mjs" type="module"></script>
+//#else
+ <script src="jswasm/sqlite3.js"></script>
+ <script src="tester1.js"></script>
+//#endif
+ </body>
+</html>
diff --git a/ext/wasm/tester1.c-pp.js b/ext/wasm/tester1.c-pp.js
new file mode 100644
index 0000000..36ca4c9
--- /dev/null
+++ b/ext/wasm/tester1.c-pp.js
@@ -0,0 +1,3237 @@
+/*
+ 2022-10-12
+
+ 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.
+
+ ***********************************************************************
+
+ Main functional and regression tests for the sqlite3 WASM API.
+
+ This mini-framework works like so:
+
+ This script adds a series of test groups, each of which contains an
+ arbitrary number of tests, into a queue. After loading of the
+ sqlite3 WASM/JS module is complete, that queue is processed. If any
+ given test fails, the whole thing fails. This script is built such
+ that it can run from the main UI thread or worker thread. Test
+ groups and individual tests can be assigned a predicate function
+ which determines whether to run them or not, and this is
+ specifically intended to be used to toggle certain tests on or off
+ for the main/worker threads or the availability (or not) of
+ optional features such as int64 support.
+
+ Each test group defines a single state object which gets applied as
+ the test functions' `this` for all tests in that group. Test
+ functions can use that to, e.g., set up a db in an early test and
+ close it in a later test. Each test gets passed the sqlite3
+ namespace object as its only argument.
+*/
+/*
+ This file is intended to be processed by c-pp to inject (or not)
+ code specific to ES6 modules which is illegal in non-module code.
+
+ Non-ES6 module build and ES6 module for the main-thread:
+
+ ./c-pp -f tester1.c-pp.js -o tester1.js
+
+ ES6 worker module build:
+
+ ./c-pp -f tester1.c-pp.js -o tester1-esm.js -Dtarget=es6-module
+*/
+//#if target=es6-module
+import {default as sqlite3InitModule} from './jswasm/sqlite3.mjs';
+globalThis.sqlite3InitModule = sqlite3InitModule;
+//#else
+'use strict';
+//#endif
+(function(self){
+ /**
+ Set up our output channel differently depending
+ on whether we are running in a worker thread or
+ the main (UI) thread.
+ */
+ let logClass;
+ /* Predicate for tests/groups. */
+ const isUIThread = ()=>(globalThis.window===self && globalThis.document);
+ /* Predicate for tests/groups. */
+ const isWorker = ()=>!isUIThread();
+ /* Predicate for tests/groups. */
+ const testIsTodo = ()=>false;
+ const haveWasmCTests = ()=>{
+ return !!wasm.exports.sqlite3_wasm_test_intptr;
+ };
+ const hasOpfs = ()=>{
+ return globalThis.FileSystemHandle
+ && globalThis.FileSystemDirectoryHandle
+ && globalThis.FileSystemFileHandle
+ && globalThis.FileSystemFileHandle.prototype.createSyncAccessHandle
+ && navigator?.storage?.getDirectory;
+ };
+
+ {
+ const mapToString = (v)=>{
+ switch(typeof v){
+ case 'number': case 'string': case 'boolean':
+ case 'undefined': case 'bigint':
+ return ''+v;
+ default: break;
+ }
+ if(null===v) return 'null';
+ if(v instanceof Error){
+ v = {
+ message: v.message,
+ stack: v.stack,
+ errorClass: v.name
+ };
+ }
+ return JSON.stringify(v,undefined,2);
+ };
+ const normalizeArgs = (args)=>args.map(mapToString);
+ if( isUIThread() ){
+ console.log("Running in the UI thread.");
+ const logTarget = document.querySelector('#test-output');
+ logClass = function(cssClass,...args){
+ const ln = document.createElement('div');
+ if(cssClass){
+ for(const c of (Array.isArray(cssClass) ? cssClass : [cssClass])){
+ ln.classList.add(c);
+ }
+ }
+ ln.append(document.createTextNode(normalizeArgs(args).join(' ')));
+ logTarget.append(ln);
+ };
+ const cbReverse = document.querySelector('#cb-log-reverse');
+ //cbReverse.setAttribute('checked','checked');
+ const cbReverseKey = 'tester1:cb-log-reverse';
+ const cbReverseIt = ()=>{
+ logTarget.classList[cbReverse.checked ? 'add' : 'remove']('reverse');
+ //localStorage.setItem(cbReverseKey, cbReverse.checked ? 1 : 0);
+ };
+ cbReverse.addEventListener('change', cbReverseIt, true);
+ /*if(localStorage.getItem(cbReverseKey)){
+ cbReverse.checked = !!(+localStorage.getItem(cbReverseKey));
+ }*/
+ cbReverseIt();
+ }else{ /* Worker thread */
+ console.log("Running in a Worker thread.");
+ logClass = function(cssClass,...args){
+ postMessage({
+ type:'log',
+ payload:{cssClass, args: normalizeArgs(args)}
+ });
+ };
+ }
+ }
+ const reportFinalTestStatus = function(pass){
+ if(isUIThread()){
+ let e = document.querySelector('#color-target');
+ e.classList.add(pass ? 'tests-pass' : 'tests-fail');
+ e = document.querySelector('title');
+ e.innerText = (pass ? 'PASS' : 'FAIL') + ': ' + e.innerText;
+ }else{
+ postMessage({type:'test-result', payload:{pass}});
+ }
+ };
+ const log = (...args)=>{
+ //console.log(...args);
+ logClass('',...args);
+ }
+ const warn = (...args)=>{
+ console.warn(...args);
+ logClass('warning',...args);
+ }
+ const error = (...args)=>{
+ console.error(...args);
+ logClass('error',...args);
+ };
+
+ const toss = (...args)=>{
+ error(...args);
+ throw new Error(args.join(' '));
+ };
+ const tossQuietly = (...args)=>{
+ throw new Error(args.join(' '));
+ };
+
+ const roundMs = (ms)=>Math.round(ms*100)/100;
+
+ /**
+ Helpers for writing sqlite3-specific tests.
+ */
+ const TestUtil = {
+ /** Running total of the number of tests run via
+ this API. */
+ counter: 0,
+ /**
+ If expr is a function, it is called and its result
+ is returned, coerced to a bool, else expr, coerced to
+ a bool, is returned.
+ */
+ toBool: function(expr){
+ return (expr instanceof Function) ? !!expr() : !!expr;
+ },
+ /** Throws if expr is false. If expr is a function, it is called
+ and its result is evaluated. If passed multiple arguments,
+ those after the first are a message string which get applied
+ as an exception message if the assertion fails. The message
+ arguments are concatenated together with a space between each.
+ */
+ assert: function f(expr, ...msg){
+ ++this.counter;
+ if(!this.toBool(expr)){
+ throw new Error(msg.length ? msg.join(' ') : "Assertion failed.");
+ }
+ return this;
+ },
+ /** Calls f() and squelches any exception it throws. If it
+ does not throw, this function throws. */
+ mustThrow: function(f, msg){
+ ++this.counter;
+ let err;
+ try{ f(); } catch(e){err=e;}
+ if(!err) throw new Error(msg || "Expected exception.");
+ return this;
+ },
+ /**
+ Works like mustThrow() but expects filter to be a regex,
+ function, or string to match/filter the resulting exception
+ against. If f() does not throw, this test fails and an Error is
+ thrown. If filter is a regex, the test passes if
+ filter.test(error.message) passes. If it's a function, the test
+ passes if filter(error) returns truthy. If it's a string, the
+ test passes if the filter matches the exception message
+ precisely. In all other cases the test fails, throwing an
+ Error.
+
+ If it throws, msg is used as the error report unless it's falsy,
+ in which case a default is used.
+ */
+ mustThrowMatching: function(f, filter, msg){
+ ++this.counter;
+ let err;
+ try{ f(); } catch(e){err=e;}
+ if(!err) throw new Error(msg || "Expected exception.");
+ let pass = false;
+ if(filter instanceof RegExp) pass = filter.test(err.message);
+ else if(filter instanceof Function) pass = filter(err);
+ else if('string' === typeof filter) pass = (err.message === filter);
+ if(!pass){
+ throw new Error(msg || ("Filter rejected this exception: "+err.message));
+ }
+ return this;
+ },
+ /** Throws if expr is truthy or expr is a function and expr()
+ returns truthy. */
+ throwIf: function(expr, msg){
+ ++this.counter;
+ if(this.toBool(expr)) throw new Error(msg || "throwIf() failed");
+ return this;
+ },
+ /** Throws if expr is falsy or expr is a function and expr()
+ returns falsy. */
+ throwUnless: function(expr, msg){
+ ++this.counter;
+ if(!this.toBool(expr)) throw new Error(msg || "throwUnless() failed");
+ return this;
+ },
+ eqApprox: (v1,v2,factor=0.05)=>(v1>=(v2-factor) && v1<=(v2+factor)),
+ TestGroup: (function(){
+ let groupCounter = 0;
+ const TestGroup = function(name, predicate){
+ this.number = ++groupCounter;
+ this.name = name;
+ this.predicate = predicate;
+ this.tests = [];
+ };
+ TestGroup.prototype = {
+ addTest: function(testObj){
+ this.tests.push(testObj);
+ return this;
+ },
+ run: async function(sqlite3){
+ logClass('group-start',"Group #"+this.number+':',this.name);
+ if(this.predicate){
+ const p = this.predicate(sqlite3);
+ if(!p || 'string'===typeof p){
+ logClass(['warning','skipping-group'],
+ "SKIPPING group:", p ? p : "predicate says to" );
+ return;
+ }
+ }
+ const assertCount = TestUtil.counter;
+ const groupState = Object.create(null);
+ const skipped = [];
+ let runtime = 0, i = 0;
+ for(const t of this.tests){
+ ++i;
+ const n = this.number+"."+i;
+ logClass('one-test-line', n+":", t.name);
+ if(t.predicate){
+ const p = t.predicate(sqlite3);
+ if(!p || 'string'===typeof p){
+ logClass(['warning','skipping-test'],
+ "SKIPPING:", p ? p : "predicate says to" );
+ skipped.push( n+': '+t.name );
+ continue;
+ }
+ }
+ const tc = TestUtil.counter, now = performance.now();
+ let rc = t.test.call(groupState, sqlite3);
+ /*if(rc instanceof Promise){
+ rc = rc.catch((e)=>{
+ error("Test failure:",e);
+ throw e;
+ });
+ }*/
+ await rc;
+ const then = performance.now();
+ runtime += then - now;
+ logClass(['faded','one-test-summary'],
+ TestUtil.counter - tc, 'assertion(s) in',
+ roundMs(then-now),'ms');
+ }
+ logClass(['green','group-end'],
+ "#"+this.number+":",
+ (TestUtil.counter - assertCount),
+ "assertion(s) in",roundMs(runtime),"ms");
+ if(0 && skipped.length){
+ logClass('warning',"SKIPPED test(s) in group",this.number+":",skipped);
+ }
+ }
+ };
+ return TestGroup;
+ })()/*TestGroup*/,
+ testGroups: [],
+ currentTestGroup: undefined,
+ addGroup: function(name, predicate){
+ this.testGroups.push( this.currentTestGroup =
+ new this.TestGroup(name, predicate) );
+ return this;
+ },
+ addTest: function(name, callback){
+ let predicate;
+ if(1===arguments.length){
+ this.currentTestGroup.addTest(arguments[0]);
+ }else{
+ this.currentTestGroup.addTest({
+ name, predicate, test: callback
+ });
+ }
+ return this;
+ },
+ runTests: async function(sqlite3){
+ return new Promise(async function(pok,pnok){
+ try {
+ let runtime = 0;
+ for(let g of this.testGroups){
+ const now = performance.now();
+ await g.run(sqlite3);
+ runtime += performance.now() - now;
+ }
+ logClass(['strong','green','full-test-summary'],
+ "Done running tests.",TestUtil.counter,"assertions in",
+ roundMs(runtime),'ms');
+ pok();
+ reportFinalTestStatus(true);
+ }catch(e){
+ error(e);
+ pnok(e);
+ reportFinalTestStatus(false);
+ }
+ }.bind(this));
+ }
+ }/*TestUtil*/;
+ const T = TestUtil;
+ T.g = T.addGroup;
+ T.t = T.addTest;
+ let capi, wasm/*assigned after module init*/;
+ const sahPoolConfig = {
+ name: 'opfs-sahpool-tester1',
+ clearOnInit: true,
+ initialCapacity: 6
+ };
+ ////////////////////////////////////////////////////////////////////////
+ // End of infrastructure setup. Now define the tests...
+ ////////////////////////////////////////////////////////////////////////
+
+ ////////////////////////////////////////////////////////////////////
+ T.g('Basic sanity checks')
+ .t({
+ name:'sqlite3_config()',
+ test:function(sqlite3){
+ for(const k of [
+ 'SQLITE_CONFIG_GETMALLOC', 'SQLITE_CONFIG_URI'
+ ]){
+ T.assert(capi[k] > 0);
+ }
+ T.assert(capi.SQLITE_MISUSE===capi.sqlite3_config(
+ capi.SQLITE_CONFIG_URI, 1
+ ), "MISUSE because the library has already been initialized.");
+ T.assert(capi.SQLITE_MISUSE === capi.sqlite3_config(
+ // not enough args
+ capi.SQLITE_CONFIG_GETMALLOC
+ ));
+ T.assert(capi.SQLITE_NOTFOUND === capi.sqlite3_config(
+ // unhandled-in-JS config option
+ capi.SQLITE_CONFIG_GETMALLOC, 1
+ ));
+ if(0){
+ log("We cannot _fully_ test sqlite3_config() after the library",
+ "has been initialized (which it necessarily has been to",
+ "set up various bindings) and we cannot shut it down ",
+ "without losing the VFS registrations.");
+ T.assert(0 === capi.sqlite3_config(
+ capi.SQLITE_CONFIG_URI, 1
+ ));
+ }
+ }
+ })/*sqlite3_config()*/
+
+ ////////////////////////////////////////////////////////////////////
+ .t({
+ name: "JS wasm-side allocator",
+ test: function(sqlite3){
+ if(sqlite3.config.useStdAlloc){
+ warn("Using system allocator. This violates the docs and",
+ "may cause grief with certain APIs",
+ "(e.g. sqlite3_deserialize()).");
+ T.assert(wasm.alloc.impl === wasm.exports.malloc)
+ .assert(wasm.dealloc === wasm.exports.free)
+ .assert(wasm.realloc.impl === wasm.exports.realloc);
+ }else{
+ T.assert(wasm.alloc.impl === wasm.exports.sqlite3_malloc)
+ .assert(wasm.dealloc === wasm.exports.sqlite3_free)
+ .assert(wasm.realloc.impl === wasm.exports.sqlite3_realloc);
+ }
+ }
+ })
+ .t('Namespace object checks', function(sqlite3){
+ const wasmCtypes = wasm.ctype;
+ T.assert(wasmCtypes.structs[0].name==='sqlite3_vfs').
+ assert(wasmCtypes.structs[0].members.szOsFile.sizeof>=4).
+ assert(wasmCtypes.structs[1/*sqlite3_io_methods*/
+ ].members.xFileSize.offset>0);
+ [ /* Spot-check a handful of constants to make sure they got installed... */
+ 'SQLITE_SCHEMA','SQLITE_NULL','SQLITE_UTF8',
+ 'SQLITE_STATIC', 'SQLITE_DIRECTONLY',
+ 'SQLITE_OPEN_CREATE', 'SQLITE_OPEN_DELETEONCLOSE'
+ ].forEach((k)=>T.assert('number' === typeof capi[k]));
+ [/* Spot-check a few of the WASM API methods. */
+ 'alloc', 'dealloc', 'installFunction'
+ ].forEach((k)=>T.assert(wasm[k] instanceof Function));
+
+ T.assert(capi.sqlite3_errstr(capi.SQLITE_IOERR_ACCESS).indexOf("I/O")>=0).
+ assert(capi.sqlite3_errstr(capi.SQLITE_CORRUPT).indexOf('malformed')>0).
+ assert(capi.sqlite3_errstr(capi.SQLITE_OK) === 'not an error');
+
+ try {
+ throw new sqlite3.WasmAllocError;
+ }catch(e){
+ T.assert(e instanceof Error)
+ .assert(e instanceof sqlite3.WasmAllocError)
+ .assert("Allocation failed." === e.message);
+ }
+ try {
+ throw new sqlite3.WasmAllocError("test",{
+ cause: 3
+ });
+ }catch(e){
+ T.assert(3 === e.cause)
+ .assert("test" === e.message);
+ }
+ try {throw new sqlite3.WasmAllocError("test","ing",".")}
+ catch(e){T.assert("test ing ." === e.message)}
+
+ try{ throw new sqlite3.SQLite3Error(capi.SQLITE_SCHEMA) }
+ catch(e){
+ T.assert('SQLITE_SCHEMA' === e.message)
+ .assert(capi.SQLITE_SCHEMA === e.resultCode);
+ }
+ try{ sqlite3.SQLite3Error.toss(capi.SQLITE_CORRUPT,{cause: true}) }
+ catch(e){
+ T.assert('SQLITE_CORRUPT' === e.message)
+ .assert(capi.SQLITE_CORRUPT === e.resultCode)
+ .assert(true===e.cause);
+ }
+ try{ sqlite3.SQLite3Error.toss("resultCode check") }
+ catch(e){
+ T.assert(capi.SQLITE_ERROR === e.resultCode)
+ .assert('resultCode check' === e.message);
+ }
+ })
+ ////////////////////////////////////////////////////////////////////
+ .t('strglob/strlike', function(sqlite3){
+ T.assert(0===capi.sqlite3_strglob("*.txt", "foo.txt")).
+ assert(0!==capi.sqlite3_strglob("*.txt", "foo.xtx")).
+ assert(0===capi.sqlite3_strlike("%.txt", "foo.txt", 0)).
+ assert(0!==capi.sqlite3_strlike("%.txt", "foo.xtx", 0));
+ })
+
+ ////////////////////////////////////////////////////////////////////
+ ;/*end of basic sanity checks*/
+
+ ////////////////////////////////////////////////////////////////////
+ T.g('C/WASM Utilities')
+ .t('sqlite3.wasm namespace', function(sqlite3){
+ // TODO: break this into smaller individual test functions.
+ const w = wasm;
+ const chr = (x)=>x.charCodeAt(0);
+ //log("heap getters...");
+ {
+ const li = [8, 16, 32];
+ if(w.bigIntEnabled) li.push(64);
+ for(const n of li){
+ const bpe = n/8;
+ const s = w.heapForSize(n,false);
+ T.assert(bpe===s.BYTES_PER_ELEMENT).
+ assert(w.heapForSize(s.constructor) === s);
+ const u = w.heapForSize(n,true);
+ T.assert(bpe===u.BYTES_PER_ELEMENT).
+ assert(s!==u).
+ assert(w.heapForSize(u.constructor) === u);
+ }
+ }
+
+ // alloc(), realloc(), allocFromTypedArray()
+ {
+ let m = w.alloc(14);
+ let m2 = w.realloc(m, 16);
+ T.assert(m === m2/* because of alignment */);
+ T.assert(0 === w.realloc(m, 0));
+ m = m2 = 0;
+
+ // Check allocation limits and allocator's responses...
+ T.assert('number' === typeof sqlite3.capi.SQLITE_MAX_ALLOCATION_SIZE);
+ if(!sqlite3.config.useStdAlloc){
+ const tooMuch = sqlite3.capi.SQLITE_MAX_ALLOCATION_SIZE + 1,
+ isAllocErr = (e)=>e instanceof sqlite3.WasmAllocError;
+ T.mustThrowMatching(()=>w.alloc(tooMuch), isAllocErr)
+ .assert(0 === w.alloc.impl(tooMuch))
+ .mustThrowMatching(()=>w.realloc(0, tooMuch), isAllocErr)
+ .assert(0 === w.realloc.impl(0, tooMuch));
+ }
+
+ // Check allocFromTypedArray()...
+ const byteList = [11,22,33]
+ const u = new Uint8Array(byteList);
+ m = w.allocFromTypedArray(u);
+ for(let i = 0; i < u.length; ++i){
+ T.assert(u[i] === byteList[i])
+ .assert(u[i] === w.peek8(m + i));
+ }
+ w.dealloc(m);
+ m = w.allocFromTypedArray(u.buffer);
+ for(let i = 0; i < u.length; ++i){
+ T.assert(u[i] === byteList[i])
+ .assert(u[i] === w.peek8(m + i));
+ }
+
+ w.dealloc(m);
+ T.mustThrowMatching(
+ ()=>w.allocFromTypedArray(1),
+ 'Value is not of a supported TypedArray type.'
+ );
+ }
+
+ { // Test peekXYZ()/pokeXYZ()...
+ const m = w.alloc(8);
+ T.assert( 17 === w.poke8(m,17).peek8(m) )
+ .assert( 31987 === w.poke16(m,31987).peek16(m) )
+ .assert( 345678 === w.poke32(m,345678).peek32(m) )
+ .assert(
+ T.eqApprox( 345678.9, w.poke32f(m,345678.9).peek32f(m) )
+ ).assert(
+ T.eqApprox( 4567890123.4, w.poke64f(m, 4567890123.4).peek64f(m) )
+ );
+ if(w.bigIntEnabled){
+ T.assert(
+ BigInt(Number.MAX_SAFE_INTEGER) ===
+ w.poke64(m, Number.MAX_SAFE_INTEGER).peek64(m)
+ );
+ }
+ w.dealloc(m);
+ }
+
+ // isPtr32()
+ {
+ const ip = w.isPtr32;
+ T.assert(ip(0))
+ .assert(!ip(-1))
+ .assert(!ip(1.1))
+ .assert(!ip(0xffffffff))
+ .assert(ip(0x7fffffff))
+ .assert(!ip())
+ .assert(!ip(null)/*might change: under consideration*/)
+ ;
+ }
+
+ //log("jstrlen()...");
+ {
+ T.assert(3 === w.jstrlen("abc")).assert(4 === w.jstrlen("äbc"));
+ }
+
+ //log("jstrcpy()...");
+ {
+ const fillChar = 10;
+ let ua = new Uint8Array(8), rc,
+ refill = ()=>ua.fill(fillChar);
+ refill();
+ rc = w.jstrcpy("hello", ua);
+ T.assert(6===rc).assert(0===ua[5]).assert(chr('o')===ua[4]);
+ refill();
+ ua[5] = chr('!');
+ rc = w.jstrcpy("HELLO", ua, 0, -1, false);
+ T.assert(5===rc).assert(chr('!')===ua[5]).assert(chr('O')===ua[4]);
+ refill();
+ rc = w.jstrcpy("the end", ua, 4);
+ //log("rc,ua",rc,ua);
+ T.assert(4===rc).assert(0===ua[7]).
+ assert(chr('e')===ua[6]).assert(chr('t')===ua[4]);
+ refill();
+ rc = w.jstrcpy("the end", ua, 4, -1, false);
+ T.assert(4===rc).assert(chr(' ')===ua[7]).
+ assert(chr('e')===ua[6]).assert(chr('t')===ua[4]);
+ refill();
+ rc = w.jstrcpy("", ua, 0, 1, true);
+ //log("rc,ua",rc,ua);
+ T.assert(1===rc).assert(0===ua[0]);
+ refill();
+ rc = w.jstrcpy("x", ua, 0, 1, true);
+ //log("rc,ua",rc,ua);
+ T.assert(1===rc).assert(0===ua[0]);
+ refill();
+ rc = w.jstrcpy('äbä', ua, 0, 1, true);
+ T.assert(1===rc, 'Must not write partial multi-byte char.')
+ .assert(0===ua[0]);
+ refill();
+ rc = w.jstrcpy('äbä', ua, 0, 2, true);
+ T.assert(1===rc, 'Must not write partial multi-byte char.')
+ .assert(0===ua[0]);
+ refill();
+ rc = w.jstrcpy('äbä', ua, 0, 2, false);
+ T.assert(2===rc).assert(fillChar!==ua[1]).assert(fillChar===ua[2]);
+ }/*jstrcpy()*/
+
+ //log("cstrncpy()...");
+ {
+ const scope = w.scopedAllocPush();
+ try {
+ let cStr = w.scopedAllocCString("hello");
+ const n = w.cstrlen(cStr);
+ let cpy = w.scopedAlloc(n+10);
+ let rc = w.cstrncpy(cpy, cStr, n+10);
+ T.assert(n+1 === rc).
+ assert("hello" === w.cstrToJs(cpy)).
+ assert(chr('o') === w.peek8(cpy+n-1)).
+ assert(0 === w.peek8(cpy+n));
+ let cStr2 = w.scopedAllocCString("HI!!!");
+ rc = w.cstrncpy(cpy, cStr2, 3);
+ T.assert(3===rc).
+ assert("HI!lo" === w.cstrToJs(cpy)).
+ assert(chr('!') === w.peek8(cpy+2)).
+ assert(chr('l') === w.peek8(cpy+3));
+ }finally{
+ w.scopedAllocPop(scope);
+ }
+ }
+
+ //log("jstrToUintArray()...");
+ {
+ let a = w.jstrToUintArray("hello", false);
+ T.assert(5===a.byteLength).assert(chr('o')===a[4]);
+ a = w.jstrToUintArray("hello", true);
+ T.assert(6===a.byteLength).assert(chr('o')===a[4]).assert(0===a[5]);
+ a = w.jstrToUintArray("äbä", false);
+ T.assert(5===a.byteLength).assert(chr('b')===a[2]);
+ a = w.jstrToUintArray("äbä", true);
+ T.assert(6===a.byteLength).assert(chr('b')===a[2]).assert(0===a[5]);
+ }
+
+ //log("allocCString()...");
+ {
+ const jstr = "hällo, world!";
+ const [cstr, n] = w.allocCString(jstr, true);
+ T.assert(14 === n)
+ .assert(0===w.peek8(cstr+n))
+ .assert(chr('!')===w.peek8(cstr+n-1));
+ w.dealloc(cstr);
+ }
+
+ //log("scopedAlloc() and friends...");
+ {
+ const alloc = w.alloc, dealloc = w.dealloc;
+ w.alloc = w.dealloc = null;
+ T.assert(!w.scopedAlloc.level)
+ .mustThrowMatching(()=>w.scopedAlloc(1), /^No scopedAllocPush/)
+ .mustThrowMatching(()=>w.scopedAllocPush(), /missing alloc/);
+ w.alloc = alloc;
+ T.mustThrowMatching(()=>w.scopedAllocPush(), /missing alloc/);
+ w.dealloc = dealloc;
+ T.mustThrowMatching(()=>w.scopedAllocPop(), /^Invalid state/)
+ .mustThrowMatching(()=>w.scopedAlloc(1), /^No scopedAllocPush/)
+ .mustThrowMatching(()=>w.scopedAlloc.level=0, /read-only/);
+ const asc = w.scopedAllocPush();
+ let asc2;
+ try {
+ const p1 = w.scopedAlloc(16),
+ p2 = w.scopedAlloc(16);
+ T.assert(1===w.scopedAlloc.level)
+ .assert(Number.isFinite(p1))
+ .assert(Number.isFinite(p2))
+ .assert(asc[0] === p1)
+ .assert(asc[1]===p2);
+ asc2 = w.scopedAllocPush();
+ const p3 = w.scopedAlloc(16);
+ T.assert(2===w.scopedAlloc.level)
+ .assert(Number.isFinite(p3))
+ .assert(2===asc.length)
+ .assert(p3===asc2[0]);
+
+ const [z1, z2, z3] = w.scopedAllocPtr(3);
+ T.assert('number'===typeof z1).assert(z2>z1).assert(z3>z2)
+ .assert(0===w.peek32(z1), 'allocPtr() must zero the targets')
+ .assert(0===w.peek32(z3));
+ }finally{
+ // Pop them in "incorrect" order to make sure they behave:
+ w.scopedAllocPop(asc);
+ T.assert(0===asc.length);
+ T.mustThrowMatching(()=>w.scopedAllocPop(asc),
+ /^Invalid state object/);
+ if(asc2){
+ T.assert(2===asc2.length,'Should be p3 and z1');
+ w.scopedAllocPop(asc2);
+ T.assert(0===asc2.length);
+ T.mustThrowMatching(()=>w.scopedAllocPop(asc2),
+ /^Invalid state object/);
+ }
+ }
+ T.assert(0===w.scopedAlloc.level);
+ w.scopedAllocCall(function(){
+ T.assert(1===w.scopedAlloc.level);
+ const [cstr, n] = w.scopedAllocCString("hello, world", true);
+ T.assert(12 === n)
+ .assert(0===w.peek8(cstr+n))
+ .assert(chr('d')===w.peek8(cstr+n-1));
+ });
+ }/*scopedAlloc()*/
+
+ //log("xCall()...");
+ {
+ const pJson = w.xCall('sqlite3_wasm_enum_json');
+ T.assert(Number.isFinite(pJson)).assert(w.cstrlen(pJson)>300);
+ }
+
+ //log("xWrap()...");
+ {
+ T.mustThrowMatching(()=>w.xWrap('sqlite3_libversion',null,'i32'),
+ /requires 0 arg/).
+ assert(w.xWrap.resultAdapter('i32') instanceof Function).
+ assert(w.xWrap.argAdapter('i32') instanceof Function);
+ let fw = w.xWrap('sqlite3_libversion','utf8');
+ T.mustThrowMatching(()=>fw(1), /requires 0 arg/);
+ let rc = fw();
+ T.assert('string'===typeof rc).assert(rc.length>5);
+ rc = w.xCallWrapped('sqlite3_wasm_enum_json','*');
+ T.assert(rc>0 && Number.isFinite(rc));
+ rc = w.xCallWrapped('sqlite3_wasm_enum_json','utf8');
+ T.assert('string'===typeof rc).assert(rc.length>300);
+
+
+ { // 'string:static' argAdapter() sanity checks...
+ let argAd = w.xWrap.argAdapter('string:static');
+ let p0 = argAd('foo'), p1 = argAd('bar');
+ T.assert(w.isPtr(p0) && w.isPtr(p1))
+ .assert(p0 !== p1)
+ .assert(p0 === argAd('foo'))
+ .assert(p1 === argAd('bar'));
+ }
+
+ // 'string:flexible' argAdapter() sanity checks...
+ w.scopedAllocCall(()=>{
+ const argAd = w.xWrap.argAdapter('string:flexible');
+ const cj = (v)=>w.cstrToJs(argAd(v));
+ T.assert('Hi' === cj('Hi'))
+ .assert('hi' === cj(['h','i']))
+ .assert('HI' === cj(new Uint8Array([72, 73])));
+ });
+
+ // jsFuncToWasm()
+ {
+ const fsum3 = (x,y,z)=>x+y+z;
+ fw = w.jsFuncToWasm('i(iii)', fsum3);
+ T.assert(fw instanceof Function)
+ .assert( fsum3 !== fw )
+ .assert( 3 === fw.length )
+ .assert( 6 === fw(1,2,3) );
+ T.mustThrowMatching( ()=>w.jsFuncToWasm('x()', function(){}),
+ 'Invalid signature letter: x');
+ }
+
+ // xWrap(Function,...)
+ {
+ let fp;
+ try {
+ const fmy = function fmy(i,s,d){
+ if(fmy.debug) log("fmy(",...arguments,")");
+ T.assert( 3 === i )
+ .assert( w.isPtr(s) )
+ .assert( w.cstrToJs(s) === 'a string' )
+ .assert( T.eqApprox(1.2, d) );
+ return w.allocCString("hi");
+ };
+ fmy.debug = false;
+ const xwArgs = ['string:dealloc', ['i32', 'string', 'f64']];
+ fw = w.xWrap(fmy, ...xwArgs);
+ const fmyArgs = [3, 'a string', 1.2];
+ let rc = fw(...fmyArgs);
+ T.assert( 'hi' === rc );
+ if(0){
+ /* Retain this as a "reminder to self"...
+
+ This extra level of indirection does not work: the
+ string argument is ending up as a null in fmy() but
+ the numeric arguments are making their ways through
+
+ What's happening is: installFunction() is creating a
+ WASM-compatible function instance. When we pass a JS string
+ into there it's getting coerced into `null` before being passed
+ on to the lower-level wrapper.
+ */
+ fmy.debug = true;
+ fp = wasm.installFunction('i(isd)', fw);
+ fw = w.functionEntry(fp);
+ rc = fw(...fmyArgs);
+ log("rc =",rc);
+ T.assert( 'hi' === rc );
+ // Similarly, this does not work:
+ //let fpw = w.xWrap(fp, null, [null,null,null]);
+ //rc = fpw(...fmyArgs);
+ //log("rc =",rc);
+ //T.assert( 'hi' === rc );
+ }
+ }finally{
+ wasm.uninstallFunction(fp);
+ }
+ }
+
+ if(haveWasmCTests()){
+ if(!sqlite3.config.useStdAlloc){
+ fw = w.xWrap('sqlite3_wasm_test_str_hello', 'utf8:dealloc',['i32']);
+ rc = fw(0);
+ T.assert('hello'===rc);
+ rc = fw(1);
+ T.assert(null===rc);
+ }
+
+ if(w.bigIntEnabled){
+ w.xWrap.resultAdapter('thrice', (v)=>3n*BigInt(v));
+ w.xWrap.argAdapter('twice', (v)=>2n*BigInt(v));
+ fw = w.xWrap('sqlite3_wasm_test_int64_times2','thrice','twice');
+ rc = fw(1);
+ T.assert(12n===rc);
+
+ w.scopedAllocCall(function(){
+ const pI1 = w.scopedAlloc(8), pI2 = pI1+4;
+ w.pokePtr([pI1, pI2], 0);
+ const f = w.xWrap('sqlite3_wasm_test_int64_minmax',undefined,['i64*','i64*']);
+ const [r1, r2] = w.peek64([pI1, pI2]);
+ T.assert(!Number.isSafeInteger(r1)).assert(!Number.isSafeInteger(r2));
+ });
+ }
+ }
+ }/*xWrap()*/
+ }/*WhWasmUtil*/)
+
+ ////////////////////////////////////////////////////////////////////
+ .t('sqlite3.StructBinder (jaccwabyt🐇)', function(sqlite3){
+ const S = sqlite3, W = S.wasm;
+ const MyStructDef = {
+ sizeof: 16,
+ members: {
+ p4: {offset: 0, sizeof: 4, signature: "i"},
+ pP: {offset: 4, sizeof: 4, signature: "P"},
+ ro: {offset: 8, sizeof: 4, signature: "i", readOnly: true},
+ cstr: {offset: 12, sizeof: 4, signature: "s"}
+ }
+ };
+ if(W.bigIntEnabled){
+ const m = MyStructDef;
+ m.members.p8 = {offset: m.sizeof, sizeof: 8, signature: "j"};
+ m.sizeof += m.members.p8.sizeof;
+ }
+ const StructType = S.StructBinder.StructType;
+ const K = S.StructBinder('my_struct',MyStructDef);
+ T.mustThrowMatching(()=>K(), /via 'new'/).
+ mustThrowMatching(()=>new K('hi'), /^Invalid pointer/);
+ const k1 = new K(), k2 = new K();
+ try {
+ T.assert(k1.constructor === K).
+ assert(K.isA(k1)).
+ assert(k1 instanceof K).
+ assert(K.prototype.lookupMember('p4').key === '$p4').
+ assert(K.prototype.lookupMember('$p4').name === 'p4').
+ mustThrowMatching(()=>K.prototype.lookupMember('nope'), /not a mapped/).
+ assert(undefined === K.prototype.lookupMember('nope',false)).
+ assert(k1 instanceof StructType).
+ assert(StructType.isA(k1)).
+ mustThrowMatching(()=>k1.$ro = 1, /read-only/);
+ Object.keys(MyStructDef.members).forEach(function(key){
+ key = K.memberKey(key);
+ T.assert(0 == k1[key],
+ "Expecting allocation to zero the memory "+
+ "for "+key+" but got: "+k1[key]+
+ " from "+k1.memoryDump());
+ });
+ T.assert('number' === typeof k1.pointer).
+ mustThrowMatching(()=>k1.pointer = 1, /pointer/);
+ k1.$p4 = 1; k1.$pP = 2;
+ T.assert(1 === k1.$p4).assert(2 === k1.$pP);
+ if(MyStructDef.members.$p8){
+ k1.$p8 = 1/*must not throw despite not being a BigInt*/;
+ k1.$p8 = BigInt(Number.MAX_SAFE_INTEGER * 2);
+ T.assert(BigInt(2 * Number.MAX_SAFE_INTEGER) === k1.$p8);
+ }
+ T.assert(!k1.ondispose);
+ k1.setMemberCString('cstr', "A C-string.");
+ T.assert(Array.isArray(k1.ondispose)).
+ assert(k1.ondispose[0] === k1.$cstr).
+ assert('number' === typeof k1.$cstr).
+ assert('A C-string.' === k1.memberToJsString('cstr'));
+ k1.$pP = k2;
+ T.assert(k1.$pP === k2.pointer);
+ k1.$pP = null/*null is special-cased to 0.*/;
+ T.assert(0===k1.$pP);
+ let ptr = k1.pointer;
+ k1.dispose();
+ T.assert(undefined === k1.pointer).
+ mustThrowMatching(()=>{k1.$pP=1}, /disposed instance/);
+ }finally{
+ k1.dispose();
+ k2.dispose();
+ }
+
+ if(!W.bigIntEnabled){
+ log("Skipping WasmTestStruct tests: BigInt not enabled.");
+ return;
+ }
+
+ const WTStructDesc =
+ W.ctype.structs.filter((e)=>'WasmTestStruct'===e.name)[0];
+ const autoResolvePtr = true /* EXPERIMENTAL */;
+ if(autoResolvePtr){
+ WTStructDesc.members.ppV.signature = 'P';
+ }
+ const WTStruct = S.StructBinder(WTStructDesc);
+ //log(WTStruct.structName, WTStruct.structInfo);
+ const wts = new WTStruct();
+ //log("WTStruct.prototype keys:",Object.keys(WTStruct.prototype));
+ try{
+ T.assert(wts.constructor === WTStruct).
+ assert(WTStruct.memberKeys().indexOf('$ppV')>=0).
+ assert(wts.memberKeys().indexOf('$v8')>=0).
+ assert(!K.isA(wts)).
+ assert(WTStruct.isA(wts)).
+ assert(wts instanceof WTStruct).
+ assert(wts instanceof StructType).
+ assert(StructType.isA(wts)).
+ assert(wts.pointer>0).assert(0===wts.$v4).assert(0n===wts.$v8).
+ assert(0===wts.$ppV).assert(0===wts.$xFunc);
+ const testFunc =
+ W.xGet('sqlite3_wasm_test_struct'/*name gets mangled in -O3 builds!*/);
+ let counter = 0;
+ //log("wts.pointer =",wts.pointer);
+ const wtsFunc = function(arg){
+ /*log("This from a JS function called from C, "+
+ "which itself was called from JS. arg =",arg);*/
+ ++counter;
+ if(3===counter){
+ tossQuietly("Testing exception propagation.");
+ }
+ }
+ wts.$v4 = 10; wts.$v8 = 20;
+ wts.$xFunc = W.installFunction(wtsFunc, wts.memberSignature('xFunc'))
+ T.assert(0===counter).assert(10 === wts.$v4).assert(20n === wts.$v8)
+ .assert(0 === wts.$ppV).assert('number' === typeof wts.$xFunc)
+ .assert(0 === wts.$cstr)
+ .assert(wts.memberIsString('$cstr'))
+ .assert(!wts.memberIsString('$v4'))
+ .assert(null === wts.memberToJsString('$cstr'))
+ .assert(W.functionEntry(wts.$xFunc) instanceof Function);
+ /* It might seem silly to assert that the values match
+ what we just set, but recall that all of those property
+ reads and writes are, via property interceptors,
+ actually marshaling their data to/from a raw memory
+ buffer, so merely reading them back is actually part of
+ testing the struct-wrapping API. */
+
+ testFunc(wts.pointer);
+ //log("wts.pointer, wts.$ppV",wts.pointer, wts.$ppV);
+ T.assert(1===counter).assert(20 === wts.$v4).assert(40n === wts.$v8)
+ .assert(wts.$ppV === wts.pointer)
+ .assert('string' === typeof wts.memberToJsString('cstr'))
+ .assert(wts.memberToJsString('cstr') === wts.memberToJsString('$cstr'))
+ .mustThrowMatching(()=>wts.memberToJsString('xFunc'),
+ /Invalid member type signature for C-string/)
+ ;
+ testFunc(wts.pointer);
+ T.assert(2===counter).assert(40 === wts.$v4).assert(80n === wts.$v8)
+ .assert(wts.$ppV === wts.pointer);
+ /** The 3rd call to wtsFunc throw from JS, which is called
+ from C, which is called from JS. Let's ensure that
+ that exception propagates back here... */
+ T.mustThrowMatching(()=>testFunc(wts.pointer),/^Testing/);
+ W.uninstallFunction(wts.$xFunc);
+ wts.$xFunc = 0;
+ wts.$ppV = 0;
+ T.assert(!wts.$ppV);
+ //WTStruct.debugFlags(0x03);
+ wts.$ppV = wts;
+ T.assert(wts.pointer === wts.$ppV)
+ wts.setMemberCString('cstr', "A C-string.");
+ T.assert(Array.isArray(wts.ondispose)).
+ assert(wts.ondispose[0] === wts.$cstr).
+ assert('A C-string.' === wts.memberToJsString('cstr'));
+ const ptr = wts.pointer;
+ wts.dispose();
+ T.assert(ptr).assert(undefined === wts.pointer);
+ }finally{
+ wts.dispose();
+ }
+
+ if(1){ // ondispose of other struct instances
+ const s1 = new WTStruct, s2 = new WTStruct, s3 = new WTStruct;
+ T.assert(s1.lookupMember instanceof Function)
+ .assert(s1.addOnDispose instanceof Function);
+ s1.addOnDispose(s2,"testing variadic args");
+ T.assert(2===s1.ondispose.length);
+ s2.addOnDispose(s3);
+ s1.dispose();
+ T.assert(!s2.pointer,"Expecting s2 to be ondispose'd by s1.");
+ T.assert(!s3.pointer,"Expecting s3 to be ondispose'd by s2.");
+ }
+ }/*StructBinder*/)
+
+ ////////////////////////////////////////////////////////////////////
+ .t('sqlite3.wasm.pstack', function(sqlite3){
+ const P = wasm.pstack;
+ const isAllocErr = (e)=>e instanceof sqlite3.WasmAllocError;
+ const stack = P.pointer;
+ T.assert(0===stack % 8 /* must be 8-byte aligned */);
+ try{
+ const remaining = P.remaining;
+ T.assert(P.quota >= 4096)
+ .assert(remaining === P.quota)
+ .mustThrowMatching(()=>P.alloc(0), isAllocErr)
+ .mustThrowMatching(()=>P.alloc(-1), isAllocErr)
+ .mustThrowMatching(
+ ()=>P.alloc('i33'),
+ (e)=>e instanceof sqlite3.WasmAllocError
+ );
+ ;
+ let p1 = P.alloc(12);
+ T.assert(p1 === stack - 16/*8-byte aligned*/)
+ .assert(P.pointer === p1);
+ let p2 = P.alloc(7);
+ T.assert(p2 === p1-8/*8-byte aligned, stack grows downwards*/)
+ .mustThrowMatching(()=>P.alloc(remaining), isAllocErr)
+ .assert(24 === stack - p2)
+ .assert(P.pointer === p2);
+ let n = remaining - (stack - p2);
+ let p3 = P.alloc(n);
+ T.assert(p3 === stack-remaining)
+ .mustThrowMatching(()=>P.alloc(1), isAllocErr);
+ }finally{
+ P.restore(stack);
+ }
+
+ T.assert(P.pointer === stack);
+ try {
+ const [p1, p2, p3] = P.allocChunks(3,'i32');
+ T.assert(P.pointer === stack-16/*always rounded to multiple of 8*/)
+ .assert(p2 === p1 + 4)
+ .assert(p3 === p2 + 4);
+ T.mustThrowMatching(()=>P.allocChunks(1024, 1024 * 16),
+ (e)=>e instanceof sqlite3.WasmAllocError)
+ }finally{
+ P.restore(stack);
+ }
+
+ T.assert(P.pointer === stack);
+ try {
+ let [p1, p2, p3] = P.allocPtr(3,false);
+ let sPos = stack-16/*always rounded to multiple of 8*/;
+ T.assert(P.pointer === sPos)
+ .assert(p2 === p1 + 4)
+ .assert(p3 === p2 + 4);
+ [p1, p2, p3] = P.allocPtr(3);
+ T.assert(P.pointer === sPos-24/*3 x 8 bytes*/)
+ .assert(p2 === p1 + 8)
+ .assert(p3 === p2 + 8);
+ p1 = P.allocPtr();
+ T.assert('number'===typeof p1);
+ }finally{
+ P.restore(stack);
+ }
+ }/*pstack tests*/)
+ ////////////////////////////////////////////////////////////////////
+ ;/*end of C/WASM utils checks*/
+
+ T.g('sqlite3_randomness()')
+ .t('To memory buffer', function(sqlite3){
+ const stack = wasm.pstack.pointer;
+ try{
+ const n = 520;
+ const p = wasm.pstack.alloc(n);
+ T.assert(0===wasm.peek8(p))
+ .assert(0===wasm.peek8(p+n-1));
+ T.assert(undefined === capi.sqlite3_randomness(n - 10, p));
+ let j, check = 0;
+ const heap = wasm.heap8u();
+ for(j = 0; j < 10 && 0===check; ++j){
+ check += heap[p + j];
+ }
+ T.assert(check > 0);
+ check = 0;
+ // Ensure that the trailing bytes were not modified...
+ for(j = n - 10; j < n && 0===check; ++j){
+ check += heap[p + j];
+ }
+ T.assert(0===check);
+ }finally{
+ wasm.pstack.restore(stack);
+ }
+ })
+ .t('To byte array', function(sqlite3){
+ const ta = new Uint8Array(117);
+ let i, n = 0;
+ for(i=0; i<ta.byteLength && 0===n; ++i){
+ n += ta[i];
+ }
+ T.assert(0===n)
+ .assert(ta === capi.sqlite3_randomness(ta));
+ for(i=ta.byteLength-10; i<ta.byteLength && 0===n; ++i){
+ n += ta[i];
+ }
+ T.assert(n>0);
+ const t0 = new Uint8Array(0);
+ T.assert(t0 === capi.sqlite3_randomness(t0),
+ "0-length array is a special case");
+ })
+ ;/*end sqlite3_randomness() checks*/
+
+ ////////////////////////////////////////////////////////////////////////
+ T.g('sqlite3.oo1')
+ .t('Create db', function(sqlite3){
+ const dbFile = '/tester1.db';
+ wasm.sqlite3_wasm_vfs_unlink(0, dbFile);
+ const db = this.db = new sqlite3.oo1.DB(dbFile, 0 ? 'ct' : 'c');
+ db.onclose = {
+ disposeAfter: [],
+ disposeBefore: [
+ (db)=>{
+ //console.debug("db.onclose.before dropping modules");
+ //sqlite3.capi.sqlite3_drop_modules(db.pointer, 0);
+ }
+ ],
+ before: function(db){
+ while(this.disposeBefore.length){
+ const v = this.disposeBefore.shift();
+ console.debug("db.onclose.before cleaning up:",v);
+ if(wasm.isPtr(v)) wasm.dealloc(v);
+ else if(v instanceof sqlite3.StructBinder.StructType){
+ v.dispose();
+ }else if(v instanceof Function){
+ try{ v(db) } catch(e){
+ console.warn("beforeDispose() callback threw:",e);
+ }
+ }
+ }
+ },
+ after: function(){
+ while(this.disposeAfter.length){
+ const v = this.disposeAfter.shift();
+ console.debug("db.onclose.after cleaning up:",v);
+ if(wasm.isPtr(v)) wasm.dealloc(v);
+ else if(v instanceof sqlite3.StructBinder.StructType){
+ v.dispose();
+ }else if(v instanceof Function){
+ try{v()} catch(e){/*ignored*/}
+ }
+ }
+ }
+ };
+
+ T.assert(wasm.isPtr(db.pointer))
+ .mustThrowMatching(()=>db.pointer=1, /read-only/)
+ .assert(0===sqlite3.capi.sqlite3_extended_result_codes(db.pointer,1))
+ .assert('main'===db.dbName(0))
+ .assert('string' === typeof db.dbVfsName())
+ .assert(db.pointer === wasm.xWrap.testConvertArg('sqlite3*',db));
+ // Custom db error message handling via sqlite3_prepare_v2/v3()
+ let rc = capi.sqlite3_prepare_v3(db.pointer, {/*invalid*/}, -1, 0, null, null);
+ T.assert(capi.SQLITE_MISUSE === rc)
+ .assert(0 === capi.sqlite3_errmsg(db.pointer).indexOf("Invalid SQL"))
+ .assert(dbFile === db.dbFilename())
+ .assert(!db.dbFilename('nope'));
+ //Sanity check DB.checkRc()...
+ let ex;
+ try{db.checkRc(rc)}
+ catch(e){ex = e}
+ T.assert(ex instanceof sqlite3.SQLite3Error)
+ .assert(capi.SQLITE_MISUSE===ex.resultCode)
+ .assert(0===ex.message.indexOf("SQLITE_MISUSE: sqlite3 result code"))
+ .assert(ex.message.indexOf("Invalid SQL")>0);
+ T.assert(db === db.checkRc(0))
+ .assert(db === sqlite3.oo1.DB.checkRc(db,0))
+ .assert(null === sqlite3.oo1.DB.checkRc(null,0));
+
+ this.progressHandlerCount = 0;
+ capi.sqlite3_progress_handler(db, 5, (p)=>{
+ ++this.progressHandlerCount;
+ return 0;
+ }, 0);
+ })
+ ////////////////////////////////////////////////////////////////////
+ .t('sqlite3_db_config() and sqlite3_db_status()', function(sqlite3){
+ let rc = capi.sqlite3_db_config(this.db, capi.SQLITE_DBCONFIG_LEGACY_ALTER_TABLE, 0, 0);
+ T.assert(0===rc);
+ rc = capi.sqlite3_db_config(this.db, capi.SQLITE_DBCONFIG_MAX+1, 0);
+ T.assert(capi.SQLITE_MISUSE === rc);
+ rc = capi.sqlite3_db_config(this.db, capi.SQLITE_DBCONFIG_MAINDBNAME, "main");
+ T.assert(0 === rc);
+ const stack = wasm.pstack.pointer;
+ try {
+ const [pCur, pHi] = wasm.pstack.allocChunks(2,'i64');
+ wasm.poke32([pCur, pHi], 0);
+ let [vCur, vHi] = wasm.peek32(pCur, pHi);
+ T.assert(0===vCur).assert(0===vHi);
+ rc = capi.sqlite3_status(capi.SQLITE_STATUS_MEMORY_USED,
+ pCur, pHi, 0);
+ [vCur, vHi] = wasm.peek32(pCur, pHi);
+ //console.warn("i32 vCur,vHi",vCur,vHi);
+ T.assert(0 === rc).assert(vCur > 0).assert(vHi >= vCur);
+ if(wasm.bigIntEnabled){
+ // Again in 64-bit. Recall that pCur and pHi are allocated
+ // large enough to account for this re-use.
+ wasm.poke64([pCur, pHi], 0);
+ rc = capi.sqlite3_status64(capi.SQLITE_STATUS_MEMORY_USED,
+ pCur, pHi, 0);
+ [vCur, vHi] = wasm.peek64([pCur, pHi]);
+ //console.warn("i64 vCur,vHi",vCur,vHi);
+ T.assert(0 === rc).assert(vCur > 0).assert(vHi >= vCur);
+ }
+ }finally{
+ wasm.pstack.restore(stack);
+ }
+ })
+
+ ////////////////////////////////////////////////////////////////////
+ .t('DB.Stmt', function(sqlite3){
+ let st = this.db.prepare(
+ new TextEncoder('utf-8').encode("select 3 as a")
+ );
+ //debug("statement =",st);
+ this.progressHandlerCount = 0;
+ let rc;
+ try {
+ T.assert(wasm.isPtr(st.pointer))
+ .mustThrowMatching(()=>st.pointer=1, /read-only/)
+ .assert(1===this.db.openStatementCount())
+ .assert(
+ capi.sqlite3_stmt_status(
+ st, capi.SQLITE_STMTSTATUS_RUN, 0
+ ) === 0)
+ .assert(!st._mayGet)
+ .assert('a' === st.getColumnName(0))
+ .mustThrowMatching(()=>st.columnCount=2,
+ /columnCount property is read-only/)
+ .assert(1===st.columnCount)
+ .assert(0===st.parameterCount)
+ .mustThrow(()=>st.bind(1,null))
+ .assert(true===st.step())
+ .assert(3 === st.get(0))
+ .mustThrow(()=>st.get(1))
+ .mustThrow(()=>st.get(0,~capi.SQLITE_INTEGER))
+ .assert(3 === st.get(0,capi.SQLITE_INTEGER))
+ .assert(3 === st.getInt(0))
+ .assert('3' === st.get(0,capi.SQLITE_TEXT))
+ .assert('3' === st.getString(0))
+ .assert(3.0 === st.get(0,capi.SQLITE_FLOAT))
+ .assert(3.0 === st.getFloat(0))
+ .assert(3 === st.get({}).a)
+ .assert(3 === st.get([])[0])
+ .assert(3 === st.getJSON(0))
+ .assert(st.get(0,capi.SQLITE_BLOB) instanceof Uint8Array)
+ .assert(1===st.get(0,capi.SQLITE_BLOB).length)
+ .assert(st.getBlob(0) instanceof Uint8Array)
+ .assert('3'.charCodeAt(0) === st.getBlob(0)[0])
+ .assert(st._mayGet)
+ .assert(false===st.step())
+ .assert(!st._mayGet)
+ .assert(
+ capi.sqlite3_stmt_status(
+ st, capi.SQLITE_STMTSTATUS_RUN, 0
+ ) > 0);
+
+ T.assert(this.progressHandlerCount > 0,
+ "Expecting progress callback.").
+ assert(0===capi.sqlite3_strglob("*.txt", "foo.txt")).
+ assert(0!==capi.sqlite3_strglob("*.txt", "foo.xtx")).
+ assert(0===capi.sqlite3_strlike("%.txt", "foo.txt", 0)).
+ assert(0!==capi.sqlite3_strlike("%.txt", "foo.xtx", 0));
+ }finally{
+ rc = st.finalize();
+ }
+ T.assert(!st.pointer)
+ .assert(0===this.db.openStatementCount())
+ .assert(0===rc);
+
+ T.mustThrowMatching(()=>new sqlite3.oo1.Stmt("hi"), function(err){
+ return (err instanceof sqlite3.SQLite3Error)
+ && capi.SQLITE_MISUSE === err.resultCode
+ && 0 < err.message.indexOf("Do not call the Stmt constructor directly.")
+ });
+ })
+
+ ////////////////////////////////////////////////////////////////////////
+ .t('sqlite3_js_...()', function(){
+ const db = this.db;
+ if(1){
+ const vfsList = capi.sqlite3_js_vfs_list();
+ T.assert(vfsList.length>1);
+ wasm.scopedAllocCall(()=>{
+ const vfsArg = (v)=>wasm.xWrap.testConvertArg('sqlite3_vfs*',v);
+ for(const v of vfsList){
+ T.assert('string' === typeof v);
+ const pVfs = capi.sqlite3_vfs_find(v);
+ T.assert(wasm.isPtr(pVfs))
+ .assert(pVfs===vfsArg(v));
+ const vfs = new capi.sqlite3_vfs(pVfs);
+ try { T.assert(vfsArg(vfs)===pVfs) }
+ finally{ vfs.dispose() }
+ }
+ });
+ }
+ /**
+ Trivia: the magic db name ":memory:" does not actually use the
+ "memdb" VFS unless "memdb" is _explicitly_ provided as the VFS
+ name. Instead, it uses the default VFS with an in-memory btree.
+ Thus this.db's VFS may not be memdb even though it's an in-memory
+ db.
+ */
+ const pVfsMem = capi.sqlite3_vfs_find('memdb'),
+ pVfsDflt = capi.sqlite3_vfs_find(0),
+ pVfsDb = capi.sqlite3_js_db_vfs(db.pointer);
+ T.assert(pVfsMem > 0)
+ .assert(pVfsDflt > 0)
+ .assert(pVfsDb > 0)
+ .assert(pVfsMem !== pVfsDflt
+ /* memdb lives on top of the default vfs */)
+ .assert(pVfsDb === pVfsDflt || pVfsdb === pVfsMem)
+ ;
+ /*const vMem = new capi.sqlite3_vfs(pVfsMem),
+ vDflt = new capi.sqlite3_vfs(pVfsDflt),
+ vDb = new capi.sqlite3_vfs(pVfsDb);*/
+ const duv = capi.sqlite3_js_db_uses_vfs;
+ T.assert(pVfsDflt === duv(db.pointer, 0)
+ || pVfsMem === duv(db.pointer,0))
+ .assert(!duv(db.pointer, "foo"))
+ ;
+ }/*sqlite3_js_...()*/)
+
+ ////////////////////////////////////////////////////////////////////
+ .t('Table t', function(sqlite3){
+ const db = this.db;
+ let list = [];
+ this.progressHandlerCount = 0;
+ let rc = db.exec({
+ sql:['CREATE TABLE t(a,b);',
+ // ^^^ using TEMP TABLE breaks the db export test
+ "INSERT INTO t(a,b) VALUES(1,2),(3,4),",
+ "(?,?)"/*intentionally missing semicolon to test for
+ off-by-one bug in string-to-WASM conversion*/],
+ saveSql: list,
+ bind: [5,6]
+ });
+ //debug("Exec'd SQL:", list);
+ T.assert(rc === db)
+ .assert(2 === list.length)
+ .assert('string'===typeof list[1])
+ .assert(3===db.changes())
+ .assert(this.progressHandlerCount > 0,
+ "Expecting progress callback.")
+ if(wasm.bigIntEnabled){
+ T.assert(3n===db.changes(false,true));
+ }
+ rc = db.exec({
+ sql: "INSERT INTO t(a,b) values('blob',X'6869') RETURNING 13",
+ rowMode: 0
+ });
+ T.assert(Array.isArray(rc))
+ .assert(1===rc.length)
+ .assert(13 === rc[0])
+ .assert(1===db.changes());
+
+ let vals = db.selectValues('select a from t order by a limit 2');
+ T.assert( 2 === vals.length )
+ .assert( 1===vals[0] && 3===vals[1] );
+ vals = db.selectValues('select a from t order by a limit $L',
+ {$L:2}, capi.SQLITE_TEXT);
+ T.assert( 2 === vals.length )
+ .assert( '1'===vals[0] && '3'===vals[1] );
+ vals = undefined;
+
+ let blob = db.selectValue("select b from t where a='blob'");
+ T.assert(blob instanceof Uint8Array).
+ assert(0x68===blob[0] && 0x69===blob[1]);
+ blob = null;
+ let counter = 0, colNames = [];
+ list.length = 0;
+ db.exec(new TextEncoder('utf-8').encode("SELECT a a, b b FROM t"),{
+ rowMode: 'object',
+ resultRows: list,
+ columnNames: colNames,
+ _myState: 3 /* Accessible from the callback */,
+ callback: function(row,stmt){
+ ++counter;
+ T.assert(
+ 3 === this._myState
+ /* Recall that "this" is the options object. */
+ ).assert(
+ this.columnNames===colNames
+ ).assert(
+ this.columnNames[0]==='a' && this.columnNames[1]==='b'
+ ).assert(
+ (row.a%2 && row.a<6) || 'blob'===row.a
+ );
+ }
+ });
+ T.assert(2 === colNames.length)
+ .assert('a' === colNames[0])
+ .assert(4 === counter)
+ .assert(4 === list.length);
+ colNames = [];
+ db.exec({
+ /* Ensure that columnNames is populated for empty result sets. */
+ sql: "SELECT a a, b B FROM t WHERE 0",
+ columnNames: colNames
+ });
+ T.assert(2===colNames.length)
+ .assert('a'===colNames[0] && 'B'===colNames[1]);
+ list.length = 0;
+ db.exec("SELECT a a, b b FROM t",{
+ rowMode: 'array',
+ callback: function(row,stmt){
+ ++counter;
+ T.assert(Array.isArray(row))
+ .assert((0===row[1]%2 && row[1]<7)
+ || (row[1] instanceof Uint8Array));
+ }
+ });
+ T.assert(8 === counter);
+ T.assert(Number.MIN_SAFE_INTEGER ===
+ db.selectValue("SELECT "+Number.MIN_SAFE_INTEGER)).
+ assert(Number.MAX_SAFE_INTEGER ===
+ db.selectValue("SELECT "+Number.MAX_SAFE_INTEGER));
+ counter = 0;
+ let rv = db.exec({
+ sql: "SELECT a FROM t",
+ callback: ()=>(1===++counter),
+ });
+ T.assert(db === rv)
+ .assert(2===counter,
+ "Expecting exec step() loop to stop if callback returns false.");
+ /** If exec() is passed neither callback nor returnValue but
+ is passed an explicit rowMode then the default returnValue
+ is the whole result set, as if an empty resultRows option
+ had been passed. */
+ rv = db.exec({
+ sql: "SELECT -1 UNION ALL SELECT -2 UNION ALL SELECT -3 ORDER BY 1 DESC",
+ rowMode: 0
+ });
+ T.assert(Array.isArray(rv)).assert(3===rv.length)
+ .assert(-1===rv[0]).assert(-3===rv[2]);
+ rv = db.exec("SELECT 1 WHERE 0",{rowMode: 0});
+ T.assert(Array.isArray(rv)).assert(0===rv.length);
+ if(wasm.bigIntEnabled && haveWasmCTests()){
+ const mI = wasm.xCall('sqlite3_wasm_test_int64_max');
+ const b = BigInt(Number.MAX_SAFE_INTEGER * 2);
+ T.assert(b === db.selectValue("SELECT "+b)).
+ assert(b === db.selectValue("SELECT ?", b)).
+ assert(mI == db.selectValue("SELECT $x", {$x:mI}));
+ }else{
+ /* Curiously, the JS spec seems to be off by one with the definitions
+ of MIN/MAX_SAFE_INTEGER:
+
+ https://github.com/emscripten-core/emscripten/issues/17391 */
+ T.mustThrow(()=>db.selectValue("SELECT "+(Number.MAX_SAFE_INTEGER+1))).
+ mustThrow(()=>db.selectValue("SELECT "+(Number.MIN_SAFE_INTEGER-1)));
+ }
+
+ let st = db.prepare("update t set b=:b where a='blob'");
+ try {
+ T.assert(0===st.columnCount);
+ const ndx = st.getParamIndex(':b');
+ T.assert(1===ndx);
+ st.bindAsBlob(ndx, "ima blob")
+ /*step() skipped intentionally*/.reset(true);
+ } finally {
+ T.assert(0===st.finalize())
+ .assert(undefined===st.finalize());
+ }
+
+ try {
+ db.prepare("/*empty SQL*/");
+ toss("Must not be reached.");
+ }catch(e){
+ T.assert(e instanceof sqlite3.SQLite3Error)
+ .assert(0==e.message.indexOf('Cannot prepare empty'));
+ }
+
+ counter = 0;
+ db.exec({
+ // Check for https://sqlite.org/forum/forumpost/895425b49a
+ sql: "pragma table_info('t')",
+ rowMode: 'object',
+ callback: function(row){
+ ++counter;
+ T.assert(row.name==='a' || row.name==='b');
+ }
+ });
+ T.assert(2===counter);
+ })/*setup table T*/
+
+ ////////////////////////////////////////////////////////////////////
+ .t({
+ name: "sqlite3_set_authorizer()",
+ test:function(sqlite3){
+ T.assert(capi.SQLITE_IGNORE>0)
+ .assert(capi.SQLITE_DENY>0);
+ const db = this.db;
+ const ssa = capi.sqlite3_set_authorizer;
+ const n = db.selectValue('select count(*) from t');
+ T.assert(n>0);
+ let authCount = 0;
+ let rc = ssa(db, function(pV, iCode, s0, s1, s2, s3){
+ ++authCount;
+ return capi.SQLITE_IGNORE;
+ }, 0);
+ T.assert(0===rc)
+ .assert(
+ undefined === db.selectValue('select count(*) from t')
+ /* Note that the count() never runs, so we get undefined
+ instead of 0. */
+ )
+ .assert(authCount>0);
+ authCount = 0;
+ db.exec("update t set a=-9999");
+ T.assert(authCount>0);
+ /* Reminder: we don't use DELETE because, from the C API docs:
+
+ "If the action code is [SQLITE_DELETE] and the callback
+ returns [SQLITE_IGNORE] then the [DELETE] operation proceeds
+ but the [truncate optimization] is disabled and all rows are
+ deleted individually."
+ */
+ rc = ssa(db, null, 0);
+ authCount = 0;
+ T.assert(-9999 != db.selectValue('select a from t'))
+ .assert(0===authCount);
+ rc = ssa(db, function(pV, iCode, s0, s1, s2, s3){
+ ++authCount;
+ return capi.SQLITE_DENY;
+ }, 0);
+ T.assert(0===rc);
+ let err;
+ try{ db.exec("select 1 from t") }
+ catch(e){ err = e }
+ T.assert(err instanceof sqlite3.SQLite3Error)
+ .assert(err.message.indexOf('not authorized'>0))
+ .assert(1===authCount);
+ authCount = 0;
+ rc = ssa(db, function(...args){
+ ++authCount;
+ return capi.SQLITE_OK;
+ }, 0);
+ T.assert(0===rc);
+ T.assert(n === db.selectValue('select count(*) from t'))
+ .assert(authCount>0);
+ authCount = 0;
+ rc = ssa(db, function(pV, iCode, s0, s1, s2, s3){
+ ++authCount;
+ throw new Error("Testing catching of authorizer.");
+ }, 0);
+ T.assert(0===rc);
+ authCount = 0;
+ err = undefined;
+ try{ db.exec("select 1 from t") }
+ catch(e){err = e}
+ T.assert(err instanceof Error)
+ .assert(err.message.indexOf('not authorized')>0)
+ /* Note that the thrown message is trumped/overwritten
+ by the authorizer process. */
+ .assert(1===authCount);
+ rc = ssa(db, 0, 0);
+ authCount = 0;
+ T.assert(0===rc);
+ T.assert(n === db.selectValue('select count(*) from t'))
+ .assert(0===authCount);
+ }
+ })/*sqlite3_set_authorizer()*/
+
+ ////////////////////////////////////////////////////////////////////////
+ .t("sqlite3_table_column_metadata()", function(sqlite3){
+ const stack = wasm.pstack.pointer;
+ try{
+ const [pzDT, pzColl, pNotNull, pPK, pAuto] =
+ wasm.pstack.allocPtr(5);
+ const rc = capi.sqlite3_table_column_metadata(
+ this.db, "main", "t", "rowid",
+ pzDT, pzColl, pNotNull, pPK, pAuto
+ );
+ T.assert(0===rc)
+ .assert("INTEGER"===wasm.cstrToJs(wasm.peekPtr(pzDT)))
+ .assert("BINARY"===wasm.cstrToJs(wasm.peekPtr(pzColl)))
+ .assert(0===wasm.peek32(pNotNull))
+ .assert(1===wasm.peek32(pPK))
+ .assert(0===wasm.peek32(pAuto))
+ }finally{
+ wasm.pstack.restore(stack);
+ }
+ })
+
+ ////////////////////////////////////////////////////////////////////////
+ .t('selectArray/Object()', function(sqlite3){
+ const db = this.db;
+ let rc = db.selectArray('select a, b from t where a=?', 5);
+ T.assert(Array.isArray(rc))
+ .assert(2===rc.length)
+ .assert(5===rc[0] && 6===rc[1]);
+ rc = db.selectArray('select a, b from t where b=-1');
+ T.assert(undefined === rc);
+ rc = db.selectObject('select a A, b b from t where b=?', 6);
+ T.assert(rc && 'object'===typeof rc)
+ .assert(5===rc.A)
+ .assert(6===rc.b);
+ rc = db.selectArray('select a, b from t where b=-1');
+ T.assert(undefined === rc);
+ })
+ ////////////////////////////////////////////////////////////////////////
+ .t('selectArrays/Objects()', function(sqlite3){
+ const db = this.db;
+ const sql = 'select a, b from t where a=? or b=? order by a';
+ let rc = db.selectArrays(sql, [1, 4]);
+ T.assert(Array.isArray(rc))
+ .assert(2===rc.length)
+ .assert(2===rc[0].length)
+ .assert(1===rc[0][0])
+ .assert(2===rc[0][1])
+ .assert(3===rc[1][0])
+ .assert(4===rc[1][1])
+ rc = db.selectArrays(sql, [99,99]);
+ T.assert(Array.isArray(rc)).assert(0===rc.length);
+ rc = db.selectObjects(sql, [1,4]);
+ T.assert(Array.isArray(rc))
+ .assert(2===rc.length)
+ .assert('object' === typeof rc[1])
+ .assert(1===rc[0].a)
+ .assert(2===rc[0].b)
+ .assert(3===rc[1].a)
+ .assert(4===rc[1].b);
+ })
+ ////////////////////////////////////////////////////////////////////////
+ .t('selectArray/Object/Values() via INSERT/UPDATE...RETURNING', function(sqlite3){
+ let rc = this.db.selectObject("INSERT INTO t(a,b) VALUES(83,84) RETURNING a as AA");
+ T.assert(83===rc.AA);
+ rc = this.db.selectArray("UPDATE T set a=85 WHERE a=83 RETURNING b as BB");
+ T.assert(Array.isArray(rc)).assert(84===rc[0]);
+ //log("select * from t:",this.db.selectObjects("select * from t order by a"));
+ rc = this.db.selectValues("UPDATE T set a=a*1 RETURNING a");
+ T.assert(Array.isArray(rc))
+ .assert(5 === rc.length)
+ .assert('number'===typeof rc[0])
+ .assert(rc[0]|0 === rc[0] /* is small integer */);
+ })
+ ////////////////////////////////////////////////////////////////////////
+ .t({
+ name: 'sqlite3_js_db_export()',
+ predicate: ()=>true,
+ test: function(sqlite3){
+ const db = this.db;
+ const xp = capi.sqlite3_js_db_export(db.pointer);
+ T.assert(xp instanceof Uint8Array)
+ .assert(xp.byteLength>0)
+ .assert(0 === xp.byteLength % 512);
+ this.dbExport = xp;
+ }
+ }/*sqlite3_js_db_export()*/)
+ .t({
+ name: 'sqlite3_js_posix_create_file()',
+ predicate: ()=>true,
+ test: function(sqlite3){
+ const db = this.db;
+ const filename = "sqlite3_js_posix_create_file.db";
+ capi.sqlite3_js_posix_create_file(filename, this.dbExport);
+ delete this.dbExport;
+ const db2 = new sqlite3.oo1.DB(filename,'r');
+ try {
+ const sql = "select count(*) from t";
+ const n = db.selectValue(sql);
+ T.assert(n>0 && db2.selectValue(sql) === n);
+ }finally{
+ db2.close();
+ wasm.sqlite3_wasm_vfs_unlink(0, filename);
+ }
+ }
+ }/*sqlite3_js_posix_create_file()*/)
+
+ ////////////////////////////////////////////////////////////////////
+ .t({
+ name:'Scalar UDFs',
+ test: function(sqlite3){
+ const db = this.db;
+ db.createFunction("foo",(pCx,a,b)=>a+b);
+ T.assert(7===db.selectValue("select foo(3,4)")).
+ assert(5===db.selectValue("select foo(3,?)",2)).
+ assert(5===db.selectValue("select foo(?,?2)",[1,4])).
+ assert(5===db.selectValue("select foo($a,$b)",{$a:0,$b:5}));
+ db.createFunction("bar", {
+ arity: -1,
+ xFunc: (pCx,...args)=>{
+ T.assert(db.pointer === capi.sqlite3_context_db_handle(pCx));
+ let rc = 0;
+ for(const v of args) rc += v;
+ return rc;
+ }
+ }).createFunction({
+ name: "asis",
+ xFunc: (pCx,arg)=>arg
+ });
+ T.assert(0===db.selectValue("select bar()")).
+ assert(1===db.selectValue("select bar(1)")).
+ assert(3===db.selectValue("select bar(1,2)")).
+ assert(-1===db.selectValue("select bar(1,2,-4)")).
+ assert('hi' === db.selectValue("select asis('hi')")).
+ assert('hi' === db.selectValue("select ?",'hi')).
+ assert(null === db.selectValue("select null")).
+ assert(null === db.selectValue("select asis(null)")).
+ assert(1 === db.selectValue("select ?",1)).
+ assert(2 === db.selectValue("select ?",[2])).
+ assert(3 === db.selectValue("select $a",{$a:3})).
+ assert(T.eqApprox(3.1,db.selectValue("select 3.0 + 0.1"))).
+ assert(T.eqApprox(1.3,db.selectValue("select asis(1 + 0.3)")));
+
+ let blobArg = new Uint8Array([0x68, 0x69]);
+ let blobRc = db.selectValue(
+ "select asis(?1)",
+ blobArg.buffer/*confirm that ArrayBuffer is handled as a Uint8Array*/
+ );
+ T.assert(blobRc instanceof Uint8Array).
+ assert(2 === blobRc.length).
+ assert(0x68==blobRc[0] && 0x69==blobRc[1]);
+ blobRc = db.selectValue("select asis(X'6869')");
+ T.assert(blobRc instanceof Uint8Array).
+ assert(2 === blobRc.length).
+ assert(0x68==blobRc[0] && 0x69==blobRc[1]);
+
+ blobArg = new Int8Array([0x68, 0x69]);
+ //debug("blobArg=",blobArg);
+ blobRc = db.selectValue("select asis(?1)", blobArg);
+ T.assert(blobRc instanceof Uint8Array).
+ assert(2 === blobRc.length);
+ //debug("blobRc=",blobRc);
+ T.assert(0x68==blobRc[0] && 0x69==blobRc[1]);
+
+ let rc = sqlite3.capi.sqlite3_create_function_v2(
+ this.db, "foo", 0, -1, 0, 0, 0, 0, 0
+ );
+ T.assert(
+ sqlite3.capi.SQLITE_FORMAT === rc,
+ "For invalid eTextRep argument."
+ );
+ rc = sqlite3.capi.sqlite3_create_function_v2(this.db, "foo", 0);
+ T.assert(
+ sqlite3.capi.SQLITE_MISUSE === rc,
+ "For invalid arg count."
+ );
+
+ /* Confirm that we can map and unmap the same function with
+ multiple arities... */
+ const fCounts = [0,0];
+ const fArityCheck = function(pCx){
+ return ++fCounts[arguments.length-1];
+ };
+ //wasm.xWrap.FuncPtrAdapter.debugFuncInstall = true;
+ rc = capi.sqlite3_create_function_v2(
+ db, "nary", 0, capi.SQLITE_UTF8, 0, fArityCheck, 0, 0, 0
+ );
+ T.assert( 0===rc );
+ rc = capi.sqlite3_create_function_v2(
+ db, "nary", 1, capi.SQLITE_UTF8, 0, fArityCheck, 0, 0, 0
+ );
+ T.assert( 0===rc );
+ const sqlFArity0 = "select nary()";
+ const sqlFArity1 = "select nary(1)";
+ T.assert( 1 === db.selectValue(sqlFArity0) )
+ .assert( 1 === fCounts[0] ).assert( 0 === fCounts[1] );
+ T.assert( 1 === db.selectValue(sqlFArity1) )
+ .assert( 1 === fCounts[0] ).assert( 1 === fCounts[1] );
+ capi.sqlite3_create_function_v2(
+ db, "nary", 0, capi.SQLITE_UTF8, 0, 0, 0, 0, 0
+ );
+ T.mustThrowMatching((()=>db.selectValue(sqlFArity0)),
+ (e)=>((e instanceof sqlite3.SQLite3Error)
+ && e.message.indexOf("wrong number of arguments")>0),
+ "0-arity variant was uninstalled.");
+ T.assert( 2 === db.selectValue(sqlFArity1) )
+ .assert( 1 === fCounts[0] ).assert( 2 === fCounts[1] );
+ capi.sqlite3_create_function_v2(
+ db, "nary", 1, capi.SQLITE_UTF8, 0, 0, 0, 0, 0
+ );
+ T.mustThrowMatching((()=>db.selectValue(sqlFArity1)),
+ (e)=>((e instanceof sqlite3.SQLite3Error)
+ && e.message.indexOf("no such function")>0),
+ "1-arity variant was uninstalled.");
+ //wasm.xWrap.FuncPtrAdapter.debugFuncInstall = false;
+ }
+ })
+
+ ////////////////////////////////////////////////////////////////////
+ .t({
+ name: 'Aggregate UDFs',
+ //predicate: ()=>false,
+ test: function(sqlite3){
+ const db = this.db;
+ const sjac = capi.sqlite3_js_aggregate_context;
+ db.createFunction({
+ name: 'summer',
+ xStep: (pCtx, n)=>{
+ const ac = sjac(pCtx, 4);
+ wasm.poke32(ac, wasm.peek32(ac) + Number(n));
+ },
+ xFinal: (pCtx)=>{
+ const ac = sjac(pCtx, 0);
+ return ac ? wasm.peek32(ac) : 0;
+ }
+ });
+ let v = db.selectValue([
+ "with cte(v) as (",
+ "select 3 union all select 5 union all select 7",
+ ") select summer(v), summer(v+1) from cte"
+ /* ------------------^^^^^^^^^^^ ensures that we're handling
+ sqlite3_aggregate_context() properly. */
+ ]);
+ T.assert(15===v);
+ T.mustThrowMatching(()=>db.selectValue("select summer(1,2)"),
+ /wrong number of arguments/);
+
+ db.createFunction({
+ name: 'summerN',
+ arity: -1,
+ xStep: (pCtx, ...args)=>{
+ const ac = sjac(pCtx, 4);
+ let sum = wasm.peek32(ac);
+ for(const v of args) sum += Number(v);
+ wasm.poke32(ac, sum);
+ },
+ xFinal: (pCtx)=>{
+ const ac = sjac(pCtx, 0);
+ capi.sqlite3_result_int( pCtx, ac ? wasm.peek32(ac) : 0 );
+ // xFinal() may either return its value directly or call
+ // sqlite3_result_xyz() and return undefined. Both are
+ // functionally equivalent.
+ }
+ });
+ T.assert(18===db.selectValue('select summerN(1,8,9), summerN(2,3,4)'));
+ T.mustThrowMatching(()=>{
+ db.createFunction('nope',{
+ xFunc: ()=>{}, xStep: ()=>{}
+ });
+ }, /scalar or aggregate\?/);
+ T.mustThrowMatching(()=>{
+ db.createFunction('nope',{xStep: ()=>{}});
+ }, /Missing xFinal/);
+ T.mustThrowMatching(()=>{
+ db.createFunction('nope',{xFinal: ()=>{}});
+ }, /Missing xStep/);
+ T.mustThrowMatching(()=>{
+ db.createFunction('nope',{});
+ }, /Missing function-type properties/);
+ T.mustThrowMatching(()=>{
+ db.createFunction('nope',{xFunc:()=>{}, xDestroy:'nope'});
+ }, /xDestroy property must be a function/);
+ T.mustThrowMatching(()=>{
+ db.createFunction('nope',{xFunc:()=>{}, pApp:'nope'});
+ }, /Invalid value for pApp/);
+ }
+ }/*aggregate UDFs*/)
+
+ ////////////////////////////////////////////////////////////////////////
+ .t({
+ name: 'Aggregate UDFs (64-bit)',
+ predicate: ()=>wasm.bigIntEnabled,
+ //predicate: ()=>false,
+ test: function(sqlite3){
+ const db = this.db;
+ const sjac = capi.sqlite3_js_aggregate_context;
+ db.createFunction({
+ name: 'summer64',
+ xStep: (pCtx, n)=>{
+ const ac = sjac(pCtx, 8);
+ wasm.poke64(ac, wasm.peek64(ac) + BigInt(n));
+ },
+ xFinal: (pCtx)=>{
+ const ac = sjac(pCtx, 0);
+ return ac ? wasm.peek64(ac) : 0n;
+ }
+ });
+ let v = db.selectValue([
+ "with cte(v) as (",
+ "select 9007199254740991 union all select 1 union all select 2",
+ ") select summer64(v), summer64(v+1) from cte"
+ ]);
+ T.assert(9007199254740994n===v);
+ }
+ }/*aggregate UDFs*/)
+
+ ////////////////////////////////////////////////////////////////////
+ .t({
+ name: 'Window UDFs',
+ //predicate: ()=>false,
+ test: function(){
+ /* Example window function, table, and results taken from:
+ https://sqlite.org/windowfunctions.html#udfwinfunc */
+ const db = this.db;
+ const sjac = (cx,n=4)=>capi.sqlite3_js_aggregate_context(cx,n);
+ const xValueFinal = (pCtx)=>{
+ const ac = sjac(pCtx, 0);
+ return ac ? wasm.peek32(ac) : 0;
+ };
+ const xStepInverse = (pCtx, n)=>{
+ const ac = sjac(pCtx);
+ wasm.poke32(ac, wasm.peek32(ac) + Number(n));
+ };
+ db.createFunction({
+ name: 'winsumint',
+ xStep: (pCtx, n)=>xStepInverse(pCtx, n),
+ xInverse: (pCtx, n)=>xStepInverse(pCtx, -n),
+ xFinal: xValueFinal,
+ xValue: xValueFinal
+ });
+ db.exec([
+ "CREATE TEMP TABLE twin(x, y); INSERT INTO twin VALUES",
+ "('a', 4),('b', 5),('c', 3),('d', 8),('e', 1)"
+ ]);
+ let rc = db.exec({
+ returnValue: 'resultRows',
+ sql:[
+ "SELECT x, winsumint(y) OVER (",
+ "ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING",
+ ") AS sum_y ",
+ "FROM twin ORDER BY x;"
+ ]
+ });
+ T.assert(Array.isArray(rc))
+ .assert(5 === rc.length);
+ let count = 0;
+ for(const row of rc){
+ switch(++count){
+ case 1: T.assert('a'===row[0] && 9===row[1]); break;
+ case 2: T.assert('b'===row[0] && 12===row[1]); break;
+ case 3: T.assert('c'===row[0] && 16===row[1]); break;
+ case 4: T.assert('d'===row[0] && 12===row[1]); break;
+ case 5: T.assert('e'===row[0] && 9===row[1]); break;
+ default: toss("Too many rows to window function.");
+ }
+ }
+ const resultRows = [];
+ rc = db.exec({
+ resultRows,
+ returnValue: 'resultRows',
+ sql:[
+ "SELECT x, winsumint(y) OVER (",
+ "ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING",
+ ") AS sum_y ",
+ "FROM twin ORDER BY x;"
+ ]
+ });
+ T.assert(rc === resultRows)
+ .assert(5 === rc.length);
+
+ rc = db.exec({
+ returnValue: 'saveSql',
+ sql: "select 1; select 2; -- empty\n; select 3"
+ });
+ T.assert(Array.isArray(rc))
+ .assert(3===rc.length)
+ .assert('select 1;' === rc[0])
+ .assert('select 2;' === rc[1])
+ .assert('-- empty\n; select 3' === rc[2]
+ /* Strange but true. */);
+ T.mustThrowMatching(()=>{
+ db.exec({sql:'', returnValue: 'nope'});
+ }, /^Invalid returnValue/);
+
+ db.exec("DROP TABLE twin");
+ }
+ }/*window UDFs*/)
+
+ ////////////////////////////////////////////////////////////////////
+ .t("ATTACH", function(){
+ const db = this.db;
+ const resultRows = [];
+ db.exec({
+ sql:new TextEncoder('utf-8').encode([
+ // ^^^ testing string-vs-typedarray handling in exec()
+ "attach 'session' as foo;",
+ "create table foo.bar(a);",
+ "insert into foo.bar(a) values(1),(2),(3);",
+ "select a from foo.bar order by a;"
+ ].join('')),
+ rowMode: 0,
+ resultRows
+ });
+ T.assert(3===resultRows.length)
+ .assert(2===resultRows[1]);
+ T.assert(2===db.selectValue('select a from foo.bar where a>1 order by a'));
+
+ /** Demonstrate the JS-simplified form of the sqlite3_exec() callback... */
+ let colCount = 0, rowCount = 0;
+ let rc = capi.sqlite3_exec(
+ db, "select a, a*2 from foo.bar", function(aVals, aNames){
+ //console.warn("execCallback(",arguments,")");
+ colCount = aVals.length;
+ ++rowCount;
+ T.assert(2===aVals.length)
+ .assert(2===aNames.length)
+ .assert(+(aVals[1]) === 2 * +(aVals[0]));
+ }, 0, 0
+ );
+ T.assert(0===rc).assert(3===rowCount).assert(2===colCount);
+ rc = capi.sqlite3_exec(
+ db.pointer, "select a from foo.bar", ()=>{
+ tossQuietly("Testing throwing from exec() callback.");
+ }, 0, 0
+ );
+ T.assert(capi.SQLITE_ABORT === rc);
+
+ /* Demonstrate how to get access to the "full" callback
+ signature, as opposed to the simplified JS-specific one... */
+ rowCount = colCount = 0;
+ const pCb = wasm.installFunction('i(pipp)', function(pVoid,nCols,aVals,aCols){
+ /* Tip: wasm.cArgvToJs() can be used to convert aVals and
+ aCols to arrays: const vals = wasm.cArgvToJs(nCols,
+ aVals); */
+ ++rowCount;
+ colCount = nCols;
+ T.assert(2 === nCols)
+ .assert(wasm.isPtr(pVoid))
+ .assert(wasm.isPtr(aVals))
+ .assert(wasm.isPtr(aCols))
+ .assert(+wasm.cstrToJs(wasm.peekPtr(aVals + wasm.ptrSizeof))
+ === 2 * +wasm.cstrToJs(wasm.peekPtr(aVals)));
+ return 0;
+ });
+ try {
+ T.assert(wasm.isPtr(pCb));
+ rc = capi.sqlite3_exec(
+ db, new TextEncoder('utf-8').encode("select a, a*2 from foo.bar"),
+ pCb, 0, 0
+ );
+ T.assert(0===rc)
+ .assert(3===rowCount)
+ .assert(2===colCount);
+ }finally{
+ wasm.uninstallFunction(pCb);
+ }
+
+ // Demonstrate that an OOM result does not propagate through sqlite3_exec()...
+ rc = capi.sqlite3_exec(
+ db, ["select a,"," a*2 from foo.bar"], (aVals, aNames)=>{
+ sqlite3.WasmAllocError.toss("just testing");
+ }, 0, 0
+ );
+ T.assert(capi.SQLITE_ABORT === rc);
+
+ db.exec("detach foo");
+ T.mustThrow(()=>db.exec("select * from foo.bar"),
+ "Because foo is no longer attached.");
+ })
+
+ ////////////////////////////////////////////////////////////////////
+ .t({
+ name: 'C-side WASM tests',
+ predicate: ()=>(haveWasmCTests() || "Not compiled in."),
+ test: function(){
+ const w = wasm, db = this.db;
+ const stack = w.scopedAllocPush();
+ let ptrInt;
+ const origValue = 512;
+ try{
+ ptrInt = w.scopedAlloc(4);
+ w.poke32(ptrInt,origValue);
+ const cf = w.xGet('sqlite3_wasm_test_intptr');
+ const oldPtrInt = ptrInt;
+ T.assert(origValue === w.peek32(ptrInt));
+ const rc = cf(ptrInt);
+ T.assert(2*origValue === rc).
+ assert(rc === w.peek32(ptrInt)).
+ assert(oldPtrInt === ptrInt);
+ const pi64 = w.scopedAlloc(8)/*ptr to 64-bit integer*/;
+ const o64 = 0x010203040506/*>32-bit integer*/;
+ if(w.bigIntEnabled){
+ w.poke64(pi64, o64);
+ //log("pi64 =",pi64, "o64 = 0x",o64.toString(16), o64);
+ const v64 = ()=>w.peek64(pi64)
+ T.assert(v64() == o64);
+ //T.assert(o64 === w.peek64(pi64));
+ const cf64w = w.xGet('sqlite3_wasm_test_int64ptr');
+ cf64w(pi64);
+ T.assert(v64() == BigInt(2 * o64));
+ cf64w(pi64);
+ T.assert(v64() == BigInt(4 * o64));
+
+ const biTimes2 = w.xGet('sqlite3_wasm_test_int64_times2');
+ T.assert(BigInt(2 * o64) ===
+ biTimes2(BigInt(o64)/*explicit conv. required to avoid TypeError
+ in the call :/ */));
+
+ const pMin = w.scopedAlloc(16);
+ const pMax = pMin + 8;
+ const g64 = (p)=>w.peek64(p);
+ w.poke64([pMin, pMax], 0);
+ const minMaxI64 = [
+ w.xCall('sqlite3_wasm_test_int64_min'),
+ w.xCall('sqlite3_wasm_test_int64_max')
+ ];
+ T.assert(minMaxI64[0] < BigInt(Number.MIN_SAFE_INTEGER)).
+ assert(minMaxI64[1] > BigInt(Number.MAX_SAFE_INTEGER));
+ //log("int64_min/max() =",minMaxI64, typeof minMaxI64[0]);
+ w.xCall('sqlite3_wasm_test_int64_minmax', pMin, pMax);
+ T.assert(g64(pMin) === minMaxI64[0], "int64 mismatch").
+ assert(g64(pMax) === minMaxI64[1], "int64 mismatch");
+ //log("pMin",g64(pMin), "pMax",g64(pMax));
+ w.poke64(pMin, minMaxI64[0]);
+ T.assert(g64(pMin) === minMaxI64[0]).
+ assert(minMaxI64[0] === db.selectValue("select ?",g64(pMin))).
+ assert(minMaxI64[1] === db.selectValue("select ?",g64(pMax)));
+ const rxRange = /too big/;
+ T.mustThrowMatching(()=>{db.prepare("select ?").bind(minMaxI64[0] - BigInt(1))},
+ rxRange).
+ mustThrowMatching(()=>{db.prepare("select ?").bind(minMaxI64[1] + BigInt(1))},
+ (e)=>rxRange.test(e.message));
+ }else{
+ log("No BigInt support. Skipping related tests.");
+ log("\"The problem\" here is that we can manipulate, at the byte level,",
+ "heap memory to set 64-bit values, but we can't get those values",
+ "back into JS because of the lack of 64-bit integer support.");
+ }
+ }finally{
+ const x = w.scopedAlloc(1), y = w.scopedAlloc(1), z = w.scopedAlloc(1);
+ //log("x=",x,"y=",y,"z=",z); // just looking at the alignment
+ w.scopedAllocPop(stack);
+ }
+ }
+ }/* jaccwabyt-specific tests */)
+
+ ////////////////////////////////////////////////////////////////////////
+ .t({
+ name: 'virtual table #1: eponymous w/ manual exception handling',
+ predicate: ()=>!!capi.sqlite3_index_info,
+ test: function(sqlite3){
+ const VT = sqlite3.vtab;
+ const tmplCols = Object.assign(Object.create(null),{
+ A: 0, B: 1
+ });
+ /**
+ The vtab demonstrated here is a JS-ification of
+ ext/misc/templatevtab.c.
+ */
+ const tmplMod = new sqlite3.capi.sqlite3_module();
+ T.assert(0===tmplMod.$xUpdate);
+ tmplMod.setupModule({
+ catchExceptions: false,
+ methods: {
+ xConnect: function(pDb, pAux, argc, argv, ppVtab, pzErr){
+ try{
+ const args = wasm.cArgvToJs(argc, argv);
+ T.assert(args.length>=3)
+ .assert(args[0] === 'testvtab')
+ .assert(args[1] === 'main')
+ .assert(args[2] === 'testvtab');
+ //console.debug("xConnect() args =",args);
+ const rc = capi.sqlite3_declare_vtab(
+ pDb, "CREATE TABLE ignored(a,b)"
+ );
+ if(0===rc){
+ const t = VT.xVtab.create(ppVtab);
+ T.assert(t === VT.xVtab.get(wasm.peekPtr(ppVtab)));
+ }
+ return rc;
+ }catch(e){
+ if(!(e instanceof sqlite3.WasmAllocError)){
+ wasm.dealloc(wasm.peekPtr, pzErr);
+ wasm.pokePtr(pzErr, wasm.allocCString(e.message));
+ }
+ return VT.xError('xConnect',e);
+ }
+ },
+ xCreate: true /* just for testing. Will be removed afterwards. */,
+ xDisconnect: function(pVtab){
+ try {
+ VT.xVtab.unget(pVtab).dispose();
+ return 0;
+ }catch(e){
+ return VT.xError('xDisconnect',e);
+ }
+ },
+ xOpen: function(pVtab, ppCursor){
+ try{
+ const t = VT.xVtab.get(pVtab),
+ c = VT.xCursor.create(ppCursor);
+ T.assert(t instanceof capi.sqlite3_vtab)
+ .assert(c instanceof capi.sqlite3_vtab_cursor);
+ c._rowId = 0;
+ return 0;
+ }catch(e){
+ return VT.xError('xOpen',e);
+ }
+ },
+ xClose: function(pCursor){
+ try{
+ const c = VT.xCursor.unget(pCursor);
+ T.assert(c instanceof capi.sqlite3_vtab_cursor)
+ .assert(!VT.xCursor.get(pCursor));
+ c.dispose();
+ return 0;
+ }catch(e){
+ return VT.xError('xClose',e);
+ }
+ },
+ xNext: function(pCursor){
+ try{
+ const c = VT.xCursor.get(pCursor);
+ ++c._rowId;
+ return 0;
+ }catch(e){
+ return VT.xError('xNext',e);
+ }
+ },
+ xColumn: function(pCursor, pCtx, iCol){
+ try{
+ const c = VT.xCursor.get(pCursor);
+ switch(iCol){
+ case tmplCols.A:
+ capi.sqlite3_result_int(pCtx, 1000 + c._rowId);
+ break;
+ case tmplCols.B:
+ capi.sqlite3_result_int(pCtx, 2000 + c._rowId);
+ break;
+ default: sqlite3.SQLite3Error.toss("Invalid column id",iCol);
+ }
+ return 0;
+ }catch(e){
+ return VT.xError('xColumn',e);
+ }
+ },
+ xRowid: function(pCursor, ppRowid64){
+ try{
+ const c = VT.xCursor.get(pCursor);
+ VT.xRowid(ppRowid64, c._rowId);
+ return 0;
+ }catch(e){
+ return VT.xError('xRowid',e);
+ }
+ },
+ xEof: function(pCursor){
+ const c = VT.xCursor.get(pCursor),
+ rc = c._rowId>=10;
+ return rc;
+ },
+ xFilter: function(pCursor, idxNum, idxCStr,
+ argc, argv/* [sqlite3_value* ...] */){
+ try{
+ const c = VT.xCursor.get(pCursor);
+ c._rowId = 0;
+ const list = capi.sqlite3_values_to_js(argc, argv);
+ T.assert(argc === list.length);
+ //log(argc,"xFilter value(s):",list);
+ return 0;
+ }catch(e){
+ return VT.xError('xFilter',e);
+ }
+ },
+ xBestIndex: function(pVtab, pIdxInfo){
+ try{
+ //const t = VT.xVtab.get(pVtab);
+ const sii = capi.sqlite3_index_info;
+ const pii = new sii(pIdxInfo);
+ pii.$estimatedRows = 10;
+ pii.$estimatedCost = 10.0;
+ //log("xBestIndex $nConstraint =",pii.$nConstraint);
+ if(pii.$nConstraint>0){
+ // Validate nthConstraint() and nthConstraintUsage()
+ const max = pii.$nConstraint;
+ for(let i=0; i < max; ++i ){
+ let v = pii.nthConstraint(i,true);
+ T.assert(wasm.isPtr(v));
+ v = pii.nthConstraint(i);
+ T.assert(v instanceof sii.sqlite3_index_constraint)
+ .assert(v.pointer >= pii.$aConstraint);
+ v.dispose();
+ v = pii.nthConstraintUsage(i,true);
+ T.assert(wasm.isPtr(v));
+ v = pii.nthConstraintUsage(i);
+ T.assert(v instanceof sii.sqlite3_index_constraint_usage)
+ .assert(v.pointer >= pii.$aConstraintUsage);
+ v.$argvIndex = i;//just to get some values into xFilter
+ v.dispose();
+ }
+ }
+ //log("xBestIndex $nOrderBy =",pii.$nOrderBy);
+ if(pii.$nOrderBy>0){
+ // Validate nthOrderBy()
+ const max = pii.$nOrderBy;
+ for(let i=0; i < max; ++i ){
+ let v = pii.nthOrderBy(i,true);
+ T.assert(wasm.isPtr(v));
+ v = pii.nthOrderBy(i);
+ T.assert(v instanceof sii.sqlite3_index_orderby)
+ .assert(v.pointer >= pii.$aOrderBy);
+ v.dispose();
+ }
+ }
+ pii.dispose();
+ return 0;
+ }catch(e){
+ return VT.xError('xBestIndex',e);
+ }
+ }
+ }
+ });
+ this.db.onclose.disposeAfter.push(tmplMod);
+ T.assert(0===tmplMod.$xUpdate)
+ .assert(tmplMod.$xCreate)
+ .assert(tmplMod.$xCreate === tmplMod.$xConnect,
+ "setup() must make these equivalent and "+
+ "installMethods() must avoid re-compiling identical functions");
+ tmplMod.$xCreate = 0 /* make tmplMod eponymous-only */;
+ let rc = capi.sqlite3_create_module(
+ this.db, "testvtab", tmplMod, 0
+ );
+ this.db.checkRc(rc);
+ const list = this.db.selectArrays(
+ "SELECT a,b FROM testvtab where a<9999 and b>1 order by a, b"
+ /* Query is shaped so that it will ensure that some constraints
+ end up in xBestIndex(). */
+ );
+ T.assert(10===list.length)
+ .assert(1000===list[0][0])
+ .assert(2009===list[list.length-1][1]);
+ }
+ })/*custom vtab #1*/
+
+ ////////////////////////////////////////////////////////////////////////
+ .t({
+ name: 'virtual table #2: non-eponymous w/ automated exception wrapping',
+ predicate: ()=>!!capi.sqlite3_index_info,
+ test: function(sqlite3){
+ const VT = sqlite3.vtab;
+ const tmplCols = Object.assign(Object.create(null),{
+ A: 0, B: 1
+ });
+ /**
+ The vtab demonstrated here is a JS-ification of
+ ext/misc/templatevtab.c.
+ */
+ let throwOnCreate = 1 ? 0 : capi.SQLITE_CANTOPEN
+ /* ^^^ just for testing exception wrapping. Note that sqlite
+ always translates errors from a vtable to a generic
+ SQLITE_ERROR unless it's from xConnect()/xCreate() and that
+ callback sets an error string. */;
+ const vtabTrace = 1
+ ? ()=>{}
+ : (methodName,...args)=>console.debug('sqlite3_module::'+methodName+'():',...args);
+ const modConfig = {
+ /* catchExceptions changes how the methods are wrapped */
+ catchExceptions: true,
+ name: "vtab2test",
+ methods:{
+ xCreate: function(pDb, pAux, argc, argv, ppVtab, pzErr){
+ vtabTrace("xCreate",...arguments);
+ if(throwOnCreate){
+ sqlite3.SQLite3Error.toss(
+ throwOnCreate,
+ "Throwing a test exception."
+ );
+ }
+ const args = wasm.cArgvToJs(argc, argv);
+ vtabTrace("xCreate","argv:",args);
+ T.assert(args.length>=3);
+ const rc = capi.sqlite3_declare_vtab(
+ pDb, "CREATE TABLE ignored(a,b)"
+ );
+ if(0===rc){
+ const t = VT.xVtab.create(ppVtab);
+ T.assert(t === VT.xVtab.get(wasm.peekPtr(ppVtab)));
+ vtabTrace("xCreate",...arguments," ppVtab =",t.pointer);
+ }
+ return rc;
+ },
+ xConnect: true,
+ xDestroy: function(pVtab){
+ vtabTrace("xDestroy/xDisconnect",pVtab);
+ VT.xVtab.dispose(pVtab);
+ },
+ xDisconnect: true,
+ xOpen: function(pVtab, ppCursor){
+ const t = VT.xVtab.get(pVtab),
+ c = VT.xCursor.create(ppCursor);
+ T.assert(t instanceof capi.sqlite3_vtab)
+ .assert(c instanceof capi.sqlite3_vtab_cursor);
+ vtabTrace("xOpen",...arguments," cursor =",c.pointer);
+ c._rowId = 0;
+ },
+ xClose: function(pCursor){
+ vtabTrace("xClose",...arguments);
+ const c = VT.xCursor.unget(pCursor);
+ T.assert(c instanceof capi.sqlite3_vtab_cursor)
+ .assert(!VT.xCursor.get(pCursor));
+ c.dispose();
+ },
+ xNext: function(pCursor){
+ vtabTrace("xNext",...arguments);
+ const c = VT.xCursor.get(pCursor);
+ ++c._rowId;
+ },
+ xColumn: function(pCursor, pCtx, iCol){
+ vtabTrace("xColumn",...arguments);
+ const c = VT.xCursor.get(pCursor);
+ switch(iCol){
+ case tmplCols.A:
+ capi.sqlite3_result_int(pCtx, 1000 + c._rowId);
+ break;
+ case tmplCols.B:
+ capi.sqlite3_result_int(pCtx, 2000 + c._rowId);
+ break;
+ default: sqlite3.SQLite3Error.toss("Invalid column id",iCol);
+ }
+ },
+ xRowid: function(pCursor, ppRowid64){
+ vtabTrace("xRowid",...arguments);
+ const c = VT.xCursor.get(pCursor);
+ VT.xRowid(ppRowid64, c._rowId);
+ },
+ xEof: function(pCursor){
+ vtabTrace("xEof",...arguments);
+ return VT.xCursor.get(pCursor)._rowId>=10;
+ },
+ xFilter: function(pCursor, idxNum, idxCStr,
+ argc, argv/* [sqlite3_value* ...] */){
+ vtabTrace("xFilter",...arguments);
+ const c = VT.xCursor.get(pCursor);
+ c._rowId = 0;
+ const list = capi.sqlite3_values_to_js(argc, argv);
+ T.assert(argc === list.length);
+ },
+ xBestIndex: function(pVtab, pIdxInfo){
+ vtabTrace("xBestIndex",...arguments);
+ //const t = VT.xVtab.get(pVtab);
+ const pii = VT.xIndexInfo(pIdxInfo);
+ pii.$estimatedRows = 10;
+ pii.$estimatedCost = 10.0;
+ pii.dispose();
+ }
+ }/*methods*/
+ };
+ const tmplMod = VT.setupModule(modConfig);
+ T.assert(1===tmplMod.$iVersion);
+ this.db.onclose.disposeAfter.push(tmplMod);
+ this.db.checkRc(capi.sqlite3_create_module(
+ this.db.pointer, modConfig.name, tmplMod.pointer, 0
+ ));
+ this.db.exec([
+ "create virtual table testvtab2 using ",
+ modConfig.name,
+ "(arg1 blah, arg2 bloop)"
+ ]);
+ if(0){
+ /* If we DROP TABLE then xDestroy() is called. If the
+ vtab is instead destroyed when the db is closed,
+ xDisconnect() is called. */
+ this.db.onclose.disposeBefore.push(function(db){
+ console.debug("Explicitly dropping testvtab2 via disposeBefore handler...");
+ db.exec(
+ /** DROP TABLE is the only way to get xDestroy() to be called. */
+ "DROP TABLE testvtab2"
+ );
+ });
+ }
+ let list = this.db.selectArrays(
+ "SELECT a,b FROM testvtab2 where a<9999 and b>1 order by a, b"
+ /* Query is shaped so that it will ensure that some
+ constraints end up in xBestIndex(). */
+ );
+ T.assert(10===list.length)
+ .assert(1000===list[0][0])
+ .assert(2009===list[list.length-1][1]);
+
+ list = this.db.selectArrays(
+ "SELECT a,b FROM testvtab2 where a<9999 and b>1 order by b, a limit 5"
+ );
+ T.assert(5===list.length)
+ .assert(1000===list[0][0])
+ .assert(2004===list[list.length-1][1]);
+
+ // Call it as a table-valued function...
+ list = this.db.selectArrays([
+ "SELECT a,b FROM ", modConfig.name,
+ " where a<9999 and b>1 order by b, a limit 1"
+ ]);
+ T.assert(1===list.length)
+ .assert(1000===list[0][0])
+ .assert(2000===list[0][1]);
+ }
+ })/*custom vtab #2*/
+ ////////////////////////////////////////////////////////////////////////
+ .t('Custom collation', function(sqlite3){
+ let collationCounter = 0;
+ let myCmp = function(pArg,n1,p1,n2,p2){
+ //int (*)(void*,int,const void*,int,const void*)
+ ++collationCounter;
+ const rc = wasm.exports.sqlite3_strnicmp(p1,p2,(n1<n2?n1:n2));
+ return rc ? rc : (n1 - n2);
+ };
+ let rc = capi.sqlite3_create_collation_v2(this.db, "mycollation", capi.SQLITE_UTF8,
+ 0, myCmp, 0);
+ this.db.checkRc(rc);
+ rc = this.db.selectValue("select 'hi' = 'HI' collate mycollation");
+ T.assert(1===rc).assert(1===collationCounter);
+ rc = this.db.selectValue("select 'hii' = 'HI' collate mycollation");
+ T.assert(0===rc).assert(2===collationCounter);
+ rc = this.db.selectValue("select 'hi' = 'HIi' collate mycollation");
+ T.assert(0===rc).assert(3===collationCounter);
+ rc = capi.sqlite3_create_collation(this.db,"hi",capi.SQLITE_UTF8/*not enough args*/);
+ T.assert(capi.SQLITE_MISUSE === rc);
+ rc = capi.sqlite3_create_collation_v2(this.db,"hi",capi.SQLITE_UTF8+1/*invalid encoding*/,0,0,0);
+ T.assert(capi.SQLITE_FORMAT === rc)
+ .mustThrowMatching(()=>this.db.checkRc(rc),
+ /SQLITE_UTF8 is the only supported encoding./);
+ /*
+ We need to ensure that replacing that collation function does
+ the right thing. We don't have a handle to the underlying WASM
+ pointer from here, so cannot verify (without digging through
+ internal state) that the old one gets uninstalled, but we can
+ verify that a new one properly replaces it. (That said,
+ console.warn() output has shown that the uninstallation does
+ happen.)
+ */
+ collationCounter = 0;
+ myCmp = function(pArg,n1,p1,n2,p2){
+ --collationCounter;
+ return 0;
+ };
+ rc = capi.sqlite3_create_collation_v2(this.db, "MYCOLLATION", capi.SQLITE_UTF8,
+ 0, myCmp, 0);
+ this.db.checkRc(rc);
+ rc = this.db.selectValue("select 'hi' = 'HI' collate mycollation");
+ T.assert(rc>0).assert(-1===collationCounter);
+ rc = this.db.selectValue("select 'a' = 'b' collate mycollation");
+ T.assert(rc>0).assert(-2===collationCounter);
+ rc = capi.sqlite3_create_collation_v2(this.db, "MYCOLLATION", capi.SQLITE_UTF8,
+ 0, null, 0);
+ this.db.checkRc(rc);
+ rc = 0;
+ try {
+ this.db.selectValue("select 'a' = 'b' collate mycollation");
+ }catch(e){
+ /* Why is e.resultCode not automatically an extended result
+ code? The DB() class enables those automatically. */
+ rc = sqlite3.capi.sqlite3_extended_errcode(this.db);
+ }
+ T.assert(capi.SQLITE_ERROR_MISSING_COLLSEQ === rc);
+ })/*custom collation*/
+
+ ////////////////////////////////////////////////////////////////////////
+ .t('Close db', function(){
+ T.assert(this.db).assert(wasm.isPtr(this.db.pointer));
+ //wasm.sqlite3_wasm_db_reset(this.db); // will leak virtual tables!
+ this.db.close();
+ T.assert(!this.db.pointer);
+ })
+ ;/* end of oo1 checks */
+
+ ////////////////////////////////////////////////////////////////////////
+ T.g('kvvfs')
+ .t({
+ name: 'kvvfs is disabled in worker',
+ predicate: ()=>(isWorker() || "test is only valid in a Worker"),
+ test: function(sqlite3){
+ T.assert(
+ !capi.sqlite3_vfs_find('kvvfs'),
+ "Expecting kvvfs to be unregistered."
+ );
+ }
+ })
+ .t({
+ name: 'kvvfs in main thread',
+ predicate: ()=>(isUIThread()
+ || "local/sessionStorage are unavailable in a Worker"),
+ test: function(sqlite3){
+ const filename = this.kvvfsDbFile = 'session';
+ const pVfs = capi.sqlite3_vfs_find('kvvfs');
+ T.assert(pVfs);
+ const JDb = this.JDb = sqlite3.oo1.JsStorageDb;
+ const unlink = this.kvvfsUnlink = ()=>{JDb.clearStorage(filename)};
+ unlink();
+ let db = new JDb(filename);
+ try {
+ db.exec([
+ 'create table kvvfs(a);',
+ 'insert into kvvfs(a) values(1),(2),(3)'
+ ]);
+ T.assert(3 === db.selectValue('select count(*) from kvvfs'));
+ db.close();
+ db = new JDb(filename);
+ db.exec('insert into kvvfs(a) values(4),(5),(6)');
+ T.assert(6 === db.selectValue('select count(*) from kvvfs'));
+ }finally{
+ db.close();
+ }
+ }
+ }/*kvvfs sanity checks*/)
+ ;/* end kvvfs tests */
+
+ ////////////////////////////////////////////////////////////////////////
+ T.g('Hook APIs')
+ .t({
+ name: "sqlite3_commit/rollback/update_hook()",
+ predicate: ()=>wasm.bigIntEnabled || "Update hook requires int64",
+ test: function(sqlite3){
+ let countCommit = 0, countRollback = 0;;
+ const db = new sqlite3.oo1.DB(':memory:',1 ? 'c' : 'ct');
+ let rc = capi.sqlite3_commit_hook(db, (p)=>{
+ ++countCommit;
+ return (1 === p) ? 0 : capi.SQLITE_ERROR;
+ }, 1);
+ T.assert( 0 === rc /*void pointer*/ );
+
+ // Commit hook...
+ T.assert( 0!=capi.sqlite3_get_autocommit(db) );
+ db.exec("BEGIN; SELECT 1; COMMIT");
+ T.assert(0 === countCommit,
+ "No-op transactions (mostly) do not trigger commit hook.");
+ db.exec("BEGIN EXCLUSIVE; SELECT 1; COMMIT");
+ T.assert(1 === countCommit,
+ "But EXCLUSIVE transactions do.");
+ db.transaction((d)=>{
+ T.assert( 0==capi.sqlite3_get_autocommit(db) );
+ d.exec("create table t(a)");
+ });
+ T.assert(2 === countCommit);
+
+ // Rollback hook:
+ rc = capi.sqlite3_rollback_hook(db, (p)=>{
+ ++countRollback;
+ T.assert( 2 === p );
+ }, 2);
+ T.assert( 0 === rc /*void pointer*/ );
+ T.mustThrowMatching(()=>{
+ db.transaction('drop table t',()=>{})
+ }, (e)=>{
+ return (capi.SQLITE_MISUSE === e.resultCode)
+ && ( e.message.indexOf('Invalid argument') > 0 );
+ });
+ T.assert(0 === countRollback, "Transaction was not started.");
+ T.mustThrowMatching(()=>{
+ db.transaction('immediate', ()=>{
+ sqlite3.SQLite3Error.toss(capi.SQLITE_FULL,'testing rollback hook');
+ });
+ }, (e)=>{
+ return capi.SQLITE_FULL === e.resultCode
+ });
+ T.assert(1 === countRollback);
+
+ // Update hook...
+ const countUpdate = Object.create(null);
+ capi.sqlite3_update_hook(db, (p,op,dbName,tbl,rowid)=>{
+ T.assert('main' === dbName.toLowerCase())
+ .assert('t' === tbl.toLowerCase())
+ .assert(3===p)
+ .assert('bigint' === typeof rowid);
+ switch(op){
+ case capi.SQLITE_INSERT:
+ case capi.SQLITE_UPDATE:
+ case capi.SQLITE_DELETE:
+ countUpdate[op] = (countUpdate[op]||0) + 1;
+ break;
+ default: toss("Unexpected hook operator:",op);
+ }
+ }, 3);
+ db.transaction((d)=>{
+ d.exec([
+ "insert into t(a) values(1);",
+ "update t set a=2;",
+ "update t set a=3;",
+ "delete from t where a=3"
+ // update hook is not called for an unqualified DELETE
+ ]);
+ });
+ T.assert(1 === countRollback)
+ .assert(3 === countCommit)
+ .assert(1 === countUpdate[capi.SQLITE_INSERT])
+ .assert(2 === countUpdate[capi.SQLITE_UPDATE])
+ .assert(1 === countUpdate[capi.SQLITE_DELETE]);
+ //wasm.xWrap.FuncPtrAdapter.debugFuncInstall = true;
+ T.assert(1 === capi.sqlite3_commit_hook(db, 0, 0));
+ T.assert(2 === capi.sqlite3_rollback_hook(db, 0, 0));
+ T.assert(3 === capi.sqlite3_update_hook(db, 0, 0));
+ //wasm.xWrap.FuncPtrAdapter.debugFuncInstall = false;
+ db.close();
+ }
+ })/* commit/rollback/update hooks */
+ .t({
+ name: "sqlite3_preupdate_hook()",
+ predicate: ()=>wasm.bigIntEnabled || "Pre-update hook requires int64",
+ test: function(sqlite3){
+ const db = new sqlite3.oo1.DB(':memory:', 1 ? 'c' : 'ct');
+ const countHook = Object.create(null);
+ let rc = capi.sqlite3_preupdate_hook(
+ db, function(p, pDb, op, zDb, zTbl, iKey1, iKey2){
+ T.assert(9 === p)
+ .assert(db.pointer === pDb)
+ .assert(1 === capi.sqlite3_preupdate_count(pDb))
+ .assert( 0 > capi.sqlite3_preupdate_blobwrite(pDb) );
+ countHook[op] = (countHook[op]||0) + 1;
+ switch(op){
+ case capi.SQLITE_INSERT:
+ case capi.SQLITE_UPDATE:
+ T.assert('number' === typeof capi.sqlite3_preupdate_new_js(pDb, 0));
+ break;
+ case capi.SQLITE_DELETE:
+ T.assert('number' === typeof capi.sqlite3_preupdate_old_js(pDb, 0));
+ break;
+ default: toss("Unexpected hook operator:",op);
+ }
+ },
+ 9
+ );
+ db.transaction((d)=>{
+ d.exec([
+ "create table t(a);",
+ "insert into t(a) values(1);",
+ "update t set a=2;",
+ "update t set a=3;",
+ "delete from t where a=3"
+ ]);
+ });
+ T.assert(1 === countHook[capi.SQLITE_INSERT])
+ .assert(2 === countHook[capi.SQLITE_UPDATE])
+ .assert(1 === countHook[capi.SQLITE_DELETE]);
+ //wasm.xWrap.FuncPtrAdapter.debugFuncInstall = true;
+ db.close();
+ //wasm.xWrap.FuncPtrAdapter.debugFuncInstall = false;
+ }
+ })/*pre-update hooks*/
+ ;/*end hook API tests*/
+
+ ////////////////////////////////////////////////////////////////////////
+ T.g('Auto-extension API')
+ .t({
+ name: "Auto-extension sanity checks.",
+ test: function(sqlite3){
+ let counter = 0;
+ const fp = wasm.installFunction('i(ppp)', function(pDb,pzErr,pApi){
+ ++counter;
+ return 0;
+ });
+ (new sqlite3.oo1.DB()).close();
+ T.assert( 0===counter );
+ capi.sqlite3_auto_extension(fp);
+ (new sqlite3.oo1.DB()).close();
+ T.assert( 1===counter );
+ (new sqlite3.oo1.DB()).close();
+ T.assert( 2===counter );
+ capi.sqlite3_cancel_auto_extension(fp);
+ wasm.uninstallFunction(fp);
+ (new sqlite3.oo1.DB()).close();
+ T.assert( 2===counter );
+ }
+ });
+
+ ////////////////////////////////////////////////////////////////////////
+ T.g('Session API')
+ .t({
+ name: 'Session API sanity checks',
+ predicate: ()=>!!capi.sqlite3changegroup_add,
+ test: function(sqlite3){
+ warn("The session API tests could use some expansion.");
+ const db1 = new sqlite3.oo1.DB(), db2 = new sqlite3.oo1.DB();
+ const sqlInit = [
+ "create table t(rowid INTEGER PRIMARY KEY,a,b); ",
+ "insert into t(rowid,a,b) values",
+ "(1,'a1','b1'),",
+ "(2,'a2','b2'),",
+ "(3,'a3','b3');"
+ ].join('');
+ db1.exec(sqlInit);
+ db2.exec(sqlInit);
+ T.assert(3 === db1.selectValue("select count(*) from t"))
+ .assert('b3' === db1.selectValue('select b from t where rowid=3'));
+ const stackPtr = wasm.pstack.pointer;
+ try{
+ let ppOut = wasm.pstack.allocPtr();
+ let rc = capi.sqlite3session_create(db1, "main", ppOut);
+ T.assert(0===rc);
+ let pSession = wasm.peekPtr(ppOut);
+ T.assert(pSession && wasm.isPtr(pSession));
+ capi.sqlite3session_table_filter(pSession, (pCtx, tbl)=>{
+ T.assert('t' === tbl).assert( 99 === pCtx );
+ return 1;
+ }, 99);
+ db1.exec([
+ "update t set b='bTwo' where rowid=2;",
+ "update t set a='aThree' where rowid=3;",
+ "delete from t where rowid=1;",
+ "insert into t(rowid,a,b) values(4,'a4','b4')"
+ ]);
+ T.assert('bTwo' === db1.selectValue("select b from t where rowid=2"))
+ .assert(undefined === db1.selectValue('select a from t where rowid=1'))
+ .assert('b4' === db1.selectValue('select b from t where rowid=4'))
+ .assert(3 === db1.selectValue('select count(*) from t'));
+
+ const testSessionEnable = false;
+ if(testSessionEnable){
+ rc = capi.sqlite3session_enable(pSession, 0);
+ T.assert( 0 === rc )
+ .assert( 0 === capi.sqlite3session_enable(pSession, -1) );
+ db1.exec("delete from t where rowid=2;");
+ rc = capi.sqlite3session_enable(pSession, 1);
+ T.assert( rc > 0 )
+ .assert( capi.sqlite3session_enable(pSession, -1) > 0 )
+ .assert(undefined === db1.selectValue('select a from t where rowid=2'));
+ }else{
+ warn("sqlite3session_enable() tests are currently disabled.");
+ }
+ let db1Count = db1.selectValue("select count(*) from t");
+ T.assert( db1Count === (testSessionEnable ? 2 : 3) );
+
+ /* Capture changeset and destroy session. */
+ let pnChanges = wasm.pstack.alloc('i32'),
+ ppChanges = wasm.pstack.allocPtr();
+ rc = capi.sqlite3session_changeset(pSession, pnChanges, ppChanges);
+ T.assert( 0 === rc );
+ capi.sqlite3session_delete(pSession);
+ pSession = 0;
+ const pChanges = wasm.peekPtr(ppChanges),
+ nChanges = wasm.peek32(pnChanges);
+ T.assert( pChanges && wasm.isPtr( pChanges ) )
+ .assert( nChanges > 0 );
+
+ /* Revert db1 via an inverted changeset, but keep pChanges
+ and nChanges for application to db2. */
+ rc = capi.sqlite3changeset_invert( nChanges, pChanges, pnChanges, ppChanges );
+ T.assert( 0 === rc );
+ rc = capi.sqlite3changeset_apply(
+ db1, wasm.peek32(pnChanges), wasm.peekPtr(ppChanges), 0, (pCtx, eConflict, pIter)=>{
+ return 1;
+ }, 0
+ );
+ T.assert( 0 === rc );
+ wasm.dealloc( wasm.peekPtr(ppChanges) );
+ pnChanges = ppChanges = 0;
+ T.assert('b2' === db1.selectValue("select b from t where rowid=2"))
+ .assert('a1' === db1.selectValue('select a from t where rowid=1'))
+ .assert(undefined === db1.selectValue('select b from t where rowid=4'));
+ db1Count = db1.selectValue("select count(*) from t");
+ T.assert(3 === db1Count);
+
+ /* Apply pre-reverted changeset (pChanges, nChanges) to
+ db2... */
+ rc = capi.sqlite3changeset_apply(
+ db2, nChanges, pChanges, 0, (pCtx, eConflict, pIter)=>{
+ return pCtx ? 1 : 0
+ }, 1
+ );
+ wasm.dealloc( pChanges );
+ T.assert( 0 === rc )
+ .assert( 'b4' === db2.selectValue('select b from t where rowid=4') )
+ .assert( 'aThree' === db2.selectValue('select a from t where rowid=3') )
+ .assert( undefined === db2.selectValue('select b from t where rowid=1') );
+ if(testSessionEnable){
+ T.assert( (undefined === db2.selectValue('select b from t where rowid=2')),
+ "But... the session was disabled when rowid=2 was deleted?" );
+ log("rowids from db2.t:",db2.selectValues('select rowid from t order by rowid'));
+ T.assert( 3 === db2.selectValue('select count(*) from t') );
+ }else{
+ T.assert( 'bTwo' === db2.selectValue('select b from t where rowid=2') )
+ .assert( 3 === db2.selectValue('select count(*) from t') );
+ }
+ }finally{
+ wasm.pstack.restore(stackPtr);
+ db1.close();
+ db2.close();
+ }
+ }
+ })/*session API sanity tests*/
+ ;/*end of session API group*/;
+
+ ////////////////////////////////////////////////////////////////////////
+ T.g('OPFS: Origin-Private File System',
+ (sqlite3)=>(sqlite3.capi.sqlite3_vfs_find("opfs")
+ || 'requires "opfs" VFS'))
+ .t({
+ name: 'OPFS db sanity checks',
+ test: async function(sqlite3){
+ const filename = this.opfsDbFile = '/dir/sqlite3-tester1.db';
+ const pVfs = this.opfsVfs = capi.sqlite3_vfs_find('opfs');
+ T.assert(pVfs);
+ const unlink = this.opfsUnlink =
+ (fn=filename)=>{wasm.sqlite3_wasm_vfs_unlink(pVfs,fn)};
+ unlink();
+ let db = new sqlite3.oo1.OpfsDb(filename);
+ try {
+ db.exec([
+ 'create table p(a);',
+ 'insert into p(a) values(1),(2),(3)'
+ ]);
+ T.assert(3 === db.selectValue('select count(*) from p'));
+ db.close();
+ db = new sqlite3.oo1.OpfsDb(filename);
+ db.exec('insert into p(a) values(4),(5),(6)');
+ T.assert(6 === db.selectValue('select count(*) from p'));
+ this.opfsDbExport = capi.sqlite3_js_db_export(db);
+ T.assert(this.opfsDbExport instanceof Uint8Array)
+ .assert(this.opfsDbExport.byteLength>0
+ && 0===this.opfsDbExport.byteLength % 512);
+ }finally{
+ db.close();
+ unlink();
+ }
+ }
+ }/*OPFS db sanity checks*/)
+ .t({
+ name: 'OPFS import',
+ test: async function(sqlite3){
+ let db;
+ try {
+ const exp = this.opfsDbExport;
+ const filename = this.opfsDbFile;
+ delete this.opfsDbExport;
+ this.opfsImportSize = await sqlite3.oo1.OpfsDb.importDb(filename, exp);
+ db = new sqlite3.oo1.OpfsDb(this.opfsDbFile);
+ T.assert(6 === db.selectValue('select count(*) from p')).
+ assert( this.opfsImportSize == exp.byteLength );
+ db.close();
+ this.opfsUnlink(filename);
+ T.assert(!(await sqlite3.opfs.entryExists(filename)));
+ // Try again with a function as an input source:
+ let cursor = 0;
+ const blockSize = 512, end = exp.byteLength;
+ const reader = async function(){
+ if(cursor >= exp.byteLength){
+ return undefined;
+ }
+ const rv = exp.subarray(cursor, cursor+blockSize>end ? end : cursor+blockSize);
+ cursor += blockSize;
+ return rv;
+ };
+ this.opfsImportSize = await sqlite3.oo1.OpfsDb.importDb(filename, reader);
+ db = new sqlite3.oo1.OpfsDb(this.opfsDbFile);
+ T.assert(6 === db.selectValue('select count(*) from p')).
+ assert( this.opfsImportSize == exp.byteLength );
+ }finally{
+ if(db) db.close();
+ }
+ }
+ }/*OPFS export/import*/)
+ .t({
+ name: '(Internal-use) OPFS utility APIs',
+ test: async function(sqlite3){
+ const filename = this.opfsDbFile;
+ const pVfs = this.opfsVfs;
+ const unlink = this.opfsUnlink;
+ T.assert(filename && pVfs && !!unlink);
+ delete this.opfsDbFile;
+ delete this.opfsVfs;
+ delete this.opfsUnlink;
+ /**************************************************************
+ ATTENTION CLIENT-SIDE USERS: sqlite3.opfs is NOT intended
+ for client-side use. It is only for this project's own
+ internal use. Its APIs are subject to change or removal at
+ any time.
+ ***************************************************************/
+ const opfs = sqlite3.opfs;
+ const fSize = this.opfsImportSize;
+ delete this.opfsImportSize;
+ let sh;
+ try{
+ T.assert(await opfs.entryExists(filename));
+ const [dirHandle, filenamePart] = await opfs.getDirForFilename(filename, false);
+ const fh = await dirHandle.getFileHandle(filenamePart);
+ sh = await fh.createSyncAccessHandle();
+ T.assert(fSize === await sh.getSize());
+ await sh.close();
+ sh = undefined;
+ unlink();
+ T.assert(!(await opfs.entryExists(filename)));
+ }finally{
+ if(sh) await sh.close();
+ unlink();
+ }
+
+ // Some sanity checks of the opfs utility functions...
+ const testDir = '/sqlite3-opfs-'+opfs.randomFilename(12);
+ const aDir = testDir+'/test/dir';
+ T.assert(await opfs.mkdir(aDir), "mkdir failed")
+ .assert(await opfs.mkdir(aDir), "mkdir must pass if the dir exists")
+ .assert(!(await opfs.unlink(testDir+'/test')), "delete 1 should have failed (dir not empty)")
+ .assert((await opfs.unlink(testDir+'/test/dir')), "delete 2 failed")
+ .assert(!(await opfs.unlink(testDir+'/test/dir')),
+ "delete 2b should have failed (dir already deleted)")
+ .assert((await opfs.unlink(testDir, true)), "delete 3 failed")
+ .assert(!(await opfs.entryExists(testDir)),
+ "entryExists(",testDir,") should have failed");
+ }
+ }/*OPFS util sanity checks*/)
+ ;/* end OPFS tests */
+
+ ////////////////////////////////////////////////////////////////////////
+ T.g('OPFS SyncAccessHandle Pool VFS',
+ (sqlite3)=>(hasOpfs() || "requires OPFS APIs"))
+ .t({
+ name: 'SAH sanity checks',
+ test: async function(sqlite3){
+ T.assert(!sqlite3.capi.sqlite3_vfs_find(sahPoolConfig.name))
+ .assert(sqlite3.capi.sqlite3_js_vfs_list().indexOf(sahPoolConfig.name) < 0)
+ const inst = sqlite3.installOpfsSAHPoolVfs,
+ catcher = (e)=>{
+ error("Cannot load SAH pool VFS.",
+ "This might not be a problem,",
+ "depending on the environment.");
+ return false;
+ };
+ let u1, u2;
+ // Ensure that two immediately-consecutive installations
+ // resolve to the same Promise instead of triggering
+ // a locking error.
+ const P1 = inst(sahPoolConfig).then(u=>u1 = u).catch(catcher),
+ P2 = inst(sahPoolConfig).then(u=>u2 = u).catch(catcher);
+ await Promise.all([P1, P2]);
+ if(!(await P1)) return;
+ T.assert(u1 === u2)
+ .assert(sahPoolConfig.name === u1.vfsName)
+ .assert(sqlite3.capi.sqlite3_vfs_find(sahPoolConfig.name))
+ .assert(u1.getCapacity() >= sahPoolConfig.initialCapacity
+ /* If a test fails before we get to nuke the VFS, we
+ can have more than the initial capacity on the next
+ run. */)
+ .assert(u1.getCapacity() + 2 === (await u2.addCapacity(2)))
+ .assert(2 === (await u2.reduceCapacity(2)))
+ .assert(sqlite3.capi.sqlite3_js_vfs_list().indexOf(sahPoolConfig.name) >= 0);
+
+ T.assert(0 === u1.getFileCount());
+ const dbName = '/foo.db';
+ let db = new u1.OpfsSAHPoolDb(dbName);
+ T.assert(db instanceof sqlite3.oo1.DB)
+ .assert(1 === u1.getFileCount());
+ db.exec([
+ 'create table t(a);',
+ 'insert into t(a) values(1),(2),(3)'
+ ]);
+ T.assert(1 === u1.getFileCount());
+ T.assert(3 === db.selectValue('select count(*) from t'));
+ db.close();
+ T.assert(1 === u1.getFileCount());
+ db = new u2.OpfsSAHPoolDb(dbName);
+ T.assert(1 === u1.getFileCount());
+ db.close();
+ const fileNames = u1.getFileNames();
+ T.assert(1 === fileNames.length)
+ .assert(dbName === fileNames[0])
+ .assert(1 === u1.getFileCount())
+
+ if(1){ // test exportFile() and importDb()
+ const dbytes = u1.exportFile(dbName);
+ T.assert(dbytes.length >= 4096);
+ const dbName2 = '/exported.db';
+ let nWrote = u1.importDb(dbName2, dbytes);
+ T.assert( 2 == u1.getFileCount() )
+ .assert( dbytes.byteLength == nWrote );
+ let db2 = new u1.OpfsSAHPoolDb(dbName2);
+ T.assert(db2 instanceof sqlite3.oo1.DB)
+ .assert(3 === db2.selectValue('select count(*) from t'));
+ db2.close();
+ T.assert(true === u1.unlink(dbName2))
+ .assert(false === u1.unlink(dbName2))
+ .assert(1 === u1.getFileCount())
+ .assert(1 === u1.getFileNames().length);
+ // Try again with a function as an input source:
+ let cursor = 0;
+ const blockSize = 1024, end = dbytes.byteLength;
+ const reader = async function(){
+ if(cursor >= dbytes.byteLength){
+ return undefined;
+ }
+ const rv = dbytes.subarray(cursor, cursor+blockSize>end ? end : cursor+blockSize);
+ cursor += blockSize;
+ return rv;
+ };
+ nWrote = await u1.importDb(dbName2, reader);
+ T.assert( 2 == u1.getFileCount() );
+ db2 = new u1.OpfsSAHPoolDb(dbName2);
+ T.assert(db2 instanceof sqlite3.oo1.DB)
+ .assert(3 === db2.selectValue('select count(*) from t'));
+ db2.close();
+ T.assert(true === u1.unlink(dbName2))
+ .assert(dbytes.byteLength == nWrote);
+ }
+
+ T.assert(true === u1.unlink(dbName))
+ .assert(false === u1.unlink(dbName))
+ .assert(0 === u1.getFileCount())
+ .assert(0 === u1.getFileNames().length);
+
+ // Demonstrate that two SAH pools can coexist so long as
+ // they have different names.
+ const conf2 = JSON.parse(JSON.stringify(sahPoolConfig));
+ conf2.name += '-test2';
+ const POther = await inst(conf2);
+ //log("Installed second SAH instance as",conf2.name);
+ T.assert(0 === POther.getFileCount())
+ .assert(true === await POther.removeVfs());
+
+ if(0){
+ /* Enable this block to inspect vfs's contents via the dev
+ console or OPFS Explorer browser extension. The
+ following bits will remove them. */
+ return;
+ }
+ T.assert(true === await u2.removeVfs())
+ .assert(false === await u1.removeVfs())
+ .assert(!sqlite3.capi.sqlite3_vfs_find(sahPoolConfig.name));
+
+ let cErr, u3;
+ conf2.$testThrowInInit = new Error("Testing throwing during init.");
+ conf2.name = sahPoolConfig.name+'-err';
+ const P3 = await inst(conf2).then(u=>u3 = u).catch((e)=>cErr=e);
+ T.assert(P3 === conf2.$testThrowInInit)
+ .assert(cErr === P3)
+ .assert(undefined === u3)
+ .assert(!sqlite3.capi.sqlite3_vfs_find(conf2.name));
+ }
+ }/*OPFS SAH Pool sanity checks*/)
+
+ ////////////////////////////////////////////////////////////////////////
+ T.g('Bug Reports')
+ .t({
+ name: 'Delete via bound parameter in subquery',
+ test: function(sqlite3){
+ // Testing https://sqlite.org/forum/forumpost/40ce55bdf5
+ // with the exception that that post uses "external content"
+ // for the FTS index.
+ const db = new sqlite3.oo1.DB();//(':memory:','wt');
+ db.exec([
+ "create virtual table f using fts5 (path);",
+ "insert into f(path) values('abc'),('def'),('ghi');"
+ ]);
+ const fetchEm = ()=> db.exec({
+ sql: "SELECT * FROM f order by path",
+ rowMode: 'array'
+ });
+ const dump = function(lbl){
+ let rc = fetchEm();
+ log((lbl ? (lbl+' results') : ''),rc);
+ };
+ //dump('Full fts table');
+ let rc = fetchEm();
+ T.assert(3===rc.length);
+ db.exec(`
+ delete from f where rowid in (
+ select rowid from f where path = :path
+ )`,
+ {bind: {":path": "def"}}
+ );
+ //dump('After deleting one entry via subquery');
+ rc = fetchEm();
+ T.assert(2===rc.length)
+ .assert('abcghi'===rc.join(''));
+ //log('rc =',rc);
+ db.close();
+ }
+ })
+ ;/*end of Bug Reports group*/;
+
+ ////////////////////////////////////////////////////////////////////////
+ log("Loading and initializing sqlite3 WASM module...");
+ if(0){
+ globalThis.sqlite3ApiConfig = {
+ debug: ()=>{},
+ log: ()=>{},
+ warn: ()=>{},
+ error: ()=>{}
+ }
+ }
+//#ifnot target=es6-module
+ if(!globalThis.sqlite3InitModule && !isUIThread()){
+ /* Vanilla worker, as opposed to an ES6 module worker */
+ /*
+ If sqlite3.js is in a directory other than this script, in order
+ to get sqlite3.js to resolve sqlite3.wasm properly, we have to
+ explicitly tell it where sqlite3.js is being loaded from. We do
+ that by passing the `sqlite3.dir=theDirName` URL argument to
+ _this_ script. That URL argument will be seen by the JS/WASM
+ loader and it will adjust the sqlite3.wasm path accordingly. If
+ sqlite3.js/.wasm are in the same directory as this script then
+ that's not needed.
+
+ URL arguments passed as part of the filename via importScripts()
+ are simply lost, and such scripts see the globalThis.location of
+ _this_ script.
+ */
+ let sqlite3Js = 'sqlite3.js';
+ const urlParams = new URL(globalThis.location.href).searchParams;
+ if(urlParams.has('sqlite3.dir')){
+ sqlite3Js = urlParams.get('sqlite3.dir') + '/' + sqlite3Js;
+ }
+ importScripts(sqlite3Js);
+ }
+//#endif
+ globalThis.sqlite3InitModule.__isUnderTest =
+ true /* disables certain API-internal cleanup so that we can
+ test internal APIs from here */;
+ globalThis.sqlite3InitModule({
+ print: log,
+ printErr: error
+ }).then(async function(sqlite3){
+ log("Done initializing WASM/JS bits. Running tests...");
+ sqlite3.config.warn("Installing sqlite3 bits as global S for local dev/test purposes.");
+ globalThis.S = sqlite3;
+ /*await sqlite3.installOpfsSAHPoolVfs(sahPoolConfig)
+ .then((u)=>log("Loaded",u.vfsName,"VFS"))
+ .catch(e=>{
+ log("Cannot install OpfsSAHPool.",e);
+ });*/
+ capi = sqlite3.capi;
+ wasm = sqlite3.wasm;
+ log("sqlite3 version:",capi.sqlite3_libversion(),
+ capi.sqlite3_sourceid());
+ if(wasm.bigIntEnabled){
+ log("BigInt/int64 support is enabled.");
+ }else{
+ logClass('warning',"BigInt/int64 support is disabled.");
+ }
+ if(haveWasmCTests()){
+ log("sqlite3_wasm_test_...() APIs are available.");
+ }else{
+ logClass('warning',"sqlite3_wasm_test_...() APIs unavailable.");
+ }
+ log("registered vfs list =",capi.sqlite3_js_vfs_list().join(', '));
+ TestUtil.runTests(sqlite3);
+ });
+})(self);
diff --git a/ext/wasm/tests/opfs/concurrency/index.html b/ext/wasm/tests/opfs/concurrency/index.html
new file mode 100644
index 0000000..595ab24
--- /dev/null
+++ b/ext/wasm/tests/opfs/concurrency/index.html
@@ -0,0 +1,49 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="../../../common/testing.css"/>
+ <title>sqlite3 OPFS Worker concurrency tester</title>
+ <style>
+ body { display: revert; }
+ body > * {}
+ #test-output {
+ font-family: monospace;
+ }
+ </style>
+ </head>
+ <body>
+ <h1></h1>
+ <p>
+ OPFS concurrency tester using multiple independent Workers.
+ Disclaimer: concurrency in OPFS is currently a pain point!
+ </p>
+ <p>
+ URL flags: pass a number of workers using
+ the <code>workers=N</code> URL flag. Set the time between each
+ workload with <code>interval=N</code> (milliseconds). Set the
+ number of worker iterations with <code>iterations=N</code>.
+ Enable OPFS VFS verbosity with <code>verbose=1-3</code> (output
+ goes to the dev console). Disable/enable "unlock ASAP" mode
+ (higher concurrency, lower speed) with <code>unlock-asap=0-1</code>.
+ </p>
+ <p>Achtung: if it does not start to do anything within a couple of
+ seconds, check the dev console: Chrome sometimes fails to load
+ the wasm module due to "cannot allocate WasmMemory." Closing and
+ re-opening the tab usually resolves it, but sometimes restarting
+ the browser is required.
+ </p>
+ <div class='input-wrapper'>
+ <input type='checkbox' id='cb-log-reverse'>
+ <label for='cb-log-reverse'>Reverse log order?</label>
+ </div>
+ <div id='test-output'></div>
+ <script>(function(){
+ document.querySelector('h1').innerHTML =
+ document.querySelector('title').innerHTML;
+ })();</script>
+ <script src="test.js?sqlite3.dir=../../../jswasm"></script>
+ </body>
+</html>
diff --git a/ext/wasm/tests/opfs/concurrency/test.js b/ext/wasm/tests/opfs/concurrency/test.js
new file mode 100644
index 0000000..14cd6f5
--- /dev/null
+++ b/ext/wasm/tests/opfs/concurrency/test.js
@@ -0,0 +1,136 @@
+(async function(self){
+
+ const logCss = (function(){
+ const mapToString = (v)=>{
+ switch(typeof v){
+ case 'number': case 'string': case 'boolean':
+ case 'undefined': case 'bigint':
+ return ''+v;
+ default: break;
+ }
+ if(null===v) return 'null';
+ if(v instanceof Error){
+ v = {
+ message: v.message,
+ stack: v.stack,
+ errorClass: v.name
+ };
+ }
+ return JSON.stringify(v,undefined,2);
+ };
+ const normalizeArgs = (args)=>args.map(mapToString);
+ const logTarget = document.querySelector('#test-output');
+ const logCss = function(cssClass,...args){
+ const ln = document.createElement('div');
+ if(cssClass){
+ for(const c of (Array.isArray(cssClass) ? cssClass : [cssClass])){
+ ln.classList.add(c);
+ }
+ }
+ ln.append(document.createTextNode(normalizeArgs(args).join(' ')));
+ logTarget.append(ln);
+ };
+ const cbReverse = document.querySelector('#cb-log-reverse');
+ const cbReverseKey = 'tester1:cb-log-reverse';
+ const cbReverseIt = ()=>{
+ logTarget.classList[cbReverse.checked ? 'add' : 'remove']('reverse');
+ localStorage.setItem(cbReverseKey, cbReverse.checked ? 1 : 0);
+ };
+ cbReverse.addEventListener('change', cbReverseIt, true);
+ if(localStorage.getItem(cbReverseKey)){
+ cbReverse.checked = !!(+localStorage.getItem(cbReverseKey));
+ }
+ cbReverseIt();
+ return logCss;
+ })();
+ const stdout = (...args)=>logCss('',...args);
+ const stderr = (...args)=>logCss('error',...args);
+
+ const wait = async (ms)=>{
+ return new Promise((resolve)=>setTimeout(resolve,ms));
+ };
+
+ const urlArgsJs = new URL(document.currentScript.src).searchParams;
+ const urlArgsHtml = new URL(self.location.href).searchParams;
+ const options = Object.create(null);
+ options.sqlite3Dir = urlArgsJs.get('sqlite3.dir');
+ options.workerCount = (
+ urlArgsHtml.has('workers') ? +urlArgsHtml.get('workers') : 3
+ ) || 4;
+ options.opfsVerbose = (
+ urlArgsHtml.has('verbose') ? +urlArgsHtml.get('verbose') : 1
+ ) || 1;
+ options.interval = (
+ urlArgsHtml.has('interval') ? +urlArgsHtml.get('interval') : 1000
+ ) || 1000;
+ options.iterations = (
+ urlArgsHtml.has('iterations') ? +urlArgsHtml.get('iterations') : 10
+ ) || 10;
+ options.unlockAsap = (
+ urlArgsHtml.has('unlock-asap') ? +urlArgsHtml.get('unlock-asap') : 0
+ ) || 0;
+ options.noUnlink = !!urlArgsHtml.has('no-unlink');
+ const workers = [];
+ workers.post = (type,...args)=>{
+ for(const w of workers) w.postMessage({type, payload:args});
+ };
+ workers.counts = {loaded: 0, passed: 0, failed: 0};
+ const checkFinished = function(){
+ if(workers.counts.passed + workers.counts.failed !== workers.length){
+ return;
+ }
+ if(workers.counts.failed>0){
+ logCss('tests-fail',"Finished with",workers.counts.failed,"failure(s).");
+ }else{
+ logCss('tests-pass',"All",workers.length,"workers finished.");
+ }
+ };
+ workers.onmessage = function(msg){
+ msg = msg.data;
+ const prefix = 'Worker #'+msg.worker+':';
+ switch(msg.type){
+ case 'loaded':
+ stdout(prefix,"loaded");
+ if(++workers.counts.loaded === workers.length){
+ stdout("All",workers.length,"workers loaded. Telling them to run...");
+ workers.post('run');
+ }
+ break;
+ case 'stdout': stdout(prefix,...msg.payload); break;
+ case 'stderr': stderr(prefix,...msg.payload); break;
+ case 'error': stderr(prefix,"ERROR:",...msg.payload); break;
+ case 'finished':
+ ++workers.counts.passed;
+ logCss('tests-pass',prefix,...msg.payload);
+ checkFinished();
+ break;
+ case 'failed':
+ ++workers.counts.failed;
+ logCss('tests-fail',prefix,"FAILED:",...msg.payload);
+ checkFinished();
+ break;
+ default: logCss('error',"Unhandled message type:",msg); break;
+ }
+ };
+
+ stdout("Launching",options.workerCount,"workers. Options:",options);
+ workers.uri = (
+ 'worker.js?'
+ + 'sqlite3.dir='+options.sqlite3Dir
+ + '&interval='+options.interval
+ + '&iterations='+options.iterations
+ + '&opfs-verbose='+options.opfsVerbose
+ + '&opfs-unlock-asap='+options.unlockAsap
+ );
+ for(let i = 0; i < options.workerCount; ++i){
+ stdout("Launching worker...");
+ workers.push(new Worker(
+ workers.uri+'&workerId='+(i+1)+(
+ (i || options.noUnlink) ? '' : '&unlink-db'
+ )
+ ));
+ }
+ // Have to delay onmessage assignment until after the loop
+ // to avoid that early workers get an undue head start.
+ workers.forEach((w)=>w.onmessage = workers.onmessage);
+})(self);
diff --git a/ext/wasm/tests/opfs/concurrency/worker.js b/ext/wasm/tests/opfs/concurrency/worker.js
new file mode 100644
index 0000000..5d28bed
--- /dev/null
+++ b/ext/wasm/tests/opfs/concurrency/worker.js
@@ -0,0 +1,113 @@
+importScripts(
+ (new URL(self.location.href).searchParams).get('sqlite3.dir') + '/sqlite3.js'
+);
+self.sqlite3InitModule().then(async function(sqlite3){
+ const urlArgs = new URL(self.location.href).searchParams;
+ const options = {
+ workerName: urlArgs.get('workerId') || Math.round(Math.random()*10000),
+ unlockAsap: urlArgs.get('opfs-unlock-asap') || 0 /*EXPERIMENTAL*/
+ };
+ const wPost = (type,...payload)=>{
+ postMessage({type, worker: options.workerName, payload});
+ };
+ const stdout = (...args)=>wPost('stdout',...args);
+ const stderr = (...args)=>wPost('stderr',...args);
+ if(!sqlite3.opfs){
+ stderr("OPFS support not detected. Aborting.");
+ return;
+ }
+
+ const wait = async (ms)=>{
+ return new Promise((resolve)=>setTimeout(resolve,ms));
+ };
+
+ const dbName = 'concurrency-tester.db';
+ if(urlArgs.has('unlink-db')){
+ await sqlite3.opfs.unlink(dbName);
+ stdout("Unlinked",dbName);
+ }
+ wPost('loaded');
+ let db;
+ const interval = Object.assign(Object.create(null),{
+ delay: urlArgs.has('interval') ? (+urlArgs.get('interval') || 750) : 750,
+ handle: undefined,
+ count: 0
+ });
+ const finish = ()=>{
+ if(db){
+ if(!db.pointer) return;
+ db.close();
+ }
+ if(interval.error){
+ wPost('failed',"Ending work after interval #"+interval.count,
+ "due to error:",interval.error);
+ }else{
+ wPost('finished',"Ending work after",interval.count,"intervals.");
+ }
+ };
+ const run = async function(){
+ db = new sqlite3.oo1.OpfsDb({
+ filename: 'file:'+dbName+'?opfs-unlock-asap='+options.unlockAsap,
+ flags: 'c'
+ });
+ sqlite3.capi.sqlite3_busy_timeout(db.pointer, 5000);
+ db.transaction((db)=>{
+ db.exec([
+ "create table if not exists t1(w TEXT UNIQUE ON CONFLICT REPLACE,v);",
+ "create table if not exists t2(w TEXT UNIQUE ON CONFLICT REPLACE,v);"
+ ]);
+ });
+
+ const maxIterations =
+ urlArgs.has('iterations') ? (+urlArgs.get('iterations') || 10) : 10;
+ stdout("Starting interval-based db updates with delay of",interval.delay,"ms.");
+ const doWork = async ()=>{
+ const tm = new Date().getTime();
+ ++interval.count;
+ const prefix = "v(#"+interval.count+")";
+ stdout("Setting",prefix,"=",tm);
+ try{
+ db.exec({
+ sql:"INSERT OR REPLACE INTO t1(w,v) VALUES(?,?)",
+ bind: [options.workerName, new Date().getTime()]
+ });
+ //stdout("Set",prefix);
+ }catch(e){
+ interval.error = e;
+ }
+ };
+ if(1){/*use setInterval()*/
+ setTimeout(async function timer(){
+ await doWork();
+ if(interval.error || maxIterations === interval.count){
+ finish();
+ }else{
+ setTimeout(timer, interval.delay);
+ }
+ }, interval.delay);
+ }else{
+ /*This approach provides no concurrency whatsoever: each worker
+ is run to completion before any others can work.*/
+ let i;
+ for(i = 0; i < maxIterations; ++i){
+ await doWork();
+ if(interval.error) break;
+ await wait(interval.ms);
+ }
+ finish();
+ }
+ }/*run()*/;
+
+ self.onmessage = function({data}){
+ switch(data.type){
+ case 'run': run().catch((e)=>{
+ if(!interval.error) interval.error = e;
+ finish();
+ });
+ break;
+ default:
+ stderr("Unhandled message type '"+data.type+"'.");
+ break;
+ }
+ };
+});
diff --git a/ext/wasm/wasmfs.make b/ext/wasm/wasmfs.make
new file mode 100644
index 0000000..71c0fe7
--- /dev/null
+++ b/ext/wasm/wasmfs.make
@@ -0,0 +1,115 @@
+#!/usr/bin/make
+#^^^^ help emacs select makefile mode
+#
+# This is a sub-make for building a standalone wasmfs-based
+# sqlite3.wasm. It is intended to be "include"d from the main
+# GNUMakefile.
+########################################################################
+MAKEFILE.wasmfs := $(lastword $(MAKEFILE_LIST))
+$(warning The WASMFS build is currently incomplete.)
+
+#dir.wasmfs := $(dir.wasm)
+dir.wasmfs := $(dir.dout)
+sqlite3-wasmfs.js := $(dir.wasmfs)/sqlite3-wasmfs.js
+sqlite3-wasmfs.mjs := $(dir.wasmfs)/sqlite3-wasmfs.mjs
+sqlite3-wasmfs.wasm := $(dir.wasmfs)/sqlite3-wasmfs.wasm
+
+CLEAN_FILES += $(sqlite3-wasmfs.js) $(sqlite3-wasmfs.wasm) \
+ $(subst .js,.worker.js,$(sqlite3-wasmfs.js)) \
+ $(sqlite3-wasmfs.mjs) \
+ $(subst .mjs,.worker.mjs,$(sqlite3-wasmfs.mjs))
+
+########################################################################
+# emcc flags for .c/.o.
+cflags.sqlite3-wasmfs :=
+cflags.sqlite3-wasmfs += -std=c99 -fPIC
+cflags.sqlite3-wasmfs += -pthread
+cflags.sqlite3-wasmfs += -DSQLITE_ENABLE_WASMFS
+
+########################################################################
+# emcc flags specific to building the final .js/.wasm file...
+emcc.flags.sqlite3-wasmfs :=
+emcc.flags.sqlite3-wasmfs += \
+ -sEXPORTED_RUNTIME_METHODS=wasmMemory,allocateUTF8OnStack,stringToUTF8OnStack
+ # wasmMemory ==> for -sIMPORTED_MEMORY
+ # *OnStack ==> wasmfs internals (leaky abstraction)
+emcc.flags.sqlite3-wasmfs += -sUSE_CLOSURE_COMPILER=0
+emcc.flags.sqlite3-wasmfs += -Wno-limited-postlink-optimizations
+# ^^^^^ it likes to warn when we have "limited optimizations" via the -g3 flag.
+emcc.flags.sqlite3-wasmfs += -sMEMORY64=0
+emcc.flags.sqlite3-wasmfs += -sINITIAL_MEMORY=$(emcc.INITIAL_MEMORY.128)
+# ^^^^ 64MB is not enough for WASMFS/OPFS test runs using batch-runner.js
+sqlite3-wasmfs.fsflags := -pthread -sWASMFS \
+ -sPTHREAD_POOL_SIZE=1 \
+ -sERROR_ON_UNDEFINED_SYMBOLS=0 -sLLD_REPORT_UNDEFINED
+# ^^^^^ why undefined symbols are necessary for the wasmfs build is anyone's guess.
+emcc.flags.sqlite3-wasmfs += $(sqlite3-wasmfs.fsflags)
+emcc.flags.sqlite3-wasmfs += -sALLOW_MEMORY_GROWTH=0
+#^^^ using ALLOW_MEMORY_GROWTH produces a warning from emcc:
+# USE_PTHREADS + ALLOW_MEMORY_GROWTH may run non-wasm code slowly,
+# see https://github.com/WebAssembly/design/issues/1271 [-Wpthreads-mem-growth]
+# And, indeed, it runs slowly if memory is permitted to grow.
+#emcc.flags.sqlite3-wasmfs.vanilla :=
+#emcc.flags.sqlite3-wasmfs.esm := -sEXPORT_ES6 -sUSE_ES6_IMPORT_META
+sqlite3-api.mjs.wasmfs := $(dir.tmp)/sqlite3-api-wasmfs.mjs
+$(eval $(call SETUP_LIB_BUILD_MODE,sqlite3-wasmfs,esm,1,\
+ $(sqlite3-api.mjs.wasmfs), $(sqlite3-wasmfs.mjs),\
+ $(c-pp.D.sqlite3-bundler-friendly) -Dwasmfs,\
+ -sEXPORT_ES6 -sUSE_ES6_IMPORT_META\
+))
+$(sqlite3-wasmfs.js) $(sqlite3-wasmfs.mjs): $(MAKEFILE.wasmfs)
+########################################################################
+# Build quirk: we cannot build BOTH .js and .mjs with our current
+# build infrastructure because the supplemental *.worker.js files get
+# generated with the name of the main module file
+# ($(sqlite3-wasmfs.{js,mjs})) hard-coded in them. Thus the last one
+# to get built gets the *.worker.js files mapped to it. In order to
+# build both modes they would need to have distinct base names or
+# output directories. "The problem" with giving them distinct base
+# names is that it means that the corresponding .wasm file is also
+# built/saved multiple times.
+#
+wasmfs.build.ext := mjs
+$(sqlite3-wasmfs.js) $(sqlite3-wasmfs.mjs): $(SOAP.js.bld)
+ifeq (js,$(wasmfs.build.ext))
+ $(sqlite3-wasmfs.wasm): $(sqlite3-wasmfs.js)
+ wasmfs: $(sqlite3-wasmfs.js)
+else
+ $(sqlite3-wasmfs.wasm): $(sqlite3-wasmfs.mjs)
+ wasmfs: $(sqlite3-wasmfs.mjs)
+endif
+#all: wasmfs
+
+########################################################################
+# speedtest1 for wasmfs.
+speedtest1-wasmfs.mjs := $(dir.wasmfs)/speedtest1-wasmfs.mjs
+speedtest1-wasmfs.wasm := $(subst .mjs,.wasm,$(speedtest1-wasmfs.mjs))
+emcc.flags.speedtest1-wasmfs := $(sqlite3-wasmfs.fsflags)
+emcc.flags.speedtest1-wasmfs += $(SQLITE_OPT)
+emcc.flags.speedtest1-wasmfs += -sALLOW_MEMORY_GROWTH=0
+emcc.flags.speedtest1-wasmfs += -sINITIAL_MEMORY=$(emcc.INITIAL_MEMORY.128)
+#$(eval $(call call-make-pre-js,speedtest1-wasmfs,ems))
+$(speedtest1-wasmfs.mjs): $(speedtest1.cfiles) $(sqlite3-wasmfs.js) \
+ $(MAKEFILE) $(MAKEFILE.wasmfs) \
+ $(pre-post-sqlite3-wasmfs-esm.deps) \
+ $(EXPORTED_FUNCTIONS.speedtest1)
+ @echo "Building $@ ..."
+ $(emcc.bin) \
+ $(pre-post-sqlite3-wasmfs-esm.flags) \
+ $(cflags.common) \
+ $(cflags.sqlite3-wasmfs) \
+ $(emcc.speedtest1.common) \
+ $(emcc.flags.speedtest1-vanilla) \
+ $(emcc.flags.sqlite3-wasmfs) \
+ $(emcc.flags.speedtest1-wasmfs) \
+ -o $@ $(speedtest1.cfiles) -lm
+ @$(call SQLITE3.xJS.ESM-EXPORT-DEFAULT,1)
+ $(maybe-wasm-strip) $(speedtest1-wasmfs.wasm)
+ chmod -x $(speedtest1-wasmfs.wasm)
+ ls -la $@ $(speedtest1-wasmfs.wasm)
+
+wasmfs: $(speedtest1-wasmfs.mjs)
+CLEAN_FILES += $(speedtest1-wasmfs.mjs) $(speedtest1-wasmfs.wasm) \
+ $(subst .js,.worker.js,$(speedtest1-wasmfs.mjs))
+# end speedtest1.js
+########################################################################