diff --git a/README.md b/README.md
index f97a333c..5286cfbd 100755
--- a/README.md
+++ b/README.md
@@ -8,11 +8,11 @@ If you need a specific version of SQLite, or specific SQLite compilation options
 
 
 
-**July 2, 2023: Update SQLite to 3.42.0** ([changelog](https://github.com/swiftlyfalling/SQLiteLib/commits/master)).
+**November 2, 2023: Update SQLite to 3.44.0** ([changelog](https://github.com/swiftlyfalling/SQLiteLib/commits/master)).
 
 **Requirements**: iOS 8.0+ / OSX 10.9+, Xcode 7.3+
 
-**SQLite Included:** **[3.42.0](https://www.sqlite.org/releaselog/3_42_0.html)**
+**SQLite Included:** **[3.44.0](https://www.sqlite.org/releaselog/3_44_0.html)**
 
 
 
diff --git a/sqlite/Makefile.in b/sqlite/Makefile.in
index 61cb3ef0..c521586a 100644
--- a/sqlite/Makefile.in
+++ b/sqlite/Makefile.in
@@ -57,6 +57,7 @@ LIBTCL = @TCL_LIB_SPEC@
 #
 READLINE_FLAGS = -DHAVE_READLINE=@TARGET_HAVE_READLINE@ @TARGET_READLINE_INC@
 READLINE_FLAGS += -DHAVE_EDITLINE=@TARGET_HAVE_EDITLINE@
+READLINE_FLAGS += -DHAVE_LINENOISE=@TARGET_HAVE_LINENOISE@
 
 # The library that programs using readline() must link against.
 #
@@ -599,7 +600,7 @@ SHELL_OPT += -DSQLITE_ENABLE_DBPAGE_VTAB
 SHELL_OPT += -DSQLITE_ENABLE_DBSTAT_VTAB
 SHELL_OPT += -DSQLITE_ENABLE_BYTECODE_VTAB
 SHELL_OPT += -DSQLITE_ENABLE_OFFSET_SQL_FUNC
-FUZZERSHELL_OPT = 
+FUZZERSHELL_OPT =
 FUZZCHECK_OPT += -I$(TOP)/test
 FUZZCHECK_OPT += -I$(TOP)/ext/recover
 FUZZCHECK_OPT += \
@@ -690,6 +691,12 @@ srcck1$(BEXE):	$(TOP)/tool/srcck1.c
 sourcetest:	srcck1$(BEXE) sqlite3.c
 	./srcck1 sqlite3.c
 
+src-verify:	$(TOP)/tool/src-verify.c
+	$(BCC) -o src-verify$(BEXE) $(TOP)/tool/src-verify.c
+
+verify-source:	./src-verify
+	./src-verify $(TOP)
+
 fuzzershell$(TEXE):	$(TOP)/tool/fuzzershell.c sqlite3.c sqlite3.h
 	$(LTLINK) -o $@ $(FUZZERSHELL_OPT) \
 	  $(TOP)/tool/fuzzershell.c sqlite3.c $(TLIBS)
@@ -700,6 +707,9 @@ fuzzcheck$(TEXE):	$(FUZZCHECK_SRC) sqlite3.c sqlite3.h $(FUZZCHECK_DEP)
 fuzzcheck-asan$(TEXE):	$(FUZZCHECK_SRC) sqlite3.c sqlite3.h $(FUZZCHECK_DEP)
 	$(LTLINK) -o $@ -fsanitize=address $(FUZZCHECK_OPT) $(FUZZCHECK_SRC) sqlite3.c $(TLIBS)
 
+fuzzcheck-ubsan$(TEXE):	$(FUZZCHECK_SRC) sqlite3.c sqlite3.h $(FUZZCHECK_DEP)
+	$(LTLINK) -o $@ -fsanitize=undefined $(FUZZCHECK_OPT) $(FUZZCHECK_SRC) sqlite3.c $(TLIBS)
+
 ossshell$(TEXE):	$(TOP)/test/ossfuzz.c $(TOP)/test/ossshell.c sqlite3.c sqlite3.h
 	$(LTLINK) -o $@ $(FUZZCHECK_OPT) $(TOP)/test/ossshell.c \
              $(TOP)/test/ossfuzz.c sqlite3.c $(TLIBS)
@@ -759,13 +769,22 @@ mptest:	mptester$(TEXE)
 	$(MPTEST2) --journalmode DELETE
 
 
+has_tclsh84:
+	sh $(TOP)/tool/cktclsh.sh 8.4 $(TCLSH_CMD)
+	touch has_tclsh84
+
+has_tclsh85:
+	sh $(TOP)/tool/cktclsh.sh 8.5 $(TCLSH_CMD)
+	touch has_tclsh85
+
+
 # This target creates a directory named "tsrc" and fills it with
 # copies of all of the C source code and header files needed to
 # build on the target system.  Some of the C source code and header
 # files are automatically generated.  This target takes care of
 # all that automatic generation.
 #
-.target_source:	$(SRC) $(TOP)/tool/vdbe-compress.tcl fts5.c
+.target_source:	$(SRC) $(TOP)/tool/vdbe-compress.tcl has_tclsh84 fts5.c
 	rm -rf tsrc
 	mkdir tsrc
 	cp -f $(SRC) tsrc
@@ -775,15 +794,15 @@ mptest:	mptester$(TEXE)
 	cp fts5.c fts5.h tsrc
 	touch .target_source
 
-sqlite3.c:	.target_source $(TOP)/tool/mksqlite3c.tcl
+sqlite3.c:	.target_source $(TOP)/tool/mksqlite3c.tcl src-verify has_tclsh84
 	$(TCLSH_CMD) $(TOP)/tool/mksqlite3c.tcl $(AMALGAMATION_LINE_MACROS)
 	cp tsrc/sqlite3ext.h .
 	cp $(TOP)/ext/session/sqlite3session.h .
 
-sqlite3r.h: sqlite3.h
+sqlite3r.h: sqlite3.h has_tclsh84
 	$(TCLSH_CMD) $(TOP)/tool/mksqlite3h.tcl $(TOP) --enable-recover >sqlite3r.h
 
-sqlite3r.c: sqlite3.c sqlite3r.h
+sqlite3r.c: sqlite3.c sqlite3r.h has_tclsh84
 	cp $(TOP)/ext/recover/sqlite3recover.c tsrc/
 	cp $(TOP)/ext/recover/sqlite3recover.h tsrc/
 	cp $(TOP)/ext/recover/dbdata.c tsrc/
@@ -798,7 +817,7 @@ tclsqlite3.c:	sqlite3.c
 	echo '#endif /* USE_SYSTEM_SQLITE */' >>tclsqlite3.c
 	cat $(TOP)/src/tclsqlite.c >>tclsqlite3.c
 
-sqlite3-all.c:	sqlite3.c $(TOP)/tool/split-sqlite3c.tcl
+sqlite3-all.c:	sqlite3.c $(TOP)/tool/split-sqlite3c.tcl has_tclsh84
 	$(TCLSH_CMD) $(TOP)/tool/split-sqlite3c.tcl
 
 # Rule to build the amalgamation
@@ -1086,10 +1105,10 @@ tclsqlite3$(TEXE):	tclsqlite-shell.lo libsqlite3.la
 
 # Rules to build opcodes.c and opcodes.h
 #
-opcodes.c:	opcodes.h $(TOP)/tool/mkopcodec.tcl
+opcodes.c:	opcodes.h $(TOP)/tool/mkopcodec.tcl has_tclsh84
 	$(TCLSH_CMD) $(TOP)/tool/mkopcodec.tcl opcodes.h >opcodes.c
 
-opcodes.h:	parse.h $(TOP)/src/vdbe.c $(TOP)/tool/mkopcodeh.tcl
+opcodes.h:	parse.h $(TOP)/src/vdbe.c $(TOP)/tool/mkopcodeh.tcl has_tclsh84
 	cat parse.h $(TOP)/src/vdbe.c | $(TCLSH_CMD) $(TOP)/tool/mkopcodeh.tcl >opcodes.h
 
 # Rules to build parse.c and parse.h - the outputs of lemon.
@@ -1100,10 +1119,10 @@ parse.c:	$(TOP)/src/parse.y lemon$(BEXE)
 	cp $(TOP)/src/parse.y .
 	./lemon$(BEXE) $(OPT_FEATURE_FLAGS) $(OPTS) -S parse.y
 
-sqlite3.h:	$(TOP)/src/sqlite.h.in $(TOP)/manifest mksourceid$(BEXE) $(TOP)/VERSION
+sqlite3.h:	$(TOP)/src/sqlite.h.in $(TOP)/manifest mksourceid$(BEXE) $(TOP)/VERSION has_tclsh84
 	$(TCLSH_CMD) $(TOP)/tool/mksqlite3h.tcl $(TOP) >sqlite3.h
 
-sqlite3rc.h:	$(TOP)/src/sqlite3.rc $(TOP)/VERSION
+sqlite3rc.h:	$(TOP)/src/sqlite3.rc $(TOP)/VERSION has_tclsh84
 	echo '#ifndef SQLITE_RESOURCE_VERSION' >$@
 	echo -n '#define SQLITE_RESOURCE_VERSION ' >>$@
 	cat $(TOP)/VERSION | $(TCLSH_CMD) $(TOP)/tool/replace.tcl exact . , >>$@
@@ -1133,12 +1152,13 @@ SHELL_SRC = \
 	$(TOP)/ext/expert/sqlite3expert.h \
 	$(TOP)/ext/misc/zipfile.c \
 	$(TOP)/ext/misc/memtrace.c \
+	$(TOP)/ext/misc/pcachetrace.c \
 	$(TOP)/ext/recover/dbdata.c \
 	$(TOP)/ext/recover/sqlite3recover.c \
 	$(TOP)/ext/recover/sqlite3recover.h \
         $(TOP)/src/test_windirent.c
 
-shell.c:	$(SHELL_SRC) $(TOP)/tool/mkshellc.tcl
+shell.c:	$(SHELL_SRC) $(TOP)/tool/mkshellc.tcl has_tclsh84
 	$(TCLSH_CMD) $(TOP)/tool/mkshellc.tcl >shell.c
 
 
@@ -1226,7 +1246,7 @@ fts5parse.c:	$(TOP)/ext/fts5/fts5parse.y lemon$(BEXE)
 
 fts5parse.h: fts5parse.c
 
-fts5.c: $(FTS5_SRC)
+fts5.c: $(FTS5_SRC) has_tclsh84
 	$(TCLSH_CMD) $(TOP)/ext/fts5/tool/mkfts5c.tcl
 	cp $(TOP)/ext/fts5/fts5.h .
 
@@ -1260,7 +1280,7 @@ TESTFIXTURE_SRC1 = sqlite3.c
 TESTFIXTURE_SRC = $(TESTSRC) $(TOP)/src/tclsqlite.c
 TESTFIXTURE_SRC += $(TESTFIXTURE_SRC$(USE_AMALGAMATION))
 
-testfixture$(TEXE):	$(TESTFIXTURE_SRC)
+testfixture$(TEXE):	has_tclsh85 $(TESTFIXTURE_SRC)
 	$(LTLINK) -DSQLITE_NO_SYNC=1 $(TEMP_STORE) $(TESTFIXTURE_FLAGS) \
 		-o $@ $(TESTFIXTURE_SRC) $(LIBTCL) $(TLIBS)
 
@@ -1284,11 +1304,17 @@ fulltestonly:	$(TESTPROGS) fuzztest
 	./testfixture$(TEXE) $(TOP)/test/full.test
 
 # Fuzz testing
-fuzztest:	fuzzcheck$(TEXE) $(FUZZDATA) sessionfuzz$(TEXE) $(TOP)/test/sessionfuzz-data1.db
+#
+# WARNING: When the "fuzztest" target is run by the testrunner.tcl script,
+# it does not actually run this code. Instead, it schedules equivalent 
+# commands. Therefore, if this target is updated, then code in
+# testrunner_data.tcl (search for "trd_fuzztest_data") must also be updated.
+#
+fuzztest:	fuzzcheck$(TEXE) $(FUZZDATA) sessionfuzz$(TEXE)
 	./fuzzcheck$(TEXE) $(FUZZDATA)
 	./sessionfuzz$(TEXE) run $(TOP)/test/sessionfuzz-data1.db
 
-valgrindfuzz:	fuzzcheck$(TEXT) $(FUZZDATA) sessionfuzz$(TEXE) $(TOP)/test/sessionfuzz-data1.db
+valgrindfuzz:	fuzzcheck$(TEXT) $(FUZZDATA) sessionfuzz$(TEXE)
 	valgrind ./fuzzcheck$(TEXE) --cell-size-check --limit-mem 10M $(FUZZDATA)
 	valgrind ./sessionfuzz$(TEXE) run $(TOP)/test/sessionfuzz-data1.db
 
@@ -1305,11 +1331,23 @@ testrunner:	testfixture$(TEXE)
 
 # Runs both fuzztest and testrunner, consecutively.
 #
-devtest:	testfixture$(TEXE) fuzztest testrunner
+devtest:	srctree-check testfixture$(TEXE) fuzztest testrunner
+
+mdevtest: srctree-check has_tclsh85
+	$(TCLSH_CMD) $(TOP)/test/testrunner.tcl mdevtest
+
+sdevtest: has_tclsh85
+	$(TCLSH_CMD) $(TOP)/test/testrunner.tcl sdevtest
+
+# Validate that various generated files in the source tree
+# are up-to-date.
+#
+srctree-check:	$(TOP)/tool/srctree-check.tcl
+	$(TCLSH_CMD) $(TOP)/tool/srctree-check.tcl
 
 # Testing for a release
 #
-releasetest: testfixture$(TEXE)
+releasetest: srctree-check testfixture$(TEXE)
 	./testfixture$(TEXE) $(TOP)/test/testrunner.tcl release
 
 # Minimal testing that runs in less than 3 minutes
@@ -1320,7 +1358,7 @@ quicktest:	./testfixture$(TEXE)
 # This is the common case.  Run many tests that do not take too long,
 # including fuzzcheck, sqlite3_analyzer, and sqldiff tests.
 #
-test:	fuzztest sourcetest $(TESTPROGS) tcltest
+test:	srctree-check fuzztest sourcetest $(TESTPROGS) tcltest
 
 # Run a test using valgrind.  This can take a really long time
 # because valgrind is so much slower than a native machine.
@@ -1338,13 +1376,13 @@ smoketest:	$(TESTPROGS) fuzzcheck$(TEXE)
 shelltest: $(TESTPROGS)
 	./testfixture$(TEXT) $(TOP)/test/permutations.test shell
 
-sqlite3_analyzer.c: sqlite3.c $(TOP)/src/tclsqlite.c $(TOP)/tool/spaceanal.tcl $(TOP)/tool/mkccode.tcl $(TOP)/tool/sqlite3_analyzer.c.in
+sqlite3_analyzer.c: sqlite3.c $(TOP)/src/tclsqlite.c $(TOP)/tool/spaceanal.tcl $(TOP)/tool/mkccode.tcl $(TOP)/tool/sqlite3_analyzer.c.in has_tclsh85
 	$(TCLSH_CMD) $(TOP)/tool/mkccode.tcl $(TOP)/tool/sqlite3_analyzer.c.in >sqlite3_analyzer.c
 
 sqlite3_analyzer$(TEXE): sqlite3_analyzer.c
 	$(LTLINK) sqlite3_analyzer.c -o $@ $(LIBTCL) $(TLIBS)
 
-sqltclsh.c: sqlite3.c $(TOP)/src/tclsqlite.c $(TOP)/tool/sqltclsh.tcl $(TOP)/ext/misc/appendvfs.c $(TOP)/tool/mkccode.tcl $(TOP)/tool/sqltclsh.c.in
+sqltclsh.c: sqlite3.c $(TOP)/src/tclsqlite.c $(TOP)/tool/sqltclsh.tcl $(TOP)/ext/misc/appendvfs.c $(TOP)/tool/mkccode.tcl $(TOP)/tool/sqltclsh.c.in has_tclsh85
 	$(TCLSH_CMD) $(TOP)/tool/mkccode.tcl $(TOP)/tool/sqltclsh.c.in >sqltclsh.c
 
 sqltclsh$(TEXE): sqltclsh.c
@@ -1363,7 +1401,7 @@ CHECKER_DEPS =\
   $(TOP)/ext/misc/btreeinfo.c \
   $(TOP)/ext/repair/sqlite3_checker.c.in
 
-sqlite3_checker.c:	$(CHECKER_DEPS)
+sqlite3_checker.c:	$(CHECKER_DEPS) has_tclsh85
 	$(TCLSH_CMD) $(TOP)/tool/mkccode.tcl $(TOP)/ext/repair/sqlite3_checker.c.in >$@
 
 sqlite3_checker$(TEXE):	sqlite3_checker.c
@@ -1449,6 +1487,11 @@ amalgamation-tarball: sqlite3.c sqlite3rc.h
 snapshot-tarball: sqlite3.c sqlite3rc.h
 	TOP=$(TOP) sh $(TOP)/tool/mkautoconfamal.sh --snapshot
 
+# Build a ZIP archive containing various command-line tools.
+#
+tool-zip:	testfixture sqlite3 sqldiff sqlite3_analyzer $(TOP)/tool/mktoolzip.tcl
+	./testfixture $(TOP)/tool/mktoolzip.tcl
+
 # The next two rules are used to support the "threadtest" target. Building
 # threadtest runs a few thread-safety tests that are implemented in C. This
 # target is invoked by the releasetest.tcl script.
@@ -1506,6 +1549,7 @@ clean:
 	rm -f LogEst$(TEXE) fts3view$(TEXE) rollback-test$(TEXE) showdb$(TEXE)
 	rm -f showjournal$(TEXE) showstat4$(TEXE) showwal$(TEXE) speedtest1$(TEXE)
 	rm -f wordcount$(TEXE) changeset$(TEXE)
+	rm -f version-info$(TEXT)
 	rm -f sqlite3.dll sqlite3.lib sqlite3.exp sqlite3.def
 	rm -f sqlite3.c
 	rm -f sqlite3rc.h
@@ -1521,6 +1565,9 @@ clean:
 	rm -f dbhash dbhash.exe
 	rm -f fts5.* fts5parse.*
 	rm -f threadtest5
+	rm -f src-verify
+	rm -f custom.rws
+	rm -f has_tclsh84 has_tclsh85
 
 distclean:	clean
 	rm -f sqlite_cfg.h config.log config.status libtool Makefile sqlite3.pc \
@@ -1549,3 +1596,28 @@ sqlite3.dll: $(REAL_LIBOBJ) sqlite3.def
 #
 fiddle: sqlite3.c shell.c
 	make -C ext/wasm fiddle emcc_opt=-Os
+
+#
+# Spell-checking for source comments
+# The sources checked are either C sources or C source templates.
+# Their comments are extracted and processed through aspell using
+# a custom dictionary that contains scads of odd identifiers that
+# find their way into the comments.
+#
+# Currently, this target is setup to be "made" in-tree only.
+# The output is ephemeral. Redirect it to guide spelling fixups,
+# either to correct spelling or add words to tool/custom.txt.
+#
+./custom.rws: ./tool/custom.txt
+	@echo 'Updating custom dictionary from tool/custom.txt'
+	aspell --lang=en create master ./custom.rws < $<
+
+misspell: ./custom.rws has_tclsh84
+	$(TCLSH_CMD) ./tool/spellsift.tcl ./src/*.c ./src/*.h ./src/*.in
+
+#
+# tool/version-info: a utility for emitting sqlite3 version info
+# in various forms.
+#
+version-info$(TEXE):	$(TOP)/tool/version-info.c Makefile sqlite3.h
+	$(LTLINK) $(ST_OPT) -o $@ $(TOP)/tool/version-info.c
diff --git a/sqlite/Makefile.linux-gcc b/sqlite/Makefile.linux-gcc
index ad5d4dd0..fe7349ad 100644
--- a/sqlite/Makefile.linux-gcc
+++ b/sqlite/Makefile.linux-gcc
@@ -22,12 +22,6 @@ TOP = ../sqlite
 BCC = gcc -g -O0
 #BCC = /opt/ancic/bin/c89 -0
 
-#### If the target operating system supports the "usleep()" system
-#    call, then define the HAVE_USLEEP macro for all C modules.
-#
-#USLEEP = 
-USLEEP = -DHAVE_USLEEP=1
-
 #### If you want the SQLite library to be safe for use within a 
 #    multi-threaded program, then define the following macro
 #    appropriately:
diff --git a/sqlite/Makefile.msc b/sqlite/Makefile.msc
index 87814894..4339be89 100644
--- a/sqlite/Makefile.msc
+++ b/sqlite/Makefile.msc
@@ -52,6 +52,13 @@ MINIMAL_AMALGAMATION = 0
 USE_STDCALL = 0
 !ENDIF
 
+# Use the USE_SEH=0 option on the nmake command line to omit structured
+# exception handling (SEH) support.  SEH is on by default.
+#
+!IFNDEF USE_SEH
+USE_SEH = 1
+!ENDIF
+
 # Set this non-0 to have the shell executable link against the core dynamic
 # link library.
 #
@@ -218,6 +225,12 @@ WIN32HEAP = 0
 OSTRACE = 0
 !ENDIF
 
+# enable address sanitizer using ASAN=1 on the command-line.
+#
+!IFNDEF ASAN
+ASAN = 0
+!ENDIF
+
 # Set this to one of the following values to enable various debugging
 # features.  Each level includes the debugging options from the previous
 # levels.  Currently, the recognized values for DEBUG are:
@@ -361,6 +374,7 @@ SQLITE_TCL_DEP =
 !IFNDEF OPT_FEATURE_FLAGS
 !IF $(MINIMAL_AMALGAMATION)==0
 OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_FTS3=1
+OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_FTS5=1
 OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_RTREE=1
 OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_GEOPOLY=1
 OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_STMTVTAB=1
@@ -389,6 +403,14 @@ OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_MATH_FUNCTIONS
 OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_RBU=1
 !ENDIF
 
+# Should structured exception handling (SEH) be enabled for WAL mode in
+# the core library?  It is on by default.  Only omit it if the
+# USE_SEH=0 option is provided on the nmake command-line.
+#
+!IF $(USE_SEH)==0
+OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_OMIT_SEH=1
+!ENDIF
+
 # These are the "extended" SQLite compilation options used when compiling for
 # the Windows 10 platform.
 #
@@ -877,6 +899,13 @@ RCC = $(RCC) -DSQLITE_WIN32_MALLOC_VALIDATE=1
 !ENDIF
 !ENDIF
 
+
+# Address sanitizer if ASAN=1
+#
+!IF $(ASAN)>0
+TCC = $(TCC) /fsanitize=address
+!ENDIF
+
 # <>
 # The locations of the Tcl header and library files.  Also, the library that
 # non-stubs enabled programs using Tcl must link against.  These variables
@@ -1565,8 +1594,7 @@ TESTEXT = \
   $(TOP)\ext\rtree\test_rtreedoc.c \
   $(TOP)\ext\recover\sqlite3recover.c \
   $(TOP)\ext\recover\test_recover.c \
-  $(TOP)\ext\recover\dbdata.c \
-  fts5.c
+  $(TOP)\ext\recover\dbdata.c 
 
 # If use of zlib is enabled, add the "zipfile.c" source file.
 #
@@ -1582,7 +1610,8 @@ TESTSRC2 = \
   $(SRC01) \
   $(SRC07) \
   $(SRC10) \
-  $(TOP)\ext\async\sqlite3async.c
+  $(TOP)\ext\async\sqlite3async.c \
+  fts5.c
 
 # Header files used by all library source files.
 #
@@ -1661,6 +1690,8 @@ SHELL_COMPILE_OPTS = $(SHELL_COMPILE_OPTS) -DSQLITE_DQS=0
 SHELL_COMPILE_OPTS = $(SHELL_COMPILE_OPTS) -DSQLITE_ENABLE_FTS4=1
 SHELL_COMPILE_OPTS = $(SHELL_COMPILE_OPTS) -DSQLITE_ENABLE_EXPLAIN_COMMENTS=1
 SHELL_COMPILE_OPTS = $(SHELL_COMPILE_OPTS) -DSQLITE_ENABLE_OFFSET_SQL_FUNC=1
+SHELL_COMPILE_OPTS = $(SHELL_COMPILE_OPTS) -DSQLITE_ENABLE_UNKNOWN_SQL_FUNCTION=1
+SHELL_COMPILE_OPTS = $(SHELL_COMPILE_OPTS) -DSQLITE_ENABLE_STMT_SCANSTATUS=1
 !ENDIF
 
 # <>
@@ -1792,8 +1823,8 @@ $(SQLITE3EXE):	shell.c $(SHELL_CORE_DEP) $(LIBRESOBJS) $(SHELL_CORE_SRC) $(SQLIT
 		/link $(SQLITE3EXEPDB) $(LDFLAGS) $(LTLINKOPTS) $(SHELL_LINK_OPTS) $(LTLIBPATHS) $(LIBRESOBJS) $(LIBREADLINE) $(LTLIBS) $(TLIBS)
 
 # <>
-sqldiff.exe:	$(TOP)\tool\sqldiff.c $(SQLITE3C) $(SQLITE3H)
-	$(LTLINK) $(NO_WARN) $(TOP)\tool\sqldiff.c $(SQLITE3C) /link $(LDFLAGS) $(LTLINKOPTS)
+sqldiff.exe:	$(TOP)\tool\sqldiff.c $(SQLITE3C) $(SQLITE3H) $(LIBRESOBJS)
+	$(LTLINK) $(NO_WARN) $(TOP)\tool\sqldiff.c $(SQLITE3C) /link $(LDFLAGS) $(LTLINKOPTS) $(LIBRESOBJS)
 
 dbhash.exe:	$(TOP)\tool\dbhash.c $(SQLITE3C) $(SQLITE3H)
 	$(LTLINK) $(NO_WARN) $(TOP)\tool\dbhash.c $(SQLITE3C) /link $(LDFLAGS) $(LTLINKOPTS)
@@ -1807,6 +1838,12 @@ srcck1.exe:	$(TOP)\tool\srcck1.c
 sourcetest:	srcck1.exe $(SQLITE3C)
 	srcck1.exe $(SQLITE3C)
 
+src-verify.exe:	$(TOP)\tool\src-verify.c
+	$(LTLINK) $(NO_WARN) $(TOP)\tool\src-verify.c
+
+verify-source:	src-verify.exe
+	src-verify.exe $(TOP)
+
 fuzzershell.exe:	$(TOP)\tool\fuzzershell.c $(SQLITE3C) $(SQLITE3H)
 	$(LTLINK) $(NO_WARN) $(FUZZERSHELL_COMPILE_OPTS) $(TOP)\tool\fuzzershell.c $(SQLITE3C) /link $(LDFLAGS) $(LTLINKOPTS)
 
@@ -1870,7 +1907,7 @@ mptest:	mptester.exe
 	move vdbe.new tsrc\vdbe.c
 	echo > .target_source
 
-sqlite3.c:	.target_source sqlite3ext.h sqlite3session.h $(MKSQLITE3C_TOOL)
+sqlite3.c:	.target_source sqlite3ext.h sqlite3session.h $(MKSQLITE3C_TOOL) src-verify.exe
 	$(TCLSH_CMD) $(MKSQLITE3C_TOOL) $(MKSQLITE3C_ARGS)
 
 sqlite3-all.c:	sqlite3.c $(TOP)\tool\split-sqlite3c.tcl
@@ -2226,8 +2263,8 @@ SHELL_SRC = \
 	$(TOP)\src\shell.c.in \
 	$(TOP)\ext\misc\appendvfs.c \
 	$(TOP)\ext\misc\completion.c \
-        $(TOP)\ext\misc\base64.c \
-        $(TOP)\ext\misc\base85.c \
+	$(TOP)\ext\misc\base64.c \
+	$(TOP)\ext\misc\base85.c \
 	$(TOP)\ext\misc\decimal.c \
 	$(TOP)\ext\misc\fileio.c \
 	$(TOP)\ext\misc\ieee754.c \
@@ -2238,9 +2275,10 @@ SHELL_SRC = \
 	$(TOP)\ext\expert\sqlite3expert.c \
 	$(TOP)\ext\expert\sqlite3expert.h \
 	$(TOP)\ext\misc\memtrace.c \
-	$(TOP)/ext/recover/dbdata.c \
-	$(TOP)/ext/recover/sqlite3recover.c \
-	$(TOP)/ext/recover/sqlite3recover.h \
+	$(TOP)\ext\misc\pcachetrace.c \
+	$(TOP)\ext\recover\dbdata.c \
+	$(TOP)\ext\recover\sqlite3recover.c \
+	$(TOP)\ext\recover\sqlite3recover.h \
 	$(TOP)\src\test_windirent.c
 
 # If use of zlib is enabled, add the "zipfile.c" source file.
@@ -2432,6 +2470,9 @@ extensiontest:	testfixture.exe testloadext.dll
 	@set PATH=$(LIBTCLPATH);$(PATH)
 	.\testfixture.exe $(TOP)\test\loadext.test $(TESTOPTS)
 
+tool-zip:	testfixture.exe sqlite3.exe sqldiff.exe sqlite3_analyzer.exe $(TOP)\tool\mktoolzip.tcl
+	.\testfixture.exe $(TOP)\tool\mktoolzip.tcl
+
 coretestprogs:	$(TESTPROGS)
 
 testprogs:	coretestprogs srcck1.exe fuzzcheck.exe sessionfuzz.exe
@@ -2484,10 +2525,13 @@ testrunner:	testfixture.exe
 #
 devtest:	testfixture.exe fuzztest testrunner
 
+mdevtest:
+	$(TCLSH_CMD) $(TOP)\test\testrunner.tcl mdevtest
+
 # Testing for a release
 #
 releasetest: testfixture.exe fuzztest
-	testfixture.exe $(TOP)/test/testrunner.tcl release
+	testfixture.exe $(TOP)\test\testrunner.tcl release
 
 
 smoketest:	$(TESTPROGS)
@@ -2515,14 +2559,14 @@ sqlite3_expert.exe: $(SQLITE3C) $(TOP)\ext\expert\sqlite3expert.h $(TOP)\ext\exp
 	$(LTLINK) $(NO_WARN)	$(TOP)\ext\expert\sqlite3expert.c $(TOP)\ext\expert\expert.c $(SQLITE3C) $(TLIBS)
 
 CHECKER_DEPS =\
-  $(TOP)/tool/mkccode.tcl \
+  $(TOP)\tool\mkccode.tcl \
   sqlite3.c \
-  $(TOP)/src/tclsqlite.c \
-  $(TOP)/ext/repair/sqlite3_checker.tcl \
-  $(TOP)/ext/repair/checkindex.c \
-  $(TOP)/ext/repair/checkfreelist.c \
-  $(TOP)/ext/misc/btreeinfo.c \
-  $(TOP)/ext/repair/sqlite3_checker.c.in
+  $(TOP)\src\tclsqlite.c \
+  $(TOP)\ext\repair\sqlite3_checker.tcl \
+  $(TOP)\ext\repair\checkindex.c \
+  $(TOP)\ext\repair\checkfreelist.c \
+  $(TOP)\ext\misc\btreeinfo.c \
+  $(TOP)\ext\repair\sqlite3_checker.c.in
 
 sqlite3_checker.c:	$(CHECKER_DEPS)
 	$(TCLSH_CMD) $(TOP)\tool\mkccode.tcl $(TOP)\ext\repair\sqlite3_checker.c.in > $@
@@ -2661,4 +2705,5 @@ clean:
 	del /Q showshm.exe sqlite3_checker.* sqlite3_expert.exe 2>NUL
 	del /Q fts5.* fts5parse.* 2>NUL
 	del /Q lsm.h lsm1.c 2>NUL
+	del /q src-verify.exe 2>NUL
 # <>
diff --git a/sqlite/README.md b/sqlite/README.md
index 0df8b58c..ebb917e2 100644
--- a/sqlite/README.md
+++ b/sqlite/README.md
@@ -25,7 +25,28 @@ If you pulled your SQLite source code from a secondary source and want to
 verify its integrity, there are hints on how to do that in the
 [Verifying Code Authenticity](#vauth) section below.
 
-## Obtaining The Code
+## Contacting The SQLite Developers
+
+The preferred way to ask questions or make comments about SQLite or to
+report bugs against SQLite is to visit the 
+[SQLite Forum](https://sqlite.org/forum) at .
+Anonymous postings are permitted.
+
+If you think you have found a bug that has security implications and
+you do not want to report it on the public forum, you can send a private
+email to drh at sqlite dot org.
+
+## Public Domain
+
+The SQLite source code is in the public domain.  See
+ for details. 
+
+Because SQLite is in the public domain, we do not normally accept pull
+requests, because if we did take a pull request, the changes in that
+pull request might carry a copyright and the SQLite source code would
+then no longer be fully in the public domain.
+
+## Obtaining The SQLite Source Code
 
 If you do not want to use Fossil, you can download tarballs or ZIP
 archives or [SQLite archives](https://sqlite.org/cli.html#sqlar) as follows:
@@ -82,9 +103,9 @@ For example:
         mkdir bld                ;#  Build will occur in a sibling directory
         cd bld                   ;#  Change to the build directory
         ../sqlite/configure      ;#  Run the configure script
-        make                     ;#  Run the makefile.
+        make                     ;#  Builds the "sqlite3" command-line tool
         make sqlite3.c           ;#  Build the "amalgamation" source file
-        make test                ;#  Run some tests (requires Tcl)
+        make devtest             ;#  Run some tests (requires Tcl)
 
 See the makefile for additional targets.
 
@@ -94,32 +115,33 @@ script does not work out for you, there is a generic makefile named
 can copy and edit to suit your needs.  Comments on the generic makefile
 show what changes are needed.
 
-## Using MSVC for Windows systems
+## Compiling for Windows Using MSVC
 
 On Windows, all applicable build products can be compiled with MSVC.
-First open the command prompt window associated with the desired compiler
-version (e.g. "Developer Command Prompt for VS2013").  Next, use NMAKE
-with the provided "Makefile.msc" to build one of the supported targets.
-
-For example, from the parent directory of the source subtree named "sqlite":
-
-        mkdir bld
-        cd bld
-        nmake /f ..\sqlite\Makefile.msc TOP=..\sqlite
-        nmake /f ..\sqlite\Makefile.msc sqlite3.c TOP=..\sqlite
-        nmake /f ..\sqlite\Makefile.msc sqlite3.dll TOP=..\sqlite
-        nmake /f ..\sqlite\Makefile.msc sqlite3.exe TOP=..\sqlite
-        nmake /f ..\sqlite\Makefile.msc test TOP=..\sqlite
-
-There are several build options that can be set via the NMAKE command
-line.  For example, to build for WinRT, simply add "FOR_WINRT=1" argument
-to the "sqlite3.dll" command line above.  When debugging into the SQLite
-code, adding the "DEBUG=1" argument to one of the above command lines is
-recommended.
-
-SQLite does not require [Tcl](http://www.tcl.tk/) to run, but a Tcl installation
-is required by the makefiles (including those for MSVC).  SQLite contains
-a lot of generated code and Tcl is used to do much of that code generation.
+You will also need a working installation of TCL.
+See the [compile-for-windows.md](doc/compile-for-windows.md) document for
+additional information about how to install MSVC and TCL and configure your
+build environment.
+
+If you want to run tests, you need to let SQLite know the location of your
+TCL library, using a command like this:
+
+        set TCLDIR=c:\Tcl
+
+SQLite uses "tclsh.exe" as part of the build process, and so that utility
+program will need to be somewhere on your %PATH%.  The finished SQLite library
+does not contain any TCL code, but it does use TCL to help with the build process
+and to run tests.
+
+Build using Makefile.msc.  Example:
+
+        nmake /f Makefile.msc
+        nmake /f Makefile.msc sqlite3.c
+        nmake /f Makefile.msc devtest
+        nmake /f Makefile.msc releasetest
+ 
+There are many other makefile targets.  See comments in Makefile.msc for
+details.
 
 ## Source Code Tour
 
@@ -290,16 +312,13 @@ Key files:
      is not part of the core SQLite library.  But as most of the tests in this
      repository are written in Tcl, the Tcl language bindings are important.
 
-  *  **test*.c** - Files in the src/ folder that begin with "test" go into
+  *  **test\*.c** - Files in the src/ folder that begin with "test" go into
      building the "testfixture.exe" program.  The testfixture.exe program is
      an enhanced Tcl shell.  The testfixture.exe program runs scripts in the
      test/ folder to validate the core SQLite code.  The testfixture program
      (and some other test programs too) is built and run when you type
      "make test".
 
-  *  **ext/misc/json1.c** - This file implements the various JSON functions
-     that are built into SQLite.
-
 There are many other source files.  Each has a succinct header comment that
 describes its purpose and role within the larger system.
 
@@ -307,8 +326,8 @@ describes its purpose and role within the larger system.
 ## Verifying Code Authenticity
 
 The `manifest` file at the root directory of the source tree
-contains either a SHA3-256 hash (for newer files) or a SHA1 hash (for
-older files) for every source file in the repository.
+contains either a SHA3-256 hash or a SHA1 hash
+for every source file in the repository.
 The name of the version of the entire source tree is just the
 SHA3-256 hash of the `manifest` file itself, possibly with the
 last line of that file omitted if the last line begins with
@@ -316,14 +335,25 @@ last line of that file omitted if the last line begins with
 The `manifest.uuid` file should contain the SHA3-256 hash of the
 `manifest` file. If all of the above hash comparisons are correct, then
 you can be confident that your source tree is authentic and unadulterated.
+Details on the format for the `manifest` files are available
+[on the Fossil website](https://fossil-scm.org/fossil/doc/trunk/www/fileformat.wiki#manifest).
+
+The process of checking source code authenticity is automated by the 
+makefile:
+
+>   make verify-source
+
+Or on windows:
+
+>   nmake /f Makefile.msc verify-source
 
-The format of the `manifest` file should be mostly self-explanatory, but
-if you want details, they are available
-[here](https://fossil-scm.org/fossil/doc/trunk/www/fileformat.wiki#manifest).
+Using the makefile to verify source integrity is good for detecting
+accidental changes to the source tree, but malicious changes could be
+hidden by also modifying the makefiles.
 
 ## Contacts
 
-The main SQLite website is [http://www.sqlite.org/](http://www.sqlite.org/)
+The main SQLite website is [http:/sqlite.org/](http://sqlite.org/)
 with geographically distributed backups at
 [http://www2.sqlite.org/](http://www2.sqlite.org) and
 [http://www3.sqlite.org/](http://www3.sqlite.org).
diff --git a/sqlite/VERSION b/sqlite/VERSION
index 476cebe0..faf0dcbb 100644
--- a/sqlite/VERSION
+++ b/sqlite/VERSION
@@ -1 +1 @@
-3.42.0
+3.44.0
diff --git a/sqlite/autoconf/Makefile.am b/sqlite/autoconf/Makefile.am
index 694419b2..1eaa560f 100644
--- a/sqlite/autoconf/Makefile.am
+++ b/sqlite/autoconf/Makefile.am
@@ -9,7 +9,7 @@ sqlite3_SOURCES = shell.c sqlite3.h
 EXTRA_sqlite3_SOURCES = sqlite3.c
 sqlite3_LDADD = @EXTRA_SHELL_OBJ@ @READLINE_LIBS@
 sqlite3_DEPENDENCIES = @EXTRA_SHELL_OBJ@
-sqlite3_CFLAGS = $(AM_CFLAGS) -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_ENABLE_DBPAGE_VTAB -DSQLITE_ENABLE_STMTVTAB -DSQLITE_ENABLE_DBSTAT_VTAB $(SHELL_CFLAGS)
+sqlite3_CFLAGS = $(AM_CFLAGS) -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_DQS=0 -DSQLITE_ENABLE_DBPAGE_VTAB -DSQLITE_ENABLE_STMTVTAB -DSQLITE_ENABLE_DBSTAT_VTAB $(SHELL_CFLAGS)
 
 include_HEADERS = sqlite3.h sqlite3ext.h
 
diff --git a/sqlite/autoconf/Makefile.msc b/sqlite/autoconf/Makefile.msc
index 09daa867..280bb95d 100644
--- a/sqlite/autoconf/Makefile.msc
+++ b/sqlite/autoconf/Makefile.msc
@@ -52,6 +52,13 @@ MINIMAL_AMALGAMATION = 0
 USE_STDCALL = 0
 !ENDIF
 
+# Use the USE_SEH=0 option on the nmake command line to omit structured
+# exception handling (SEH) support.  SEH is on by default.
+#
+!IFNDEF USE_SEH
+USE_SEH = 1
+!ENDIF
+
 # Set this non-0 to have the shell executable link against the core dynamic
 # link library.
 #
@@ -180,6 +187,12 @@ WIN32HEAP = 0
 OSTRACE = 0
 !ENDIF
 
+# enable address sanitizer using ASAN=1 on the command-line.
+#
+!IFNDEF ASAN
+ASAN = 0
+!ENDIF
+
 # Set this to one of the following values to enable various debugging
 # features.  Each level includes the debugging options from the previous
 # levels.  Currently, the recognized values for DEBUG are:
@@ -283,6 +296,7 @@ SQLITE3EXEPDB = /pdb:sqlite3sh.pdb
 !IFNDEF OPT_FEATURE_FLAGS
 !IF $(MINIMAL_AMALGAMATION)==0
 OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_FTS3=1
+OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_FTS5=1
 OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_RTREE=1
 OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_GEOPOLY=1
 OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_STMTVTAB=1
@@ -311,6 +325,14 @@ OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_MATH_FUNCTIONS
 OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_ENABLE_RBU=1
 !ENDIF
 
+# Should structured exception handling (SEH) be enabled for WAL mode in
+# the core library?  It is on by default.  Only omit it if the
+# USE_SEH=0 option is provided on the nmake command-line.
+#
+!IF $(USE_SEH)==0
+OPT_FEATURE_FLAGS = $(OPT_FEATURE_FLAGS) -DSQLITE_OMIT_SEH=1
+!ENDIF
+
 # These are the "extended" SQLite compilation options used when compiling for
 # the Windows 10 platform.
 #
@@ -718,6 +740,13 @@ RCC = $(RCC) -DSQLITE_WIN32_MALLOC_VALIDATE=1
 !ENDIF
 
 
+# Address sanitizer if ASAN=1
+#
+!IF $(ASAN)>0
+TCC = $(TCC) /fsanitize=address
+!ENDIF
+
+
 # Compiler options needed for programs that use the readline() library.
 #
 !IFNDEF READLINE_FLAGS
@@ -959,6 +988,8 @@ SHELL_COMPILE_OPTS = $(SHELL_COMPILE_OPTS) -DSQLITE_DQS=0
 SHELL_COMPILE_OPTS = $(SHELL_COMPILE_OPTS) -DSQLITE_ENABLE_FTS4=1
 SHELL_COMPILE_OPTS = $(SHELL_COMPILE_OPTS) -DSQLITE_ENABLE_EXPLAIN_COMMENTS=1
 SHELL_COMPILE_OPTS = $(SHELL_COMPILE_OPTS) -DSQLITE_ENABLE_OFFSET_SQL_FUNC=1
+SHELL_COMPILE_OPTS = $(SHELL_COMPILE_OPTS) -DSQLITE_ENABLE_UNKNOWN_SQL_FUNCTION=1
+SHELL_COMPILE_OPTS = $(SHELL_COMPILE_OPTS) -DSQLITE_ENABLE_STMT_SCANSTATUS=1
 !ENDIF
 
 
diff --git a/sqlite/autoconf/tea/configure.ac b/sqlite/autoconf/tea/configure.ac
index e26780e2..e34bda9f 100644
--- a/sqlite/autoconf/tea/configure.ac
+++ b/sqlite/autoconf/tea/configure.ac
@@ -19,7 +19,7 @@ dnl	to configure the system for the local environment.
 # so that we create the export library with the dll.
 #-----------------------------------------------------------------------
 
-AC_INIT([sqlite],[3.42.0])
+AC_INIT([sqlite],[3.44.0])
 
 #--------------------------------------------------------------------
 # Call TEA_INIT as the first TEA_ macro to set up initial vars.
diff --git a/sqlite/configure b/sqlite/configure
index 29ca76b6..7b31f6f7 100755
--- a/sqlite/configure
+++ b/sqlite/configure
@@ -1,6 +1,6 @@
 #! /bin/sh
 # Guess values for system-dependent variables and create Makefiles.
-# Generated by GNU Autoconf 2.69 for sqlite 3.42.0.
+# Generated by GNU Autoconf 2.69 for sqlite 3.44.0.
 #
 #
 # Copyright (C) 1992-1996, 1998-2012 Free Software Foundation, Inc.
@@ -726,8 +726,8 @@ MAKEFLAGS=
 # Identity of this package.
 PACKAGE_NAME='sqlite'
 PACKAGE_TARNAME='sqlite'
-PACKAGE_VERSION='3.42.0'
-PACKAGE_STRING='sqlite 3.42.0'
+PACKAGE_VERSION='3.44.0'
+PACKAGE_STRING='sqlite 3.44.0'
 PACKAGE_BUGREPORT=''
 PACKAGE_URL=''
 
@@ -776,6 +776,7 @@ OPT_FEATURE_FLAGS
 HAVE_ZLIB
 USE_AMALGAMATION
 TARGET_DEBUG
+TARGET_HAVE_LINENOISE
 TARGET_HAVE_EDITLINE
 TARGET_HAVE_READLINE
 TARGET_READLINE_INC
@@ -903,6 +904,7 @@ enable_editline
 enable_readline
 with_readline_lib
 with_readline_inc
+with_linenoise
 enable_debug
 enable_amalgamation
 enable_load_extension
@@ -1470,7 +1472,7 @@ if test "$ac_init_help" = "long"; then
   # Omit some internal or obsolete options to make the list less imposing.
   # This message is too long to be a string in the A/UX 3.1 sh.
   cat <<_ACEOF
-\`configure' configures sqlite 3.42.0 to adapt to many kinds of systems.
+\`configure' configures sqlite 3.44.0 to adapt to many kinds of systems.
 
 Usage: $0 [OPTION]... [VAR=VALUE]...
 
@@ -1535,7 +1537,7 @@ fi
 
 if test -n "$ac_init_help"; then
   case $ac_init_help in
-     short | recursive ) echo "Configuration of sqlite 3.42.0:";;
+     short | recursive ) echo "Configuration of sqlite 3.44.0:";;
    esac
   cat <<\_ACEOF
 
@@ -1587,6 +1589,7 @@ Optional Packages:
                           (tclConfig.sh)
   --with-readline-lib     specify readline library
   --with-readline-inc     specify readline include paths
+  --with-linenoise=DIR    source directory for linenoise library
 
 Some influential environment variables:
   CC          C compiler command
@@ -1665,7 +1668,7 @@ fi
 test -n "$ac_init_help" && exit $ac_status
 if $ac_init_version; then
   cat <<\_ACEOF
-sqlite configure 3.42.0
+sqlite configure 3.44.0
 generated by GNU Autoconf 2.69
 
 Copyright (C) 2012 Free Software Foundation, Inc.
@@ -2084,7 +2087,7 @@ cat >config.log <<_ACEOF
 This file contains any messages produced by compilers while
 running configure, to aid debugging if configure makes a mistake.
 
-It was created by sqlite $as_me 3.42.0, which was
+It was created by sqlite $as_me 3.44.0, which was
 generated by GNU Autoconf 2.69.  Invocation command line was
 
   $ $0 $@
@@ -3942,13 +3945,13 @@ if ${lt_cv_nm_interface+:} false; then :
 else
   lt_cv_nm_interface="BSD nm"
   echo "int some_variable = 0;" > conftest.$ac_ext
-  (eval echo "\"\$as_me:3945: $ac_compile\"" >&5)
+  (eval echo "\"\$as_me:3948: $ac_compile\"" >&5)
   (eval "$ac_compile" 2>conftest.err)
   cat conftest.err >&5
-  (eval echo "\"\$as_me:3948: $NM \\\"conftest.$ac_objext\\\"\"" >&5)
+  (eval echo "\"\$as_me:3951: $NM \\\"conftest.$ac_objext\\\"\"" >&5)
   (eval "$NM \"conftest.$ac_objext\"" 2>conftest.err > conftest.out)
   cat conftest.err >&5
-  (eval echo "\"\$as_me:3951: output\"" >&5)
+  (eval echo "\"\$as_me:3954: output\"" >&5)
   cat conftest.out >&5
   if $GREP 'External.*some_variable' conftest.out > /dev/null; then
     lt_cv_nm_interface="MS dumpbin"
@@ -5154,7 +5157,7 @@ ia64-*-hpux*)
   ;;
 *-*-irix6*)
   # Find out which ABI we are using.
-  echo '#line 5157 "configure"' > conftest.$ac_ext
+  echo '#line 5160 "configure"' > conftest.$ac_ext
   if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_compile\""; } >&5
   (eval $ac_compile) 2>&5
   ac_status=$?
@@ -6679,11 +6682,11 @@ else
    -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
    -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
    -e 's:$: $lt_compiler_flag:'`
-   (eval echo "\"\$as_me:6682: $lt_compile\"" >&5)
+   (eval echo "\"\$as_me:6685: $lt_compile\"" >&5)
    (eval "$lt_compile" 2>conftest.err)
    ac_status=$?
    cat conftest.err >&5
-   echo "$as_me:6686: \$? = $ac_status" >&5
+   echo "$as_me:6689: \$? = $ac_status" >&5
    if (exit $ac_status) && test -s "$ac_outfile"; then
      # The compiler can only warn and ignore the option if not recognized
      # So say no if there are warnings other than the usual output.
@@ -7018,11 +7021,11 @@ else
    -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
    -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
    -e 's:$: $lt_compiler_flag:'`
-   (eval echo "\"\$as_me:7021: $lt_compile\"" >&5)
+   (eval echo "\"\$as_me:7024: $lt_compile\"" >&5)
    (eval "$lt_compile" 2>conftest.err)
    ac_status=$?
    cat conftest.err >&5
-   echo "$as_me:7025: \$? = $ac_status" >&5
+   echo "$as_me:7028: \$? = $ac_status" >&5
    if (exit $ac_status) && test -s "$ac_outfile"; then
      # The compiler can only warn and ignore the option if not recognized
      # So say no if there are warnings other than the usual output.
@@ -7123,11 +7126,11 @@ else
    -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
    -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
    -e 's:$: $lt_compiler_flag:'`
-   (eval echo "\"\$as_me:7126: $lt_compile\"" >&5)
+   (eval echo "\"\$as_me:7129: $lt_compile\"" >&5)
    (eval "$lt_compile" 2>out/conftest.err)
    ac_status=$?
    cat out/conftest.err >&5
-   echo "$as_me:7130: \$? = $ac_status" >&5
+   echo "$as_me:7133: \$? = $ac_status" >&5
    if (exit $ac_status) && test -s out/conftest2.$ac_objext
    then
      # The compiler can only warn and ignore the option if not recognized
@@ -7178,11 +7181,11 @@ else
    -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
    -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
    -e 's:$: $lt_compiler_flag:'`
-   (eval echo "\"\$as_me:7181: $lt_compile\"" >&5)
+   (eval echo "\"\$as_me:7184: $lt_compile\"" >&5)
    (eval "$lt_compile" 2>out/conftest.err)
    ac_status=$?
    cat out/conftest.err >&5
-   echo "$as_me:7185: \$? = $ac_status" >&5
+   echo "$as_me:7188: \$? = $ac_status" >&5
    if (exit $ac_status) && test -s out/conftest2.$ac_objext
    then
      # The compiler can only warn and ignore the option if not recognized
@@ -9558,7 +9561,7 @@ else
   lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2
   lt_status=$lt_dlunknown
   cat > conftest.$ac_ext <<_LT_EOF
-#line 9561 "configure"
+#line 9564 "configure"
 #include "confdefs.h"
 
 #if HAVE_DLFCN_H
@@ -9654,7 +9657,7 @@ else
   lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2
   lt_status=$lt_dlunknown
   cat > conftest.$ac_ext <<_LT_EOF
-#line 9657 "configure"
+#line 9660 "configure"
 #include "confdefs.h"
 
 #if HAVE_DLFCN_H
@@ -11245,6 +11248,27 @@ fi
 	fi
 fi
 
+# Check whether --with-linenoise was given.
+if test "${with_linenoise+set}" = set; then :
+  withval=$with_linenoise; with_linenoise=$withval
+else
+  with_linenoise="no"
+fi
+
+if test "x$with_linenoise" != "xno"; then
+   TARGET_HAVE_READLINE=0
+   TARGET_HAVE_EDITLINE=0
+   TARGET_HAVE_LINENOISE=1
+   TARGET_READLINE_INC="-I${with_linenoise}"
+   TARGET_READLINE_LIBS="${with_linenoise}/linenoise.c"
+   echo "using linenoise source code at ${with_linenoise}"
+else
+   TARGET_HAVE_LINENOISE=0
+   echo "not using linenoise"
+fi
+
+
+
 
 
 
@@ -11321,7 +11345,7 @@ fi
 { $as_echo "$as_me:${as_lineno-$LINENO}: checking build type" >&5
 $as_echo_n "checking build type... " >&6; }
 if test "${enable_debug}" = "yes" ; then
-  TARGET_DEBUG="-DSQLITE_DEBUG=1 -DSQLITE_ENABLE_SELECTTRACE -DSQLITE_ENABLE_WHERETRACE -O0"
+  TARGET_DEBUG="-DSQLITE_DEBUG=1 -DSQLITE_ENABLE_SELECTTRACE -DSQLITE_ENABLE_WHERETRACE -O0 -Wall"
   { $as_echo "$as_me:${as_lineno-$LINENO}: result: debug" >&5
 $as_echo "debug" >&6; }
 else
@@ -12457,7 +12481,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
 # report actual input values of CONFIG_FILES etc. instead of their
 # values after options handling.
 ac_log="
-This file was extended by sqlite $as_me 3.42.0, which was
+This file was extended by sqlite $as_me 3.44.0, which was
 generated by GNU Autoconf 2.69.  Invocation command line was
 
   CONFIG_FILES    = $CONFIG_FILES
@@ -12523,7 +12547,7 @@ _ACEOF
 cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
 ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`"
 ac_cs_version="\\
-sqlite config.status 3.42.0
+sqlite config.status 3.44.0
 configured by $0, generated by GNU Autoconf 2.69,
   with options \\"\$ac_cs_config\\"
 
diff --git a/sqlite/configure.ac b/sqlite/configure.ac
index 53be0a68..837d2fb0 100644
--- a/sqlite/configure.ac
+++ b/sqlite/configure.ac
@@ -598,11 +598,28 @@ if test x"$with_readline" != xno; then
 		TARGET_HAVE_READLINE=1
 	fi
 fi
+AC_ARG_WITH([linenoise],
+            [AS_HELP_STRING([--with-linenoise=DIR],[source directory for linenoise library])],
+            [with_linenoise=$withval],
+            [with_linenoise="no"])
+if test "x$with_linenoise" != "xno"; then
+   TARGET_HAVE_READLINE=0
+   TARGET_HAVE_EDITLINE=0
+   TARGET_HAVE_LINENOISE=1
+   TARGET_READLINE_INC="-I${with_linenoise}"
+   TARGET_READLINE_LIBS="${with_linenoise}/linenoise.c"
+   echo "using linenoise source code at ${with_linenoise}"
+else
+   TARGET_HAVE_LINENOISE=0
+   echo "not using linenoise"
+fi
 
 AC_SUBST(TARGET_READLINE_LIBS)
 AC_SUBST(TARGET_READLINE_INC)
 AC_SUBST(TARGET_HAVE_READLINE)
 AC_SUBST(TARGET_HAVE_EDITLINE)
+AC_SUBST(TARGET_HAVE_LINENOISE)
+
 
 ##########
 # Figure out what C libraries are required to compile programs
@@ -615,7 +632,7 @@ AC_SEARCH_LIBS(fdatasync, [rt])
 AC_ARG_ENABLE(debug, AS_HELP_STRING([--enable-debug],[enable debugging & verbose explain]))
 AC_MSG_CHECKING([build type])
 if test "${enable_debug}" = "yes" ; then
-  TARGET_DEBUG="-DSQLITE_DEBUG=1 -DSQLITE_ENABLE_SELECTTRACE -DSQLITE_ENABLE_WHERETRACE -O0"
+  TARGET_DEBUG="-DSQLITE_DEBUG=1 -DSQLITE_ENABLE_SELECTTRACE -DSQLITE_ENABLE_WHERETRACE -O0 -Wall"
   AC_MSG_RESULT([debug])
 else
   TARGET_DEBUG="-DNDEBUG"
diff --git a/sqlite/doc/compile-for-windows.md b/sqlite/doc/compile-for-windows.md
new file mode 100644
index 00000000..20f79afa
--- /dev/null
+++ b/sqlite/doc/compile-for-windows.md
@@ -0,0 +1,136 @@
+# Notes On Compiling SQLite On Windows 11
+
+Here are step-by-step instructions on how to build SQLite from
+canonical source on a new Windows 11 PC, as of 2023-08-16:
+
+  1.  Install Microsoft Visual Studio. The free "community edition" 
+      will work fine.  Do a standard install for C++ development.
+      SQLite only needs the
+      "cl" compiler and the "nmake" build tool.
+
+  2.  Under the "Start" menu, find "All Apps" then go to "Visual Studio 20XX"
+      and find "x64 Native Tools Command Prompt for VS 20XX".  Pin that
+      application to your task bar, as you will use it a lot.  Bring up
+      an instance of this command prompt and do all of the subsequent steps
+      in that "x64 Native Tools" command prompt.  (Or use "x86" if you want
+      a 32-bit build.)  The subsequent steps will not work in a vanilla
+      DOS prompt.  Nor will they work in PowerShell.
+
+  3.  Install TCL development libraries.  This note assumes that you will
+      install the TCL development libraries in the "`c:\Tcl`" directory.
+      Make adjustments
+      if you want TCL installed somewhere else.  SQLite needs both the
+      "tclsh.exe" command-line tool as part of the build process, and
+      the "tcl86.lib" library in order to run tests.  You will need
+      TCL version 8.6 or later.
+      
+      - Get the TCL source archive, perhaps from
+      [https://www.tcl.tk/software/tcltk/download.html](https://www.tcl.tk/software/tcltk/download.html).
+      
- Untar or unzip the source archive.  CD into the "win/" subfolder
+          of the source tree.
+      
- Run: `nmake /f makefile.vc release`
+      
- Run: `nmake /f makefile.vc INSTALLDIR=c:\Tcl install`
+      
- CD to `c:\Tcl\lib`.  In that subfolder make a copy of the
+          "`tcl86t.lib`" file to the alternative name "`tcl86.lib`"
+          (omitting the second 't').  Leave the copy in the same directory
+          as the original.
+      
- CD to `c:\Tcl\bin`.  Make a copy of the "`tclsh86t.exe`"
+          file into "`tclsh.exe`" (without the "86t") in the same directory.
+      
- Add `c:\Tcl\bin` to your %PATH%.  To do this, go to Settings
+          and search for "path".  Select "edit environment variables for
+          your account" and modify your default PATH accordingly.
+          You will need to close and reopen your command prompts after
+          making this change.
+      
+
+  4.  Download the SQLite source tree and unpack it. CD into the
+      toplevel directory of the source tree.
+
+  5.  Set the TCLDIR environment variable to point to your TCL installation.
+      Like this:
+      
+
+  6.  Run the "`Makefile.msc`" makefile with an appropriate target.
+      Examples:
+
+      -   `nmake /f makefile.msc`
+      
-   `nmake /f makefile.msc sqlite3.c`
+      
-   `nmake /f makefile.msc devtest`
+      
-   `nmake /f makefile.msc releasetest`
+      
+
+## 32-bit Builds
+
+Doing a 32-bit build is just like doing a 64-bit build with the
+following minor changes:
+
+  1.  Use the "x86 Native Tools Command Prompt" instead of
+      "x64 Native Tools Command Prompt".  "**x86**" instead of "**x64**".
+
+  2.  Use a different installation directory for TCL.
+      The recommended directory is `c:\tcl32`.  Thus you end up
+      with two TCL builds:
+
+      -  `c:\tcl` ←  64-bit (the default)
+      
-  `c:\tcl32` ←  32-bit
+      
+
+  3.  Ensure that `c:\tcl32\bin` comes before `c:\tcl\bin` on
+      your PATH environment variable.  You can achieve this using
+      a command like:
+
+      -   `set PATH=c:\tcl32\bin;%PATH%`
+      
+
+## Statically Linking The TCL Library
+
+Some utility programs associated with SQLite need to be linked
+with TCL in order to function.  The [sqlite3_analyzer.exe program](https://sqlite.org/sqlanalyze.html)
+is an example.  You can build as described above, and then
+enter:
+
+> ~~~~
+nmake /f Makefile.msc sqlite3_analyzer.exe
+~~~~
+
+And you will end up with a working executable.  However, that executable
+will depend on having the "tcl86.dll" library somewhere on your %PATH%.
+Use the following steps to build an executable that has the TCL library
+statically linked so that it does not depend on separate DLL:
+
+  1.  Use the appropriate "Command Prompt" window - either x86 or
+      x64, depending on whether you want a 32-bit or 64-bit executable.
+
+  2.  Untar the TCL source tarball into a fresh directory.  CD into
+      the "win/" subfolder.
+
+  3.  Run: `nmake /f makefile.vc OPTS=nothreads,static shell`
+
+
+  4.  CD into the "Release*" subfolder that is created (note the
+      wildcard - the full name of the directory might vary).  There
+      you will find the "tcl86s.lib" file.  Copy this file into the
+      same directory that you put the "tcl86.lib" on your initial
+      installation.  (In this document, that directory is
+      "`C:\Tcl32\lib`" for 32-bit builds and
+      "`C:\Tcl\lib`" for 64-bit builds.)
+
+  5.  CD into your SQLite source code directory and build the desired
+      utility program, but add the following extra arguments to the
+      nmake command line:
+
+      CCOPTS="-DSTATIC_BUILD" LIBTCL="tcl86s.lib netapi32.lib user32.lib"
+      
+      So, for example, to build a statically linked version of
+      sqlite3_analyzer.exe, you might type:
+      
+      nmake /f Makefile.msc CCOPTS="-DSTATIC_BUILD" LIBTCL="tcl86s.lib netapi32.lib user32.lib" sqlite3_analyzer.exe
+      
+
+  6.  After your executable is built, you can verify that it does not
+      depend on the TCL DLL by running:
+      
+      dumpbin /dependents sqlite3_analyzer.exe
+      
diff --git a/sqlite/doc/lemon.html b/sqlite/doc/lemon.html
index 16aea878..66665f46 100644
--- a/sqlite/doc/lemon.html
+++ b/sqlite/doc/lemon.html
@@ -1229,8 +1229,8 @@ 6.0 History of Lemon
 
 Lemon was originally written by Richard Hipp sometime in the late
 1980s on a Sun4 Workstation using K&R C.  
-There was a companion LL(1) parser generator program named "Lime", the
-source code to which as been lost.
+There was a companion LL(1) parser generator program named "Lime".
+The Lime source code has been lost.
 
 The lemon.c source file was originally many separate files that were
 compiled together to generate the "lemon" executable.  Sometime in the
diff --git a/sqlite/doc/testrunner.md b/sqlite/doc/testrunner.md
new file mode 100644
index 00000000..d828fd76
--- /dev/null
+++ b/sqlite/doc/testrunner.md
@@ -0,0 +1,284 @@
+
+
+# The testrunner.tcl Script
+
+# 1. Overview
+
+testrunner.tcl is a Tcl script used to run multiple SQLite tests using 
+multiple jobs. It supports the following types of tests:
+
+  *  Tcl test scripts.
+
+  *  Tests run with [make] commands. Specifically, at time of writing, 
+     [make fuzztest], [make mptest], [make sourcetest] and [make threadtest].
+
+testrunner.tcl pipes the output of all tests and builds run into log file
+**testrunner.log**, created in the cwd directory. Searching this file for
+"failed" is a good way to find the output of a failed test.
+
+testrunner.tcl also populates SQLite database **testrunner.db**. This database
+contains details of all tests run, running and to be run. A useful query
+might be:
+
+```
+  SELECT * FROM script WHERE state='failed'
+```
+
+Running the command:
+
+```
+  ./testfixture $(TESTDIR)/testrunner.tcl status
+```
+
+in the directory containing the testrunner.db database runs various queries
+to produce a succinct report on the state of a running testrunner.tcl script.
+Running:
+
+```
+  watch ./testfixture $(TESTDIR)/testrunner.tcl status
+```
+
+in another terminal is a good way to keep an eye on a long running test.
+
+Sometimes testrunner.tcl uses the [testfixture] binary that it is run with
+to run tests (see "Binary Tests" below). Sometimes it builds testfixture and
+other binaries in specific configurations to test (see "Source Tests").
+
+# 2. Binary Tests
+
+The commands described in this section all run various combinations of the Tcl
+test scripts using the [testfixture] binary used to run the testrunner.tcl
+script (i.e. they do not invoke the compiler to build new binaries, or the
+[make] command to run tests that are not Tcl scripts). The procedure to run
+these tests is therefore:
+
+  1. Build the "testfixture" (or "testfixture.exe" for windows) binary using
+     whatever method seems convenient.
+
+  2. Test the binary built in step 1 by running testrunner.tcl with it, 
+     perhaps with various options.
+
+The following sub-sections describe the various options that can be
+passed to testrunner.tcl to test binary testfixture builds.
+
+## 2.1. Organization of Tcl Tests
+
+Tcl tests are stored in files that match the pattern *\*.test*. They are
+found in both the $TOP/test/ directory, and in the various sub-directories
+of the $TOP/ext/ directory of the source tree. Not all *\*.test* files
+contain Tcl tests - a handful are Tcl scripts designed to invoke other
+*\*.test* files.
+
+The **veryquick** set of tests is a subset of all Tcl test scripts in the
+source tree. In includes most tests, but excludes some that are very slow.
+Almost all fault-injection tests (those that test the response of the library
+to OOM or IO errors) are excluded. It is defined in source file 
+*test/permutations.test*.
+
+The **full** set of tests includes all Tcl test scripts in the source tree.
+To run a "full" test is to run all Tcl test scripts that can be found in the
+source tree.
+
+File *permutations.test* defines various test "permutations". A permutation
+consists of:
+
+  *  A subset of Tcl test scripts, and 
+
+  *  Runtime configuration to apply before running each test script 
+     (e.g. enabling auto-vacuum, or disable lookaside).
+
+Running **all** tests is to run all tests in the full test set, plus a dozen
+or so permutations. The specific permutations that are run as part of "all"
+are defined in file *testrunner_data.tcl*.
+
+## 2.2. Commands to Run Tests
+
+To run the "veryquick" test set, use either of the following:
+
+```
+  ./testfixture $TESTDIR/testrunner.tcl
+  ./testfixture $TESTDIR/testrunner.tcl veryquick
+```
+
+To run the "full" test suite:
+
+```
+  ./testfixture $TESTDIR/testrunner.tcl full
+```
+
+To run the subset of the "full" test suite for which the test file name matches
+a specified pattern (e.g. all tests that start with "fts5"), either of:
+
+```
+  ./testfixture $TESTDIR/testrunner.tcl fts5%
+  ./testfixture $TESTDIR/testrunner.tcl 'fts5*'
+```
+
+To run "all" tests (full + permutations):
+
+```
+  ./testfixture $TESTDIR/testrunner.tcl all
+```
+
+
+## 2.3. Investigating Binary Test Failures
+
+If a test fails, testrunner.tcl reports name of the Tcl test script and, if
+applicable, the name of the permutation, to stdout. This information can also
+be retrieved from either *testrunner.log* or *testrunner.db*.
+
+If there is no permutation, the individual test script may be run with:
+
+```
+  ./testfixture $PATH_TO_SCRIPT
+```
+
+Or, if the failure occured as part of a permutation:
+
+```
+  ./testfixture $TESTDIR/testrunner.tcl $PERMUTATION $PATH_TO_SCRIPT
+```
+
+TODO: An example instead of "$PERMUTATION" and $PATH\_TO\_SCRIPT?
+
+# 3. Source Code Tests
+
+The commands described in this section invoke the C compiler to build 
+binaries from the source tree, then use those binaries to run Tcl and
+other tests. The advantages of this are that:
+
+  *  it is possible to test multiple build configurations with a single
+     command, and 
+
+  *  it ensures that tests are always run using binaries created with the
+     same set of compiler options.
+
+The testrunner.tcl commands described in this section may be run using
+either a *testfixture* (or testfixture.exe) build, or with any other Tcl
+shell that supports SQLite 3.31.1 or newer via "package require sqlite3".
+
+TODO: ./configure + Makefile.msc build systems.
+
+## Commands to Run SQLite Tests
+
+The **mdevtest** command is equivalent to running the veryquick tests and
+the [make fuzztest] target once for each of two --enable-all builds - one 
+with debugging enabled and one without:
+
+```
+  tclsh $TESTDIR/testrunner.tcl mdevtest
+```
+
+In other words, it is equivalent to running:
+
+```
+  $TOP/configure --enable-all --enable-debug
+  make fuzztest
+  make testfixture
+  ./testfixture $TOP/test/testrunner.tcl veryquick
+
+  # Then, after removing files created by the tests above:
+  $TOP/configure --enable-all OPTS="-O0"
+  make fuzztest
+  make testfixture
+  ./testfixture $TOP/test/testrunner.tcl veryquick
+```
+
+The **sdevtest** command is identical to the mdevtest command, except that the
+second of the two builds is a sanitizer build. Specifically, this means that
+OPTS="-fsanitize=address,undefined" is specified instead of OPTS="-O0":
+
+```
+  tclsh $TESTDIR/testrunner.tcl sdevtest
+```
+
+The **release** command runs lots of tests under lots of builds. It runs
+different combinations of builds and tests depending on whether it is run
+on Linux, Windows or OSX. Refer to *testrunner\_data.tcl* for the details
+of the specific tests run.
+
+```
+  tclsh $TESTDIR/testrunner.tcl release
+```
+
+## Running ZipVFS Tests
+
+testrunner.tcl can build a zipvfs-enabled testfixture and use it to run
+tests from the Zipvfs project with the following command:
+
+```
+  tclsh $TESTDIR/testrunner.tcl --zipvfs $PATH_TO_ZIPVFS
+```
+
+This can be combined with any of "mdevtest", "sdevtest" or "release" to
+test both SQLite and Zipvfs with a single command:
+
+```
+  tclsh $TESTDIR/testrunner.tcl --zipvfs $PATH_TO_ZIPVFS mdevtest
+```
+
+## Investigating Source Code Test Failures
+
+Investigating a test failure that occurs during source code testing is a
+two step process:
+
+  1. Recreating the build configuration in which the test failed, and
+
+  2. Re-running the actual test.
+
+To recreate a build configuration, use the testrunner.tcl **script** command
+to create a build script. A build script is a bash script on Linux or OSX, or
+a dos \*.bat file on windows. For example:
+
+```
+  # Create a script that recreates build configuration "Device-One" on 
+  # Linux or OSX:
+  tclsh $TESTDIR/testrunner.tcl script Device-One > make.sh 
+
+  # Create a script that recreates build configuration "Have-Not" on Windows:
+  tclsh $TESTDIR/testrunner.tcl script Have-Not > make.bat 
+```
+
+The generated bash or \*.bat file script accepts a single argument - a makefile
+target to build. This may be used either to run a [make] command test directly,
+or else to build a testfixture (or testfixture.exe) binary with which to
+run a Tcl test script, as described above.
+
+
+
+# 4. Controlling CPU Core Utilization
+
+When running either binary or source code tests, testrunner.tcl reports the
+number of jobs it intends to use to stdout. e.g.
+
+```
+  $ ./testfixture $TESTDIR/testrunner.tcl
+  splitting work across 16 jobs
+  ... more output ...
+```
+
+By default, testfixture.tcl attempts to set the number of jobs to the number 
+of real cores on the machine. This can be overridden using the "--jobs" (or -j)
+switch:
+
+```
+  $ ./testfixture $TESTDIR/testrunner.tcl --jobs 8
+  splitting work across 8 jobs
+  ... more output ...
+```
+
+The number of jobs may also be changed while an instance of testrunner.tcl is
+running by exucuting the following command from the directory containing the
+testrunner.log and testrunner.db files:
+
+```
+  $ ./testfixture $TESTDIR/testrunner.tcl njob $NEW_NUMBER_OF_JOBS
+```
+
+
+
+
+
+
+
+
diff --git a/sqlite/ext/expert/expert1.test b/sqlite/ext/expert/expert1.test
index dee4eb9e..45333423 100644
--- a/sqlite/ext/expert/expert1.test
+++ b/sqlite/ext/expert/expert1.test
@@ -464,4 +464,23 @@ do_execsql_test 5.3 {
   t2 t2_idx_0001295b {100 20 5}
 }
 
+if 0 {
+do_test expert1-6.0 {
+  catchcmd :memory: {
+.expert
+select base64('');
+.expert
+select name from pragma_collation_list order by name collate uint;
+}
+} {0 {(no new indexes)
+
+SCAN CONSTANT ROW
+
+(no new indexes)
+
+SCAN pragma_collation_list VIRTUAL TABLE INDEX 0:
+USE TEMP B-TREE FOR ORDER BY
+}}
+}
+
 finish_test
diff --git a/sqlite/ext/expert/sqlite3expert.c b/sqlite/ext/expert/sqlite3expert.c
index c01feff5..33d62226 100644
--- a/sqlite/ext/expert/sqlite3expert.c
+++ b/sqlite/ext/expert/sqlite3expert.c
@@ -32,7 +32,7 @@
 #endif /* !defined(SQLITE_AMALGAMATION) */
 
 
-#ifndef SQLITE_OMIT_VIRTUALTABLE 
+#ifndef SQLITE_OMIT_VIRTUALTABLE
 
 typedef sqlite3_int64 i64;
 typedef sqlite3_uint64 u64;
@@ -662,6 +662,7 @@ static int idxRegisterVtab(sqlite3expert *p){
     0,                            /* xRelease */
     0,                            /* xRollbackTo */
     0,                            /* xShadowName */
+    0,                            /* xIntegrity */
   };
 
   return sqlite3_create_module(p->dbv, "expert", &expertModule, (void*)p);
@@ -1818,6 +1819,88 @@ static int idxPopulateStat1(sqlite3expert *p, char **pzErr){
   return rc;
 }
 
+/*
+** Define and possibly pretend to use a useless collation sequence.
+** This pretense allows expert to accept SQL using custom collations.
+*/
+int dummyCompare(void *up1, int up2, const void *up3, int up4, const void *up5){
+  (void)up1;
+  (void)up2;
+  (void)up3;
+  (void)up4;
+  (void)up5;
+  assert(0); /* VDBE should never be run. */
+  return 0;
+}
+/* And a callback to register above upon actual need */
+void useDummyCS(void *up1, sqlite3 *db, int etr, const char *zName){
+  (void)up1;
+  sqlite3_create_collation_v2(db, zName, etr, 0, dummyCompare, 0);
+}
+
+#if !defined(SQLITE_OMIT_SCHEMA_PRAGMAS) \
+  && !defined(SQLITE_OMIT_INTROSPECTION_PRAGMAS)
+/*
+** dummy functions for no-op implementation of UDFs during expert's work
+*/
+void dummyUDF(sqlite3_context *up1, int up2, sqlite3_value **up3){
+  (void)up1;
+  (void)up2;
+  (void)up3;
+  assert(0); /* VDBE should never be run. */
+}
+void dummyUDFvalue(sqlite3_context *up1){
+  (void)up1;
+  assert(0); /* VDBE should never be run. */
+}
+
+/*
+** Register UDFs from user database with another.
+*/
+int registerUDFs(sqlite3 *dbSrc, sqlite3 *dbDst){
+  sqlite3_stmt *pStmt;
+  int rc = sqlite3_prepare_v2(dbSrc,
+            "SELECT name,type,enc,narg,flags "
+            "FROM pragma_function_list() "
+            "WHERE builtin==0", -1, &pStmt, 0);
+  if( rc==SQLITE_OK ){
+    while( SQLITE_ROW==(rc = sqlite3_step(pStmt)) ){
+      int nargs = sqlite3_column_int(pStmt,3);
+      int flags = sqlite3_column_int(pStmt,4);
+      const char *name = (char*)sqlite3_column_text(pStmt,0);
+      const char *type = (char*)sqlite3_column_text(pStmt,1);
+      const char *enc = (char*)sqlite3_column_text(pStmt,2);
+      if( name==0 || type==0 || enc==0 ){
+        /* no-op.  Only happens on OOM */
+      }else{
+        int ienc = SQLITE_UTF8;
+        int rcf = SQLITE_ERROR;
+        if( strcmp(enc,"utf16le")==0 ) ienc = SQLITE_UTF16LE;
+        else if( strcmp(enc,"utf16be")==0 ) ienc = SQLITE_UTF16BE;
+        ienc |= (flags & (SQLITE_DETERMINISTIC|SQLITE_DIRECTONLY));
+        if( strcmp(type,"w")==0 ){
+          rcf = sqlite3_create_window_function(dbDst,name,nargs,ienc,0,
+                                               dummyUDF,dummyUDFvalue,0,0,0);
+        }else if( strcmp(type,"a")==0 ){
+          rcf = sqlite3_create_function(dbDst,name,nargs,ienc,0,
+                                        0,dummyUDF,dummyUDFvalue);
+        }else if( strcmp(type,"s")==0 ){
+          rcf = sqlite3_create_function(dbDst,name,nargs,ienc,0,
+                                        dummyUDF,0,0);
+        }
+        if( rcf!=SQLITE_OK ){
+          rc = rcf;
+          break;
+        }
+      }
+    }
+    sqlite3_finalize(pStmt);
+    if( rc==SQLITE_DONE ) rc = SQLITE_OK;
+  }
+  return rc;
+}
+#endif
+
 /*
 ** Allocate a new sqlite3expert object.
 */
@@ -1844,7 +1927,21 @@ sqlite3expert *sqlite3_expert_new(sqlite3 *db, char **pzErrmsg){
       sqlite3_db_config(pNew->dbm, SQLITE_DBCONFIG_TRIGGER_EQP, 1, (int*)0);
     }
   }
-  
+
+  /* Allow custom collations to be dealt with through prepare. */
+  if( rc==SQLITE_OK ) rc = sqlite3_collation_needed(pNew->dbm,0,useDummyCS);
+  if( rc==SQLITE_OK ) rc = sqlite3_collation_needed(pNew->dbv,0,useDummyCS);
+
+#if !defined(SQLITE_OMIT_SCHEMA_PRAGMAS) \
+  && !defined(SQLITE_OMIT_INTROSPECTION_PRAGMAS)
+  /* Register UDFs from database [db] with [dbm] and [dbv]. */
+  if( rc==SQLITE_OK ){
+    rc = registerUDFs(pNew->db, pNew->dbm);
+  }
+  if( rc==SQLITE_OK ){
+    rc = registerUDFs(pNew->db, pNew->dbv);
+  }
+#endif
 
   /* Copy the entire schema of database [db] into [dbm]. */
   if( rc==SQLITE_OK ){
@@ -1920,6 +2017,10 @@ int sqlite3_expert_sql(
 
   while( rc==SQLITE_OK && zStmt && zStmt[0] ){
     sqlite3_stmt *pStmt = 0;
+    /* Ensure that the provided statement compiles against user's DB. */
+    rc = idxPrepareStmt(p->db, &pStmt, pzErr, zStmt);
+    if( rc!=SQLITE_OK ) break;
+    sqlite3_finalize(pStmt);
     rc = sqlite3_prepare_v2(p->dbv, zStmt, -1, &pStmt, &zStmt);
     if( rc==SQLITE_OK ){
       if( pStmt ){
diff --git a/sqlite/ext/fts3/fts3.c b/sqlite/ext/fts3/fts3.c
index 43a9daf6..65852804 100644
--- a/sqlite/ext/fts3/fts3.c
+++ b/sqlite/ext/fts3/fts3.c
@@ -640,6 +640,7 @@ static void fts3DeclareVtab(int *pRc, Fts3Table *p){
 
     zLanguageid = (p->zLanguageid ? p->zLanguageid : "__langid");
     sqlite3_vtab_config(p->db, SQLITE_VTAB_CONSTRAINT_SUPPORT, 1);
+    sqlite3_vtab_config(p->db, SQLITE_VTAB_INNOCUOUS);
 
     /* Create a list of user columns for the virtual table */
     zCols = sqlite3_mprintf("%Q, ", p->azColumn[0]);
@@ -3889,6 +3890,8 @@ static int fts3RenameMethod(
     rc = sqlite3Fts3PendingTermsFlush(p);
   }
 
+  p->bIgnoreSavepoint = 1;
+
   if( p->zContentTbl==0 ){
     fts3DbExec(&rc, db,
       "ALTER TABLE %Q.'%q_content'  RENAME TO '%q_content';",
@@ -3916,6 +3919,8 @@ static int fts3RenameMethod(
     "ALTER TABLE %Q.'%q_segdir'   RENAME TO '%q_segdir';",
     p->zDb, p->zName, zName
   );
+
+  p->bIgnoreSavepoint = 0;
   return rc;
 }
 
@@ -3926,12 +3931,28 @@ static int fts3RenameMethod(
 */
 static int fts3SavepointMethod(sqlite3_vtab *pVtab, int iSavepoint){
   int rc = SQLITE_OK;
-  UNUSED_PARAMETER(iSavepoint);
-  assert( ((Fts3Table *)pVtab)->inTransaction );
-  assert( ((Fts3Table *)pVtab)->mxSavepoint <= iSavepoint );
-  TESTONLY( ((Fts3Table *)pVtab)->mxSavepoint = iSavepoint );
-  if( ((Fts3Table *)pVtab)->bIgnoreSavepoint==0 ){
-    rc = fts3SyncMethod(pVtab);
+  Fts3Table *pTab = (Fts3Table*)pVtab;
+  assert( pTab->inTransaction );
+  assert( pTab->mxSavepoint<=iSavepoint );
+  TESTONLY( pTab->mxSavepoint = iSavepoint );
+
+  if( pTab->bIgnoreSavepoint==0 ){
+    if( fts3HashCount(&pTab->aIndex[0].hPending)>0 ){
+      char *zSql = sqlite3_mprintf("INSERT INTO %Q.%Q(%Q) VALUES('flush')",
+          pTab->zDb, pTab->zName, pTab->zName
+          );
+      if( zSql ){
+        pTab->bIgnoreSavepoint = 1;
+        rc = sqlite3_exec(pTab->db, zSql, 0, 0, 0);
+        pTab->bIgnoreSavepoint = 0;
+        sqlite3_free(zSql);
+      }else{
+        rc = SQLITE_NOMEM;
+      }
+    }
+    if( rc==SQLITE_OK ){
+      pTab->iSavepoint = iSavepoint+1;
+    }
   }
   return rc;
 }
@@ -3942,12 +3963,11 @@ static int fts3SavepointMethod(sqlite3_vtab *pVtab, int iSavepoint){
 ** This is a no-op.
 */
 static int fts3ReleaseMethod(sqlite3_vtab *pVtab, int iSavepoint){
-  TESTONLY( Fts3Table *p = (Fts3Table*)pVtab );
-  UNUSED_PARAMETER(iSavepoint);
-  UNUSED_PARAMETER(pVtab);
-  assert( p->inTransaction );
-  assert( p->mxSavepoint >= iSavepoint );
-  TESTONLY( p->mxSavepoint = iSavepoint-1 );
+  Fts3Table *pTab = (Fts3Table*)pVtab;
+  assert( pTab->inTransaction );
+  assert( pTab->mxSavepoint >= iSavepoint );
+  TESTONLY( pTab->mxSavepoint = iSavepoint-1 );
+  pTab->iSavepoint = iSavepoint;
   return SQLITE_OK;
 }
 
@@ -3957,11 +3977,13 @@ static int fts3ReleaseMethod(sqlite3_vtab *pVtab, int iSavepoint){
 ** Discard the contents of the pending terms table.
 */
 static int fts3RollbackToMethod(sqlite3_vtab *pVtab, int iSavepoint){
-  Fts3Table *p = (Fts3Table*)pVtab;
+  Fts3Table *pTab = (Fts3Table*)pVtab;
   UNUSED_PARAMETER(iSavepoint);
-  assert( p->inTransaction );
-  TESTONLY( p->mxSavepoint = iSavepoint );
-  sqlite3Fts3PendingTermsClear(p);
+  assert( pTab->inTransaction );
+  TESTONLY( pTab->mxSavepoint = iSavepoint );
+  if( (iSavepoint+1)<=pTab->iSavepoint ){
+    sqlite3Fts3PendingTermsClear(pTab);
+  }
   return SQLITE_OK;
 }
 
@@ -3980,8 +4002,49 @@ static int fts3ShadowName(const char *zName){
   return 0;
 }
 
+/*
+** Implementation of the xIntegrity() method on the FTS3/FTS4 virtual
+** table.
+*/
+static int fts3Integrity(
+  sqlite3_vtab *pVtab,      /* The virtual table to be checked */
+  const char *zSchema,      /* Name of schema in which pVtab lives */
+  const char *zTabname,     /* Name of the pVTab table */
+  int isQuick,              /* True if this is a quick_check */
+  char **pzErr              /* Write error message here */
+){
+  Fts3Table *p = (Fts3Table*)pVtab;
+  char *zSql;
+  int rc;
+  char *zErr = 0;
+
+  assert( pzErr!=0 );
+  assert( *pzErr==0 );
+  UNUSED_PARAMETER(isQuick);
+  zSql = sqlite3_mprintf(
+            "INSERT INTO \"%w\".\"%w\"(\"%w\") VALUES('integrity-check');",
+            zSchema, zTabname, zTabname);
+  if( zSql==0 ){
+    return SQLITE_NOMEM;
+  }
+  rc = sqlite3_exec(p->db, zSql, 0, 0, &zErr);
+  sqlite3_free(zSql);
+  if( (rc&0xff)==SQLITE_CORRUPT ){
+    *pzErr = sqlite3_mprintf("malformed inverted index for FTS%d table %s.%s",
+                p->bFts4 ? 4 : 3, zSchema, zTabname);
+  }else if( rc!=SQLITE_OK ){
+    *pzErr = sqlite3_mprintf("unable to validate the inverted index for"
+                             " FTS%d table %s.%s: %s",
+                p->bFts4 ? 4 : 3, zSchema, zTabname, zErr);
+  }
+  sqlite3_free(zErr);
+  return SQLITE_OK;
+}
+
+
+
 static const sqlite3_module fts3Module = {
-  /* iVersion      */ 3,
+  /* iVersion      */ 4,
   /* xCreate       */ fts3CreateMethod,
   /* xConnect      */ fts3ConnectMethod,
   /* xBestIndex    */ fts3BestIndexMethod,
@@ -4005,6 +4068,7 @@ static const sqlite3_module fts3Module = {
   /* xRelease      */ fts3ReleaseMethod,
   /* xRollbackTo   */ fts3RollbackToMethod,
   /* xShadowName   */ fts3ShadowName,
+  /* xIntegrity    */ fts3Integrity,
 };
 
 /*
diff --git a/sqlite/ext/fts3/fts3Int.h b/sqlite/ext/fts3/fts3Int.h
index 3a8a884f..5a5b123b 100644
--- a/sqlite/ext/fts3/fts3Int.h
+++ b/sqlite/ext/fts3/fts3Int.h
@@ -265,6 +265,7 @@ struct Fts3Table {
   int nPgsz;                      /* Page size for host database */
   char *zSegmentsTbl;             /* Name of %_segments table */
   sqlite3_blob *pSegments;        /* Blob handle open on %_segments table */
+  int iSavepoint;
 
   /* 
   ** The following array of hash tables is used to buffer pending index 
diff --git a/sqlite/ext/fts3/fts3_aux.c b/sqlite/ext/fts3/fts3_aux.c
index d3b194c9..439d5793 100644
--- a/sqlite/ext/fts3/fts3_aux.c
+++ b/sqlite/ext/fts3/fts3_aux.c
@@ -545,7 +545,8 @@ int sqlite3Fts3InitAux(sqlite3 *db){
      0,                           /* xSavepoint    */
      0,                           /* xRelease      */
      0,                           /* xRollbackTo   */
-     0                            /* xShadowName   */
+     0,                           /* xShadowName   */
+     0                            /* xIntegrity    */
   };
   int rc;                         /* Return code */
 
diff --git a/sqlite/ext/fts3/fts3_term.c b/sqlite/ext/fts3/fts3_term.c
index 47e244e2..f3a9746a 100644
--- a/sqlite/ext/fts3/fts3_term.c
+++ b/sqlite/ext/fts3/fts3_term.c
@@ -362,7 +362,8 @@ int sqlite3Fts3InitTerm(sqlite3 *db){
      0,                           /* xSavepoint    */
      0,                           /* xRelease      */
      0,                           /* xRollbackTo   */
-     0                            /* xShadowName   */
+     0,                           /* xShadowName   */
+     0                            /* xIntegrity    */
   };
   int rc;                         /* Return code */
 
diff --git a/sqlite/ext/fts3/fts3_tokenize_vtab.c b/sqlite/ext/fts3/fts3_tokenize_vtab.c
index 65d7eef4..7e8d09bd 100644
--- a/sqlite/ext/fts3/fts3_tokenize_vtab.c
+++ b/sqlite/ext/fts3/fts3_tokenize_vtab.c
@@ -445,7 +445,8 @@ int sqlite3Fts3InitTok(sqlite3 *db, Fts3Hash *pHash, void(*xDestroy)(void*)){
      0,                           /* xSavepoint    */
      0,                           /* xRelease      */
      0,                           /* xRollbackTo   */
-     0                            /* xShadowName   */
+     0,                           /* xShadowName   */
+     0                            /* xIntegrity    */
   };
   int rc;                         /* Return code */
 
diff --git a/sqlite/ext/fts3/fts3_write.c b/sqlite/ext/fts3/fts3_write.c
index 393f8a87..0f894943 100644
--- a/sqlite/ext/fts3/fts3_write.c
+++ b/sqlite/ext/fts3/fts3_write.c
@@ -3325,7 +3325,6 @@ int sqlite3Fts3PendingTermsFlush(Fts3Table *p){
     rc = fts3SegmentMerge(p, p->iPrevLangid, i, FTS3_SEGCURSOR_PENDING);
     if( rc==SQLITE_DONE ) rc = SQLITE_OK;
   }
-  sqlite3Fts3PendingTermsClear(p);
 
   /* Determine the auto-incr-merge setting if unknown.  If enabled,
   ** estimate the number of leaf blocks of content to be written
@@ -3347,6 +3346,10 @@ int sqlite3Fts3PendingTermsFlush(Fts3Table *p){
       rc = sqlite3_reset(pStmt);
     }
   }
+
+  if( rc==SQLITE_OK ){
+    sqlite3Fts3PendingTermsClear(p);
+  }
   return rc;
 }
 
@@ -3978,6 +3981,8 @@ static int fts3AppendToNode(
 
   blobGrowBuffer(pPrev, nTerm, &rc);
   if( rc!=SQLITE_OK ) return rc;
+  assert( pPrev!=0 );
+  assert( pPrev->a!=0 );
 
   nPrefix = fts3PrefixCompress(pPrev->a, pPrev->n, zTerm, nTerm);
   nSuffix = nTerm - nPrefix;
@@ -4034,9 +4039,13 @@ static int fts3IncrmergeAppend(
   nSpace += sqlite3Fts3VarintLen(nDoclist) + nDoclist;
 
   /* If the current block is not empty, and if adding this term/doclist
-  ** to the current block would make it larger than Fts3Table.nNodeSize
-  ** bytes, write this block out to the database. */
-  if( pLeaf->block.n>0 && (pLeaf->block.n + nSpace)>p->nNodeSize ){
+  ** to the current block would make it larger than Fts3Table.nNodeSize bytes,
+  ** and if there is still room for another leaf page, write this block out to
+  ** the database. */
+  if( pLeaf->block.n>0 
+   && (pLeaf->block.n + nSpace)>p->nNodeSize 
+   && pLeaf->iBlock < (pWriter->iStart + pWriter->nLeafEst)
+  ){
     rc = fts3WriteSegment(p, pLeaf->iBlock, pLeaf->block.a, pLeaf->block.n);
     pWriter->nWork++;
 
@@ -4347,6 +4356,7 @@ static int fts3IncrmergeLoad(
 
       for(i=nHeight; i>=0 && rc==SQLITE_OK; i--){
         NodeReader reader;
+        memset(&reader, 0, sizeof(reader));
         pNode = &pWriter->aNodeWriter[i];
 
         if( pNode->block.a){
@@ -4367,7 +4377,7 @@ static int fts3IncrmergeLoad(
               rc = sqlite3Fts3ReadBlock(p, reader.iChild, &aBlock, &nBlock,0);
               blobGrowBuffer(&pNode->block, 
                   MAX(nBlock, p->nNodeSize)+FTS3_NODE_PADDING, &rc
-                  );
+              );
               if( rc==SQLITE_OK ){
                 memcpy(pNode->block.a, aBlock, nBlock);
                 pNode->block.n = nBlock;
@@ -5217,7 +5227,7 @@ static u64 fts3ChecksumIndex(
   int rc;
   u64 cksum = 0;
 
-  assert( *pRc==SQLITE_OK );
+  if( *pRc ) return 0;
 
   memset(&filter, 0, sizeof(filter));
   memset(&csr, 0, sizeof(csr));
@@ -5432,8 +5442,11 @@ static int fts3SpecialInsert(Fts3Table *p, sqlite3_value *pVal){
     rc = fts3DoIncrmerge(p, &zVal[6]);
   }else if( nVal>10 && 0==sqlite3_strnicmp(zVal, "automerge=", 10) ){
     rc = fts3DoAutoincrmerge(p, &zVal[10]);
+  }else if( nVal==5 && 0==sqlite3_strnicmp(zVal, "flush", 5) ){
+    rc = sqlite3Fts3PendingTermsFlush(p);
+  }
 #if defined(SQLITE_DEBUG) || defined(SQLITE_TEST)
-  }else{
+  else{
     int v;
     if( nVal>9 && 0==sqlite3_strnicmp(zVal, "nodesize=", 9) ){
       v = atoi(&zVal[9]);
@@ -5451,8 +5464,8 @@ static int fts3SpecialInsert(Fts3Table *p, sqlite3_value *pVal){
       if( v>=4 && v<=FTS3_MERGE_COUNT && (v&1)==0 ) p->nMergeCount = v;
       rc = SQLITE_OK;
     }
-#endif
   }
+#endif
   return rc;
 }
 
diff --git a/sqlite/ext/fts5/fts5.h b/sqlite/ext/fts5/fts5.h
index 081e534f..323d73a2 100644
--- a/sqlite/ext/fts5/fts5.h
+++ b/sqlite/ext/fts5/fts5.h
@@ -263,7 +263,7 @@ struct Fts5PhraseIter {
 **   See xPhraseFirstColumn above.
 */
 struct Fts5ExtensionApi {
-  int iVersion;                   /* Currently always set to 3 */
+  int iVersion;                   /* Currently always set to 2 */
 
   void *(*xUserData)(Fts5Context*);
 
@@ -492,8 +492,8 @@ struct Fts5ExtensionApi {
 **   as separate queries of the FTS index are required for each synonym.
 **
 **   When using methods (2) or (3), it is important that the tokenizer only
-**   provide synonyms when tokenizing document text (method (2)) or query
-**   text (method (3)), not both. Doing so will not cause any errors, but is
+**   provide synonyms when tokenizing document text (method (3)) or query
+**   text (method (2)), not both. Doing so will not cause any errors, but is
 **   inefficient.
 */
 typedef struct Fts5Tokenizer Fts5Tokenizer;
@@ -541,7 +541,7 @@ struct fts5_api {
   int (*xCreateTokenizer)(
     fts5_api *pApi,
     const char *zName,
-    void *pContext,
+    void *pUserData,
     fts5_tokenizer *pTokenizer,
     void (*xDestroy)(void*)
   );
@@ -550,7 +550,7 @@ struct fts5_api {
   int (*xFindTokenizer)(
     fts5_api *pApi,
     const char *zName,
-    void **ppContext,
+    void **ppUserData,
     fts5_tokenizer *pTokenizer
   );
 
@@ -558,7 +558,7 @@ struct fts5_api {
   int (*xCreateFunction)(
     fts5_api *pApi,
     const char *zName,
-    void *pContext,
+    void *pUserData,
     fts5_extension_function xFunction,
     void (*xDestroy)(void*)
   );
diff --git a/sqlite/ext/fts5/fts5Int.h b/sqlite/ext/fts5/fts5Int.h
index 5d05da87..8bbafbaa 100644
--- a/sqlite/ext/fts5/fts5Int.h
+++ b/sqlite/ext/fts5/fts5Int.h
@@ -154,6 +154,10 @@ typedef struct Fts5Config Fts5Config;
 **   attempt to merge together. A value of 1 sets the object to use the 
 **   compile time default. Zero disables auto-merge altogether.
 **
+** bContentlessDelete:
+**   True if the contentless_delete option was present in the CREATE 
+**   VIRTUAL TABLE statement.
+**
 ** zContent:
 **
 ** zContentRowid:
@@ -188,6 +192,7 @@ struct Fts5Config {
   int nPrefix;                    /* Number of prefix indexes */
   int *aPrefix;                   /* Sizes in bytes of nPrefix prefix indexes */
   int eContent;                   /* An FTS5_CONTENT value */
+  int bContentlessDelete;         /* "contentless_delete=" option (dflt==0) */
   char *zContent;                 /* content table */ 
   char *zContentRowid;            /* "content_rowid=" option value */ 
   int bColumnsize;                /* "columnsize=" option value (dflt==1) */
@@ -209,6 +214,7 @@ struct Fts5Config {
   char *zRank;                    /* Name of rank function */
   char *zRankArgs;                /* Arguments to rank function */
   int bSecureDelete;              /* 'secure-delete' */
+  int nDeleteMerge;           /* 'deletemerge' */
 
   /* If non-NULL, points to sqlite3_vtab.base.zErrmsg. Often NULL. */
   char **pzErrmsg;
@@ -531,6 +537,9 @@ int sqlite3Fts5IndexReset(Fts5Index *p);
 
 int sqlite3Fts5IndexLoadConfig(Fts5Index *p);
 
+int sqlite3Fts5IndexGetOrigin(Fts5Index *p, i64 *piOrigin);
+int sqlite3Fts5IndexContentlessDelete(Fts5Index *p, i64 iOrigin, i64 iRowid);
+
 /*
 ** End of interface to code in fts5_index.c.
 **************************************************************************/
@@ -615,6 +624,11 @@ int sqlite3Fts5HashWrite(
 */
 void sqlite3Fts5HashClear(Fts5Hash*);
 
+/*
+** Return true if the hash is empty, false otherwise.
+*/
+int sqlite3Fts5HashIsEmpty(Fts5Hash*);
+
 int sqlite3Fts5HashQuery(
   Fts5Hash*,                      /* Hash table to query */
   int nPre,
@@ -636,6 +650,7 @@ void sqlite3Fts5HashScanEntry(Fts5Hash *,
 );
 
 
+
 /*
 ** End of interface to code in fts5_hash.c.
 **************************************************************************/
diff --git a/sqlite/ext/fts5/fts5_aux.c b/sqlite/ext/fts5/fts5_aux.c
index b178f473..fa58b9aa 100644
--- a/sqlite/ext/fts5/fts5_aux.c
+++ b/sqlite/ext/fts5/fts5_aux.c
@@ -110,15 +110,19 @@ static int fts5CInstIterInit(
 */
 typedef struct HighlightContext HighlightContext;
 struct HighlightContext {
-  CInstIter iter;                 /* Coalesced Instance Iterator */
-  int iPos;                       /* Current token offset in zIn[] */
+  /* Constant parameters to fts5HighlightCb() */
   int iRangeStart;                /* First token to include */
   int iRangeEnd;                  /* If non-zero, last token to include */
   const char *zOpen;              /* Opening highlight */
   const char *zClose;             /* Closing highlight */
   const char *zIn;                /* Input text */
   int nIn;                        /* Size of input text in bytes */
-  int iOff;                       /* Current offset within zIn[] */
+
+  /* Variables modified by fts5HighlightCb() */
+  CInstIter iter;                 /* Coalesced Instance Iterator */
+  int iPos;                       /* Current token offset in zIn[] */
+  int iOff;                       /* Have copied up to this offset in zIn[] */
+  int bOpen;                      /* True if highlight is open */
   char *zOut;                     /* Output value */
 };
 
@@ -151,8 +155,8 @@ static int fts5HighlightCb(
   int tflags,                     /* Mask of FTS5_TOKEN_* flags */
   const char *pToken,             /* Buffer containing token */
   int nToken,                     /* Size of token in bytes */
-  int iStartOff,                  /* Start offset of token */
-  int iEndOff                     /* End offset of token */
+  int iStartOff,                  /* Start byte offset of token */
+  int iEndOff                     /* End byte offset of token */
 ){
   HighlightContext *p = (HighlightContext*)pContext;
   int rc = SQLITE_OK;
@@ -168,30 +172,47 @@ static int fts5HighlightCb(
     if( p->iRangeStart && iPos==p->iRangeStart ) p->iOff = iStartOff;
   }
 
-  if( iPos==p->iter.iStart ){
+  /* If the parenthesis is open, and this token is not part of the current
+  ** phrase, and the starting byte offset of this token is past the point
+  ** that has currently been copied into the output buffer, close the
+  ** parenthesis. */
+  if( p->bOpen 
+   && (iPos<=p->iter.iStart || p->iter.iStart<0)
+   && iStartOff>p->iOff 
+  ){
+    fts5HighlightAppend(&rc, p, p->zClose, -1);
+    p->bOpen = 0;
+  }
+
+  /* If this is the start of a new phrase, and the highlight is not open:
+  **
+  **   * copy text from the input up to the start of the phrase, and
+  **   * open the highlight.
+  */
+  if( iPos==p->iter.iStart && p->bOpen==0 ){
     fts5HighlightAppend(&rc, p, &p->zIn[p->iOff], iStartOff - p->iOff);
     fts5HighlightAppend(&rc, p, p->zOpen, -1);
     p->iOff = iStartOff;
+    p->bOpen = 1;
   }
 
   if( iPos==p->iter.iEnd ){
-    if( p->iRangeEnd>=0 && p->iter.iStartiRangeStart ){
+    if( p->bOpen==0 ){
+      assert( p->iRangeEnd>=0 );
       fts5HighlightAppend(&rc, p, p->zOpen, -1);
+      p->bOpen = 1;
     }
     fts5HighlightAppend(&rc, p, &p->zIn[p->iOff], iEndOff - p->iOff);
-    fts5HighlightAppend(&rc, p, p->zClose, -1);
     p->iOff = iEndOff;
+
     if( rc==SQLITE_OK ){
       rc = fts5CInstIterNext(&p->iter);
     }
   }
 
-  if( p->iRangeEnd>=0 && iPos==p->iRangeEnd ){
+  if( iPos==p->iRangeEnd ){
     fts5HighlightAppend(&rc, p, &p->zIn[p->iOff], iEndOff - p->iOff);
     p->iOff = iEndOff;
-    if( iPos>=p->iter.iStart && iPositer.iEnd ){
-      fts5HighlightAppend(&rc, p, p->zClose, -1);
-    }
   }
 
   return rc;
@@ -232,6 +253,9 @@ static void fts5HighlightFunction(
     if( rc==SQLITE_OK ){
       rc = pApi->xTokenize(pFts, ctx.zIn, ctx.nIn, (void*)&ctx,fts5HighlightCb);
     }
+    if( ctx.bOpen ){
+      fts5HighlightAppend(&rc, &ctx, ctx.zClose, -1);
+    }
     fts5HighlightAppend(&rc, &ctx, &ctx.zIn[ctx.iOff], ctx.nIn - ctx.iOff);
 
     if( rc==SQLITE_OK ){
@@ -510,6 +534,9 @@ static void fts5SnippetFunction(
     if( rc==SQLITE_OK ){
       rc = pApi->xTokenize(pFts, ctx.zIn, ctx.nIn, (void*)&ctx,fts5HighlightCb);
     }
+    if( ctx.bOpen ){
+      fts5HighlightAppend(&rc, &ctx, ctx.zClose, -1);
+    }
     if( ctx.iRangeEnd>=(nColSize-1) ){
       fts5HighlightAppend(&rc, &ctx, &ctx.zIn[ctx.iOff], ctx.nIn - ctx.iOff);
     }else{
diff --git a/sqlite/ext/fts5/fts5_config.c b/sqlite/ext/fts5/fts5_config.c
index 7a4c7b81..5d077050 100644
--- a/sqlite/ext/fts5/fts5_config.c
+++ b/sqlite/ext/fts5/fts5_config.c
@@ -22,6 +22,8 @@
 #define FTS5_DEFAULT_CRISISMERGE   16
 #define FTS5_DEFAULT_HASHSIZE    (1024*1024)
 
+#define FTS5_DEFAULT_DELETE_AUTOMERGE 10      /* default 10% */
+
 /* Maximum allowed page size */
 #define FTS5_MAX_PAGE_SIZE (64*1024)
 
@@ -352,6 +354,16 @@ static int fts5ConfigParseSpecial(
     return rc;
   }
 
+  if( sqlite3_strnicmp("contentless_delete", zCmd, nCmd)==0 ){
+    if( (zArg[0]!='0' && zArg[0]!='1') || zArg[1]!='\0' ){
+      *pzErr = sqlite3_mprintf("malformed contentless_delete=... directive");
+      rc = SQLITE_ERROR;
+    }else{
+      pConfig->bContentlessDelete = (zArg[0]=='1');
+    }
+    return rc;
+  }
+
   if( sqlite3_strnicmp("content_rowid", zCmd, nCmd)==0 ){
     if( pConfig->zContentRowid ){
       *pzErr = sqlite3_mprintf("multiple content_rowid=... directives");
@@ -596,6 +608,28 @@ int sqlite3Fts5ConfigParse(
     sqlite3_free(zTwo);
   }
 
+  /* We only allow contentless_delete=1 if the table is indeed contentless. */
+  if( rc==SQLITE_OK 
+   && pRet->bContentlessDelete 
+   && pRet->eContent!=FTS5_CONTENT_NONE 
+  ){
+    *pzErr = sqlite3_mprintf(
+        "contentless_delete=1 requires a contentless table"
+    );
+    rc = SQLITE_ERROR;
+  }
+
+  /* We only allow contentless_delete=1 if columnsize=0 is not present. 
+  **
+  ** This restriction may be removed at some point. 
+  */
+  if( rc==SQLITE_OK && pRet->bContentlessDelete && pRet->bColumnsize==0 ){
+    *pzErr = sqlite3_mprintf(
+        "contentless_delete=1 is incompatible with columnsize=0"
+    );
+    rc = SQLITE_ERROR;
+  }
+
   /* If a tokenizer= option was successfully parsed, the tokenizer has
   ** already been allocated. Otherwise, allocate an instance of the default
   ** tokenizer (unicode61) now.  */
@@ -890,6 +924,18 @@ int sqlite3Fts5ConfigSetValue(
     }
   }
 
+  else if( 0==sqlite3_stricmp(zKey, "deletemerge") ){
+    int nVal = -1;
+    if( SQLITE_INTEGER==sqlite3_value_numeric_type(pVal) ){
+      nVal = sqlite3_value_int(pVal);
+    }else{
+      *pbBadkey = 1;
+    }
+    if( nVal<0 ) nVal = FTS5_DEFAULT_DELETE_AUTOMERGE;
+    if( nVal>100 ) nVal = 0;
+    pConfig->nDeleteMerge = nVal;
+  }
+
   else if( 0==sqlite3_stricmp(zKey, "rank") ){
     const char *zIn = (const char*)sqlite3_value_text(pVal);
     char *zRank;
@@ -938,6 +984,7 @@ int sqlite3Fts5ConfigLoad(Fts5Config *pConfig, int iCookie){
   pConfig->nUsermerge = FTS5_DEFAULT_USERMERGE;
   pConfig->nCrisisMerge = FTS5_DEFAULT_CRISISMERGE;
   pConfig->nHashSize = FTS5_DEFAULT_HASHSIZE;
+  pConfig->nDeleteMerge = FTS5_DEFAULT_DELETE_AUTOMERGE;
 
   zSql = sqlite3Fts5Mprintf(&rc, zSelect, pConfig->zDb, pConfig->zName);
   if( zSql ){
diff --git a/sqlite/ext/fts5/fts5_expr.c b/sqlite/ext/fts5/fts5_expr.c
index 0e018420..f5101ba0 100644
--- a/sqlite/ext/fts5/fts5_expr.c
+++ b/sqlite/ext/fts5/fts5_expr.c
@@ -2477,7 +2477,7 @@ Fts5ExprNode *sqlite3Fts5ParseImplicitAnd(
   return pRet;
 }
 
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
 static char *fts5ExprTermPrint(Fts5ExprTerm *pTerm){
   sqlite3_int64 nByte = 0;
   Fts5ExprTerm *p;
@@ -2583,6 +2583,8 @@ static char *fts5ExprPrintTcl(
       if( zRet==0 ) return 0;
     }
 
+  }else if( pExpr->eType==0 ){
+    zRet = sqlite3_mprintf("{}");
   }else{
     char const *zOp = 0;
     int i;
@@ -2844,14 +2846,14 @@ static void fts5ExprFold(
     sqlite3_result_int(pCtx, sqlite3Fts5UnicodeFold(iCode, bRemoveDiacritics));
   }
 }
-#endif /* ifdef SQLITE_TEST */
+#endif /* if SQLITE_TEST || SQLITE_FTS5_DEBUG */
 
 /*
 ** This is called during initialization to register the fts5_expr() scalar
 ** UDF with the SQLite handle passed as the only argument.
 */
 int sqlite3Fts5ExprInit(Fts5Global *pGlobal, sqlite3 *db){
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
   struct Fts5ExprFunc {
     const char *z;
     void (*x)(sqlite3_context*,int,sqlite3_value**);
diff --git a/sqlite/ext/fts5/fts5_hash.c b/sqlite/ext/fts5/fts5_hash.c
index bc9244fc..7e50c366 100644
--- a/sqlite/ext/fts5/fts5_hash.c
+++ b/sqlite/ext/fts5/fts5_hash.c
@@ -475,7 +475,6 @@ static int fts5HashEntrySort(
     pList = fts5HashEntryMerge(pList, ap[i]);
   }
 
-  pHash->nEntry = 0;
   sqlite3_free(ap);
   *ppSorted = pList;
   return SQLITE_OK;
@@ -529,6 +528,28 @@ int sqlite3Fts5HashScanInit(
   return fts5HashEntrySort(p, pTerm, nTerm, &p->pScan);
 }
 
+#ifdef SQLITE_DEBUG
+static int fts5HashCount(Fts5Hash *pHash){
+  int nEntry = 0;
+  int ii;
+  for(ii=0; iinSlot; ii++){
+    Fts5HashEntry *p = 0;
+    for(p=pHash->aSlot[ii]; p; p=p->pHashNext){
+      nEntry++;
+    }
+  }
+  return nEntry;
+}
+#endif
+
+/*
+** Return true if the hash table is empty, false otherwise.
+*/
+int sqlite3Fts5HashIsEmpty(Fts5Hash *pHash){
+  assert( pHash->nEntry==fts5HashCount(pHash) );
+  return pHash->nEntry==0;
+}
+
 void sqlite3Fts5HashScanNext(Fts5Hash *p){
   assert( !sqlite3Fts5HashScanEof(p) );
   p->pScan = p->pScan->pScanNext;
diff --git a/sqlite/ext/fts5/fts5_index.c b/sqlite/ext/fts5/fts5_index.c
index eaeeeff4..4e6afb28 100644
--- a/sqlite/ext/fts5/fts5_index.c
+++ b/sqlite/ext/fts5/fts5_index.c
@@ -56,6 +56,24 @@
 
 #define FTS5_MAX_LEVEL 64
 
+/*
+** There are two versions of the format used for the structure record:
+**
+**   1. the legacy format, that may be read by all fts5 versions, and
+**
+**   2. the V2 format, which is used by contentless_delete=1 databases.
+**
+** Both begin with a 4-byte "configuration cookie" value. Then, a legacy
+** format structure record contains a varint - the number of levels in
+** the structure. Whereas a V2 structure record contains the constant
+** 4 bytes [0xff 0x00 0x00 0x01]. This is unambiguous as the value of a
+** varint has to be at least 16256 to begin with "0xFF". And the default
+** maximum number of levels is 64. 
+**
+** See below for more on structure record formats.
+*/
+#define FTS5_STRUCTURE_V2 "\xFF\x00\x00\x01"
+
 /*
 ** Details:
 **
@@ -63,7 +81,7 @@
 **
 **     CREATE TABLE %_data(id INTEGER PRIMARY KEY, block BLOB);
 **
-** , contains the following 5 types of records. See the comments surrounding
+** , contains the following 6 types of records. See the comments surrounding
 ** the FTS5_*_ROWID macros below for a description of how %_data rowids are 
 ** assigned to each fo them.
 **
@@ -71,13 +89,13 @@
 **
 **   The set of segments that make up an index - the index structure - are
 **   recorded in a single record within the %_data table. The record consists
-**   of a single 32-bit configuration cookie value followed by a list of 
-**   SQLite varints. If the FTS table features more than one index (because
-**   there are one or more prefix indexes), it is guaranteed that all share
-**   the same cookie value.
+**   of a single 32-bit configuration cookie value followed by a list of
+**   SQLite varints. 
 **
-**   Immediately following the configuration cookie, the record begins with
-**   three varints:
+**   If the structure record is a V2 record, the configuration cookie is 
+**   followed by the following 4 bytes: [0xFF 0x00 0x00 0x01]. 
+**
+**   Next, the record continues with three varints:
 **
 **     + number of levels,
 **     + total number of segments on all levels,
@@ -92,6 +110,12 @@
 **         + first leaf page number (often 1, always greater than 0)
 **         + final leaf page number
 **
+**      Then, for V2 structures only:
+**
+**         + lower origin counter value,
+**         + upper origin counter value,
+**         + the number of tombstone hash pages.
+**
 ** 2. The Averages Record:
 **
 **   A single record within the %_data table. The data is a list of varints.
@@ -207,6 +231,38 @@
 **     * A list of delta-encoded varints - the first rowid on each subsequent
 **       child page. 
 **
+** 6. Tombstone Hash Page
+**
+**   These records are only ever present in contentless_delete=1 tables. 
+**   There are zero or more of these associated with each segment. They
+**   are used to store the tombstone rowids for rows contained in the
+**   associated segments.
+**
+**   The set of nHashPg tombstone hash pages associated with a single 
+**   segment together form a single hash table containing tombstone rowids.
+**   To find the page of the hash on which a key might be stored:
+**
+**       iPg = (rowid % nHashPg)
+**
+**   Then, within page iPg, which has nSlot slots:
+**
+**       iSlot = (rowid / nHashPg) % nSlot
+**
+**   Each tombstone hash page begins with an 8 byte header: 
+**
+**     1-byte:  Key-size (the size in bytes of each slot). Either 4 or 8.
+**     1-byte:  rowid-0-tombstone flag. This flag is only valid on the 
+**              first tombstone hash page for each segment (iPg=0). If set,
+**              the hash table contains rowid 0. If clear, it does not.
+**              Rowid 0 is handled specially.
+**     2-bytes: unused.
+**     4-bytes: Big-endian integer containing number of entries on page.
+**
+**   Following this are nSlot 4 or 8 byte slots (depending on the key-size
+**   in the first byte of the page header). The number of slots may be
+**   determined based on the size of the page record and the key-size:
+**
+**     nSlot = (nByte - 8) / key-size
 */
 
 /*
@@ -240,6 +296,7 @@
 
 #define FTS5_SEGMENT_ROWID(segid, pgno)       fts5_dri(segid, 0, 0, pgno)
 #define FTS5_DLIDX_ROWID(segid, height, pgno) fts5_dri(segid, 1, height, pgno)
+#define FTS5_TOMBSTONE_ROWID(segid,ipg)       fts5_dri(segid+(1<<16), 0, 0, ipg)
 
 #ifdef SQLITE_DEBUG
 int sqlite3Fts5Corrupt() { return SQLITE_CORRUPT_VTAB; }
@@ -275,6 +332,12 @@ struct Fts5Data {
 
 /*
 ** One object per %_data table.
+**
+** nContentlessDelete:
+**   The number of contentless delete operations since the most recent
+**   call to fts5IndexFlush() or fts5IndexDiscardData(). This is tracked
+**   so that extra auto-merge work can be done by fts5IndexFlush() to
+**   account for the delete operations.
 */
 struct Fts5Index {
   Fts5Config *pConfig;            /* Virtual table configuration */
@@ -289,6 +352,8 @@ struct Fts5Index {
   int nPendingData;               /* Current bytes of pending data */
   i64 iWriteRowid;                /* Rowid for current doc being written */
   int bDelete;                    /* Current write is a delete */
+  int nContentlessDelete;         /* Number of contentless delete ops */
+  int nPendingRow;                /* Number of INSERT in hash table */
 
   /* Error state. */
   int rc;                         /* Current error code */
@@ -323,11 +388,23 @@ struct Fts5DoclistIter {
 ** The contents of the "structure" record for each index are represented
 ** using an Fts5Structure record in memory. Which uses instances of the 
 ** other Fts5StructureXXX types as components.
+**
+** nOriginCntr:
+**   This value is set to non-zero for structure records created for
+**   contentlessdelete=1 tables only. In that case it represents the
+**   origin value to apply to the next top-level segment created.
 */
 struct Fts5StructureSegment {
   int iSegid;                     /* Segment id */
   int pgnoFirst;                  /* First leaf page number in segment */
   int pgnoLast;                   /* Last leaf page number in segment */
+
+  /* contentlessdelete=1 tables only: */
+  u64 iOrigin1;
+  u64 iOrigin2;
+  int nPgTombstone;               /* Number of tombstone hash table pages */
+  u64 nEntryTombstone;            /* Number of tombstone entries that "count" */
+  u64 nEntry;                     /* Number of rows in this segment */
 };
 struct Fts5StructureLevel {
   int nMerge;                     /* Number of segments in incr-merge */
@@ -337,6 +414,7 @@ struct Fts5StructureLevel {
 struct Fts5Structure {
   int nRef;                       /* Object reference count */
   u64 nWriteCounter;              /* Total leaves written to level 0 */
+  u64 nOriginCntr;                /* Origin value for next top-level segment */
   int nSegment;                   /* Total segments in this structure */
   int nLevel;                     /* Number of levels in this index */
   Fts5StructureLevel aLevel[1];   /* Array of nLevel level objects */
@@ -425,6 +503,13 @@ struct Fts5CResult {
 **
 ** iTermIdx:
 **     Index of current term on iTermLeafPgno.
+**
+** apTombstone/nTombstone:
+**     These are used for contentless_delete=1 tables only. When the cursor
+**     is first allocated, the apTombstone[] array is allocated so that it
+**     is large enough for all tombstones hash pages associated with the
+**     segment. The pages themselves are loaded lazily from the database as
+**     they are required.
 */
 struct Fts5SegIter {
   Fts5StructureSegment *pSeg;     /* Segment to iterate through */
@@ -433,6 +518,8 @@ struct Fts5SegIter {
   Fts5Data *pLeaf;                /* Current leaf data */
   Fts5Data *pNextLeaf;            /* Leaf page (iLeafPgno+1) */
   i64 iLeafOffset;                /* Byte offset within current leaf */
+  Fts5Data **apTombstone;         /* Array of tombstone pages */
+  int nTombstone;
 
   /* Next method */
   void (*xNext)(Fts5Index*, Fts5SegIter*, int*);
@@ -562,6 +649,60 @@ static u16 fts5GetU16(const u8 *aIn){
   return ((u16)aIn[0] << 8) + aIn[1];
 } 
 
+/*
+** The only argument points to a buffer at least 8 bytes in size. This
+** function interprets the first 8 bytes of the buffer as a 64-bit big-endian
+** unsigned integer and returns the result.
+*/
+static u64 fts5GetU64(u8 *a){
+  return ((u64)a[0] << 56)
+       + ((u64)a[1] << 48)
+       + ((u64)a[2] << 40)
+       + ((u64)a[3] << 32)
+       + ((u64)a[4] << 24)
+       + ((u64)a[5] << 16)
+       + ((u64)a[6] << 8)
+       + ((u64)a[7] << 0);
+}
+
+/*
+** The only argument points to a buffer at least 4 bytes in size. This
+** function interprets the first 4 bytes of the buffer as a 32-bit big-endian
+** unsigned integer and returns the result.
+*/
+static u32 fts5GetU32(const u8 *a){
+  return ((u32)a[0] << 24)
+       + ((u32)a[1] << 16)
+       + ((u32)a[2] << 8)
+       + ((u32)a[3] << 0);
+} 
+
+/*
+** Write iVal, formated as a 64-bit big-endian unsigned integer, to the
+** buffer indicated by the first argument.
+*/
+static void fts5PutU64(u8 *a, u64 iVal){
+  a[0] = ((iVal >> 56) & 0xFF);
+  a[1] = ((iVal >> 48) & 0xFF);
+  a[2] = ((iVal >> 40) & 0xFF);
+  a[3] = ((iVal >> 32) & 0xFF);
+  a[4] = ((iVal >> 24) & 0xFF);
+  a[5] = ((iVal >> 16) & 0xFF);
+  a[6] = ((iVal >>  8) & 0xFF);
+  a[7] = ((iVal >>  0) & 0xFF);
+}
+
+/*
+** Write iVal, formated as a 32-bit big-endian unsigned integer, to the
+** buffer indicated by the first argument.
+*/
+static void fts5PutU32(u8 *a, u32 iVal){
+  a[0] = ((iVal >> 24) & 0xFF);
+  a[1] = ((iVal >> 16) & 0xFF);
+  a[2] = ((iVal >>  8) & 0xFF);
+  a[3] = ((iVal >>  0) & 0xFF);
+}
+
 /*
 ** Allocate and return a buffer at least nByte bytes in size.
 **
@@ -789,10 +930,17 @@ static void fts5DataDelete(Fts5Index *p, i64 iFirst, i64 iLast){
 /*
 ** Remove all records associated with segment iSegid.
 */
-static void fts5DataRemoveSegment(Fts5Index *p, int iSegid){
+static void fts5DataRemoveSegment(Fts5Index *p, Fts5StructureSegment *pSeg){
+  int iSegid = pSeg->iSegid;
   i64 iFirst = FTS5_SEGMENT_ROWID(iSegid, 0);
   i64 iLast = FTS5_SEGMENT_ROWID(iSegid+1, 0)-1;
   fts5DataDelete(p, iFirst, iLast);
+
+  if( pSeg->nPgTombstone ){
+    i64 iTomb1 = FTS5_TOMBSTONE_ROWID(iSegid, 0);
+    i64 iTomb2 = FTS5_TOMBSTONE_ROWID(iSegid, pSeg->nPgTombstone-1);
+    fts5DataDelete(p, iTomb1, iTomb2);
+  }
   if( p->pIdxDeleter==0 ){
     Fts5Config *pConfig = p->pConfig;
     fts5IndexPrepareStmt(p, &p->pIdxDeleter, sqlite3_mprintf(
@@ -903,11 +1051,19 @@ static int fts5StructureDecode(
   int nSegment = 0;
   sqlite3_int64 nByte;            /* Bytes of space to allocate at pRet */
   Fts5Structure *pRet = 0;        /* Structure object to return */
+  int bStructureV2 = 0;           /* True for FTS5_STRUCTURE_V2 */
+  u64 nOriginCntr = 0;            /* Largest origin value seen so far */
 
   /* Grab the cookie value */
   if( piCookie ) *piCookie = sqlite3Fts5Get32(pData);
   i = 4;
 
+  /* Check if this is a V2 structure record. Set bStructureV2 if it is. */
+  if( 0==memcmp(&pData[i], FTS5_STRUCTURE_V2, 4) ){
+    i += 4;
+    bStructureV2 = 1;
+  }
+
   /* Read the total number of levels and segments from the start of the
   ** structure record.  */
   i += fts5GetVarint32(&pData[i], nLevel);
@@ -958,6 +1114,14 @@ static int fts5StructureDecode(
           i += fts5GetVarint32(&pData[i], pSeg->iSegid);
           i += fts5GetVarint32(&pData[i], pSeg->pgnoFirst);
           i += fts5GetVarint32(&pData[i], pSeg->pgnoLast);
+          if( bStructureV2 ){
+            i += fts5GetVarint(&pData[i], &pSeg->iOrigin1);
+            i += fts5GetVarint(&pData[i], &pSeg->iOrigin2);
+            i += fts5GetVarint32(&pData[i], pSeg->nPgTombstone);
+            i += fts5GetVarint(&pData[i], &pSeg->nEntryTombstone);
+            i += fts5GetVarint(&pData[i], &pSeg->nEntry);
+            nOriginCntr = MAX(nOriginCntr, pSeg->iOrigin2);
+          }
           if( pSeg->pgnoLastpgnoFirst ){
             rc = FTS5_CORRUPT;
             break;
@@ -968,6 +1132,9 @@ static int fts5StructureDecode(
       }
     }
     if( nSegment!=0 && rc==SQLITE_OK ) rc = FTS5_CORRUPT;
+    if( bStructureV2 ){
+      pRet->nOriginCntr = nOriginCntr+1;
+    }
 
     if( rc!=SQLITE_OK ){
       fts5StructureRelease(pRet);
@@ -1180,6 +1347,7 @@ static void fts5StructureWrite(Fts5Index *p, Fts5Structure *pStruct){
     Fts5Buffer buf;               /* Buffer to serialize record into */
     int iLvl;                     /* Used to iterate through levels */
     int iCookie;                  /* Cookie value to store */
+    int nHdr = (pStruct->nOriginCntr>0 ? (4+4+9+9+9) : (4+9+9));
 
     assert( pStruct->nSegment==fts5StructureCountSegments(pStruct) );
     memset(&buf, 0, sizeof(Fts5Buffer));
@@ -1188,9 +1356,12 @@ static void fts5StructureWrite(Fts5Index *p, Fts5Structure *pStruct){
     iCookie = p->pConfig->iCookie;
     if( iCookie<0 ) iCookie = 0;
 
-    if( 0==sqlite3Fts5BufferSize(&p->rc, &buf, 4+9+9+9) ){
+    if( 0==sqlite3Fts5BufferSize(&p->rc, &buf, nHdr) ){
       sqlite3Fts5Put32(buf.p, iCookie);
       buf.n = 4;
+      if( pStruct->nOriginCntr>0 ){
+        fts5BufferSafeAppendBlob(&buf, FTS5_STRUCTURE_V2, 4);
+      }
       fts5BufferSafeAppendVarint(&buf, pStruct->nLevel);
       fts5BufferSafeAppendVarint(&buf, pStruct->nSegment);
       fts5BufferSafeAppendVarint(&buf, (i64)pStruct->nWriteCounter);
@@ -1204,9 +1375,17 @@ static void fts5StructureWrite(Fts5Index *p, Fts5Structure *pStruct){
       assert( pLvl->nMerge<=pLvl->nSeg );
 
       for(iSeg=0; iSegnSeg; iSeg++){
-        fts5BufferAppendVarint(&p->rc, &buf, pLvl->aSeg[iSeg].iSegid);
-        fts5BufferAppendVarint(&p->rc, &buf, pLvl->aSeg[iSeg].pgnoFirst);
-        fts5BufferAppendVarint(&p->rc, &buf, pLvl->aSeg[iSeg].pgnoLast);
+        Fts5StructureSegment *pSeg = &pLvl->aSeg[iSeg];
+        fts5BufferAppendVarint(&p->rc, &buf, pSeg->iSegid);
+        fts5BufferAppendVarint(&p->rc, &buf, pSeg->pgnoFirst);
+        fts5BufferAppendVarint(&p->rc, &buf, pSeg->pgnoLast);
+        if( pStruct->nOriginCntr>0 ){
+          fts5BufferAppendVarint(&p->rc, &buf, pSeg->iOrigin1);
+          fts5BufferAppendVarint(&p->rc, &buf, pSeg->iOrigin2);
+          fts5BufferAppendVarint(&p->rc, &buf, pSeg->nPgTombstone);
+          fts5BufferAppendVarint(&p->rc, &buf, pSeg->nEntryTombstone);
+          fts5BufferAppendVarint(&p->rc, &buf, pSeg->nEntry);
+        }
       }
     }
 
@@ -1729,6 +1908,23 @@ static void fts5SegIterSetNext(Fts5Index *p, Fts5SegIter *pIter){
   }
 }
 
+/*
+** Allocate a tombstone hash page array (pIter->apTombstone) for the 
+** iterator passed as the second argument. If an OOM error occurs, leave
+** an error in the Fts5Index object.
+*/
+static void fts5SegIterAllocTombstone(Fts5Index *p, Fts5SegIter *pIter){
+  const int nTomb = pIter->pSeg->nPgTombstone;
+  if( nTomb>0 ){
+    Fts5Data **apTomb = 0;
+    apTomb = (Fts5Data**)sqlite3Fts5MallocZero(&p->rc, sizeof(Fts5Data)*nTomb);
+    if( apTomb ){
+      pIter->apTombstone = apTomb;
+      pIter->nTombstone = nTomb;
+    }
+  }
+}
+
 /*
 ** Initialize the iterator object pIter to iterate through the entries in
 ** segment pSeg. The iterator is left pointing to the first entry when 
@@ -1770,6 +1966,7 @@ static void fts5SegIterInit(
     pIter->iPgidxOff = pIter->pLeaf->szLeaf+1;
     fts5SegIterLoadTerm(p, pIter, 0);
     fts5SegIterLoadNPos(p, pIter);
+    fts5SegIterAllocTombstone(p, pIter);
   }
 }
 
@@ -2471,6 +2668,7 @@ static void fts5SegIterSeekInit(
   }
 
   fts5SegIterSetNext(p, pIter);
+  fts5SegIterAllocTombstone(p, pIter);
 
   /* Either:
   **
@@ -2551,6 +2749,20 @@ static void fts5SegIterHashInit(
   fts5SegIterSetNext(p, pIter);
 }
 
+/*
+** Array ap[] contains n elements. Release each of these elements using
+** fts5DataRelease(). Then free the array itself using sqlite3_free().
+*/
+static void fts5IndexFreeArray(Fts5Data **ap, int n){
+  if( ap ){
+    int ii;
+    for(ii=0; iiterm);
   fts5DataRelease(pIter->pLeaf);
   fts5DataRelease(pIter->pNextLeaf);
+  fts5IndexFreeArray(pIter->apTombstone, pIter->nTombstone);
   fts5DlidxIterFree(pIter->pDlidx);
   sqlite3_free(pIter->aRowidOffset);
   memset(pIter, 0, sizeof(Fts5SegIter));
@@ -2691,7 +2904,6 @@ static int fts5MultiIterDoCompare(Fts5Iter *pIter, int iOut){
       assert_nc( i2!=0 );
       pRes->bTermEq = 1;
       if( p1->iRowid==p2->iRowid ){
-        p1->bDel = p2->bDel;
         return i2;
       }
       res = ((p1->iRowid > p2->iRowid)==pIter->bRev) ? -1 : +1;
@@ -2895,6 +3107,84 @@ static void fts5MultiIterSetEof(Fts5Iter *pIter){
   pIter->iSwitchRowid = pSeg->iRowid;
 }
 
+/*
+** The argument to this macro must be an Fts5Data structure containing a
+** tombstone hash page. This macro returns the key-size of the hash-page.
+*/
+#define TOMBSTONE_KEYSIZE(pPg) (pPg->p[0]==4 ? 4 : 8)
+
+#define TOMBSTONE_NSLOT(pPg)   \
+  ((pPg->nn > 16) ? ((pPg->nn-8) / TOMBSTONE_KEYSIZE(pPg)) : 1)
+
+/*
+** Query a single tombstone hash table for rowid iRowid. Return true if
+** it is found or false otherwise. The tombstone hash table is one of
+** nHashTable tables.
+*/
+static int fts5IndexTombstoneQuery(
+  Fts5Data *pHash,                /* Hash table page to query */
+  int nHashTable,                 /* Number of pages attached to segment */
+  u64 iRowid                      /* Rowid to query hash for */
+){
+  const int szKey = TOMBSTONE_KEYSIZE(pHash);
+  const int nSlot = TOMBSTONE_NSLOT(pHash);
+  int iSlot = (iRowid / nHashTable) % nSlot;
+  int nCollide = nSlot;
+
+  if( iRowid==0 ){
+    return pHash->p[1];
+  }else if( szKey==4 ){
+    u32 *aSlot = (u32*)&pHash->p[8];
+    while( aSlot[iSlot] ){
+      if( fts5GetU32((u8*)&aSlot[iSlot])==iRowid ) return 1;
+      if( nCollide--==0 ) break;
+      iSlot = (iSlot+1)%nSlot;
+    }
+  }else{
+    u64 *aSlot = (u64*)&pHash->p[8];
+    while( aSlot[iSlot] ){
+      if( fts5GetU64((u8*)&aSlot[iSlot])==iRowid ) return 1;
+      if( nCollide--==0 ) break;
+      iSlot = (iSlot+1)%nSlot;
+    }
+  }
+
+  return 0;
+}
+
+/*
+** Return true if the iterator passed as the only argument points
+** to an segment entry for which there is a tombstone. Return false
+** if there is no tombstone or if the iterator is already at EOF.
+*/
+static int fts5MultiIterIsDeleted(Fts5Iter *pIter){
+  int iFirst = pIter->aFirst[1].iFirst;
+  Fts5SegIter *pSeg = &pIter->aSeg[iFirst];
+
+  if( pSeg->pLeaf && pSeg->nTombstone ){
+    /* Figure out which page the rowid might be present on. */
+    int iPg = ((u64)pSeg->iRowid) % pSeg->nTombstone;
+    assert( iPg>=0 );
+
+    /* If tombstone hash page iPg has not yet been loaded from the 
+    ** database, load it now. */
+    if( pSeg->apTombstone[iPg]==0 ){
+      pSeg->apTombstone[iPg] = fts5DataRead(pIter->pIndex,
+          FTS5_TOMBSTONE_ROWID(pSeg->pSeg->iSegid, iPg)
+      );
+      if( pSeg->apTombstone[iPg]==0 ) return 0;
+    }
+
+    return fts5IndexTombstoneQuery(
+        pSeg->apTombstone[iPg],
+        pSeg->nTombstone,
+        pSeg->iRowid
+    );
+  }
+
+  return 0;
+}
+
 /*
 ** Move the iterator to the next entry. 
 **
@@ -2932,7 +3222,9 @@ static void fts5MultiIterNext(
 
     fts5AssertMultiIterSetup(p, pIter);
     assert( pSeg==&pIter->aSeg[pIter->aFirst[1].iFirst] && pSeg->pLeaf );
-    if( pIter->bSkipEmpty==0 || pSeg->nPos ){
+    if( (pIter->bSkipEmpty==0 || pSeg->nPos) 
+      && 0==fts5MultiIterIsDeleted(pIter)
+    ){
       pIter->xSetOutputs(pIter, pSeg);
       return;
     }
@@ -2964,7 +3256,9 @@ static void fts5MultiIterNext2(
       }
       fts5AssertMultiIterSetup(p, pIter);
 
-    }while( fts5MultiIterIsEmpty(p, pIter) );
+    }while( (fts5MultiIterIsEmpty(p, pIter) || fts5MultiIterIsDeleted(pIter)) 
+         && (p->rc==SQLITE_OK)
+    );
   }
 }
 
@@ -2977,7 +3271,7 @@ static Fts5Iter *fts5MultiIterAlloc(
   int nSeg
 ){
   Fts5Iter *pNew;
-  int nSlot;                      /* Power of two >= nSeg */
+  i64 nSlot;                      /* Power of two >= nSeg */
 
   for(nSlot=2; nSlotbSkipEmpty && fts5MultiIterIsEmpty(p, pNew) ){
+    if( (pNew->bSkipEmpty && fts5MultiIterIsEmpty(p, pNew))
+     || fts5MultiIterIsDeleted(pNew)
+    ){
       fts5MultiIterNext(p, pNew, 0, 0);
     }else if( pNew->base.bEof==0 ){
       Fts5SegIter *pSeg = &pNew->aSeg[pNew->aFirst[1].iFirst];
@@ -3697,7 +3993,9 @@ static void fts5IndexDiscardData(Fts5Index *p){
   if( p->pHash ){
     sqlite3Fts5HashClear(p->pHash);
     p->nPendingData = 0;
+    p->nPendingRow = 0;
   }
+  p->nContentlessDelete = 0;
 }
 
 /*
@@ -4334,6 +4632,12 @@ static void fts5IndexMergeLevel(
 
     /* Read input from all segments in the input level */
     nInput = pLvl->nSeg;
+
+    /* Set the range of origins that will go into the output segment. */
+    if( pStruct->nOriginCntr>0 ){
+      pSeg->iOrigin1 = pLvl->aSeg[0].iOrigin1;
+      pSeg->iOrigin2 = pLvl->aSeg[pLvl->nSeg-1].iOrigin2;
+    }
   }
   bOldest = (pLvlOut->nSeg==1 && pStruct->nLevel==iLvl+2);
 
@@ -4393,8 +4697,11 @@ static void fts5IndexMergeLevel(
     int i;
 
     /* Remove the redundant segments from the %_data table */
+    assert( pSeg->nEntry==0 );
     for(i=0; iaSeg[i].iSegid);
+      Fts5StructureSegment *pOld = &pLvl->aSeg[i];
+      pSeg->nEntry += (pOld->nEntry - pOld->nEntryTombstone);
+      fts5DataRemoveSegment(p, pOld);
     }
 
     /* Remove the redundant segments from the input level */
@@ -4420,6 +4727,43 @@ static void fts5IndexMergeLevel(
   if( pnRem ) *pnRem -= writer.nLeafWritten;
 }
 
+/*
+** If this is not a contentless_delete=1 table, or if the 'deletemerge'
+** configuration option is set to 0, then this function always returns -1.
+** Otherwise, it searches the structure object passed as the second argument
+** for a level suitable for merging due to having a large number of 
+** tombstones in the tombstone hash. If one is found, its index is returned.
+** Otherwise, if there is no suitable level, -1.
+*/
+static int fts5IndexFindDeleteMerge(Fts5Index *p, Fts5Structure *pStruct){
+  Fts5Config *pConfig = p->pConfig;
+  int iRet = -1;
+  if( pConfig->bContentlessDelete && pConfig->nDeleteMerge>0 ){
+    int ii;
+    int nBest = 0;
+
+    for(ii=0; iinLevel; ii++){
+      Fts5StructureLevel *pLvl = &pStruct->aLevel[ii];
+      i64 nEntry = 0;
+      i64 nTomb = 0;
+      int iSeg;
+      for(iSeg=0; iSegnSeg; iSeg++){
+        nEntry += pLvl->aSeg[iSeg].nEntry;
+        nTomb += pLvl->aSeg[iSeg].nEntryTombstone;
+      }
+      assert_nc( nEntry>0 || pLvl->nSeg==0 );
+      if( nEntry>0 ){
+        int nPercent = (nTomb * 100) / nEntry;
+        if( nPercent>=pConfig->nDeleteMerge && nPercent>nBest ){
+          iRet = ii;
+          nBest = nPercent;
+        }
+      }
+    }
+  }
+  return iRet;
+}
+
 /*
 ** Do up to nPg pages of automerge work on the index.
 **
@@ -4439,14 +4783,15 @@ static int fts5IndexMerge(
     int iBestLvl = 0;           /* Level offering the most input segments */
     int nBest = 0;              /* Number of input segments on best level */
 
-    /* Set iBestLvl to the level to read input segments from. */
+    /* Set iBestLvl to the level to read input segments from. Or to -1 if
+    ** there is no level suitable to merge segments from.  */
     assert( pStruct->nLevel>0 );
     for(iLvl=0; iLvlnLevel; iLvl++){
       Fts5StructureLevel *pLvl = &pStruct->aLevel[iLvl];
       if( pLvl->nMerge ){
         if( pLvl->nMerge>nBest ){
           iBestLvl = iLvl;
-          nBest = pLvl->nMerge;
+          nBest = nMin;
         }
         break;
       }
@@ -4455,22 +4800,18 @@ static int fts5IndexMerge(
         iBestLvl = iLvl;
       }
     }
-
-    /* If nBest is still 0, then the index must be empty. */
-#ifdef SQLITE_DEBUG
-    for(iLvl=0; nBest==0 && iLvlnLevel; iLvl++){
-      assert( pStruct->aLevel[iLvl].nSeg==0 );
+    if( nBestaLevel[iBestLvl].nMerge==0 ){
-      break;
-    }
+    if( iBestLvl<0 ) break;
     bRet = 1;
     fts5IndexMergeLevel(p, &pStruct, iBestLvl, &nRem);
     if( p->rc==SQLITE_OK && pStruct->aLevel[iBestLvl].nMerge==0 ){
       fts5StructurePromote(p, iBestLvl+1, pStruct);
     }
+
+    if( nMin==1 ) nMin = 2;
   }
   *ppStruct = pStruct;
   return bRet;
@@ -4636,7 +4977,7 @@ static void fts5SecureDeleteOverflow(
       pLeaf = 0;
     }else if( bDetailNone ){
       break;
-    }else if( iNext>=pLeaf->szLeaf || iNext<4 ){
+    }else if( iNext>=pLeaf->szLeaf || pLeaf->nnszLeaf || iNext<4 ){
       p->rc = FTS5_CORRUPT;
       break;
     }else{
@@ -4655,9 +4996,13 @@ static void fts5SecureDeleteOverflow(
         int i1 = pLeaf->szLeaf;
         int i2 = 0;
 
+        i1 += fts5GetVarint32(&aPg[i1], iFirst);
+        if( iFirstrc = FTS5_CORRUPT;
+          break;
+        }
         aIdx = sqlite3Fts5MallocZero(&p->rc, (pLeaf->nn-pLeaf->szLeaf)+2);
         if( aIdx==0 ) break;
-        i1 += fts5GetVarint32(&aPg[i1], iFirst);
         i2 = sqlite3Fts5PutVarint(aIdx, iFirst-nShift);
         if( i1nn ){
           memcpy(&aIdx[i2], &aPg[i1], pLeaf->nn-i1);
@@ -4702,7 +5047,6 @@ static void fts5DoSecureDelete(
   int iPgIdx = pSeg->pLeaf->szLeaf;
 
   u64 iDelta = 0;
-  u64 iNextDelta = 0;
   int iNextOff = 0;
   int iOff = 0;
   int nIdx = 0;
@@ -4710,8 +5054,6 @@ static void fts5DoSecureDelete(
   int bLastInDoclist = 0;
   int iIdx = 0;
   int iStart = 0;
-  int iKeyOff = 0;
-  int iPrevKeyOff = 0;
   int iDelKeyOff = 0;       /* Offset of deleted key, if any */
 
   nIdx = nPg-iPgIdx;
@@ -4736,10 +5078,21 @@ static void fts5DoSecureDelete(
   ** This block sets the following variables:
   **
   **   iStart:
+  **     The offset of the first byte of the rowid or delta-rowid
+  **     value for the doclist entry being removed.
+  **
   **   iDelta:
+  **     The value of the rowid or delta-rowid value for the doclist
+  **     entry being removed.
+  **
+  **   iNextOff:
+  **     The offset of the next entry following the position list
+  **     for the one being removed. If the position list for this
+  **     entry overflows onto the next leaf page, this value will be
+  **     greater than pLeaf->szLeaf.
   */
   {
-    int iSOP;
+    int iSOP;                     /* Start-Of-Position-list */
     if( pSeg->iLeafPgno==pSeg->iTermLeafPgno ){
       iStart = pSeg->iTermLeafOffset;
     }else{
@@ -4775,47 +5128,75 @@ static void fts5DoSecureDelete(
   }
 
   iOff = iStart;
-  if( iNextOff>=iPgIdx ){
-    int pgno = pSeg->iLeafPgno+1;
-    fts5SecureDeleteOverflow(p, pSeg->pSeg, pgno, &bLastInDoclist);
-    iNextOff = iPgIdx;
-  }else{
-    /* Set bLastInDoclist to true if the entry being removed is the last
-    ** in its doclist.  */
-    for(iIdx=0, iKeyOff=0; iIdxbDel==0 ){
+    if( iNextOff>=iPgIdx ){
+      int pgno = pSeg->iLeafPgno+1;
+      fts5SecureDeleteOverflow(p, pSeg->pSeg, pgno, &bLastInDoclist);
+      iNextOff = iPgIdx;
+    }else{
+      /* Loop through the page-footer. If iNextOff (offset of the
+      ** entry following the one we are removing) is equal to the 
+      ** offset of a key on this page, then the entry is the last 
+      ** in its doclist.  */
+      int iKeyOff = 0;
+      for(iIdx=0; iIdxbDel ){
+    iOff += sqlite3Fts5PutVarint(&aPg[iOff], iDelta);
+    aPg[iOff++] = 0x01;
+  }else if( bLastInDoclist==0 ){
     if( iNextOff!=iPgIdx ){
+      u64 iNextDelta = 0;
       iNextOff += fts5GetVarint(&aPg[iNextOff], &iNextDelta);
       iOff += sqlite3Fts5PutVarint(&aPg[iOff], iDelta + iNextDelta);
     }
   }else if( 
-      iStart==pSeg->iTermLeafOffset && pSeg->iLeafPgno==pSeg->iTermLeafPgno 
+      pSeg->iLeafPgno==pSeg->iTermLeafPgno 
+   && iStart==pSeg->iTermLeafOffset 
   ){
     /* The entry being removed was the only position list in its
     ** doclist. Therefore the term needs to be removed as well. */
     int iKey = 0;
-    for(iIdx=0, iKeyOff=0; iIdx(u32)iStart ) break;
       iKeyOff += iVal;
     }
+    assert_nc( iKey>=1 );
 
+    /* Set iDelKeyOff to the value of the footer entry to remove from 
+    ** the page. */
     iDelKeyOff = iOff = iKeyOff;
+
     if( iNextOff!=iPgIdx ){
+      /* This is the only position-list associated with the term, and there
+      ** is another term following it on this page. So the subsequent term
+      ** needs to be moved to replace the term associated with the entry
+      ** being removed. */
       int nPrefix = 0;
       int nSuffix = 0;
       int nPrefix2 = 0;
@@ -4840,7 +5221,9 @@ static void fts5DoSecureDelete(
           iOff += sqlite3Fts5PutVarint(&aPg[iOff], nPrefix);
         }
         iOff += sqlite3Fts5PutVarint(&aPg[iOff], nSuffix);
-        if( nPrefix2>nPrefix ){
+        if( nPrefix2>pSeg->term.n ){
+          p->rc = FTS5_CORRUPT;
+        }else if( nPrefix2>nPrefix ){
           memcpy(&aPg[iOff], &pSeg->term.p[nPrefix], nPrefix2-nPrefix);
           iOff += (nPrefix2-nPrefix);
         }
@@ -4850,80 +5233,88 @@ static void fts5DoSecureDelete(
       }
     }
   }else if( iStart==4 ){
-      int iPgno;
-
-      assert_nc( pSeg->iLeafPgno>pSeg->iTermLeafPgno );
-      /* The entry being removed may be the only position list in
-      ** its doclist. */
-      for(iPgno=pSeg->iLeafPgno-1; iPgno>pSeg->iTermLeafPgno; iPgno-- ){
-        Fts5Data *pPg = fts5DataRead(p, FTS5_SEGMENT_ROWID(iSegid, iPgno));
-        int bEmpty = (pPg && pPg->nn==4);
-        fts5DataRelease(pPg);
-        if( bEmpty==0 ) break;
-      }
-
-      if( iPgno==pSeg->iTermLeafPgno ){
-        i64 iId = FTS5_SEGMENT_ROWID(iSegid, pSeg->iTermLeafPgno);
-        Fts5Data *pTerm = fts5DataRead(p, iId);
-        if( pTerm && pTerm->szLeaf==pSeg->iTermLeafOffset ){
-          u8 *aTermIdx = &pTerm->p[pTerm->szLeaf];
-          int nTermIdx = pTerm->nn - pTerm->szLeaf;
-          int iTermIdx = 0;
-          int iTermOff = 0;
-
-          while( 1 ){
-            u32 iVal = 0;
-            int nByte = fts5GetVarint32(&aTermIdx[iTermIdx], iVal);
-            iTermOff += iVal;
-            if( (iTermIdx+nByte)>=nTermIdx ) break;
-            iTermIdx += nByte;
-          }
-          nTermIdx = iTermIdx;
+    int iPgno;
+
+    assert_nc( pSeg->iLeafPgno>pSeg->iTermLeafPgno );
+    /* The entry being removed may be the only position list in
+    ** its doclist. */
+    for(iPgno=pSeg->iLeafPgno-1; iPgno>pSeg->iTermLeafPgno; iPgno-- ){
+      Fts5Data *pPg = fts5DataRead(p, FTS5_SEGMENT_ROWID(iSegid, iPgno));
+      int bEmpty = (pPg && pPg->nn==4);
+      fts5DataRelease(pPg);
+      if( bEmpty==0 ) break;
+    }
+
+    if( iPgno==pSeg->iTermLeafPgno ){
+      i64 iId = FTS5_SEGMENT_ROWID(iSegid, pSeg->iTermLeafPgno);
+      Fts5Data *pTerm = fts5DataRead(p, iId);
+      if( pTerm && pTerm->szLeaf==pSeg->iTermLeafOffset ){
+        u8 *aTermIdx = &pTerm->p[pTerm->szLeaf];
+        int nTermIdx = pTerm->nn - pTerm->szLeaf;
+        int iTermIdx = 0;
+        int iTermOff = 0;
+
+        while( 1 ){
+          u32 iVal = 0;
+          int nByte = fts5GetVarint32(&aTermIdx[iTermIdx], iVal);
+          iTermOff += iVal;
+          if( (iTermIdx+nByte)>=nTermIdx ) break;
+          iTermIdx += nByte;
+        }
+        nTermIdx = iTermIdx;
 
-          memmove(&pTerm->p[iTermOff], &pTerm->p[pTerm->szLeaf], nTermIdx);
-          fts5PutU16(&pTerm->p[2], iTermOff);
+        memmove(&pTerm->p[iTermOff], &pTerm->p[pTerm->szLeaf], nTermIdx);
+        fts5PutU16(&pTerm->p[2], iTermOff);
 
-          fts5DataWrite(p, iId, pTerm->p, iTermOff+nTermIdx);
-          if( nTermIdx==0 ){
-            fts5SecureDeleteIdxEntry(p, iSegid, pSeg->iTermLeafPgno);
-          }
+        fts5DataWrite(p, iId, pTerm->p, iTermOff+nTermIdx);
+        if( nTermIdx==0 ){
+          fts5SecureDeleteIdxEntry(p, iSegid, pSeg->iTermLeafPgno);
         }
-        fts5DataRelease(pTerm);
       }
+      fts5DataRelease(pTerm);
     }
+  }
 
-    if( p->rc==SQLITE_OK ){
-      const int nMove = nPg - iNextOff;
-      int nShift = 0;
+  /* Assuming no error has occurred, this block does final edits to the
+  ** leaf page before writing it back to disk. Input variables are:
+  **
+  **   nPg: Total initial size of leaf page.
+  **   iPgIdx: Initial offset of page footer.
+  **
+  **   iOff: Offset to move data to
+  **   iNextOff: Offset to move data from
+  */
+  if( p->rc==SQLITE_OK ){
+    const int nMove = nPg - iNextOff;     /* Number of bytes to move */
+    int nShift = iNextOff - iOff;         /* Distance to move them */
 
-      memmove(&aPg[iOff], &aPg[iNextOff], nMove);
-      iPgIdx -= (iNextOff - iOff);
-      nPg = iPgIdx;
-      fts5PutU16(&aPg[2], iPgIdx);
+    int iPrevKeyOut = 0;
+    int iKeyIn = 0;
 
-      nShift = iNextOff - iOff;
-      for(iIdx=0, iKeyOff=0, iPrevKeyOff=0; iIdxiOff ){
-            iKeyOff -= nShift;
-            nShift = 0;
-          }
-          nPg += sqlite3Fts5PutVarint(&aPg[nPg], iKeyOff - iPrevKeyOff);
-          iPrevKeyOff = iKeyOff;
-        }
-      }
+    memmove(&aPg[iOff], &aPg[iNextOff], nMove);
+    iPgIdx -= nShift;
+    nPg = iPgIdx;
+    fts5PutU16(&aPg[2], iPgIdx);
 
-      if( iPgIdx==nPg && nIdx>0 && pSeg->iLeafPgno!=1 ){
-        fts5SecureDeleteIdxEntry(p, iSegid, pSeg->iLeafPgno);
+    for(iIdx=0; iIdxiOff ? nShift : 0));
+        nPg += sqlite3Fts5PutVarint(&aPg[nPg], iKeyOut - iPrevKeyOut);
+        iPrevKeyOut = iKeyOut;
       }
+    }
 
-      assert_nc( nPg>4 || fts5GetU16(aPg)==0 );
-      fts5DataWrite(p, FTS5_SEGMENT_ROWID(iSegid,pSeg->iLeafPgno), aPg,nPg);
+    if( iPgIdx==nPg && nIdx>0 && pSeg->iLeafPgno!=1 ){
+      fts5SecureDeleteIdxEntry(p, iSegid, pSeg->iLeafPgno);
     }
-    sqlite3_free(aIdx);
+
+    assert_nc( nPg>4 || fts5GetU16(aPg)==0 );
+    fts5DataWrite(p, FTS5_SEGMENT_ROWID(iSegid,pSeg->iLeafPgno), aPg, nPg);
+  }
+  sqlite3_free(aIdx);
 }
 
 /*
@@ -4977,184 +5368,198 @@ static void fts5FlushOneHash(Fts5Index *p){
   /* Obtain a reference to the index structure and allocate a new segment-id
   ** for the new level-0 segment.  */
   pStruct = fts5StructureRead(p);
-  iSegid = fts5AllocateSegid(p, pStruct);
   fts5StructureInvalidate(p);
 
-  if( iSegid ){
-    const int pgsz = p->pConfig->pgsz;
-    int eDetail = p->pConfig->eDetail;
-    int bSecureDelete = p->pConfig->bSecureDelete;
-    Fts5StructureSegment *pSeg;   /* New segment within pStruct */
-    Fts5Buffer *pBuf;             /* Buffer in which to assemble leaf page */
-    Fts5Buffer *pPgidx;           /* Buffer in which to assemble pgidx */
-
-    Fts5SegWriter writer;
-    fts5WriteInit(p, &writer, iSegid);
-
-    pBuf = &writer.writer.buf;
-    pPgidx = &writer.writer.pgidx;
-
-    /* fts5WriteInit() should have initialized the buffers to (most likely)
-    ** the maximum space required. */
-    assert( p->rc || pBuf->nSpace>=(pgsz + FTS5_DATA_PADDING) );
-    assert( p->rc || pPgidx->nSpace>=(pgsz + FTS5_DATA_PADDING) );
-
-    /* Begin scanning through hash table entries. This loop runs once for each
-    ** term/doclist currently stored within the hash table. */
-    if( p->rc==SQLITE_OK ){
-      p->rc = sqlite3Fts5HashScanInit(pHash, 0, 0);
-    }
-    while( p->rc==SQLITE_OK && 0==sqlite3Fts5HashScanEof(pHash) ){
-      const char *zTerm;          /* Buffer containing term */
-      int nTerm;                  /* Size of zTerm in bytes */
-      const u8 *pDoclist;         /* Pointer to doclist for this term */
-      int nDoclist;               /* Size of doclist in bytes */
-
-      /* Get the term and doclist for this entry. */
-      sqlite3Fts5HashScanEntry(pHash, &zTerm, &pDoclist, &nDoclist);
-      nTerm = (int)strlen(zTerm);
-      if( bSecureDelete==0 ){
-        fts5WriteAppendTerm(p, &writer, nTerm, (const u8*)zTerm);
-        if( p->rc!=SQLITE_OK ) break;
-        assert( writer.bFirstRowidInPage==0 );
-      }
-
-      if( !bSecureDelete && pgsz>=(pBuf->n + pPgidx->n + nDoclist + 1) ){
-        /* The entire doclist will fit on the current leaf. */
-        fts5BufferSafeAppendBlob(pBuf, pDoclist, nDoclist);
-      }else{
-        int bTermWritten = !bSecureDelete;
-        i64 iRowid = 0;
-        i64 iPrev = 0;
-        int iOff = 0;
-
-        /* The entire doclist will not fit on this leaf. The following 
-        ** loop iterates through the poslists that make up the current 
-        ** doclist.  */
-        while( p->rc==SQLITE_OK && iOffpConfig->pgsz;
+      int eDetail = p->pConfig->eDetail;
+      int bSecureDelete = p->pConfig->bSecureDelete;
+      Fts5StructureSegment *pSeg; /* New segment within pStruct */
+      Fts5Buffer *pBuf;           /* Buffer in which to assemble leaf page */
+      Fts5Buffer *pPgidx;         /* Buffer in which to assemble pgidx */
+  
+      Fts5SegWriter writer;
+      fts5WriteInit(p, &writer, iSegid);
+  
+      pBuf = &writer.writer.buf;
+      pPgidx = &writer.writer.pgidx;
+  
+      /* fts5WriteInit() should have initialized the buffers to (most likely)
+      ** the maximum space required. */
+      assert( p->rc || pBuf->nSpace>=(pgsz + FTS5_DATA_PADDING) );
+      assert( p->rc || pPgidx->nSpace>=(pgsz + FTS5_DATA_PADDING) );
+  
+      /* Begin scanning through hash table entries. This loop runs once for each
+      ** term/doclist currently stored within the hash table. */
+      if( p->rc==SQLITE_OK ){
+        p->rc = sqlite3Fts5HashScanInit(pHash, 0, 0);
+      }
+      while( p->rc==SQLITE_OK && 0==sqlite3Fts5HashScanEof(pHash) ){
+        const char *zTerm;        /* Buffer containing term */
+        int nTerm;                /* Size of zTerm in bytes */
+        const u8 *pDoclist;       /* Pointer to doclist for this term */
+        int nDoclist;             /* Size of doclist in bytes */
+  
+        /* Get the term and doclist for this entry. */
+        sqlite3Fts5HashScanEntry(pHash, &zTerm, &pDoclist, &nDoclist);
+        nTerm = (int)strlen(zTerm);
+        if( bSecureDelete==0 ){
+          fts5WriteAppendTerm(p, &writer, nTerm, (const u8*)zTerm);
+          if( p->rc!=SQLITE_OK ) break;
+          assert( writer.bFirstRowidInPage==0 );
+        }
+  
+        if( !bSecureDelete && pgsz>=(pBuf->n + pPgidx->n + nDoclist + 1) ){
+          /* The entire doclist will fit on the current leaf. */
+          fts5BufferSafeAppendBlob(pBuf, pDoclist, nDoclist);
+        }else{
+          int bTermWritten = !bSecureDelete;
+          i64 iRowid = 0;
+          i64 iPrev = 0;
+          int iOff = 0;
+  
+          /* The entire doclist will not fit on this leaf. The following 
+          ** loop iterates through the poslists that make up the current 
+          ** doclist.  */
+          while( p->rc==SQLITE_OK && iOffrc!=SQLITE_OK || pDoclist[iOff]==0x01 ){
                   iOff++;
-                  nDoclist = 0;
-                }else{
                   continue;
                 }
               }
-            }else if( (pDoclist[iOff] & 0x01) ){
-              fts5FlushSecureDelete(p, pStruct, zTerm, iRowid);
-              if( p->rc!=SQLITE_OK || pDoclist[iOff]==0x01 ){
-                iOff++;
-                continue;
-              }
             }
-          }
-
-          if( p->rc==SQLITE_OK && bTermWritten==0 ){
-            fts5WriteAppendTerm(p, &writer, nTerm, (const u8*)zTerm);
-            bTermWritten = 1;
-            assert( p->rc!=SQLITE_OK || writer.bFirstRowidInPage==0 );
-          }
-          
-          if( writer.bFirstRowidInPage ){
-            fts5PutU16(&pBuf->p[0], (u16)pBuf->n);   /* first rowid on page */
-            pBuf->n += sqlite3Fts5PutVarint(&pBuf->p[pBuf->n], iRowid);
-            writer.bFirstRowidInPage = 0;
-            fts5WriteDlidxAppend(p, &writer, iRowid);
-          }else{
-            pBuf->n += sqlite3Fts5PutVarint(&pBuf->p[pBuf->n], iRowid-iPrev);
-          }
-          if( p->rc!=SQLITE_OK ) break;
-          assert( pBuf->n<=pBuf->nSpace );
-          iPrev = iRowid;
-
-          if( eDetail==FTS5_DETAIL_NONE ){
-            if( iOffp[pBuf->n++] = 0;
-              iOff++;
+  
+            if( p->rc==SQLITE_OK && bTermWritten==0 ){
+              fts5WriteAppendTerm(p, &writer, nTerm, (const u8*)zTerm);
+              bTermWritten = 1;
+              assert( p->rc!=SQLITE_OK || writer.bFirstRowidInPage==0 );
+            }
+            
+            if( writer.bFirstRowidInPage ){
+              fts5PutU16(&pBuf->p[0], (u16)pBuf->n);   /* first rowid on page */
+              pBuf->n += sqlite3Fts5PutVarint(&pBuf->p[pBuf->n], iRowid);
+              writer.bFirstRowidInPage = 0;
+              fts5WriteDlidxAppend(p, &writer, iRowid);
+            }else{
+              u64 iRowidDelta = (u64)iRowid - (u64)iPrev;
+              pBuf->n += sqlite3Fts5PutVarint(&pBuf->p[pBuf->n], iRowidDelta);
+            }
+            if( p->rc!=SQLITE_OK ) break;
+            assert( pBuf->n<=pBuf->nSpace );
+            iPrev = iRowid;
+  
+            if( eDetail==FTS5_DETAIL_NONE ){
               if( iOffp[pBuf->n++] = 0;
                 iOff++;
+                if( iOffp[pBuf->n++] = 0;
+                  iOff++;
+                }
+              }
+              if( (pBuf->n + pPgidx->n)>=pgsz ){
+                fts5WriteFlushLeaf(p, &writer);
               }
-            }
-            if( (pBuf->n + pPgidx->n)>=pgsz ){
-              fts5WriteFlushLeaf(p, &writer);
-            }
-          }else{
-            int bDummy;
-            int nPos;
-            int nCopy = fts5GetPoslistSize(&pDoclist[iOff], &nPos, &bDummy);
-            nCopy += nPos;
-            if( (pBuf->n + pPgidx->n + nCopy) <= pgsz ){
-              /* The entire poslist will fit on the current leaf. So copy
-              ** it in one go. */
-              fts5BufferSafeAppendBlob(pBuf, &pDoclist[iOff], nCopy);
             }else{
-              /* The entire poslist will not fit on this leaf. So it needs
-              ** to be broken into sections. The only qualification being
-              ** that each varint must be stored contiguously.  */
-              const u8 *pPoslist = &pDoclist[iOff];
-              int iPos = 0;
-              while( p->rc==SQLITE_OK ){
-                int nSpace = pgsz - pBuf->n - pPgidx->n;
-                int n = 0;
-                if( (nCopy - iPos)<=nSpace ){
-                  n = nCopy - iPos;
-                }else{
-                  n = fts5PoslistPrefix(&pPoslist[iPos], nSpace);
-                }
-                assert( n>0 );
-                fts5BufferSafeAppendBlob(pBuf, &pPoslist[iPos], n);
-                iPos += n;
-                if( (pBuf->n + pPgidx->n)>=pgsz ){
-                  fts5WriteFlushLeaf(p, &writer);
+              int bDel = 0;
+              int nPos = 0;
+              int nCopy = fts5GetPoslistSize(&pDoclist[iOff], &nPos, &bDel);
+              if( bDel && bSecureDelete ){
+                fts5BufferAppendVarint(&p->rc, pBuf, nPos*2);
+                iOff += nCopy;
+                nCopy = nPos;
+              }else{
+                nCopy += nPos;
+              }
+              if( (pBuf->n + pPgidx->n + nCopy) <= pgsz ){
+                /* The entire poslist will fit on the current leaf. So copy
+                ** it in one go. */
+                fts5BufferSafeAppendBlob(pBuf, &pDoclist[iOff], nCopy);
+              }else{
+                /* The entire poslist will not fit on this leaf. So it needs
+                ** to be broken into sections. The only qualification being
+                ** that each varint must be stored contiguously.  */
+                const u8 *pPoslist = &pDoclist[iOff];
+                int iPos = 0;
+                while( p->rc==SQLITE_OK ){
+                  int nSpace = pgsz - pBuf->n - pPgidx->n;
+                  int n = 0;
+                  if( (nCopy - iPos)<=nSpace ){
+                    n = nCopy - iPos;
+                  }else{
+                    n = fts5PoslistPrefix(&pPoslist[iPos], nSpace);
+                  }
+                  assert( n>0 );
+                  fts5BufferSafeAppendBlob(pBuf, &pPoslist[iPos], n);
+                  iPos += n;
+                  if( (pBuf->n + pPgidx->n)>=pgsz ){
+                    fts5WriteFlushLeaf(p, &writer);
+                  }
+                  if( iPos>=nCopy ) break;
                 }
-                if( iPos>=nCopy ) break;
               }
+              iOff += nCopy;
             }
-            iOff += nCopy;
           }
         }
+  
+        /* TODO2: Doclist terminator written here. */
+        /* pBuf->p[pBuf->n++] = '\0'; */
+        assert( pBuf->n<=pBuf->nSpace );
+        if( p->rc==SQLITE_OK ) sqlite3Fts5HashScanNext(pHash);
       }
-
-      /* TODO2: Doclist terminator written here. */
-      /* pBuf->p[pBuf->n++] = '\0'; */
-      assert( pBuf->n<=pBuf->nSpace );
-      if( p->rc==SQLITE_OK ) sqlite3Fts5HashScanNext(pHash);
-    }
-    sqlite3Fts5HashClear(pHash);
-    fts5WriteFinish(p, &writer, &pgnoLast);
-
-    assert( p->rc!=SQLITE_OK || bSecureDelete || pgnoLast>0 );
-    if( pgnoLast>0 ){
-      /* Update the Fts5Structure. It is written back to the database by the
-      ** fts5StructureRelease() call below.  */
-      if( pStruct->nLevel==0 ){
-        fts5StructureAddLevel(&p->rc, &pStruct);
-      }
-      fts5StructureExtendLevel(&p->rc, pStruct, 0, 1, 0);
-      if( p->rc==SQLITE_OK ){
-        pSeg = &pStruct->aLevel[0].aSeg[ pStruct->aLevel[0].nSeg++ ];
-        pSeg->iSegid = iSegid;
-        pSeg->pgnoFirst = 1;
-        pSeg->pgnoLast = pgnoLast;
-        pStruct->nSegment++;
+      fts5WriteFinish(p, &writer, &pgnoLast);
+  
+      assert( p->rc!=SQLITE_OK || bSecureDelete || pgnoLast>0 );
+      if( pgnoLast>0 ){
+        /* Update the Fts5Structure. It is written back to the database by the
+        ** fts5StructureRelease() call below.  */
+        if( pStruct->nLevel==0 ){
+          fts5StructureAddLevel(&p->rc, &pStruct);
+        }
+        fts5StructureExtendLevel(&p->rc, pStruct, 0, 1, 0);
+        if( p->rc==SQLITE_OK ){
+          pSeg = &pStruct->aLevel[0].aSeg[ pStruct->aLevel[0].nSeg++ ];
+          pSeg->iSegid = iSegid;
+          pSeg->pgnoFirst = 1;
+          pSeg->pgnoLast = pgnoLast;
+          if( pStruct->nOriginCntr>0 ){
+            pSeg->iOrigin1 = pStruct->nOriginCntr;
+            pSeg->iOrigin2 = pStruct->nOriginCntr;
+            pSeg->nEntry = p->nPendingRow;
+            pStruct->nOriginCntr++;
+          }
+          pStruct->nSegment++;
+        }
+        fts5StructurePromote(p, 0, pStruct);
       }
-      fts5StructurePromote(p, 0, pStruct);
     }
   }
 
-  fts5IndexAutomerge(p, &pStruct, pgnoLast);
+  fts5IndexAutomerge(p, &pStruct, pgnoLast + p->nContentlessDelete);
   fts5IndexCrisismerge(p, &pStruct);
   fts5StructureWrite(p, pStruct);
   fts5StructureRelease(pStruct);
@@ -5165,10 +5570,15 @@ static void fts5FlushOneHash(Fts5Index *p){
 */
 static void fts5IndexFlush(Fts5Index *p){
   /* Unless it is empty, flush the hash table to disk */
-  if( p->nPendingData ){
+  if( p->nPendingData || p->nContentlessDelete ){
     assert( p->pHash );
-    p->nPendingData = 0;
     fts5FlushOneHash(p);
+    if( p->rc==SQLITE_OK ){
+      sqlite3Fts5HashClear(p->pHash);
+      p->nPendingData = 0;
+      p->nPendingRow = 0;
+      p->nContentlessDelete = 0;
+    }
   }
 }
 
@@ -5184,17 +5594,22 @@ static Fts5Structure *fts5IndexOptimizeStruct(
   /* Figure out if this structure requires optimization. A structure does
   ** not require optimization if either:
   **
-  **  + it consists of fewer than two segments, or 
-  **  + all segments are on the same level, or
-  **  + all segments except one are currently inputs to a merge operation.
+  **  1. it consists of fewer than two segments, or 
+  **  2. all segments are on the same level, or
+  **  3. all segments except one are currently inputs to a merge operation.
   **
-  ** In the first case, return NULL. In the second, increment the ref-count
-  ** on *pStruct and return a copy of the pointer to it.
+  ** In the first case, if there are no tombstone hash pages, return NULL. In
+  ** the second, increment the ref-count on *pStruct and return a copy of the
+  ** pointer to it.
   */
-  if( nSeg<2 ) return 0;
+  if( nSeg==0 ) return 0;
   for(i=0; inLevel; i++){
     int nThis = pStruct->aLevel[i].nSeg;
-    if( nThis==nSeg || (nThis==nSeg-1 && pStruct->aLevel[i].nMerge==nThis) ){
+    int nMerge = pStruct->aLevel[i].nMerge;
+    if( nThis>0 && (nThis==nSeg || (nThis==nSeg-1 && nMerge==nThis)) ){
+      if( nSeg==1 && nThis==1 && pStruct->aLevel[i].aSeg[0].nPgTombstone==0 ){
+        return 0;
+      }
       fts5StructureRef(pStruct);
       return pStruct;
     }
@@ -5210,6 +5625,7 @@ static Fts5Structure *fts5IndexOptimizeStruct(
     pNew->nLevel = MIN(pStruct->nLevel+1, FTS5_MAX_LEVEL);
     pNew->nRef = 1;
     pNew->nWriteCounter = pStruct->nWriteCounter;
+    pNew->nOriginCntr = pStruct->nOriginCntr;
     pLvl = &pNew->aLevel[pNew->nLevel-1];
     pLvl->aSeg = (Fts5StructureSegment*)sqlite3Fts5MallocZero(&p->rc, nByte);
     if( pLvl->aSeg ){
@@ -5240,6 +5656,7 @@ int sqlite3Fts5IndexOptimize(Fts5Index *p){
 
   assert( p->rc==SQLITE_OK );
   fts5IndexFlush(p);
+  assert( p->nContentlessDelete==0 );
   pStruct = fts5StructureRead(p);
   fts5StructureInvalidate(p);
 
@@ -5269,7 +5686,10 @@ int sqlite3Fts5IndexOptimize(Fts5Index *p){
 ** INSERT command.
 */
 int sqlite3Fts5IndexMerge(Fts5Index *p, int nMerge){
-  Fts5Structure *pStruct = fts5StructureRead(p);
+  Fts5Structure *pStruct = 0;
+
+  fts5IndexFlush(p);
+  pStruct = fts5StructureRead(p);
   if( pStruct ){
     int nMin = p->pConfig->nUsermerge;
     fts5StructureInvalidate(p);
@@ -5277,7 +5697,7 @@ int sqlite3Fts5IndexMerge(Fts5Index *p, int nMerge){
       Fts5Structure *pNew = fts5IndexOptimizeStruct(p, pStruct);
       fts5StructureRelease(pStruct);
       pStruct = pNew;
-      nMin = 2;
+      nMin = 1;
       nMerge = nMerge*-1;
     }
     if( pStruct && pStruct->nLevel ){
@@ -5791,6 +6211,9 @@ int sqlite3Fts5IndexBeginWrite(Fts5Index *p, int bDelete, i64 iRowid){
 
   p->iWriteRowid = iRowid;
   p->bDelete = bDelete;
+  if( bDelete==0 ){
+    p->nPendingRow++;
+  }
   return fts5IndexReturn(p);
 }
 
@@ -5828,6 +6251,9 @@ int sqlite3Fts5IndexReinit(Fts5Index *p){
   fts5StructureInvalidate(p);
   fts5IndexDiscardData(p);
   memset(&s, 0, sizeof(Fts5Structure));
+  if( p->pConfig->bContentlessDelete ){
+    s.nOriginCntr = 1;
+  }
   fts5DataWrite(p, FTS5_AVERAGES_ROWID, (const u8*)"", 0);
   fts5StructureWrite(p, &s);
   return fts5IndexReturn(p);
@@ -6219,6 +6645,347 @@ int sqlite3Fts5IndexLoadConfig(Fts5Index *p){
   return fts5IndexReturn(p);
 }
 
+/*
+** Retrieve the origin value that will be used for the segment currently
+** being accumulated in the in-memory hash table when it is flushed to
+** disk. If successful, SQLITE_OK is returned and (*piOrigin) set to
+** the queried value. Or, if an error occurs, an error code is returned
+** and the final value of (*piOrigin) is undefined.
+*/
+int sqlite3Fts5IndexGetOrigin(Fts5Index *p, i64 *piOrigin){
+  Fts5Structure *pStruct;
+  pStruct = fts5StructureRead(p);
+  if( pStruct ){
+    *piOrigin = pStruct->nOriginCntr;
+    fts5StructureRelease(pStruct);
+  }
+  return fts5IndexReturn(p);
+}
+
+/*
+** Buffer pPg contains a page of a tombstone hash table - one of nPg pages
+** associated with the same segment. This function adds rowid iRowid to
+** the hash table. The caller is required to guarantee that there is at
+** least one free slot on the page.
+**
+** If parameter bForce is false and the hash table is deemed to be full
+** (more than half of the slots are occupied), then non-zero is returned
+** and iRowid not inserted. Or, if bForce is true or if the hash table page
+** is not full, iRowid is inserted and zero returned.
+*/
+static int fts5IndexTombstoneAddToPage(
+  Fts5Data *pPg, 
+  int bForce,
+  int nPg, 
+  u64 iRowid
+){
+  const int szKey = TOMBSTONE_KEYSIZE(pPg);
+  const int nSlot = TOMBSTONE_NSLOT(pPg);
+  const int nElem = fts5GetU32(&pPg->p[4]);
+  int iSlot = (iRowid / nPg) % nSlot;
+  int nCollide = nSlot;
+
+  if( szKey==4 && iRowid>0xFFFFFFFF ) return 2;
+  if( iRowid==0 ){
+    pPg->p[1] = 0x01;
+    return 0;
+  }
+
+  if( bForce==0 && nElem>=(nSlot/2) ){
+    return 1;
+  }
+
+  fts5PutU32(&pPg->p[4], nElem+1);
+  if( szKey==4 ){
+    u32 *aSlot = (u32*)&pPg->p[8];
+    while( aSlot[iSlot] ){
+      iSlot = (iSlot + 1) % nSlot;
+      if( nCollide--==0 ) return 0;
+    }
+    fts5PutU32((u8*)&aSlot[iSlot], (u32)iRowid);
+  }else{
+    u64 *aSlot = (u64*)&pPg->p[8];
+    while( aSlot[iSlot] ){
+      iSlot = (iSlot + 1) % nSlot;
+      if( nCollide--==0 ) return 0;
+    }
+    fts5PutU64((u8*)&aSlot[iSlot], iRowid);
+  }
+
+  return 0;
+}
+
+/*
+** This function attempts to build a new hash containing all the keys 
+** currently in the tombstone hash table for segment pSeg. The new
+** hash will be stored in the nOut buffers passed in array apOut[].
+** All pages of the new hash use key-size szKey (4 or 8).
+**
+** Return 0 if the hash is successfully rebuilt into the nOut pages. 
+** Or non-zero if it is not (because one page became overfull). In this 
+** case the caller should retry with a larger nOut parameter.
+**
+** Parameter pData1 is page iPg1 of the hash table being rebuilt.
+*/
+static int fts5IndexTombstoneRehash(
+  Fts5Index *p,
+  Fts5StructureSegment *pSeg,     /* Segment to rebuild hash of */
+  Fts5Data *pData1,               /* One page of current hash - or NULL */
+  int iPg1,                       /* Which page of the current hash is pData1 */
+  int szKey,                      /* 4 or 8, the keysize */
+  int nOut,                       /* Number of output pages */
+  Fts5Data **apOut                /* Array of output hash pages */
+){
+  int ii;
+  int res = 0;
+
+  /* Initialize the headers of all the output pages */
+  for(ii=0; iip[0] = szKey;
+    fts5PutU32(&apOut[ii]->p[4], 0);
+  }
+
+  /* Loop through the current pages of the hash table. */ 
+  for(ii=0; res==0 && iinPgTombstone; ii++){
+    Fts5Data *pData = 0;          /* Page ii of the current hash table */
+    Fts5Data *pFree = 0;          /* Free this at the end of the loop */
+
+    if( iPg1==ii ){
+      pData = pData1;
+    }else{
+      pFree = pData = fts5DataRead(p, FTS5_TOMBSTONE_ROWID(pSeg->iSegid, ii));
+    }
+
+    if( pData ){
+      int szKeyIn = TOMBSTONE_KEYSIZE(pData);
+      int nSlotIn = (pData->nn - 8) / szKeyIn;
+      int iIn;
+      for(iIn=0; iInp[8];
+          if( aSlot[iIn] ) iVal = fts5GetU32((u8*)&aSlot[iIn]);
+        }else{
+          u64 *aSlot = (u64*)&pData->p[8];
+          if( aSlot[iIn] ) iVal = fts5GetU64((u8*)&aSlot[iIn]);
+        }
+
+        /* If iVal is not 0 at this point, insert it into the new hash table */
+        if( iVal ){
+          Fts5Data *pPg = apOut[(iVal % nOut)];
+          res = fts5IndexTombstoneAddToPage(pPg, 0, nOut, iVal);
+          if( res ) break;
+        }
+      }
+
+      /* If this is page 0 of the old hash, copy the rowid-0-flag from the
+      ** old hash to the new.  */
+      if( ii==0 ){
+        apOut[0]->p[1] = pData->p[1];
+      }
+    }
+    fts5DataRelease(pFree);
+  }
+
+  return res;
+}
+
+/*
+** This is called to rebuild the hash table belonging to segment pSeg.
+** If parameter pData1 is not NULL, then one page of the existing hash table
+** has already been loaded - pData1, which is page iPg1. The key-size for
+** the new hash table is szKey (4 or 8).
+**
+** If successful, the new hash table is not written to disk. Instead, 
+** output parameter (*pnOut) is set to the number of pages in the new
+** hash table, and (*papOut) to point to an array of buffers containing
+** the new page data.
+**
+** If an error occurs, an error code is left in the Fts5Index object and
+** both output parameters set to 0 before returning.
+*/
+static void fts5IndexTombstoneRebuild(
+  Fts5Index *p,
+  Fts5StructureSegment *pSeg,     /* Segment to rebuild hash of */
+  Fts5Data *pData1,               /* One page of current hash - or NULL */
+  int iPg1,                       /* Which page of the current hash is pData1 */
+  int szKey,                      /* 4 or 8, the keysize */
+  int *pnOut,                     /* OUT: Number of output pages */
+  Fts5Data ***papOut              /* OUT: Output hash pages */
+){
+  const int MINSLOT = 32;
+  int nSlotPerPage = MAX(MINSLOT, (p->pConfig->pgsz - 8) / szKey);
+  int nSlot = 0;                  /* Number of slots in each output page */
+  int nOut = 0;
+
+  /* Figure out how many output pages (nOut) and how many slots per 
+  ** page (nSlot).  There are three possibilities: 
+  **
+  **   1. The hash table does not yet exist. In this case the new hash
+  **      table will consist of a single page with MINSLOT slots.
+  **
+  **   2. The hash table exists but is currently a single page. In this
+  **      case an attempt is made to grow the page to accommodate the new
+  **      entry. The page is allowed to grow up to nSlotPerPage (see above)
+  **      slots.
+  **
+  **   3. The hash table already consists of more than one page, or of
+  **      a single page already so large that it cannot be grown. In this
+  **      case the new hash consists of (nPg*2+1) pages of nSlotPerPage
+  **      slots each, where nPg is the current number of pages in the 
+  **      hash table.
+  */
+  if( pSeg->nPgTombstone==0 ){
+    /* Case 1. */
+    nOut = 1;
+    nSlot = MINSLOT;
+  }else if( pSeg->nPgTombstone==1 ){
+    /* Case 2. */
+    int nElem = (int)fts5GetU32(&pData1->p[4]);
+    assert( pData1 && iPg1==0 );
+    nOut = 1;
+    nSlot = MAX(nElem*4, MINSLOT);
+    if( nSlot>nSlotPerPage ) nOut = 0; 
+  }
+  if( nOut==0 ){
+    /* Case 3. */
+    nOut = (pSeg->nPgTombstone * 2 + 1);
+    nSlot = nSlotPerPage;
+  }
+
+  /* Allocate the required array and output pages */
+  while( 1 ){
+    int res = 0;
+    int ii = 0;
+    int szPage = 0;
+    Fts5Data **apOut = 0;
+
+    /* Allocate space for the new hash table */
+    assert( nSlot>=MINSLOT );
+    apOut = (Fts5Data**)sqlite3Fts5MallocZero(&p->rc, sizeof(Fts5Data*) * nOut);
+    szPage = 8 + nSlot*szKey;
+    for(ii=0; iirc, 
+          sizeof(Fts5Data)+szPage
+      );
+      if( pNew ){
+        pNew->nn = szPage;
+        pNew->p = (u8*)&pNew[1];
+        apOut[ii] = pNew;
+      }
+    }
+
+    /* Rebuild the hash table. */
+    if( p->rc==SQLITE_OK ){
+      res = fts5IndexTombstoneRehash(p, pSeg, pData1, iPg1, szKey, nOut, apOut);
+    }
+    if( res==0 ){
+      if( p->rc ){
+        fts5IndexFreeArray(apOut, nOut);
+        apOut = 0;
+        nOut = 0;
+      }
+      *pnOut = nOut;
+      *papOut = apOut;
+      break;
+    }
+    
+    /* If control flows to here, it was not possible to rebuild the hash
+    ** table. Free all buffers and then try again with more pages. */
+    assert( p->rc==SQLITE_OK );
+    fts5IndexFreeArray(apOut, nOut);
+    nSlot = nSlotPerPage;
+    nOut = nOut*2 + 1;
+  }
+}
+
+
+/*
+** Add a tombstone for rowid iRowid to segment pSeg.
+*/
+static void fts5IndexTombstoneAdd(
+  Fts5Index *p, 
+  Fts5StructureSegment *pSeg, 
+  u64 iRowid
+){
+  Fts5Data *pPg = 0;
+  int iPg = -1;
+  int szKey = 0;
+  int nHash = 0;
+  Fts5Data **apHash = 0;
+
+  p->nContentlessDelete++;
+
+  if( pSeg->nPgTombstone>0 ){
+    iPg = iRowid % pSeg->nPgTombstone;
+    pPg = fts5DataRead(p, FTS5_TOMBSTONE_ROWID(pSeg->iSegid,iPg));
+    if( pPg==0 ){
+      assert( p->rc!=SQLITE_OK );
+      return;
+    }
+
+    if( 0==fts5IndexTombstoneAddToPage(pPg, 0, pSeg->nPgTombstone, iRowid) ){
+      fts5DataWrite(p, FTS5_TOMBSTONE_ROWID(pSeg->iSegid,iPg), pPg->p, pPg->nn);
+      fts5DataRelease(pPg);
+      return;
+    }
+  }
+
+  /* Have to rebuild the hash table. First figure out the key-size (4 or 8). */
+  szKey = pPg ? TOMBSTONE_KEYSIZE(pPg) : 4;
+  if( iRowid>0xFFFFFFFF ) szKey = 8;
+
+  /* Rebuild the hash table */
+  fts5IndexTombstoneRebuild(p, pSeg, pPg, iPg, szKey, &nHash, &apHash);
+  assert( p->rc==SQLITE_OK || (nHash==0 && apHash==0) );
+
+  /* If all has succeeded, write the new rowid into one of the new hash
+  ** table pages, then write them all out to disk. */
+  if( nHash ){
+    int ii = 0;
+    fts5IndexTombstoneAddToPage(apHash[iRowid % nHash], 1, nHash, iRowid);
+    for(ii=0; iiiSegid, ii);
+      fts5DataWrite(p, iTombstoneRowid, apHash[ii]->p, apHash[ii]->nn);
+    }
+    pSeg->nPgTombstone = nHash;
+    fts5StructureWrite(p, p->pStruct);
+  }
+
+  fts5DataRelease(pPg);
+  fts5IndexFreeArray(apHash, nHash);
+}
+
+/*
+** Add iRowid to the tombstone list of the segment or segments that contain
+** rows from origin iOrigin. Return SQLITE_OK if successful, or an SQLite
+** error code otherwise.
+*/
+int sqlite3Fts5IndexContentlessDelete(Fts5Index *p, i64 iOrigin, i64 iRowid){
+  Fts5Structure *pStruct;
+  pStruct = fts5StructureRead(p);
+  if( pStruct ){
+    int bFound = 0;               /* True after pSeg->nEntryTombstone incr. */
+    int iLvl;
+    for(iLvl=pStruct->nLevel-1; iLvl>=0; iLvl--){
+      int iSeg;
+      for(iSeg=pStruct->aLevel[iLvl].nSeg-1; iSeg>=0; iSeg--){
+        Fts5StructureSegment *pSeg = &pStruct->aLevel[iLvl].aSeg[iSeg];
+        if( pSeg->iOrigin1<=(u64)iOrigin && pSeg->iOrigin2>=(u64)iOrigin ){
+          if( bFound==0 ){
+            pSeg->nEntryTombstone++;
+            bFound = 1;
+          }
+          fts5IndexTombstoneAdd(p, pSeg, iRowid);
+        }
+      }
+    }
+    fts5StructureRelease(pStruct);
+  }
+  return fts5IndexReturn(p);
+}
 
 /*************************************************************************
 **************************************************************************
@@ -6770,13 +7537,14 @@ int sqlite3Fts5IndexIntegrityCheck(Fts5Index *p, u64 cksum, int bUseCksum){
 ** function only.
 */
 
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
 /*
 ** Decode a segment-data rowid from the %_data table. This function is
 ** the opposite of macro FTS5_SEGMENT_ROWID().
 */
 static void fts5DecodeRowid(
   i64 iRowid,                     /* Rowid from %_data table */
+  int *pbTombstone,               /* OUT: Tombstone hash flag */
   int *piSegid,                   /* OUT: Segment id */
   int *pbDlidx,                   /* OUT: Dlidx flag */
   int *piHeight,                  /* OUT: Height */
@@ -6792,13 +7560,16 @@ static void fts5DecodeRowid(
   iRowid >>= FTS5_DATA_DLI_B;
 
   *piSegid = (int)(iRowid & (((i64)1 << FTS5_DATA_ID_B) - 1));
+  iRowid >>= FTS5_DATA_ID_B;
+
+  *pbTombstone = (int)(iRowid & 0x0001);
 }
-#endif /* SQLITE_TEST */
+#endif /* SQLITE_TEST || SQLITE_FTS5_DEBUG */
 
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
 static void fts5DebugRowid(int *pRc, Fts5Buffer *pBuf, i64 iKey){
-  int iSegid, iHeight, iPgno, bDlidx;       /* Rowid compenents */
-  fts5DecodeRowid(iKey, &iSegid, &bDlidx, &iHeight, &iPgno);
+  int iSegid, iHeight, iPgno, bDlidx, bTomb;     /* Rowid compenents */
+  fts5DecodeRowid(iKey, &bTomb, &iSegid, &bDlidx, &iHeight, &iPgno);
 
   if( iSegid==0 ){
     if( iKey==FTS5_AVERAGES_ROWID ){
@@ -6808,14 +7579,16 @@ static void fts5DebugRowid(int *pRc, Fts5Buffer *pBuf, i64 iKey){
     }
   }
   else{
-    sqlite3Fts5BufferAppendPrintf(pRc, pBuf, "{%ssegid=%d h=%d pgno=%d}",
-        bDlidx ? "dlidx " : "", iSegid, iHeight, iPgno
+    sqlite3Fts5BufferAppendPrintf(pRc, pBuf, "{%s%ssegid=%d h=%d pgno=%d}",
+        bDlidx ? "dlidx " : "", 
+        bTomb ? "tombstone " : "", 
+        iSegid, iHeight, iPgno
     );
   }
 }
-#endif /* SQLITE_TEST */
+#endif /* SQLITE_TEST || SQLITE_FTS5_DEBUG */
 
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
 static void fts5DebugStructure(
   int *pRc,                       /* IN/OUT: error code */
   Fts5Buffer *pBuf,
@@ -6830,16 +7603,22 @@ static void fts5DebugStructure(
     );
     for(iSeg=0; iSegnSeg; iSeg++){
       Fts5StructureSegment *pSeg = &pLvl->aSeg[iSeg];
-      sqlite3Fts5BufferAppendPrintf(pRc, pBuf, " {id=%d leaves=%d..%d}", 
+      sqlite3Fts5BufferAppendPrintf(pRc, pBuf, " {id=%d leaves=%d..%d",
           pSeg->iSegid, pSeg->pgnoFirst, pSeg->pgnoLast
       );
+      if( pSeg->iOrigin1>0 ){
+        sqlite3Fts5BufferAppendPrintf(pRc, pBuf, " origin=%lld..%lld",
+            pSeg->iOrigin1, pSeg->iOrigin2
+        );
+      }
+      sqlite3Fts5BufferAppendPrintf(pRc, pBuf, "}");
     }
     sqlite3Fts5BufferAppendPrintf(pRc, pBuf, "}");
   }
 }
-#endif /* SQLITE_TEST */
+#endif /* SQLITE_TEST || SQLITE_FTS5_DEBUG */
 
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
 /*
 ** This is part of the fts5_decode() debugging aid.
 **
@@ -6864,9 +7643,9 @@ static void fts5DecodeStructure(
   fts5DebugStructure(pRc, pBuf, p);
   fts5StructureRelease(p);
 }
-#endif /* SQLITE_TEST */
+#endif /* SQLITE_TEST || SQLITE_FTS5_DEBUG */
 
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
 /*
 ** This is part of the fts5_decode() debugging aid.
 **
@@ -6889,9 +7668,9 @@ static void fts5DecodeAverages(
     zSpace = " ";
   }
 }
-#endif /* SQLITE_TEST */
+#endif /* SQLITE_TEST || SQLITE_FTS5_DEBUG */
 
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
 /*
 ** Buffer (a/n) is assumed to contain a list of serialized varints. Read
 ** each varint and append its string representation to buffer pBuf. Return
@@ -6908,9 +7687,9 @@ static int fts5DecodePoslist(int *pRc, Fts5Buffer *pBuf, const u8 *a, int n){
   }
   return iOff;
 }
-#endif /* SQLITE_TEST */
+#endif /* SQLITE_TEST || SQLITE_FTS5_DEBUG */
 
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
 /*
 ** The start of buffer (a/n) contains the start of a doclist. The doclist
 ** may or may not finish within the buffer. This function appends a text
@@ -6943,9 +7722,9 @@ static int fts5DecodeDoclist(int *pRc, Fts5Buffer *pBuf, const u8 *a, int n){
 
   return iOff;
 }
-#endif /* SQLITE_TEST */
+#endif /* SQLITE_TEST || SQLITE_FTS5_DEBUG */
 
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
 /*
 ** This function is part of the fts5_decode() debugging function. It is 
 ** only ever used with detail=none tables.
@@ -6986,9 +7765,9 @@ static void fts5DecodeRowidList(
     sqlite3Fts5BufferAppendPrintf(pRc, pBuf, " %lld%s", iRowid, zApp);
   }
 }
-#endif /* SQLITE_TEST */
+#endif /* SQLITE_TEST || SQLITE_FTS5_DEBUG */
 
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
 /*
 ** The implementation of user-defined scalar function fts5_decode().
 */
@@ -6999,6 +7778,7 @@ static void fts5DecodeFunction(
 ){
   i64 iRowid;                     /* Rowid for record being decoded */
   int iSegid,iHeight,iPgno,bDlidx;/* Rowid components */
+  int bTomb;
   const u8 *aBlob; int n;         /* Record to decode */
   u8 *a = 0;
   Fts5Buffer s;                   /* Build up text to return here */
@@ -7021,7 +7801,7 @@ static void fts5DecodeFunction(
   if( a==0 ) goto decode_out;
   if( n>0 ) memcpy(a, aBlob, n);
 
-  fts5DecodeRowid(iRowid, &iSegid, &bDlidx, &iHeight, &iPgno);
+  fts5DecodeRowid(iRowid, &bTomb, &iSegid, &bDlidx, &iHeight, &iPgno);
 
   fts5DebugRowid(&rc, &s, iRowid);
   if( bDlidx ){
@@ -7040,6 +7820,28 @@ static void fts5DecodeFunction(
           " %d(%lld)", lvl.iLeafPgno, lvl.iRowid
       );
     }
+  }else if( bTomb ){
+    u32 nElem  = fts5GetU32(&a[4]);
+    int szKey = (aBlob[0]==4 || aBlob[0]==8) ? aBlob[0] : 8;
+    int nSlot = (n - 8) / szKey;
+    int ii;
+    sqlite3Fts5BufferAppendPrintf(&rc, &s, " nElem=%d", (int)nElem);
+    if( aBlob[1] ){
+      sqlite3Fts5BufferAppendPrintf(&rc, &s, " 0");
+    }
+    for(ii=0; iiszLeaf ){
+        rc = FTS5_CORRUPT;
+      }else{
+        fts5DecodeRowidList(&rc, &s, &a[iOff], iTermOff-iOff);
+      }
       iOff = iTermOff;
       if( iOffestimatedCost = (double)100;
+  pIdxInfo->estimatedRows = 100;
+  pIdxInfo->idxNum = 0;
+  for(i=0, p=pIdxInfo->aConstraint; inConstraint; i++, p++){
+    if( p->usable==0 ) continue;
+    if( p->op==SQLITE_INDEX_CONSTRAINT_EQ && p->iColumn==11 ){
+      rc = SQLITE_OK;
+      pIdxInfo->aConstraintUsage[i].omit = 1;
+      pIdxInfo->aConstraintUsage[i].argvIndex = 1;
+      break;
+    }
+  }
+  return rc;
+}
+
+/*
+** This method is the destructor for bytecodevtab objects.
+*/
+static int fts5structDisconnectMethod(sqlite3_vtab *pVtab){
+  Fts5StructVtab *p = (Fts5StructVtab*)pVtab;
+  sqlite3_free(p);
+  return SQLITE_OK;
+}
+
+/*
+** Constructor for a new bytecodevtab_cursor object.
+*/
+static int fts5structOpenMethod(sqlite3_vtab *p, sqlite3_vtab_cursor **ppCsr){
+  int rc = SQLITE_OK;
+  Fts5StructVcsr *pNew = 0;
+
+  pNew = sqlite3Fts5MallocZero(&rc, sizeof(*pNew));
+  *ppCsr = (sqlite3_vtab_cursor*)pNew;
+
+  return SQLITE_OK;
+}
+
+/*
+** Destructor for a bytecodevtab_cursor.
+*/
+static int fts5structCloseMethod(sqlite3_vtab_cursor *cur){
+  Fts5StructVcsr *pCsr = (Fts5StructVcsr*)cur;
+  fts5StructureRelease(pCsr->pStruct);
+  sqlite3_free(pCsr);
+  return SQLITE_OK;
+}
+
+
+/*
+** Advance a bytecodevtab_cursor to its next row of output.
+*/
+static int fts5structNextMethod(sqlite3_vtab_cursor *cur){
+  Fts5StructVcsr *pCsr = (Fts5StructVcsr*)cur;
+  Fts5Structure *p = pCsr->pStruct;
+
+  assert( pCsr->pStruct );
+  pCsr->iSeg++;
+  pCsr->iRowid++;
+  while( pCsr->iLevelnLevel && pCsr->iSeg>=p->aLevel[pCsr->iLevel].nSeg ){
+    pCsr->iLevel++;
+    pCsr->iSeg = 0;
+  }
+  if( pCsr->iLevel>=p->nLevel ){
+    fts5StructureRelease(pCsr->pStruct);
+    pCsr->pStruct = 0;
+  }
+  return SQLITE_OK;
+}
+
+/*
+** Return TRUE if the cursor has been moved off of the last
+** row of output.
+*/
+static int fts5structEofMethod(sqlite3_vtab_cursor *cur){
+  Fts5StructVcsr *pCsr = (Fts5StructVcsr*)cur;
+  return pCsr->pStruct==0;
+}
+
+static int fts5structRowidMethod(
+  sqlite3_vtab_cursor *cur, 
+  sqlite_int64 *piRowid
+){
+  Fts5StructVcsr *pCsr = (Fts5StructVcsr*)cur;
+  *piRowid = pCsr->iRowid;
+  return SQLITE_OK;
+}
+
+/*
+** Return values of columns for the row at which the bytecodevtab_cursor
+** is currently pointing.
+*/
+static int fts5structColumnMethod(
+  sqlite3_vtab_cursor *cur,   /* The cursor */
+  sqlite3_context *ctx,       /* First argument to sqlite3_result_...() */
+  int i                       /* Which column to return */
+){
+  Fts5StructVcsr *pCsr = (Fts5StructVcsr*)cur;
+  Fts5Structure *p = pCsr->pStruct;
+  Fts5StructureSegment *pSeg = &p->aLevel[pCsr->iLevel].aSeg[pCsr->iSeg];
+
+  switch( i ){
+    case 0: /* level */
+      sqlite3_result_int(ctx, pCsr->iLevel);
+      break;
+    case 1: /* segment */
+      sqlite3_result_int(ctx, pCsr->iSeg);
+      break;
+    case 2: /* merge */
+      sqlite3_result_int(ctx, pCsr->iSeg < p->aLevel[pCsr->iLevel].nMerge);
+      break;
+    case 3: /* segid */
+      sqlite3_result_int(ctx, pSeg->iSegid);
+      break;
+    case 4: /* leaf1 */
+      sqlite3_result_int(ctx, pSeg->pgnoFirst);
+      break;
+    case 5: /* leaf2 */
+      sqlite3_result_int(ctx, pSeg->pgnoLast);
+      break;
+    case 6: /* origin1 */
+      sqlite3_result_int64(ctx, pSeg->iOrigin1);
+      break;
+    case 7: /* origin2 */
+      sqlite3_result_int64(ctx, pSeg->iOrigin2);
+      break;
+    case 8: /* npgtombstone */
+      sqlite3_result_int(ctx, pSeg->nPgTombstone);
+      break;
+    case 9: /* nentrytombstone */
+      sqlite3_result_int64(ctx, pSeg->nEntryTombstone);
+      break;
+    case 10: /* nentry */
+      sqlite3_result_int64(ctx, pSeg->nEntry);
+      break;
+  }
+  return SQLITE_OK;
+}
+
+/*
+** Initialize a cursor.
+**
+**    idxNum==0     means show all subprograms
+**    idxNum==1     means show only the main bytecode and omit subprograms.
+*/
+static int fts5structFilterMethod(
+  sqlite3_vtab_cursor *pVtabCursor, 
+  int idxNum, const char *idxStr,
+  int argc, sqlite3_value **argv
+){
+  Fts5StructVcsr *pCsr = (Fts5StructVcsr *)pVtabCursor;
+  int rc = SQLITE_OK;
+
+  const u8 *aBlob = 0;
+  int nBlob = 0;
+
+  assert( argc==1 );
+  fts5StructureRelease(pCsr->pStruct);
+  pCsr->pStruct = 0;
+
+  nBlob = sqlite3_value_bytes(argv[0]);
+  aBlob = (const u8*)sqlite3_value_blob(argv[0]);
+  rc = fts5StructureDecode(aBlob, nBlob, 0, &pCsr->pStruct);
+  if( rc==SQLITE_OK ){
+    pCsr->iLevel = 0;
+    pCsr->iRowid = 0;
+    pCsr->iSeg = -1;
+    rc = fts5structNextMethod(pVtabCursor);
+  }
+
+  return rc;
+}
+
+#endif /* SQLITE_TEST || SQLITE_FTS5_DEBUG */
 
 /*
 ** This is called as part of registering the FTS5 module with database
@@ -7244,7 +8277,7 @@ static void fts5RowidFunction(
 ** SQLite error code is returned instead.
 */
 int sqlite3Fts5IndexInit(sqlite3 *db){
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
   int rc = sqlite3_create_function(
       db, "fts5_decode", 2, SQLITE_UTF8, 0, fts5DecodeFunction, 0, 0
   );
@@ -7261,6 +8294,37 @@ int sqlite3Fts5IndexInit(sqlite3 *db){
         db, "fts5_rowid", -1, SQLITE_UTF8, 0, fts5RowidFunction, 0, 0
     );
   }
+
+  if( rc==SQLITE_OK ){
+    static const sqlite3_module fts5structure_module = {
+      0,                           /* iVersion      */
+      0,                           /* xCreate       */
+      fts5structConnectMethod,     /* xConnect      */
+      fts5structBestIndexMethod,   /* xBestIndex    */
+      fts5structDisconnectMethod,  /* xDisconnect   */
+      0,                           /* xDestroy      */
+      fts5structOpenMethod,        /* xOpen         */
+      fts5structCloseMethod,       /* xClose        */
+      fts5structFilterMethod,      /* xFilter       */
+      fts5structNextMethod,        /* xNext         */
+      fts5structEofMethod,         /* xEof          */
+      fts5structColumnMethod,      /* xColumn       */
+      fts5structRowidMethod,       /* xRowid        */
+      0,                           /* xUpdate       */
+      0,                           /* xBegin        */
+      0,                           /* xSync         */
+      0,                           /* xCommit       */
+      0,                           /* xRollback     */
+      0,                           /* xFindFunction */
+      0,                           /* xRename       */
+      0,                           /* xSavepoint    */
+      0,                           /* xRelease      */
+      0,                           /* xRollbackTo   */
+      0,                           /* xShadowName   */
+      0                            /* xIntegrity    */
+    };
+    rc = sqlite3_create_module(db, "fts5_structure", &fts5structure_module, 0);
+  }
   return rc;
 #else
   return SQLITE_OK;
diff --git a/sqlite/ext/fts5/fts5_main.c b/sqlite/ext/fts5/fts5_main.c
index 13921ce4..6e86ca59 100644
--- a/sqlite/ext/fts5/fts5_main.c
+++ b/sqlite/ext/fts5/fts5_main.c
@@ -117,6 +117,8 @@ struct Fts5FullTable {
   Fts5Storage *pStorage;          /* Document store */
   Fts5Global *pGlobal;            /* Global (connection wide) data */
   Fts5Cursor *pSortCsr;           /* Sort data from this cursor */
+  int iSavepoint;                 /* Successful xSavepoint()+1 */
+  int bInSavepoint;
 #ifdef SQLITE_DEBUG
   struct Fts5TransactionState ts;
 #endif
@@ -405,6 +407,13 @@ static int fts5InitVtab(
     pConfig->pzErrmsg = 0;
   }
 
+  if( rc==SQLITE_OK && pConfig->eContent==FTS5_CONTENT_NORMAL ){
+    rc = sqlite3_vtab_config(db, SQLITE_VTAB_CONSTRAINT_SUPPORT, (int)1);
+  }
+  if( rc==SQLITE_OK ){
+    rc = sqlite3_vtab_config(db, SQLITE_VTAB_INNOCUOUS);
+  }
+
   if( rc!=SQLITE_OK ){
     fts5FreeVtab(pTab);
     pTab = 0;
@@ -1329,6 +1338,9 @@ static int fts5FilterMethod(
     pCsr->iFirstRowid = fts5GetRowidLimit(pRowidGe, SMALLEST_INT64);
   }
 
+  rc = sqlite3Fts5IndexLoadConfig(pTab->p.pIndex);
+  if( rc!=SQLITE_OK ) goto filter_out;
+
   if( pTab->pSortCsr ){
     /* If pSortCsr is non-NULL, then this call is being made as part of 
     ** processing for a "... MATCH  ORDER BY rank" query (ePlan is
@@ -1351,6 +1363,7 @@ static int fts5FilterMethod(
     pCsr->pExpr = pTab->pSortCsr->pExpr;
     rc = fts5CursorFirst(pTab, pCsr, bDesc);
   }else if( pCsr->pExpr ){
+    assert( rc==SQLITE_OK );
     rc = fts5CursorParseRank(pConfig, pCsr, pRank);
     if( rc==SQLITE_OK ){
       if( bOrderByRank ){
@@ -1522,6 +1535,7 @@ static int fts5SpecialInsert(
   Fts5Config *pConfig = pTab->p.pConfig;
   int rc = SQLITE_OK;
   int bError = 0;
+  int bLoadConfig = 0;
 
   if( 0==sqlite3_stricmp("delete-all", zCmd) ){
     if( pConfig->eContent==FTS5_CONTENT_NORMAL ){
@@ -1533,6 +1547,7 @@ static int fts5SpecialInsert(
     }else{
       rc = sqlite3Fts5StorageDeleteAll(pTab->pStorage);
     }
+    bLoadConfig = 1;
   }else if( 0==sqlite3_stricmp("rebuild", zCmd) ){
     if( pConfig->eContent==FTS5_CONTENT_NONE ){
       fts5SetVtabError(pTab, 
@@ -1542,6 +1557,7 @@ static int fts5SpecialInsert(
     }else{
       rc = sqlite3Fts5StorageRebuild(pTab->pStorage);
     }
+    bLoadConfig = 1;
   }else if( 0==sqlite3_stricmp("optimize", zCmd) ){
     rc = sqlite3Fts5StorageOptimize(pTab->pStorage);
   }else if( 0==sqlite3_stricmp("merge", zCmd) ){
@@ -1554,6 +1570,8 @@ static int fts5SpecialInsert(
   }else if( 0==sqlite3_stricmp("prefix-index", zCmd) ){
     pConfig->bPrefixIndex = sqlite3_value_int(pVal);
 #endif
+  }else if( 0==sqlite3_stricmp("flush", zCmd) ){
+    rc = sqlite3Fts5FlushToDisk(&pTab->p);
   }else{
     rc = sqlite3Fts5IndexLoadConfig(pTab->p.pIndex);
     if( rc==SQLITE_OK ){
@@ -1567,6 +1585,12 @@ static int fts5SpecialInsert(
       }
     }
   }
+
+  if( rc==SQLITE_OK && bLoadConfig ){
+    pTab->p.pConfig->iCookie--;
+    rc = sqlite3Fts5IndexLoadConfig(pTab->p.pIndex);
+  }
+
   return rc;
 }
 
@@ -1624,7 +1648,6 @@ static int fts5UpdateMethod(
   int eType0;                     /* value_type() of apVal[0] */
   int rc = SQLITE_OK;             /* Return code */
   int bUpdateOrDelete = 0;
-  
 
   /* A transaction must be open when this is called. */
   assert( pTab->ts.eState==1 || pTab->ts.eState==2 );
@@ -1654,7 +1677,14 @@ static int fts5UpdateMethod(
     if( pConfig->eContent!=FTS5_CONTENT_NORMAL 
       && 0==sqlite3_stricmp("delete", z) 
     ){
-      rc = fts5SpecialDelete(pTab, apVal);
+      if( pConfig->bContentlessDelete ){
+        fts5SetVtabError(pTab, 
+            "'delete' may not be used with a contentless_delete=1 table"
+        );
+        rc = SQLITE_ERROR;
+      }else{
+        rc = fts5SpecialDelete(pTab, apVal);
+      }
     }else{
       rc = fts5SpecialInsert(pTab, z, apVal[2 + pConfig->nCol + 1]);
     }
@@ -1671,7 +1701,7 @@ static int fts5UpdateMethod(
     ** Cases 3 and 4 may violate the rowid constraint.
     */
     int eConflict = SQLITE_ABORT;
-    if( pConfig->eContent==FTS5_CONTENT_NORMAL ){
+    if( pConfig->eContent==FTS5_CONTENT_NORMAL || pConfig->bContentlessDelete ){
       eConflict = sqlite3_vtab_on_conflict(pConfig->db);
     }
 
@@ -1679,8 +1709,12 @@ static int fts5UpdateMethod(
     assert( nArg!=1 || eType0==SQLITE_INTEGER );
 
     /* Filter out attempts to run UPDATE or DELETE on contentless tables.
-    ** This is not suported.  */
-    if( eType0==SQLITE_INTEGER && fts5IsContentless(pTab) ){
+    ** This is not suported. Except - they are both supported if the CREATE
+    ** VIRTUAL TABLE statement contained "contentless_delete=1". */
+    if( eType0==SQLITE_INTEGER 
+     && pConfig->eContent==FTS5_CONTENT_NONE 
+     && pConfig->bContentlessDelete==0
+    ){
       pTab->p.base.zErrMsg = sqlite3_mprintf(
           "cannot %s contentless fts5 table: %s", 
           (nArg>1 ? "UPDATE" : "DELETE from"), pConfig->zName
@@ -1704,7 +1738,8 @@ static int fts5UpdateMethod(
       }
 
       else if( eType0!=SQLITE_INTEGER ){     
-        /* If this is a REPLACE, first remove the current entry (if any) */
+        /* An INSERT statement. If the conflict-mode is REPLACE, first remove
+        ** the current entry (if any). */
         if( eConflict==SQLITE_REPLACE && eType1==SQLITE_INTEGER ){
           i64 iNew = sqlite3_value_int64(apVal[1]);  /* Rowid to delete */
           rc = sqlite3Fts5StorageDelete(pTab->pStorage, iNew, 0);
@@ -1767,8 +1802,7 @@ static int fts5SyncMethod(sqlite3_vtab *pVtab){
   Fts5FullTable *pTab = (Fts5FullTable*)pVtab;
   fts5CheckTransactionState(pTab, FTS5_SYNC, 0);
   pTab->p.pConfig->pzErrmsg = &pTab->p.base.zErrMsg;
-  fts5TripCursors(pTab);
-  rc = sqlite3Fts5StorageSync(pTab->pStorage);
+  rc = sqlite3Fts5FlushToDisk(&pTab->p);
   pTab->p.pConfig->pzErrmsg = 0;
   return rc;
 }
@@ -2535,6 +2569,12 @@ static int fts5ColumnMethod(
       sqlite3_result_value(pCtx, sqlite3_column_value(pCsr->pStmt, iCol+1));
     }
     pConfig->pzErrmsg = 0;
+  }else if( pConfig->bContentlessDelete && sqlite3_vtab_nochange(pCtx) ){
+    char *zErr = sqlite3_mprintf("cannot UPDATE a subset of "
+        "columns on fts5 contentless-delete table: %s", pConfig->zName
+    );
+    sqlite3_result_error(pCtx, zErr, -1);
+    sqlite3_free(zErr);
   }
   return rc;
 }
@@ -2573,8 +2613,12 @@ static int fts5RenameMethod(
   sqlite3_vtab *pVtab,            /* Virtual table handle */
   const char *zName               /* New name of table */
 ){
+  int rc;
   Fts5FullTable *pTab = (Fts5FullTable*)pVtab;
-  return sqlite3Fts5StorageRename(pTab->pStorage, zName);
+  pTab->bInSavepoint = 1;
+  rc = sqlite3Fts5StorageRename(pTab->pStorage, zName);
+  pTab->bInSavepoint = 0;
+  return rc;
 }
 
 int sqlite3Fts5FlushToDisk(Fts5Table *pTab){
@@ -2588,9 +2632,29 @@ int sqlite3Fts5FlushToDisk(Fts5Table *pTab){
 ** Flush the contents of the pending-terms table to disk.
 */
 static int fts5SavepointMethod(sqlite3_vtab *pVtab, int iSavepoint){
-  UNUSED_PARAM(iSavepoint);  /* Call below is a no-op for NDEBUG builds */
-  fts5CheckTransactionState((Fts5FullTable*)pVtab, FTS5_SAVEPOINT, iSavepoint);
-  return sqlite3Fts5FlushToDisk((Fts5Table*)pVtab);
+  Fts5FullTable *pTab = (Fts5FullTable*)pVtab;
+  int rc = SQLITE_OK;
+  char *zSql = 0;
+  fts5CheckTransactionState(pTab, FTS5_SAVEPOINT, iSavepoint);
+
+  if( pTab->bInSavepoint==0 ){
+    zSql = sqlite3_mprintf("INSERT INTO %Q.%Q(%Q) VALUES('flush')",
+        pTab->p.pConfig->zDb, pTab->p.pConfig->zName, pTab->p.pConfig->zName
+    );
+    if( zSql ){
+      pTab->bInSavepoint = 1;
+      rc = sqlite3_exec(pTab->p.pConfig->db, zSql, 0, 0, 0);
+      pTab->bInSavepoint = 0;
+      sqlite3_free(zSql);
+    }else{
+      rc = SQLITE_NOMEM;
+    }
+    if( rc==SQLITE_OK ){
+      pTab->iSavepoint = iSavepoint+1;
+    }
+  }
+
+  return rc;
 }
 
 /*
@@ -2599,9 +2663,16 @@ static int fts5SavepointMethod(sqlite3_vtab *pVtab, int iSavepoint){
 ** This is a no-op.
 */
 static int fts5ReleaseMethod(sqlite3_vtab *pVtab, int iSavepoint){
-  UNUSED_PARAM(iSavepoint);  /* Call below is a no-op for NDEBUG builds */
-  fts5CheckTransactionState((Fts5FullTable*)pVtab, FTS5_RELEASE, iSavepoint);
-  return sqlite3Fts5FlushToDisk((Fts5Table*)pVtab);
+  Fts5FullTable *pTab = (Fts5FullTable*)pVtab;
+  int rc = SQLITE_OK;
+  fts5CheckTransactionState(pTab, FTS5_RELEASE, iSavepoint);
+  if( (iSavepoint+1)iSavepoint ){
+    rc = sqlite3Fts5FlushToDisk(&pTab->p);
+    if( rc==SQLITE_OK ){
+      pTab->iSavepoint = iSavepoint;
+    }
+  }
+  return rc;
 }
 
 /*
@@ -2611,11 +2682,14 @@ static int fts5ReleaseMethod(sqlite3_vtab *pVtab, int iSavepoint){
 */
 static int fts5RollbackToMethod(sqlite3_vtab *pVtab, int iSavepoint){
   Fts5FullTable *pTab = (Fts5FullTable*)pVtab;
-  UNUSED_PARAM(iSavepoint);  /* Call below is a no-op for NDEBUG builds */
+  int rc = SQLITE_OK;
   fts5CheckTransactionState(pTab, FTS5_ROLLBACKTO, iSavepoint);
   fts5TripCursors(pTab);
   pTab->p.pConfig->pgsz = 0;
-  return sqlite3Fts5StorageRollback(pTab->pStorage);
+  if( (iSavepoint+1)<=pTab->iSavepoint ){
+    rc = sqlite3Fts5StorageRollback(pTab->pStorage);
+  }
+  return rc;
 }
 
 /*
@@ -2835,9 +2909,46 @@ static int fts5ShadowName(const char *zName){
   return 0;
 }
 
+/*
+** Run an integrity check on the FTS5 data structures.  Return a string
+** if anything is found amiss.  Return a NULL pointer if everything is
+** OK.
+*/
+static int fts5Integrity(
+  sqlite3_vtab *pVtab,    /* the FTS5 virtual table to check */
+  const char *zSchema,    /* Name of schema in which this table lives */
+  const char *zTabname,   /* Name of the table itself */
+  int isQuick,            /* True if this is a quick-check */
+  char **pzErr            /* Write error message here */
+){
+  Fts5FullTable *pTab = (Fts5FullTable*)pVtab;
+  Fts5Config *pConfig = pTab->p.pConfig;
+  char *zSql;
+  char *zErr = 0;
+  int rc;
+  assert( pzErr!=0 && *pzErr==0 );
+  UNUSED_PARAM(isQuick);
+  zSql = sqlite3_mprintf(
+            "INSERT INTO \"%w\".\"%w\"(\"%w\") VALUES('integrity-check');",
+            zSchema, zTabname, pConfig->zName);
+  if( zSql==0 ) return SQLITE_NOMEM;
+  rc = sqlite3_exec(pConfig->db, zSql, 0, 0, &zErr);
+  sqlite3_free(zSql);
+  if( (rc&0xff)==SQLITE_CORRUPT ){
+    *pzErr = sqlite3_mprintf("malformed inverted index for FTS5 table %s.%s",
+                zSchema, zTabname);
+  }else if( rc!=SQLITE_OK ){
+    *pzErr = sqlite3_mprintf("unable to validate the inverted index for"
+                             " FTS5 table %s.%s: %s",
+                zSchema, zTabname, zErr);
+  }
+  sqlite3_free(zErr);
+  return SQLITE_OK;
+}
+
 static int fts5Init(sqlite3 *db){
   static const sqlite3_module fts5Mod = {
-    /* iVersion      */ 3,
+    /* iVersion      */ 4,
     /* xCreate       */ fts5CreateMethod,
     /* xConnect      */ fts5ConnectMethod,
     /* xBestIndex    */ fts5BestIndexMethod,
@@ -2860,7 +2971,8 @@ static int fts5Init(sqlite3 *db){
     /* xSavepoint    */ fts5SavepointMethod,
     /* xRelease      */ fts5ReleaseMethod,
     /* xRollbackTo   */ fts5RollbackToMethod,
-    /* xShadowName   */ fts5ShadowName
+    /* xShadowName   */ fts5ShadowName,
+    /* xIntegrity    */ fts5Integrity
   };
 
   int rc;
diff --git a/sqlite/ext/fts5/fts5_storage.c b/sqlite/ext/fts5/fts5_storage.c
index 02b98d9e..9480da7c 100644
--- a/sqlite/ext/fts5/fts5_storage.c
+++ b/sqlite/ext/fts5/fts5_storage.c
@@ -77,10 +77,10 @@ static int fts5StorageGetStmt(
       "INSERT INTO %Q.'%q_content' VALUES(%s)",         /* INSERT_CONTENT  */
       "REPLACE INTO %Q.'%q_content' VALUES(%s)",        /* REPLACE_CONTENT */
       "DELETE FROM %Q.'%q_content' WHERE id=?",         /* DELETE_CONTENT  */
-      "REPLACE INTO %Q.'%q_docsize' VALUES(?,?)",       /* REPLACE_DOCSIZE  */
+      "REPLACE INTO %Q.'%q_docsize' VALUES(?,?%s)",     /* REPLACE_DOCSIZE  */
       "DELETE FROM %Q.'%q_docsize' WHERE id=?",         /* DELETE_DOCSIZE  */
 
-      "SELECT sz FROM %Q.'%q_docsize' WHERE id=?",      /* LOOKUP_DOCSIZE  */
+      "SELECT sz%s FROM %Q.'%q_docsize' WHERE id=?",    /* LOOKUP_DOCSIZE  */
 
       "REPLACE INTO %Q.'%q_config' VALUES(?,?)",        /* REPLACE_CONFIG */
       "SELECT %s FROM %s AS T",                         /* SCAN */
@@ -128,6 +128,19 @@ static int fts5StorageGetStmt(
         break;
       }
 
+      case FTS5_STMT_REPLACE_DOCSIZE: 
+        zSql = sqlite3_mprintf(azStmt[eStmt], pC->zDb, pC->zName,
+          (pC->bContentlessDelete ? ",?" : "")
+        );
+        break;
+
+      case FTS5_STMT_LOOKUP_DOCSIZE: 
+        zSql = sqlite3_mprintf(azStmt[eStmt], 
+            (pC->bContentlessDelete ? ",origin" : ""),
+            pC->zDb, pC->zName
+        );
+        break;
+
       default:
         zSql = sqlite3_mprintf(azStmt[eStmt], pC->zDb, pC->zName);
         break;
@@ -317,9 +330,11 @@ int sqlite3Fts5StorageOpen(
     }
 
     if( rc==SQLITE_OK && pConfig->bColumnsize ){
-      rc = sqlite3Fts5CreateTable(
-          pConfig, "docsize", "id INTEGER PRIMARY KEY, sz BLOB", 0, pzErr
-      );
+      const char *zCols = "id INTEGER PRIMARY KEY, sz BLOB";
+      if( pConfig->bContentlessDelete ){
+        zCols = "id INTEGER PRIMARY KEY, sz BLOB, origin INTEGER";
+      }
+      rc = sqlite3Fts5CreateTable(pConfig, "docsize", zCols, 0, pzErr);
     }
     if( rc==SQLITE_OK ){
       rc = sqlite3Fts5CreateTable(
@@ -396,7 +411,7 @@ static int fts5StorageDeleteFromIndex(
 ){
   Fts5Config *pConfig = p->pConfig;
   sqlite3_stmt *pSeek = 0;        /* SELECT to read row iDel from %_data */
-  int rc;                         /* Return code */
+  int rc = SQLITE_OK;             /* Return code */
   int rc2;                        /* sqlite3_reset() return code */
   int iCol;
   Fts5InsertCtx ctx;
@@ -412,7 +427,6 @@ static int fts5StorageDeleteFromIndex(
 
   ctx.pStorage = p;
   ctx.iCol = -1;
-  rc = sqlite3Fts5IndexBeginWrite(p->pIndex, 1, iDel);
   for(iCol=1; rc==SQLITE_OK && iCol<=pConfig->nCol; iCol++){
     if( pConfig->abUnindexed[iCol-1]==0 ){
       const char *zText;
@@ -449,6 +463,37 @@ static int fts5StorageDeleteFromIndex(
   return rc;
 }
 
+/*
+** This function is called to process a DELETE on a contentless_delete=1
+** table. It adds the tombstone required to delete the entry with rowid 
+** iDel. If successful, SQLITE_OK is returned. Or, if an error occurs,
+** an SQLite error code.
+*/
+static int fts5StorageContentlessDelete(Fts5Storage *p, i64 iDel){
+  i64 iOrigin = 0;
+  sqlite3_stmt *pLookup = 0;
+  int rc = SQLITE_OK;
+
+  assert( p->pConfig->bContentlessDelete );
+  assert( p->pConfig->eContent==FTS5_CONTENT_NONE );
+
+  /* Look up the origin of the document in the %_docsize table. Store
+  ** this in stack variable iOrigin.  */
+  rc = fts5StorageGetStmt(p, FTS5_STMT_LOOKUP_DOCSIZE, &pLookup, 0);
+  if( rc==SQLITE_OK ){
+    sqlite3_bind_int64(pLookup, 1, iDel);
+    if( SQLITE_ROW==sqlite3_step(pLookup) ){
+      iOrigin = sqlite3_column_int64(pLookup, 1);
+    }
+    rc = sqlite3_reset(pLookup);
+  }
+
+  if( rc==SQLITE_OK && iOrigin!=0 ){
+    rc = sqlite3Fts5IndexContentlessDelete(p->pIndex, iOrigin, iDel);
+  }
+
+  return rc;
+}
 
 /*
 ** Insert a record into the %_docsize table. Specifically, do:
@@ -469,10 +514,17 @@ static int fts5StorageInsertDocsize(
     rc = fts5StorageGetStmt(p, FTS5_STMT_REPLACE_DOCSIZE, &pReplace, 0);
     if( rc==SQLITE_OK ){
       sqlite3_bind_int64(pReplace, 1, iRowid);
-      sqlite3_bind_blob(pReplace, 2, pBuf->p, pBuf->n, SQLITE_STATIC);
-      sqlite3_step(pReplace);
-      rc = sqlite3_reset(pReplace);
-      sqlite3_bind_null(pReplace, 2);
+      if( p->pConfig->bContentlessDelete ){
+        i64 iOrigin = 0;
+        rc = sqlite3Fts5IndexGetOrigin(p->pIndex, &iOrigin);
+        sqlite3_bind_int64(pReplace, 3, iOrigin);
+      }
+      if( rc==SQLITE_OK ){
+        sqlite3_bind_blob(pReplace, 2, pBuf->p, pBuf->n, SQLITE_STATIC);
+        sqlite3_step(pReplace);
+        rc = sqlite3_reset(pReplace);
+        sqlite3_bind_null(pReplace, 2);
+      }
     }
   }
   return rc;
@@ -536,7 +588,15 @@ int sqlite3Fts5StorageDelete(Fts5Storage *p, i64 iDel, sqlite3_value **apVal){
 
   /* Delete the index records */
   if( rc==SQLITE_OK ){
-    rc = fts5StorageDeleteFromIndex(p, iDel, apVal);
+    rc = sqlite3Fts5IndexBeginWrite(p->pIndex, 1, iDel);
+  }
+
+  if( rc==SQLITE_OK ){
+    if( p->pConfig->bContentlessDelete ){
+      rc = fts5StorageContentlessDelete(p, iDel);
+    }else{
+      rc = fts5StorageDeleteFromIndex(p, iDel, apVal);
+    }
   }
 
   /* Delete the %_docsize record */
@@ -1124,7 +1184,9 @@ int sqlite3Fts5StorageSync(Fts5Storage *p){
   i64 iLastRowid = sqlite3_last_insert_rowid(p->pConfig->db);
   if( p->bTotalsValid ){
     rc = fts5StorageSaveTotals(p);
-    p->bTotalsValid = 0;
+    if( rc==SQLITE_OK ){
+      p->bTotalsValid = 0;
+    }
   }
   if( rc==SQLITE_OK ){
     rc = sqlite3Fts5IndexSync(p->pIndex);
diff --git a/sqlite/ext/fts5/fts5_test_tok.c b/sqlite/ext/fts5/fts5_test_tok.c
index a5d839da..994d304d 100644
--- a/sqlite/ext/fts5/fts5_test_tok.c
+++ b/sqlite/ext/fts5/fts5_test_tok.c
@@ -472,7 +472,8 @@ int sqlite3Fts5TestRegisterTok(sqlite3 *db, fts5_api *pApi){
      0,                           /* xSavepoint    */
      0,                           /* xRelease      */
      0,                           /* xRollbackTo   */
-     0                            /* xShadowName   */
+     0,                           /* xShadowName   */
+     0                            /* xIntegrity    */
   };
   int rc;                         /* Return code */
 
diff --git a/sqlite/ext/fts5/fts5_vocab.c b/sqlite/ext/fts5/fts5_vocab.c
index 18774c4e..d738ada3 100644
--- a/sqlite/ext/fts5/fts5_vocab.c
+++ b/sqlite/ext/fts5/fts5_vocab.c
@@ -783,7 +783,8 @@ int sqlite3Fts5VocabInit(Fts5Global *pGlobal, sqlite3 *db){
     /* xSavepoint    */ 0,
     /* xRelease      */ 0,
     /* xRollbackTo   */ 0,
-    /* xShadowName   */ 0
+    /* xShadowName   */ 0,
+    /* xIntegrity    */ 0
   };
   void *p = (void*)pGlobal;
 
diff --git a/sqlite/ext/fts5/test/fts5aa.test b/sqlite/ext/fts5/test/fts5aa.test
index 59ce4f6a..e1551fc5 100644
--- a/sqlite/ext/fts5/test/fts5aa.test
+++ b/sqlite/ext/fts5/test/fts5aa.test
@@ -65,7 +65,9 @@ foreach w {a b c d e f} {
 
 do_execsql_test 2.4 {
   INSERT INTO t1(t1) VALUES('integrity-check');
-}
+  PRAGMA integrity_check;
+  PRAGMA integrity_check(t1);
+} {ok ok}
 
 
 #-------------------------------------------------------------------------
@@ -88,6 +90,7 @@ foreach {i x y} {
 } {
   do_execsql_test 3.$i.1 { INSERT INTO t1 VALUES($x, $y) }
   do_execsql_test 3.$i.2 { INSERT INTO t1(t1) VALUES('integrity-check') }
+  do_execsql_test 3.$i.3 { PRAGMA integrity_check(t1) } ok
   if {[set_test_counter errors]} break
 }
 
@@ -135,7 +138,7 @@ foreach {i x y} {
    10 {ddd abcde dddd dd c} {dddd c c d abcde}
 } {
   do_execsql_test 5.$i.1 { INSERT INTO t1 VALUES($x, $y) }
-  do_execsql_test 5.$i.2 { INSERT INTO t1(t1) VALUES('integrity-check') }
+  do_execsql_test 5.$i.2 { PRAGMA integrity_check(t1) } ok
   if {[set_test_counter errors]} break
 }
 
diff --git a/sqlite/ext/fts5/test/fts5aux.test b/sqlite/ext/fts5/test/fts5aux.test
index 561067c4..b5a13aea 100644
--- a/sqlite/ext/fts5/test/fts5aux.test
+++ b/sqlite/ext/fts5/test/fts5aux.test
@@ -307,5 +307,31 @@ do_catchsql_test 10.1.4 {
   SELECT group_concat(firstcol(t1), '.') FROM t1 GROUP BY rowid
 } {1 {unable to use function firstcol in the requested context}}
 
-finish_test
+#-------------------------------------------------------------------------
+# Test that xInstCount() works from within an xPhraseQuery() callback.
+#
+reset_db
+
+proc xCallback {cmd} {
+  incr ::hitcount [$cmd xInstCount]
+  return SQLITE_OK
+}
+proc fts5_hitcount {cmd} {
+  set ::hitcount 0
+  $cmd xQueryPhrase 0 xCallback
+  return $::hitcount
+}
+sqlite3_fts5_create_function db fts5_hitcount fts5_hitcount
+
+do_execsql_test 11.1 {
+  CREATE VIRTUAL TABLE x1 USING fts5(z);
+  INSERT INTO x1 VALUES('one two three');
+  INSERT INTO x1 VALUES('one two one three one');
+  INSERT INTO x1 VALUES('one two three');
+}
 
+do_execsql_test 11.2 {
+  SELECT fts5_hitcount(x1) FROM x1('one') LIMIT 1;
+} {5}
+
+finish_test
diff --git a/sqlite/ext/fts5/test/fts5bigid.test b/sqlite/ext/fts5/test/fts5bigid.test
new file mode 100644
index 00000000..ae20ec64
--- /dev/null
+++ b/sqlite/ext/fts5/test/fts5bigid.test
@@ -0,0 +1,62 @@
+# 2023 May 28
+#
+# 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.
+#
+#*************************************************************************
+#
+
+source [file join [file dirname [info script]] fts5_common.tcl]
+set testprefix fts5bigid
+
+# If SQLITE_ENABLE_FTS5 is not defined, omit this file.
+ifcapable !fts5 {
+  finish_test
+  return
+}
+
+set nRow 20000
+
+proc do_ascdesc_test {tn query} {
+  set ::lAsc  [db eval { SELECT rowid FROM x1($query) }] 
+  set ::lDesc [db eval { SELECT rowid FROM x1($query) ORDER BY rowid DESC }] 
+  do_test $tn.1 { lsort -integer $::lAsc } $::lAsc
+  do_test $tn.2 { lsort -integer -decr $::lDesc } $::lDesc
+  do_test $tn.3 { lsort -integer $::lDesc } $::lAsc
+}
+
+do_execsql_test 1.0 {
+  CREATE VIRTUAL TABLE x1 USING fts5(a);
+}
+
+do_test 1.1 {
+  for {set ii 0} {$ii < $nRow} {incr ii} {
+    db eval {
+      REPLACE INTO x1(rowid, a) VALUES(random(), 'movement at the station');
+    }
+  }
+} {}
+
+do_ascdesc_test 1.2 "the"
+
+do_execsql_test 1.3 {
+  DELETE FROM x1
+}
+
+do_test 1.4 {
+  for {set ii 0} {$ii < $nRow} {incr ii} {
+    db eval {
+      INSERT INTO x1(rowid, a) VALUES(
+          $ii + 0x6FFFFFFFFFFFFFFF, 'movement at the station'
+      );
+    }
+  }
+} {}
+
+do_ascdesc_test 1.5 "movement"
+
+finish_test
diff --git a/sqlite/ext/fts5/test/fts5conflict.test b/sqlite/ext/fts5/test/fts5conflict.test
index 644db53a..b5bf0a11 100644
--- a/sqlite/ext/fts5/test/fts5conflict.test
+++ b/sqlite/ext/fts5/test/fts5conflict.test
@@ -65,4 +65,44 @@ do_execsql_test 2.1 {
   INSERT INTO fts_idx(fts_idx) VALUES('integrity-check');
 }
 
+#-------------------------------------------------------------------------
+# Tests for OR IGNORE conflict handling.
+#
+reset_db
+foreach_detail_mode $::testprefix {
+
+  do_execsql_test 3.0 {
+    CREATE VIRTUAL TABLE t1 USING fts5(xyz, detail=%DETAIL%);
+
+    BEGIN;
+    INSERT INTO t1(rowid, xyz) VALUES(13, 'thirteen documents');
+    INSERT INTO t1(rowid, xyz) VALUES(14, 'fourteen documents');
+    INSERT INTO t1(rowid, xyz) VALUES(15, 'fifteen documents');
+    COMMIT;
+  }
+
+  set db_cksum [cksum]
+    foreach {tn sql} {
+    1 {
+      INSERT OR IGNORE INTO t1(rowid, xyz) VALUES(14, 'new text');
+    }
+    2 {
+      UPDATE OR IGNORE t1 SET rowid=13 WHERE rowid=15;
+    }
+    3 {
+      INSERT OR IGNORE INTO t1(rowid, xyz) 
+      SELECT 13, 'some text'
+      UNION ALL
+      SELECT 14, 'some text'
+      UNION ALL
+      SELECT 15, 'some text'
+    }
+  } {
+    do_execsql_test 3.1.$tn.1 $sql
+    do_test 3.1.$tn.2 { cksum } $db_cksum
+  }
+
+}
+
+
 finish_test
diff --git a/sqlite/ext/fts5/test/fts5content.test b/sqlite/ext/fts5/test/fts5content.test
index 74a74e2a..ca2726a9 100644
--- a/sqlite/ext/fts5/test/fts5content.test
+++ b/sqlite/ext/fts5/test/fts5content.test
@@ -294,4 +294,3 @@ do_catchsql_test 7.2.5 {
 } {1 {recursively defined fts5 content table}}
 
 finish_test
-
diff --git a/sqlite/ext/fts5/test/fts5contentless.test b/sqlite/ext/fts5/test/fts5contentless.test
new file mode 100644
index 00000000..48cfd10f
--- /dev/null
+++ b/sqlite/ext/fts5/test/fts5contentless.test
@@ -0,0 +1,270 @@
+# 2014 Dec 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 file contains tests for the content= and content_rowid= options.
+#
+
+source [file join [file dirname [info script]] fts5_common.tcl]
+set testprefix fts5contentless
+
+# If SQLITE_ENABLE_FTS5 is defined, omit this file.
+ifcapable !fts5 {
+  finish_test
+  return
+}
+
+# Check that it is not possible to specify "contentless_delete=1" for 
+# anything other than a contentless table.
+#
+set res(0) {0 {}}
+set res(1) {1 {contentless_delete=1 requires a contentless table}}
+foreach {tn sql bError} {
+  1 "(a, b, contentless_delete=1)"              1
+  2 "(a, b, contentless_delete=1, content=abc)" 1
+  3 "(a, b, contentless_delete=1, content=)"    0
+  4 "(content=, contentless_delete=1, a)"       0
+  5 "(content='', contentless_delete=1, hello)" 0
+} {
+  execsql { BEGIN }
+  do_catchsql_test 1.$tn "CREATE VIRTUAL TABLE t1 USING fts5 $sql" $res($bError)
+  execsql { ROLLBACK }
+}
+
+# Check that it is not possible to specify "contentless_delete=1" 
+# along with columnsize=1. 
+#
+set res(0) {0 {}}
+set res(1) {1 {contentless_delete=1 is incompatible with columnsize=0}}
+foreach {tn sql bError} {
+  2 "(a, b, content='', contentless_delete=1, columnsize=0)" 1 
+} {
+  execsql { BEGIN }
+  do_catchsql_test 1.$tn "CREATE VIRTUAL TABLE t1 USING fts5 $sql" $res($bError)
+  execsql { ROLLBACK }
+}
+
+# Check that if contentless_delete=1 is specified, then the "origin" 
+# column is added to the %_docsize table.
+reset_db
+do_execsql_test 3.0 {
+  CREATE VIRTUAL TABLE x1 USING fts5(c, content='');
+  CREATE VIRTUAL TABLE x2 USING fts5(c, content='', contentless_delete=1);
+}
+do_execsql_test 3.1 {
+  SELECT sql FROM sqlite_schema WHERE name IN ('x1_docsize', 'x2_docsize');
+} {
+  {CREATE TABLE 'x1_docsize'(id INTEGER PRIMARY KEY, sz BLOB)} 
+  {CREATE TABLE 'x2_docsize'(id INTEGER PRIMARY KEY, sz BLOB, origin INTEGER)}
+}
+
+do_execsql_test 3.2.1 {
+  SELECT hex(block) FROM x1_data WHERE id=10
+} {00000000000000}
+do_execsql_test 3.2.2 {
+  SELECT hex(block) FROM x2_data WHERE id=10
+} {00000000FF000001000000}
+
+do_execsql_test 3.3 {
+  INSERT INTO x2 VALUES('first text');
+  INSERT INTO x2 VALUES('second text');
+}
+do_execsql_test 3.4 {
+  SELECT id, origin FROM x2_docsize
+} {1 1 2 2}
+do_execsql_test 3.5 {
+  SELECT level, segment, loc1, loc2 FROM fts5_structure(
+    (SELECT block FROM x2_data WHERE id=10)
+  )
+} {
+  0 0   1 1
+  0 1   2 2
+}
+do_execsql_test 3.6 {
+  INSERT INTO x2(x2) VALUES('optimize');
+}
+do_execsql_test 3.7 {
+  SELECT level, segment, loc1, loc2 FROM fts5_structure(
+    (SELECT block FROM x2_data WHERE id=10)
+  )
+} {
+  1 0   1 2
+}
+
+do_execsql_test 3.8 {
+  DELETE FROM x2 WHERE rowid=2;
+}
+
+do_execsql_test 3.9 {
+  SELECT rowid FROM x2('text')
+} {1}
+
+#--------------------------------------------------------------------------
+reset_db
+proc document {n} {
+  set vocab [list A B C D E F G H I J K L M N O P Q R S T U V W X Y Z]
+  set ret [list]
+  for {set ii 0} {$ii < $n} {incr ii} {
+    lappend ret [lindex $vocab [expr int(rand()*[llength $vocab])]]
+  }
+  set ret
+}
+
+set nRow 1000
+
+do_execsql_test 4.0 {
+  CREATE TABLE t1(x);
+  CREATE VIRTUAL TABLE ft USING fts5(x, content='', contentless_delete=1);
+  INSERT INTO ft(ft, rank) VALUES('pgsz', 100);
+}
+do_test 4.1 {
+  for {set ii 0} {$ii < $nRow} {incr ii} {
+    set doc [document 6]
+    execsql {
+      INSERT INTO t1 VALUES($doc);
+      INSERT INTO ft VALUES($doc);
+    }
+  }
+} {}
+
+foreach v {A B C D E F G H I J K L M N O P Q R S T U V W X Y Z} {
+  set L1 [execsql {SELECT rowid FROM t1 WHERE x LIKE '%'||$v||'%'}]
+  set L2 [execsql {SELECT rowid FROM ft($v)}]
+  do_test 4.2.$v { set L1 } $L2
+}
+
+do_test 4.3 {
+  for {set ii 1} {$ii < $nRow} {incr ii 2} { 
+    execsql {
+       DELETE FROM ft WHERE rowid=$ii;
+       DELETE FROM t1 WHERE rowid=$ii;
+    }
+  }
+} {}
+
+foreach v {A B C D E F G H I J K L M N O P Q R S T U V W X Y Z} {
+  set L1 [execsql {SELECT rowid FROM t1 WHERE x LIKE '%'||$v||'%'}]
+  set L2 [execsql {SELECT rowid FROM ft($v)}]
+  do_test 4.4.$v { set L1 } $L2
+}
+
+do_execsql_test 4.5 {
+  INSERT INTO ft(ft) VALUES('optimize');
+} {}
+
+foreach v {A B C D E F G H I J K L M N O P Q R S T U V W X Y Z} {
+  set L1 [execsql {SELECT rowid FROM t1 WHERE x LIKE '%'||$v||'%'}]
+  set L2 [execsql {SELECT rowid FROM ft($v)}]
+  do_test 4.6.$v { set L1 } $L2
+}
+
+#execsql_pp { SELECT fts5_decode(id, block) FROM ft_data }
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 5.0 {
+  CREATE VIRTUAL TABLE ft USING fts5(x, content='', contentless_delete=1);
+  INSERT INTO ft(rowid, x) VALUES(1, 'one two three');
+  INSERT INTO ft(rowid, x) VALUES(2, 'one two four');
+  INSERT INTO ft(rowid, x) VALUES(3, 'one two five');
+  INSERT INTO ft(rowid, x) VALUES(4, 'one two seven');
+  INSERT INTO ft(rowid, x) VALUES(5, 'one two eight');
+}
+
+do_execsql_test 5.1 {
+  DELETE FROM ft WHERE rowid=2
+}
+
+do_execsql_test 5.2 {
+  SELECT rowid FROM ft
+} {1 3 4 5}
+
+do_catchsql_test 5.3 {
+  UPDATE ft SET x='four six' WHERE rowid=3
+} {0 {}}
+
+do_execsql_test 5.4 {
+  SELECT rowid FROM ft('one');
+} {1 4 5}
+
+do_execsql_test 5.5 {
+  REPLACE INTO ft(rowid, x) VALUES(3, 'four six');
+  SELECT rowid FROM ft('one');
+} {1 4 5}
+
+do_execsql_test 5.6 {
+  REPLACE INTO ft(rowid, x) VALUES(6, 'one two eleven');
+  SELECT rowid FROM ft('one');
+} {1 4 5 6}
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 6.0 {
+  CREATE VIRTUAL TABLE ft USING fts5(x, content='', contentless_delete=1);
+  INSERT INTO ft(rowid, x) VALUES(1, 'one two three');
+  INSERT INTO ft(rowid, x) VALUES(2, 'one two four');
+}
+
+do_test 6.1 {
+  db eval { SELECT rowid FROM ft('one two') } {
+    if {$rowid==1} {
+      db eval { INSERT INTO ft(rowid, x) VALUES(3, 'one two four') }
+    }
+  }
+} {}
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 7.0 {
+  CREATE VIRTUAL TABLE ft USING fts5(x, content='', contentless_delete=1);
+}
+
+set lRowid [list -450 0 1 2 42]
+
+do_test 7.1 {
+  execsql BEGIN
+  foreach r $lRowid {
+    execsql { INSERT INTO ft(rowid, x) VALUES($r, 'one one one'); }
+  }
+  execsql COMMIT
+} {}
+
+do_test 7.2 {
+  execsql BEGIN
+  foreach r $lRowid {
+    execsql { REPLACE INTO ft(rowid, x) VALUES($r, 'two two two'); }
+  }
+  execsql COMMIT
+} {}
+
+do_execsql_test 7.3 { SELECT rowid FROM ft('one'); } {}
+do_execsql_test 7.4 { SELECT rowid FROM ft('two'); } $lRowid
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 8.0 {
+  CREATE VIRTUAL TABLE ft USING fts5(x, content='', contentless_delete=1);
+  INSERT INTO ft VALUES('hello world');
+  INSERT INTO ft VALUES('one two three');
+}
+
+do_catchsql_test 8.1 {
+  INSERT INTO ft(ft, rowid, x) VALUES('delete', 1, 'hello world');
+} {1 {'delete' may not be used with a contentless_delete=1 table}}
+
+do_execsql_test 8.2 {
+  BEGIN;
+    INSERT INTO ft(rowid, x) VALUES(3, 'four four four');
+    DELETE FROM ft WHERE rowid=3;
+  COMMIT;
+  SELECT rowid FROM ft('four');
+} {}
+
+finish_test
diff --git a/sqlite/ext/fts5/test/fts5contentless2.test b/sqlite/ext/fts5/test/fts5contentless2.test
new file mode 100644
index 00000000..fdd7a60f
--- /dev/null
+++ b/sqlite/ext/fts5/test/fts5contentless2.test
@@ -0,0 +1,207 @@
+# 2023 July 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.
+#
+#***********************************************************************
+#
+# This file contains tests for the content= and content_rowid= options.
+#
+
+source [file join [file dirname [info script]] fts5_common.tcl]
+set testprefix fts5contentless2
+
+# If SQLITE_ENABLE_FTS5 is defined, omit this file.
+ifcapable !fts5 {
+  finish_test
+  return
+}
+
+proc vocab {} {
+  list aaa bbb ccc ddd eee fff ggg hhh iii jjj kkk lll mmm nnn ooo ppp
+}
+
+proc document {nToken} {
+  set doc [list]
+  set vocab [vocab]
+  for {set ii 0} {$ii < $nToken} {incr ii} {
+    lappend doc [lindex $vocab [expr int(rand()*[llength $vocab])]]
+  }
+  set doc
+}
+db func document document
+
+proc contains {doc token} {
+  expr {[lsearch $doc $token]>=0}
+}
+db func contains contains
+
+proc do_compare_tables_test {tn} {
+  uplevel [list do_test $tn {
+    foreach v [vocab] {
+      set l1 [execsql { SELECT rowid FROM t1 WHERE contains(doc, $v) }]
+      set l2 [execsql { SELECT rowid FROM t2($v) }]
+      if {$l1!=$l2} { error "1: query mismatch ($l1) ($l2)" }
+
+      set w "[string range $v 0 1]*"
+      set l1 [execsql { SELECT rowid FROM t1 WHERE contains(doc, $w) }]
+      set l2 [execsql { SELECT rowid FROM t2($w) }]
+      if {$l1!=$l2} { error "2: query mismatch ($l1) ($l2)" }
+
+      set w "[string range $v 0 0]*"
+      set l1 [execsql { SELECT rowid FROM t1 WHERE contains(doc, $w) }]
+      set l2 [execsql { SELECT rowid FROM t2($w) }]
+      if {$l1!=$l2} { error "2: query mismatch ($l1) ($l2)" }
+
+      set l1 [execsql { 
+        SELECT rowid FROM t1 WHERE contains(doc, $v) ORDER BY rowid DESC 
+      }]
+      set l2 [execsql { SELECT rowid FROM t2($v) ORDER BY rowid DESC }]
+      if {$l1!=$l2} { error "1: query mismatch ($l1) ($l2)" }
+    }
+    set {} {}
+  } {}]
+}
+
+proc lshuffle {in} {
+  set L [list]
+  set ret [list]
+  foreach elem $in { lappend L [list [expr rand()] $elem] }
+  foreach pair [lsort -index 0 $L] { lappend ret [lindex $pair 1] }
+  set ret
+}
+
+expr srand(0)
+
+do_execsql_test 1.0 {
+  CREATE VIRTUAL TABLE t2 USING fts5(
+      doc, prefix=2, content=, contentless_delete=1
+  );
+
+  CREATE TABLE t1(doc);
+  CREATE TRIGGER tr1 AFTER DELETE ON t1 BEGIN
+    DELETE FROM t2 WHERE rowid = old.rowid;
+  END;
+}
+
+set SMALLEST64 -9223372036854775808
+set LARGEST64   9223372036854775807
+
+foreach {tn r1 r2} {
+  1   0               50
+  2   $SMALLEST64     $SMALLEST64+50
+  3   $LARGEST64-50   $LARGEST64
+  4   -50             -1
+} {
+  set r1 [expr $r1]
+  set r2 [expr $r2]
+
+  do_test 1.1.$tn {
+    execsql BEGIN
+    for {set ii $r1} {$ii <= $r2} {incr ii} {
+      execsql { INSERT INTO t1(rowid, doc) VALUES ($ii, document(8)); }
+    }
+    execsql COMMIT
+  } {}
+}
+do_test 1.2 {
+  db eval { SELECT rowid, doc FROM t1 } {
+    execsql { INSERT INTO t2(rowid, doc) VALUES($rowid, $doc) }
+  }
+} {}
+
+foreach {tn rowid} {
+  1  $SMALLEST64
+  2  0
+  3  -5
+  4  -30
+  5  $LARGEST64
+  6  $LARGEST64-1
+} {
+  set rowid [expr $rowid]
+  do_execsql_test 1.3.$tn.1 {
+    DELETE FROM t1 WHERE rowid=$rowid
+  }
+  do_compare_tables_test 1.3.$tn.2
+}
+
+set iTest 1
+foreach r [lshuffle [execsql {SELECT rowid FROM t1}]] {
+  if {($iTest % 50)==0} {
+    execsql { INSERT INTO t2(t2) VALUES('optimize') }
+  }
+  if {($iTest % 5)==0} {
+    execsql { INSERT INTO t2(t2, rank) VALUES('merge', 5) }
+  }
+  do_execsql_test 1.4.$iTest.1($r) {
+    DELETE FROM t1 WHERE rowid=$r
+  }
+  do_compare_tables_test 1.4.$iTest.2
+  incr iTest
+}
+
+do_execsql_test 1.5 {
+  SELECT * FROM t1
+} {}
+
+#-------------------------------------------------------------------------
+reset_db
+db func document document
+
+do_execsql_test 2.0 {
+  CREATE VIRTUAL TABLE t2 USING fts5(doc, content=, contentless_delete=1);
+  WITH s(i) AS (
+    SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000
+  )
+  INSERT INTO t2(rowid, doc) SELECT i, i || ' ' || i FROM s;
+}
+
+do_execsql_test 2.1 {
+  BEGIN;
+    DELETE FROM t2 WHERE rowid=32;
+    DELETE FROM t2 WHERE rowid=64;
+    DELETE FROM t2 WHERE rowid=96;
+    DELETE FROM t2 WHERE rowid=128;
+    DELETE FROM t2 WHERE rowid=160;
+    DELETE FROM t2 WHERE rowid=192;
+  COMMIT;
+}
+
+do_execsql_test 2.2 {
+  SELECT * FROM t2('128');
+} {}
+
+#-------------------------------------------------------------------------
+
+foreach {tn step} {
+  1     3 
+  2     7
+  3     15
+} {
+  set step [expr $step]
+
+  reset_db
+  db func document document
+  do_execsql_test 3.$tn.0 {
+    CREATE VIRTUAL TABLE t2 USING fts5(doc, content=, contentless_delete=1);
+    INSERT INTO t2(t2, rank) VALUES('pgsz', 100);
+    WITH s(i) AS (
+        SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000
+    )
+    INSERT INTO t2(rowid, doc) SELECT i, i || ' ' || i FROM s;
+  }
+  do_execsql_test 3.$tn.1 {
+    DELETE FROM t2 WHERE (rowid % $step)==0
+  }
+  do_execsql_test 3.$tn.2 {
+    SELECT * FROM t2( $step * 5 )
+  } {}
+}
+
+
+
+finish_test
diff --git a/sqlite/ext/fts5/test/fts5contentless3.test b/sqlite/ext/fts5/test/fts5contentless3.test
new file mode 100644
index 00000000..76119dc5
--- /dev/null
+++ b/sqlite/ext/fts5/test/fts5contentless3.test
@@ -0,0 +1,195 @@
+# 2023 July 21
+#
+# The author disclaims copyright to this source code.  In place of
+# a legal notice, here is a blessing:
+#
+#    May you do good and not evil.
+#    May you find forgiveness for yourself and forgive others.
+#    May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+# This file contains tests for the content= and content_rowid= options.
+#
+
+source [file join [file dirname [info script]] fts5_common.tcl]
+set testprefix fts5contentless3
+
+# If SQLITE_ENABLE_FTS5 is defined, omit this file.
+ifcapable !fts5 {
+  finish_test
+  return
+}
+
+do_execsql_test 1.0 {
+  CREATE VIRTUAL TABLE ft USING fts5(x, content=, contentless_delete=1);
+  BEGIN;
+    INSERT INTO ft VALUES('one one one');
+    INSERT INTO ft VALUES('two two two');
+    INSERT INTO ft VALUES('three three three');
+    INSERT INTO ft VALUES('four four four');
+    INSERT INTO ft VALUES('five five five');
+    INSERT INTO ft VALUES('six six six');
+    INSERT INTO ft VALUES('seven seven seven');
+    INSERT INTO ft VALUES('eight eight eight');
+    INSERT INTO ft VALUES('nine nine nine');
+  COMMIT;
+
+  DELETE FROM ft WHERE rowid=3;
+}
+
+proc myhex {hex} { binary decode hex $hex }
+db func myhex myhex
+
+do_execsql_test 1.1 {
+  UPDATE ft_data SET block = 
+    myhex('04000000 00000001'                   ||
+          '01020304 01020304 01020304 01020304' ||
+          '01020304 01020304 01020304 01020304'
+    )
+  WHERE id = (SELECT max(id) FROM ft_data);
+}
+
+do_execsql_test 1.2 {
+  DELETE FROM ft WHERE rowid=1
+}
+
+do_execsql_test 1.3 {
+  SELECT rowid FROM ft('two');
+} {2}
+
+do_execsql_test 1.3 {
+  UPDATE ft_data SET block = 
+    myhex('08000000 00000001'                                               ||
+      '0000000001020304 0000000001020304 0000000001020304 0000000001020304' ||
+      '0000000001020304 0000000001020304 0000000001020304 0000000001020304'
+    )
+  WHERE id = (SELECT max(id) FROM ft_data);
+}
+
+do_execsql_test 1.4 {
+  SELECT rowid FROM ft('two');
+} {2}
+
+do_execsql_test 1.5 {
+  DELETE FROM ft WHERE rowid=4
+}
+
+do_execsql_test 1.6 {
+  UPDATE ft_data SET block = myhex('04000000 00000000')
+    WHERE id = (SELECT max(id) FROM ft_data);
+}
+do_execsql_test 1.7 {
+  SELECT rowid FROM ft('two');
+} {2}
+
+do_execsql_test 1.8 {
+  UPDATE ft_data SET block = myhex('04000000 00000000')
+    WHERE id = (SELECT max(id) FROM ft_data);
+}
+do_execsql_test 1.9 {
+  DELETE FROM ft WHERE rowid=8
+} {}
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 2.0 {
+  CREATE VIRTUAL TABLE ft USING fts5(x, content=, contentless_delete=1);
+  INSERT INTO ft VALUES('one one one');
+  INSERT INTO ft VALUES('two two two');
+  INSERT INTO ft VALUES('three three three');
+  INSERT INTO ft VALUES('four four four');
+  INSERT INTO ft VALUES('five five five');
+  INSERT INTO ft VALUES('six six six');
+  INSERT INTO ft VALUES('seven seven seven');
+  INSERT INTO ft VALUES('eight eight eight');
+  INSERT INTO ft VALUES('nine nine nine');
+}
+
+do_execsql_test 2.1 {
+  INSERT INTO ft(ft) VALUES('optimize');
+}
+do_execsql_test 2.2 {
+  SELECT count(*) FROM ft_data
+} {3}
+do_execsql_test 2.3 {
+  DELETE FROM ft WHERE rowid=5
+}
+do_execsql_test 2.4 {
+  SELECT count(*) FROM ft_data
+} {4}
+
+# Check that an 'optimize' works (rewrites the index) if there is a single
+# segment with one or more tombstone hash pages.
+do_execsql_test 2.5 {
+  INSERT INTO ft(ft) VALUES('optimize');
+}
+do_execsql_test 2.6 {
+  SELECT count(*) FROM ft_data
+} {3}
+
+# Check that an 'optimize' is a no-op if there is a single segment
+# and no tombstone hash pages.
+do_execsql_test 2.7 {
+  INSERT INTO ft(ft) VALUES('optimize');
+  SELECT rowid FROM ft_data;
+} [db eval {SELECT rowid FROM ft_data}]
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 3.0 {
+  CREATE VIRTUAL TABLE ft USING fts5(x, content=, contentless_delete=1);
+  INSERT INTO ft(ft, rank) VALUES('pgsz', 64);
+  WITH s(i) AS (
+    SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000
+  )
+  INSERT INTO ft(rowid, x) SELECT i, i||' '||i||' '||i||' '||i FROM s;
+  INSERT INTO ft(ft) VALUES('optimize');
+}
+
+do_execsql_test 3.1 {
+  SELECT count(*) FROM ft_data
+} {200}
+
+do_execsql_test 3.2 {
+  DELETE FROM ft WHERE (rowid % 50)==0;
+  SELECT count(*) FROM ft_data;
+} {203}
+
+do_execsql_test 3.3 {
+  INSERT INTO ft(ft, rank) VALUES('merge', 500);
+  SELECT rowid FROM ft_data;
+} [db eval {SELECT rowid FROM ft_data}]
+
+do_execsql_test 3.4 {
+  INSERT INTO ft(ft, rank) VALUES('merge', -1000);
+  SELECT count(*) FROM ft_data;
+} {197}
+
+do_execsql_test 3.5 {
+  DELETE FROM ft WHERE (rowid % 50)==1;
+  SELECT count(*) FROM ft_data;
+} {200}
+
+do_execsql_test 3.6 {
+  SELECT level, segment, npgtombstone FROM fts5_structure(
+    (SELECT block FROM ft_data WHERE id=10)
+  )
+} {1 0 3}
+
+do_test 3.6 {
+  while 1 {
+    set nChange [db total_changes]
+    execsql { INSERT INTO ft(ft, rank) VALUES('merge', -5) }
+    if {([db total_changes] - $nChange)<2} break
+  }
+} {}
+
+do_execsql_test 3.7 {
+  SELECT level, segment, npgtombstone FROM fts5_structure(
+    (SELECT block FROM ft_data WHERE id=10)
+  )
+} {2 0 0}
+
+
+finish_test
diff --git a/sqlite/ext/fts5/test/fts5contentless4.test b/sqlite/ext/fts5/test/fts5contentless4.test
new file mode 100644
index 00000000..702b33a9
--- /dev/null
+++ b/sqlite/ext/fts5/test/fts5contentless4.test
@@ -0,0 +1,247 @@
+# 2023 July 21
+#
+# The author disclaims copyright to this source code.  In place of
+# a legal notice, here is a blessing:
+#
+#    May you do good and not evil.
+#    May you find forgiveness for yourself and forgive others.
+#    May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+# This file contains tests for the content= and content_rowid= options.
+#
+
+source [file join [file dirname [info script]] fts5_common.tcl]
+set testprefix fts5contentless4
+
+# If SQLITE_ENABLE_FTS5 is defined, omit this file.
+ifcapable !fts5 {
+  finish_test
+  return
+}
+
+proc document {n} {
+  set vocab [list A B C D E F G H I J K L M N O P Q R S T U V W X Y Z]
+  set ret [list]
+  for {set ii 0} {$ii < $n} {incr ii} {
+    lappend ret [lindex $vocab [expr int(rand()*[llength $vocab])]]
+  }
+  set ret
+}
+db func document document
+
+do_execsql_test 1.0 {
+  CREATE VIRTUAL TABLE ft USING fts5(x, content='', contentless_delete=1);
+  INSERT INTO ft(ft, rank) VALUES('pgsz', 240);
+  WITH s(i) AS (
+    SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000
+  )
+  INSERT INTO ft SELECT document(12) FROM s;
+}
+
+do_execsql_test 1.1 {
+  INSERT INTO ft(ft) VALUES('optimize');
+}
+
+do_execsql_test 1.2 {
+  SELECT level, segment, nentry, nentrytombstone FROM fts5_structure((
+    SELECT block FROM ft_data WHERE id=10
+  ))
+} {0 0 1000 0}
+
+do_execsql_test 1.3 {
+  DELETE FROM ft WHERE rowid < 50
+}
+
+do_execsql_test 1.4 {
+  SELECT level, segment, nentry, nentrytombstone FROM fts5_structure((
+    SELECT block FROM ft_data WHERE id=10
+  ))
+} {0 0 1000 49}
+
+do_execsql_test 1.5 {
+  DELETE FROM ft WHERE rowid < 1000
+}
+
+do_execsql_test 1.6 {
+  SELECT level, segment, nentry, nentrytombstone FROM fts5_structure((
+    SELECT block FROM ft_data WHERE id=10
+  ))
+} {1 0 1 0}
+
+#--------------------------------------------------------------------------
+reset_db
+db func document document
+
+do_execsql_test 2.0 {
+  CREATE VIRTUAL TABLE ft USING fts5(x, content='', contentless_delete=1);
+}
+
+do_test 2.1 {
+  for {set ii 0} {$ii < 5000} {incr ii} {
+    execsql { INSERT INTO ft VALUES( document(12) ) }
+  }
+} {}
+
+do_execsql_test 2.2 {
+  SELECT sum(nentry) - sum(nentrytombstone) FROM fts5_structure((
+    SELECT block FROM ft_data WHERE id=10
+  ))
+} {5000}
+
+for {set ii 5000} {$ii >= 0} {incr ii -100} {
+  do_execsql_test 2.3.$ii {
+    DELETE FROM ft WHERE rowid > $ii
+  }
+  do_execsql_test 2.3.$ii.2 {
+    SELECT 
+      CAST((total(nentry) - total(nentrytombstone)) AS integer) 
+    FROM 
+      fts5_structure( (SELECT block FROM ft_data WHERE id=10) )
+  } $ii
+}
+
+execsql_pp {
+  SELECT * FROM fts5_structure((
+    SELECT block FROM ft_data WHERE id=10
+  ))
+}
+
+do_test 2.4 {
+  for {set ii 0} {$ii < 5000} {incr ii} {
+    execsql { INSERT INTO ft VALUES( document(12) ) }
+  }
+} {}
+
+for {set ii 1} {$ii <= 5000} {incr ii 10} {
+  do_execsql_test 2.3.$ii {
+    DELETE FROM ft WHERE rowid = $ii;
+    INSERT INTO ft VALUES( document(12) );
+    INSERT INTO ft(ft, rank) VALUES('merge', -10);
+  }
+
+  do_execsql_test 2.3.$ii.2 {
+    SELECT 
+      CAST((total(nentry) - total(nentrytombstone)) AS integer) 
+    FROM 
+      fts5_structure( (SELECT block FROM ft_data WHERE id=10) )
+  } 5000
+}
+
+#-------------------------------------------------------------------------
+reset_db
+db func document document
+do_execsql_test 3.0 {
+  CREATE VIRTUAL TABLE ft USING fts5(x, content='', contentless_delete=1);
+  WITH s(i) AS (
+    SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<100
+  )
+  INSERT INTO ft SELECT document(12) FROM s;
+}
+
+do_catchsql_test 3.1 {
+  INSERT INTO ft(ft, rank) VALUES('deletemerge', 'text');
+} {1 {SQL logic error}}
+do_catchsql_test 3.2 {
+  INSERT INTO ft(ft, rank) VALUES('deletemerge', 50);
+} {0 {}}
+do_execsql_test 3.3 {
+  SELECT * FROM ft_config WHERE k='deletemerge'
+} {deletemerge 50}
+do_catchsql_test 3.4 {
+  INSERT INTO ft(ft, rank) VALUES('deletemerge', 101);
+} {0 {}}
+do_execsql_test 3.5 {
+  SELECT * FROM ft_config WHERE k='deletemerge'
+} {deletemerge 101}
+
+do_execsql_test 3.6 {
+  DELETE FROM ft WHERE rowid<95
+}
+
+do_execsql_test 3.7 {
+  SELECT nentrytombstone, nentry FROM fts5_structure((
+    SELECT block FROM ft_data WHERE id=10
+  ))
+} {94 100}
+
+do_execsql_test 3.8 {
+  DELETE FROM ft WHERE rowid=95
+}
+
+do_execsql_test 3.9 {
+  SELECT nentrytombstone, nentry FROM fts5_structure((
+    SELECT block FROM ft_data WHERE id=10
+  ))
+} {95 100}
+
+do_execsql_test 3.10 {
+  DELETE FROM ft;
+  WITH s(i) AS (
+    SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<100
+  )
+  INSERT INTO ft SELECT document(12) FROM s;
+  INSERT INTO ft(ft, rank) VALUES('deletemerge', 50);
+}
+
+do_execsql_test 3.11 {
+  DELETE FROM ft WHERE rowid<95
+}
+
+do_execsql_test 3.12 {
+  SELECT nentrytombstone, nentry FROM fts5_structure((
+    SELECT block FROM ft_data WHERE id=10
+  ))
+} {0 6}
+
+#-------------------------------------------------------------------------
+reset_db
+db func document document
+do_execsql_test 4.0 {
+  CREATE VIRTUAL TABLE x1 USING fts5(x, content='', contentless_delete=1);
+  INSERT INTO x1(x1, rank) VALUES('usermerge', 16);
+  INSERT INTO x1(x1, rank) VALUES('deletemerge', 40);
+  INSERT INTO x1 VALUES('one');
+  INSERT INTO x1 VALUES('two');
+  INSERT INTO x1 VALUES('three');
+  INSERT INTO x1 VALUES('four');
+  INSERT INTO x1 VALUES('five');
+  INSERT INTO x1 VALUES('six');
+  INSERT INTO x1 VALUES('seven');
+  INSERT INTO x1 VALUES('eight');
+  INSERT INTO x1 VALUES('nine');
+  INSERT INTO x1 VALUES('ten');
+}
+
+do_execsql_test 4.1 {
+  SELECT level, segment FROM fts5_structure((
+    SELECT block FROM x1_data WHERE id=10
+  ))
+} {
+  0 0 0 1 0 2 0 3 0 4 0 5 0 6 0 7 0 8 0 9
+}
+
+for {set ii 1} {$ii < 4} {incr ii} {
+  do_execsql_test 4.2.$ii {
+    DELETE FROM x1 WHERE rowid = $ii;
+    INSERT INTO x1(x1, rank) VALUES('merge', 5);
+    SELECT level, segment FROM fts5_structure((
+          SELECT block FROM x1_data WHERE id=10
+    ))
+  } {
+    0 0 0 1 0 2 0 3 0 4 0 5 0 6 0 7 0 8 0 9
+  }
+}
+
+do_execsql_test 4.3 {
+  DELETE FROM x1 WHERE rowid = $ii;
+  INSERT INTO x1(x1, rank) VALUES('merge', 5);
+  SELECT level, segment, nentry FROM fts5_structure((
+        SELECT block FROM x1_data WHERE id=10
+  ))
+} {
+  1 0   6
+}
+
+finish_test
diff --git a/sqlite/ext/fts5/test/fts5contentless5.test b/sqlite/ext/fts5/test/fts5contentless5.test
new file mode 100644
index 00000000..a20134d1
--- /dev/null
+++ b/sqlite/ext/fts5/test/fts5contentless5.test
@@ -0,0 +1,58 @@
+# 2023 August 7
+#
+# 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 tests for the content= and content_rowid= options.
+#
+
+source [file join [file dirname [info script]] fts5_common.tcl]
+set testprefix fts5contentless5
+
+# If SQLITE_ENABLE_FTS5 is defined, omit this file.
+ifcapable !fts5 {
+  finish_test
+  return
+}
+
+do_execsql_test 1.0 {
+  CREATE VIRTUAL TABLE t1 USING fts5(a, b, c, content='', contentless_delete=1);
+  INSERT INTO t1 VALUES('A', 'B', 'C');
+  INSERT INTO t1 VALUES('D', 'E', 'F');
+  INSERT INTO t1 VALUES('G', 'H', 'I');
+}
+
+do_execsql_test 1.01 {
+  CREATE TABLE t2(x, y);
+  INSERT INTO t2 VALUES('x', 'y');
+}
+
+# explain_i "UPDATE t1 SET a='a' WHERE t1.rowid=1"    
+breakpoint
+explain_i "UPDATE t1 SET a='a' FROM t2 WHERE t1.rowid=1 AND b IS NULL"     
+
+#breakpoint
+#explain_i "UPDATE t1 SET a='a' WHERE b IS NULL AND rowid=?"
+
+foreach {tn up err} {
+  1   "UPDATE t1 SET a='a', b='b', c='c' WHERE rowid=1"                  0
+  2   "UPDATE t1 SET a='a', b='b'        WHERE rowid=1"                  1
+  3   "UPDATE t1 SET        b='b', c='c' WHERE rowid=1"                  1
+  4   "UPDATE t1 SET a='a',        c='c' WHERE rowid=1"                  1
+  5   "UPDATE t1 SET a='a',        c='c' WHERE t1.rowid=1 AND b IS NULL" 1
+  6   "UPDATE t1 SET a='a' FROM t2 WHERE t1.rowid=1"                     1
+  7   "UPDATE t1 SET a='a', b='b', c='c' FROM t2 WHERE t1.rowid=1"       0
+} {
+
+  set res(0) {0 {}}
+  set res(1) {1 {cannot UPDATE a subset of columns on fts5 contentless-delete table: t1}}
+  do_catchsql_test 1.$tn $up $res($err)
+}
+
+finish_test
diff --git a/sqlite/ext/fts5/test/fts5corrupt.test b/sqlite/ext/fts5/test/fts5corrupt.test
index 5f13513e..9aa84a0e 100644
--- a/sqlite/ext/fts5/test/fts5corrupt.test
+++ b/sqlite/ext/fts5/test/fts5corrupt.test
@@ -48,6 +48,10 @@ do_test 1.3 {
   }
   catchsql { INSERT INTO t1(t1) VALUES('integrity-check') }
 } {1 {database disk image is malformed}}
+do_execsql_test 1.3b {
+  PRAGMA integrity_check(t1);
+} {{malformed inverted index for FTS5 table main.t1}}
+
 
 do_test 1.4 {
   db_restore_and_reopen
diff --git a/sqlite/ext/fts5/test/fts5corrupt2.test b/sqlite/ext/fts5/test/fts5corrupt2.test
index a815320b..06e2e742 100644
--- a/sqlite/ext/fts5/test/fts5corrupt2.test
+++ b/sqlite/ext/fts5/test/fts5corrupt2.test
@@ -167,6 +167,9 @@ foreach {tn hdr} {
       do_test 3.$tn.$tn2.2 {
         catchsql { INSERT INTO x3(x3) VALUES('integrity-check') }
       } {1 {database disk image is malformed}}
+      do_execsql_test 3.$tn.$tn2.3 {
+        PRAGMA integrity_check(x3);
+      } {{malformed inverted index for FTS5 table main.x3}}
     }
 
     execsql ROLLBACK
diff --git a/sqlite/ext/fts5/test/fts5corrupt5.test b/sqlite/ext/fts5/test/fts5corrupt5.test
index 16682b13..efbe2e13 100644
--- a/sqlite/ext/fts5/test/fts5corrupt5.test
+++ b/sqlite/ext/fts5/test/fts5corrupt5.test
@@ -15,7 +15,7 @@
 #
 
 source [file join [file dirname [info script]] fts5_common.tcl]
-set testprefix fts5corrupt3
+set testprefix fts5corrupt5
 
 # If SQLITE_ENABLE_FTS5 is defined, omit this file.
 ifcapable !fts5 {
@@ -793,6 +793,94 @@ do_catchsql_test 4.5 {
   REPLACE INTO t1(rowid,a,b,rowid) VALUES(200,1,2,3);
 } {1 {database disk image is malformed}}
 
+#-------------------------------------------------------------------------
+reset_db
+do_test 5.0 {
+  sqlite3 db {}
+  db deserialize [decode_hexdb {
+.open --hexdb
+| size 28672 pagesize 4096 filename crash-0c6d3451d11597.db
+| page 1 offset 0
+|      0: 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00   SQLite format 3.
+|     16: 10 00 01 01 00 40 20 20 00 00 00 00 00 00 00 07   .....@  ........
+|     32: 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 04   ................
+|     96: 00 00 00 00 0d 00 00 00 07 0d d2 00 0f c4 0f 6d   ...............m
+|    112: 0f 02 0e ab 0e 4e 0d f6 0d d2 00 00 00 00 00 00   .....N..........
+|   3536: 00 00 22 07 06 17 11 11 01 31 74 61 62 6c 65 74   .........1tablet
+|   3552: 32 74 32 07 43 52 45 41 54 45 20 54 41 42 4c 45   2t2.CREATE TABLE
+|   3568: 20 74 32 28 78 29 56 06 06 17 1f 1f 01 7d 74 61    t2(x)V.......ta
+|   3584: 62 6c 65 74 31 5f 63 6f 6e 66 69 67 74 31 5f 63   blet1_configt1_c
+|   3600: 6f 6e 66 69 67 06 43 52 45 41 54 45 20 54 41 42   onfig.CREATE TAB
+|   3616: 4c 45 20 27 74 31 5f 63 6f 6e 66 69 67 27 28 6b   LE 't1_config'(k
+|   3632: 20 50 52 49 4d 41 52 59 20 4b 45 59 2c 20 76 29    PRIMARY KEY, v)
+|   3648: 20 57 49 54 48 4f 55 54 20 52 4f 57 49 44 5b 05    WITHOUT ROWID[.
+|   3664: 07 17 21 21 01 81 01 74 61 62 6c 65 74 31 5f 64   ..!!...tablet1_d
+|   3680: 6f 63 73 69 7a 65 74 31 5f 64 6f 63 73 69 7a 65   ocsizet1_docsize
+|   3696: 05 43 52 45 41 54 45 20 54 41 42 4c 45 20 27 74   .CREATE TABLE 't
+|   3712: 31 5f 64 6f 63 73 69 7a 65 27 28 69 64 20 49 4e   1_docsize'(id IN
+|   3728: 54 45 47 45 52 20 50 52 49 4d 41 52 59 20 4b 45   TEGER PRIMARY KE
+|   3744: 59 2c 20 73 7a 20 42 4c 4f 42 29 55 04 06 17 21   Y, sz BLOB)U...!
+|   3760: 21 01 77 74 61 62 6c 65 74 31 5f 63 6f 6e 74 65   !.wtablet1_conte
+|   3776: 6e 74 74 31 5f 63 6f 6e 74 65 6e 74 04 43 52 45   ntt1_content.CRE
+|   3792: 41 54 45 20 54 41 42 4c 45 20 27 74 31 5f 63 6f   ATE TABLE 't1_co
+|   3808: 6e 74 65 6e 74 27 28 69 64 20 49 4e 54 45 47 45   ntent'(id INTEGE
+|   3824: 52 20 50 52 49 4d 41 52 59 20 4b 45 59 2c 20 63   R PRIMARY KEY, c
+|   3840: 30 29 69 03 07 17 19 19 01 81 2d 74 61 62 6c 65   0)i.......-table
+|   3856: 74 31 5f 69 64 78 74 31 5f 69 64 78 03 43 52 45   t1_idxt1_idx.CRE
+|   3872: 41 54 45 20 54 41 42 4c 45 20 27 74 31 5f 69 64   ATE TABLE 't1_id
+|   3888: 78 27 28 73 65 67 69 64 2c 20 74 65 72 6d 2c 20   x'(segid, term, 
+|   3904: 70 67 6e 6f 2c 20 50 52 49 4d 41 52 59 20 4b 45   pgno, PRIMARY KE
+|   3920: 59 28 73 65 67 69 64 2c 20 74 65 72 6d 29 29 20   Y(segid, term)) 
+|   3936: 57 49 54 48 4f 55 54 20 52 4f 57 49 44 55 02 07   WITHOUT ROWIDU..
+|   3952: 17 1b 1b 01 81 01 74 61 62 6c 65 74 31 5f 64 61   ......tablet1_da
+|   3968: 74 61 74 31 5f 64 61 74 61 02 43 52 45 41 54 45   tat1_data.CREATE
+|   3984: 20 54 41 42 4c 45 20 27 74 31 5f 64 61 74 61 27    TABLE 't1_data'
+|   4000: 28 69 64 20 49 4e 54 45 47 45 52 20 50 52 49 4d   (id INTEGER PRIM
+|   4016: 41 52 b9 20 4b 45 59 2c 20 62 6c 6f 63 6b 20 42   AR. KEY, block B
+|   4032: 4c 4f 42 29 3a 01 06 17 11 11 08 63 74 61 62 6c   LOB):......ctabl
+|   4048: 65 74 31 74 31 43 52 45 41 54 45 20 56 49 52 54   et1t1CREATE VIRT
+|   4064: 55 41 4c 20 54 41 42 4c 45 20 74 31 20 55 53 49   UAL TABLE t1 USI
+|   4080: 4e 47 20 66 74 73 35 28 63 6f 6e 74 65 6e 74 29   NG fts5(content)
+| page 2 offset 4096
+|      0: 0d 00 00 00 03 0f bd 00 0f e8 0f ef 0f bd 00 00   ................
+|     16: 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
+|   4016: 00 00 00 00 00 00 00 00 00 00 00 00 00 24 84 80   .............$..
+|   4032: 80 80 80 01 03 00 4e 00 00 00 1e 06 30 61 62 61   ......N.....0aba
+|   4048: 63 6b 01 02 02 04 02 66 74 02 02 02 04 04 6e 64   ck.....ft.....nd
+|   4064: 6f 6e 03 02 02 04 0a 07 05 01 03 00 10 03 03 0f   on..............
+|   4080: 0a 03 00 24 00 00 00 00 01 01 01 00 01 01 01 11   ...$............
+| page 3 offset 8192
+|      0: 0a 00 00 00 01 0f 00 00 00 00 00 00 00 00 00 00   ................
+|   4080: 00 00 00 00 00 00 00 00 00 00 05 04 09 0c 01 02   ................
+| page 4 offset 12288
+|      0: 0d 00 00 00 03 0f e0 00 0f f6 0f ec 0f e0 00 00   ................
+|   4064: 0a 03 03 00 1b 61 62 61 6e 64 6f 6e 08 02 03 00   .....abandon....
+|   4080: 17 61 62 61 66 74 08 01 03 00 17 61 62 61 63 6b   .abaft.....aback
+| page 5 offset 16384
+|      0: 0d 00 00 00 03 0f ee 00 0f fa 0f f4 0f ee 00 00   ................
+|     16: 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
+|   4064: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 03   ................
+|   4080: 03 00 0e 01 04 02 03 00 0e 01 04 01 03 00 0e 01   ................
+| page 6 offset 20480
+|      0: 0a 00 00 00 01 0f f4 00 0f f4 00 00 00 00 00 00   ................
+|   4080: 00 00 00 00 0b 03 1b 01 76 65 72 73 69 6f 6e 04   ........version.
+| page 7 offset 24576
+|      0: 0d 00 00 10 03 0f d6 00 0f f4 10 e1 0f d6 00 00   ................
+|     16: 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00   ................
+|   4048: 00 00 00 00 00 00 09 03 02 1b 72 65 62 75 69 6c   ..........rebuil
+|   4064: 64 11 02 02 2b 69 6e 74 65 67 72 69 74 79 2d 63   d...+integrity-c
+|   4080: 68 65 63 6b 0a 01 02 1d 6f 70 74 69 6d 00 00 00   heck....optim...
+| end crash-0c6d3451d11597.db
+}]} {}
+
+do_execsql_test 5.1 {
+  INSERT INTO t1(t1,rank) VALUES('secure-delete',1);
+}
+do_catchsql_test 5.4 {
+  UPDATE t1 SET content=randomblob(500);
+} {1 {database disk image is malformed}}
+
+
 sqlite3_fts5_may_be_corrupt 0
 finish_test
 
diff --git a/sqlite/ext/fts5/test/fts5corrupt7.test b/sqlite/ext/fts5/test/fts5corrupt7.test
index 75995a7c..ae7f9da7 100644
--- a/sqlite/ext/fts5/test/fts5corrupt7.test
+++ b/sqlite/ext/fts5/test/fts5corrupt7.test
@@ -96,4 +96,33 @@ set r 137438953475
   db close
 }
 
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 2.0 {
+  CREATE VIRTUAL TABLE t1 USING fts5(x);
+  BEGIN;
+    INSERT INTO t1 VALUES('abc');
+    INSERT INTO t1 VALUES('b d d d');
+  COMMIT;
+  INSERT INTO t1(t1, rank) VALUES('secure-delete', 1);
+}
+
+execsql_pp {
+  SELECT id, quote(block) FROM t1_data
+}
+
+do_execsql_test 2.1 {
+  SELECT quote(block) FROM t1_data WHERE id > 10;
+}        {X'0000001A04306162630102020101620202020101640206030303040806'}
+
+do_execsql_test 2.2 {
+  UPDATE t1_data SET 
+    block=X'0000001A04306162630102025501620202020101640206030303040806'
+  WHERE id>10
+}
+
+do_catchsql_test 2.3 {
+  DELETE FROM t1 WHERE rowid = 1
+} {1 {database disk image is malformed}}
+
 finish_test
diff --git a/sqlite/ext/fts5/test/fts5eb.test b/sqlite/ext/fts5/test/fts5eb.test
index ce40e471..0c775791 100644
--- a/sqlite/ext/fts5/test/fts5eb.test
+++ b/sqlite/ext/fts5/test/fts5eb.test
@@ -95,6 +95,9 @@ do_execsql_test 3.3 {
   SELECT rowid, bm25(e1) FROM e1 WHERE e1 MATCH '"/" OR "just"' ORDER BY rank;
 } {1 -1e-06}
 
+do_execsql_test 3.4 "
+  SELECT fts5_expr_tcl('e AND \" \"');
+" {{AND [nearset -- {e}] [{}]}}
 
 
 finish_test
diff --git a/sqlite/ext/fts5/test/fts5faultF.test b/sqlite/ext/fts5/test/fts5faultF.test
new file mode 100644
index 00000000..96cc2b08
--- /dev/null
+++ b/sqlite/ext/fts5/test/fts5faultF.test
@@ -0,0 +1,111 @@
+# 2023 July 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 file is focused on OOM errors. Particularly those that may occur
+# when using contentless_delete=1 databases.
+#
+
+source [file join [file dirname [info script]] fts5_common.tcl]
+source $testdir/malloc_common.tcl
+set testprefix fts5faultF
+
+# If SQLITE_ENABLE_FTS5 is not defined, omit this file.
+ifcapable !fts5 {
+  finish_test
+  return
+}
+
+faultsim_save_and_close
+do_faultsim_test 1 -prep {
+  faultsim_restore_and_reopen
+} -body {
+  execsql { 
+    CREATE VIRTUAL TABLE t1 USING fts5(x, y, content=, contentless_delete=1) 
+  }
+} -test {
+  faultsim_test_result {0 {}} {1 {vtable constructor failed: t1}} 
+}
+
+reset_db
+do_execsql_test 2.0 {
+  CREATE VIRTUAL TABLE t1 USING fts5(doc, content=, contentless_delete=1);
+  BEGIN;
+    INSERT INTO t1(rowid, doc) VALUES(1, 'a b c d');
+    INSERT INTO t1(rowid, doc) VALUES(2, 'a b c d');
+    INSERT INTO t1(rowid, doc) VALUES(3, 'a b c d');
+    INSERT INTO t1(rowid, doc) VALUES(4, 'a b c d');
+  COMMIT;
+  DELETE FROM t1 WHERE rowid IN (2, 4);
+}
+
+do_faultsim_test 2 -prep {
+  sqlite3 db test.db
+  execsql { SELECT rowid FROM t1 }
+} -body {
+  execsql { 
+    SELECT rowid FROM t1('b c');
+  }
+} -test {
+  faultsim_test_result {0 {1 3}}
+}
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 3.0 {
+  CREATE VIRTUAL TABLE t1 USING fts5(doc, content=, contentless_delete=1);
+  BEGIN;
+    INSERT INTO t1(rowid, doc) VALUES(1, 'a b c d');
+    INSERT INTO t1(rowid, doc) VALUES(2, 'a b c d');
+    INSERT INTO t1(rowid, doc) VALUES(3, 'a b c d');
+    INSERT INTO t1(rowid, doc) VALUES(4, 'a b c d');
+  COMMIT;
+}
+
+faultsim_save_and_close
+do_faultsim_test 3 -prep {
+  faultsim_restore_and_reopen
+  execsql { SELECT rowid FROM t1 }
+} -body {
+  execsql { 
+    INSERT INTO t1(rowid, doc) VALUES(5, 'a b c d');
+  }
+} -test {
+  faultsim_test_result {0 {}}
+}
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 4.0 {
+  CREATE VIRTUAL TABLE t1 USING fts5(doc, content=, contentless_delete=1);
+  INSERT INTO t1(t1, rank) VALUES('pgsz', 64);
+  WITH s(i) AS (
+    SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000
+  )
+  INSERT INTO t1(rowid, doc) SELECT i, 'a b c d' FROM s;
+}
+
+do_execsql_test 4.1 { DELETE FROM t1 WHERE rowid <= 25 }
+
+faultsim_save_and_close
+do_faultsim_test 4 -faults oom-t* -prep {
+  faultsim_restore_and_reopen
+  execsql { SELECT rowid FROM t1 }
+} -body {
+  execsql { 
+    DELETE FROM t1 WHERE rowid < 100
+  }
+} -test {
+  faultsim_test_result {0 {}}
+}
+
+
+finish_test
+
diff --git a/sqlite/ext/fts5/test/fts5faultG.test b/sqlite/ext/fts5/test/fts5faultG.test
new file mode 100644
index 00000000..bdcc153a
--- /dev/null
+++ b/sqlite/ext/fts5/test/fts5faultG.test
@@ -0,0 +1,50 @@
+# 2010 June 15
+#
+# 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.
+#
+#***********************************************************************
+#
+
+source [file join [file dirname [info script]] fts5_common.tcl]
+source $testdir/malloc_common.tcl
+set testprefix fts5faultG
+
+# If SQLITE_ENABLE_FTS5 is defined, omit this file.
+ifcapable !fts5 {
+  finish_test
+  return
+}
+
+set ::testprefix fts5faultG
+
+
+do_execsql_test 1.0 {
+  CREATE VIRTUAL TABLE t1 USING fts5(a);
+  INSERT INTO t1 VALUES('test renaming the table');
+  INSERT INTO t1 VALUES(' after it has been written');
+  INSERT INTO t1 VALUES(' actually other stuff instead');
+}
+faultsim_save_and_close
+do_faultsim_test 1 -faults oom* -prep { 
+  faultsim_restore_and_reopen
+  execsql {
+    BEGIN;
+      DELETE FROM t1 WHERE rowid=2;
+  }
+} -body {
+  execsql {
+    DELETE FROM t1;
+  }
+} -test {
+  catchsql { COMMIT }
+  faultsim_integrity_check
+  faultsim_test_result {0 {}}
+}
+
+
+finish_test
diff --git a/sqlite/ext/fts5/test/fts5integrity.test b/sqlite/ext/fts5/test/fts5integrity.test
index 40388308..f9851dc1 100644
--- a/sqlite/ext/fts5/test/fts5integrity.test
+++ b/sqlite/ext/fts5/test/fts5integrity.test
@@ -77,6 +77,9 @@ do_catchsql_test 4.2 {
     UPDATE aa_docsize SET sz = X'44' WHERE rowid = 3;
     INSERT INTO aa(aa) VALUES('integrity-check'); 
 } {1 {database disk image is malformed}}
+do_execsql_test 4.2.1 {
+  PRAGMA integrity_check(aa);
+} {{malformed inverted index for FTS5 table main.aa}}
 
 do_catchsql_test 4.3 { 
   ROLLBACK;
@@ -317,4 +320,39 @@ do_catchsql_test 10.5.3 {
   INSERT INTO vt0(vt0) VALUES('integrity-check');
 } {0 {}}
 
+reset_db
+proc slang {in} {return [string map {th d e eh} $in]}
+db function slang -deterministic -innocuous slang
+do_execsql_test 11.0 {
+  CREATE TABLE t1(a INTEGER PRIMARY KEY, b TEXT, c TEXT AS (slang(b)));
+  INSERT INTO t1(b) VALUES('the quick fox jumps over the lazy brown dog');
+  SELECT c FROM t1;
+} {{deh quick fox jumps ovehr deh lazy brown dog}}
+
+do_execsql_test 11.1 {
+  CREATE VIRTUAL TABLE t2 USING fts5(content="t1", c);
+  INSERT INTO t2(t2) VALUES('rebuild');
+  SELECT rowid FROM t2 WHERE t2 MATCH 'deh';
+} {1}
+
+do_execsql_test 11.2 {
+  PRAGMA integrity_check(t2);
+} {ok}
+db close
+sqlite3 db test.db
+
+# FIX ME?
+#
+# FTS5 integrity-check does not care if the content table is unreadable or
+# does not exist.  It only looks for internal inconsistencies in the
+# inverted index.
+#
+do_execsql_test 11.3 {
+  PRAGMA integrity_check(t2);
+} {ok}
+do_execsql_test 11.4 {
+  DROP TABLE t1;
+  PRAGMA integrity_check(t2);
+} {ok}
+
 finish_test
diff --git a/sqlite/ext/fts5/test/fts5misc.test b/sqlite/ext/fts5/test/fts5misc.test
index da3f6526..d67d79e2 100644
--- a/sqlite/ext/fts5/test/fts5misc.test
+++ b/sqlite/ext/fts5/test/fts5misc.test
@@ -44,12 +44,12 @@ do_catchsql_test 1.2.2 {
 
 do_catchsql_test 1.3.1 { 
   SELECT highlight(t1, 4, '', '') FROM t1('*reads'); 
-} {1 {no such cursor: 1}}
+} {1 {no such cursor: 2}}
 
 do_catchsql_test 1.3.2 {
   SELECT a FROM t1
     WHERE rank = (SELECT highlight(t1, 4, '', '') FROM t1('*reads'));
-} {1 {no such cursor: 1}}
+} {1 {no such cursor: 2}}
 
 db close
 sqlite3 db test.db
@@ -424,10 +424,12 @@ do_execsql_test -db db2 15.3 {
   SAVEPOINT one;
 } {}
 do_execsql_test 15.4 END
-do_test 15.4 {
+do_test 15.5 {
   list [catch { db2 eval COMMIT } msg] $msg
 } {0 {}}
 
+db2 close
+
 #-------------------------------------------------------------------------
 reset_db
 forcedelete test.db2
@@ -469,6 +471,8 @@ do_execsql_test -db db2 16.6 {
   SELECT * FROM x1
 } {abc def}
 
+db2 close
+
 #-------------------------------------------------------------------------
 reset_db
 do_execsql_test 17.1 {
diff --git a/sqlite/ext/fts5/test/fts5optimize2.test b/sqlite/ext/fts5/test/fts5optimize2.test
index a0782ee7..b0b28874 100644
--- a/sqlite/ext/fts5/test/fts5optimize2.test
+++ b/sqlite/ext/fts5/test/fts5optimize2.test
@@ -9,7 +9,7 @@
 #
 #***********************************************************************
 #
-# TESTRUNNER: slow
+# TESTRUNNER: superslow
 #
 
 source [file join [file dirname [info script]] fts5_common.tcl]
@@ -42,23 +42,4 @@ do_execsql_test 1.2 {
   SELECT count(*) FROM t1('mno')
 } $nLoop
 
-do_execsql_test 2.0 {
-  CREATE VIRTUAL TABLE t2 USING fts5(x);
-  INSERT INTO t2(t2, rank) VALUES('pgsz', 32);
-}
-
-do_test 2.1 {
-  for {set ii 0} {$ii < $nLoop} {incr ii} {
-    execsql {
-      INSERT INTO t2 VALUES('abc def ghi');
-      INSERT INTO t2 VALUES('jkl mno pqr');
-      INSERT INTO t2(t2, rank) VALUES('merge', -1);
-    }
-  }
-} {}
-
-do_execsql_test 2.2 {
-  SELECT count(*) FROM t2('mno')
-} $nLoop
-
 finish_test
diff --git a/sqlite/ext/fts5/test/fts5optimize3.test b/sqlite/ext/fts5/test/fts5optimize3.test
new file mode 100644
index 00000000..7b11b940
--- /dev/null
+++ b/sqlite/ext/fts5/test/fts5optimize3.test
@@ -0,0 +1,45 @@
+# 2023 Aug 27
+#
+# 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.
+#
+#***********************************************************************
+#
+# TESTRUNNER: superslow
+#
+
+source [file join [file dirname [info script]] fts5_common.tcl]
+set testprefix fts5optimize2
+
+# If SQLITE_ENABLE_FTS5 is defined, omit this file.
+ifcapable !fts5 {
+  finish_test
+  return
+}
+
+set nLoop 2500
+
+do_execsql_test 1.0 {
+  CREATE VIRTUAL TABLE t2 USING fts5(x);
+  INSERT INTO t2(t2, rank) VALUES('pgsz', 32);
+}
+
+do_test 1.1 {
+  for {set ii 0} {$ii < $nLoop} {incr ii} {
+    execsql {
+      INSERT INTO t2 VALUES('abc def ghi');
+      INSERT INTO t2 VALUES('jkl mno pqr');
+      INSERT INTO t2(t2, rank) VALUES('merge', -1);
+    }
+  }
+} {}
+
+do_execsql_test 1.2 {
+  SELECT count(*) FROM t2('mno')
+} $nLoop
+
+finish_test
diff --git a/sqlite/ext/fts5/test/fts5rank.test b/sqlite/ext/fts5/test/fts5rank.test
index 22534e8e..8cf223f4 100644
--- a/sqlite/ext/fts5/test/fts5rank.test
+++ b/sqlite/ext/fts5/test/fts5rank.test
@@ -180,4 +180,28 @@ do_execsql_test 6.1 {
   {table table table} {the table names.} {rank on an fts5 table}
 }
 
+
+#-------------------------------------------------------------------------
+# forum post: https://sqlite.org/forum/forumpost/a2dd636330
+#
+reset_db
+do_execsql_test 1.0 {
+  CREATE VIRTUAL TABLE t USING fts5 (a, b);  
+  INSERT INTO t (a, b) VALUES ('data1', 'sentence1'), ('data2', 'sentence2'); 
+  INSERT INTO t(t, rank) VALUES ('rank', 'bm25(10.0,1.0)');
+}
+
+sqlite3 db2 test.db 
+do_execsql_test -db db2 1.1 {
+  SELECT *, rank<0.0 FROM t('data*') ORDER BY RANK;
+} {data1 sentence1 1 data2 sentence2 1}
+
+do_execsql_test 1.2 {
+  INSERT INTO t(t, rank) VALUES ('rank', 'bm25(10.0,1.0)');
+}
+do_execsql_test -db db2 1.3 {
+  SELECT *, rank<0.0 FROM t('data*') ORDER BY RANK;
+} {data1 sentence1 1 data2 sentence2 1}
+db2 close
+
 finish_test
diff --git a/sqlite/ext/fts5/test/fts5savepoint.test b/sqlite/ext/fts5/test/fts5savepoint.test
index e431f9f5..11262227 100644
--- a/sqlite/ext/fts5/test/fts5savepoint.test
+++ b/sqlite/ext/fts5/test/fts5savepoint.test
@@ -71,7 +71,7 @@ ifcapable fts3 {
 
   do_catchsql_test 3.2 {
     DROP TABLE vt1;
-  } {1 {SQL logic error}}
+  } {0 {}}
 
   do_execsql_test 3.3 {
     SAVEPOINT x;
diff --git a/sqlite/ext/fts5/test/fts5secure.test b/sqlite/ext/fts5/test/fts5secure.test
index 50d84cef..73149461 100644
--- a/sqlite/ext/fts5/test/fts5secure.test
+++ b/sqlite/ext/fts5/test/fts5secure.test
@@ -273,6 +273,76 @@ do_execsql_test 5.3 {
 do_execsql_test 5.4 { SELECT rowid FROM t1('abc'); } 2
 do_execsql_test 5.5 { SELECT rowid FROM t1('aa'); } 2
 
+#-------------------------------------------------------------------------
+# Tests for the bug fixed by https://sqlite.org/src/info/4b60a1c3
+#
+reset_db
+do_execsql_test 6.0 {
+  CREATE VIRTUAL TABLE fts USING fts5(content);
+  INSERT INTO fts(fts, rank) VALUES ('secure-delete', 1);
+  INSERT INTO fts(rowid, content) VALUES
+    (3407, 'profile profile profile profile profile profile profile profile pull pulling pulling really');
+  DELETE FROM fts WHERE rowid IS 3407;
+  INSERT INTO fts(fts) VALUES ('integrity-check');
+}
+
+foreach {tn detail} {
+  1 full
+  2 column
+  3 none
+} {
+  do_execsql_test 6.1.$detail "
+    DROP TABLE IF EXISTS t1;
+    CREATE VIRTUAL TABLE t1 USING fts5(x, detail=$detail);
+  "
+
+  do_execsql_test 6.2.$detail {
+    INSERT INTO t1(t1, rank) VALUES('secure-delete', 1);
+  }
+
+  for {set ii 1} {$ii < 100} {incr ii} {
+    do_execsql_test 6.3.$detail.$ii.1 {
+      BEGIN;
+        INSERT INTO t1(rowid, x) VALUES(10, 'word1');
+        WITH s(i) AS (
+          SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i/dev/null))
+#  SQLITE_C_IS_SEE := 0
+#else
+#  SQLITE_C_IS_SEE := 1
+#  $(info This is an SEE build.)
+#endif
+
+.NOTPARALLEL: $(sqlite3.h)
+$(sqlite3.h):
+	$(MAKE) -C $(dir.top) sqlite3.c
+$(sqlite3.c): $(sqlite3.h)
+
+opt.threadsafe ?= 1
+opt.fatal-oom ?= 1
+opt.debug ?= 1
+opt.metrics ?= 1
+SQLITE_OPT = \
+  -DSQLITE_THREADSAFE=$(opt.threadsafe) \
+  -DSQLITE_TEMP_STORE=2 \
+  -DSQLITE_USE_URI=1 \
+  -DSQLITE_OMIT_LOAD_EXTENSION \
+  -DSQLITE_OMIT_DEPRECATED \
+  -DSQLITE_OMIT_SHARED_CACHE \
+  -DSQLITE_C=$(sqlite3.c) \
+  -DSQLITE_JNI_FATAL_OOM=$(opt.fatal-oom) \
+  -DSQLITE_JNI_ENABLE_METRICS=$(opt.metrics)
+
+opt.extras ?= 1
+ifeq (1,$(opt.extras))
+SQLITE_OPT += -DSQLITE_ENABLE_RTREE \
+  -DSQLITE_ENABLE_EXPLAIN_COMMENTS \
+  -DSQLITE_ENABLE_STMTVTAB \
+  -DSQLITE_ENABLE_DBPAGE_VTAB \
+  -DSQLITE_ENABLE_DBSTAT_VTAB \
+  -DSQLITE_ENABLE_BYTECODE_VTAB \
+  -DSQLITE_ENABLE_OFFSET_SQL_FUNC \
+  -DSQLITE_ENABLE_PREUPDATE_HOOK \
+  -DSQLITE_ENABLE_NORMALIZE \
+  -DSQLITE_ENABLE_SQLLOG
+endif
+
+ifeq (1,$(opt.debug))
+  SQLITE_OPT += -DSQLITE_DEBUG -g -DDEBUG -UNDEBUG
+else
+  SQLITE_OPT += -Os
+endif
+
+ifeq (1,$(enable.fts5))
+  SQLITE_OPT += -DSQLITE_ENABLE_FTS5
+endif
+
+sqlite3-jni.c := $(dir.src.c)/sqlite3-jni.c
+sqlite3-jni.o := $(dir.bld.c)/sqlite3-jni.o
+sqlite3-jni.h   := $(dir.src.c)/sqlite3-jni.h
+package.dll := $(dir.bld.c)/libsqlite3-jni.so
+# All javac-generated .h files must be listed in $(sqlite3-jni.h.in):
+sqlite3-jni.h.in :=
+# $(java.with.jni) lists all Java files which contain JNI decls:
+java.with.jni :=
+define ADD_JNI_H
+sqlite3-jni.h.in += $$(dir.bld.c)/org_sqlite_jni$(3)_$(2).h
+java.with.jni += $(1)/$(2).java
+$$(dir.bld.c)/org_sqlite_jni$(3)_$(2).h: $(1)/$(2).java
+endef
+# Invoke ADD_JNI_H once for each Java file which includes JNI
+# declarations:
+$(eval $(call ADD_JNI_H,$(dir.src.capi),CApi,_capi))
+$(eval $(call ADD_JNI_H,$(dir.src.capi),SQLTester,_capi))
+ifeq (1,$(enable.fts5))
+ $(eval $(call ADD_JNI_H,$(dir.src.fts5),Fts5ExtensionApi,_fts5))
+ $(eval $(call ADD_JNI_H,$(dir.src.fts5),fts5_api,_fts5))
+ $(eval $(call ADD_JNI_H,$(dir.src.fts5),fts5_tokenizer,_fts5))
+endif
+$(sqlite3-jni.h.in): $(dir.bld.c)
+
+#package.dll.cfiles :=
+package.dll.cflags = \
+  -std=c99 \
+  -fPIC \
+  -I. \
+  -I$(dir $(sqlite3.h)) \
+  -I$(dir.src.c) \
+  -I$(JDK_HOME)/include \
+  $(patsubst %,-I%,$(patsubst %.h,,$(wildcard $(JDK_HOME)/include/*))) \
+  -Wall
+# The gross $(patsubst...) above is to include the platform-specific
+# subdir which lives under $(JDK_HOME)/include and is a required
+# include path for client-level code.
+#
+# Using (-Wall -Wextra) triggers an untennable number of
+# gcc warnings from sqlite3.c for mundane things like
+# unused parameters.
+########################################################################
+ifeq (1,$(enable.tester))
+  package.dll.cflags += -DSQLITE_JNI_ENABLE_SQLTester
+endif
+
+$(sqlite3-jni.h): $(sqlite3-jni.h.in) $(MAKEFILE)
+	@cat $(sqlite3-jni.h.in) > $@.tmp
+	@if cmp $@ $@.tmp >/dev/null; then \
+		rm -f $@.tmp; \
+		echo "$@ not modified"; \
+	else \
+		mv $@.tmp $@; \
+		echo "Updated $@"; \
+	fi
+	@if [ x1 != x$(enable.fts5) ]; then \
+		echo "*** REMINDER:"; \
+		echo "*** enable.fts5=0, so please do not check in changes to $@."; \
+	fi
+
+$(package.dll): $(sqlite3-jni.h) $(sqlite3.c) $(sqlite3.h)
+$(package.dll): $(sqlite3-jni.c) $(MAKEFILE)
+	$(CC) $(package.dll.cflags) $(SQLITE_OPT) \
+			$(sqlite3-jni.c) -shared -o $@
+all: $(package.dll)
+
+.PHONY: test test-one
+Tester1.flags ?=
+Tester2.flags ?=
+test.flags.jvm = -ea -Djava.library.path=$(dir.bld.c) \
+                  $(java.flags) -cp $(classpath)
+test.deps := $(CLASS_FILES) $(package.dll)
+test-one: $(test.deps)
+	$(bin.java) $(test.flags.jvm) org.sqlite.jni.capi.Tester1 $(Tester1.flags)
+	$(bin.java) $(test.flags.jvm) org.sqlite.jni.wrapper1.Tester2 $(Tester2.flags)
+test-sqllog: $(test.deps)
+	@echo "Testing with -sqllog..."
+	$(bin.java) $(test.flags.jvm) -sqllog
+test-mt: $(test.deps)
+	@echo "Testing in multi-threaded mode:";
+	$(bin.java) $(test.flags.jvm) org.sqlite.jni.capi.Tester1 \
+    -t 7 -r 50 -shuffle $(Tester1.flags)
+	$(bin.java) $(test.flags.jvm) org.sqlite.jni.wrapper1.Tester2 \
+    -t 7 -r 50 -shuffle $(Tester2.flags)
+
+test: test-one test-mt
+tests: test test-sqllog
+
+tester.scripts := $(sort $(wildcard $(dir.src)/tests/*.test))
+tester.flags ?= # --verbose
+.PHONY: tester tester-local tester-ext
+ifeq (1,$(enable.tester))
+tester-local: $(CLASS_FILES.tester) $(package.dll)
+	$(bin.java) -ea -Djava.library.path=$(dir.bld.c) \
+		$(java.flags) -cp $(classpath) \
+		org.sqlite.jni.capi.SQLTester $(tester.flags) $(tester.scripts)
+tester: tester-local
+else
+tester:
+	@echo "SQLTester support is disabled."
+endif
+
+tester.extdir.default := $(dir.tests)/ext
+tester.extdir ?= $(tester.extdir.default)
+tester.extern-scripts := $(wildcard $(tester.extdir)/*.test)
+ifneq (,$(tester.extern-scripts))
+tester-ext:
+	$(bin.java) -ea -Djava.library.path=$(dir.bld.c) \
+		$(java.flags) -cp $(classpath) \
+		org.sqlite.jni.capi.SQLTester $(tester.flags) $(tester.extern-scripts)
+else
+tester-ext:
+	@echo "******************************************************"; \
+		echo "*** Include the out-of-tree test suite in the 'tester'"; \
+		echo "*** target by either symlinking its directory to"; \
+		echo "*** $(tester.extdir.default) or passing it to make"; \
+		echo "*** as tester.extdir=/path/to/that/dir."; \
+		echo "******************************************************";
+endif
+
+tester-ext: tester-local
+tester: tester-ext
+tests: tester
+########################################################################
+# Build each SQLITE_THREADMODE variant and run all tests against them.
+multitest: clean
+define MULTIOPT
+multitest: multitest-$(1)
+multitest-$(1):
+	$$(MAKE) opt.debug=$$(opt.debug) $(patsubst %,opt.%,$(2)) \
+		tests clean enable.fts5=1
+endef
+
+$(eval $(call MULTIOPT,01,threadsafe=0 oom=1))
+$(eval $(call MULTIOPT,00,threadsafe=0 oom=0))
+$(eval $(call MULTIOPT,11,threadsafe=1 oom=1))
+$(eval $(call MULTIOPT,10,threadsafe=1 oom=0))
+$(eval $(call MULTIOPT,21,threadsafe=2 oom=1))
+$(eval $(call MULTIOPT,20,threadsafe=2 oom=0))
+
+
+########################################################################
+# jar bundle...
+package.jar.in := $(abspath $(dir.src)/jar.in)
+CLEAN_FILES += $(package.jar.in)
+JAVA_FILES.jar := $(JAVA_FILES.main) $(JAVA_FILES.unittest) $(JAVA_FILES.package.info)
+CLASS_FILES.jar := $(filter-out %/package-info.class,$(JAVA_FILES.jar:.java=.class))
+$(package.jar.in): $(package.dll) $(MAKEFILE)
+	ls -1 \
+		$(dir.src.jni)/*/*.java $(dir.src.jni)/*/*.class \
+	| sed -e 's,^$(dir.src)/,,' | sort > $@
+
+$(package.jar): $(CLASS_FILES.jar) $(MAKEFILE) $(package.jar.in)
+	@rm -f $(dir.src)/c/*~ $(dir.src.jni)/*~
+	cd $(dir.src); $(bin.jar) -cfe ../$@ org.sqlite.jni.capi.Tester1 @$(package.jar.in)
+	@ls -la $@
+	@echo "To use this jar you will need the -Djava.library.path=DIR/CONTAINING/libsqlite3-jni.so flag."
+	@echo "e.g. java -Djava.library.path=bld -jar $@"
+
+jar: $(package.jar)
+run-jar: $(package.jar) $(package.dll)
+	$(bin.java) -Djava.library.path=$(dir.bld) -jar $(package.jar) $(run-jar.flags)
+
+########################################################################
+# javadoc...
+dir.doc   := $(dir.jni)/javadoc
+doc.index := $(dir.doc)/index.html
+javadoc.exclude := -exclude org.sqlite.jni.fts5
+# ^^^^ 2023-09-13: elide the fts5 parts from the public docs for
+# the time being, as it's not clear where the Java bindings for
+# those bits are going.
+# javadoc.exclude += -exclude org.sqlite.jni.capi
+# ^^^^ exclude the capi API only for certain builds (TBD)
+$(doc.index): $(JAVA_FILES.main) $(MAKEFILE)
+	@if [ -d $(dir.doc) ]; then rm -fr $(dir.doc)/*; fi
+	$(bin.javadoc) -cp $(classpath) -d $(dir.doc) -quiet \
+		-subpackages org.sqlite.jni $(javadoc.exclude)
+	@echo "javadoc output is in $@"
+
+.PHONY: doc javadoc docserve
+.FORCE: doc
+doc: $(doc.index)
+javadoc: $(doc.index)
+# Force rebild of docs
+redoc:
+	@rm -f $(doc.index)
+	@$(MAKE) doc
+docserve: $(doc.index)
+	cd $(dir.doc) && althttpd -max-age 1 -page index.html
+########################################################################
+# Clean up...
+CLEAN_FILES += $(dir.bld.c)/* \
+  $(dir.src.jni)/*.class \
+  $(dir.src.jni)/*/*.class \
+  $(package.dll) \
+  hs_err_pid*.log
+
+.PHONY: clean distclean
+clean:
+	-rm -f $(CLEAN_FILES)
+distclean: clean
+	-rm -f $(DISTCLEAN_FILES)
+	-rm -fr $(dir.bld.c) $(dir.doc)
+
+########################################################################
+# disttribution bundle rules...
+
+ifeq (,$(filter snapshot,$(MAKECMDGOALS)))
+dist-name-prefix := sqlite-jni
+else
+dist-name-prefix := sqlite-jni-snapshot-$(shell /usr/bin/date +%Y%m%d)
+endif
+dist-name := $(dist-name-prefix)-TEMP
+
+
+dist-dir.top := $(dist-name)
+dist-dir.src := $(dist-dir.top)/src
+dist.top.extras := \
+  README.md
+
+.PHONY: dist snapshot
+
+dist: \
+    $(bin.version-info) $(sqlite3.canonical.c) \
+    $(package.jar) $(MAKEFILE)
+	@echo "Making end-user deliverables..."
+	@echo "****************************************************************************"; \
+	echo  "*** WARNING: be sure to build this with JDK8 (javac 1.8) for compatibility."; \
+	echo  "*** reasons!"; $$($(bin.javac) -version); \
+	echo  "****************************************************************************"
+	@rm -fr $(dist-dir.top)
+	@mkdir -p $(dist-dir.src)
+	@cp -p $(dist.top.extras) $(dist-dir.top)/.
+	@cp -p jar-dist.make $(dist-dir.top)/Makefile
+	@cp -p $(dir.src.c)/*.[ch] $(dist-dir.src)/.
+	@cp -p $(sqlite3.canonical.c) $(sqlite3.canonical.h) $(dist-dir.src)/.
+	@set -e; \
+		vnum=$$($(bin.version-info) --download-version); \
+		vjar=$$($(bin.version-info) --version); \
+		vdir=$(dist-name-prefix)-$$vnum; \
+		arczip=$$vdir.zip; \
+		cp -p $(package.jar) $(dist-dir.top)/sqlite3-jni-$${vjar}.jar; \
+		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."
+
+snapshot: dist
+
+.PHONY: dist-clean
+clean: dist-clean
+dist-clean:
+	rm -fr $(dist-name) $(wildcard sqlite-jni-*.zip)
diff --git a/sqlite/ext/jni/README.md b/sqlite/ext/jni/README.md
new file mode 100644
index 00000000..f2811fdd
--- /dev/null
+++ b/sqlite/ext/jni/README.md
@@ -0,0 +1,313 @@
+SQLite3 via JNI
+========================================================================
+
+This directory houses a Java Native Interface (JNI) binding for the
+sqlite3 API. If you are reading this from the distribution ZIP file,
+links to resources in the canonical source tree will note work. The
+canonical copy of this file can be browsed at:
+
+  
+
+Technical support is available in the forum:
+
+  
+
+
+> **FOREWARNING:** this subproject is very much in development and
+  subject to any number of changes. Please do not rely on any
+  information about its API until this disclaimer is removed.  The JNI
+  bindings released with version 3.43 are a "tech preview" and 3.44
+  will be "final," at which point strong backward compatibility
+  guarantees will apply.
+
+Project goals/requirements:
+
+- A [1-to-1(-ish) mapping of the C API](#1to1ish) to Java via JNI,
+  insofar as cross-language semantics allow for. A closely-related
+  goal is that [the C documentation](https://sqlite.org/c3ref/intro.html)
+  should be usable as-is, insofar as possible, for the JNI binding.
+
+- Support Java as far back as version 8 (2014).
+
+- Environment-independent. Should work everywhere both Java
+  and SQLite3 do.
+
+- No 3rd-party dependencies beyond the JDK. That includes no
+  build-level dependencies for specific IDEs and toolchains.  We
+  welcome the addition of build files for arbitrary environments
+  insofar as they neither interfere with each other nor become
+  a maintenance burden for the sqlite developers.
+
+Non-goals:
+
+- Creation of high-level OO wrapper APIs. Clients are free to create
+  them off of the C-style API.
+
+- Support for mixed-mode operation, where client code accesses SQLite
+  both via the Java-side API and the C API via their own native
+  code. In such cases, proxy functionalities (primarily callback
+  handler wrappers of all sorts) may fail because the C-side use of
+  the SQLite APIs will bypass those proxies.
+
+
+Hello World
+-----------------------------------------------------------------------
+
+```java
+import org.sqlite.jni.*;
+import static org.sqlite.jni.CApi.*;
+
+...
+
+final sqlite3 db = sqlite3_open(":memory:");
+try {
+  final int rc = sqlite3_errcode(db);
+  if( 0 != rc ){
+    if( null != db ){
+      System.out.print("Error opening db: "+sqlite3_errmsg(db));
+    }else{
+      System.out.print("Error opening db: rc="+rc);
+    }
+    ... handle error ...
+  }
+  // ... else use the db ...
+}finally{
+  // ALWAYS close databases using sqlite3_close() or sqlite3_close_v2()
+  // when done with them. All of their active statement handles must
+  // first have been passed to sqlite3_finalize().
+  sqlite3_close_v2(db);
+}
+```
+
+
+Building
+========================================================================
+
+The canonical builds assumes a Linux-like environment and requires:
+
+- GNU Make
+- A JDK supporting Java 8 or higher
+- A modern C compiler. gcc and clang should both work.
+
+Put simply:
+
+```console
+$ export JAVA_HOME=/path/to/jdk/root
+$ make
+$ make test
+$ make clean
+```
+
+The jar distribution can be created with `make jar`, but note that it
+does not contain the binary DLL file. A different DLL is needed for
+each target platform.
+
+
+
+One-to-One(-ish) Mapping to C
+========================================================================
+
+This JNI binding aims to provide as close to a 1-to-1 experience with
+the C API as cross-language semantics allow. Interface changes are
+necessarily made where cross-language semantics do not allow a 1-to-1,
+and judiciously made where a 1-to-1 mapping would be unduly cumbersome
+to use in Java. In all cases, this binding makes every effort to
+provide semantics compatible with the C API documentation even if the
+interface to those semantics is slightly different.  Any cases which
+deviate from those semantics (either removing or adding semantics) are
+clearly documented.
+
+Where it makes sense to do so for usability, Java-side overloads are
+provided which accept or return data in alternative forms or provide
+sensible default argument values. In all such cases they are thin
+proxies around the corresponding C APIs and do not introduce new
+semantics.
+
+In some very few cases, Java-specific capabilities have been added in
+new APIs, all of which have "_java" somewhere in their names.
+Examples include:
+
+- `sqlite3_result_java_object()`
+- `sqlite3_column_java_object()`
+- `sqlite3_column_java_casted()`
+- `sqlite3_value_java_object()`
+- `sqlite3_value_java_casted()`
+
+which, as one might surmise, collectively enable the passing of
+arbitrary Java objects from user-defined SQL functions through to the
+caller.
+
+
+Golden Rule: Garbage Collection Cannot Free SQLite Resources
+------------------------------------------------------------------------
+
+It is important that all databases and prepared statement handles get
+cleaned up by client code. A database cannot be closed if it has open
+statement handles. `sqlite3_close()` fails if the db cannot be closed
+whereas `sqlite3_close_v2()` recognizes that case and marks the db as
+a "zombie," pending finalization when the library detects that all
+pending statements have been closed. Be aware that Java garbage
+collection _cannot_ close a database or finalize a prepared statement.
+Those things require explicit API calls.
+
+
+Golden Rule #2: _Never_ Throw from Callbacks (Unless...)
+------------------------------------------------------------------------
+
+All routines in this API, barring explicitly documented exceptions,
+retain C-like semantics. For example, they are not permitted to throw
+or propagate exceptions and must return error information (if any) via
+result codes or `null`. The only cases where the C-style APIs may
+throw is through client-side misuse, e.g. passing in a null where it
+shouldn't be used. The APIs clearly mark function parameters which
+should not be null, but does not actively defend itself against such
+misuse. Some C-style APIs explicitly accept `null` as a no-op for
+usability's sake, and some of the JNI APIs deliberately return an
+error code, instead of segfaulting, when passed a `null`.
+
+Client-defined callbacks _must never throw exceptions_ unless _very
+explicitly documented_ as being throw-safe. Exceptions are generally
+reserved for higher-level bindings which are constructed to
+specifically deal with them and ensure that they do not leak C-level
+resources. In some cases, callback handlers are permitted to throw, in
+which cases they get translated to C-level result codes and/or
+messages. If a callback which is not permitted to throw throws, its
+exception may trigger debug output but will otherwise be suppressed.
+
+The reason some callbacks are permitted to throw and others not is
+because all such callbacks act as proxies for C function callback
+interfaces and some of those interfaces have no error-reporting
+mechanism. Those which are capable of propagating errors back through
+the library convert exceptions from callbacks into corresponding
+C-level error information. Those which cannot propagate errors
+necessarily suppress any exceptions in order to maintain the C-style
+semantics of the APIs.
+
+
+Unwieldy Constructs are Re-mapped
+------------------------------------------------------------------------
+
+Some constructs, when modelled 1-to-1 from C to Java, are unduly
+clumsy to work with in Java because they try to shoehorn C's way of
+doing certain things into Java's wildly different ways. The following
+subsections cover those, starting with a verbose explanation and
+demonstration of where such changes are "really necessary"...
+
+### Custom Collations
+
+A prime example of where interface changes for Java are necessary for
+usability is [registration of a custom
+collation](https://sqlite.org/c3ref/create_collation.html):
+
+```c
+// C:
+int sqlite3_create_collation(sqlite3 * db, const char * name, int eTextRep,
+                             void *pUserData,
+                             int (*xCompare)(void*,int,void const *,int,void const *));
+
+int sqlite3_create_collation_v2(sqlite3 * db, const char * name, int eTextRep,
+                                void *pUserData,
+                                int (*xCompare)(void*,int,void const *,int,void const *),
+                                void (*xDestroy)(void*));
+```
+
+The `pUserData` object is optional client-defined state for the
+`xCompare()` and/or `xDestroy()` callback functions, both of which are
+passed that object as their first argument. That data is passed around
+"externally" in C because that's how C models the world. If we were to
+bind that part as-is to Java, the result would be awkward to use (^Yes,
+we tried this.):
+
+```java
+// Java:
+int sqlite3_create_collation(sqlite3 db, String name, int eTextRep,
+                             Object pUserData, xCompareType xCompare);
+
+int sqlite3_create_collation_v2(sqlite3 db, String name, int eTextRep,
+                                Object pUserData,
+                                xCompareType xCompare, xDestroyType xDestroy);
+```
+
+The awkwardness comes from (A) having two distinctly different objects
+for callbacks and (B) having their internal state provided separately,
+which is ill-fitting in Java. For the sake of usability, C APIs which
+follow that pattern use a slightly different Java interface:
+
+```java
+int sqlite3_create_collation(sqlite3 db, String name, int eTextRep,
+                             SomeCallbackType collation);
+```
+
+Where the `Collation` class has an abstract `call()` method and
+no-op `xDestroy()` method which can be overridden if needed, leading to
+a much more Java-esque usage:
+
+```java
+int rc = sqlite3_create_collation(db, "mycollation", SQLITE_UTF8, new SomeCallbackType(){
+
+  // Required comparison function:
+  @Override public int call(byte[] lhs, byte[] rhs){ ... }
+
+  // Optional finalizer function:
+  @Override public void xDestroy(){ ... }
+
+  // Optional local state:
+  private String localState1 =
+    "This is local state. There are many like it, but this one is mine.";
+  private MyStateType localState2 = new MyStateType();
+  ...
+});
+```
+
+Noting that:
+
+- It is possible to bind in call-scope-local state via closures, if
+  desired, as opposed to packing it into the Collation object.
+
+- No capabilities of the C API are lost or unduly obscured via the
+  above API reshaping, so power users need not make any compromises.
+
+- In the specific example above, `sqlite3_create_collation_v2()`
+  becomes superfluous because the provided interface effectively
+  provides both the v1 and v2 interfaces, the difference being that
+  overriding the `xDestroy()` method effectively gives it v2
+  semantics.
+
+
+### User-defined SQL Functions (a.k.a. UDFs)
+
+The [`sqlite3_create_function()`](https://sqlite.org/c3ref/create_function.html)
+family of APIs make heavy use of function pointers to provide
+client-defined callbacks, necessitating interface changes in the JNI
+binding. The Java API has only one core function-registration function:
+
+```java
+int sqlite3_create_function(sqlite3 db, String funcName, int nArgs,
+                            int encoding, SQLFunction func);
+```
+
+> Design question: does the encoding argument serve any purpose in
+  Java? That's as-yet undetermined. If not, it will be removed.
+
+`SQLFunction` is not used directly, but is instead instantiated via
+one of its three subclasses:
+
+- `SQLFunction.Scalar` implements simple scalar functions using but a
+  single callback.
+- `SQLFunction.Aggregate` implements aggregate functions using two
+  callbacks.
+- `SQLFunction.Window` implements window functions using four
+  callbacks.
+
+Search [`Tester1.java`](/file/ext/jni/src/org/sqlite/jni/Tester1.java) for
+`SQLFunction` for how it's used.
+
+Reminder: see the disclaimer at the top of this document regarding the
+in-flux nature of this API.
+
+### And so on...
+
+Various APIs which accept callbacks, e.g. `sqlite3_trace_v2()` and
+`sqlite3_update_hook()`, use interfaces similar to those shown above.
+Despite the changes in signature, the JNI layer makes every effort to
+provide the same semantics as the C API documentation suggests.
diff --git a/sqlite/ext/jni/jar-dist.make b/sqlite/ext/jni/jar-dist.make
new file mode 100644
index 00000000..7596c99f
--- /dev/null
+++ b/sqlite/ext/jni/jar-dist.make
@@ -0,0 +1,60 @@
+#!/this/is/make
+#^^^^ help emacs out
+#
+# This is a POSIX-make-compatible makefile for building the sqlite3
+# JNI library from "dist" zip file. It must be edited to set the
+# proper top-level JDK directory and, depending on the platform, add a
+# platform-specific -I directory. It should build as-is with any
+# 2020s-era version of gcc or clang. It requires JDK version 8 or
+# higher and that JAVA_HOME points to the top-most installation
+# directory of that JDK. On Ubuntu-style systems the JDK is typically
+# installed under /usr/lib/jvm/java-VERSION-PLATFORM.
+
+default: all
+
+JAVA_HOME = /usr/lib/jvm/java-1.8.0-openjdk-amd64
+CFLAGS = \
+  -fPIC \
+  -Isrc \
+  -I$(JAVA_HOME)/include \
+  -I$(JAVA_HOME)/include/linux \
+  -I$(JAVA_HOME)/include/apple \
+  -I$(JAVA_HOME)/include/bsd \
+  -Wall
+
+SQLITE_OPT = \
+  -DSQLITE_ENABLE_RTREE \
+  -DSQLITE_ENABLE_EXPLAIN_COMMENTS \
+  -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_SHARED_CACHE \
+  -DSQLITE_THREADSAFE=1 \
+  -DSQLITE_TEMP_STORE=2 \
+  -DSQLITE_USE_URI=1 \
+  -DSQLITE_ENABLE_FTS5 \
+  -DSQLITE_DEBUG
+
+sqlite3-jni.dll = libsqlite3-jni.so
+$(sqlite3-jni.dll):
+	@echo "************************************************************************"; \
+	echo  "*** If this fails to build, be sure to edit this makefile            ***"; \
+	echo  "*** to configure it for your system.                                 ***"; \
+	echo  "************************************************************************"
+	$(CC) $(CFLAGS) $(SQLITE_OPT) \
+		src/sqlite3-jni.c -shared -o $@
+	@echo "Now try running it with: make test"
+
+test.flags = -Djava.library.path=. sqlite3-jni-*.jar
+test: $(sqlite3-jni.dll)
+	java -jar $(test.flags)
+	java -jar $(test.flags) -t 7 -r 10 -shuffle
+
+clean:
+	-rm -f $(sqlite3-jni.dll)
+
+all: $(sqlite3-jni.dll)
diff --git a/sqlite/ext/jni/src/c/sqlite3-jni.c b/sqlite/ext/jni/src/c/sqlite3-jni.c
new file mode 100644
index 00000000..245ce4f9
--- /dev/null
+++ b/sqlite/ext/jni/src/c/sqlite3-jni.c
@@ -0,0 +1,5899 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file implements the JNI bindings declared in
+** org.sqlite.jni.capi.CApi (from which sqlite3-jni.h is generated).
+*/
+
+/*
+** If you found this comment by searching the code for
+** CallStaticObjectMethod then you're the victim of an OpenJDK bug:
+**
+** https://bugs.openjdk.org/browse/JDK-8130659
+**
+** It's known to happen with OpenJDK v8 but not with v19.
+**
+** This code does not use JNI's CallStaticObjectMethod().
+*/
+
+/*
+** 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
+# define SQLITE_DEFAULT_CACHE_SIZE -16384
+#endif
+#if !defined(SQLITE_DEFAULT_PAGE_SIZE)
+# define SQLITE_DEFAULT_PAGE_SIZE 8192
+#endif
+#ifndef SQLITE_DQS
+#  define SQLITE_DQS 0
+#endif
+
+/**********************************************************************/
+/* SQLITE_ENABLE_... */
+/*
+** Unconditionally enable API_ARMOR in the JNI 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_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_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_J... */
+#ifdef SQLITE_JNI_FATAL_OOM
+#if !SQLITE_JNI_FATAL_OOM
+#undef SQLITE_JNI_FATAL_OOM
+#endif
+#endif
+
+/**********************************************************************/
+/* SQLITE_M... */
+#ifndef SQLITE_MAX_ALLOCATION_SIZE
+# define SQLITE_MAX_ALLOCATION_SIZE 0x1fffffff
+#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
+#ifdef SQLITE_OMIT_UTF16
+/* UTF16 is required for java */
+# undef SQLITE_OMIT_UTF16 1
+#endif
+
+/**********************************************************************/
+/* SQLITE_T... */
+#ifndef SQLITE_TEMP_STORE
+# define SQLITE_TEMP_STORE 2
+#endif
+#ifndef SQLITE_THREADSAFE
+# define SQLITE_THREADSAFE 1
+#endif
+
+/**********************************************************************/
+/* SQLITE_USE_... */
+#ifndef SQLITE_USE_URI
+#  define SQLITE_USE_URI 1
+#endif
+
+
+/*
+** Which sqlite3.c we're using needs to be configurable to enable
+** building against a custom copy, e.g. the SEE variant. We have to
+** include sqlite3.c, as opposed to sqlite3.h, in order to get access
+** to some interal details like SQLITE_MAX_... and friends. This
+** increases the rebuild time considerably but we need this in order
+** to access some internal functionality and keep the to-Java-exported
+** values of SQLITE_MAX_... and SQLITE_LIMIT_... in sync with the C
+** build.
+*/
+#ifndef SQLITE_C
+# define SQLITE_C sqlite3.c
+#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
+
+/*
+** End of the sqlite3 lib setup. What follows is JNI-specific.
+*/
+
+#include "sqlite3-jni.h"
+#include 
+#include  /* only for testing/debugging */
+#include  /* intptr_t for 32-bit builds */
+
+/* Only for debugging */
+#define MARKER(pfexp)                                               \
+  do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__);   \
+    printf pfexp;                                                   \
+  } while(0)
+
+/*
+** Creates a verbose JNI function name. Suffix must be
+** the JNI-mangled form of the function's name, minus the
+** prefix seen in this macro.
+*/
+#define JniFuncName(Suffix) \
+  Java_org_sqlite_jni_capi_CApi_sqlite3_ ## Suffix
+
+/* Prologue for JNI function declarations and definitions. */
+#define JniDecl(ReturnType,Suffix) \
+  JNIEXPORT ReturnType JNICALL JniFuncName(Suffix)
+
+/*
+** S3JniApi's intent is that CFunc be the C API func(s) the
+** being-declared JNI function is wrapping, making it easier to find
+** that function's JNI-side entry point. The other args are for JniDecl.
+** See the many examples in this file.
+*/
+#define S3JniApi(CFunc,ReturnType,Suffix) JniDecl(ReturnType,Suffix)
+
+/*
+** S3JniCast_L2P and P2L cast jlong (64-bit) to/from pointers. This is
+** required for casting warning-free on 32-bit builds, where we
+** otherwise get complaints that we're casting between different-sized
+** int types.
+**
+** This use of intptr_t is the _only_ reason we require 
+** which, in turn, requires building with -std=c99 (or later).
+*/
+#define S3JniCast_L2P(JLongAsPtr) (void*)((intptr_t)(JLongAsPtr))
+#define S3JniCast_P2L(PTR) (jlong)((intptr_t)(PTR))
+
+/*
+** Shortcuts for the first 2 parameters to all JNI bindings.
+**
+** The type of the jSelf arg differs, but no docs seem to mention
+** this: for static methods it's of type jclass and for non-static
+** it's jobject. jobject actually works for all funcs, in the sense
+** that it compiles and runs so long as we don't use jSelf (which is
+** only rarely needed in this code), but to be pedantically correct we
+** need the proper type in the signature.
+**
+** https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#jni_interface_functions_and_pointers
+*/
+#define JniArgsEnvObj JNIEnv * const env, jobject jSelf
+#define JniArgsEnvClass JNIEnv * const env, jclass jKlazz
+/*
+** Helpers to account for -Xcheck:jni warnings about not having
+** checked for exceptions.
+*/
+#define S3JniIfThrew if( (*env)->ExceptionCheck(env) )
+#define S3JniExceptionClear (*env)->ExceptionClear(env)
+#define S3JniExceptionReport (*env)->ExceptionDescribe(env)
+#define S3JniExceptionIgnore S3JniIfThrew S3JniExceptionClear
+#define S3JniExceptionWarnIgnore \
+  S3JniIfThrew {S3JniExceptionReport; S3JniExceptionClear;}(void)0
+#define S3JniExceptionWarnCallbackThrew(STR)             \
+  MARKER(("WARNING: " STR " MUST NOT THROW.\n"));  \
+  (*env)->ExceptionDescribe(env)
+
+/** To be used for cases where we're _really_ not expecting an
+    exception, e.g. looking up well-defined Java class members. */
+#define S3JniExceptionIsFatal(MSG) S3JniIfThrew {\
+    S3JniExceptionReport; S3JniExceptionClear; \
+    (*env)->FatalError(env, MSG); \
+  }
+
+/*
+** Declares local var env = s3jni_env(). All JNI calls involve a
+** JNIEnv somewhere, always named env, and many of our macros assume
+** env is in scope. Where it's not, but should be, use this to make it
+** so.
+*/
+#define S3JniDeclLocal_env JNIEnv * const env = s3jni_env()
+
+/* Fail fatally with an OOM message. */
+static inline void s3jni_oom(JNIEnv * const env){
+  (*env)->FatalError(env, "SQLite3 JNI is out of memory.") /* does not return */;
+}
+
+/*
+** sqlite3_malloc() proxy which fails fatally on OOM.  This should
+** only be used for routines which manage global state and have no
+** recovery strategy for OOM. For sqlite3 API which can reasonably
+** return SQLITE_NOMEM, s3jni_malloc() should be used instead.
+*/
+static void * s3jni_malloc_or_die(JNIEnv * const env, size_t n){
+  void * const rv = sqlite3_malloc(n);
+  if( n && !rv ) s3jni_oom(env);
+  return rv;
+}
+
+/*
+** Works like sqlite3_malloc() unless built with SQLITE_JNI_FATAL_OOM,
+** in which case it calls s3jni_oom() on OOM.
+*/
+#ifdef SQLITE_JNI_FATAL_OOM
+#define s3jni_malloc(SIZE) s3jni_malloc_or_die(env, SIZE)
+#else
+#define s3jni_malloc(SIZE) sqlite3_malloc(((void)env,(SIZE)))
+/* the ((void)env) trickery here is to avoid ^^^^^^ an otherwise
+   unused arg in at least one place. */
+#endif
+
+/*
+** Works like sqlite3_realloc() unless built with SQLITE_JNI_FATAL_OOM,
+** in which case it calls s3jni_oom() on OOM.
+*/
+#ifdef SQLITE_JNI_FATAL_OOM
+static void * s3jni_realloc_or_die(JNIEnv * const env, void * p, size_t n){
+  void * const rv = sqlite3_realloc(p, (int)n);
+  if( n && !rv ) s3jni_oom(env);
+  return rv;
+}
+#define s3jni_realloc(MEM,SIZE) s3jni_realloc_or_die(env, (MEM), (SIZE))
+#else
+#define s3jni_realloc(MEM,SIZE) sqlite3_realloc((MEM), ((void)env, (SIZE)))
+#endif
+
+/* Fail fatally if !EXPR. */
+#define s3jni_oom_fatal(EXPR) if( !(EXPR) ) s3jni_oom(env)
+/* Maybe fail fatally if !EXPR. */
+#ifdef SQLITE_JNI_FATAL_OOM
+#define s3jni_oom_check s3jni_oom_fatal
+#else
+#define s3jni_oom_check(EXPR)
+#endif
+//#define S3JniDb_oom(pDb,EXPR) ((EXPR) ? sqlite3OomFault(pDb) : 0)
+
+#define s3jni_db_oom(pDb) (void)((pDb) ? ((pDb)->mallocFailed=1) : 0)
+
+/* Helpers for Java value reference management. */
+static jobject s3jni_ref_global(JNIEnv * const env, jobject const v){
+  jobject const rv = v ? (*env)->NewGlobalRef(env, v) : NULL;
+  s3jni_oom_fatal( v ? !!rv : 1 );
+  return rv;
+}
+static jobject s3jni_ref_local(JNIEnv * const env, jobject const v){
+  jobject const rv = v ? (*env)->NewLocalRef(env, v) : NULL;
+  s3jni_oom_fatal( v ? !!rv : 1 );
+  return rv;
+}
+static inline void s3jni_unref_global(JNIEnv * const env, jobject const v){
+  if( v ) (*env)->DeleteGlobalRef(env, v);
+}
+static inline void s3jni_unref_local(JNIEnv * const env, jobject const v){
+  if( v ) (*env)->DeleteLocalRef(env, v);
+}
+#define S3JniRefGlobal(VAR) s3jni_ref_global(env, (VAR))
+#define S3JniRefLocal(VAR) s3jni_ref_local(env, (VAR))
+#define S3JniUnrefGlobal(VAR) s3jni_unref_global(env, (VAR))
+#define S3JniUnrefLocal(VAR) s3jni_unref_local(env, (VAR))
+
+/*
+** Lookup key type for use with s3jni_nphop() and a cache of a
+** frequently-needed Java-side class reference and one or two Java
+** class member IDs.
+*/
+typedef struct S3JniNphOp S3JniNphOp;
+struct S3JniNphOp {
+  const int index             /* index into S3JniGlobal.nph[] */;
+  const char * const zName    /* Full Java name of the class */;
+  const char * const zMember  /* Name of member property */;
+  const char * const zTypeSig /* JNI type signature of zMember */;
+  /*
+  ** klazz is a global ref to the class represented by pRef.
+  **
+  ** According to:
+  **
+  **   https://developer.ibm.com/articles/j-jni/
+  **
+  ** > ... the IDs returned for a given class don't change for the
+  **   lifetime of the JVM process. But the call to get the field or
+  **   method can require significant work in the JVM, because fields
+  **   and methods might have been inherited from superclasses, making
+  **   the JVM walk up the class hierarchy to find them. Because the
+  **   IDs are the same for a given class, you should look them up
+  **   once and then reuse them. Similarly, looking up class objects
+  **   can be expensive, so they should be cached as well.
+  */
+  jclass klazz;
+  volatile jfieldID fidValue  /* NativePointerHolder.nativePointer or
+                              ** OutputPointer.T.value */;
+  volatile jmethodID midCtor  /* klazz's no-arg constructor. Used by
+                              ** NativePointerHolder_new(). */;
+};
+
+/*
+** Cache keys for each concrete NativePointerHolder subclasses and
+** OutputPointer.T types. The members are to be used with s3jni_nphop()
+** and friends, and each one's member->index corresponds to its index
+** in the S3JniGlobal.nph[] array.
+*/
+static const struct {
+  const S3JniNphOp sqlite3;
+  const S3JniNphOp sqlite3_backup;
+  const S3JniNphOp sqlite3_blob;
+  const S3JniNphOp sqlite3_context;
+  const S3JniNphOp sqlite3_stmt;
+  const S3JniNphOp sqlite3_value;
+  const S3JniNphOp OutputPointer_Bool;
+  const S3JniNphOp OutputPointer_Int32;
+  const S3JniNphOp OutputPointer_Int64;
+  const S3JniNphOp OutputPointer_sqlite3;
+  const S3JniNphOp OutputPointer_sqlite3_blob;
+  const S3JniNphOp OutputPointer_sqlite3_stmt;
+  const S3JniNphOp OutputPointer_sqlite3_value;
+  const S3JniNphOp OutputPointer_String;
+#ifdef SQLITE_ENABLE_FTS5
+  const S3JniNphOp OutputPointer_ByteArray;
+  const S3JniNphOp Fts5Context;
+  const S3JniNphOp Fts5ExtensionApi;
+  const S3JniNphOp fts5_api;
+  const S3JniNphOp fts5_tokenizer;
+  const S3JniNphOp Fts5Tokenizer;
+#endif
+} S3JniNphOps = {
+#define MkRef(INDEX, KLAZZ, MEMBER, SIG) \
+  { INDEX, "org/sqlite/jni/" KLAZZ, MEMBER, SIG }
+/* NativePointerHolder ref */
+#define RefN(INDEX, KLAZZ) MkRef(INDEX, KLAZZ, "nativePointer", "J")
+/* OutputPointer.T ref */
+#define RefO(INDEX, KLAZZ, SIG) MkRef(INDEX, KLAZZ, "value", SIG)
+  RefN(0,  "capi/sqlite3"),
+  RefN(1,  "capi/sqlite3_backup"),
+  RefN(2,  "capi/sqlite3_blob"),
+  RefN(3,  "capi/sqlite3_context"),
+  RefN(4,  "capi/sqlite3_stmt"),
+  RefN(5,  "capi/sqlite3_value"),
+  RefO(6,  "capi/OutputPointer$Bool",  "Z"),
+  RefO(7,  "capi/OutputPointer$Int32", "I"),
+  RefO(8,  "capi/OutputPointer$Int64", "J"),
+  RefO(9,  "capi/OutputPointer$sqlite3",
+           "Lorg/sqlite/jni/capi/sqlite3;"),
+  RefO(10, "capi/OutputPointer$sqlite3_blob",
+           "Lorg/sqlite/jni/capi/sqlite3_blob;"),
+  RefO(11, "capi/OutputPointer$sqlite3_stmt",
+           "Lorg/sqlite/jni/capi/sqlite3_stmt;"),
+  RefO(12, "capi/OutputPointer$sqlite3_value",
+           "Lorg/sqlite/jni/capi/sqlite3_value;"),
+  RefO(13, "capi/OutputPointer$String", "Ljava/lang/String;"),
+#ifdef SQLITE_ENABLE_FTS5
+  RefO(14, "capi/OutputPointer$ByteArray", "[B"),
+  RefN(15, "fts5/Fts5Context"),
+  RefN(16, "fts5/Fts5ExtensionApi"),
+  RefN(17, "fts5/fts5_api"),
+  RefN(18, "fts5/fts5_tokenizer"),
+  RefN(19, "fts5/Fts5Tokenizer")
+#endif
+#undef MkRef
+#undef RefN
+#undef RefO
+};
+
+#define S3JniNph(T) &S3JniNphOps.T
+
+enum {
+  /*
+  ** Size of the NativePointerHolder cache.  Need enough space for
+  ** (only) the library's NativePointerHolder and OutputPointer types,
+  ** a fixed count known at build-time.  This value needs to be
+  ** exactly the number of S3JniNphOp entries in the S3JniNphOps
+  ** object.
+  */
+  S3Jni_NphCache_size = sizeof(S3JniNphOps) / sizeof(S3JniNphOp)
+};
+
+/*
+** State for binding C callbacks to Java methods.
+*/
+typedef struct S3JniHook S3JniHook;
+struct S3JniHook{
+  jobject jObj            /* global ref to Java instance */;
+  jmethodID midCallback   /* callback method. Signature depends on
+                          ** jObj's type */;
+  /* We lookup the jObj.xDestroy() method as-needed for contexts which
+  ** support custom finalizers. Fundamentally we can support them for
+  ** any Java type, but we only want to expose support for them where
+  ** the C API does. */
+  jobject jExtra          /* Global ref to a per-hook-type value */;
+  int doXDestroy          /* If true then S3JniHook_unref() will call
+                             jObj->xDestroy() if it's available. */;
+  S3JniHook * pNext      /* Next entry in S3Global.hooks.aFree */;
+};
+/* For clean bitwise-copy init of local instances. */
+static const S3JniHook S3JniHook_empty = {0,0,0,0,0};
+
+/*
+** Per-(sqlite3*) state for various JNI bindings.  This state is
+** allocated as needed, cleaned up in sqlite3_close(_v2)(), and
+** recycled when possible.
+**
+** Trivia: vars and parameters of this type are often named "ps"
+** because this class used to have a name for which that abbreviation
+** made sense.
+*/
+typedef struct S3JniDb S3JniDb;
+struct S3JniDb {
+  sqlite3 *pDb  /* The associated db handle */;
+  jobject jDb   /* A global ref of the output object which gets
+                   returned from sqlite3_open(_v2)(). We need this in
+                   order to have an object to pass to routines like
+                   sqlite3_collation_needed()'s callback, or else we
+                   have to dynamically create one for that purpose,
+                   which would be fine except that it would be a
+                   different instance (and maybe even a different
+                   class) than the one the user may expect to
+                   receive. */;
+  char * zMainDbName  /* Holds the string allocated on behalf of
+                         SQLITE_DBCONFIG_MAINDBNAME. */;
+  struct {
+    S3JniHook busyHandler;
+    S3JniHook collationNeeded;
+    S3JniHook commit;
+    S3JniHook progress;
+    S3JniHook rollback;
+    S3JniHook trace;
+    S3JniHook update;
+    S3JniHook auth;
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+    S3JniHook preUpdate;
+#endif
+  } hooks;
+#ifdef SQLITE_ENABLE_FTS5
+  /* FTS5-specific state */
+  struct {
+    jobject jApi  /* global ref to s3jni_fts5_api_from_db() */;
+  } fts;
+#endif
+  S3JniDb * pNext /* Next entry in SJG.perDb.aFree */;
+};
+
+static const char * const S3JniDb_clientdata_key = "S3JniDb";
+#define S3JniDb_from_clientdata(pDb)                                \
+  (pDb ? sqlite3_get_clientdata(pDb, S3JniDb_clientdata_key) : 0)
+
+/*
+** Cache for per-JNIEnv (i.e. per-thread) data.
+**
+** Trivia: vars and parameters of this type are often named "jc"
+** because this class used to have a name for which that abbreviation
+** made sense.
+*/
+typedef struct S3JniEnv S3JniEnv;
+struct S3JniEnv {
+  JNIEnv *env /* JNIEnv in which this cache entry was created */;
+  /*
+  ** pdbOpening is used to coordinate the Java/DB connection of a
+  ** being-open()'d db in the face of auto-extensions.
+  ** Auto-extensions run before we can bind the C db to its Java
+  ** representation, but auto-extensions require that binding to pass
+  ** on to their Java-side callbacks. We handle this as follows:
+  **
+  ** - In the JNI side of sqlite3_open(), allocate the Java side of
+  **   that connection and set pdbOpening to point to that
+  **   object.
+  **
+  ** - Call sqlite3_open(), which triggers the auto-extension
+  **   handler.  That handler uses pdbOpening to connect the native
+  **   db handle which it receives with pdbOpening.
+  **
+  ** - When sqlite3_open() returns, check whether pdbOpening->pDb is
+  **   NULL. If it isn't, auto-extension handling set it up.  If it
+  **   is, complete the Java/C binding unless sqlite3_open() returns
+  **   a NULL db, in which case free pdbOpening.
+  */
+  S3JniDb * pdbOpening;
+  S3JniEnv * pNext /* Next entry in SJG.envCache.aHead or
+                      SJG.envCache.aFree */;
+};
+
+/*
+** State for proxying sqlite3_auto_extension() in Java. This was
+** initially a separate class from S3JniHook and now the older name is
+** retained for readability in the APIs which use this, as well as for
+** its better code-searchability.
+*/
+typedef S3JniHook S3JniAutoExtension;
+
+/*
+** Type IDs for SQL function categories.
+*/
+enum UDFType {
+  UDF_UNKNOWN_TYPE = 0/*for error propagation*/,
+  UDF_SCALAR,
+  UDF_AGGREGATE,
+  UDF_WINDOW
+};
+
+/*
+** State for binding Java-side UDFs.
+*/
+typedef struct S3JniUdf S3JniUdf;
+struct S3JniUdf {
+  jobject jObj           /* SQLFunction instance */;
+  char * zFuncName       /* Only for error reporting and debug logging */;
+  enum UDFType type      /* UDF type */;
+  /** Method IDs for the various UDF methods. */
+  jmethodID jmidxFunc    /* xFunc method (scalar) */;
+  jmethodID jmidxStep    /* xStep method (aggregate/window) */;
+  jmethodID jmidxFinal   /* xFinal method (aggregate/window) */;
+  jmethodID jmidxValue   /* xValue method (window) */;
+  jmethodID jmidxInverse /* xInverse method (window) */;
+  S3JniUdf * pNext       /* Next entry in SJG.udf.aFree. */;
+};
+
+#if defined(SQLITE_JNI_ENABLE_METRICS) && 0==SQLITE_JNI_ENABLE_METRICS
+#  undef SQLITE_JNI_ENABLE_METRICS
+#endif
+
+/*
+** If true, modifying S3JniGlobal.metrics is protected by a mutex,
+** else it isn't.
+*/
+#ifdef SQLITE_DEBUG
+#  define S3JNI_METRICS_MUTEX SQLITE_THREADSAFE
+#else
+#  define S3JNI_METRICS_MUTEX 0
+#endif
+#ifndef SQLITE_JNI_ENABLE_METRICS
+#  undef S3JNI_METRICS_MUTEX
+#  define S3JNI_METRICS_MUTEX 0
+#endif
+
+/*
+** Global state, e.g. caches and metrics.
+*/
+typedef struct S3JniGlobalType S3JniGlobalType;
+struct S3JniGlobalType {
+  /*
+  ** According to: https://developer.ibm.com/articles/j-jni/
+  **
+  ** > A thread can get a JNIEnv by calling GetEnv() using the JNI
+  **   invocation interface through a JavaVM object. The JavaVM object
+  **   itself can be obtained by calling the JNI GetJavaVM() method
+  **   using a JNIEnv object and can be cached and shared across
+  **   threads. Caching a copy of the JavaVM object enables any thread
+  **   with access to the cached object to get access to its own
+  **   JNIEnv when necessary.
+  */
+  JavaVM * jvm;
+  /*
+  ** Global mutex. It must not be used for anything which might call
+  ** back into the JNI layer.
+  */
+  sqlite3_mutex * mutex;
+  /*
+  ** Cache of references to Java classes and method IDs for
+  ** NativePointerHolder subclasses and OutputPointer.T types.
+  */
+  struct {
+    S3JniNphOp list[S3Jni_NphCache_size];
+    sqlite3_mutex * mutex;    /* mutex for this->list */
+    volatile void const * locker;  /* sanity-checking-only context object
+                                      for this->mutex */
+  } nph;
+  /*
+  ** Cache of per-thread state.
+  */
+  struct {
+    S3JniEnv * aHead      /* Linked list of in-use instances */;
+    S3JniEnv * aFree      /* Linked list of free instances */;
+    sqlite3_mutex * mutex /* mutex for aHead and aFree. */;
+    volatile void const * locker  /* env mutex is held on this
+                                     object's behalf.  Used only for
+                                     sanity checking. */;
+  } envCache;
+  /*
+  ** Per-db state. This can move into the core library once we can tie
+  ** client-defined state to db handles there.
+  */
+  struct {
+    S3JniDb * aFree  /* Linked list of free instances */;
+    sqlite3_mutex * mutex /* mutex for aHead and aFree */;
+    volatile void const * locker
+    /* perDb mutex is held on this object's behalf. Used only for
+       sanity checking. Note that the mutex is at the class level, not
+       instance level. */;
+  } perDb;
+  struct {
+    S3JniUdf * aFree    /* Head of the free-item list. Guarded by global
+                           mutex. */;
+  } udf;
+  /*
+  ** Refs to global classes and methods. Obtained during static init
+  ** and never released.
+  */
+  struct {
+    jclass cLong             /* global ref to java.lang.Long */;
+    jclass cString           /* global ref to java.lang.String */;
+    jobject oCharsetUtf8     /* global ref to StandardCharset.UTF_8 */;
+    jmethodID ctorLong1      /* the Long(long) constructor */;
+    jmethodID ctorStringBA   /* the String(byte[],Charset) constructor */;
+    jmethodID stringGetBytes /* the String.getBytes(Charset) method */;
+  } g;
+  /*
+  ** The list of Java-side auto-extensions
+  ** (org.sqlite.jni.capi.AutoExtensionCallback objects).
+  */
+  struct {
+    S3JniAutoExtension *aExt /* The auto-extension list. It is
+                                maintained such that all active
+                                entries are in the first contiguous
+                                nExt array elements. */;
+    int nAlloc               /* number of entries allocated for aExt,
+                                as distinct from the number of active
+                                entries. */;
+    int nExt                 /* number of active entries in aExt, all in the
+                                first nExt'th array elements. */;
+    sqlite3_mutex * mutex    /* mutex for manipulation/traversal of aExt */;
+    volatile const void * locker /* object on whose behalf the mutex
+                                    is held.  Only for sanity checking
+                                    in debug builds. */;
+  } autoExt;
+#ifdef SQLITE_ENABLE_FTS5
+  struct {
+    volatile jobject jExt /* Global ref to Java singleton for the
+                             Fts5ExtensionApi instance. */;
+    struct {
+      jfieldID fidA       /* Fts5Phrase::a member */;
+      jfieldID fidB       /* Fts5Phrase::b member */;
+    } jPhraseIter;
+  } fts5;
+#endif
+  struct {
+#ifdef SQLITE_ENABLE_SQLLOG
+    S3JniHook sqllog      /* sqlite3_config(SQLITE_CONFIG_SQLLOG) callback */;
+#endif
+    S3JniHook configlog   /* sqlite3_config(SQLITE_CONFIG_LOG) callback */;
+    S3JniHook * aFree     /* free-item list, for recycling. */;
+    sqlite3_mutex * mutex /* mutex for aFree */;
+    volatile const void * locker /* object on whose behalf the mutex
+                                    is held.  Only for sanity checking
+                                    in debug builds. */;
+  } hook;
+#ifdef SQLITE_JNI_ENABLE_METRICS
+  /* Internal metrics. */
+  struct {
+    volatile unsigned nEnvHit;
+    volatile unsigned nEnvMiss;
+    volatile unsigned nEnvAlloc;
+    volatile unsigned nMutexEnv       /* number of times envCache.mutex was entered for
+                                         a S3JniEnv operation. */;
+    volatile unsigned nMutexNph       /* number of times SJG.mutex was entered */;
+    volatile unsigned nMutexHook      /* number of times SJG.mutex hooks.was entered */;
+    volatile unsigned nMutexPerDb     /* number of times perDb.mutex was entered */;
+    volatile unsigned nMutexAutoExt   /* number of times autoExt.mutex was entered */;
+    volatile unsigned nMutexGlobal    /* number of times global mutex was entered. */;
+    volatile unsigned nMutexUdf       /* number of times global mutex was entered
+                                         for UDFs. */;
+    volatile unsigned nDestroy        /* xDestroy() calls across all types */;
+    volatile unsigned nPdbAlloc       /* Number of S3JniDb alloced. */;
+    volatile unsigned nPdbRecycled    /* Number of S3JniDb reused. */;
+    volatile unsigned nUdfAlloc       /* Number of S3JniUdf alloced. */;
+    volatile unsigned nUdfRecycled    /* Number of S3JniUdf reused. */;
+    volatile unsigned nHookAlloc      /* Number of S3JniHook alloced. */;
+    volatile unsigned nHookRecycled   /* Number of S3JniHook reused. */;
+    struct {
+      /* Number of calls for each type of UDF callback. */
+      volatile unsigned nFunc;
+      volatile unsigned nStep;
+      volatile unsigned nFinal;
+      volatile unsigned nValue;
+      volatile unsigned nInverse;
+    } udf;
+    unsigned nMetrics                 /* Total number of mutex-locked
+                                         metrics increments. */;
+#if S3JNI_METRICS_MUTEX
+    sqlite3_mutex * mutex;
+#endif
+  } metrics;
+#endif /* SQLITE_JNI_ENABLE_METRICS */
+};
+static S3JniGlobalType S3JniGlobal = {};
+#define SJG S3JniGlobal
+
+/* Increments *p, possibly protected by a mutex. */
+#ifndef SQLITE_JNI_ENABLE_METRICS
+#define s3jni_incr(PTR)
+#elif S3JNI_METRICS_MUTEX
+static void s3jni_incr( volatile unsigned int * const p ){
+  sqlite3_mutex_enter(SJG.metrics.mutex);
+  ++SJG.metrics.nMetrics;
+  ++(*p);
+  sqlite3_mutex_leave(SJG.metrics.mutex);
+}
+#else
+#define s3jni_incr(PTR) ++(*(PTR))
+#endif
+
+/* Helpers for working with specific mutexes. */
+#if SQLITE_THREADSAFE
+#define s3jni_mutex_enter2(M, Metric) \
+  sqlite3_mutex_enter( M );           \
+  s3jni_incr( &SJG.metrics.Metric )
+#define s3jni_mutex_leave2(M) \
+  sqlite3_mutex_leave( M )
+
+#define s3jni_mutex_enter(M, L, Metric)                    \
+  assert( (void*)env != (void*)L && "Invalid use of " #L); \
+  s3jni_mutex_enter2( M, Metric );                         \
+  L = env
+#define s3jni_mutex_leave(M, L)                            \
+  assert( (void*)env == (void*)L && "Invalid use of " #L); \
+  L = 0;                                                   \
+  s3jni_mutex_leave2( M )
+
+#define S3JniEnv_mutex_assertLocked \
+  assert( 0 != SJG.envCache.locker && "Misuse of S3JniGlobal.envCache.mutex" )
+#define S3JniEnv_mutex_assertLocker \
+  assert( (env) == SJG.envCache.locker && "Misuse of S3JniGlobal.envCache.mutex" )
+#define S3JniEnv_mutex_assertNotLocker \
+  assert( (env) != SJG.envCache.locker && "Misuse of S3JniGlobal.envCache.mutex" )
+
+#define S3JniEnv_mutex_enter \
+  s3jni_mutex_enter( SJG.envCache.mutex, SJG.envCache.locker, nMutexEnv )
+#define S3JniEnv_mutex_leave \
+  s3jni_mutex_leave( SJG.envCache.mutex, SJG.envCache.locker )
+
+#define S3JniAutoExt_mutex_enter \
+  s3jni_mutex_enter( SJG.autoExt.mutex, SJG.autoExt.locker, nMutexAutoExt )
+#define S3JniAutoExt_mutex_leave \
+  s3jni_mutex_leave( SJG.autoExt.mutex, SJG.autoExt.locker )
+#define S3JniAutoExt_mutex_assertLocker                     \
+  assert( env == SJG.autoExt.locker && "Misuse of S3JniGlobal.autoExt.mutex" )
+
+#define S3JniGlobal_mutex_enter \
+  s3jni_mutex_enter2( SJG.mutex, nMutexGlobal )
+#define S3JniGlobal_mutex_leave \
+  s3jni_mutex_leave2( SJG.mutex )
+
+#define S3JniHook_mutex_enter \
+  s3jni_mutex_enter( SJG.hook.mutex, SJG.hook.locker, nMutexHook )
+#define S3JniHook_mutex_leave \
+  s3jni_mutex_leave( SJG.hook.mutex, SJG.hook.locker )
+
+#define S3JniNph_mutex_enter \
+  s3jni_mutex_enter( SJG.nph.mutex, SJG.nph.locker, nMutexNph )
+#define S3JniNph_mutex_leave \
+  s3jni_mutex_leave( SJG.nph.mutex, SJG.nph.locker )
+
+#define S3JniDb_mutex_assertLocker \
+  assert( (env) == SJG.perDb.locker && "Misuse of S3JniGlobal.perDb.mutex" )
+#define S3JniDb_mutex_enter \
+  s3jni_mutex_enter( SJG.perDb.mutex, SJG.perDb.locker, nMutexPerDb )
+#define S3JniDb_mutex_leave \
+  s3jni_mutex_leave( SJG.perDb.mutex, SJG.perDb.locker )
+
+#else /* SQLITE_THREADSAFE==0 */
+#define S3JniAutoExt_mutex_assertLocker
+#define S3JniAutoExt_mutex_enter
+#define S3JniAutoExt_mutex_leave
+#define S3JniDb_mutex_assertLocker
+#define S3JniDb_mutex_enter
+#define S3JniDb_mutex_leave
+#define S3JniEnv_mutex_assertLocked
+#define S3JniEnv_mutex_assertLocker
+#define S3JniEnv_mutex_assertNotLocker
+#define S3JniEnv_mutex_enter
+#define S3JniEnv_mutex_leave
+#define S3JniGlobal_mutex_enter
+#define S3JniGlobal_mutex_leave
+#define S3JniHook_mutex_enter
+#define S3JniHook_mutex_leave
+#define S3JniNph_mutex_enter
+#define S3JniNph_mutex_leave
+#endif
+
+/* Helpers for jstring and jbyteArray. */
+static const char * s3jni__jstring_to_mutf8(JNIEnv * const env, jstring v ){
+  const char *z = v ? (*env)->GetStringUTFChars(env, v, NULL) : 0;
+  s3jni_oom_check( v ? !!z : !z );
+  return z;
+}
+
+#define s3jni_jstring_to_mutf8(ARG) s3jni__jstring_to_mutf8(env, (ARG))
+#define s3jni_mutf8_release(ARG,VAR) if( VAR ) (*env)->ReleaseStringUTFChars(env, ARG, VAR)
+
+/*
+** If jBA is not NULL then its GetByteArrayElements() value is
+** returned. If jBA is not NULL and nBA is not NULL then *nBA is set
+** to the GetArrayLength() of jBA. If GetByteArrayElements() requires
+** an allocation and that allocation fails then this function either
+** fails fatally or returns 0, depending on build-time options.
+ */
+static jbyte * s3jni__jbyteArray_bytes2(JNIEnv * const env, jbyteArray jBA, jsize * nBA ){
+  jbyte * const rv = jBA ? (*env)->GetByteArrayElements(env, jBA, NULL) : 0;
+  s3jni_oom_check( jBA ? !!rv : 1 );
+  if( jBA && nBA ) *nBA = (*env)->GetArrayLength(env, jBA);
+  return rv;
+}
+
+#define s3jni_jbyteArray_bytes2(jByteArray,ptrToSz) \
+  s3jni__jbyteArray_bytes2(env, (jByteArray), (ptrToSz))
+#define s3jni_jbyteArray_bytes(jByteArray) s3jni__jbyteArray_bytes2(env, (jByteArray), 0)
+#define s3jni_jbyteArray_release(jByteArray,jBytes) \
+  if( jBytes ) (*env)->ReleaseByteArrayElements(env, jByteArray, jBytes, JNI_ABORT)
+#define s3jni_jbyteArray_commit(jByteArray,jBytes) \
+  if( jBytes ) (*env)->ReleaseByteArrayElements(env, jByteArray, jBytes, JNI_COMMIT)
+
+/*
+** Returns the current JNIEnv object. Fails fatally if it cannot find
+** the object.
+*/
+static JNIEnv * s3jni_env(void){
+  JNIEnv * env = 0;
+  if( (*SJG.jvm)->GetEnv(SJG.jvm, (void **)&env,
+                                 JNI_VERSION_1_8) ){
+    fprintf(stderr, "Fatal error: cannot get current JNIEnv.\n");
+    abort();
+  }
+  return env;
+}
+
+/*
+** Fetches the S3JniGlobal.envCache row for the given env, allocing a
+** row if needed. When a row is allocated, its state is initialized
+** insofar as possible. Calls (*env)->FatalError() if allocation of an
+** entry fails. That's hypothetically possible but "shouldn't happen."
+*/
+static S3JniEnv * S3JniEnv__get(JNIEnv * const env){
+  struct S3JniEnv * row;
+  S3JniEnv_mutex_enter;
+  row = SJG.envCache.aHead;
+  for( ; row; row = row->pNext ){
+    if( row->env == env ){
+      s3jni_incr( &SJG.metrics.nEnvHit );
+      S3JniEnv_mutex_leave;
+      return row;
+    }
+  }
+  s3jni_incr( &SJG.metrics.nEnvMiss );
+  row = SJG.envCache.aFree;
+  if( row ){
+    SJG.envCache.aFree = row->pNext;
+  }else{
+    row = s3jni_malloc_or_die(env, sizeof(*row));
+    s3jni_incr( &SJG.metrics.nEnvAlloc );
+  }
+  memset(row, 0, sizeof(*row));
+  row->pNext = SJG.envCache.aHead;
+  SJG.envCache.aHead = row;
+  row->env = env;
+
+  S3JniEnv_mutex_leave;
+  return row;
+}
+
+#define S3JniEnv_get() S3JniEnv__get(env)
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own Java/JNI bindings.
+**
+** For purposes of certain hand-crafted JNI function bindings, we
+** need a way of reporting errors which is consistent with the rest of
+** the C API, as opposed to throwing Java exceptions. To that end, this
+** internal-use-only function is a thin proxy around
+** sqlite3ErrorWithMessage(). The intent is that it only be used from
+** JNI bindings such as sqlite3_prepare_v2/v3(), and definitely not
+** from client code.
+**
+** Returns err_code.
+*/
+static int s3jni_db_error(sqlite3* const db, int err_code,
+                          const char * const zMsg){
+  if( db!=0 ){
+    if( 0==zMsg ){
+      sqlite3Error(db, err_code);
+    }else{
+      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));
+    }
+  }
+  return err_code;
+}
+
+/*
+** Creates a new jByteArray of length nP, copies p's contents into it,
+** and returns that byte array (NULL on OOM unless fail-fast alloc
+** errors are enabled). p may be NULL, in which case the array is
+** created but no bytes are filled.
+*/
+static jbyteArray s3jni__new_jbyteArray(JNIEnv * const env,
+                                       const void * const p, int nP){
+  jbyteArray jba = (*env)->NewByteArray(env, (jint)nP);
+
+  s3jni_oom_check( jba );
+  if( jba && p ){
+    (*env)->SetByteArrayRegion(env, jba, 0, (jint)nP, (const jbyte*)p);
+  }
+  return jba;
+}
+
+#define s3jni_new_jbyteArray(P,n) s3jni__new_jbyteArray(env, P, n)
+
+
+/*
+** Uses the java.lang.String(byte[],Charset) constructor to create a
+** new String from UTF-8 string z. n is the number of bytes to
+** copy. If n<0 then sqlite3Strlen30() is used to calculate it.
+**
+** Returns NULL if z is NULL or on OOM, else returns a new jstring
+** owned by the caller.
+**
+** Sidebar: this is a painfully inefficient way to convert from
+** standard UTF-8 to a Java string, but JNI offers only algorithms for
+** working with MUTF-8, not UTF-8.
+*/
+static jstring s3jni__utf8_to_jstring(JNIEnv * const env,
+                                      const char * const z, int n){
+  jstring rv = NULL;
+  if( 0==n || (n<0 && z && !z[0]) ){
+    /* Fast-track the empty-string case via the MUTF-8 API. We could
+       hypothetically do this for any strings where n<4 and z is
+       NUL-terminated and none of z[0..3] are NUL bytes. */
+    rv = (*env)->NewStringUTF(env, "");
+    s3jni_oom_check( rv );
+  }else if( z ){
+    jbyteArray jba;
+    if( n<0 ) n = sqlite3Strlen30(z);
+    jba = s3jni_new_jbyteArray((unsigned const char *)z, n);
+    if( jba ){
+      rv = (*env)->NewObject(env, SJG.g.cString, SJG.g.ctorStringBA,
+                             jba, SJG.g.oCharsetUtf8);
+      S3JniIfThrew{
+        S3JniExceptionReport;
+        S3JniExceptionClear;
+      }
+      S3JniUnrefLocal(jba);
+    }
+    s3jni_oom_check( rv );
+  }
+  return rv;
+}
+#define s3jni_utf8_to_jstring(CStr,n) s3jni__utf8_to_jstring(env, CStr, n)
+
+/*
+** Converts the given java.lang.String object into a NUL-terminated
+** UTF-8 C-string by calling jstr.getBytes(StandardCharset.UTF_8).
+** Returns NULL if jstr is NULL or on allocation error. If jstr is not
+** NULL and nLen is not NULL then nLen is set to the length of the
+** returned string, not including the terminating NUL. If jstr is not
+** NULL and it returns NULL, this indicates an allocation error. In
+** that case, if nLen is not NULL then it is either set to 0 (if
+** fetching of jstr's bytes fails to allocate) or set to what would
+** have been the length of the string had C-string allocation
+** succeeded.
+**
+** The returned memory is allocated from sqlite3_malloc() and
+** ownership is transferred to the caller.
+*/
+static char * s3jni__jstring_to_utf8(JNIEnv * const env,
+                                    jstring jstr, int *nLen){
+  jbyteArray jba;
+  jsize nBA;
+  char *rv;
+
+  if( !jstr ) return 0;
+  jba = (*env)->CallObjectMethod(env, jstr, SJG.g.stringGetBytes,
+                                 SJG.g.oCharsetUtf8);
+
+  if( (*env)->ExceptionCheck(env) || !jba
+      /* order of these checks is significant for -Xlint:jni */ ) {
+    S3JniExceptionReport;
+    s3jni_oom_check( jba );
+    if( nLen ) *nLen = 0;
+    return 0;
+  }
+  nBA = (*env)->GetArrayLength(env, jba);
+  if( nLen ) *nLen = (int)nBA;
+  rv = s3jni_malloc( nBA + 1 );
+  if( rv ){
+    (*env)->GetByteArrayRegion(env, jba, 0, nBA, (jbyte*)rv);
+    rv[nBA] = 0;
+  }
+  S3JniUnrefLocal(jba);
+  return rv;
+}
+#define s3jni_jstring_to_utf8(JStr,n) s3jni__jstring_to_utf8(env, JStr, n)
+
+/*
+** Expects to be passed a pointer from sqlite3_column_text16() or
+** sqlite3_value_text16() and a byte-length value from
+** sqlite3_column_bytes16() or sqlite3_value_bytes16(). It creates a
+** Java String of exactly half that character length, returning NULL
+** if !p or (*env)->NewString() fails.
+*/
+static jstring s3jni_text16_to_jstring(JNIEnv * const env, const void * const p, int nP){
+  jstring const rv = p
+    ? (*env)->NewString(env, (const jchar *)p, (jsize)(nP/2))
+    : NULL;
+  s3jni_oom_check( p ? !!rv : 1 );
+  return rv;
+}
+
+/*
+** Requires jx to be a Throwable. Calls its toString() method and
+** returns its value converted to a UTF-8 string. The caller owns the
+** returned string and must eventually sqlite3_free() it.  Returns 0
+** if there is a problem fetching the info or on OOM.
+**
+** Design note: we use toString() instead of getMessage() because the
+** former includes the exception type's name:
+**
+**  Exception e = new RuntimeException("Hi");
+**  System.out.println(e.toString()); // java.lang.RuntimeException: Hi
+**  System.out.println(e.getMessage()); // Hi
+*/
+static char * s3jni_exception_error_msg(JNIEnv * const env, jthrowable jx){
+  jmethodID mid;
+  jstring msg;
+  char * zMsg;
+  jclass const klazz = (*env)->GetObjectClass(env, jx);
+  mid = (*env)->GetMethodID(env, klazz, "toString", "()Ljava/lang/String;");
+  S3JniUnrefLocal(klazz);
+  S3JniIfThrew{
+    S3JniExceptionReport;
+    S3JniExceptionClear;
+    return 0;
+  }
+  msg = (*env)->CallObjectMethod(env, jx, mid);
+  S3JniIfThrew{
+    S3JniExceptionReport;
+    S3JniExceptionClear;
+    return 0;
+  }
+  zMsg = s3jni_jstring_to_utf8( msg, 0);
+  S3JniUnrefLocal(msg);
+  return zMsg;
+}
+
+/*
+** Extracts env's current exception, sets ps->pDb's error message to
+** its message string, and clears the exception. If errCode is non-0,
+** it is used as-is, else SQLITE_ERROR is assumed. If there's a
+** problem extracting the exception's message, it's treated as
+** non-fatal and zDfltMsg is used in its place.
+**
+** Locks the global S3JniDb mutex.
+**
+** This must only be called if a JNI exception is pending.
+**
+** Returns errCode unless it is 0, in which case SQLITE_ERROR is
+** returned.
+*/
+static int s3jni__db_exception(JNIEnv * const env, sqlite3 * const pDb,
+                              int errCode, const char *zDfltMsg){
+  jthrowable const ex = (*env)->ExceptionOccurred(env);
+
+  if( 0==errCode ) errCode = SQLITE_ERROR;
+  if( ex ){
+    char * zMsg;
+    S3JniExceptionClear;
+    zMsg = s3jni_exception_error_msg(env, ex);
+    s3jni_db_error(pDb, errCode, zMsg ? zMsg : zDfltMsg);
+    sqlite3_free(zMsg);
+    S3JniUnrefLocal(ex);
+  }else if( zDfltMsg ){
+    s3jni_db_error(pDb, errCode, zDfltMsg);
+  }
+  return errCode;
+}
+#define s3jni_db_exception(pDb,ERRCODE,DFLTMSG) \
+  s3jni__db_exception(env, (pDb), (ERRCODE), (DFLTMSG) )
+
+/*
+** Extracts the (void xDestroy()) method from jObj and applies it to
+** jObj. If jObj is NULL, this is a no-op. The lack of an xDestroy()
+** method is silently ignored. Any exceptions thrown by xDestroy()
+** trigger a warning to stdout or stderr and then the exception is
+** suppressed.
+*/
+static void s3jni__call_xDestroy(JNIEnv * const env, jobject jObj){
+  if( jObj ){
+    jclass const klazz = (*env)->GetObjectClass(env, jObj);
+    jmethodID method = (*env)->GetMethodID(env, klazz, "xDestroy", "()V");
+
+    S3JniUnrefLocal(klazz);
+    if( method ){
+      s3jni_incr( &SJG.metrics.nDestroy );
+      (*env)->CallVoidMethod(env, jObj, method);
+      S3JniIfThrew{
+        S3JniExceptionWarnCallbackThrew("xDestroy() callback");
+        S3JniExceptionClear;
+      }
+    }else{
+      /* Non-fatal. */
+      S3JniExceptionClear;
+    }
+  }
+}
+#define s3jni_call_xDestroy(JOBJ) s3jni__call_xDestroy(env, (JOBJ))
+
+/*
+** Internal helper for many hook callback impls. Locks the S3JniDb
+** mutex, makes a copy of src into dest, with a some differences: (1)
+** if src->jObj or src->jExtra are not NULL then dest will be a new
+** LOCAL ref to it instead of a copy of the prior GLOBAL ref. (2)
+** dest->doXDestroy is always false.
+**
+** If dest->jObj is not NULL when this returns then the caller is
+** obligated to eventually free the new ref by passing *dest to
+** S3JniHook_localundup(). The dest pointer must NOT be passed to
+** S3JniHook_unref(), as that routine assumes that dest->jObj/jExtra
+** are GLOBAL refs (it's illegal to try to unref the wrong ref type).
+**
+** Background: when running a hook we need a call-local copy lest
+** another thread modify the hook while we're running it. That copy
+** has to have its own Java reference, but it need only be call-local.
+*/
+static void S3JniHook__localdup( JNIEnv * const env, S3JniHook const * const src,
+                                 S3JniHook * const dest ){
+  S3JniHook_mutex_enter;
+  *dest = *src;
+  if(src->jObj) dest->jObj = S3JniRefLocal(src->jObj);
+  if(src->jExtra) dest->jExtra = S3JniRefLocal(src->jExtra);
+  dest->doXDestroy = 0;
+  S3JniHook_mutex_leave;
+}
+#define S3JniHook_localdup(src,dest) S3JniHook__localdup(env,src,dest)
+
+static void S3JniHook__localundup( JNIEnv * const env, S3JniHook * const h  ){
+  S3JniUnrefLocal(h->jObj);
+  S3JniUnrefLocal(h->jExtra);
+  *h = S3JniHook_empty;
+}
+#define S3JniHook_localundup(HOOK) S3JniHook__localundup(env, &(HOOK))
+
+/*
+** Removes any Java references from s and clears its state. If
+** doXDestroy is true and s->jObj is not NULL, s->jObj
+** is passed to s3jni_call_xDestroy() before any references are
+** cleared. It is legal to call this when the object has no Java
+** references. s must not be NULL.
+*/
+static void S3JniHook__unref(JNIEnv * const env, S3JniHook * const s){
+  if( s->jObj ){
+    if( s->doXDestroy ){
+      s3jni_call_xDestroy(s->jObj);
+    }
+    S3JniUnrefGlobal(s->jObj);
+    S3JniUnrefGlobal(s->jExtra);
+  }else{
+    assert( !s->jExtra );
+  }
+  *s = S3JniHook_empty;
+}
+#define S3JniHook_unref(hook) S3JniHook__unref(env, (hook))
+
+/*
+** Allocates one blank S3JniHook object from the recycling bin, if
+** available, else from the heap. Returns NULL or dies on OOM,
+** depending on build options.  Locks on SJG.hooks.mutex.
+*/
+static S3JniHook *S3JniHook__alloc(JNIEnv  * const env){
+  S3JniHook * p = 0;
+  S3JniHook_mutex_enter;
+  if( SJG.hook.aFree ){
+    p = SJG.hook.aFree;
+    SJG.hook.aFree = p->pNext;
+    p->pNext = 0;
+    s3jni_incr(&SJG.metrics.nHookRecycled);
+  }
+  S3JniHook_mutex_leave;
+  if( 0==p ){
+    p = s3jni_malloc(sizeof(S3JniHook));
+    if( p ){
+      s3jni_incr(&SJG.metrics.nHookAlloc);
+    }
+  }
+  if( p ){
+    *p = S3JniHook_empty;
+  }
+  return p;
+}
+#define S3JniHook_alloc() S3JniHook__alloc(env)
+
+/*
+** The rightful fate of all results from S3JniHook_alloc(). Locks on
+** SJG.hook.mutex.
+*/
+static void S3JniHook__free(JNIEnv  * const env, S3JniHook * const p){
+  if(p){
+    assert( !p->pNext );
+    S3JniHook_unref(p);
+    S3JniHook_mutex_enter;
+    p->pNext = SJG.hook.aFree;
+    SJG.hook.aFree = p;
+    S3JniHook_mutex_leave;
+  }
+}
+#define S3JniHook_free(hook) S3JniHook__free(env, hook)
+
+#if 0
+/* S3JniHook__free() without the lock: caller must hold the global mutex */
+static void S3JniHook__free_unlocked(JNIEnv  * const env, S3JniHook * const p){
+  if(p){
+    assert( !p->pNext );
+    assert( p->pNext != SJG.hook.aFree );
+    S3JniHook_unref(p);
+    p->pNext = SJG.hook.aFree;
+    SJG.hook.aFree = p;
+  }
+}
+#define S3JniHook_free_unlocked(hook) S3JniHook__free_unlocked(env, hook)
+#endif
+
+/*
+** Clears all of s's state. Requires that that the caller has locked
+** S3JniGlobal.perDb.mutex. Make sure to do anything needed with
+** s->pNext and s->pPrev before calling this, as this clears them.
+*/
+static void S3JniDb_clear(JNIEnv * const env, S3JniDb * const s){
+  S3JniDb_mutex_assertLocker;
+  sqlite3_free( s->zMainDbName );
+#define UNHOOK(MEMBER) \
+  S3JniHook_unref(&s->hooks.MEMBER)
+  UNHOOK(auth);
+  UNHOOK(busyHandler);
+  UNHOOK(collationNeeded);
+  UNHOOK(commit);
+  UNHOOK(progress);
+  UNHOOK(rollback);
+  UNHOOK(trace);
+  UNHOOK(update);
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+  UNHOOK(preUpdate);
+#endif
+#undef UNHOOK
+  S3JniUnrefGlobal(s->jDb);
+  memset(s, 0, sizeof(S3JniDb));
+}
+
+/*
+** Clears s's state and moves it to the free-list. Requires that
+** S3JniGlobal.perDb.mutex is locked.
+*/
+static void S3JniDb__set_aside_unlocked(JNIEnv * const env, S3JniDb * const s){
+  assert( s );
+  S3JniDb_mutex_assertLocker;
+  if( s ){
+    S3JniDb_clear(env, s);
+    s->pNext = SJG.perDb.aFree;
+    SJG.perDb.aFree = s;
+  }
+}
+#define S3JniDb_set_aside_unlocked(JniDb) S3JniDb__set_aside_unlocked(env, JniDb)
+
+static void S3JniDb__set_aside(JNIEnv * const env, S3JniDb * const s){
+  S3JniDb_mutex_enter;
+  S3JniDb_set_aside_unlocked(s);
+  S3JniDb_mutex_leave;
+}
+#define S3JniDb_set_aside(JNIDB) S3JniDb__set_aside(env, JNIDB)
+
+/*
+** Uncache any state for the given JNIEnv, clearing all Java
+** references the cache owns. Returns true if env was cached and false
+** if it was not found in the cache. Ownership of the S3JniEnv object
+** associated with the given argument is transferred to this function,
+** which makes it free for re-use.
+**
+** Requires that the env mutex be locked.
+*/
+static int S3JniEnv_uncache(JNIEnv * const env){
+  struct S3JniEnv * row;
+  struct S3JniEnv * pPrev = 0;
+
+  S3JniEnv_mutex_assertLocked;
+  row = SJG.envCache.aHead;
+  for( ; row; pPrev = row, row = row->pNext ){
+    if( row->env == env ){
+      break;
+    }
+  }
+  if( !row ){
+    return 0;
+  }
+  if( pPrev) pPrev->pNext = row->pNext;
+  else{
+    assert( SJG.envCache.aHead == row );
+    SJG.envCache.aHead = row->pNext;
+  }
+  memset(row, 0, sizeof(S3JniEnv));
+  row->pNext = SJG.envCache.aFree;
+  SJG.envCache.aFree = row;
+  return 1;
+}
+
+/*
+** Fetches the given nph-ref from cache the cache and returns the
+** object with its klazz member set. This is an O(1) operation except
+** on the first call for a given pRef, during which pRef->klazz and
+** pRef->pRef are initialized thread-safely. In the latter case it's
+** still effectively O(1), but with a much longer 1.
+**
+** It is up to the caller to populate the other members of the
+** returned object if needed, taking care to lock the modification
+** with S3JniNph_mutex_enter/leave.
+**
+** This simple cache catches >99% of searches in the current
+** (2023-07-31) tests.
+*/
+static S3JniNphOp * s3jni__nphop(JNIEnv * const env, S3JniNphOp const* pRef){
+  S3JniNphOp * const pNC = &SJG.nph.list[pRef->index];
+
+  assert( (void*)pRef>=(void*)&S3JniNphOps && (void*)pRef<(void*)(&S3JniNphOps + 1)
+          && "pRef is out of range" );
+  assert( pRef->index>=0
+          && (pRef->index < (sizeof(S3JniNphOps) / sizeof(S3JniNphOp)))
+          && "pRef->index is out of range" );
+  if( !pNC->klazz ){
+    S3JniNph_mutex_enter;
+    if( !pNC->klazz ){
+      jclass const klazz = (*env)->FindClass(env, pRef->zName);
+      //printf("FindClass %s\n", pRef->zName);
+      S3JniExceptionIsFatal("FindClass() unexpectedly threw");
+      pNC->klazz = S3JniRefGlobal(klazz);
+    }
+    S3JniNph_mutex_leave;
+  }
+  assert( pNC->klazz );
+  return pNC;
+}
+
+#define s3jni_nphop(PRef) s3jni__nphop(env, PRef)
+
+/*
+** Common code for accessor functions for NativePointerHolder and
+** OutputPointer types. pRef must be a pointer from S3JniNphOps. jOut
+** must be an instance of that class (Java's type safety takes care of
+** that requirement). If necessary, this fetches the jfieldID for
+** jOut's pRef->zMember, which must be of the type represented by the
+** JNI type signature pRef->zTypeSig, and stores it in
+** S3JniGlobal.nph.list[pRef->index].  Fails fatally if the pRef->zMember
+** property is not found, as that presents a serious internal misuse.
+**
+** Property lookups are cached on a per-pRef basis.
+*/
+static jfieldID s3jni_nphop_field(JNIEnv * const env, S3JniNphOp const* pRef){
+  S3JniNphOp * const pNC = s3jni_nphop(pRef);
+
+  if( !pNC->fidValue ){
+    S3JniNph_mutex_enter;
+    if( !pNC->fidValue ){
+      pNC->fidValue = (*env)->GetFieldID(env, pNC->klazz,
+                                         pRef->zMember, pRef->zTypeSig);
+      S3JniExceptionIsFatal("Code maintenance required: missing "
+                            "required S3JniNphOp::fidValue.");
+    }
+    S3JniNph_mutex_leave;
+  }
+  assert( pNC->fidValue );
+  return pNC->fidValue;
+}
+
+/*
+** Sets a native ptr value in NativePointerHolder object jNph,
+** which must be of the native type described by pRef. jNph
+** may not be NULL.
+*/
+static void NativePointerHolder__set(JNIEnv * const env, S3JniNphOp const* pRef,
+                                     jobject jNph, const void * p){
+  assert( jNph );
+  (*env)->SetLongField(env, jNph, s3jni_nphop_field(env, pRef),
+                       S3JniCast_P2L(p));
+  S3JniExceptionIsFatal("Could not set NativePointerHolder.nativePointer.");
+}
+
+#define NativePointerHolder_set(PREF,JNPH,P) \
+  NativePointerHolder__set(env, PREF, JNPH, P)
+
+/*
+** Fetches a native ptr value from NativePointerHolder object jNph,
+** which must be of the native type described by pRef.  This is a
+** no-op if jNph is NULL.
+*/
+static void * NativePointerHolder__get(JNIEnv * env, jobject jNph,
+                                       S3JniNphOp const* pRef){
+  void * rv = 0;
+  if( jNph ){
+    rv = S3JniCast_L2P(
+      (*env)->GetLongField(env, jNph, s3jni_nphop_field(env, pRef))
+    );
+    S3JniExceptionIsFatal("Cannot fetch NativePointerHolder.nativePointer.");
+  }
+  return rv;
+}
+
+#define NativePointerHolder_get(JOBJ,NPHREF) \
+  NativePointerHolder__get(env, (JOBJ), (NPHREF))
+
+/*
+** Helpers for extracting pointers from jobjects, noting that we rely
+** on the corresponding Java interfaces having already done the
+** type-checking. OBJ must be a jobject referring to a
+** NativePointerHolder, where T matches PtrGet_T. Don't use these
+** in contexts where that's not the case. Note that these aren't
+** type-safe in the strictest sense:
+**
+**   sqlite3 * s = PtrGet_sqlite3_stmt(...)
+**
+** will work, despite the incorrect macro name, so long as the
+** argument is a Java sqlite3 object, as this operation only has void
+** pointers to work with.
+*/
+#define PtrGet_T(T,OBJ) (T*)NativePointerHolder_get(OBJ, S3JniNph(T))
+#define PtrGet_sqlite3(OBJ) PtrGet_T(sqlite3, OBJ)
+#define PtrGet_sqlite3_backup(OBJ) PtrGet_T(sqlite3_backup, OBJ)
+#define PtrGet_sqlite3_blob(OBJ) PtrGet_T(sqlite3_blob, OBJ)
+#define PtrGet_sqlite3_context(OBJ) PtrGet_T(sqlite3_context, OBJ)
+#define PtrGet_sqlite3_stmt(OBJ) PtrGet_T(sqlite3_stmt, OBJ)
+#define PtrGet_sqlite3_value(OBJ) PtrGet_T(sqlite3_value, OBJ)
+/*
+** S3JniLongPtr_T(X,Y) expects X to be an unqualified sqlite3 struct
+** type name and Y to be a native pointer to such an object in the
+** form of a jlong value. The jlong is simply cast to (X*). This
+** approach is, as of 2023-09-27, supplanting the former approach. We
+** now do the native pointer extraction in the Java side, rather than
+** the C side, because it's reportedly significantly faster. The
+** intptr_t part here is necessary for compatibility with (at least)
+** ARM32.
+*/
+#define S3JniLongPtr_T(T,JLongAsPtr) (T*)((intptr_t)(JLongAsPtr))
+#define S3JniLongPtr_sqlite3(JLongAsPtr) S3JniLongPtr_T(sqlite3,JLongAsPtr)
+#define S3JniLongPtr_sqlite3_backup(JLongAsPtr) S3JniLongPtr_T(sqlite3_backup,JLongAsPtr)
+#define S3JniLongPtr_sqlite3_blob(JLongAsPtr) S3JniLongPtr_T(sqlite3_blob,JLongAsPtr)
+#define S3JniLongPtr_sqlite3_stmt(JLongAsPtr) S3JniLongPtr_T(sqlite3_stmt,JLongAsPtr)
+#define S3JniLongPtr_sqlite3_value(JLongAsPtr) S3JniLongPtr_T(sqlite3_value,JLongAsPtr)
+/*
+** Extracts the new S3JniDb instance from the free-list, or allocates
+** one if needed, associates it with pDb, and returns.  Returns NULL
+** on OOM. The returned object MUST, on success of the calling
+** operation, subsequently be associated with jDb via
+** NativePointerHolder_set() or freed using S3JniDb_set_aside().
+*/
+static S3JniDb * S3JniDb_alloc(JNIEnv * const env, jobject jDb){
+  S3JniDb * rv = 0;
+  S3JniDb_mutex_enter;
+  if( SJG.perDb.aFree ){
+    rv = SJG.perDb.aFree;
+    SJG.perDb.aFree = rv->pNext;
+    rv->pNext = 0;
+    s3jni_incr( &SJG.metrics.nPdbRecycled );
+  }
+  S3JniDb_mutex_leave;
+  if( 0==rv ){
+    rv = s3jni_malloc(sizeof(S3JniDb));
+    if( rv ){
+      s3jni_incr( &SJG.metrics.nPdbAlloc );
+    }
+  }
+  if( rv ){
+    memset(rv, 0, sizeof(S3JniDb));
+    rv->jDb = S3JniRefGlobal(jDb);
+  }
+  return rv;
+}
+
+/*
+** Returns the S3JniDb object for the given org.sqlite.jni.capi.sqlite3
+** object, or NULL if jDb is NULL, no pointer can be extracted
+** from it, or no matching entry can be found.
+*/
+static S3JniDb * S3JniDb__from_java(JNIEnv * const env, jobject jDb){
+  sqlite3 * const pDb = jDb ? PtrGet_sqlite3(jDb) : 0;
+  return pDb ? S3JniDb_from_clientdata(pDb) : 0;
+}
+#define S3JniDb_from_java(jObject) S3JniDb__from_java(env,(jObject))
+
+/*
+** S3JniDb finalizer for use with sqlite3_set_clientdata().
+*/
+static void S3JniDb_xDestroy(void *p){
+  S3JniDeclLocal_env;
+  S3JniDb * const ps = p;
+  assert( !ps->pNext && "Else ps is already in the free-list.");
+  S3JniDb_set_aside(ps);
+}
+
+/*
+** Evaluates to the S3JniDb object for the given sqlite3 object, or
+** NULL if pDb is NULL or was not initialized via the JNI interfaces.
+*/
+#define S3JniDb_from_c(sqlite3Ptr) \
+  ((sqlite3Ptr) ? S3JniDb_from_clientdata(sqlite3Ptr) : 0)
+#define S3JniDb_from_jlong(sqlite3PtrAsLong) \
+  S3JniDb_from_c(S3JniLongPtr_T(sqlite3,sqlite3PtrAsLong))
+
+/*
+** Unref any Java-side state in (S3JniAutoExtension*) AX and zero out
+** AX.
+*/
+#define S3JniAutoExtension_clear(AX) S3JniHook_unref(AX);
+
+/*
+** Initializes a pre-allocated S3JniAutoExtension object.  Returns
+** non-0 if there is an error collecting the required state from
+** jAutoExt (which must be an AutoExtensionCallback object). On error,
+** it passes ax to S3JniAutoExtension_clear().
+*/
+static int S3JniAutoExtension_init(JNIEnv *const env,
+                                   S3JniAutoExtension * const ax,
+                                   jobject const jAutoExt){
+  jclass const klazz = (*env)->GetObjectClass(env, jAutoExt);
+
+  S3JniAutoExt_mutex_assertLocker;
+  *ax = S3JniHook_empty;
+  ax->midCallback = (*env)->GetMethodID(env, klazz, "call",
+                                        "(Lorg/sqlite/jni/capi/sqlite3;)I");
+  S3JniUnrefLocal(klazz);
+  S3JniExceptionWarnIgnore;
+  if( !ax->midCallback ){
+    S3JniAutoExtension_clear(ax);
+    return SQLITE_ERROR;
+  }
+  ax->jObj = S3JniRefGlobal(jAutoExt);
+  return 0;
+}
+
+/*
+** Sets the value property of the OutputPointer.Bool jOut object to
+** v.
+*/
+static void OutputPointer_set_Bool(JNIEnv * const env, jobject const jOut,
+                                    int v){
+  (*env)->SetBooleanField(env, jOut, s3jni_nphop_field(
+                            env, S3JniNph(OutputPointer_Bool)
+                          ), v ? JNI_TRUE : JNI_FALSE );
+  S3JniExceptionIsFatal("Cannot set OutputPointer.Bool.value");
+}
+
+/*
+** Sets the value property of the OutputPointer.Int32 jOut object to
+** v.
+*/
+static void OutputPointer_set_Int32(JNIEnv * const env, jobject const jOut,
+                                    int v){
+  (*env)->SetIntField(env, jOut, s3jni_nphop_field(
+                        env, S3JniNph(OutputPointer_Int32)
+                      ), (jint)v);
+  S3JniExceptionIsFatal("Cannot set OutputPointer.Int32.value");
+}
+
+/*
+** Sets the value property of the OutputPointer.Int64 jOut object to
+** v.
+*/
+static void OutputPointer_set_Int64(JNIEnv * const env, jobject const jOut,
+                                    jlong v){
+  (*env)->SetLongField(env, jOut, s3jni_nphop_field(
+                         env, S3JniNph(OutputPointer_Int64)
+                       ), v);
+  S3JniExceptionIsFatal("Cannot set OutputPointer.Int64.value");
+}
+
+/*
+** Internal helper for OutputPointer_set_TYPE() where TYPE is an
+** Object type.
+*/
+static void OutputPointer_set_obj(JNIEnv * const env,
+                                  S3JniNphOp const * const pRef,
+                                  jobject const jOut,
+                                  jobject v){
+  (*env)->SetObjectField(env, jOut, s3jni_nphop_field(env, pRef), v);
+  S3JniExceptionIsFatal("Cannot set OutputPointer.T.value");
+}
+
+#ifdef SQLITE_ENABLE_FTS5
+#if 0
+/*
+** Sets the value property of the OutputPointer.ByteArray jOut object
+** to v.
+*/
+static void OutputPointer_set_ByteArray(JNIEnv * const env, jobject const jOut,
+                                        jbyteArray const v){
+  OutputPointer_set_obj(env, S3JniNph(OutputPointer_ByteArray), jOut, v);
+}
+#endif
+#endif /* SQLITE_ENABLE_FTS5 */
+
+/*
+** Sets the value property of the OutputPointer.String jOut object to
+** v.
+*/
+static void OutputPointer_set_String(JNIEnv * const env, jobject const jOut,
+                                     jstring const v){
+  OutputPointer_set_obj(env, S3JniNph(OutputPointer_String), jOut, v);
+}
+
+/*
+** Returns true if eTextRep is a valid sqlite3 encoding constant, else
+** returns false.
+*/
+static int encodingTypeIsValid(int eTextRep){
+  switch( eTextRep ){
+    case SQLITE_UTF8: case SQLITE_UTF16:
+    case SQLITE_UTF16LE: case SQLITE_UTF16BE:
+      return 1;
+    default:
+      return 0;
+  }
+}
+
+/* For use with sqlite3_result/value_pointer() */
+static const char * const ResultJavaValuePtrStr = "org.sqlite.jni.capi.ResultJavaVal";
+
+/*
+** If v is not NULL, it must be a jobject global reference. Its
+** reference is relinquished.
+*/
+static void S3Jni_jobject_finalizer(void *v){
+  if( v ){
+    S3JniDeclLocal_env;
+    S3JniUnrefGlobal((jobject)v);
+  }
+}
+
+/*
+** Returns a new Java instance of the class referred to by pRef, which
+** MUST be interface-compatible with NativePointerHolder and MUST have
+** a no-arg constructor. The NativePointerHolder_set() method is
+** passed the new Java object (which must not be NULL) and pNative
+** (which may be NULL). Hypothetically returns NULL if Java fails to
+** allocate, but the JNI docs are not entirely clear on that detail.
+**
+** Always use a static pointer from the S3JniNphOps struct for the
+** 2nd argument.
+*/
+static jobject NativePointerHolder_new(JNIEnv * const env,
+                                       S3JniNphOp const * pRef,
+                                       const void * pNative){
+  jobject rv = 0;
+  S3JniNphOp * const pNC = s3jni_nphop(pRef);
+  if( !pNC->midCtor ){
+    S3JniNph_mutex_enter;
+    if( !pNC->midCtor ){
+      pNC->midCtor = (*env)->GetMethodID(env, pNC->klazz, "", "()V");
+      S3JniExceptionIsFatal("Cannot find constructor for class.");
+    }
+    S3JniNph_mutex_leave;
+  }
+  rv = (*env)->NewObject(env, pNC->klazz, pNC->midCtor);
+  S3JniExceptionIsFatal("No-arg constructor threw.");
+  s3jni_oom_check(rv);
+  if( rv ) NativePointerHolder_set(pRef, rv, pNative);
+  return rv;
+}
+
+static inline jobject new_java_sqlite3(JNIEnv * const env, sqlite3 *sv){
+  return NativePointerHolder_new(env, S3JniNph(sqlite3), sv);
+}
+static inline jobject new_java_sqlite3_backup(JNIEnv * const env, sqlite3_backup *sv){
+  return NativePointerHolder_new(env, S3JniNph(sqlite3_backup), sv);
+}
+static inline jobject new_java_sqlite3_blob(JNIEnv * const env, sqlite3_blob *sv){
+  return NativePointerHolder_new(env, S3JniNph(sqlite3_blob), sv);
+}
+static inline jobject new_java_sqlite3_context(JNIEnv * const env, sqlite3_context *sv){
+  return NativePointerHolder_new(env, S3JniNph(sqlite3_context), sv);
+}
+static inline jobject new_java_sqlite3_stmt(JNIEnv * const env, sqlite3_stmt *sv){
+  return NativePointerHolder_new(env, S3JniNph(sqlite3_stmt), sv);
+}
+static inline jobject new_java_sqlite3_value(JNIEnv * const env, sqlite3_value *sv){
+  return NativePointerHolder_new(env, S3JniNph(sqlite3_value), sv);
+}
+
+/* Helper typedefs for UDF callback types. */
+typedef void (*udf_xFunc_f)(sqlite3_context*,int,sqlite3_value**);
+typedef void (*udf_xStep_f)(sqlite3_context*,int,sqlite3_value**);
+typedef void (*udf_xFinal_f)(sqlite3_context*);
+/*typedef void (*udf_xValue_f)(sqlite3_context*);*/
+/*typedef void (*udf_xInverse_f)(sqlite3_context*,int,sqlite3_value**);*/
+
+/*
+** Allocate a new S3JniUdf (User-defined Function) and associate it
+** with the SQLFunction-type jObj. Returns NULL on OOM. If the
+** returned object's type==UDF_UNKNOWN_TYPE then the type of UDF was
+** not unambiguously detected based on which callback members it has,
+** which falls into the category of user error.
+**
+** The caller must arrange for the returned object to eventually be
+** passed to S3JniUdf_free().
+*/
+static S3JniUdf * S3JniUdf_alloc(JNIEnv * const env, jobject jObj){
+  S3JniUdf * s = 0;
+
+  S3JniGlobal_mutex_enter;
+  s3jni_incr(&SJG.metrics.nMutexUdf);
+  if( SJG.udf.aFree ){
+    s = SJG.udf.aFree;
+    SJG.udf.aFree = s->pNext;
+    s->pNext = 0;
+    s3jni_incr(&SJG.metrics.nUdfRecycled);
+  }
+  S3JniGlobal_mutex_leave;
+  if( !s ){
+    s = s3jni_malloc( sizeof(*s));
+    s3jni_incr(&SJG.metrics.nUdfAlloc);
+  }
+  if( s ){
+    const char * zFSI = /* signature for xFunc, xStep, xInverse */
+      "(Lorg/sqlite/jni/capi/sqlite3_context;[Lorg/sqlite/jni/capi/sqlite3_value;)V";
+    const char * zFV = /* signature for xFinal, xValue */
+      "(Lorg/sqlite/jni/capi/sqlite3_context;)V";
+    jclass const klazz = (*env)->GetObjectClass(env, jObj);
+
+    memset(s, 0, sizeof(*s));
+    s->jObj = S3JniRefGlobal(jObj);
+
+#define FGET(FuncName,FuncSig,Field)                               \
+    s->Field = (*env)->GetMethodID(env, klazz, FuncName, FuncSig); \
+    if( !s->Field ) (*env)->ExceptionClear(env)
+
+    FGET("xFunc",    zFSI, jmidxFunc);
+    FGET("xStep",    zFSI, jmidxStep);
+    FGET("xFinal",   zFV,  jmidxFinal);
+    FGET("xValue",   zFV,  jmidxValue);
+    FGET("xInverse", zFSI, jmidxInverse);
+#undef FGET
+
+    S3JniUnrefLocal(klazz);
+    if( s->jmidxFunc ) s->type = UDF_SCALAR;
+    else if( s->jmidxStep && s->jmidxFinal ){
+      s->type = (s->jmidxValue && s->jmidxInverse)
+        ? UDF_WINDOW : UDF_AGGREGATE;
+    }else{
+      s->type = UDF_UNKNOWN_TYPE;
+    }
+  }
+  return s;
+}
+
+/*
+** Frees up all resources owned by s, clears its state, then either
+** caches it for reuse (if cacheIt is true) or frees it. The former
+** requires locking the global mutex, so it must not be held when this
+** is called.
+*/
+static void S3JniUdf_free(JNIEnv * const env, S3JniUdf * const s,
+                          int cacheIt){
+  assert( !s->pNext );
+  if( s->jObj ){
+    s3jni_call_xDestroy(s->jObj);
+    S3JniUnrefGlobal(s->jObj);
+    sqlite3_free(s->zFuncName);
+    assert( !s->pNext );
+    memset(s, 0, sizeof(*s));
+  }
+  if( cacheIt ){
+    S3JniGlobal_mutex_enter;
+    s->pNext = S3JniGlobal.udf.aFree;
+    S3JniGlobal.udf.aFree = s;
+    S3JniGlobal_mutex_leave;
+  }else{
+    sqlite3_free( s );
+  }
+}
+
+/* Finalizer for sqlite3_create_function() and friends. */
+static void S3JniUdf_finalizer(void * s){
+  S3JniUdf_free(s3jni_env(), (S3JniUdf*)s, 1);
+}
+
+/*
+** Helper for processing args to UDF handlers with signature
+** (sqlite3_context*,int,sqlite3_value**).
+*/
+typedef struct {
+  jobject jcx         /* sqlite3_context */;
+  jobjectArray jargv  /* sqlite3_value[] */;
+} udf_jargs;
+
+/*
+** Converts the given (cx, argc, argv) into arguments for the given
+** UDF, writing the result (Java wrappers for cx and argv) in the
+** final 2 arguments. Returns 0 on success, SQLITE_NOMEM on allocation
+** error. On error *jCx and *jArgv will be set to 0. The output
+** objects are of type org.sqlite.jni.capi.sqlite3_context and
+** array-of-org.sqlite.jni.capi.sqlite3_value, respectively.
+*/
+static int udf_args(JNIEnv *env,
+                    sqlite3_context * const cx,
+                    int argc, sqlite3_value**argv,
+                    jobject * jCx, jobjectArray *jArgv){
+  jobjectArray ja = 0;
+  jobject jcx = new_java_sqlite3_context(env, cx);
+  jint i;
+  *jCx = 0;
+  *jArgv = 0;
+  if( !jcx ) goto error_oom;
+  ja = (*env)->NewObjectArray(
+    env, argc, s3jni_nphop(S3JniNph(sqlite3_value))->klazz,
+    NULL);
+  s3jni_oom_check( ja );
+  if( !ja ) goto error_oom;
+  for(i = 0; i < argc; ++i){
+    jobject jsv = new_java_sqlite3_value(env, argv[i]);
+    if( !jsv ) goto error_oom;
+    (*env)->SetObjectArrayElement(env, ja, i, jsv);
+    S3JniUnrefLocal(jsv)/*ja has a ref*/;
+  }
+  *jCx = jcx;
+  *jArgv = ja;
+  return 0;
+error_oom:
+  S3JniUnrefLocal(jcx);
+  S3JniUnrefLocal(ja);
+  return SQLITE_NOMEM;
+}
+
+/*
+** Requires that jCx and jArgv are sqlite3_context
+** resp. array-of-sqlite3_value values initialized by udf_args(). This
+** function zeroes out the nativePointer member of jCx and each entry
+** in jArgv. This is a safety-net precaution to avoid undefined
+** behavior if a Java-side UDF holds a reference to one of its
+** arguments. This MUST be called from any function which successfully
+** calls udf_args(), after calling the corresponding UDF and checking
+** its exception status. It MUST NOT be called in any other case.
+*/
+static void udf_unargs(JNIEnv *env, jobject jCx, int argc, jobjectArray jArgv){
+  int i = 0;
+  assert(jCx);
+  NativePointerHolder_set(S3JniNph(sqlite3_context), jCx, 0);
+  for( ; i < argc; ++i ){
+    jobject jsv = (*env)->GetObjectArrayElement(env, jArgv, i);
+    assert(jsv);
+    NativePointerHolder_set(S3JniNph(sqlite3_value), jsv, 0);
+  }
+}
+
+
+/*
+** Must be called immediately after a Java-side UDF callback throws.
+** If translateToErr is true then it sets the exception's message in
+** the result error using sqlite3_result_error(). If translateToErr is
+** false then it emits a warning that the function threw but should
+** not do so. In either case, it clears the exception state.
+**
+** Returns SQLITE_NOMEM if an allocation fails, else SQLITE_ERROR. In
+** the former case it calls sqlite3_result_error_nomem().
+*/
+static int udf_report_exception(JNIEnv * const env, int translateToErr,
+                                sqlite3_context * cx,
+                                const char *zFuncName, const char *zFuncType ){
+  jthrowable const ex = (*env)->ExceptionOccurred(env);
+  int rc = SQLITE_ERROR;
+
+  assert(ex && "This must only be called when a Java exception is pending.");
+  if( translateToErr ){
+    char * zMsg;
+    char * z;
+
+    S3JniExceptionClear;
+    zMsg = s3jni_exception_error_msg(env, ex);
+    z = sqlite3_mprintf("Client-defined SQL function %s.%s() threw: %s",
+                        zFuncName ? zFuncName : "", zFuncType,
+                        zMsg ? zMsg : "Unknown exception" );
+    sqlite3_free(zMsg);
+    if( z ){
+      sqlite3_result_error(cx, z, -1);
+      sqlite3_free(z);
+    }else{
+      sqlite3_result_error_nomem(cx);
+      rc = SQLITE_NOMEM;
+    }
+  }else{
+    S3JniExceptionWarnCallbackThrew("client-defined SQL function");
+    S3JniExceptionClear;
+  }
+  S3JniUnrefLocal(ex);
+  return rc;
+}
+
+/*
+** Sets up the state for calling a Java-side xFunc/xStep/xInverse()
+** UDF, calls it, and returns 0 on success.
+*/
+static int udf_xFSI(sqlite3_context* const pCx, int argc,
+                    sqlite3_value** const argv, S3JniUdf * const s,
+                    jmethodID xMethodID, const char * const zFuncType){
+  S3JniDeclLocal_env;
+  udf_jargs args = {0,0};
+  int rc = udf_args(env, pCx, argc, argv, &args.jcx, &args.jargv);
+
+  if( 0 == rc ){
+    (*env)->CallVoidMethod(env, s->jObj, xMethodID, args.jcx, args.jargv);
+    S3JniIfThrew{
+      rc = udf_report_exception(env, 'F'==zFuncType[1]/*xFunc*/, pCx,
+                                s->zFuncName, zFuncType);
+    }
+    udf_unargs(env, args.jcx, argc, args.jargv);
+  }
+  S3JniUnrefLocal(args.jcx);
+  S3JniUnrefLocal(args.jargv);
+  return rc;
+}
+
+/*
+** Sets up the state for calling a Java-side xFinal/xValue() UDF,
+** calls it, and returns 0 on success.
+*/
+static int udf_xFV(sqlite3_context* cx, S3JniUdf * s,
+                   jmethodID xMethodID,
+                   const char *zFuncType){
+  S3JniDeclLocal_env;
+  jobject jcx = new_java_sqlite3_context(env, cx);
+  int rc = 0;
+  int const isFinal = 'F'==zFuncType[1]/*xFinal*/;
+
+  if( jcx ){
+    (*env)->CallVoidMethod(env, s->jObj, xMethodID, jcx);
+    S3JniIfThrew{
+      rc = udf_report_exception(env, isFinal, cx, s->zFuncName,
+                                zFuncType);
+    }
+    S3JniUnrefLocal(jcx);
+  }else{
+    if( isFinal ) sqlite3_result_error_nomem(cx);
+    rc = SQLITE_NOMEM;
+  }
+  return rc;
+}
+
+/* Proxy for C-to-Java xFunc. */
+static void udf_xFunc(sqlite3_context* cx, int argc,
+                      sqlite3_value** argv){
+  S3JniUdf * const s = (S3JniUdf*)sqlite3_user_data(cx);
+  s3jni_incr( &SJG.metrics.udf.nFunc );
+  udf_xFSI(cx, argc, argv, s, s->jmidxFunc, "xFunc");
+}
+/* Proxy for C-to-Java xStep. */
+static void udf_xStep(sqlite3_context* cx, int argc,
+                      sqlite3_value** argv){
+  S3JniUdf * const s = (S3JniUdf*)sqlite3_user_data(cx);
+  s3jni_incr( &SJG.metrics.udf.nStep );
+  udf_xFSI(cx, argc, argv, s, s->jmidxStep, "xStep");
+}
+/* Proxy for C-to-Java xFinal. */
+static void udf_xFinal(sqlite3_context* cx){
+  S3JniUdf * const s = (S3JniUdf*)sqlite3_user_data(cx);
+  s3jni_incr( &SJG.metrics.udf.nFinal );
+  udf_xFV(cx, s, s->jmidxFinal, "xFinal");
+}
+/* Proxy for C-to-Java xValue. */
+static void udf_xValue(sqlite3_context* cx){
+  S3JniUdf * const s = (S3JniUdf*)sqlite3_user_data(cx);
+  s3jni_incr( &SJG.metrics.udf.nValue );
+  udf_xFV(cx, s, s->jmidxValue, "xValue");
+}
+/* Proxy for C-to-Java xInverse. */
+static void udf_xInverse(sqlite3_context* cx, int argc,
+                         sqlite3_value** argv){
+  S3JniUdf * const s = (S3JniUdf*)sqlite3_user_data(cx);
+  s3jni_incr( &SJG.metrics.udf.nInverse );
+  udf_xFSI(cx, argc, argv, s, s->jmidxInverse, "xInverse");
+}
+
+
+////////////////////////////////////////////////////////////////////////
+// What follows is the JNI/C bindings. They are in alphabetical order
+// except for this macro-generated subset which are kept together
+// (alphabetized) here at the front...
+////////////////////////////////////////////////////////////////////////
+
+/** Create a trivial JNI wrapper for (int CName(void)). */
+#define WRAP_INT_VOID(JniNameSuffix,CName)      \
+  JniDecl(jint,JniNameSuffix)(JniArgsEnvClass){ \
+    return (jint)CName();                       \
+  }
+/** Create a trivial JNI wrapper for (int CName(int)). */
+#define WRAP_INT_INT(JniNameSuffix,CName)                 \
+  JniDecl(jint,JniNameSuffix)(JniArgsEnvClass, jint arg){ \
+    return (jint)CName((int)arg);                         \
+  }
+/*
+** Create a trivial JNI wrapper for (const mutf8_string *
+** CName(void)). This is only valid for functions which are known to
+** return ASCII or text which is equivalent in UTF-8 and MUTF-8.
+*/
+#define WRAP_MUTF8_VOID(JniNameSuffix,CName)                   \
+  JniDecl(jstring,JniNameSuffix)(JniArgsEnvClass){             \
+    jstring const rv = (*env)->NewStringUTF( env, CName() );   \
+    s3jni_oom_check(rv);                                       \
+    return rv;                                                 \
+  }
+/** Create a trivial JNI wrapper for (int CName(sqlite3_stmt*)). */
+#define WRAP_INT_STMT(JniNameSuffix,CName)                    \
+  JniDecl(jint,JniNameSuffix)(JniArgsEnvClass, jlong jpStmt){ \
+    return (jint)CName(S3JniLongPtr_sqlite3_stmt(jpStmt));    \
+  }
+/** Create a trivial JNI wrapper for (int CName(sqlite3_stmt*,int)). */
+#define WRAP_INT_STMT_INT(JniNameSuffix,CName)                         \
+  JniDecl(jint,JniNameSuffix)(JniArgsEnvClass, jlong jpStmt, jint n){ \
+    return (jint)CName(S3JniLongPtr_sqlite3_stmt(jpStmt), (int)n);            \
+  }
+/** Create a trivial JNI wrapper for (boolean CName(sqlite3_stmt*)). */
+#define WRAP_BOOL_STMT(JniNameSuffix,CName)                           \
+  JniDecl(jboolean,JniNameSuffix)(JniArgsEnvClass, jobject jStmt){     \
+    return CName(PtrGet_sqlite3_stmt(jStmt)) ? JNI_TRUE : JNI_FALSE; \
+  }
+/** Create a trivial JNI wrapper for (jstring CName(sqlite3_stmt*,int)). */
+#define WRAP_STR_STMT_INT(JniNameSuffix,CName)                             \
+  JniDecl(jstring,JniNameSuffix)(JniArgsEnvClass, jlong jpStmt, jint ndx){ \
+    return s3jni_utf8_to_jstring(                                       \
+      CName(S3JniLongPtr_sqlite3_stmt(jpStmt), (int)ndx),               \
+      -1);                                                              \
+  }
+/** Create a trivial JNI wrapper for (boolean CName(sqlite3*)). */
+#define WRAP_BOOL_DB(JniNameSuffix,CName)                           \
+  JniDecl(jboolean,JniNameSuffix)(JniArgsEnvClass, jlong jpDb){     \
+    return CName(S3JniLongPtr_sqlite3(jpDb)) ? JNI_TRUE : JNI_FALSE; \
+  }
+/** Create a trivial JNI wrapper for (int CName(sqlite3*)). */
+#define WRAP_INT_DB(JniNameSuffix,CName)                    \
+  JniDecl(jint,JniNameSuffix)(JniArgsEnvClass, jlong jpDb){ \
+  return (jint)CName(S3JniLongPtr_sqlite3(jpDb)); \
+  }
+/** Create a trivial JNI wrapper for (int64 CName(sqlite3*)). */
+#define WRAP_INT64_DB(JniNameSuffix,CName)                   \
+  JniDecl(jlong,JniNameSuffix)(JniArgsEnvClass, jlong jpDb){ \
+  return (jlong)CName(S3JniLongPtr_sqlite3(jpDb));  \
+  }
+/** Create a trivial JNI wrapper for (jstring CName(sqlite3*,int)). */
+#define WRAP_STR_DB_INT(JniNameSuffix,CName)                             \
+  JniDecl(jstring,JniNameSuffix)(JniArgsEnvClass, jlong jpDb, jint ndx){ \
+    return s3jni_utf8_to_jstring(                                       \
+      CName(S3JniLongPtr_sqlite3(jpDb), (int)ndx),               \
+      -1);                                                              \
+  }
+/** Create a trivial JNI wrapper for (int CName(sqlite3_value*)). */
+#define WRAP_INT_SVALUE(JniNameSuffix,CName,DfltOnNull)         \
+  JniDecl(jint,JniNameSuffix)(JniArgsEnvClass, jlong jpSValue){ \
+    sqlite3_value * const sv = S3JniLongPtr_sqlite3_value(jpSValue); \
+    return (jint)(sv ? CName(sv): DfltOnNull);                      \
+  }
+/** Create a trivial JNI wrapper for (boolean CName(sqlite3_value*)). */
+#define WRAP_BOOL_SVALUE(JniNameSuffix,CName,DfltOnNull)            \
+  JniDecl(jboolean,JniNameSuffix)(JniArgsEnvClass, jlong jpSValue){ \
+    sqlite3_value * const sv = S3JniLongPtr_sqlite3_value(jpSValue); \
+    return (jint)(sv ? CName(sv) : DfltOnNull)                       \
+      ? JNI_TRUE : JNI_FALSE;                                       \
+  }
+
+WRAP_INT_DB(1changes,                  sqlite3_changes)
+WRAP_INT64_DB(1changes64,              sqlite3_changes64)
+WRAP_INT_STMT(1clear_1bindings,        sqlite3_clear_bindings)
+WRAP_INT_STMT_INT(1column_1bytes,      sqlite3_column_bytes)
+WRAP_INT_STMT_INT(1column_1bytes16,    sqlite3_column_bytes16)
+WRAP_INT_STMT(1column_1count,          sqlite3_column_count)
+WRAP_STR_STMT_INT(1column_1decltype,   sqlite3_column_decltype)
+WRAP_STR_STMT_INT(1column_1name,       sqlite3_column_name)
+WRAP_STR_STMT_INT(1column_1database_1name,  sqlite3_column_database_name)
+WRAP_STR_STMT_INT(1column_1origin_1name,    sqlite3_column_origin_name)
+WRAP_STR_STMT_INT(1column_1table_1name,     sqlite3_column_table_name)
+WRAP_INT_STMT_INT(1column_1type,       sqlite3_column_type)
+WRAP_INT_STMT(1data_1count,            sqlite3_data_count)
+WRAP_STR_DB_INT(1db_1name,             sqlite3_db_name)
+WRAP_INT_DB(1error_1offset,            sqlite3_error_offset)
+WRAP_INT_DB(1extended_1errcode,        sqlite3_extended_errcode)
+WRAP_BOOL_DB(1get_1autocommit,         sqlite3_get_autocommit)
+WRAP_MUTF8_VOID(1libversion,           sqlite3_libversion)
+WRAP_INT_VOID(1libversion_1number,     sqlite3_libversion_number)
+WRAP_INT_VOID(1keyword_1count,         sqlite3_keyword_count)
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+WRAP_INT_DB(1preupdate_1blobwrite,     sqlite3_preupdate_blobwrite)
+WRAP_INT_DB(1preupdate_1count,         sqlite3_preupdate_count)
+WRAP_INT_DB(1preupdate_1depth,         sqlite3_preupdate_depth)
+#endif
+WRAP_INT_INT(1release_1memory,         sqlite3_release_memory)
+WRAP_INT_INT(1sleep,                   sqlite3_sleep)
+WRAP_MUTF8_VOID(1sourceid,             sqlite3_sourceid)
+WRAP_BOOL_STMT(1stmt_1busy,            sqlite3_stmt_busy)
+WRAP_INT_STMT_INT(1stmt_1explain,      sqlite3_stmt_explain)
+WRAP_INT_STMT(1stmt_1isexplain,        sqlite3_stmt_isexplain)
+WRAP_BOOL_STMT(1stmt_1readonly,        sqlite3_stmt_readonly)
+WRAP_INT_DB(1system_1errno,            sqlite3_system_errno)
+WRAP_INT_VOID(1threadsafe,             sqlite3_threadsafe)
+WRAP_INT_DB(1total_1changes,           sqlite3_total_changes)
+WRAP_INT64_DB(1total_1changes64,       sqlite3_total_changes64)
+WRAP_INT_SVALUE(1value_1encoding,      sqlite3_value_encoding,SQLITE_UTF8)
+WRAP_BOOL_SVALUE(1value_1frombind,     sqlite3_value_frombind,0)
+WRAP_INT_SVALUE(1value_1nochange,      sqlite3_value_nochange,0)
+WRAP_INT_SVALUE(1value_1numeric_1type, sqlite3_value_numeric_type,SQLITE_NULL)
+WRAP_INT_SVALUE(1value_1subtype,       sqlite3_value_subtype,0)
+WRAP_INT_SVALUE(1value_1type,          sqlite3_value_type,SQLITE_NULL)
+
+#undef WRAP_BOOL_DB
+#undef WRAP_BOOL_STMT
+#undef WRAP_BOOL_SVALUE
+#undef WRAP_INT64_DB
+#undef WRAP_INT_DB
+#undef WRAP_INT_INT
+#undef WRAP_INT_STMT
+#undef WRAP_INT_STMT_INT
+#undef WRAP_INT_SVALUE
+#undef WRAP_INT_VOID
+#undef WRAP_MUTF8_VOID
+#undef WRAP_STR_STMT_INT
+#undef WRAP_STR_DB_INT
+
+S3JniApi(sqlite3_aggregate_context(),jlong,1aggregate_1context)(
+  JniArgsEnvClass, jobject jCx, jboolean initialize
+){
+  sqlite3_context * const pCx = PtrGet_sqlite3_context(jCx);
+  void * const p = pCx
+    ? sqlite3_aggregate_context(pCx, (int)(initialize
+                                           ? (int)sizeof(void*)
+                                           : 0))
+    : 0;
+  return S3JniCast_P2L(p);
+}
+
+/* Central auto-extension handler. */
+static int s3jni_run_java_auto_extensions(sqlite3 *pDb, const char **pzErr,
+                                          const struct sqlite3_api_routines *ignored){
+  int rc = 0;
+  unsigned i, go = 1;
+  JNIEnv * env = 0;
+  S3JniDb * ps;
+  S3JniEnv * jc;
+
+  if( 0==SJG.autoExt.nExt ) return 0;
+  env = s3jni_env();
+  jc = S3JniEnv_get();
+  S3JniDb_mutex_enter;
+  ps = jc->pdbOpening ? jc->pdbOpening : S3JniDb_from_c(pDb);
+  if( !ps ){
+    *pzErr = sqlite3_mprintf("Unexpected arrival of null S3JniDb in "
+                             "auto-extension runner.");
+    S3JniDb_mutex_leave;
+    return SQLITE_ERROR;
+  }
+  assert( ps->jDb );
+  if( !ps->pDb ){
+    assert( jc->pdbOpening == ps );
+    rc = sqlite3_set_clientdata(pDb, S3JniDb_clientdata_key,
+                                ps, 0/* we'll re-set this after open()
+                                        completes. */);
+    if( rc ){
+      S3JniDb_mutex_leave;
+      return rc;
+    }
+  }
+  else{
+    assert( ps == jc->pdbOpening );
+    jc->pdbOpening = 0;
+  }
+  S3JniDb_mutex_leave;
+  NativePointerHolder_set(S3JniNph(sqlite3), ps->jDb, pDb)
+    /* As of here, the Java/C connection is complete except for the
+       (temporary) lack of finalizer for the ps object. */;
+  ps->pDb = pDb;
+  for( i = 0; go && 0==rc; ++i ){
+    S3JniAutoExtension ax = S3JniHook_empty
+      /* We need a copy of the auto-extension object, with our own
+      ** local reference to it, to avoid a race condition with another
+      ** thread manipulating the list during the call and invaliding
+      ** what ax references. */;
+    S3JniAutoExt_mutex_enter;
+    if( i >= SJG.autoExt.nExt ){
+      go = 0;
+    }else{
+      S3JniHook_localdup(&SJG.autoExt.aExt[i], &ax);
+    }
+    S3JniAutoExt_mutex_leave;
+    if( ax.jObj ){
+      rc = (*env)->CallIntMethod(env, ax.jObj, ax.midCallback, ps->jDb);
+      S3JniHook_localundup(ax);
+      S3JniIfThrew {
+        jthrowable const ex = (*env)->ExceptionOccurred(env);
+        char * zMsg;
+        S3JniExceptionClear;
+        zMsg = s3jni_exception_error_msg(env, ex);
+        S3JniUnrefLocal(ex);
+        *pzErr = sqlite3_mprintf("auto-extension threw: %s", zMsg);
+        sqlite3_free(zMsg);
+        rc = SQLITE_ERROR;
+      }
+    }
+  }
+  return rc;
+}
+
+S3JniApi(sqlite3_auto_extension(),jint,1auto_1extension)(
+  JniArgsEnvClass, jobject jAutoExt
+){
+  int i;
+  S3JniAutoExtension * ax = 0;
+  int rc = 0;
+
+  if( !jAutoExt ) return SQLITE_MISUSE;
+  S3JniAutoExt_mutex_enter;
+  for( i = 0; i < SJG.autoExt.nExt; ++i ){
+    /* Look for a match. */
+    ax = &SJG.autoExt.aExt[i];
+    if( ax->jObj && (*env)->IsSameObject(env, ax->jObj, jAutoExt) ){
+      /* same object, so this is a no-op. */
+      S3JniAutoExt_mutex_leave;
+      return 0;
+    }
+  }
+  if( i == SJG.autoExt.nExt ){
+    assert( SJG.autoExt.nExt <= SJG.autoExt.nAlloc );
+    if( SJG.autoExt.nExt == SJG.autoExt.nAlloc ){
+      /* Allocate another slot. */
+      unsigned n = 1 + SJG.autoExt.nAlloc;
+      S3JniAutoExtension * const aNew =
+        s3jni_realloc( SJG.autoExt.aExt, n * sizeof(*ax) );
+      if( !aNew ){
+        rc = SQLITE_NOMEM;
+      }else{
+        SJG.autoExt.aExt = aNew;
+        ++SJG.autoExt.nAlloc;
+      }
+    }
+    if( 0==rc ){
+      ax = &SJG.autoExt.aExt[SJG.autoExt.nExt];
+      rc = S3JniAutoExtension_init(env, ax, jAutoExt);
+      assert( rc ? (0==ax->jObj && 0==ax->midCallback)
+              : (0!=ax->jObj && 0!=ax->midCallback) );
+    }
+  }
+  if( 0==rc ){
+    static int once = 0;
+    if( 0==once && ++once ){
+      rc = sqlite3_auto_extension(
+        (void(*)(void))s3jni_run_java_auto_extensions
+        /* Reminder: the JNI binding of sqlite3_reset_auto_extension()
+        ** does not call the core-lib impl. It only clears Java-side
+        ** auto-extensions. */
+      );
+      if( rc ){
+        assert( ax );
+        S3JniAutoExtension_clear(ax);
+      }
+    }
+    if( 0==rc ){
+      ++SJG.autoExt.nExt;
+    }
+  }
+  S3JniAutoExt_mutex_leave;
+  return rc;
+}
+
+S3JniApi(sqlite3_backup_finish(),jint,1backup_1finish)(
+  JniArgsEnvClass, jlong jpBack
+){
+  int rc = 0;
+  if( jpBack!=0 ){
+    rc = sqlite3_backup_finish( S3JniLongPtr_sqlite3_backup(jpBack) );
+  }
+  return rc;
+}
+
+S3JniApi(sqlite3_backup_init(),jobject,1backup_1init)(
+  JniArgsEnvClass, jlong jpDbDest, jstring jTDest,
+  jlong jpDbSrc, jstring jTSrc
+){
+  sqlite3 * const pDest = S3JniLongPtr_sqlite3(jpDbDest);
+  sqlite3 * const pSrc = S3JniLongPtr_sqlite3(jpDbSrc);
+  char * const zDest = s3jni_jstring_to_utf8(jTDest, 0);
+  char * const zSrc = s3jni_jstring_to_utf8(jTSrc, 0);
+  jobject rv = 0;
+
+  if( pDest && pSrc && zDest && zSrc ){
+    sqlite3_backup * const pB =
+      sqlite3_backup_init(pDest, zDest, pSrc, zSrc);
+    if( pB ){
+      rv = new_java_sqlite3_backup(env, pB);
+      if( !rv ){
+        sqlite3_backup_finish( pB );
+      }
+    }
+  }
+  sqlite3_free(zDest);
+  sqlite3_free(zSrc);
+  return rv;
+}
+
+S3JniApi(sqlite3_backup_pagecount(),jint,1backup_1pagecount)(
+  JniArgsEnvClass, jlong jpBack
+){
+  return sqlite3_backup_pagecount(S3JniLongPtr_sqlite3_backup(jpBack));
+}
+
+S3JniApi(sqlite3_backup_remaining(),jint,1backup_1remaining)(
+  JniArgsEnvClass, jlong jpBack
+){
+  return sqlite3_backup_remaining(S3JniLongPtr_sqlite3_backup(jpBack));
+}
+
+S3JniApi(sqlite3_backup_step(),jint,1backup_1step)(
+  JniArgsEnvClass, jlong jpBack, jint nPage
+){
+  return sqlite3_backup_step(S3JniLongPtr_sqlite3_backup(jpBack), (int)nPage);
+}
+
+S3JniApi(sqlite3_bind_blob(),jint,1bind_1blob)(
+  JniArgsEnvClass, jlong jpStmt, jint ndx, jbyteArray baData, jint nMax
+){
+  jsize nBA = 0;
+  jbyte * const pBuf = baData ? s3jni_jbyteArray_bytes2(baData, &nBA) : 0;
+  int rc;
+  if( pBuf ){
+    if( nMax>nBA ){
+      nMax = nBA;
+    }
+    rc = sqlite3_bind_blob(S3JniLongPtr_sqlite3_stmt(jpStmt), (int)ndx,
+                           pBuf, (int)nMax, SQLITE_TRANSIENT);
+    s3jni_jbyteArray_release(baData, pBuf);
+  }else{
+    rc = baData
+      ? SQLITE_NOMEM
+      : sqlite3_bind_null( S3JniLongPtr_sqlite3_stmt(jpStmt), ndx );
+  }
+  return (jint)rc;
+}
+
+S3JniApi(sqlite3_bind_double(),jint,1bind_1double)(
+  JniArgsEnvClass, jlong jpStmt, jint ndx, jdouble val
+){
+  return (jint)sqlite3_bind_double(S3JniLongPtr_sqlite3_stmt(jpStmt),
+                                   (int)ndx, (double)val);
+}
+
+S3JniApi(sqlite3_bind_int(),jint,1bind_1int)(
+  JniArgsEnvClass, jlong jpStmt, jint ndx, jint val
+){
+  return (jint)sqlite3_bind_int(S3JniLongPtr_sqlite3_stmt(jpStmt), (int)ndx, (int)val);
+}
+
+S3JniApi(sqlite3_bind_int64(),jint,1bind_1int64)(
+  JniArgsEnvClass, jlong jpStmt, jint ndx, jlong val
+){
+  return (jint)sqlite3_bind_int64(S3JniLongPtr_sqlite3_stmt(jpStmt), (int)ndx, (sqlite3_int64)val);
+}
+
+/*
+** Bind a new global ref to Object `val` using sqlite3_bind_pointer().
+*/
+S3JniApi(sqlite3_bind_java_object(),jint,1bind_1java_1object)(
+  JniArgsEnvClass, jlong jpStmt, jint ndx, jobject val
+){
+  sqlite3_stmt * const pStmt = S3JniLongPtr_sqlite3_stmt(jpStmt);
+  int rc = SQLITE_MISUSE;
+
+  if(pStmt){
+    jobject const rv = S3JniRefGlobal(val);
+    if( rv ){
+      rc = sqlite3_bind_pointer(pStmt, ndx, rv, ResultJavaValuePtrStr,
+                                S3Jni_jobject_finalizer);
+    }else if(val){
+      rc = SQLITE_NOMEM;
+    }else{
+      rc = sqlite3_bind_null(pStmt, ndx);
+    }
+  }
+  return rc;
+}
+
+S3JniApi(sqlite3_bind_null(),jint,1bind_1null)(
+  JniArgsEnvClass, jlong jpStmt, jint ndx
+){
+  return (jint)sqlite3_bind_null(S3JniLongPtr_sqlite3_stmt(jpStmt), (int)ndx);
+}
+
+S3JniApi(sqlite3_bind_parameter_count(),jint,1bind_1parameter_1count)(
+  JniArgsEnvClass, jlong jpStmt
+){
+  return (jint)sqlite3_bind_parameter_count(S3JniLongPtr_sqlite3_stmt(jpStmt));
+}
+
+S3JniApi(sqlite3_bind_parameter_index(),jint,1bind_1parameter_1index)(
+  JniArgsEnvClass, jlong jpStmt, jbyteArray jName
+){
+  int rc = 0;
+  jbyte * const pBuf = s3jni_jbyteArray_bytes(jName);
+  if( pBuf ){
+    rc = sqlite3_bind_parameter_index(S3JniLongPtr_sqlite3_stmt(jpStmt),
+                                      (const char *)pBuf);
+    s3jni_jbyteArray_release(jName, pBuf);
+  }
+  return rc;
+}
+
+S3JniApi(sqlite3_bind_parameter_name(),jstring,1bind_1parameter_1name)(
+  JniArgsEnvClass, jlong jpStmt, jint ndx
+){
+  const char *z =
+    sqlite3_bind_parameter_name(S3JniLongPtr_sqlite3_stmt(jpStmt), (int)ndx);
+  return z ? s3jni_utf8_to_jstring(z, -1) : 0;
+}
+
+/*
+** Impl of sqlite3_bind_text/text16().
+*/
+static int s3jni__bind_text(int is16, JNIEnv *env, jlong jpStmt, jint ndx,
+                            jbyteArray baData, jint nMax){
+  jsize nBA = 0;
+  jbyte * const pBuf =
+    baData ? s3jni_jbyteArray_bytes2(baData, &nBA) : 0;
+  int rc;
+  if( pBuf ){
+    if( nMax>nBA ){
+      nMax = nBA;
+    }
+    /* Note that we rely on the Java layer having assured that baData
+       is NUL-terminated if nMax is negative. In order to avoid UB for
+       such cases, we do not expose the byte-limit arguments in the
+       public API. */
+    rc = is16
+      ? sqlite3_bind_text16(S3JniLongPtr_sqlite3_stmt(jpStmt), (int)ndx,
+                            pBuf, (int)nMax, SQLITE_TRANSIENT)
+      : sqlite3_bind_text(S3JniLongPtr_sqlite3_stmt(jpStmt), (int)ndx,
+                          (const char *)pBuf,
+                          (int)nMax, SQLITE_TRANSIENT);
+  }else{
+    rc = baData
+      ? sqlite3_bind_null(S3JniLongPtr_sqlite3_stmt(jpStmt), (int)ndx)
+      : SQLITE_NOMEM;
+  }
+  s3jni_jbyteArray_release(baData, pBuf);
+  return (jint)rc;
+
+}
+
+S3JniApi(sqlite3_bind_text(),jint,1bind_1text)(
+  JniArgsEnvClass, jlong jpStmt, jint ndx, jbyteArray baData, jint nMax
+){
+  return s3jni__bind_text(0, env, jpStmt, ndx, baData, nMax);
+}
+
+S3JniApi(sqlite3_bind_text16(),jint,1bind_1text16)(
+  JniArgsEnvClass, jlong jpStmt, jint ndx, jbyteArray baData, jint nMax
+){
+  return s3jni__bind_text(1, env, jpStmt, ndx, baData, nMax);
+}
+
+S3JniApi(sqlite3_bind_value(),jint,1bind_1value)(
+  JniArgsEnvClass, jlong jpStmt, jint ndx, jlong jpValue
+){
+  int rc = 0;
+  sqlite3_stmt * pStmt = S3JniLongPtr_sqlite3_stmt(jpStmt);
+  if( pStmt ){
+    sqlite3_value *v = S3JniLongPtr_sqlite3_value(jpValue);
+    if( v ){
+      rc = sqlite3_bind_value(pStmt, (int)ndx, v);
+    }else{
+      rc = sqlite3_bind_null(pStmt, (int)ndx);
+    }
+  }else{
+    rc = SQLITE_MISUSE;
+  }
+  return (jint)rc;
+}
+
+S3JniApi(sqlite3_bind_zeroblob(),jint,1bind_1zeroblob)(
+  JniArgsEnvClass, jlong jpStmt, jint ndx, jint n
+){
+  return (jint)sqlite3_bind_zeroblob(S3JniLongPtr_sqlite3_stmt(jpStmt),
+                                     (int)ndx, (int)n);
+}
+
+S3JniApi(sqlite3_bind_zeroblob64(),jint,1bind_1zeroblob64)(
+  JniArgsEnvClass, jlong jpStmt, jint ndx, jlong n
+){
+  return (jint)sqlite3_bind_zeroblob64(S3JniLongPtr_sqlite3_stmt(jpStmt),
+                                       (int)ndx, (sqlite3_uint64)n);
+}
+
+S3JniApi(sqlite3_blob_bytes(),jint,1blob_1bytes)(
+  JniArgsEnvClass, jlong jpBlob
+){
+  return sqlite3_blob_bytes(S3JniLongPtr_sqlite3_blob(jpBlob));
+}
+
+S3JniApi(sqlite3_blob_close(),jint,1blob_1close)(
+  JniArgsEnvClass, jlong jpBlob
+){
+  sqlite3_blob * const b = S3JniLongPtr_sqlite3_blob(jpBlob);
+  return b ? (jint)sqlite3_blob_close(b) : SQLITE_MISUSE;
+}
+
+S3JniApi(sqlite3_blob_open(),jint,1blob_1open)(
+  JniArgsEnvClass, jlong jpDb, jstring jDbName, jstring jTbl, jstring jCol,
+  jlong jRowId, jint flags, jobject jOut
+){
+  sqlite3 * const db = S3JniLongPtr_sqlite3(jpDb);
+  sqlite3_blob * pBlob = 0;
+  char * zDbName = 0, * zTableName = 0, * zColumnName = 0;
+  int rc;
+
+  if( !db || !jDbName || !jTbl || !jCol ) return SQLITE_MISUSE;
+  zDbName = s3jni_jstring_to_utf8(jDbName,0);
+  zTableName = zDbName ? s3jni_jstring_to_utf8(jTbl,0) : 0;
+  zColumnName = zTableName ? s3jni_jstring_to_utf8(jCol,0) : 0;
+  rc = zColumnName
+    ? sqlite3_blob_open(db, zDbName, zTableName, zColumnName,
+                        (sqlite3_int64)jRowId, (int)flags, &pBlob)
+    : SQLITE_NOMEM;
+  if( 0==rc ){
+    jobject rv = new_java_sqlite3_blob(env, pBlob);
+    if( !rv ){
+      sqlite3_blob_close(pBlob);
+      rc = SQLITE_NOMEM;
+    }
+    OutputPointer_set_obj(env, S3JniNph(OutputPointer_sqlite3_blob), jOut, rv);
+  }
+  sqlite3_free(zDbName);
+  sqlite3_free(zTableName);
+  sqlite3_free(zColumnName);
+  return rc;
+}
+
+S3JniApi(sqlite3_blob_read(),jint,1blob_1read)(
+  JniArgsEnvClass, jlong jpBlob, jbyteArray jTgt, jint iOffset
+){
+  jbyte * const pBa = s3jni_jbyteArray_bytes(jTgt);
+  int rc = jTgt ? (pBa ? SQLITE_MISUSE : SQLITE_NOMEM) : SQLITE_MISUSE;
+  if( pBa ){
+    jsize const nTgt = (*env)->GetArrayLength(env, jTgt);
+    rc = sqlite3_blob_read(S3JniLongPtr_sqlite3_blob(jpBlob), pBa,
+                           (int)nTgt, (int)iOffset);
+    if( 0==rc ){
+      s3jni_jbyteArray_commit(jTgt, pBa);
+    }else{
+      s3jni_jbyteArray_release(jTgt, pBa);
+    }
+  }
+  return rc;
+}
+
+S3JniApi(sqlite3_blob_reopen(),jint,1blob_1reopen)(
+  JniArgsEnvClass, jlong jpBlob, jlong iNewRowId
+){
+  return (jint)sqlite3_blob_reopen(S3JniLongPtr_sqlite3_blob(jpBlob),
+                                   (sqlite3_int64)iNewRowId);
+}
+
+S3JniApi(sqlite3_blob_write(),jint,1blob_1write)(
+  JniArgsEnvClass, jlong jpBlob, jbyteArray jBa, jint iOffset
+){
+  sqlite3_blob * const b = S3JniLongPtr_sqlite3_blob(jpBlob);
+  jbyte * const pBuf = b ? s3jni_jbyteArray_bytes(jBa) : 0;
+  const jsize nBA = pBuf ? (*env)->GetArrayLength(env, jBa) : 0;
+  int rc = SQLITE_MISUSE;
+  if(b && pBuf){
+    rc = sqlite3_blob_write( b, pBuf, (int)nBA, (int)iOffset );
+  }
+  s3jni_jbyteArray_release(jBa, pBuf);
+  return (jint)rc;
+}
+
+/* Central C-to-Java busy handler proxy. */
+static int s3jni_busy_handler(void* pState, int n){
+  S3JniDb * const ps = (S3JniDb *)pState;
+  int rc = 0;
+  S3JniDeclLocal_env;
+  S3JniHook hook;
+
+  S3JniHook_localdup(&ps->hooks.busyHandler, &hook);
+  if( hook.jObj ){
+    rc = (*env)->CallIntMethod(env, hook.jObj,
+                               hook.midCallback, (jint)n);
+    S3JniIfThrew{
+      S3JniExceptionWarnCallbackThrew("sqlite3_busy_handler() callback");
+      rc = s3jni_db_exception(ps->pDb, SQLITE_ERROR,
+                              "sqlite3_busy_handler() callback threw.");
+    }
+    S3JniHook_localundup(hook);
+  }
+  return rc;
+}
+
+S3JniApi(sqlite3_busy_handler(),jint,1busy_1handler)(
+  JniArgsEnvClass, jlong jpDb, jobject jBusy
+){
+  S3JniDb * const ps = S3JniDb_from_jlong(jpDb);
+  S3JniHook * const pHook = ps ? &ps->hooks.busyHandler : 0;
+  S3JniHook hook = S3JniHook_empty;
+  int rc = 0;
+
+  if( !ps ) return (jint)SQLITE_MISUSE;
+  S3JniDb_mutex_enter;
+  if( jBusy ){
+    if( pHook->jObj && (*env)->IsSameObject(env, pHook->jObj, jBusy) ){
+      /* Same object - this is a no-op. */
+    }else{
+      jclass const klazz = (*env)->GetObjectClass(env, jBusy);
+      hook.jObj = S3JniRefGlobal(jBusy);
+      hook.midCallback = (*env)->GetMethodID(env, klazz, "call", "(I)I");
+      S3JniUnrefLocal(klazz);
+      S3JniIfThrew {
+        rc = SQLITE_ERROR;
+      }
+    }
+  }
+  if( 0==rc ){
+    if( jBusy ){
+      if( hook.jObj ){ /* Replace handler */
+        rc = sqlite3_busy_handler(ps->pDb, s3jni_busy_handler, ps);
+        if( 0==rc ){
+          S3JniHook_unref(pHook);
+          *pHook = hook /* transfer Java ref ownership */;
+          hook = S3JniHook_empty;
+        }
+      }/* else no-op */
+    }else{ /* Clear handler */
+      rc = sqlite3_busy_handler(ps->pDb, 0, 0);
+      if( 0==rc ){
+        S3JniHook_unref(pHook);
+      }
+    }
+  }
+  S3JniHook_unref(&hook);
+  S3JniDb_mutex_leave;
+  return rc;
+}
+
+S3JniApi(sqlite3_busy_timeout(),jint,1busy_1timeout)(
+  JniArgsEnvClass, jlong jpDb, jint ms
+){
+  S3JniDb * const ps = S3JniDb_from_jlong(jpDb);
+  int rc = SQLITE_MISUSE;
+  if( ps ){
+    S3JniDb_mutex_enter;
+    S3JniHook_unref(&ps->hooks.busyHandler);
+    rc = sqlite3_busy_timeout(ps->pDb, (int)ms);
+    S3JniDb_mutex_leave;
+  }
+  return rc;
+}
+
+S3JniApi(sqlite3_cancel_auto_extension(),jboolean,1cancel_1auto_1extension)(
+  JniArgsEnvClass, jobject jAutoExt
+){
+  S3JniAutoExtension * ax;
+  jboolean rc = JNI_FALSE;
+  int i;
+
+  if( !jAutoExt ){
+    return rc;
+  }
+  S3JniAutoExt_mutex_enter;
+  /* This algo corresponds to the one in the core. */
+  for( i = SJG.autoExt.nExt-1; i >= 0; --i ){
+    ax = &SJG.autoExt.aExt[i];
+    if( ax->jObj && (*env)->IsSameObject(env, ax->jObj, jAutoExt) ){
+      S3JniAutoExtension_clear(ax);
+      /* Move final entry into this slot. */
+      --SJG.autoExt.nExt;
+      *ax = SJG.autoExt.aExt[SJG.autoExt.nExt];
+      SJG.autoExt.aExt[SJG.autoExt.nExt] = S3JniHook_empty;
+      assert( !SJG.autoExt.aExt[SJG.autoExt.nExt].jObj );
+      rc = JNI_TRUE;
+      break;
+    }
+  }
+  S3JniAutoExt_mutex_leave;
+  return rc;
+}
+
+/* Wrapper for sqlite3_close(_v2)(). */
+static jint s3jni_close_db(JNIEnv * const env, jlong jpDb, int version){
+  int rc = 0;
+  S3JniDb * const ps = S3JniDb_from_jlong(jpDb);
+
+  assert(version == 1 || version == 2);
+  if( ps ){
+    rc = 1==version
+      ? (jint)sqlite3_close(ps->pDb)
+      : (jint)sqlite3_close_v2(ps->pDb);
+  }
+  return (jint)rc;
+}
+
+S3JniApi(sqlite3_close(),jint,1close)(JniArgsEnvClass, jlong pDb){
+  return s3jni_close_db(env, pDb, 1);
+}
+
+S3JniApi(sqlite3_close_v2(),jint,1close_1v2)(JniArgsEnvClass, jlong pDb){
+  return s3jni_close_db(env, pDb, 2);
+}
+
+/*
+** Assumes z is an array of unsigned short and returns the index in
+** that array of the first element with the value 0.
+*/
+static unsigned int s3jni_utf16_strlen(void const * z){
+  unsigned int i = 0;
+  const unsigned short * p = z;
+  while( p[i] ) ++i;
+  return i;
+}
+
+/* Descriptive alias for use with sqlite3_collation_needed(). */
+typedef S3JniHook S3JniCollationNeeded;
+
+/* Central C-to-Java sqlite3_collation_needed16() hook impl. */
+static void s3jni_collation_needed_impl16(void *pState, sqlite3 *pDb,
+                                          int eTextRep, const void * z16Name){
+  S3JniCollationNeeded * const pHook = pState;
+  S3JniDeclLocal_env;
+  S3JniHook hook;
+
+  S3JniHook_localdup(pHook, &hook);
+  if( hook.jObj ){
+    unsigned int const nName = s3jni_utf16_strlen(z16Name);
+    jstring jName = (*env)->NewString(env, (jchar const *)z16Name, nName);
+
+    s3jni_oom_check( jName );
+    assert( hook.jExtra );
+    S3JniIfThrew{
+      S3JniExceptionClear;
+    }else if( hook.jExtra ){
+      (*env)->CallVoidMethod(env, hook.jObj, hook.midCallback,
+                             hook.jExtra, (jint)eTextRep, jName);
+      S3JniIfThrew{
+        S3JniExceptionWarnCallbackThrew("sqlite3_collation_needed() callback");
+      }
+    }
+    S3JniUnrefLocal(jName);
+    S3JniHook_localundup(hook);
+  }
+}
+
+S3JniApi(sqlite3_collation_needed(),jint,1collation_1needed)(
+  JniArgsEnvClass, jlong jpDb, jobject jHook
+){
+  S3JniDb * ps;
+  S3JniCollationNeeded * pHook;
+  int rc = 0;
+
+  S3JniDb_mutex_enter;
+  ps = S3JniDb_from_jlong(jpDb);
+  if( !ps ){
+    S3JniDb_mutex_leave;
+    return SQLITE_MISUSE;
+  }
+  pHook = &ps->hooks.collationNeeded;
+  if( pHook->jObj && jHook &&
+     (*env)->IsSameObject(env, pHook->jObj, jHook) ){
+    /* no-op */
+  }else if( !jHook ){
+    rc = sqlite3_collation_needed(ps->pDb, 0, 0);
+    if( 0==rc ){
+      S3JniHook_unref(pHook);
+    }
+  }else{
+    jclass const klazz = (*env)->GetObjectClass(env, jHook);
+    jmethodID const xCallback = (*env)->GetMethodID(
+      env, klazz, "call", "(Lorg/sqlite/jni/capi/sqlite3;ILjava/lang/String;)I"
+    );
+    S3JniUnrefLocal(klazz);
+    S3JniIfThrew {
+      rc = s3jni_db_exception(ps->pDb, SQLITE_MISUSE,
+                              "Cannot not find matching call() in "
+                              "CollationNeededCallback object.");
+    }else{
+      rc = sqlite3_collation_needed16(ps->pDb, pHook,
+                                      s3jni_collation_needed_impl16);
+      if( 0==rc ){
+        S3JniHook_unref(pHook);
+        pHook->midCallback = xCallback;
+        pHook->jObj = S3JniRefGlobal(jHook);
+        pHook->jExtra = S3JniRefGlobal(ps->jDb);
+      }
+    }
+  }
+  S3JniDb_mutex_leave;
+  return rc;
+}
+
+S3JniApi(sqlite3_column_blob(),jbyteArray,1column_1blob)(
+  JniArgsEnvClass, jobject jpStmt, jint ndx
+){
+  sqlite3_stmt * const pStmt = PtrGet_sqlite3_stmt(jpStmt);
+  void const * const p = sqlite3_column_blob(pStmt, (int)ndx);
+  int const n = p ? sqlite3_column_bytes(pStmt, (int)ndx) : 0;
+
+  return p ? s3jni_new_jbyteArray(p, n) : 0;
+}
+
+S3JniApi(sqlite3_column_double(),jdouble,1column_1double)(
+  JniArgsEnvClass, jobject jpStmt, jint ndx
+){
+  return (jdouble)sqlite3_column_double(PtrGet_sqlite3_stmt(jpStmt), (int)ndx);
+}
+
+S3JniApi(sqlite3_column_int(),jint,1column_1int)(
+  JniArgsEnvClass, jobject jpStmt, jint ndx
+){
+  return (jint)sqlite3_column_int(PtrGet_sqlite3_stmt(jpStmt), (int)ndx);
+}
+
+S3JniApi(sqlite3_column_int64(),jlong,1column_1int64)(
+  JniArgsEnvClass, jobject jpStmt, jint ndx
+){
+  return (jlong)sqlite3_column_int64(PtrGet_sqlite3_stmt(jpStmt), (int)ndx);
+}
+
+S3JniApi(sqlite3_column_text(),jbyteArray,1column_1text)(
+  JniArgsEnvClass, jobject jpStmt, jint ndx
+){
+  sqlite3_stmt * const stmt = PtrGet_sqlite3_stmt(jpStmt);
+  const unsigned char * const p = stmt ? sqlite3_column_text(stmt, (int)ndx) : 0;
+  const int n = p ? sqlite3_column_bytes(stmt, (int)ndx) : 0;
+  return p ? s3jni_new_jbyteArray(p, n) : NULL;
+}
+
+#if 0
+// this impl might prove useful.
+S3JniApi(sqlite3_column_text(),jstring,1column_1text)(
+  JniArgsEnvClass, jobject jpStmt, jint ndx
+){
+  sqlite3_stmt * const stmt = PtrGet_sqlite3_stmt(jpStmt);
+  const unsigned char * const p = stmt ? sqlite3_column_text(stmt, (int)ndx) : 0;
+  const int n = p ? sqlite3_column_bytes(stmt, (int)ndx) : 0;
+  return p ? s3jni_utf8_to_jstring( (const char *)p, n) : 0;
+}
+#endif
+
+S3JniApi(sqlite3_column_text16(),jstring,1column_1text16)(
+  JniArgsEnvClass, jobject jpStmt, jint ndx
+){
+  sqlite3_stmt * const stmt = PtrGet_sqlite3_stmt(jpStmt);
+  const void * const p = stmt ? sqlite3_column_text16(stmt, (int)ndx) : 0;
+  const int n = p ? sqlite3_column_bytes16(stmt, (int)ndx) : 0;
+  return s3jni_text16_to_jstring(env, p, n);
+}
+
+S3JniApi(sqlite3_column_value(),jobject,1column_1value)(
+  JniArgsEnvClass, jobject jpStmt, jint ndx
+){
+  sqlite3_value * const sv =
+    sqlite3_column_value(PtrGet_sqlite3_stmt(jpStmt), (int)ndx)
+    /* reminder: returns an SQL NULL if jpStmt==NULL */;
+  return new_java_sqlite3_value(env, sv);
+}
+
+/*
+** Impl for commit hooks (if isCommit is true) or rollback hooks.
+*/
+static int s3jni_commit_rollback_hook_impl(int isCommit, S3JniDb * const ps){
+  S3JniDeclLocal_env;
+  int rc = 0;
+  S3JniHook hook;
+
+  S3JniHook_localdup(isCommit
+                     ? &ps->hooks.commit : &ps->hooks.rollback,
+                     &hook);
+  if( hook.jObj ){
+    rc = isCommit
+      ? (int)(*env)->CallIntMethod(env, hook.jObj, hook.midCallback)
+      : (int)((*env)->CallVoidMethod(env, hook.jObj, hook.midCallback), 0);
+    S3JniIfThrew{
+      rc = s3jni_db_exception(ps->pDb, SQLITE_ERROR, "hook callback threw");
+    }
+    S3JniHook_localundup(hook);
+  }
+  return rc;
+}
+
+/* C-to-Java commit hook wrapper. */
+static int s3jni_commit_hook_impl(void *pP){
+  return s3jni_commit_rollback_hook_impl(1, pP);
+}
+
+/* C-to-Java rollback hook wrapper. */
+static void s3jni_rollback_hook_impl(void *pP){
+  (void)s3jni_commit_rollback_hook_impl(0, pP);
+}
+
+/*
+** Proxy for sqlite3_commit_hook() (if isCommit is true) or
+** sqlite3_rollback_hook().
+*/
+static jobject s3jni_commit_rollback_hook(int isCommit, JNIEnv * const env,
+                                          jlong jpDb, jobject jHook){
+  S3JniDb * ps;
+  jobject pOld = 0;  /* previous hoook */
+  S3JniHook * pHook; /* ps->hooks.commit|rollback */
+
+  S3JniDb_mutex_enter;
+  ps = S3JniDb_from_jlong(jpDb);
+  if( !ps ){
+    s3jni_db_error(ps->pDb, SQLITE_MISUSE, 0);
+    S3JniDb_mutex_leave;
+    return 0;
+  }
+  pHook = isCommit ? &ps->hooks.commit : &ps->hooks.rollback;
+  pOld = pHook->jObj;
+  if( pOld && jHook &&
+      (*env)->IsSameObject(env, pOld, jHook) ){
+    /* No-op. */
+  }else if( !jHook ){
+    if( pOld ){
+      jobject tmp = S3JniRefLocal(pOld);
+      S3JniUnrefGlobal(pOld);
+      pOld = tmp;
+    }
+    *pHook = S3JniHook_empty;
+    if( isCommit ) sqlite3_commit_hook(ps->pDb, 0, 0);
+    else sqlite3_rollback_hook(ps->pDb, 0, 0);
+  }else{
+    jclass const klazz = (*env)->GetObjectClass(env, jHook);
+    jmethodID const xCallback = (*env)->GetMethodID(env, klazz, "call",
+                                                    isCommit ? "()I" : "()V");
+    S3JniUnrefLocal(klazz);
+    S3JniIfThrew {
+      S3JniExceptionReport;
+      S3JniExceptionClear;
+      s3jni_db_error(ps->pDb, SQLITE_ERROR,
+                     "Cannot not find matching call() method in"
+                     "hook object.");
+    }else{
+      pHook->midCallback = xCallback;
+      pHook->jObj = S3JniRefGlobal(jHook);
+      if( isCommit ) sqlite3_commit_hook(ps->pDb, s3jni_commit_hook_impl, ps);
+      else sqlite3_rollback_hook(ps->pDb, s3jni_rollback_hook_impl, ps);
+      if( pOld ){
+        jobject tmp = S3JniRefLocal(pOld);
+        S3JniUnrefGlobal(pOld);
+        pOld = tmp;
+      }
+    }
+  }
+  S3JniDb_mutex_leave;
+  return pOld;
+}
+
+S3JniApi(sqlite3_commit_hook(),jobject,1commit_1hook)(
+  JniArgsEnvClass, jlong jpDb, jobject jHook
+){
+  return s3jni_commit_rollback_hook(1, env, jpDb, jHook);
+}
+
+S3JniApi(sqlite3_compileoption_get(),jstring,1compileoption_1get)(
+  JniArgsEnvClass, jint n
+){
+  const char * z = sqlite3_compileoption_get(n);
+  jstring const rv = z ? (*env)->NewStringUTF( env, z ) : 0;
+    /* We know these to be ASCII, so MUTF-8 is fine. */;
+  s3jni_oom_check(z ? !!rv : 1);
+  return rv;
+}
+
+S3JniApi(sqlite3_compileoption_used(),jboolean,1compileoption_1used)(
+  JniArgsEnvClass, jstring name
+){
+  const char *zUtf8 = s3jni_jstring_to_mutf8(name)
+    /* We know these to be ASCII, so MUTF-8 is fine (and
+       hypothetically faster to convert). */;
+  const jboolean rc =
+    0==sqlite3_compileoption_used(zUtf8) ? JNI_FALSE : JNI_TRUE;
+  s3jni_mutf8_release(name, zUtf8);
+  return rc;
+}
+
+S3JniApi(sqlite3_complete(),int,1complete)(
+  JniArgsEnvClass, jbyteArray jSql
+){
+  jbyte * const pBuf = s3jni_jbyteArray_bytes(jSql);
+  const jsize nBA = pBuf ? (*env)->GetArrayLength(env, jSql) : 0;
+  int rc;
+
+  assert( (nBA>0 ? 0==pBuf[nBA-1] : (pBuf ? 0==*pBuf : 1))
+          && "Byte array is not NUL-terminated." );
+  rc = (pBuf && 0==pBuf[(nBA ? nBA-1 : 0)])
+    ? sqlite3_complete( (const char *)pBuf )
+    : (jSql ? SQLITE_NOMEM : SQLITE_MISUSE);
+  s3jni_jbyteArray_release(jSql, pBuf);
+  return rc;
+}
+
+S3JniApi(sqlite3_config() /*for a small subset of options.*/,
+         jint,1config__I)(JniArgsEnvClass, jint n){
+  switch( n ){
+    case SQLITE_CONFIG_SINGLETHREAD:
+    case SQLITE_CONFIG_MULTITHREAD:
+    case SQLITE_CONFIG_SERIALIZED:
+      return sqlite3_config( n );
+    default:
+      return SQLITE_MISUSE;
+  }
+}
+/* C-to-Java SQLITE_CONFIG_LOG wrapper. */
+static void s3jni_config_log(void *ignored, int errCode, const char *z){
+  S3JniDeclLocal_env;
+  S3JniHook hook = S3JniHook_empty;
+
+  S3JniHook_localdup(&SJG.hook.configlog, &hook);
+  if( hook.jObj ){
+    jstring const jArg1 = z ? s3jni_utf8_to_jstring(z, -1) : 0;
+    if( z ? !!jArg1 : 1 ){
+      (*env)->CallVoidMethod(env, hook.jObj, hook.midCallback, errCode, jArg1);
+    }
+    S3JniIfThrew{
+      S3JniExceptionWarnCallbackThrew("SQLITE_CONFIG_LOG callback");
+      S3JniExceptionClear;
+    }
+    S3JniHook_localundup(hook);
+    S3JniUnrefLocal(jArg1);
+  }
+}
+
+S3JniApi(sqlite3_config() /* for SQLITE_CONFIG_LOG */,
+         jint, 1config__Lorg_sqlite_jni_ConfigLogCallback_2
+)(JniArgsEnvClass, jobject jLog){
+  S3JniHook * const pHook = &SJG.hook.configlog;
+  int rc = 0;
+
+  S3JniGlobal_mutex_enter;
+  if( !jLog ){
+    rc = sqlite3_config( SQLITE_CONFIG_LOG, NULL, NULL );
+    if( 0==rc ){
+      S3JniHook_unref(pHook);
+    }
+  }else if( pHook->jObj && (*env)->IsSameObject(env, jLog, pHook->jObj) ){
+    /* No-op */
+  }else {
+    jclass const klazz = (*env)->GetObjectClass(env, jLog);
+    jmethodID const midCallback = (*env)->GetMethodID(env, klazz, "call",
+                                                      "(ILjava/lang/String;)V");
+    S3JniUnrefLocal(klazz);
+    if( midCallback ){
+      rc = sqlite3_config( SQLITE_CONFIG_LOG, s3jni_config_log, NULL );
+      if( 0==rc ){
+        S3JniHook_unref(pHook);
+        pHook->midCallback = midCallback;
+        pHook->jObj = S3JniRefGlobal(jLog);
+      }
+    }else{
+      S3JniExceptionWarnIgnore;
+      rc = SQLITE_ERROR;
+    }
+  }
+  S3JniGlobal_mutex_leave;
+  return rc;
+}
+
+#ifdef SQLITE_ENABLE_SQLLOG
+/* C-to-Java SQLITE_CONFIG_SQLLOG wrapper. */
+static void s3jni_config_sqllog(void *ignored, sqlite3 *pDb, const char *z, int op){
+  jobject jArg0 = 0;
+  jstring jArg1 = 0;
+  S3JniDeclLocal_env;
+  S3JniDb * const ps = S3JniDb_from_c(pDb);
+  S3JniHook hook = S3JniHook_empty;
+
+  if( ps ){
+    S3JniHook_localdup(&SJG.hook.sqllog, &hook);
+  }
+  if( !hook.jObj ) return;
+  jArg0 = S3JniRefLocal(ps->jDb);
+  switch( op ){
+    case 0: /* db opened */
+    case 1: /* SQL executed */
+      jArg1 = s3jni_utf8_to_jstring( z, -1);
+      break;
+    case 2: /* db closed */
+      break;
+    default:
+      (*env)->FatalError(env, "Unhandled 4th arg to SQLITE_CONFIG_SQLLOG.");
+      break;
+  }
+  (*env)->CallVoidMethod(env, hook.jObj, hook.midCallback, jArg0, jArg1, op);
+  S3JniIfThrew{
+    S3JniExceptionWarnCallbackThrew("SQLITE_CONFIG_SQLLOG callback");
+    S3JniExceptionClear;
+  }
+  S3JniHook_localundup(hook);
+  S3JniUnrefLocal(jArg0);
+  S3JniUnrefLocal(jArg1);
+}
+//! Requirement of SQLITE_CONFIG_SQLLOG.
+void sqlite3_init_sqllog(void){
+  sqlite3_config( SQLITE_CONFIG_SQLLOG, s3jni_config_sqllog, 0 );
+}
+#endif
+
+S3JniApi(sqlite3_config() /* for SQLITE_CONFIG_SQLLOG */,
+         jint, 1config__Lorg_sqlite_jni_ConfigSqllogCallback_2)(
+           JniArgsEnvClass, jobject jLog){
+#ifndef SQLITE_ENABLE_SQLLOG
+  return SQLITE_MISUSE;
+#else
+  S3JniHook * const pHook = &SJG.hook.sqllog;
+  int rc = 0;
+
+  S3JniGlobal_mutex_enter;
+  if( !jLog ){
+    rc = sqlite3_config( SQLITE_CONFIG_SQLLOG, NULL );
+    if( 0==rc ){
+      S3JniHook_unref(pHook);
+    }
+  }else if( pHook->jObj && (*env)->IsSameObject(env, jLog, pHook->jObj) ){
+    /* No-op */
+  }else {
+    jclass const klazz = (*env)->GetObjectClass(env, jLog);
+    jmethodID const midCallback = (*env)->GetMethodID(env, klazz, "call",
+                                                      "(Lorg/sqlite/jni/capi/sqlite3;"
+                                                      "Ljava/lang/String;"
+                                                      "I)V");
+    S3JniUnrefLocal(klazz);
+    if( midCallback ){
+      rc = sqlite3_config( SQLITE_CONFIG_SQLLOG, s3jni_config_sqllog, NULL );
+      if( 0==rc ){
+        S3JniHook_unref(pHook);
+        pHook->midCallback = midCallback;
+        pHook->jObj = S3JniRefGlobal(jLog);
+      }
+    }else{
+      S3JniExceptionWarnIgnore;
+      rc = SQLITE_ERROR;
+    }
+  }
+  S3JniGlobal_mutex_leave;
+  return rc;
+#endif
+}
+
+S3JniApi(sqlite3_context_db_handle(),jobject,1context_1db_1handle)(
+  JniArgsEnvClass, jobject jpCx
+){
+  sqlite3_context * const pCx = PtrGet_sqlite3_context(jpCx);
+  sqlite3 * const pDb = pCx ? sqlite3_context_db_handle(pCx) : 0;
+  S3JniDb * const ps = pDb ? S3JniDb_from_c(pDb) : 0;
+  return ps ? ps->jDb : 0;
+}
+
+/*
+** State for CollationCallbacks. This used to be its own separate
+** type, but has since been consolidated with S3JniHook. It retains
+** its own typedef for code legibility and searchability reasons.
+*/
+typedef S3JniHook S3JniCollationCallback;
+
+/*
+** Proxy for Java-side CollationCallback.xCompare() callbacks.
+*/
+static int CollationCallback_xCompare(void *pArg, int nLhs, const void *lhs,
+                                      int nRhs, const void *rhs){
+  S3JniCollationCallback * const pCC = pArg;
+  S3JniDeclLocal_env;
+  jint rc = 0;
+  if( pCC->jObj ){
+    jbyteArray jbaLhs = s3jni_new_jbyteArray(lhs, (jint)nLhs);
+    jbyteArray jbaRhs = jbaLhs
+      ? s3jni_new_jbyteArray(rhs, (jint)nRhs) : 0;
+    if( !jbaRhs ){
+      S3JniUnrefLocal(jbaLhs);
+      /* We have no recovery strategy here. */
+      s3jni_oom_check( jbaRhs );
+      return 0;
+    }
+    rc = (*env)->CallIntMethod(env, pCC->jObj, pCC->midCallback,
+                               jbaLhs, jbaRhs);
+    S3JniExceptionIgnore;
+    S3JniUnrefLocal(jbaLhs);
+    S3JniUnrefLocal(jbaRhs);
+  }
+  return (int)rc;
+}
+
+/* CollationCallback finalizer for use by the sqlite3 internals. */
+static void CollationCallback_xDestroy(void *pArg){
+  S3JniCollationCallback * const pCC = pArg;
+  S3JniDeclLocal_env;
+  S3JniHook_free(pCC);
+}
+
+S3JniApi(sqlite3_create_collation() sqlite3_create_collation_v2(),
+         jint,1create_1collation
+)(JniArgsEnvClass, jobject jDb, jstring name, jint eTextRep,
+  jobject oCollation){
+  int rc;
+  S3JniDb * ps;
+
+  if( !jDb || !name || !encodingTypeIsValid(eTextRep) ){
+    return (jint)SQLITE_MISUSE;
+  }
+  S3JniDb_mutex_enter;
+  ps = S3JniDb_from_java(jDb);
+  jclass const klazz = (*env)->GetObjectClass(env, oCollation);
+  jmethodID const midCallback =
+    (*env)->GetMethodID(env, klazz, "call", "([B[B)I");
+  S3JniUnrefLocal(klazz);
+  S3JniIfThrew{
+    rc = s3jni_db_error(ps->pDb, SQLITE_ERROR,
+                        "Could not get call() method from "
+                        "CollationCallback object.");
+  }else{
+    char * const zName = s3jni_jstring_to_utf8(name, 0);
+    S3JniCollationCallback * const pCC =
+      zName ? S3JniHook_alloc() : 0;
+    if( pCC ){
+      rc = sqlite3_create_collation_v2(ps->pDb, zName, (int)eTextRep,
+                                       pCC, CollationCallback_xCompare,
+                                       CollationCallback_xDestroy);
+      if( 0==rc ){
+        pCC->midCallback = midCallback;
+        pCC->jObj = S3JniRefGlobal(oCollation);
+        pCC->doXDestroy = 1;
+      }else{
+        CollationCallback_xDestroy(pCC);
+      }
+    }else{
+      rc = SQLITE_NOMEM;
+    }
+    sqlite3_free(zName);
+  }
+  S3JniDb_mutex_leave;
+  return (jint)rc;
+}
+
+S3JniApi(sqlite3_create_function() sqlite3_create_function_v2()
+         sqlite3_create_window_function(),
+         jint,1create_1function
+)(JniArgsEnvClass, jobject jDb, jstring jFuncName, jint nArg,
+  jint eTextRep, jobject jFunctor){
+  S3JniUdf * s = 0;
+  int rc;
+  sqlite3 * const pDb = PtrGet_sqlite3(jDb);
+  char * zFuncName = 0;
+
+  if( !pDb || !jFuncName ){
+    return SQLITE_MISUSE;
+  }else if( !encodingTypeIsValid(eTextRep) ){
+    return s3jni_db_error(pDb, SQLITE_FORMAT,
+                          "Invalid function encoding option.");
+  }
+  s = S3JniUdf_alloc(env, jFunctor);
+  if( !s ) return SQLITE_NOMEM;
+
+  if( UDF_UNKNOWN_TYPE==s->type ){
+    rc = s3jni_db_error(pDb, SQLITE_MISUSE,
+                        "Cannot unambiguously determine function type.");
+    S3JniUdf_free(env, s, 1);
+    goto error_cleanup;
+  }
+  zFuncName = s3jni_jstring_to_utf8(jFuncName,0);
+  if( !zFuncName ){
+    rc = SQLITE_NOMEM;
+    S3JniUdf_free(env, s, 1);
+    goto error_cleanup;
+  }
+  s->zFuncName = zFuncName /* pass on ownership */;
+  if( UDF_WINDOW == s->type ){
+    rc = sqlite3_create_window_function(pDb, zFuncName, nArg, eTextRep, s,
+                                        udf_xStep, udf_xFinal, udf_xValue,
+                                        udf_xInverse, S3JniUdf_finalizer);
+  }else{
+    udf_xFunc_f xFunc = 0;
+    udf_xStep_f xStep = 0;
+    udf_xFinal_f xFinal = 0;
+    if( UDF_SCALAR == s->type ){
+      xFunc = udf_xFunc;
+    }else{
+      assert( UDF_AGGREGATE == s->type );
+      xStep = udf_xStep;
+      xFinal = udf_xFinal;
+    }
+    rc = sqlite3_create_function_v2(pDb, zFuncName, nArg, eTextRep, s,
+                                    xFunc, xStep, xFinal, S3JniUdf_finalizer);
+  }
+error_cleanup:
+  /* Reminder: on sqlite3_create_function() error, s will be
+  ** destroyed via create_function(). */
+  return (jint)rc;
+}
+
+
+S3JniApi(sqlite3_db_config() /*for MAINDBNAME*/,
+         jint,1db_1config__Lorg_sqlite_jni_capi_sqlite3_2ILjava_lang_String_2
+)(JniArgsEnvClass, jobject jDb, jint op, jstring jStr){
+  S3JniDb * const ps = S3JniDb_from_java(jDb);
+  int rc;
+  char *zStr;
+
+  switch( (ps && jStr) ? op : 0 ){
+    case SQLITE_DBCONFIG_MAINDBNAME:
+      S3JniDb_mutex_enter
+        /* Protect against a race in modifying/freeing
+           ps->zMainDbName. */;
+      zStr = s3jni_jstring_to_utf8( jStr, 0);
+      if( zStr ){
+        rc = sqlite3_db_config(ps->pDb, (int)op, zStr);
+        if( rc ){
+          sqlite3_free( zStr );
+        }else{
+          sqlite3_free( ps->zMainDbName );
+          ps->zMainDbName = zStr;
+        }
+      }else{
+        rc = SQLITE_NOMEM;
+      }
+      S3JniDb_mutex_leave;
+      break;
+    case 0:
+    default:
+      rc = SQLITE_MISUSE;
+  }
+  return rc;
+}
+
+S3JniApi(
+  sqlite3_db_config(),
+  /* WARNING: openjdk v19 creates a different mangled name for this
+  ** function than openjdk v8 does. We account for that by exporting
+  ** both versions of the name. */
+  jint,1db_1config__Lorg_sqlite_jni_capi_sqlite3_2IILorg_sqlite_jni_capi_OutputPointer_Int32_2
+)(
+  JniArgsEnvClass, jobject jDb, jint op, jint onOff, jobject jOut
+){
+  S3JniDb * const ps = S3JniDb_from_java(jDb);
+  int rc;
+  switch( ps ? op : 0 ){
+    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: {
+      int pOut = 0;
+      rc = sqlite3_db_config( ps->pDb, (int)op, onOff, &pOut );
+      if( 0==rc && jOut ){
+        OutputPointer_set_Int32(env, jOut, pOut);
+      }
+      break;
+    }
+    case 0:
+    default:
+      rc = SQLITE_MISUSE;
+  }
+  return (jint)rc;
+}
+
+/*
+** This is a workaround for openjdk v19 (and possibly others) encoding
+** this function's name differently than JDK v8 does. If we do not
+** install both names for this function then Java will not be able to
+** find the function in both environments.
+*/
+JniDecl(jint,1db_1config__Lorg_sqlite_jni_capi_sqlite3_2IILorg_sqlite_jni_capi_OutputPointer_00024Int32_2)(
+  JniArgsEnvClass, jobject jDb, jint op, jint onOff, jobject jOut
+){
+  return JniFuncName(1db_1config__Lorg_sqlite_jni_capi_sqlite3_2IILorg_sqlite_jni_capi_OutputPointer_Int32_2)(
+    env, jKlazz, jDb, op, onOff, jOut
+  );
+}
+
+S3JniApi(sqlite3_db_filename(),jstring,1db_1filename)(
+  JniArgsEnvClass, jobject jDb, jstring jDbName
+){
+  S3JniDb * const ps = S3JniDb_from_java(jDb);
+  char *zDbName;
+  jstring jRv = 0;
+  int nStr = 0;
+
+  if( !ps || !jDbName ){
+    return 0;
+  }
+  zDbName = s3jni_jstring_to_utf8( jDbName, &nStr);
+  if( zDbName ){
+    char const * zRv = sqlite3_db_filename(ps->pDb, zDbName);
+    sqlite3_free(zDbName);
+    if( zRv ){
+      jRv = s3jni_utf8_to_jstring( zRv, -1);
+    }
+  }
+  return jRv;
+}
+
+S3JniApi(sqlite3_db_handle(),jobject,1db_1handle)(
+  JniArgsEnvClass, jobject jpStmt
+){
+  sqlite3_stmt * const pStmt = PtrGet_sqlite3_stmt(jpStmt);
+  sqlite3 * const pDb = pStmt ? sqlite3_db_handle(pStmt) : 0;
+  S3JniDb * const ps = pDb ? S3JniDb_from_c(pDb) : 0;
+  return ps ? ps->jDb : 0;
+}
+
+S3JniApi(sqlite3_db_readonly(),jint,1db_1readonly)(
+  JniArgsEnvClass, jobject jDb, jstring jDbName
+){
+  int rc = 0;
+  S3JniDb * const ps = S3JniDb_from_java(jDb);
+  char *zDbName = jDbName ? s3jni_jstring_to_utf8( jDbName, 0 ) : 0;
+  rc = sqlite3_db_readonly(ps ? ps->pDb : 0, zDbName);
+  sqlite3_free(zDbName);
+  return (jint)rc;
+}
+
+S3JniApi(sqlite3_db_release_memory(),int,1db_1release_1memory)(
+  JniArgsEnvClass, jobject jDb
+){
+  sqlite3 * const pDb = PtrGet_sqlite3(jDb);
+  return pDb ? sqlite3_db_release_memory(pDb) : SQLITE_MISUSE;
+}
+
+S3JniApi(sqlite3_db_status(),jint,1db_1status)(
+  JniArgsEnvClass, jobject jDb, jint op, jobject jOutCurrent,
+                        jobject jOutHigh, jboolean reset
+){
+  int iCur = 0, iHigh = 0;
+  sqlite3 * const pDb = PtrGet_sqlite3(jDb);
+  int rc = sqlite3_db_status( pDb, op, &iCur, &iHigh, reset );
+  if( 0==rc ){
+    OutputPointer_set_Int32(env, jOutCurrent, iCur);
+    OutputPointer_set_Int32(env, jOutHigh, iHigh);
+  }
+  return (jint)rc;
+}
+
+S3JniApi(sqlite3_errcode(),jint,1errcode)(
+  JniArgsEnvClass, jobject jpDb
+){
+  sqlite3 * const pDb = PtrGet_sqlite3(jpDb);
+  return pDb ? sqlite3_errcode(pDb) : SQLITE_MISUSE;
+}
+
+S3JniApi(sqlite3_errmsg(),jstring,1errmsg)(
+  JniArgsEnvClass, jobject jpDb
+){
+  sqlite3 * const pDb = PtrGet_sqlite3(jpDb);
+  return pDb ? s3jni_utf8_to_jstring( sqlite3_errmsg(pDb), -1) : 0
+    /* We don't use errmsg16() directly only because it would cause an
+       additional level of internal encoding in sqlite3. The end
+       effect should be identical to using errmsg16(), however. */;
+}
+
+S3JniApi(sqlite3_errstr(),jstring,1errstr)(
+  JniArgsEnvClass, jint rcCode
+){
+  jstring const rv = (*env)->NewStringUTF(env, sqlite3_errstr((int)rcCode))
+    /* We know these values to be plain ASCII, so pose no MUTF-8
+    ** incompatibility */;
+  s3jni_oom_check( rv );
+  return rv;
+}
+
+#ifndef SQLITE_ENABLE_NORMALIZE
+/* Dummy stub for sqlite3_normalized_sql(). Never called. */
+static const char * sqlite3_normalized_sql(sqlite3_stmt *s){
+  S3JniDeclLocal_env;
+  (*env)->FatalError(env, "dummy sqlite3_normalized_sql() was "
+                     "impossibly called.") /* does not return */;
+  return 0;
+}
+#endif
+
+/*
+** Impl for sqlite3_expanded_sql() (if isExpanded is true) and
+** sqlite3_normalized_sql().
+*/
+static jstring s3jni_xn_sql(int isExpanded, JNIEnv *env, jobject jpStmt){
+  jstring rv = 0;
+  sqlite3_stmt * const pStmt = PtrGet_sqlite3_stmt(jpStmt);
+
+  if( pStmt ){
+    char * zSql = isExpanded
+      ? sqlite3_expanded_sql(pStmt)
+      : (char*)sqlite3_normalized_sql(pStmt);
+    s3jni_oom_fatal(zSql);
+    if( zSql ){
+      rv = s3jni_utf8_to_jstring(zSql, -1);
+      if( isExpanded ) sqlite3_free(zSql);
+    }
+  }
+  return rv;
+}
+
+S3JniApi(sqlite3_expanded_sql(),jstring,1expanded_1sql)(
+  JniArgsEnvClass, jobject jpStmt
+){
+  return s3jni_xn_sql(1, env, jpStmt);
+}
+
+S3JniApi(sqlite3_normalized_sql(),jstring,1normalized_1sql)(
+  JniArgsEnvClass, jobject jpStmt
+){
+#ifdef SQLITE_ENABLE_NORMALIZE
+  return s3jni_xn_sql(0, env, jpStmt);
+#else
+  return 0;
+#endif
+}
+
+S3JniApi(sqlite3_extended_result_codes(),jboolean,1extended_1result_1codes)(
+  JniArgsEnvClass, jobject jpDb, jboolean onoff
+){
+  sqlite3 * const pDb = PtrGet_sqlite3(jpDb);
+  int const rc = pDb ? sqlite3_extended_result_codes(pDb, onoff ? 1 : 0) : 0;
+  return rc ? JNI_TRUE : JNI_FALSE;
+}
+
+S3JniApi(sqlite3_finalize(),jint,1finalize)(
+  JniArgsEnvClass, jlong jpStmt
+){
+  return jpStmt
+    ? sqlite3_finalize(S3JniLongPtr_sqlite3_stmt(jpStmt))
+    : 0;
+}
+
+S3JniApi(sqlite3_get_auxdata(),jobject,1get_1auxdata)(
+  JniArgsEnvClass, jobject jCx, jint n
+){
+  return sqlite3_get_auxdata(PtrGet_sqlite3_context(jCx), (int)n);
+}
+
+S3JniApi(sqlite3_initialize(),jint,1initialize)(
+  JniArgsEnvClass
+){
+  return sqlite3_initialize();
+}
+
+S3JniApi(sqlite3_interrupt(),void,1interrupt)(
+  JniArgsEnvClass, jobject jpDb
+){
+  sqlite3 * const pDb = PtrGet_sqlite3(jpDb);
+  if( pDb ){
+    sqlite3_interrupt(pDb);
+  }
+}
+
+S3JniApi(sqlite3_is_interrupted(),jboolean,1is_1interrupted)(
+  JniArgsEnvClass, jobject jpDb
+){
+  int rc = 0;
+  sqlite3 * const pDb = PtrGet_sqlite3(jpDb);
+  if( pDb ){
+    rc = sqlite3_is_interrupted(pDb);
+  }
+  return rc ? JNI_TRUE : JNI_FALSE;
+}
+
+/*
+** Uncaches the current JNIEnv from the S3JniGlobal state, clearing
+** any resources owned by that cache entry and making that slot
+** available for re-use.
+*/
+JniDecl(jboolean,1java_1uncache_1thread)(JniArgsEnvClass){
+  int rc;
+  S3JniEnv_mutex_enter;
+  rc = S3JniEnv_uncache(env);
+  S3JniEnv_mutex_leave;
+  return rc ? JNI_TRUE : JNI_FALSE;
+}
+
+S3JniApi(sqlite3_keyword_check(),jboolean,1keyword_1check)(
+  JniArgsEnvClass, jstring jWord
+){
+  int nWord = 0;
+  char * zWord = s3jni_jstring_to_utf8(jWord, &nWord);
+  int rc = 0;
+
+  s3jni_oom_check(jWord ? !!zWord : 1);
+  if( zWord && nWord ){
+    rc = sqlite3_keyword_check(zWord, nWord);
+  }
+  sqlite3_free(zWord);
+  return rc ? JNI_TRUE : JNI_FALSE;
+}
+
+S3JniApi(sqlite3_keyword_name(),jstring,1keyword_1name)(
+  JniArgsEnvClass, jint ndx
+){
+  const char * zWord = 0;
+  int n = 0;
+  jstring rv = 0;
+
+  if( 0==sqlite3_keyword_name(ndx, &zWord, &n) ){
+    rv = s3jni_utf8_to_jstring(zWord, n);
+  }
+  return rv;
+}
+
+
+S3JniApi(sqlite3_last_insert_rowid(),jlong,1last_1insert_1rowid)(
+  JniArgsEnvClass, jobject jpDb
+){
+  return (jlong)sqlite3_last_insert_rowid(PtrGet_sqlite3(jpDb));
+}
+
+S3JniApi(sqlite3_limit(),jint,1limit)(
+  JniArgsEnvClass, jobject jpDb, jint id, jint newVal
+){
+  jint rc = 0;
+  sqlite3 * const pDb = PtrGet_sqlite3(jpDb);
+  if( pDb ){
+    rc = sqlite3_limit( pDb, (int)id, (int)newVal );
+  }
+  return rc;
+}
+
+/* Pre-open() code common to sqlite3_open[_v2](). */
+static int s3jni_open_pre(JNIEnv * const env, S3JniEnv **jc,
+                          jstring jDbName, char **zDbName,
+                          S3JniDb ** ps){
+  int rc = 0;
+  jobject jDb = 0;
+
+  *jc = S3JniEnv_get();
+  if( !*jc ){
+    rc = SQLITE_NOMEM;
+    goto end;
+  }
+  *zDbName = jDbName ? s3jni_jstring_to_utf8( jDbName, 0) : 0;
+  if( jDbName && !*zDbName ){
+    rc = SQLITE_NOMEM;
+    goto end;
+  }
+  jDb = new_java_sqlite3(env, 0);
+  if( !jDb ){
+    sqlite3_free(*zDbName);
+    *zDbName = 0;
+    rc = SQLITE_NOMEM;
+    goto end;
+  }
+  *ps = S3JniDb_alloc(env, jDb);
+  if( *ps ){
+    (*jc)->pdbOpening = *ps;
+  }else{
+    S3JniUnrefLocal(jDb);
+    rc = SQLITE_NOMEM;
+  }
+end:
+  return rc;
+}
+
+/*
+** Post-open() code common to both the sqlite3_open() and
+** sqlite3_open_v2() bindings. ps->jDb must be the
+** org.sqlite.jni.capi.sqlite3 object which will hold the db's native
+** pointer. theRc must be the result code of the open() op. If
+** *ppDb is NULL then ps is set aside and its state cleared,
+** else ps is associated with *ppDb. If *ppDb is not NULL then
+** ps->jDb is stored in jOut (an OutputPointer.sqlite3 instance).
+**
+** Must be called if s3jni_open_pre() succeeds and must not be called
+** if it doesn't.
+**
+** Returns theRc.
+*/
+static int s3jni_open_post(JNIEnv * const env, S3JniEnv * const jc,
+                           S3JniDb * ps, sqlite3 **ppDb,
+                           jobject jOut, int theRc){
+  int rc = 0;
+  jc->pdbOpening = 0;
+  if( *ppDb ){
+    assert(ps->jDb);
+    if( 0==ps->pDb ){
+      ps->pDb = *ppDb;
+      NativePointerHolder_set(S3JniNph(sqlite3), ps->jDb, *ppDb);
+    }else{
+      assert( ps->pDb==*ppDb
+              && "Set up via s3jni_run_java_auto_extensions()" );
+    }
+    rc = sqlite3_set_clientdata(ps->pDb, S3JniDb_clientdata_key,
+                                ps, S3JniDb_xDestroy)
+      /* As of here, the Java/C connection is complete */;
+  }else{
+    S3JniDb_set_aside(ps);
+    ps = 0;
+  }
+  OutputPointer_set_obj(env, S3JniNph(OutputPointer_sqlite3),
+                        jOut, ps ? ps->jDb : 0);
+  return theRc ? theRc : rc;
+}
+
+S3JniApi(sqlite3_open(),jint,1open)(
+  JniArgsEnvClass, jstring strName, jobject jOut
+){
+  sqlite3 * pOut = 0;
+  char *zName = 0;
+  S3JniDb * ps = 0;
+  S3JniEnv * jc = 0;
+  int rc;
+
+  if( 0==jOut ) return SQLITE_MISUSE;
+  rc = s3jni_open_pre(env, &jc, strName, &zName, &ps);
+  if( 0==rc ){
+    rc = s3jni_open_post(env, jc, ps, &pOut, jOut,
+                         sqlite3_open(zName, &pOut));
+    assert(rc==0 ? pOut!=0 : 1);
+    sqlite3_free(zName);
+  }
+  return (jint)rc;
+}
+
+S3JniApi(sqlite3_open_v2(),jint,1open_1v2)(
+  JniArgsEnvClass, jstring strName,
+  jobject jOut, jint flags, jstring strVfs
+){
+  sqlite3 * pOut = 0;
+  char *zName = 0;
+  S3JniDb * ps = 0;
+  S3JniEnv * jc = 0;
+  char *zVfs = 0;
+  int rc;
+
+  if( 0==jOut ) return SQLITE_MISUSE;
+  rc = s3jni_open_pre(env, &jc, strName, &zName, &ps);
+  if( 0==rc ){
+    if( strVfs ){
+      zVfs = s3jni_jstring_to_utf8( strVfs, 0);
+      if( !zVfs ){
+        rc = SQLITE_NOMEM;
+      }
+    }
+    if( 0==rc ){
+      rc = sqlite3_open_v2(zName, &pOut, (int)flags, zVfs);
+    }
+    rc = s3jni_open_post(env, jc, ps, &pOut, jOut, rc);
+  }
+  assert(rc==0 ? pOut!=0 : 1);
+  sqlite3_free(zName);
+  sqlite3_free(zVfs);
+  return (jint)rc;
+}
+
+/* Proxy for the sqlite3_prepare[_v2/3]() family. */
+jint sqlite3_jni_prepare_v123( int prepVersion, JNIEnv * const env, jclass self,
+                               jlong jpDb, jbyteArray baSql,
+                               jint nMax, jint prepFlags,
+                               jobject jOutStmt, jobject outTail){
+  sqlite3_stmt * pStmt = 0;
+  jobject jStmt = 0;
+  const char * zTail = 0;
+  sqlite3 * const pDb = S3JniLongPtr_sqlite3(jpDb);
+  jbyte * const pBuf = pDb ? s3jni_jbyteArray_bytes(baSql)  : 0;
+  int rc = SQLITE_ERROR;
+
+  assert(prepVersion==1 || prepVersion==2 || prepVersion==3);
+  if( !pDb || !jOutStmt ){
+    rc = SQLITE_MISUSE;
+    goto end;
+  }else if( !pBuf ){
+    rc = baSql ? SQLITE_NOMEM : SQLITE_MISUSE;
+    goto end;
+  }
+  jStmt = new_java_sqlite3_stmt(env, 0);
+  if( !jStmt ){
+    rc = SQLITE_NOMEM;
+    goto end;
+  }
+  switch( prepVersion ){
+    case 1: rc = sqlite3_prepare(pDb, (const char *)pBuf,
+                                 (int)nMax, &pStmt, &zTail);
+      break;
+    case 2: rc = sqlite3_prepare_v2(pDb, (const char *)pBuf,
+                                    (int)nMax, &pStmt, &zTail);
+      break;
+    case 3: rc = sqlite3_prepare_v3(pDb, (const char *)pBuf,
+                                    (int)nMax, (unsigned int)prepFlags,
+                                    &pStmt, &zTail);
+      break;
+    default:
+      assert(!"Invalid prepare() version");
+  }
+end:
+  s3jni_jbyteArray_release(baSql,pBuf);
+  if( 0==rc ){
+    if( 0!=outTail ){
+      /* Noting that pBuf is deallocated now but its address is all we need for
+      ** what follows... */
+      assert(zTail ? ((void*)zTail>=(void*)pBuf) : 1);
+      assert(zTail ? (((int)((void*)zTail - (void*)pBuf)) >= 0) : 1);
+      OutputPointer_set_Int32(
+        env, outTail, (int)(zTail ? (zTail - (const char *)pBuf) : 0)
+      );
+    }
+    if( pStmt ){
+      NativePointerHolder_set(S3JniNph(sqlite3_stmt), jStmt, pStmt);
+    }else{
+      /* Happens for comments and whitespace. */
+      S3JniUnrefLocal(jStmt);
+      jStmt = 0;
+    }
+  }else{
+    S3JniUnrefLocal(jStmt);
+    jStmt = 0;
+  }
+  if( jOutStmt ){
+    OutputPointer_set_obj(env, S3JniNph(OutputPointer_sqlite3_stmt),
+                          jOutStmt, jStmt);
+  }
+  return (jint)rc;
+}
+S3JniApi(sqlite3_prepare(),jint,1prepare)(
+  JNIEnv * const env, jclass self, jlong jpDb, jbyteArray baSql,
+                     jint nMax, jobject jOutStmt, jobject outTail
+){
+  return sqlite3_jni_prepare_v123(1, env, self, jpDb, baSql, nMax, 0,
+                                  jOutStmt, outTail);
+}
+S3JniApi(sqlite3_prepare_v2(),jint,1prepare_1v2)(
+  JNIEnv * const env, jclass self, jlong jpDb, jbyteArray baSql,
+                         jint nMax, jobject jOutStmt, jobject outTail
+){
+  return sqlite3_jni_prepare_v123(2, env, self, jpDb, baSql, nMax, 0,
+                                  jOutStmt, outTail);
+}
+S3JniApi(sqlite3_prepare_v3(),jint,1prepare_1v3)(
+  JNIEnv * const env, jclass self, jlong jpDb, jbyteArray baSql,
+                         jint nMax, jint prepFlags, jobject jOutStmt, jobject outTail
+){
+  return sqlite3_jni_prepare_v123(3, env, self, jpDb, baSql, nMax,
+                                  prepFlags, jOutStmt, outTail);
+}
+
+/*
+** Impl for C-to-Java of the callbacks for both sqlite3_update_hook()
+** and sqlite3_preupdate_hook().  The differences are that for
+** update_hook():
+**
+** - pDb is NULL
+** - iKey1 is the row ID
+** - iKey2 is unused
+*/
+static void s3jni_updatepre_hook_impl(void * pState, sqlite3 *pDb, int opId,
+                                      const char *zDb, const char *zTable,
+                                      sqlite3_int64 iKey1, sqlite3_int64 iKey2){
+  S3JniDb * const ps = pState;
+  S3JniDeclLocal_env;
+  jstring jDbName;
+  jstring jTable;
+  const int isPre = 0!=pDb;
+  S3JniHook hook;
+
+  S3JniHook_localdup(isPre ?
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+                 &ps->hooks.preUpdate
+#else
+                 &S3JniHook_empty
+#endif
+                 : &ps->hooks.update, &hook);
+  if( !hook.jObj ){
+    return;
+  }
+  jDbName  = s3jni_utf8_to_jstring( zDb, -1);
+  jTable = jDbName ? s3jni_utf8_to_jstring( zTable, -1) : 0;
+  S3JniIfThrew {
+    S3JniExceptionClear;
+    s3jni_db_error(ps->pDb, SQLITE_NOMEM, 0);
+  }else{
+    assert( hook.jObj );
+    assert( hook.midCallback );
+    assert( ps->jDb );
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+    if( isPre ) (*env)->CallVoidMethod(env, hook.jObj, hook.midCallback,
+                                       ps->jDb, (jint)opId, jDbName, jTable,
+                                       (jlong)iKey1, (jlong)iKey2);
+    else
+#endif
+    (*env)->CallVoidMethod(env, hook.jObj, hook.midCallback,
+                           (jint)opId, jDbName, jTable, (jlong)iKey1);
+    S3JniIfThrew{
+      S3JniExceptionWarnCallbackThrew("sqlite3_(pre)update_hook() callback");
+      s3jni_db_exception(ps->pDb, 0,
+                         "sqlite3_(pre)update_hook() callback threw");
+    }
+  }
+  S3JniUnrefLocal(jDbName);
+  S3JniUnrefLocal(jTable);
+  S3JniHook_localundup(hook);
+}
+
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+static void s3jni_preupdate_hook_impl(void * pState, sqlite3 *pDb, int opId,
+                                      const char *zDb, const char *zTable,
+                                      sqlite3_int64 iKey1, sqlite3_int64 iKey2){
+  return s3jni_updatepre_hook_impl(pState, pDb, opId, zDb, zTable,
+                                   iKey1, iKey2);
+}
+#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */
+
+static void s3jni_update_hook_impl(void * pState, int opId, const char *zDb,
+                                   const char *zTable, sqlite3_int64 nRowid){
+  return s3jni_updatepre_hook_impl(pState, NULL, opId, zDb, zTable, nRowid, 0);
+}
+
+#if !defined(SQLITE_ENABLE_PREUPDATE_HOOK)
+/* We need no-op impls for preupdate_{count,depth,blobwrite}() */
+S3JniApi(sqlite3_preupdate_blobwrite(),int,1preupdate_1blobwrite)(
+  JniArgsEnvClass, jlong jDb){ return SQLITE_MISUSE; }
+S3JniApi(sqlite3_preupdate_count(),int,1preupdate_1count)(
+  JniArgsEnvClass, jlong jDb){ return SQLITE_MISUSE; }
+S3JniApi(sqlite3_preupdate_depth(),int,1preupdate_1depth)(
+  JniArgsEnvClass, jlong jDb){ return SQLITE_MISUSE; }
+#endif /* !SQLITE_ENABLE_PREUPDATE_HOOK */
+
+/*
+** JNI wrapper for both sqlite3_update_hook() and
+** sqlite3_preupdate_hook() (if isPre is true).
+*/
+static jobject s3jni_updatepre_hook(JNIEnv * env, int isPre, jlong jpDb, jobject jHook){
+  S3JniDb * const ps = S3JniDb_from_jlong(jpDb);
+  jclass klazz;
+  jobject pOld = 0;
+  jmethodID xCallback;
+  S3JniHook * pHook;
+
+  if( !ps ) return 0;
+  S3JniDb_mutex_enter;
+  pHook = isPre ?
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+    &ps->hooks.preUpdate
+#else
+    0
+#endif
+    : &ps->hooks.update;
+  if( !pHook ){
+    goto end;
+  }
+  pOld = pHook->jObj;
+  if( pOld && jHook && (*env)->IsSameObject(env, pOld, jHook) ){
+    goto end;
+  }
+  if( !jHook ){
+    if( pOld ){
+      jobject tmp = S3JniRefLocal(pOld);
+      S3JniUnrefGlobal(pOld);
+      pOld = tmp;
+    }
+    *pHook = S3JniHook_empty;
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+    if( isPre ) sqlite3_preupdate_hook(ps->pDb, 0, 0);
+    else
+#endif
+    sqlite3_update_hook(ps->pDb, 0, 0);
+    goto end;
+  }
+  klazz = (*env)->GetObjectClass(env, jHook);
+  xCallback = isPre
+    ? (*env)->GetMethodID(env, klazz, "call",
+                          "(Lorg/sqlite/jni/capi/sqlite3;"
+                          "I"
+                          "Ljava/lang/String;"
+                          "Ljava/lang/String;"
+                          "JJ)V")
+    : (*env)->GetMethodID(env, klazz, "call",
+                          "(ILjava/lang/String;Ljava/lang/String;J)V");
+  S3JniUnrefLocal(klazz);
+  S3JniIfThrew {
+    S3JniExceptionClear;
+    s3jni_db_error(ps->pDb, SQLITE_ERROR,
+                   "Cannot not find matching callback on "
+                   "(pre)update hook object.");
+  }else{
+    pHook->midCallback = xCallback;
+    pHook->jObj = S3JniRefGlobal(jHook);
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+    if( isPre ) sqlite3_preupdate_hook(ps->pDb, s3jni_preupdate_hook_impl, ps);
+    else
+#endif
+    sqlite3_update_hook(ps->pDb, s3jni_update_hook_impl, ps);
+    if( pOld ){
+      jobject tmp = S3JniRefLocal(pOld);
+      S3JniUnrefGlobal(pOld);
+      pOld = tmp;
+    }
+  }
+end:
+  S3JniDb_mutex_leave;
+  return pOld;
+}
+
+
+S3JniApi(sqlite3_preupdate_hook(),jobject,1preupdate_1hook)(
+  JniArgsEnvClass, jlong jpDb, jobject jHook
+){
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+  return s3jni_updatepre_hook(env, 1, jpDb, jHook);
+#else
+  return NULL;
+#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */
+}
+
+/* Impl for sqlite3_preupdate_{new,old}(). */
+static int s3jni_preupdate_newold(JNIEnv * const env, int isNew, jlong jpDb,
+                                  jint iCol, jobject jOut){
+#ifdef SQLITE_ENABLE_PREUPDATE_HOOK
+  sqlite3 * const pDb = S3JniLongPtr_sqlite3(jpDb);
+  int rc = SQLITE_MISUSE;
+  if( pDb ){
+    sqlite3_value * pOut = 0;
+    int (*fOrig)(sqlite3*,int,sqlite3_value**) =
+      isNew ? sqlite3_preupdate_new : sqlite3_preupdate_old;
+    rc = fOrig(pDb, (int)iCol, &pOut);
+    if( 0==rc ){
+      jobject pWrap = new_java_sqlite3_value(env, pOut);
+      if( !pWrap ){
+        rc = SQLITE_NOMEM;
+      }
+      OutputPointer_set_obj(env, S3JniNph(OutputPointer_sqlite3_value),
+                            jOut, pWrap);
+      S3JniUnrefLocal(pWrap);
+    }
+  }
+  return rc;
+#else
+  return SQLITE_MISUSE;
+#endif
+}
+
+S3JniApi(sqlite3_preupdate_new(),jint,1preupdate_1new)(
+  JniArgsEnvClass, jlong jpDb, jint iCol, jobject jOut
+){
+  return s3jni_preupdate_newold(env, 1, jpDb, iCol, jOut);
+}
+
+S3JniApi(sqlite3_preupdate_old(),jint,1preupdate_1old)(
+  JniArgsEnvClass, jlong jpDb, jint iCol, jobject jOut
+){
+  return s3jni_preupdate_newold(env, 0, jpDb, iCol, jOut);
+}
+
+
+/* Central C-to-Java sqlite3_progress_handler() proxy. */
+static int s3jni_progress_handler_impl(void *pP){
+  S3JniDb * const ps = (S3JniDb *)pP;
+  int rc = 0;
+  S3JniDeclLocal_env;
+  S3JniHook hook;
+
+  S3JniHook_localdup(&ps->hooks.progress, &hook);
+  if( hook.jObj ){
+    rc = (int)(*env)->CallIntMethod(env, hook.jObj, hook.midCallback);
+    S3JniIfThrew{
+      rc = s3jni_db_exception(ps->pDb, rc,
+                              "sqlite3_progress_handler() callback threw");
+    }
+    S3JniHook_localundup(hook);
+  }
+  return rc;
+}
+
+S3JniApi(sqlite3_progress_handler(),void,1progress_1handler)(
+  JniArgsEnvClass,jobject jDb, jint n, jobject jProgress
+){
+  S3JniDb * const ps = S3JniDb_from_java(jDb);
+  S3JniHook * const pHook = ps ? &ps->hooks.progress : 0;
+
+  if( !ps ) return;
+  S3JniDb_mutex_enter;
+  if( n<1 || !jProgress ){
+    S3JniHook_unref(pHook);
+    sqlite3_progress_handler(ps->pDb, 0, 0, 0);
+  }else{
+    jclass const klazz = (*env)->GetObjectClass(env, jProgress);
+    jmethodID const xCallback = (*env)->GetMethodID(env, klazz, "call", "()I");
+    S3JniUnrefLocal(klazz);
+    S3JniIfThrew {
+      S3JniExceptionClear;
+      s3jni_db_error(ps->pDb, SQLITE_ERROR,
+                     "Cannot not find matching xCallback() on "
+                     "ProgressHandler object.");
+    }else{
+      S3JniUnrefGlobal(pHook->jObj);
+      pHook->midCallback = xCallback;
+      pHook->jObj = S3JniRefGlobal(jProgress);
+      sqlite3_progress_handler(ps->pDb, (int)n, s3jni_progress_handler_impl, ps);
+    }
+  }
+  S3JniDb_mutex_leave;
+}
+
+S3JniApi(sqlite3_randomness(),void,1randomness)(
+  JniArgsEnvClass, jbyteArray jTgt
+){
+  jbyte * const jba = s3jni_jbyteArray_bytes(jTgt);
+  if( jba ){
+    jsize const nTgt = (*env)->GetArrayLength(env, jTgt);
+    sqlite3_randomness( (int)nTgt, jba );
+    s3jni_jbyteArray_commit(jTgt, jba);
+  }
+}
+
+
+S3JniApi(sqlite3_reset(),jint,1reset)(
+  JniArgsEnvClass, jobject jpStmt
+){
+  sqlite3_stmt * const pStmt = PtrGet_sqlite3_stmt(jpStmt);
+  return pStmt ? sqlite3_reset(pStmt) : SQLITE_MISUSE;
+}
+
+/* Clears all entries from S3JniGlobal.autoExt. */
+static void s3jni_reset_auto_extension(JNIEnv *env){
+  int i;
+  S3JniAutoExt_mutex_enter;
+  for( i = 0; i < SJG.autoExt.nExt; ++i ){
+    S3JniAutoExtension_clear( &SJG.autoExt.aExt[i] );
+  }
+  SJG.autoExt.nExt = 0;
+  S3JniAutoExt_mutex_leave;
+}
+
+S3JniApi(sqlite3_reset_auto_extension(),void,1reset_1auto_1extension)(
+  JniArgsEnvClass
+){
+  s3jni_reset_auto_extension(env);
+}
+
+/* Impl for sqlite3_result_text/blob() and friends. */
+static void result_blob_text(int as64     /* true for text64/blob64() mode */,
+                             int eTextRep /* 0 for blobs, else SQLITE_UTF... */,
+                             JNIEnv * const env, sqlite3_context *pCx,
+                             jbyteArray jBa, jlong nMax){
+  int const asBlob = 0==eTextRep;
+  if( !pCx ){
+    /* We should arguably emit a warning here. But where to log it? */
+    return;
+  }else if( jBa ){
+    jbyte * const pBuf = s3jni_jbyteArray_bytes(jBa);
+    jsize nBA = (*env)->GetArrayLength(env, jBa);
+    if( nMax>=0 && nBA>(jsize)nMax ){
+      nBA = (jsize)nMax;
+      /**
+         From the sqlite docs:
+
+         > If the 3rd parameter to any of the sqlite3_result_text*
+           interfaces other than sqlite3_result_text64() is negative,
+           then SQLite computes the string length itself by searching
+           the 2nd parameter for the first zero character.
+
+         Note that the text64() interfaces take an unsigned value for
+         the length, which Java does not support. This binding takes
+         the approach of passing on negative values to the C API,
+         which will in turn fail with SQLITE_TOOBIG at some later
+         point (recall that the sqlite3_result_xyz() family do not
+         have result values).
+      */
+    }
+    if( as64 ){ /* 64-bit... */
+      static const jsize nLimit64 =
+        SQLITE_MAX_ALLOCATION_SIZE/*only _kinda_ arbitrary*/;
+      if( nBA > nLimit64 ){
+        sqlite3_result_error_toobig(pCx);
+      }else if( asBlob ){
+        sqlite3_result_blob64(pCx, pBuf, (sqlite3_uint64)nBA,
+                              SQLITE_TRANSIENT);
+      }else{ /* text64... */
+        if( encodingTypeIsValid(eTextRep) ){
+          sqlite3_result_text64(pCx, (const char *)pBuf,
+                                (sqlite3_uint64)nBA,
+                                SQLITE_TRANSIENT, eTextRep);
+        }else{
+          sqlite3_result_error_code(pCx, SQLITE_FORMAT);
+        }
+      }
+    }else{ /* 32-bit... */
+      static const jsize nLimit = SQLITE_MAX_ALLOCATION_SIZE;
+      if( nBA > nLimit ){
+        sqlite3_result_error_toobig(pCx);
+      }else if( asBlob ){
+        sqlite3_result_blob(pCx, pBuf, (int)nBA,
+                            SQLITE_TRANSIENT);
+      }else{
+        switch( eTextRep ){
+          case SQLITE_UTF8:
+            sqlite3_result_text(pCx, (const char *)pBuf, (int)nBA,
+                                SQLITE_TRANSIENT);
+            break;
+          case SQLITE_UTF16:
+            sqlite3_result_text16(pCx, (const char *)pBuf, (int)nBA,
+                                  SQLITE_TRANSIENT);
+            break;
+          case SQLITE_UTF16LE:
+            sqlite3_result_text16le(pCx, (const char *)pBuf, (int)nBA,
+                                    SQLITE_TRANSIENT);
+            break;
+          case SQLITE_UTF16BE:
+            sqlite3_result_text16be(pCx, (const char *)pBuf, (int)nBA,
+                                    SQLITE_TRANSIENT);
+            break;
+        }
+      }
+      s3jni_jbyteArray_release(jBa, pBuf);
+    }
+  }else{
+    sqlite3_result_null(pCx);
+  }
+}
+
+S3JniApi(sqlite3_result_blob(),void,1result_1blob)(
+  JniArgsEnvClass, jobject jpCx, jbyteArray jBa, jint nMax
+){
+  return result_blob_text(0, 0, env, PtrGet_sqlite3_context(jpCx), jBa, nMax);
+}
+
+S3JniApi(sqlite3_result_blob64(),void,1result_1blob64)(
+  JniArgsEnvClass, jobject jpCx, jbyteArray jBa, jlong nMax
+){
+  return result_blob_text(1, 0, env, PtrGet_sqlite3_context(jpCx), jBa, nMax);
+}
+
+S3JniApi(sqlite3_result_double(),void,1result_1double)(
+  JniArgsEnvClass, jobject jpCx, jdouble v
+){
+  sqlite3_result_double(PtrGet_sqlite3_context(jpCx), v);
+}
+
+S3JniApi(sqlite3_result_error(),void,1result_1error)(
+  JniArgsEnvClass, jobject jpCx, jbyteArray baMsg, int eTextRep
+){
+  const char * zUnspecified = "Unspecified error.";
+  jsize const baLen = (*env)->GetArrayLength(env, baMsg);
+  jbyte * const pjBuf = baMsg ? s3jni_jbyteArray_bytes(baMsg) : NULL;
+  switch( pjBuf ? eTextRep : SQLITE_UTF8 ){
+    case SQLITE_UTF8: {
+      const char *zMsg = pjBuf ? (const char *)pjBuf : zUnspecified;
+      int const n = pjBuf ? (int)baLen : (int)sqlite3Strlen30(zMsg);
+      sqlite3_result_error(PtrGet_sqlite3_context(jpCx), zMsg, n);
+      break;
+    }
+    case SQLITE_UTF16: {
+      const void *zMsg = pjBuf;
+      sqlite3_result_error16(PtrGet_sqlite3_context(jpCx), zMsg, (int)baLen);
+      break;
+    }
+    default:
+      sqlite3_result_error(PtrGet_sqlite3_context(jpCx),
+                           "Invalid encoding argument passed "
+                           "to sqlite3_result_error().", -1);
+      break;
+  }
+  s3jni_jbyteArray_release(baMsg,pjBuf);
+}
+
+S3JniApi(sqlite3_result_error_code(),void,1result_1error_1code)(
+  JniArgsEnvClass, jobject jpCx, jint v
+){
+  sqlite3_result_error_code(PtrGet_sqlite3_context(jpCx), (int)v);
+}
+
+S3JniApi(sqlite3_result_error_nomem(),void,1result_1error_1nomem)(
+  JniArgsEnvClass, jobject jpCx
+){
+  sqlite3_result_error_nomem(PtrGet_sqlite3_context(jpCx));
+}
+
+S3JniApi(sqlite3_result_error_toobig(),void,1result_1error_1toobig)(
+  JniArgsEnvClass, jobject jpCx
+){
+  sqlite3_result_error_toobig(PtrGet_sqlite3_context(jpCx));
+}
+
+S3JniApi(sqlite3_result_int(),void,1result_1int)(
+  JniArgsEnvClass, jobject jpCx, jint v
+){
+  sqlite3_result_int(PtrGet_sqlite3_context(jpCx), (int)v);
+}
+
+S3JniApi(sqlite3_result_int64(),void,1result_1int64)(
+  JniArgsEnvClass, jobject jpCx, jlong v
+){
+  sqlite3_result_int64(PtrGet_sqlite3_context(jpCx), (sqlite3_int64)v);
+}
+
+S3JniApi(sqlite3_result_java_object(),void,1result_1java_1object)(
+  JniArgsEnvClass, jobject jpCx, jobject v
+){
+  sqlite3_context * pCx = PtrGet_sqlite3_context(jpCx);
+  if( !pCx ) return;
+  else if( v ){
+    jobject const rjv = S3JniRefGlobal(v);
+    if( rjv ){
+      sqlite3_result_pointer(pCx, rjv,
+                             ResultJavaValuePtrStr, S3Jni_jobject_finalizer);
+    }else{
+      sqlite3_result_error_nomem(PtrGet_sqlite3_context(jpCx));
+    }
+  }else{
+    sqlite3_result_null(PtrGet_sqlite3_context(jpCx));
+  }
+}
+
+S3JniApi(sqlite3_result_null(),void,1result_1null)(
+  JniArgsEnvClass, jobject jpCx
+){
+  sqlite3_result_null(PtrGet_sqlite3_context(jpCx));
+}
+
+S3JniApi(sqlite3_result_text(),void,1result_1text)(
+  JniArgsEnvClass, jobject jpCx, jbyteArray jBa, jint nMax
+){
+  return result_blob_text(0, SQLITE_UTF8, env,
+                          PtrGet_sqlite3_context(jpCx), jBa, nMax);
+}
+
+S3JniApi(sqlite3_result_text64(),void,1result_1text64)(
+  JniArgsEnvClass, jobject jpCx, jbyteArray jBa, jlong nMax,
+                            jint eTextRep
+){
+  return result_blob_text(1, eTextRep, env,
+                          PtrGet_sqlite3_context(jpCx), jBa, nMax);
+}
+
+S3JniApi(sqlite3_result_value(),void,1result_1value)(
+  JniArgsEnvClass, jobject jpCx, jobject jpSVal
+){
+  sqlite3_result_value(PtrGet_sqlite3_context(jpCx),
+                       PtrGet_sqlite3_value(jpSVal));
+}
+
+S3JniApi(sqlite3_result_zeroblob(),void,1result_1zeroblob)(
+  JniArgsEnvClass, jobject jpCx, jint v
+){
+  sqlite3_result_zeroblob(PtrGet_sqlite3_context(jpCx), (int)v);
+}
+
+S3JniApi(sqlite3_result_zeroblob64(),jint,1result_1zeroblob64)(
+  JniArgsEnvClass, jobject jpCx, jlong v
+){
+  return (jint)sqlite3_result_zeroblob64(PtrGet_sqlite3_context(jpCx),
+                                         (sqlite3_int64)v);
+}
+
+S3JniApi(sqlite3_rollback_hook(),jobject,1rollback_1hook)(
+  JniArgsEnvClass, jlong jpDb, jobject jHook
+){
+  return s3jni_commit_rollback_hook(0, env, jpDb, jHook);
+}
+
+/* Callback for sqlite3_set_authorizer(). */
+int s3jni_xAuth(void* pState, int op,const char*z0, const char*z1,
+                const char*z2,const char*z3){
+  S3JniDb * const ps = pState;
+  S3JniDeclLocal_env;
+  S3JniHook hook;
+  int rc = 0;
+
+  S3JniHook_localdup(&ps->hooks.auth, &hook );
+  if( hook.jObj ){
+    jstring const s0 = z0 ? s3jni_utf8_to_jstring( z0, -1) : 0;
+    jstring const s1 = z1 ? s3jni_utf8_to_jstring( z1, -1) : 0;
+    jstring const s2 = z2 ? s3jni_utf8_to_jstring( z2, -1) : 0;
+    jstring const s3 = z3 ? s3jni_utf8_to_jstring( z3, -1) : 0;
+
+    rc = (*env)->CallIntMethod(env, hook.jObj, hook.midCallback, (jint)op,
+                               s0, s1, s3, s3);
+    S3JniIfThrew{
+      rc = s3jni_db_exception(ps->pDb, rc, "sqlite3_set_authorizer() callback");
+    }
+    S3JniUnrefLocal(s0);
+    S3JniUnrefLocal(s1);
+    S3JniUnrefLocal(s2);
+    S3JniUnrefLocal(s3);
+    S3JniHook_localundup(hook);
+  }
+  return rc;
+}
+
+S3JniApi(sqlite3_set_authorizer(),jint,1set_1authorizer)(
+  JniArgsEnvClass,jobject jDb, jobject jHook
+){
+  S3JniDb * const ps = S3JniDb_from_java(jDb);
+  S3JniHook * const pHook = ps ? &ps->hooks.auth : 0;
+  int rc = 0;
+
+  if( !ps ) return SQLITE_MISUSE;
+  S3JniDb_mutex_enter;
+  if( !jHook ){
+    S3JniHook_unref(pHook);
+    rc = sqlite3_set_authorizer( ps->pDb, 0, 0 );
+  }else{
+    jclass klazz;
+    if( pHook->jObj ){
+      if( (*env)->IsSameObject(env, pHook->jObj, jHook) ){
+      /* Same object - this is a no-op. */
+        S3JniDb_mutex_leave;
+        return 0;
+      }
+      S3JniHook_unref(pHook);
+    }
+    pHook->jObj = S3JniRefGlobal(jHook);
+    klazz = (*env)->GetObjectClass(env, jHook);
+    pHook->midCallback = (*env)->GetMethodID(env, klazz,
+                                             "call",
+                                             "(I"
+                                             "Ljava/lang/String;"
+                                             "Ljava/lang/String;"
+                                             "Ljava/lang/String;"
+                                             "Ljava/lang/String;"
+                                             ")I");
+    S3JniUnrefLocal(klazz);
+    S3JniIfThrew {
+      rc = s3jni_db_error(ps->pDb, SQLITE_ERROR,
+                          "Error setting up Java parts of authorizer hook.");
+    }else{
+      rc = sqlite3_set_authorizer(ps->pDb, s3jni_xAuth, ps);
+    }
+    if( rc ) S3JniHook_unref(pHook);
+  }
+  S3JniDb_mutex_leave;
+  return rc;
+}
+
+S3JniApi(sqlite3_set_auxdata(),void,1set_1auxdata)(
+  JniArgsEnvClass, jobject jCx, jint n, jobject jAux
+){
+  sqlite3_set_auxdata(PtrGet_sqlite3_context(jCx), (int)n,
+                      S3JniRefGlobal(jAux), S3Jni_jobject_finalizer);
+}
+
+S3JniApi(sqlite3_set_last_insert_rowid(),void,1set_1last_1insert_1rowid)(
+  JniArgsEnvClass, jobject jpDb, jlong rowId
+){
+  sqlite3_set_last_insert_rowid(PtrGet_sqlite3(jpDb),
+                                (sqlite3_int64)rowId);
+}
+
+S3JniApi(sqlite3_shutdown(),jint,1shutdown)(
+  JniArgsEnvClass
+){
+  s3jni_reset_auto_extension(env);
+#ifdef SQLITE_ENABLE_SQLLOG
+  S3JniHook_unref(&SJG.hook.sqllog);
+#endif
+  S3JniHook_unref(&SJG.hook.configlog);
+  /* Free up S3JniDb recycling bin. */
+  S3JniDb_mutex_enter; {
+    while( S3JniGlobal.perDb.aFree ){
+      S3JniDb * const d = S3JniGlobal.perDb.aFree;
+      S3JniGlobal.perDb.aFree = d->pNext;
+      S3JniDb_clear(env, d);
+      sqlite3_free(d);
+    }
+  } S3JniDb_mutex_leave;
+  S3JniGlobal_mutex_enter; {
+    /* Free up S3JniUdf recycling bin. */
+    while( S3JniGlobal.udf.aFree ){
+      S3JniUdf * const u = S3JniGlobal.udf.aFree;
+      S3JniGlobal.udf.aFree = u->pNext;
+      u->pNext = 0;
+      S3JniUdf_free(env, u, 0);
+    }
+  } S3JniGlobal_mutex_leave;
+  S3JniHook_mutex_enter; {
+    /* Free up S3JniHook recycling bin. */
+    while( S3JniGlobal.hook.aFree ){
+      S3JniHook * const u = S3JniGlobal.hook.aFree;
+      S3JniGlobal.hook.aFree = u->pNext;
+      u->pNext = 0;
+      assert( !u->doXDestroy );
+      assert( !u->jObj );
+      assert( !u->jExtra );
+      sqlite3_free( u );
+    }
+  } S3JniHook_mutex_leave;
+  /* Free up env cache. */
+  S3JniEnv_mutex_enter; {
+    while( SJG.envCache.aHead ){
+      S3JniEnv_uncache( SJG.envCache.aHead->env );
+    }
+  } S3JniEnv_mutex_leave;
+#if 0
+  /*
+  ** Is automatically closing any still-open dbs a good idea? We will
+  ** get rid of the perDb list once sqlite3 gets a per-db client
+  ** state, at which point we won't have a central list of databases
+  ** to close.
+  */
+  S3JniDb_mutex_enter;
+  while( SJG.perDb.pHead ){
+    s3jni_close_db(env, SJG.perDb.pHead->jDb, 2);
+  }
+  S3JniDb_mutex_leave;
+#endif
+  /* Do not clear S3JniGlobal.jvm: it's legal to restart the lib. */
+  return sqlite3_shutdown();
+}
+
+S3JniApi(sqlite3_status(),jint,1status)(
+  JniArgsEnvClass, jint op, jobject jOutCurrent, jobject jOutHigh,
+                    jboolean reset
+){
+  int iCur = 0, iHigh = 0;
+  int rc = sqlite3_status( op, &iCur, &iHigh, reset );
+  if( 0==rc ){
+    OutputPointer_set_Int32(env, jOutCurrent, iCur);
+    OutputPointer_set_Int32(env, jOutHigh, iHigh);
+  }
+  return (jint)rc;
+}
+
+S3JniApi(sqlite3_status64(),jint,1status64)(
+  JniArgsEnvClass, jint op, jobject jOutCurrent, jobject jOutHigh,
+                      jboolean reset
+){
+  sqlite3_int64 iCur = 0, iHigh = 0;
+  int rc = sqlite3_status64( op, &iCur, &iHigh, reset );
+  if( 0==rc ){
+    OutputPointer_set_Int64(env, jOutCurrent, iCur);
+    OutputPointer_set_Int64(env, jOutHigh, iHigh);
+  }
+  return (jint)rc;
+}
+
+S3JniApi(sqlite3_stmt_status(),jint,1stmt_1status)(
+  JniArgsEnvClass, jobject jStmt, jint op, jboolean reset
+){
+  return sqlite3_stmt_status(PtrGet_sqlite3_stmt(jStmt),
+                             (int)op, reset ? 1 : 0);
+}
+
+
+static int s3jni_strlike_glob(int isLike, JNIEnv *const env,
+                              jbyteArray baG, jbyteArray baT, jint escLike){
+  int rc = 0;
+  jbyte * const pG = s3jni_jbyteArray_bytes(baG);
+  jbyte * const pT = pG ? s3jni_jbyteArray_bytes(baT) : 0;
+
+  /* Note that we're relying on the byte arrays having been
+     NUL-terminated on the Java side. */
+  rc = isLike
+    ? sqlite3_strlike((const char *)pG, (const char *)pT,
+                      (unsigned int)escLike)
+    : sqlite3_strglob((const char *)pG, (const char *)pT);
+  s3jni_jbyteArray_release(baG, pG);
+  s3jni_jbyteArray_release(baT, pT);
+  return rc;
+}
+
+S3JniApi(sqlite3_strglob(),jint,1strglob)(
+  JniArgsEnvClass, jbyteArray baG, jbyteArray baT
+){
+  return s3jni_strlike_glob(0, env, baG, baT, 0);
+}
+
+S3JniApi(sqlite3_strlike(),jint,1strlike)(
+  JniArgsEnvClass, jbyteArray baG, jbyteArray baT, jint escChar
+){
+  return s3jni_strlike_glob(1, env, baG, baT, escChar);
+}
+
+S3JniApi(sqlite3_sql(),jstring,1sql)(
+  JniArgsEnvClass, jobject jpStmt
+){
+  sqlite3_stmt * const pStmt = PtrGet_sqlite3_stmt(jpStmt);
+  jstring rv = 0;
+  if( pStmt ){
+    const char * zSql = 0;
+    zSql = sqlite3_sql(pStmt);
+    rv = s3jni_utf8_to_jstring( zSql, -1);
+  }
+  return rv;
+}
+
+S3JniApi(sqlite3_step(),jint,1step)(
+  JniArgsEnvClass,jobject jStmt
+){
+  sqlite3_stmt * const pStmt = PtrGet_sqlite3_stmt(jStmt);
+  return pStmt ? (jint)sqlite3_step(pStmt) : (jint)SQLITE_MISUSE;
+}
+
+S3JniApi(sqlite3_table_column_metadata(),int,1table_1column_1metadata)(
+  JniArgsEnvClass, jobject jDb, jstring jDbName, jstring jTableName,
+  jstring jColumnName, jobject jDataType, jobject jCollSeq, jobject jNotNull,
+  jobject jPrimaryKey, jobject jAutoinc
+){
+  sqlite3 * const db = PtrGet_sqlite3(jDb);
+  char * zDbName = 0, * zTableName = 0, * zColumnName = 0;
+  const char * pzCollSeq = 0;
+  const char * pzDataType = 0;
+  int pNotNull = 0, pPrimaryKey = 0, pAutoinc = 0;
+  int rc;
+
+  if( !db || !jDbName || !jTableName ) return SQLITE_MISUSE;
+  zDbName = s3jni_jstring_to_utf8(jDbName,0);
+  zTableName = zDbName ? s3jni_jstring_to_utf8(jTableName,0) : 0;
+  zColumnName = (zTableName && jColumnName)
+    ? s3jni_jstring_to_utf8(jColumnName,0) : 0;
+  rc = zTableName
+    ? sqlite3_table_column_metadata(db, zDbName, zTableName,
+                                    zColumnName, &pzDataType, &pzCollSeq,
+                                    &pNotNull, &pPrimaryKey, &pAutoinc)
+    : SQLITE_NOMEM;
+  if( 0==rc ){
+    jstring jseq = jCollSeq
+      ? (pzCollSeq ? s3jni_utf8_to_jstring(pzCollSeq, -1) : 0)
+      : 0;
+    jstring jdtype = jDataType
+      ? (pzDataType ? s3jni_utf8_to_jstring(pzDataType, -1) : 0)
+      : 0;
+    if( (jCollSeq && pzCollSeq && !jseq)
+        || (jDataType && pzDataType && !jdtype) ){
+      rc = SQLITE_NOMEM;
+    }else{
+      if( jNotNull ) OutputPointer_set_Bool(env, jNotNull, pNotNull);
+      if( jPrimaryKey ) OutputPointer_set_Bool(env, jPrimaryKey, pPrimaryKey);
+      if( jAutoinc ) OutputPointer_set_Bool(env, jAutoinc, pAutoinc);
+      if( jCollSeq ) OutputPointer_set_String(env, jCollSeq, jseq);
+      if( jDataType ) OutputPointer_set_String(env, jDataType, jdtype);
+    }
+    S3JniUnrefLocal(jseq);
+    S3JniUnrefLocal(jdtype);
+  }
+  sqlite3_free(zDbName);
+  sqlite3_free(zTableName);
+  sqlite3_free(zColumnName);
+  return rc;
+}
+
+static int s3jni_trace_impl(unsigned traceflag, void *pC, void *pP, void *pX){
+  S3JniDb * const ps = (S3JniDb *)pC;
+  S3JniDeclLocal_env;
+  jobject jX = NULL  /* the tracer's X arg */;
+  jobject jP = NULL  /* the tracer's P arg */;
+  jobject jPUnref = NULL /* potentially a local ref to jP */;
+  int rc = 0;
+  S3JniHook hook;
+
+  S3JniHook_localdup(&ps->hooks.trace, &hook );
+  if( !hook.jObj ){
+    return 0;
+  }
+  switch( traceflag ){
+    case SQLITE_TRACE_STMT:
+      jX = s3jni_utf8_to_jstring( (const char *)pX, -1);
+      if( !jX ) rc = SQLITE_NOMEM;
+      break;
+    case SQLITE_TRACE_PROFILE:
+      jX = (*env)->NewObject(env, SJG.g.cLong, SJG.g.ctorLong1,
+                             (jlong)*((sqlite3_int64*)pX));
+      // hmm. ^^^ (*pX) really is zero.
+      // MARKER(("profile time = %llu\n", *((sqlite3_int64*)pX)));
+      s3jni_oom_check( jX );
+      if( !jX ) rc = SQLITE_NOMEM;
+      break;
+    case SQLITE_TRACE_ROW:
+      break;
+    case SQLITE_TRACE_CLOSE:
+      jP = jPUnref = S3JniRefLocal(ps->jDb);
+      break;
+    default:
+      assert(!"cannot happen - unknown trace flag");
+      rc =  SQLITE_ERROR;
+  }
+  if( 0==rc ){
+    if( !jP ){
+      /* Create a new temporary sqlite3_stmt wrapper */
+      jP = jPUnref = new_java_sqlite3_stmt(env, pP);
+      if( !jP ){
+        rc = SQLITE_NOMEM;
+      }
+    }
+    if( 0==rc ){
+      assert(jP);
+      rc = (int)(*env)->CallIntMethod(env, hook.jObj, hook.midCallback,
+                                      (jint)traceflag, jP, jX);
+      S3JniIfThrew{
+        rc = s3jni_db_exception(ps->pDb, SQLITE_ERROR,
+                                "sqlite3_trace_v2() callback threw.");
+      }
+    }
+  }
+  S3JniUnrefLocal(jPUnref);
+  S3JniUnrefLocal(jX);
+  S3JniHook_localundup(hook);
+  return rc;
+}
+
+S3JniApi(sqlite3_trace_v2(),jint,1trace_1v2)(
+  JniArgsEnvClass,jobject jDb, jint traceMask, jobject jTracer
+){
+  S3JniDb * const ps = S3JniDb_from_java(jDb);
+  int rc;
+
+  if( !ps ) return SQLITE_MISUSE;
+  if( !traceMask || !jTracer ){
+    S3JniDb_mutex_enter;
+    rc = (jint)sqlite3_trace_v2(ps->pDb, 0, 0, 0);
+    S3JniHook_unref(&ps->hooks.trace);
+    S3JniDb_mutex_leave;
+  }else{
+    jclass const klazz = (*env)->GetObjectClass(env, jTracer);
+    S3JniHook hook = S3JniHook_empty;
+    hook.midCallback = (*env)->GetMethodID(
+      env, klazz, "call", "(ILjava/lang/Object;Ljava/lang/Object;)I"
+    );
+    S3JniUnrefLocal(klazz);
+    S3JniIfThrew {
+      S3JniExceptionClear;
+      rc = s3jni_db_error(ps->pDb, SQLITE_ERROR,
+                          "Cannot not find matching call() on "
+                          "TracerCallback object.");
+    }else{
+      hook.jObj = S3JniRefGlobal(jTracer);
+      S3JniDb_mutex_enter;
+      rc = sqlite3_trace_v2(ps->pDb, (unsigned)traceMask, s3jni_trace_impl, ps);
+      if( 0==rc ){
+        S3JniHook_unref(&ps->hooks.trace);
+        ps->hooks.trace = hook /* transfer ownership of reference */;
+      }else{
+        S3JniHook_unref(&hook);
+      }
+      S3JniDb_mutex_leave;
+    }
+  }
+  return rc;
+}
+
+S3JniApi(sqlite3_txn_state(),jint,1txn_1state)(
+  JniArgsEnvClass,jobject jDb, jstring jSchema
+){
+  sqlite3 * const pDb = PtrGet_sqlite3(jDb);
+  int rc = SQLITE_MISUSE;
+  if( pDb ){
+    char * zSchema = jSchema
+      ? s3jni_jstring_to_utf8(jSchema, 0)
+      : 0;
+    if( !jSchema || (zSchema && jSchema) ){
+      rc = sqlite3_txn_state(pDb, zSchema);
+      sqlite3_free(zSchema);
+    }else{
+      rc = SQLITE_NOMEM;
+    }
+  }
+  return rc;
+}
+
+S3JniApi(sqlite3_update_hook(),jobject,1update_1hook)(
+  JniArgsEnvClass, jlong jpDb, jobject jHook
+){
+  return s3jni_updatepre_hook(env, 0, jpDb, jHook);
+}
+
+
+S3JniApi(sqlite3_value_blob(),jbyteArray,1value_1blob)(
+  JniArgsEnvClass, jlong jpSVal
+){
+  sqlite3_value * const sv = S3JniLongPtr_sqlite3_value(jpSVal);
+  const jbyte * pBytes = sv ? sqlite3_value_blob(sv) : 0;
+  int const nLen = pBytes ? sqlite3_value_bytes(sv) : 0;
+
+  s3jni_oom_check( nLen ? !!pBytes : 1 );
+  return pBytes
+    ? s3jni_new_jbyteArray(pBytes, nLen)
+    : NULL;
+}
+
+S3JniApi(sqlite3_value_bytes(),int,1value_1bytes)(
+  JniArgsEnvClass, jlong jpSVal
+){
+  sqlite3_value * const sv = S3JniLongPtr_sqlite3_value(jpSVal);
+  return sv ? sqlite3_value_bytes(sv) : 0;
+}
+
+S3JniApi(sqlite3_value_bytes16(),int,1value_1bytes16)(
+  JniArgsEnvClass, jlong jpSVal
+){
+  sqlite3_value * const sv = S3JniLongPtr_sqlite3_value(jpSVal);
+  return sv ? sqlite3_value_bytes16(sv) : 0;
+}
+
+
+S3JniApi(sqlite3_value_double(),jdouble,1value_1double)(
+  JniArgsEnvClass, jlong jpSVal
+){
+  sqlite3_value * const sv = S3JniLongPtr_sqlite3_value(jpSVal);
+  return (jdouble) (sv ? sqlite3_value_double(sv) : 0.0);
+}
+
+
+S3JniApi(sqlite3_value_dup(),jobject,1value_1dup)(
+  JniArgsEnvClass, jlong jpSVal
+){
+  sqlite3_value * const sv = S3JniLongPtr_sqlite3_value(jpSVal);
+  sqlite3_value * const sd = sv ? sqlite3_value_dup(sv) : 0;
+  jobject rv = sd ? new_java_sqlite3_value(env, sd) : 0;
+  if( sd && !rv ) {
+    /* OOM */
+    sqlite3_value_free(sd);
+  }
+  return rv;
+}
+
+S3JniApi(sqlite3_value_free(),void,1value_1free)(
+  JniArgsEnvClass, jlong jpSVal
+){
+  sqlite3_value * const sv = S3JniLongPtr_sqlite3_value(jpSVal);
+  if( sv ){
+    sqlite3_value_free(sv);
+  }
+}
+
+S3JniApi(sqlite3_value_int(),jint,1value_1int)(
+  JniArgsEnvClass, jlong jpSVal
+){
+  sqlite3_value * const sv = S3JniLongPtr_sqlite3_value(jpSVal);
+  return (jint) (sv ? sqlite3_value_int(sv) : 0);
+}
+
+S3JniApi(sqlite3_value_int64(),jlong,1value_1int64)(
+  JniArgsEnvClass, jlong jpSVal
+){
+  sqlite3_value * const sv = S3JniLongPtr_sqlite3_value(jpSVal);
+  return (jlong) (sv ? sqlite3_value_int64(sv) : 0LL);
+}
+
+S3JniApi(sqlite3_value_java_object(),jobject,1value_1java_1object)(
+  JniArgsEnvClass, jlong jpSVal
+){
+  sqlite3_value * const sv = S3JniLongPtr_sqlite3_value(jpSVal);
+  return sv
+    ? sqlite3_value_pointer(sv, ResultJavaValuePtrStr)
+    : 0;
+}
+
+S3JniApi(sqlite3_value_text(),jbyteArray,1value_1text)(
+  JniArgsEnvClass, jlong jpSVal
+){
+  sqlite3_value * const sv = S3JniLongPtr_sqlite3_value(jpSVal);
+  const unsigned char * const p = sv ? sqlite3_value_text(sv) : 0;
+  int const n = p ? sqlite3_value_bytes(sv) : 0;
+  return p ? s3jni_new_jbyteArray(p, n) : 0;
+}
+
+#if 0
+// this impl might prove useful.
+S3JniApi(sqlite3_value_text(),jstring,1value_1text)(
+  JniArgsEnvClass, jlong jpSVal
+){
+  sqlite3_value * const sv = S3JniLongPtr_sqlite3_value(jpSVal);
+  const unsigned char * const p = sv ? sqlite3_value_text(sv) : 0;
+  int const n = p ? sqlite3_value_bytes(sv) : 0;
+  return p ? s3jni_utf8_to_jstring( (const char *)p, n) : 0;
+}
+#endif
+
+S3JniApi(sqlite3_value_text16(),jstring,1value_1text16)(
+  JniArgsEnvClass, jlong jpSVal
+){
+  sqlite3_value * const sv = S3JniLongPtr_sqlite3_value(jpSVal);
+  const int n = sv ? sqlite3_value_bytes16(sv) : 0;
+  const void * const p = sv ? sqlite3_value_text16(sv) : 0;
+  return p ? s3jni_text16_to_jstring(env, p, n) : 0;
+}
+
+JniDecl(void,1jni_1internal_1details)(JniArgsEnvClass){
+  MARKER(("\nVarious bits of internal info:\n"));
+  puts("FTS5 is "
+#ifdef SQLITE_ENABLE_FTS5
+       "available"
+#else
+       "unavailable"
+#endif
+       "."
+       );
+  puts("sizeofs:");
+#define SO(T) printf("\tsizeof(" #T ") = %u\n", (unsigned)sizeof(T))
+  SO(void*);
+  SO(jmethodID);
+  SO(jfieldID);
+  SO(S3JniEnv);
+  SO(S3JniHook);
+  SO(S3JniDb);
+  SO(S3JniNphOps);
+  printf("\t(^^^ %u NativePointerHolder/OutputPointer.T types)\n",
+         (unsigned)S3Jni_NphCache_size);
+  SO(S3JniGlobal);
+  SO(S3JniGlobal.nph);
+  SO(S3JniGlobal.metrics);
+  SO(S3JniAutoExtension);
+  SO(S3JniUdf);
+#undef SO
+#ifdef SQLITE_JNI_ENABLE_METRICS
+  printf("Cache info:\n");
+  printf("\tJNIEnv cache: %u allocs, %u misses, %u hits\n",
+         SJG.metrics.nEnvAlloc, SJG.metrics.nEnvMiss,
+         SJG.metrics.nEnvHit);
+  printf("Mutex entry:"
+         "\n\tglobal       = %u"
+         "\n\tenv          = %u"
+         "\n\tnph          = %u for S3JniNphOp init"
+         "\n\thook         = %u"
+         "\n\tperDb        = %u"
+         "\n\tautoExt list = %u"
+         "\n\tS3JniUdf     = %u (free-list)"
+         "\n\tmetrics      = %u\n",
+         SJG.metrics.nMutexGlobal, SJG.metrics.nMutexEnv,
+         SJG.metrics.nMutexNph, SJG.metrics.nMutexHook,
+         SJG.metrics.nMutexPerDb, SJG.metrics.nMutexAutoExt,
+         SJG.metrics.nMutexUdf, SJG.metrics.nMetrics);
+  puts("Allocs:");
+  printf("\tS3JniDb:  %u alloced (*%u = %u bytes), %u recycled\n",
+         SJG.metrics.nPdbAlloc, (unsigned) sizeof(S3JniDb),
+         (unsigned)(SJG.metrics.nPdbAlloc * sizeof(S3JniDb)),
+         SJG.metrics.nPdbRecycled);
+  printf("\tS3JniUdf: %u alloced (*%u = %u bytes), %u recycled\n",
+         SJG.metrics.nUdfAlloc, (unsigned) sizeof(S3JniUdf),
+         (unsigned)(SJG.metrics.nUdfAlloc * sizeof(S3JniUdf)),
+         SJG.metrics.nUdfRecycled);
+  printf("\tS3JniHook: %u alloced (*%u = %u bytes), %u recycled\n",
+         SJG.metrics.nHookAlloc, (unsigned) sizeof(S3JniHook),
+         (unsigned)(SJG.metrics.nHookAlloc * sizeof(S3JniHook)),
+         SJG.metrics.nHookRecycled);
+  printf("\tS3JniEnv: %u alloced (*%u = %u bytes)\n",
+         SJG.metrics.nEnvAlloc, (unsigned) sizeof(S3JniEnv),
+         (unsigned)(SJG.metrics.nEnvAlloc * sizeof(S3JniEnv)));
+  puts("Java-side UDF calls:");
+#define UDF(T) printf("\t%-8s = %u\n", "x" #T, SJG.metrics.udf.n##T)
+  UDF(Func); UDF(Step); UDF(Final); UDF(Value); UDF(Inverse);
+#undef UDF
+  printf("xDestroy calls across all callback types: %u\n",
+         SJG.metrics.nDestroy);
+#else
+  puts("Built without SQLITE_JNI_ENABLE_METRICS.");
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////
+// End of the sqlite3_... API bindings. Next up, FTS5...
+////////////////////////////////////////////////////////////////////////
+#ifdef SQLITE_ENABLE_FTS5
+
+/* Creates a verbose JNI Fts5 function name. */
+#define JniFuncNameFtsXA(Suffix)                  \
+  Java_org_sqlite_jni_fts5_Fts5ExtensionApi_ ## Suffix
+#define JniFuncNameFtsApi(Suffix)                  \
+  Java_org_sqlite_jni_fts5_fts5_1api_ ## Suffix
+#define JniFuncNameFtsTok(Suffix)                  \
+  Java_org_sqlite_jni_fts5_fts5_tokenizer_ ## Suffix
+
+#define JniDeclFtsXA(ReturnType,Suffix)           \
+  JNIEXPORT ReturnType JNICALL                  \
+  JniFuncNameFtsXA(Suffix)
+#define JniDeclFtsApi(ReturnType,Suffix)          \
+  JNIEXPORT ReturnType JNICALL                  \
+  JniFuncNameFtsApi(Suffix)
+#define JniDeclFtsTok(ReturnType,Suffix)          \
+  JNIEXPORT ReturnType JNICALL                  \
+  JniFuncNameFtsTok(Suffix)
+
+#define PtrGet_fts5_api(OBJ) NativePointerHolder_get(OBJ,S3JniNph(fts5_api))
+#define PtrGet_fts5_tokenizer(OBJ) NativePointerHolder_get(OBJ,S3JniNph(fts5_tokenizer))
+#define PtrGet_Fts5Context(OBJ) NativePointerHolder_get(OBJ,S3JniNph(Fts5Context))
+#define PtrGet_Fts5Tokenizer(OBJ) NativePointerHolder_get(OBJ,S3JniNph(Fts5Tokenizer))
+#define s3jni_ftsext() &sFts5Api/*singleton from sqlite3.c*/
+#define Fts5ExtDecl Fts5ExtensionApi const * const ext = s3jni_ftsext()
+
+/**
+   State for binding Java-side FTS5 auxiliary functions.
+*/
+typedef struct {
+  jobject jObj          /* functor instance */;
+  jobject jUserData     /* 2nd arg to JNI binding of
+                           xCreateFunction(), ostensibly the 3rd arg
+                           to the lib-level xCreateFunction(), except
+                           that we necessarily use that slot for a
+                           Fts5JniAux instance. */;
+  char * zFuncName      /* Only for error reporting and debug logging */;
+  jmethodID jmid        /* callback member's method ID */;
+} Fts5JniAux;
+
+static void Fts5JniAux_free(Fts5JniAux * const s){
+  S3JniDeclLocal_env;
+  if( env ){
+    /*MARKER(("FTS5 aux function cleanup: %s\n", s->zFuncName));*/
+    s3jni_call_xDestroy(s->jObj);
+    S3JniUnrefGlobal(s->jObj);
+    S3JniUnrefGlobal(s->jUserData);
+  }
+  sqlite3_free(s->zFuncName);
+  sqlite3_free(s);
+}
+
+static void Fts5JniAux_xDestroy(void *p){
+  if( p ) Fts5JniAux_free(p);
+}
+
+static Fts5JniAux * Fts5JniAux_alloc(JNIEnv * const env, jobject jObj){
+  Fts5JniAux * s = s3jni_malloc( sizeof(Fts5JniAux));
+
+  if( s ){
+    jclass klazz;
+    memset(s, 0, sizeof(Fts5JniAux));
+    s->jObj = S3JniRefGlobal(jObj);
+    klazz = (*env)->GetObjectClass(env, jObj);
+    s->jmid = (*env)->GetMethodID(env, klazz, "call",
+                                  "(Lorg/sqlite/jni/fts5/Fts5ExtensionApi;"
+                                  "Lorg/sqlite/jni/fts5/Fts5Context;"
+                                  "Lorg/sqlite/jni/capi/sqlite3_context;"
+                                  "[Lorg/sqlite/jni/capi/sqlite3_value;)V");
+    S3JniUnrefLocal(klazz);
+    S3JniIfThrew{
+      S3JniExceptionReport;
+      S3JniExceptionClear;
+      Fts5JniAux_free(s);
+      s = 0;
+    }
+  }
+  return s;
+}
+
+static inline jobject new_java_Fts5Context(JNIEnv * const env, Fts5Context *sv){
+  return NativePointerHolder_new(env, S3JniNph(Fts5Context), sv);
+}
+static inline jobject new_java_fts5_api(JNIEnv * const env, fts5_api *sv){
+  return NativePointerHolder_new(env, S3JniNph(fts5_api), sv);
+}
+
+/*
+** Returns a per-JNIEnv global ref to the Fts5ExtensionApi singleton
+** instance, or NULL on OOM.
+*/
+static jobject s3jni_getFts5ExensionApi(JNIEnv * const env){
+  if( !SJG.fts5.jExt ){
+    S3JniGlobal_mutex_enter;
+    if( !SJG.fts5.jExt ){
+      jobject const pNPH = NativePointerHolder_new(
+        env, S3JniNph(Fts5ExtensionApi), s3jni_ftsext()
+      );
+      if( pNPH ){
+        SJG.fts5.jExt = S3JniRefGlobal(pNPH);
+        S3JniUnrefLocal(pNPH);
+      }
+    }
+    S3JniGlobal_mutex_leave;
+  }
+  return SJG.fts5.jExt;
+}
+
+/*
+** Returns a pointer to the fts5_api instance for database connection
+** db.  If an error occurs, returns NULL and leaves an error in the
+** database handle (accessible using sqlite3_errcode()/errmsg()).
+*/
+static fts5_api *s3jni_fts5_api_from_db(sqlite3 *db){
+  fts5_api *pRet = 0;
+  sqlite3_stmt *pStmt = 0;
+  if( SQLITE_OK==sqlite3_prepare(db, "SELECT fts5(?1)", -1, &pStmt, 0) ){
+    sqlite3_bind_pointer(pStmt, 1, (void*)&pRet, "fts5_api_ptr", NULL);
+    sqlite3_step(pStmt);
+  }
+  sqlite3_finalize(pStmt);
+  return pRet;
+}
+
+JniDeclFtsApi(jobject,getInstanceForDb)(JniArgsEnvClass,jobject jDb){
+  S3JniDb * const ps = S3JniDb_from_java(jDb);
+#if 0
+  jobject rv = 0;
+  if( !ps ) return 0;
+  else if( ps->fts.jApi ){
+    rv = ps->fts.jApi;
+  }else{
+    fts5_api * const pApi = s3jni_fts5_api_from_db(ps->pDb);
+    if( pApi ){
+      rv = new_java_fts5_api(env, pApi);
+      ps->fts.jApi = rv ? S3JniRefGlobal(rv) : 0;
+    }
+  }
+  return rv;
+#else
+  if( ps && !ps->fts.jApi ){
+    S3JniDb_mutex_enter;
+    if( !ps->fts.jApi ){
+      fts5_api * const pApi = s3jni_fts5_api_from_db(ps->pDb);
+      if( pApi ){
+        jobject const rv = new_java_fts5_api(env, pApi);
+        ps->fts.jApi = rv ? S3JniRefGlobal(rv) : 0;
+      }
+    }
+    S3JniDb_mutex_leave;
+  }
+  return ps ? ps->fts.jApi : 0;
+#endif
+}
+
+
+JniDeclFtsXA(jobject,getInstance)(JniArgsEnvClass){
+  return s3jni_getFts5ExensionApi(env);
+}
+
+JniDeclFtsXA(jint,xColumnCount)(JniArgsEnvObj,jobject jCtx){
+  Fts5ExtDecl;
+  return (jint)ext->xColumnCount(PtrGet_Fts5Context(jCtx));
+}
+
+JniDeclFtsXA(jint,xColumnSize)(JniArgsEnvObj,jobject jCtx, jint iIdx, jobject jOut32){
+  Fts5ExtDecl;
+  int n1 = 0;
+  int const rc = ext->xColumnSize(PtrGet_Fts5Context(jCtx), (int)iIdx, &n1);
+  if( 0==rc ) OutputPointer_set_Int32(env, jOut32, n1);
+  return rc;
+}
+
+JniDeclFtsXA(jint,xColumnText)(JniArgsEnvObj,jobject jCtx, jint iCol,
+                           jobject jOut){
+  Fts5ExtDecl;
+  const char *pz = 0;
+  int pn = 0;
+  int rc = ext->xColumnText(PtrGet_Fts5Context(jCtx), (int)iCol,
+                             &pz, &pn);
+  if( 0==rc ){
+    jstring jstr = pz ? s3jni_utf8_to_jstring( pz, pn) : 0;
+    if( pz ){
+      if( jstr ){
+        OutputPointer_set_String(env, jOut, jstr);
+        S3JniUnrefLocal(jstr)/*jOut has a reference*/;
+      }else{
+        rc = SQLITE_NOMEM;
+      }
+    }
+  }
+  return (jint)rc;
+}
+
+JniDeclFtsXA(jint,xColumnTotalSize)(JniArgsEnvObj,jobject jCtx, jint iCol, jobject jOut64){
+  Fts5ExtDecl;
+  sqlite3_int64 nOut = 0;
+  int const rc = ext->xColumnTotalSize(PtrGet_Fts5Context(jCtx), (int)iCol, &nOut);
+  if( 0==rc && jOut64 ) OutputPointer_set_Int64(env, jOut64, (jlong)nOut);
+  return (jint)rc;
+}
+
+/*
+** Proxy for fts5_extension_function instances plugged in via
+** fts5_api::xCreateFunction().
+*/
+static void s3jni_fts5_extension_function(Fts5ExtensionApi const *pApi,
+                                          Fts5Context *pFts,
+                                          sqlite3_context *pCx,
+                                          int argc,
+                                          sqlite3_value **argv){
+  Fts5JniAux * const pAux = pApi->xUserData(pFts);
+  jobject jpCx = 0;
+  jobjectArray jArgv = 0;
+  jobject jpFts = 0;
+  jobject jFXA;
+  int rc;
+  S3JniDeclLocal_env;
+
+  assert(pAux);
+  jFXA = s3jni_getFts5ExensionApi(env);
+  if( !jFXA ) goto error_oom;
+  jpFts = new_java_Fts5Context(env, pFts);
+  if( !jpFts ) goto error_oom;
+  rc = udf_args(env, pCx, argc, argv, &jpCx, &jArgv);
+  if( rc ) goto error_oom;
+  (*env)->CallVoidMethod(env, pAux->jObj, pAux->jmid,
+                         jFXA, jpFts, jpCx, jArgv);
+  S3JniIfThrew{
+    udf_report_exception(env, 1, pCx, pAux->zFuncName, "call");
+  }
+  udf_unargs(env, jpCx, argc, jArgv);
+  S3JniUnrefLocal(jpFts);
+  S3JniUnrefLocal(jpCx);
+  S3JniUnrefLocal(jArgv);
+  return;
+error_oom:
+  s3jni_db_oom( sqlite3_context_db_handle(pCx) );
+  assert( !jArgv );
+  assert( !jpCx );
+  S3JniUnrefLocal(jpFts);
+  sqlite3_result_error_nomem(pCx);
+  return;
+}
+
+JniDeclFtsApi(jint,xCreateFunction)(JniArgsEnvObj, jstring jName,
+                                  jobject jUserData, jobject jFunc){
+  fts5_api * const pApi = PtrGet_fts5_api(jSelf);
+  int rc;
+  char * zName;
+  Fts5JniAux * pAux;
+
+  assert(pApi);
+  zName = s3jni_jstring_to_utf8( jName, 0);
+  if(!zName) return SQLITE_NOMEM;
+  pAux = Fts5JniAux_alloc(env, jFunc);
+  if( pAux ){
+    rc = pApi->xCreateFunction(pApi, zName, pAux,
+                               s3jni_fts5_extension_function,
+                               Fts5JniAux_xDestroy);
+  }else{
+    rc = SQLITE_NOMEM;
+  }
+  if( 0==rc ){
+    pAux->jUserData = jUserData ? S3JniRefGlobal(jUserData) : 0;
+    pAux->zFuncName = zName;
+  }else{
+    sqlite3_free(zName);
+  }
+  return (jint)rc;
+}
+
+
+typedef struct S3JniFts5AuxData S3JniFts5AuxData;
+/*
+** TODO: this middle-man struct is no longer necessary. Conider
+** removing it and passing around jObj itself instead.
+*/
+struct S3JniFts5AuxData {
+  jobject jObj;
+};
+
+static void S3JniFts5AuxData_xDestroy(void *x){
+  if( x ){
+    S3JniFts5AuxData * const p = x;
+    if( p->jObj ){
+      S3JniDeclLocal_env;
+      s3jni_call_xDestroy(p->jObj);
+      S3JniUnrefGlobal(p->jObj);
+    }
+    sqlite3_free(x);
+  }
+}
+
+JniDeclFtsXA(jobject,xGetAuxdata)(JniArgsEnvObj,jobject jCtx, jboolean bClear){
+  Fts5ExtDecl;
+  jobject rv = 0;
+  S3JniFts5AuxData * const pAux = ext->xGetAuxdata(PtrGet_Fts5Context(jCtx), bClear);
+  if( pAux ){
+    if( bClear ){
+      if( pAux->jObj ){
+        rv = S3JniRefLocal(pAux->jObj);
+        S3JniUnrefGlobal(pAux->jObj);
+      }
+      /* Note that we do not call xDestroy() in this case. */
+      sqlite3_free(pAux);
+    }else{
+      rv = pAux->jObj;
+    }
+  }
+  return rv;
+}
+
+JniDeclFtsXA(jint,xInst)(JniArgsEnvObj,jobject jCtx, jint iIdx, jobject jOutPhrase,
+                    jobject jOutCol, jobject jOutOff){
+  Fts5ExtDecl;
+  int n1 = 0, n2 = 2, n3 = 0;
+  int const rc = ext->xInst(PtrGet_Fts5Context(jCtx), (int)iIdx, &n1, &n2, &n3);
+  if( 0==rc ){
+    OutputPointer_set_Int32(env, jOutPhrase, n1);
+    OutputPointer_set_Int32(env, jOutCol, n2);
+    OutputPointer_set_Int32(env, jOutOff, n3);
+  }
+  return rc;
+}
+
+JniDeclFtsXA(jint,xInstCount)(JniArgsEnvObj,jobject jCtx, jobject jOut32){
+  Fts5ExtDecl;
+  int nOut = 0;
+  int const rc = ext->xInstCount(PtrGet_Fts5Context(jCtx), &nOut);
+  if( 0==rc && jOut32 ) OutputPointer_set_Int32(env, jOut32, nOut);
+  return (jint)rc;
+}
+
+JniDeclFtsXA(jint,xPhraseCount)(JniArgsEnvObj,jobject jCtx){
+  Fts5ExtDecl;
+  return (jint)ext->xPhraseCount(PtrGet_Fts5Context(jCtx));
+}
+
+/* Copy the 'a' and 'b' fields from pSrc to Fts5PhraseIter object jIter. */
+static void s3jni_phraseIter_NToJ(JNIEnv *const env,
+                                  Fts5PhraseIter const * const pSrc,
+                                  jobject jIter){
+  S3JniGlobalType * const g = &S3JniGlobal;
+  assert(g->fts5.jPhraseIter.fidA);
+  (*env)->SetLongField(env, jIter, g->fts5.jPhraseIter.fidA,
+                       S3JniCast_P2L(pSrc->a));
+  S3JniExceptionIsFatal("Cannot set Fts5PhraseIter.a field.");
+  (*env)->SetLongField(env, jIter, g->fts5.jPhraseIter.fidB,
+                       S3JniCast_P2L(pSrc->b));
+  S3JniExceptionIsFatal("Cannot set Fts5PhraseIter.b field.");
+}
+
+/* Copy the 'a' and 'b' fields from Fts5PhraseIter object jIter to pDest. */
+static void s3jni_phraseIter_JToN(JNIEnv *const env,  jobject jIter,
+                                  Fts5PhraseIter * const pDest){
+  S3JniGlobalType * const g = &S3JniGlobal;
+  assert(g->fts5.jPhraseIter.fidA);
+  pDest->a = S3JniCast_L2P(
+    (*env)->GetLongField(env, jIter, g->fts5.jPhraseIter.fidA)
+  );
+  S3JniExceptionIsFatal("Cannot get Fts5PhraseIter.a field.");
+  pDest->b = S3JniCast_L2P(
+    (*env)->GetLongField(env, jIter, g->fts5.jPhraseIter.fidB)
+  );
+  S3JniExceptionIsFatal("Cannot get Fts5PhraseIter.b field.");
+}
+
+JniDeclFtsXA(jint,xPhraseFirst)(JniArgsEnvObj,jobject jCtx, jint iPhrase,
+                            jobject jIter, jobject jOutCol,
+                            jobject jOutOff){
+  Fts5ExtDecl;
+  Fts5PhraseIter iter;
+  int rc, iCol = 0, iOff = 0;
+  rc = ext->xPhraseFirst(PtrGet_Fts5Context(jCtx), (int)iPhrase,
+                         &iter, &iCol, &iOff);
+  if( 0==rc ){
+    OutputPointer_set_Int32(env, jOutCol, iCol);
+    OutputPointer_set_Int32(env, jOutOff, iOff);
+    s3jni_phraseIter_NToJ(env, &iter, jIter);
+  }
+  return rc;
+}
+
+JniDeclFtsXA(jint,xPhraseFirstColumn)(JniArgsEnvObj,jobject jCtx, jint iPhrase,
+                                  jobject jIter, jobject jOutCol){
+  Fts5ExtDecl;
+  Fts5PhraseIter iter;
+  int rc, iCol = 0;
+  rc = ext->xPhraseFirstColumn(PtrGet_Fts5Context(jCtx), (int)iPhrase,
+                                &iter, &iCol);
+  if( 0==rc ){
+    OutputPointer_set_Int32(env, jOutCol, iCol);
+    s3jni_phraseIter_NToJ(env, &iter, jIter);
+  }
+  return rc;
+}
+
+JniDeclFtsXA(void,xPhraseNext)(JniArgsEnvObj,jobject jCtx, jobject jIter,
+                           jobject jOutCol, jobject jOutOff){
+  Fts5ExtDecl;
+  Fts5PhraseIter iter;
+  int iCol = 0, iOff = 0;
+  s3jni_phraseIter_JToN(env, jIter, &iter);
+  ext->xPhraseNext(PtrGet_Fts5Context(jCtx), &iter, &iCol, &iOff);
+  OutputPointer_set_Int32(env, jOutCol, iCol);
+  OutputPointer_set_Int32(env, jOutOff, iOff);
+  s3jni_phraseIter_NToJ(env, &iter, jIter);
+}
+
+JniDeclFtsXA(void,xPhraseNextColumn)(JniArgsEnvObj,jobject jCtx, jobject jIter,
+                                 jobject jOutCol){
+  Fts5ExtDecl;
+  Fts5PhraseIter iter;
+  int iCol = 0;
+  s3jni_phraseIter_JToN(env, jIter, &iter);
+  ext->xPhraseNextColumn(PtrGet_Fts5Context(jCtx), &iter, &iCol);
+  OutputPointer_set_Int32(env, jOutCol, iCol);
+  s3jni_phraseIter_NToJ(env, &iter, jIter);
+}
+
+
+JniDeclFtsXA(jint,xPhraseSize)(JniArgsEnvObj,jobject jCtx, jint iPhrase){
+  Fts5ExtDecl;
+  return (jint)ext->xPhraseSize(PtrGet_Fts5Context(jCtx), (int)iPhrase);
+}
+
+/* State for use with xQueryPhrase() and xTokenize(). */
+struct s3jni_xQueryPhraseState {
+  Fts5ExtensionApi const * ext;
+  jmethodID midCallback; /* jCallback->call() method */
+  jobject jCallback;   /* Fts5ExtensionApi.XQueryPhraseCallback instance */
+  jobject jFcx;        /* (Fts5Context*) for xQueryPhrase()
+                          callback. This is NOT the instance that is
+                          passed to xQueryPhrase(), it's the one
+                          created by xQueryPhrase() for use by its
+                          callback. */
+  /* State for xTokenize() */
+  struct {
+    const char * zPrev;
+    int nPrev;
+    jbyteArray jba;
+  } tok;
+};
+
+static int s3jni_xQueryPhrase(const Fts5ExtensionApi *xapi,
+                              Fts5Context * pFcx, void *pData){
+  struct s3jni_xQueryPhraseState * const s = pData;
+  S3JniDeclLocal_env;
+
+  if( !s->jFcx ){
+    s->jFcx = new_java_Fts5Context(env, pFcx);
+    if( !s->jFcx ) return SQLITE_NOMEM;
+  }
+  int rc = (int)(*env)->CallIntMethod(env, s->jCallback, s->midCallback,
+                                      SJG.fts5.jExt, s->jFcx);
+  S3JniIfThrew{
+    S3JniExceptionWarnCallbackThrew("xQueryPhrase() callback");
+    S3JniExceptionClear;
+    rc = SQLITE_ERROR;
+  }
+  return rc;
+}
+
+JniDeclFtsXA(jint,xQueryPhrase)(JniArgsEnvObj,jobject jFcx, jint iPhrase,
+                            jobject jCallback){
+  Fts5ExtDecl;
+  int rc;
+  struct s3jni_xQueryPhraseState s;
+  jclass klazz = jCallback ? (*env)->GetObjectClass(env, jCallback) : NULL;
+
+  if( !klazz ) return SQLITE_MISUSE;
+  s.jCallback = jCallback;
+  s.jFcx = 0;
+  s.ext = ext;
+  s.midCallback = (*env)->GetMethodID(env, klazz, "call",
+                                      "(Lorg/sqlite/jni/fts5/Fts5ExtensionApi;"
+                                      "Lorg/sqlite/jni/fts5/Fts5Context;)I");
+  S3JniUnrefLocal(klazz);
+  S3JniExceptionIsFatal("Could not extract xQueryPhraseCallback.call() method.");
+  rc = ext->xQueryPhrase(PtrGet_Fts5Context(jFcx), iPhrase, &s,
+                         s3jni_xQueryPhrase);
+  S3JniUnrefLocal(s.jFcx);
+  return (jint)rc;
+}
+
+
+JniDeclFtsXA(jint,xRowCount)(JniArgsEnvObj,jobject jCtx, jobject jOut64){
+  Fts5ExtDecl;
+  sqlite3_int64 nOut = 0;
+  int const rc = ext->xRowCount(PtrGet_Fts5Context(jCtx), &nOut);
+  if( 0==rc && jOut64 ) OutputPointer_set_Int64(env, jOut64, (jlong)nOut);
+  return (jint)rc;
+}
+
+JniDeclFtsXA(jlong,xRowid)(JniArgsEnvObj,jobject jCtx){
+  Fts5ExtDecl;
+  return (jlong)ext->xRowid(PtrGet_Fts5Context(jCtx));
+}
+
+JniDeclFtsXA(int,xSetAuxdata)(JniArgsEnvObj,jobject jCtx, jobject jAux){
+  Fts5ExtDecl;
+  int rc;
+  S3JniFts5AuxData * pAux;
+
+  pAux = s3jni_malloc( sizeof(*pAux));
+  if( !pAux ){
+    if( jAux ){
+      /* Emulate how xSetAuxdata() behaves when it cannot alloc
+      ** its auxdata wrapper. */
+      s3jni_call_xDestroy(jAux);
+    }
+    return SQLITE_NOMEM;
+  }
+  pAux->jObj = S3JniRefGlobal(jAux);
+  rc = ext->xSetAuxdata(PtrGet_Fts5Context(jCtx), pAux,
+                         S3JniFts5AuxData_xDestroy);
+  return rc;
+}
+
+/* xToken() impl for xTokenize(). */
+static int s3jni_xTokenize_xToken(void *p, int tFlags, const char* z,
+                                  int nZ, int iStart, int iEnd){
+  int rc;
+  S3JniDeclLocal_env;
+  struct s3jni_xQueryPhraseState * const s = p;
+  jbyteArray jba;
+
+  S3JniUnrefLocal(s->tok.jba);
+  s->tok.zPrev = z;
+  s->tok.nPrev = nZ;
+  s->tok.jba = s3jni_new_jbyteArray(z, nZ);
+  if( !s->tok.jba ) return SQLITE_NOMEM;
+  jba = s->tok.jba;
+  rc = (int)(*env)->CallIntMethod(env, s->jCallback, s->midCallback,
+                                  (jint)tFlags, jba, (jint)iStart,
+                                  (jint)iEnd);
+  S3JniIfThrew {
+    S3JniExceptionWarnCallbackThrew("xTokenize() callback");
+    rc = SQLITE_ERROR;
+  }
+  return rc;
+}
+
+/*
+** Proxy for Fts5ExtensionApi.xTokenize() and
+** fts5_tokenizer.xTokenize()
+*/
+static jint s3jni_fts5_xTokenize(JniArgsEnvObj, S3JniNphOp const *pRef,
+                                 jint tokFlags, jobject jFcx,
+                                 jbyteArray jbaText, jobject jCallback){
+  Fts5ExtDecl;
+  struct s3jni_xQueryPhraseState s;
+  int rc = 0;
+  jbyte * const pText = jCallback ? s3jni_jbyteArray_bytes(jbaText) : 0;
+  jsize nText = pText ? (*env)->GetArrayLength(env, jbaText) : 0;
+  jclass const klazz = jCallback ? (*env)->GetObjectClass(env, jCallback) : NULL;
+
+  if( !klazz ) return SQLITE_MISUSE;
+  memset(&s, 0, sizeof(s));
+  s.jCallback = jCallback;
+  s.jFcx = jFcx;
+  s.ext = ext;
+  s.midCallback = (*env)->GetMethodID(env, klazz, "call", "(I[BII)I");
+  S3JniUnrefLocal(klazz);
+  S3JniIfThrew {
+    S3JniExceptionReport;
+    S3JniExceptionClear;
+    s3jni_jbyteArray_release(jbaText, pText);
+    return SQLITE_ERROR;
+  }
+  s.tok.jba = S3JniRefLocal(jbaText);
+  s.tok.zPrev = (const char *)pText;
+  s.tok.nPrev = (int)nText;
+  if( pRef == S3JniNph(Fts5ExtensionApi) ){
+    rc = ext->xTokenize(PtrGet_Fts5Context(jFcx),
+                         (const char *)pText, (int)nText,
+                         &s, s3jni_xTokenize_xToken);
+  }else if( pRef == S3JniNph(fts5_tokenizer) ){
+    fts5_tokenizer * const pTok = PtrGet_fts5_tokenizer(jSelf);
+    rc = pTok->xTokenize(PtrGet_Fts5Tokenizer(jFcx), &s, tokFlags,
+                         (const char *)pText, (int)nText,
+                         s3jni_xTokenize_xToken);
+  }else{
+    (*env)->FatalError(env, "This cannot happen. Maintenance required.");
+  }
+  if( s.tok.jba ){
+    assert( s.tok.zPrev );
+    S3JniUnrefLocal(s.tok.jba);
+  }
+  s3jni_jbyteArray_release(jbaText, pText);
+  return (jint)rc;
+}
+
+JniDeclFtsXA(jint,xTokenize)(JniArgsEnvObj,jobject jFcx, jbyteArray jbaText,
+                             jobject jCallback){
+  return s3jni_fts5_xTokenize(env, jSelf, S3JniNph(Fts5ExtensionApi),
+                              0, jFcx, jbaText, jCallback);
+}
+
+JniDeclFtsTok(jint,xTokenize)(JniArgsEnvObj,jobject jFcx, jint tokFlags,
+                              jbyteArray jbaText, jobject jCallback){
+  return s3jni_fts5_xTokenize(env, jSelf, S3JniNph(Fts5Tokenizer),
+                              tokFlags, jFcx, jbaText, jCallback);
+}
+
+
+JniDeclFtsXA(jobject,xUserData)(JniArgsEnvObj,jobject jFcx){
+  Fts5ExtDecl;
+  Fts5JniAux * const pAux = ext->xUserData(PtrGet_Fts5Context(jFcx));
+  return pAux ? pAux->jUserData : 0;
+}
+
+#endif /* SQLITE_ENABLE_FTS5 */
+
+////////////////////////////////////////////////////////////////////////
+// End of the main API bindings. Start of SQLTester bits...
+////////////////////////////////////////////////////////////////////////
+
+#ifdef SQLITE_JNI_ENABLE_SQLTester
+typedef struct SQLTesterJni SQLTesterJni;
+struct SQLTesterJni {
+  sqlite3_int64 nDup;
+};
+static SQLTesterJni SQLTester = {
+  0
+};
+
+static void SQLTester_dup_destructor(void*pToFree){
+  u64 *p = (u64*)pToFree;
+  assert( p!=0 );
+  p--;
+  assert( p[0]==0x2bbf4b7c );
+  p[0] = 0;
+  p[1] = 0;
+  sqlite3_free(p);
+}
+
+/*
+** Implementation of
+**
+**         dup(TEXT)
+**
+** This SQL function simply makes a copy of its text argument.  But it
+** returns the result using a custom destructor, in order to provide
+** tests for the use of Mem.xDel() in the SQLite VDBE.
+*/
+static void SQLTester_dup_func(
+  sqlite3_context *context,
+  int argc,
+  sqlite3_value **argv
+){
+  u64 *pOut;
+  char *z;
+  int n = sqlite3_value_bytes(argv[0]);
+  SQLTesterJni * const p = (SQLTesterJni *)sqlite3_user_data(context);
+  S3JniDeclLocal_env;
+
+  ++p->nDup;
+  if( n>0 && (pOut = s3jni_malloc( (n+16)&~7 ))!=0 ){
+    pOut[0] = 0x2bbf4b7c;
+    z = (char*)&pOut[1];
+    memcpy(z, sqlite3_value_text(argv[0]), n);
+    z[n] = 0;
+    sqlite3_result_text(context, z, n, SQLTester_dup_destructor);
+  }
+  return;
+}
+
+/*
+** Return the number of calls to the dup() SQL function since the
+** SQLTester context was opened or since the last dup_count() call.
+*/
+static void SQLTester_dup_count_func(
+  sqlite3_context *context,
+  int argc,
+  sqlite3_value **argv
+){
+  SQLTesterJni * const p = (SQLTesterJni *)sqlite3_user_data(context);
+  sqlite3_result_int64(context, p->nDup);
+  p->nDup = 0;
+}
+
+/*
+** 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 SQLTester_strnotglob(const char *zGlob, const char *z){
+  int c, c2;
+  int invert;
+  int seen;
+
+  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 && SQLTester_strnotglob(zGlob-1,z)==0 ){
+          z++;
+        }
+        return (*z)!=0;
+      }
+      while( (c2 = (*(z++)))!=0 ){
+        while( c2!=c ){
+          c2 = *(z++);
+          if( c2==0 ) return 0;
+        }
+        if( SQLTester_strnotglob(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;
+}
+
+JNIEXPORT jint JNICALL
+Java_org_sqlite_jni_capi_SQLTester_strglob(
+  JniArgsEnvClass, jbyteArray baG, jbyteArray baT
+){
+  int rc = 0;
+  jbyte * const pG = s3jni_jbyteArray_bytes(baG);
+  jbyte * const pT = pG ? s3jni_jbyteArray_bytes(baT) : 0;
+
+  s3jni_oom_fatal(pT);
+  /* Note that we're relying on the byte arrays having been
+     NUL-terminated on the Java side. */
+  rc = !SQLTester_strnotglob((const char *)pG, (const char *)pT);
+  s3jni_jbyteArray_release(baG, pG);
+  s3jni_jbyteArray_release(baT, pT);
+  return rc;
+}
+
+
+static int SQLTester_auto_extension(sqlite3 *pDb, const char **pzErr,
+                                    const struct sqlite3_api_routines *ignored){
+  sqlite3_create_function(pDb, "dup", 1, SQLITE_UTF8, &SQLTester,
+                          SQLTester_dup_func, 0, 0);
+  sqlite3_create_function(pDb, "dup_count", 0, SQLITE_UTF8, &SQLTester,
+                          SQLTester_dup_count_func, 0, 0);
+  return 0;
+}
+
+JNIEXPORT void JNICALL
+Java_org_sqlite_jni_capi_SQLTester_installCustomExtensions(JniArgsEnvClass){
+  sqlite3_auto_extension( (void(*)(void))SQLTester_auto_extension );
+}
+
+#endif /* SQLITE_JNI_ENABLE_SQLTester */
+////////////////////////////////////////////////////////////////////////
+// End of SQLTester bindings. Start of lower-level bits.
+////////////////////////////////////////////////////////////////////////
+
+/*
+** Called during static init of the CApi class to set up global
+** state.
+*/
+JNIEXPORT void JNICALL
+Java_org_sqlite_jni_capi_CApi_init(JniArgsEnvClass){
+  jclass klazz;
+
+  memset(&S3JniGlobal, 0, sizeof(S3JniGlobal));
+  if( (*env)->GetJavaVM(env, &SJG.jvm) ){
+    (*env)->FatalError(env, "GetJavaVM() failure shouldn't be possible.");
+    return;
+  }
+
+  /* Grab references to various global classes and objects... */
+  SJG.g.cLong = S3JniRefGlobal((*env)->FindClass(env,"java/lang/Long"));
+  S3JniExceptionIsFatal("Error getting reference to Long class.");
+  SJG.g.ctorLong1 = (*env)->GetMethodID(env, SJG.g.cLong,
+                                         "", "(J)V");
+  S3JniExceptionIsFatal("Error getting reference to Long constructor.");
+
+  SJG.g.cString = S3JniRefGlobal((*env)->FindClass(env,"java/lang/String"));
+  S3JniExceptionIsFatal("Error getting reference to String class.");
+  SJG.g.ctorStringBA =
+    (*env)->GetMethodID(env, SJG.g.cString,
+                        "", "([BLjava/nio/charset/Charset;)V");
+  S3JniExceptionIsFatal("Error getting reference to String(byte[],Charset) ctor.");
+  SJG.g.stringGetBytes =
+    (*env)->GetMethodID(env, SJG.g.cString,
+                        "getBytes", "(Ljava/nio/charset/Charset;)[B");
+  S3JniExceptionIsFatal("Error getting reference to String.getBytes(Charset).");
+
+  { /* java.nio.charset.StandardCharsets.UTF_8 */
+    jfieldID fUtf8;
+    klazz = (*env)->FindClass(env,"java/nio/charset/StandardCharsets");
+    S3JniExceptionIsFatal("Error getting reference to StandardCharsets class.");
+    fUtf8 = (*env)->GetStaticFieldID(env, klazz, "UTF_8",
+                                     "Ljava/nio/charset/Charset;");
+    S3JniExceptionIsFatal("Error getting StandardCharsets.UTF_8 field.");
+    SJG.g.oCharsetUtf8 =
+      S3JniRefGlobal((*env)->GetStaticObjectField(env, klazz, fUtf8));
+    S3JniExceptionIsFatal("Error getting reference to StandardCharsets.UTF_8.");
+    S3JniUnrefLocal(klazz);
+  }
+
+#ifdef SQLITE_ENABLE_FTS5
+  klazz = (*env)->FindClass(env, "org/sqlite/jni/fts5/Fts5PhraseIter");
+  S3JniExceptionIsFatal("Error getting reference to org.sqlite.jni.fts5.Fts5PhraseIter.");
+  SJG.fts5.jPhraseIter.fidA = (*env)->GetFieldID(env, klazz, "a", "J");
+  S3JniExceptionIsFatal("Cannot get Fts5PhraseIter.a field.");
+  SJG.fts5.jPhraseIter.fidB = (*env)->GetFieldID(env, klazz, "b", "J");
+  S3JniExceptionIsFatal("Cannot get Fts5PhraseIter.b field.");
+  S3JniUnrefLocal(klazz);
+#endif
+
+  SJG.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST);
+  s3jni_oom_fatal( SJG.mutex );
+  SJG.hook.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST);
+  s3jni_oom_fatal( SJG.hook.mutex );
+  SJG.nph.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST);
+  s3jni_oom_fatal( SJG.nph.mutex );
+  SJG.envCache.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST);
+  s3jni_oom_fatal( SJG.envCache.mutex );
+  SJG.perDb.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST);
+  s3jni_oom_fatal( SJG.perDb.mutex );
+  SJG.autoExt.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST);
+  s3jni_oom_fatal( SJG.autoExt.mutex );
+
+#if S3JNI_METRICS_MUTEX
+  SJG.metrics.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST);
+  s3jni_oom_fatal( SJG.metrics.mutex );
+#endif
+
+  sqlite3_shutdown()
+    /* So that it becomes legal for Java-level code to call
+    ** sqlite3_config(). */;
+}
diff --git a/sqlite/ext/jni/src/c/sqlite3-jni.h b/sqlite/ext/jni/src/c/sqlite3-jni.h
new file mode 100644
index 00000000..bf6df7ac
--- /dev/null
+++ b/sqlite/ext/jni/src/c/sqlite3-jni.h
@@ -0,0 +1,2379 @@
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include 
+/* Header for class org_sqlite_jni_capi_CApi */
+
+#ifndef _Included_org_sqlite_jni_capi_CApi
+#define _Included_org_sqlite_jni_capi_CApi
+#ifdef __cplusplus
+extern "C" {
+#endif
+#undef org_sqlite_jni_capi_CApi_SQLITE_ACCESS_EXISTS
+#define org_sqlite_jni_capi_CApi_SQLITE_ACCESS_EXISTS 0L
+#undef org_sqlite_jni_capi_CApi_SQLITE_ACCESS_READWRITE
+#define org_sqlite_jni_capi_CApi_SQLITE_ACCESS_READWRITE 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_ACCESS_READ
+#define org_sqlite_jni_capi_CApi_SQLITE_ACCESS_READ 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DENY
+#define org_sqlite_jni_capi_CApi_SQLITE_DENY 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IGNORE
+#define org_sqlite_jni_capi_CApi_SQLITE_IGNORE 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CREATE_INDEX
+#define org_sqlite_jni_capi_CApi_SQLITE_CREATE_INDEX 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CREATE_TABLE
+#define org_sqlite_jni_capi_CApi_SQLITE_CREATE_TABLE 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CREATE_TEMP_INDEX
+#define org_sqlite_jni_capi_CApi_SQLITE_CREATE_TEMP_INDEX 3L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CREATE_TEMP_TABLE
+#define org_sqlite_jni_capi_CApi_SQLITE_CREATE_TEMP_TABLE 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CREATE_TEMP_TRIGGER
+#define org_sqlite_jni_capi_CApi_SQLITE_CREATE_TEMP_TRIGGER 5L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CREATE_TEMP_VIEW
+#define org_sqlite_jni_capi_CApi_SQLITE_CREATE_TEMP_VIEW 6L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CREATE_TRIGGER
+#define org_sqlite_jni_capi_CApi_SQLITE_CREATE_TRIGGER 7L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CREATE_VIEW
+#define org_sqlite_jni_capi_CApi_SQLITE_CREATE_VIEW 8L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DELETE
+#define org_sqlite_jni_capi_CApi_SQLITE_DELETE 9L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DROP_INDEX
+#define org_sqlite_jni_capi_CApi_SQLITE_DROP_INDEX 10L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DROP_TABLE
+#define org_sqlite_jni_capi_CApi_SQLITE_DROP_TABLE 11L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DROP_TEMP_INDEX
+#define org_sqlite_jni_capi_CApi_SQLITE_DROP_TEMP_INDEX 12L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DROP_TEMP_TABLE
+#define org_sqlite_jni_capi_CApi_SQLITE_DROP_TEMP_TABLE 13L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DROP_TEMP_TRIGGER
+#define org_sqlite_jni_capi_CApi_SQLITE_DROP_TEMP_TRIGGER 14L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DROP_TEMP_VIEW
+#define org_sqlite_jni_capi_CApi_SQLITE_DROP_TEMP_VIEW 15L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DROP_TRIGGER
+#define org_sqlite_jni_capi_CApi_SQLITE_DROP_TRIGGER 16L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DROP_VIEW
+#define org_sqlite_jni_capi_CApi_SQLITE_DROP_VIEW 17L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INSERT
+#define org_sqlite_jni_capi_CApi_SQLITE_INSERT 18L
+#undef org_sqlite_jni_capi_CApi_SQLITE_PRAGMA
+#define org_sqlite_jni_capi_CApi_SQLITE_PRAGMA 19L
+#undef org_sqlite_jni_capi_CApi_SQLITE_READ
+#define org_sqlite_jni_capi_CApi_SQLITE_READ 20L
+#undef org_sqlite_jni_capi_CApi_SQLITE_SELECT
+#define org_sqlite_jni_capi_CApi_SQLITE_SELECT 21L
+#undef org_sqlite_jni_capi_CApi_SQLITE_TRANSACTION
+#define org_sqlite_jni_capi_CApi_SQLITE_TRANSACTION 22L
+#undef org_sqlite_jni_capi_CApi_SQLITE_UPDATE
+#define org_sqlite_jni_capi_CApi_SQLITE_UPDATE 23L
+#undef org_sqlite_jni_capi_CApi_SQLITE_ATTACH
+#define org_sqlite_jni_capi_CApi_SQLITE_ATTACH 24L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DETACH
+#define org_sqlite_jni_capi_CApi_SQLITE_DETACH 25L
+#undef org_sqlite_jni_capi_CApi_SQLITE_ALTER_TABLE
+#define org_sqlite_jni_capi_CApi_SQLITE_ALTER_TABLE 26L
+#undef org_sqlite_jni_capi_CApi_SQLITE_REINDEX
+#define org_sqlite_jni_capi_CApi_SQLITE_REINDEX 27L
+#undef org_sqlite_jni_capi_CApi_SQLITE_ANALYZE
+#define org_sqlite_jni_capi_CApi_SQLITE_ANALYZE 28L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CREATE_VTABLE
+#define org_sqlite_jni_capi_CApi_SQLITE_CREATE_VTABLE 29L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DROP_VTABLE
+#define org_sqlite_jni_capi_CApi_SQLITE_DROP_VTABLE 30L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FUNCTION
+#define org_sqlite_jni_capi_CApi_SQLITE_FUNCTION 31L
+#undef org_sqlite_jni_capi_CApi_SQLITE_SAVEPOINT
+#define org_sqlite_jni_capi_CApi_SQLITE_SAVEPOINT 32L
+#undef org_sqlite_jni_capi_CApi_SQLITE_RECURSIVE
+#define org_sqlite_jni_capi_CApi_SQLITE_RECURSIVE 33L
+#undef org_sqlite_jni_capi_CApi_SQLITE_STATIC
+#define org_sqlite_jni_capi_CApi_SQLITE_STATIC 0LL
+#undef org_sqlite_jni_capi_CApi_SQLITE_TRANSIENT
+#define org_sqlite_jni_capi_CApi_SQLITE_TRANSIENT -1LL
+#undef org_sqlite_jni_capi_CApi_SQLITE_CHANGESETSTART_INVERT
+#define org_sqlite_jni_capi_CApi_SQLITE_CHANGESETSTART_INVERT 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CHANGESETAPPLY_NOSAVEPOINT
+#define org_sqlite_jni_capi_CApi_SQLITE_CHANGESETAPPLY_NOSAVEPOINT 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CHANGESETAPPLY_INVERT
+#define org_sqlite_jni_capi_CApi_SQLITE_CHANGESETAPPLY_INVERT 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CHANGESETAPPLY_IGNORENOOP
+#define org_sqlite_jni_capi_CApi_SQLITE_CHANGESETAPPLY_IGNORENOOP 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CHANGESET_DATA
+#define org_sqlite_jni_capi_CApi_SQLITE_CHANGESET_DATA 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CHANGESET_NOTFOUND
+#define org_sqlite_jni_capi_CApi_SQLITE_CHANGESET_NOTFOUND 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CHANGESET_CONFLICT
+#define org_sqlite_jni_capi_CApi_SQLITE_CHANGESET_CONFLICT 3L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CHANGESET_CONSTRAINT
+#define org_sqlite_jni_capi_CApi_SQLITE_CHANGESET_CONSTRAINT 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CHANGESET_FOREIGN_KEY
+#define org_sqlite_jni_capi_CApi_SQLITE_CHANGESET_FOREIGN_KEY 5L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CHANGESET_OMIT
+#define org_sqlite_jni_capi_CApi_SQLITE_CHANGESET_OMIT 0L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CHANGESET_REPLACE
+#define org_sqlite_jni_capi_CApi_SQLITE_CHANGESET_REPLACE 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CHANGESET_ABORT
+#define org_sqlite_jni_capi_CApi_SQLITE_CHANGESET_ABORT 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_SINGLETHREAD
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_SINGLETHREAD 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_MULTITHREAD
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_MULTITHREAD 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_SERIALIZED
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_SERIALIZED 3L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_MALLOC
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_MALLOC 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_GETMALLOC
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_GETMALLOC 5L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_SCRATCH
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_SCRATCH 6L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_PAGECACHE
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_PAGECACHE 7L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_HEAP
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_HEAP 8L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_MEMSTATUS
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_MEMSTATUS 9L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_MUTEX
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_MUTEX 10L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_GETMUTEX
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_GETMUTEX 11L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_LOOKASIDE
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_LOOKASIDE 13L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_PCACHE
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_PCACHE 14L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_GETPCACHE
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_GETPCACHE 15L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_LOG
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_LOG 16L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_URI
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_URI 17L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_PCACHE2
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_PCACHE2 18L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_GETPCACHE2
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_GETPCACHE2 19L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_COVERING_INDEX_SCAN
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_COVERING_INDEX_SCAN 20L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_SQLLOG
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_SQLLOG 21L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_MMAP_SIZE
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_MMAP_SIZE 22L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_WIN32_HEAPSIZE
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_WIN32_HEAPSIZE 23L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_PCACHE_HDRSZ
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_PCACHE_HDRSZ 24L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_PMASZ
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_PMASZ 25L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_STMTJRNL_SPILL
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_STMTJRNL_SPILL 26L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_SMALL_MALLOC
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_SMALL_MALLOC 27L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_SORTERREF_SIZE
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_SORTERREF_SIZE 28L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONFIG_MEMDB_MAXSIZE
+#define org_sqlite_jni_capi_CApi_SQLITE_CONFIG_MEMDB_MAXSIZE 29L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INTEGER
+#define org_sqlite_jni_capi_CApi_SQLITE_INTEGER 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FLOAT
+#define org_sqlite_jni_capi_CApi_SQLITE_FLOAT 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_TEXT
+#define org_sqlite_jni_capi_CApi_SQLITE_TEXT 3L
+#undef org_sqlite_jni_capi_CApi_SQLITE_BLOB
+#define org_sqlite_jni_capi_CApi_SQLITE_BLOB 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_NULL
+#define org_sqlite_jni_capi_CApi_SQLITE_NULL 5L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_MAINDBNAME
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_MAINDBNAME 1000L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_LOOKASIDE
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_LOOKASIDE 1001L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_ENABLE_FKEY
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_ENABLE_FKEY 1002L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_ENABLE_TRIGGER
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_ENABLE_TRIGGER 1003L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER 1004L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION 1005L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE 1006L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_ENABLE_QPSG
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_ENABLE_QPSG 1007L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_TRIGGER_EQP
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_TRIGGER_EQP 1008L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_RESET_DATABASE
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_RESET_DATABASE 1009L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_DEFENSIVE
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_DEFENSIVE 1010L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_WRITABLE_SCHEMA
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_WRITABLE_SCHEMA 1011L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_LEGACY_ALTER_TABLE
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_LEGACY_ALTER_TABLE 1012L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_DQS_DML
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_DQS_DML 1013L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_DQS_DDL
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_DQS_DDL 1014L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_ENABLE_VIEW
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_ENABLE_VIEW 1015L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_LEGACY_FILE_FORMAT
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_LEGACY_FILE_FORMAT 1016L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_TRUSTED_SCHEMA
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_TRUSTED_SCHEMA 1017L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_STMT_SCANSTATUS
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_STMT_SCANSTATUS 1018L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_REVERSE_SCANORDER
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_REVERSE_SCANORDER 1019L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_MAX
+#define org_sqlite_jni_capi_CApi_SQLITE_DBCONFIG_MAX 1019L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_LOOKASIDE_USED
+#define org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_LOOKASIDE_USED 0L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_CACHE_USED
+#define org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_CACHE_USED 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_SCHEMA_USED
+#define org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_SCHEMA_USED 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_STMT_USED
+#define org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_STMT_USED 3L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_LOOKASIDE_HIT
+#define org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_LOOKASIDE_HIT 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE
+#define org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE 5L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL
+#define org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL 6L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_CACHE_HIT
+#define org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_CACHE_HIT 7L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_CACHE_MISS
+#define org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_CACHE_MISS 8L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_CACHE_WRITE
+#define org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_CACHE_WRITE 9L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_DEFERRED_FKS
+#define org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_DEFERRED_FKS 10L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_CACHE_USED_SHARED
+#define org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_CACHE_USED_SHARED 11L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_CACHE_SPILL
+#define org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_CACHE_SPILL 12L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_MAX
+#define org_sqlite_jni_capi_CApi_SQLITE_DBSTATUS_MAX 12L
+#undef org_sqlite_jni_capi_CApi_SQLITE_UTF8
+#define org_sqlite_jni_capi_CApi_SQLITE_UTF8 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_UTF16LE
+#define org_sqlite_jni_capi_CApi_SQLITE_UTF16LE 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_UTF16BE
+#define org_sqlite_jni_capi_CApi_SQLITE_UTF16BE 3L
+#undef org_sqlite_jni_capi_CApi_SQLITE_UTF16
+#define org_sqlite_jni_capi_CApi_SQLITE_UTF16 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_UTF16_ALIGNED
+#define org_sqlite_jni_capi_CApi_SQLITE_UTF16_ALIGNED 8L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_LOCKSTATE
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_LOCKSTATE 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_GET_LOCKPROXYFILE
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_GET_LOCKPROXYFILE 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_SET_LOCKPROXYFILE
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_SET_LOCKPROXYFILE 3L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_LAST_ERRNO
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_LAST_ERRNO 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_SIZE_HINT
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_SIZE_HINT 5L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_CHUNK_SIZE
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_CHUNK_SIZE 6L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_FILE_POINTER
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_FILE_POINTER 7L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_SYNC_OMITTED
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_SYNC_OMITTED 8L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_WIN32_AV_RETRY
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_WIN32_AV_RETRY 9L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_PERSIST_WAL
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_PERSIST_WAL 10L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_OVERWRITE
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_OVERWRITE 11L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_VFSNAME
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_VFSNAME 12L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_POWERSAFE_OVERWRITE
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_POWERSAFE_OVERWRITE 13L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_PRAGMA
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_PRAGMA 14L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_BUSYHANDLER
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_BUSYHANDLER 15L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_TEMPFILENAME
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_TEMPFILENAME 16L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_MMAP_SIZE
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_MMAP_SIZE 18L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_TRACE
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_TRACE 19L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_HAS_MOVED
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_HAS_MOVED 20L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_SYNC
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_SYNC 21L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_COMMIT_PHASETWO
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_COMMIT_PHASETWO 22L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_WIN32_SET_HANDLE
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_WIN32_SET_HANDLE 23L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_WAL_BLOCK
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_WAL_BLOCK 24L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_ZIPVFS
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_ZIPVFS 25L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_RBU
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_RBU 26L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_VFS_POINTER
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_VFS_POINTER 27L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_JOURNAL_POINTER
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_JOURNAL_POINTER 28L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_WIN32_GET_HANDLE
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_WIN32_GET_HANDLE 29L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_PDB
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_PDB 30L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_BEGIN_ATOMIC_WRITE
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_BEGIN_ATOMIC_WRITE 31L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_COMMIT_ATOMIC_WRITE
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_COMMIT_ATOMIC_WRITE 32L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE 33L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_LOCK_TIMEOUT
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_LOCK_TIMEOUT 34L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_DATA_VERSION
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_DATA_VERSION 35L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_SIZE_LIMIT
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_SIZE_LIMIT 36L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_CKPT_DONE
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_CKPT_DONE 37L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_RESERVE_BYTES
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_RESERVE_BYTES 38L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_CKPT_START
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_CKPT_START 39L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_EXTERNAL_READER
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_EXTERNAL_READER 40L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_CKSM_FILE
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_CKSM_FILE 41L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FCNTL_RESET_CACHE
+#define org_sqlite_jni_capi_CApi_SQLITE_FCNTL_RESET_CACHE 42L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LOCK_NONE
+#define org_sqlite_jni_capi_CApi_SQLITE_LOCK_NONE 0L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LOCK_SHARED
+#define org_sqlite_jni_capi_CApi_SQLITE_LOCK_SHARED 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LOCK_RESERVED
+#define org_sqlite_jni_capi_CApi_SQLITE_LOCK_RESERVED 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LOCK_PENDING
+#define org_sqlite_jni_capi_CApi_SQLITE_LOCK_PENDING 3L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LOCK_EXCLUSIVE
+#define org_sqlite_jni_capi_CApi_SQLITE_LOCK_EXCLUSIVE 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOCAP_ATOMIC
+#define org_sqlite_jni_capi_CApi_SQLITE_IOCAP_ATOMIC 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOCAP_ATOMIC512
+#define org_sqlite_jni_capi_CApi_SQLITE_IOCAP_ATOMIC512 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOCAP_ATOMIC1K
+#define org_sqlite_jni_capi_CApi_SQLITE_IOCAP_ATOMIC1K 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOCAP_ATOMIC2K
+#define org_sqlite_jni_capi_CApi_SQLITE_IOCAP_ATOMIC2K 8L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOCAP_ATOMIC4K
+#define org_sqlite_jni_capi_CApi_SQLITE_IOCAP_ATOMIC4K 16L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOCAP_ATOMIC8K
+#define org_sqlite_jni_capi_CApi_SQLITE_IOCAP_ATOMIC8K 32L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOCAP_ATOMIC16K
+#define org_sqlite_jni_capi_CApi_SQLITE_IOCAP_ATOMIC16K 64L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOCAP_ATOMIC32K
+#define org_sqlite_jni_capi_CApi_SQLITE_IOCAP_ATOMIC32K 128L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOCAP_ATOMIC64K
+#define org_sqlite_jni_capi_CApi_SQLITE_IOCAP_ATOMIC64K 256L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOCAP_SAFE_APPEND
+#define org_sqlite_jni_capi_CApi_SQLITE_IOCAP_SAFE_APPEND 512L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOCAP_SEQUENTIAL
+#define org_sqlite_jni_capi_CApi_SQLITE_IOCAP_SEQUENTIAL 1024L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN
+#define org_sqlite_jni_capi_CApi_SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN 2048L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOCAP_POWERSAFE_OVERWRITE
+#define org_sqlite_jni_capi_CApi_SQLITE_IOCAP_POWERSAFE_OVERWRITE 4096L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOCAP_IMMUTABLE
+#define org_sqlite_jni_capi_CApi_SQLITE_IOCAP_IMMUTABLE 8192L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOCAP_BATCH_ATOMIC
+#define org_sqlite_jni_capi_CApi_SQLITE_IOCAP_BATCH_ATOMIC 16384L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LIMIT_LENGTH
+#define org_sqlite_jni_capi_CApi_SQLITE_LIMIT_LENGTH 0L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LIMIT_SQL_LENGTH
+#define org_sqlite_jni_capi_CApi_SQLITE_LIMIT_SQL_LENGTH 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LIMIT_COLUMN
+#define org_sqlite_jni_capi_CApi_SQLITE_LIMIT_COLUMN 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LIMIT_EXPR_DEPTH
+#define org_sqlite_jni_capi_CApi_SQLITE_LIMIT_EXPR_DEPTH 3L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LIMIT_COMPOUND_SELECT
+#define org_sqlite_jni_capi_CApi_SQLITE_LIMIT_COMPOUND_SELECT 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LIMIT_VDBE_OP
+#define org_sqlite_jni_capi_CApi_SQLITE_LIMIT_VDBE_OP 5L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LIMIT_FUNCTION_ARG
+#define org_sqlite_jni_capi_CApi_SQLITE_LIMIT_FUNCTION_ARG 6L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LIMIT_ATTACHED
+#define org_sqlite_jni_capi_CApi_SQLITE_LIMIT_ATTACHED 7L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LIMIT_LIKE_PATTERN_LENGTH
+#define org_sqlite_jni_capi_CApi_SQLITE_LIMIT_LIKE_PATTERN_LENGTH 8L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LIMIT_VARIABLE_NUMBER
+#define org_sqlite_jni_capi_CApi_SQLITE_LIMIT_VARIABLE_NUMBER 9L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LIMIT_TRIGGER_DEPTH
+#define org_sqlite_jni_capi_CApi_SQLITE_LIMIT_TRIGGER_DEPTH 10L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LIMIT_WORKER_THREADS
+#define org_sqlite_jni_capi_CApi_SQLITE_LIMIT_WORKER_THREADS 11L
+#undef org_sqlite_jni_capi_CApi_SQLITE_OPEN_READONLY
+#define org_sqlite_jni_capi_CApi_SQLITE_OPEN_READONLY 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_OPEN_READWRITE
+#define org_sqlite_jni_capi_CApi_SQLITE_OPEN_READWRITE 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_OPEN_CREATE
+#define org_sqlite_jni_capi_CApi_SQLITE_OPEN_CREATE 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_OPEN_URI
+#define org_sqlite_jni_capi_CApi_SQLITE_OPEN_URI 64L
+#undef org_sqlite_jni_capi_CApi_SQLITE_OPEN_MEMORY
+#define org_sqlite_jni_capi_CApi_SQLITE_OPEN_MEMORY 128L
+#undef org_sqlite_jni_capi_CApi_SQLITE_OPEN_NOMUTEX
+#define org_sqlite_jni_capi_CApi_SQLITE_OPEN_NOMUTEX 32768L
+#undef org_sqlite_jni_capi_CApi_SQLITE_OPEN_FULLMUTEX
+#define org_sqlite_jni_capi_CApi_SQLITE_OPEN_FULLMUTEX 65536L
+#undef org_sqlite_jni_capi_CApi_SQLITE_OPEN_SHAREDCACHE
+#define org_sqlite_jni_capi_CApi_SQLITE_OPEN_SHAREDCACHE 131072L
+#undef org_sqlite_jni_capi_CApi_SQLITE_OPEN_PRIVATECACHE
+#define org_sqlite_jni_capi_CApi_SQLITE_OPEN_PRIVATECACHE 262144L
+#undef org_sqlite_jni_capi_CApi_SQLITE_OPEN_NOFOLLOW
+#define org_sqlite_jni_capi_CApi_SQLITE_OPEN_NOFOLLOW 16777216L
+#undef org_sqlite_jni_capi_CApi_SQLITE_OPEN_EXRESCODE
+#define org_sqlite_jni_capi_CApi_SQLITE_OPEN_EXRESCODE 33554432L
+#undef org_sqlite_jni_capi_CApi_SQLITE_PREPARE_PERSISTENT
+#define org_sqlite_jni_capi_CApi_SQLITE_PREPARE_PERSISTENT 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_PREPARE_NORMALIZE
+#define org_sqlite_jni_capi_CApi_SQLITE_PREPARE_NORMALIZE 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_PREPARE_NO_VTAB
+#define org_sqlite_jni_capi_CApi_SQLITE_PREPARE_NO_VTAB 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_OK
+#define org_sqlite_jni_capi_CApi_SQLITE_OK 0L
+#undef org_sqlite_jni_capi_CApi_SQLITE_ERROR
+#define org_sqlite_jni_capi_CApi_SQLITE_ERROR 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INTERNAL
+#define org_sqlite_jni_capi_CApi_SQLITE_INTERNAL 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_PERM
+#define org_sqlite_jni_capi_CApi_SQLITE_PERM 3L
+#undef org_sqlite_jni_capi_CApi_SQLITE_ABORT
+#define org_sqlite_jni_capi_CApi_SQLITE_ABORT 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_BUSY
+#define org_sqlite_jni_capi_CApi_SQLITE_BUSY 5L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LOCKED
+#define org_sqlite_jni_capi_CApi_SQLITE_LOCKED 6L
+#undef org_sqlite_jni_capi_CApi_SQLITE_NOMEM
+#define org_sqlite_jni_capi_CApi_SQLITE_NOMEM 7L
+#undef org_sqlite_jni_capi_CApi_SQLITE_READONLY
+#define org_sqlite_jni_capi_CApi_SQLITE_READONLY 8L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INTERRUPT
+#define org_sqlite_jni_capi_CApi_SQLITE_INTERRUPT 9L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR 10L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CORRUPT
+#define org_sqlite_jni_capi_CApi_SQLITE_CORRUPT 11L
+#undef org_sqlite_jni_capi_CApi_SQLITE_NOTFOUND
+#define org_sqlite_jni_capi_CApi_SQLITE_NOTFOUND 12L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FULL
+#define org_sqlite_jni_capi_CApi_SQLITE_FULL 13L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CANTOPEN
+#define org_sqlite_jni_capi_CApi_SQLITE_CANTOPEN 14L
+#undef org_sqlite_jni_capi_CApi_SQLITE_PROTOCOL
+#define org_sqlite_jni_capi_CApi_SQLITE_PROTOCOL 15L
+#undef org_sqlite_jni_capi_CApi_SQLITE_EMPTY
+#define org_sqlite_jni_capi_CApi_SQLITE_EMPTY 16L
+#undef org_sqlite_jni_capi_CApi_SQLITE_SCHEMA
+#define org_sqlite_jni_capi_CApi_SQLITE_SCHEMA 17L
+#undef org_sqlite_jni_capi_CApi_SQLITE_TOOBIG
+#define org_sqlite_jni_capi_CApi_SQLITE_TOOBIG 18L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT
+#define org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT 19L
+#undef org_sqlite_jni_capi_CApi_SQLITE_MISMATCH
+#define org_sqlite_jni_capi_CApi_SQLITE_MISMATCH 20L
+#undef org_sqlite_jni_capi_CApi_SQLITE_MISUSE
+#define org_sqlite_jni_capi_CApi_SQLITE_MISUSE 21L
+#undef org_sqlite_jni_capi_CApi_SQLITE_NOLFS
+#define org_sqlite_jni_capi_CApi_SQLITE_NOLFS 22L
+#undef org_sqlite_jni_capi_CApi_SQLITE_AUTH
+#define org_sqlite_jni_capi_CApi_SQLITE_AUTH 23L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FORMAT
+#define org_sqlite_jni_capi_CApi_SQLITE_FORMAT 24L
+#undef org_sqlite_jni_capi_CApi_SQLITE_RANGE
+#define org_sqlite_jni_capi_CApi_SQLITE_RANGE 25L
+#undef org_sqlite_jni_capi_CApi_SQLITE_NOTADB
+#define org_sqlite_jni_capi_CApi_SQLITE_NOTADB 26L
+#undef org_sqlite_jni_capi_CApi_SQLITE_NOTICE
+#define org_sqlite_jni_capi_CApi_SQLITE_NOTICE 27L
+#undef org_sqlite_jni_capi_CApi_SQLITE_WARNING
+#define org_sqlite_jni_capi_CApi_SQLITE_WARNING 28L
+#undef org_sqlite_jni_capi_CApi_SQLITE_ROW
+#define org_sqlite_jni_capi_CApi_SQLITE_ROW 100L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DONE
+#define org_sqlite_jni_capi_CApi_SQLITE_DONE 101L
+#undef org_sqlite_jni_capi_CApi_SQLITE_ERROR_MISSING_COLLSEQ
+#define org_sqlite_jni_capi_CApi_SQLITE_ERROR_MISSING_COLLSEQ 257L
+#undef org_sqlite_jni_capi_CApi_SQLITE_ERROR_RETRY
+#define org_sqlite_jni_capi_CApi_SQLITE_ERROR_RETRY 513L
+#undef org_sqlite_jni_capi_CApi_SQLITE_ERROR_SNAPSHOT
+#define org_sqlite_jni_capi_CApi_SQLITE_ERROR_SNAPSHOT 769L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_READ
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_READ 266L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_SHORT_READ
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_SHORT_READ 522L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_WRITE
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_WRITE 778L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_FSYNC
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_FSYNC 1034L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_DIR_FSYNC
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_DIR_FSYNC 1290L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_TRUNCATE
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_TRUNCATE 1546L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_FSTAT
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_FSTAT 1802L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_UNLOCK
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_UNLOCK 2058L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_RDLOCK
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_RDLOCK 2314L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_DELETE
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_DELETE 2570L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_BLOCKED
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_BLOCKED 2826L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_NOMEM
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_NOMEM 3082L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_ACCESS
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_ACCESS 3338L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_CHECKRESERVEDLOCK
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_CHECKRESERVEDLOCK 3594L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_LOCK
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_LOCK 3850L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_CLOSE
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_CLOSE 4106L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_DIR_CLOSE
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_DIR_CLOSE 4362L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_SHMOPEN
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_SHMOPEN 4618L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_SHMSIZE
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_SHMSIZE 4874L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_SHMLOCK
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_SHMLOCK 5130L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_SHMMAP
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_SHMMAP 5386L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_SEEK
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_SEEK 5642L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_DELETE_NOENT
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_DELETE_NOENT 5898L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_MMAP
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_MMAP 6154L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_GETTEMPPATH
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_GETTEMPPATH 6410L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_CONVPATH
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_CONVPATH 6666L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_VNODE
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_VNODE 6922L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_AUTH
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_AUTH 7178L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_BEGIN_ATOMIC
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_BEGIN_ATOMIC 7434L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_COMMIT_ATOMIC
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_COMMIT_ATOMIC 7690L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_ROLLBACK_ATOMIC
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_ROLLBACK_ATOMIC 7946L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_DATA
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_DATA 8202L
+#undef org_sqlite_jni_capi_CApi_SQLITE_IOERR_CORRUPTFS
+#define org_sqlite_jni_capi_CApi_SQLITE_IOERR_CORRUPTFS 8458L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LOCKED_SHAREDCACHE
+#define org_sqlite_jni_capi_CApi_SQLITE_LOCKED_SHAREDCACHE 262L
+#undef org_sqlite_jni_capi_CApi_SQLITE_LOCKED_VTAB
+#define org_sqlite_jni_capi_CApi_SQLITE_LOCKED_VTAB 518L
+#undef org_sqlite_jni_capi_CApi_SQLITE_BUSY_RECOVERY
+#define org_sqlite_jni_capi_CApi_SQLITE_BUSY_RECOVERY 261L
+#undef org_sqlite_jni_capi_CApi_SQLITE_BUSY_SNAPSHOT
+#define org_sqlite_jni_capi_CApi_SQLITE_BUSY_SNAPSHOT 517L
+#undef org_sqlite_jni_capi_CApi_SQLITE_BUSY_TIMEOUT
+#define org_sqlite_jni_capi_CApi_SQLITE_BUSY_TIMEOUT 773L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CANTOPEN_NOTEMPDIR
+#define org_sqlite_jni_capi_CApi_SQLITE_CANTOPEN_NOTEMPDIR 270L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CANTOPEN_ISDIR
+#define org_sqlite_jni_capi_CApi_SQLITE_CANTOPEN_ISDIR 526L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CANTOPEN_FULLPATH
+#define org_sqlite_jni_capi_CApi_SQLITE_CANTOPEN_FULLPATH 782L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CANTOPEN_CONVPATH
+#define org_sqlite_jni_capi_CApi_SQLITE_CANTOPEN_CONVPATH 1038L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CANTOPEN_SYMLINK
+#define org_sqlite_jni_capi_CApi_SQLITE_CANTOPEN_SYMLINK 1550L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CORRUPT_VTAB
+#define org_sqlite_jni_capi_CApi_SQLITE_CORRUPT_VTAB 267L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CORRUPT_SEQUENCE
+#define org_sqlite_jni_capi_CApi_SQLITE_CORRUPT_SEQUENCE 523L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CORRUPT_INDEX
+#define org_sqlite_jni_capi_CApi_SQLITE_CORRUPT_INDEX 779L
+#undef org_sqlite_jni_capi_CApi_SQLITE_READONLY_RECOVERY
+#define org_sqlite_jni_capi_CApi_SQLITE_READONLY_RECOVERY 264L
+#undef org_sqlite_jni_capi_CApi_SQLITE_READONLY_CANTLOCK
+#define org_sqlite_jni_capi_CApi_SQLITE_READONLY_CANTLOCK 520L
+#undef org_sqlite_jni_capi_CApi_SQLITE_READONLY_ROLLBACK
+#define org_sqlite_jni_capi_CApi_SQLITE_READONLY_ROLLBACK 776L
+#undef org_sqlite_jni_capi_CApi_SQLITE_READONLY_DBMOVED
+#define org_sqlite_jni_capi_CApi_SQLITE_READONLY_DBMOVED 1032L
+#undef org_sqlite_jni_capi_CApi_SQLITE_READONLY_CANTINIT
+#define org_sqlite_jni_capi_CApi_SQLITE_READONLY_CANTINIT 1288L
+#undef org_sqlite_jni_capi_CApi_SQLITE_READONLY_DIRECTORY
+#define org_sqlite_jni_capi_CApi_SQLITE_READONLY_DIRECTORY 1544L
+#undef org_sqlite_jni_capi_CApi_SQLITE_ABORT_ROLLBACK
+#define org_sqlite_jni_capi_CApi_SQLITE_ABORT_ROLLBACK 516L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_CHECK
+#define org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_CHECK 275L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_COMMITHOOK
+#define org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_COMMITHOOK 531L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_FOREIGNKEY
+#define org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_FOREIGNKEY 787L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_FUNCTION
+#define org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_FUNCTION 1043L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_NOTNULL
+#define org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_NOTNULL 1299L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_PRIMARYKEY
+#define org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_PRIMARYKEY 1555L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_TRIGGER
+#define org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_TRIGGER 1811L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_UNIQUE
+#define org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_UNIQUE 2067L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_VTAB
+#define org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_VTAB 2323L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_ROWID
+#define org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_ROWID 2579L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_PINNED
+#define org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_PINNED 2835L
+#undef org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_DATATYPE
+#define org_sqlite_jni_capi_CApi_SQLITE_CONSTRAINT_DATATYPE 3091L
+#undef org_sqlite_jni_capi_CApi_SQLITE_NOTICE_RECOVER_WAL
+#define org_sqlite_jni_capi_CApi_SQLITE_NOTICE_RECOVER_WAL 283L
+#undef org_sqlite_jni_capi_CApi_SQLITE_NOTICE_RECOVER_ROLLBACK
+#define org_sqlite_jni_capi_CApi_SQLITE_NOTICE_RECOVER_ROLLBACK 539L
+#undef org_sqlite_jni_capi_CApi_SQLITE_WARNING_AUTOINDEX
+#define org_sqlite_jni_capi_CApi_SQLITE_WARNING_AUTOINDEX 284L
+#undef org_sqlite_jni_capi_CApi_SQLITE_AUTH_USER
+#define org_sqlite_jni_capi_CApi_SQLITE_AUTH_USER 279L
+#undef org_sqlite_jni_capi_CApi_SQLITE_OK_LOAD_PERMANENTLY
+#define org_sqlite_jni_capi_CApi_SQLITE_OK_LOAD_PERMANENTLY 256L
+#undef org_sqlite_jni_capi_CApi_SQLITE_SERIALIZE_NOCOPY
+#define org_sqlite_jni_capi_CApi_SQLITE_SERIALIZE_NOCOPY 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DESERIALIZE_FREEONCLOSE
+#define org_sqlite_jni_capi_CApi_SQLITE_DESERIALIZE_FREEONCLOSE 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DESERIALIZE_READONLY
+#define org_sqlite_jni_capi_CApi_SQLITE_DESERIALIZE_READONLY 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DESERIALIZE_RESIZEABLE
+#define org_sqlite_jni_capi_CApi_SQLITE_DESERIALIZE_RESIZEABLE 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_SESSION_CONFIG_STRMSIZE
+#define org_sqlite_jni_capi_CApi_SQLITE_SESSION_CONFIG_STRMSIZE 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_SESSION_OBJCONFIG_SIZE
+#define org_sqlite_jni_capi_CApi_SQLITE_SESSION_OBJCONFIG_SIZE 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_STATUS_MEMORY_USED
+#define org_sqlite_jni_capi_CApi_SQLITE_STATUS_MEMORY_USED 0L
+#undef org_sqlite_jni_capi_CApi_SQLITE_STATUS_PAGECACHE_USED
+#define org_sqlite_jni_capi_CApi_SQLITE_STATUS_PAGECACHE_USED 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_STATUS_PAGECACHE_OVERFLOW
+#define org_sqlite_jni_capi_CApi_SQLITE_STATUS_PAGECACHE_OVERFLOW 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_STATUS_MALLOC_SIZE
+#define org_sqlite_jni_capi_CApi_SQLITE_STATUS_MALLOC_SIZE 5L
+#undef org_sqlite_jni_capi_CApi_SQLITE_STATUS_PARSER_STACK
+#define org_sqlite_jni_capi_CApi_SQLITE_STATUS_PARSER_STACK 6L
+#undef org_sqlite_jni_capi_CApi_SQLITE_STATUS_PAGECACHE_SIZE
+#define org_sqlite_jni_capi_CApi_SQLITE_STATUS_PAGECACHE_SIZE 7L
+#undef org_sqlite_jni_capi_CApi_SQLITE_STATUS_MALLOC_COUNT
+#define org_sqlite_jni_capi_CApi_SQLITE_STATUS_MALLOC_COUNT 9L
+#undef org_sqlite_jni_capi_CApi_SQLITE_STMTSTATUS_FULLSCAN_STEP
+#define org_sqlite_jni_capi_CApi_SQLITE_STMTSTATUS_FULLSCAN_STEP 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_STMTSTATUS_SORT
+#define org_sqlite_jni_capi_CApi_SQLITE_STMTSTATUS_SORT 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_STMTSTATUS_AUTOINDEX
+#define org_sqlite_jni_capi_CApi_SQLITE_STMTSTATUS_AUTOINDEX 3L
+#undef org_sqlite_jni_capi_CApi_SQLITE_STMTSTATUS_VM_STEP
+#define org_sqlite_jni_capi_CApi_SQLITE_STMTSTATUS_VM_STEP 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_STMTSTATUS_REPREPARE
+#define org_sqlite_jni_capi_CApi_SQLITE_STMTSTATUS_REPREPARE 5L
+#undef org_sqlite_jni_capi_CApi_SQLITE_STMTSTATUS_RUN
+#define org_sqlite_jni_capi_CApi_SQLITE_STMTSTATUS_RUN 6L
+#undef org_sqlite_jni_capi_CApi_SQLITE_STMTSTATUS_FILTER_MISS
+#define org_sqlite_jni_capi_CApi_SQLITE_STMTSTATUS_FILTER_MISS 7L
+#undef org_sqlite_jni_capi_CApi_SQLITE_STMTSTATUS_FILTER_HIT
+#define org_sqlite_jni_capi_CApi_SQLITE_STMTSTATUS_FILTER_HIT 8L
+#undef org_sqlite_jni_capi_CApi_SQLITE_STMTSTATUS_MEMUSED
+#define org_sqlite_jni_capi_CApi_SQLITE_STMTSTATUS_MEMUSED 99L
+#undef org_sqlite_jni_capi_CApi_SQLITE_SYNC_NORMAL
+#define org_sqlite_jni_capi_CApi_SQLITE_SYNC_NORMAL 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_SYNC_FULL
+#define org_sqlite_jni_capi_CApi_SQLITE_SYNC_FULL 3L
+#undef org_sqlite_jni_capi_CApi_SQLITE_SYNC_DATAONLY
+#define org_sqlite_jni_capi_CApi_SQLITE_SYNC_DATAONLY 16L
+#undef org_sqlite_jni_capi_CApi_SQLITE_TRACE_STMT
+#define org_sqlite_jni_capi_CApi_SQLITE_TRACE_STMT 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_TRACE_PROFILE
+#define org_sqlite_jni_capi_CApi_SQLITE_TRACE_PROFILE 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_TRACE_ROW
+#define org_sqlite_jni_capi_CApi_SQLITE_TRACE_ROW 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_TRACE_CLOSE
+#define org_sqlite_jni_capi_CApi_SQLITE_TRACE_CLOSE 8L
+#undef org_sqlite_jni_capi_CApi_SQLITE_TXN_NONE
+#define org_sqlite_jni_capi_CApi_SQLITE_TXN_NONE 0L
+#undef org_sqlite_jni_capi_CApi_SQLITE_TXN_READ
+#define org_sqlite_jni_capi_CApi_SQLITE_TXN_READ 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_TXN_WRITE
+#define org_sqlite_jni_capi_CApi_SQLITE_TXN_WRITE 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DETERMINISTIC
+#define org_sqlite_jni_capi_CApi_SQLITE_DETERMINISTIC 2048L
+#undef org_sqlite_jni_capi_CApi_SQLITE_DIRECTONLY
+#define org_sqlite_jni_capi_CApi_SQLITE_DIRECTONLY 524288L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INNOCUOUS
+#define org_sqlite_jni_capi_CApi_SQLITE_INNOCUOUS 2097152L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INDEX_SCAN_UNIQUE
+#define org_sqlite_jni_capi_CApi_SQLITE_INDEX_SCAN_UNIQUE 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_EQ
+#define org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_EQ 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_GT
+#define org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_GT 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_LE
+#define org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_LE 8L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_LT
+#define org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_LT 16L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_GE
+#define org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_GE 32L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_MATCH
+#define org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_MATCH 64L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_LIKE
+#define org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_LIKE 65L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_GLOB
+#define org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_GLOB 66L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_REGEXP
+#define org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_REGEXP 67L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_NE
+#define org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_NE 68L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_ISNOT
+#define org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_ISNOT 69L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_ISNOTNULL
+#define org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_ISNOTNULL 70L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_ISNULL
+#define org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_ISNULL 71L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_IS
+#define org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_IS 72L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_LIMIT
+#define org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_LIMIT 73L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_OFFSET
+#define org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_OFFSET 74L
+#undef org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_FUNCTION
+#define org_sqlite_jni_capi_CApi_SQLITE_INDEX_CONSTRAINT_FUNCTION 150L
+#undef org_sqlite_jni_capi_CApi_SQLITE_VTAB_CONSTRAINT_SUPPORT
+#define org_sqlite_jni_capi_CApi_SQLITE_VTAB_CONSTRAINT_SUPPORT 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_VTAB_INNOCUOUS
+#define org_sqlite_jni_capi_CApi_SQLITE_VTAB_INNOCUOUS 2L
+#undef org_sqlite_jni_capi_CApi_SQLITE_VTAB_DIRECTONLY
+#define org_sqlite_jni_capi_CApi_SQLITE_VTAB_DIRECTONLY 3L
+#undef org_sqlite_jni_capi_CApi_SQLITE_VTAB_USES_ALL_SCHEMAS
+#define org_sqlite_jni_capi_CApi_SQLITE_VTAB_USES_ALL_SCHEMAS 4L
+#undef org_sqlite_jni_capi_CApi_SQLITE_ROLLBACK
+#define org_sqlite_jni_capi_CApi_SQLITE_ROLLBACK 1L
+#undef org_sqlite_jni_capi_CApi_SQLITE_FAIL
+#define org_sqlite_jni_capi_CApi_SQLITE_FAIL 3L
+#undef org_sqlite_jni_capi_CApi_SQLITE_REPLACE
+#define org_sqlite_jni_capi_CApi_SQLITE_REPLACE 5L
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    init
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_init
+  (JNIEnv *, jclass);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_java_uncache_thread
+ * Signature: ()Z
+ */
+JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1java_1uncache_1thread
+  (JNIEnv *, jclass);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_aggregate_context
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;Z)J
+ */
+JNIEXPORT jlong JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1aggregate_1context
+  (JNIEnv *, jclass, jobject, jboolean);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_auto_extension
+ * Signature: (Lorg/sqlite/jni/capi/AutoExtensionCallback;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1auto_1extension
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_backup_finish
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1backup_1finish
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_backup_init
+ * Signature: (JLjava/lang/String;JLjava/lang/String;)Lorg/sqlite/jni/capi/sqlite3_backup;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1backup_1init
+  (JNIEnv *, jclass, jlong, jstring, jlong, jstring);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_backup_pagecount
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1backup_1pagecount
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_backup_remaining
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1backup_1remaining
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_backup_step
+ * Signature: (JI)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1backup_1step
+  (JNIEnv *, jclass, jlong, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_bind_blob
+ * Signature: (JI[BI)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1bind_1blob
+  (JNIEnv *, jclass, jlong, jint, jbyteArray, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_bind_double
+ * Signature: (JID)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1bind_1double
+  (JNIEnv *, jclass, jlong, jint, jdouble);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_bind_int
+ * Signature: (JII)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1bind_1int
+  (JNIEnv *, jclass, jlong, jint, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_bind_int64
+ * Signature: (JIJ)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1bind_1int64
+  (JNIEnv *, jclass, jlong, jint, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_bind_java_object
+ * Signature: (JILjava/lang/Object;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1bind_1java_1object
+  (JNIEnv *, jclass, jlong, jint, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_bind_null
+ * Signature: (JI)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1bind_1null
+  (JNIEnv *, jclass, jlong, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_bind_parameter_count
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1bind_1parameter_1count
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_bind_parameter_index
+ * Signature: (J[B)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1bind_1parameter_1index
+  (JNIEnv *, jclass, jlong, jbyteArray);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_bind_parameter_name
+ * Signature: (JI)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1bind_1parameter_1name
+  (JNIEnv *, jclass, jlong, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_bind_text
+ * Signature: (JI[BI)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1bind_1text
+  (JNIEnv *, jclass, jlong, jint, jbyteArray, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_bind_text16
+ * Signature: (JI[BI)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1bind_1text16
+  (JNIEnv *, jclass, jlong, jint, jbyteArray, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_bind_value
+ * Signature: (JIJ)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1bind_1value
+  (JNIEnv *, jclass, jlong, jint, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_bind_zeroblob
+ * Signature: (JII)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1bind_1zeroblob
+  (JNIEnv *, jclass, jlong, jint, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_bind_zeroblob64
+ * Signature: (JIJ)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1bind_1zeroblob64
+  (JNIEnv *, jclass, jlong, jint, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_blob_bytes
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1blob_1bytes
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_blob_close
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1blob_1close
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_blob_open
+ * Signature: (JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;JILorg/sqlite/jni/capi/OutputPointer/sqlite3_blob;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1blob_1open
+  (JNIEnv *, jclass, jlong, jstring, jstring, jstring, jlong, jint, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_blob_read
+ * Signature: (J[BI)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1blob_1read
+  (JNIEnv *, jclass, jlong, jbyteArray, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_blob_reopen
+ * Signature: (JJ)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1blob_1reopen
+  (JNIEnv *, jclass, jlong, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_blob_write
+ * Signature: (J[BI)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1blob_1write
+  (JNIEnv *, jclass, jlong, jbyteArray, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_busy_handler
+ * Signature: (JLorg/sqlite/jni/capi/BusyHandlerCallback;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1busy_1handler
+  (JNIEnv *, jclass, jlong, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_busy_timeout
+ * Signature: (JI)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1busy_1timeout
+  (JNIEnv *, jclass, jlong, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_cancel_auto_extension
+ * Signature: (Lorg/sqlite/jni/capi/AutoExtensionCallback;)Z
+ */
+JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1cancel_1auto_1extension
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_changes
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1changes
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_changes64
+ * Signature: (J)J
+ */
+JNIEXPORT jlong JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1changes64
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_clear_bindings
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1clear_1bindings
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_close
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1close
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_close_v2
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1close_1v2
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_column_blob
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_stmt;I)[B
+ */
+JNIEXPORT jbyteArray JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1column_1blob
+  (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_column_bytes
+ * Signature: (JI)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1column_1bytes
+  (JNIEnv *, jclass, jlong, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_column_bytes16
+ * Signature: (JI)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1column_1bytes16
+  (JNIEnv *, jclass, jlong, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_column_count
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1column_1count
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_column_decltype
+ * Signature: (JI)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1column_1decltype
+  (JNIEnv *, jclass, jlong, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_column_double
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_stmt;I)D
+ */
+JNIEXPORT jdouble JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1column_1double
+  (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_column_int
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_stmt;I)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1column_1int
+  (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_column_int64
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_stmt;I)J
+ */
+JNIEXPORT jlong JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1column_1int64
+  (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_column_name
+ * Signature: (JI)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1column_1name
+  (JNIEnv *, jclass, jlong, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_column_database_name
+ * Signature: (JI)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1column_1database_1name
+  (JNIEnv *, jclass, jlong, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_column_origin_name
+ * Signature: (JI)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1column_1origin_1name
+  (JNIEnv *, jclass, jlong, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_column_table_name
+ * Signature: (JI)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1column_1table_1name
+  (JNIEnv *, jclass, jlong, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_column_text
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_stmt;I)[B
+ */
+JNIEXPORT jbyteArray JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1column_1text
+  (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_column_text16
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_stmt;I)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1column_1text16
+  (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_column_type
+ * Signature: (JI)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1column_1type
+  (JNIEnv *, jclass, jlong, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_column_value
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_stmt;I)Lorg/sqlite/jni/capi/sqlite3_value;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1column_1value
+  (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_collation_needed
+ * Signature: (JLorg/sqlite/jni/capi/CollationNeededCallback;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1collation_1needed
+  (JNIEnv *, jclass, jlong, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_commit_hook
+ * Signature: (JLorg/sqlite/jni/capi/CommitHookCallback;)Lorg/sqlite/jni/capi/CommitHookCallback;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1commit_1hook
+  (JNIEnv *, jclass, jlong, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_compileoption_get
+ * Signature: (I)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1compileoption_1get
+  (JNIEnv *, jclass, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_compileoption_used
+ * Signature: (Ljava/lang/String;)Z
+ */
+JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1compileoption_1used
+  (JNIEnv *, jclass, jstring);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_complete
+ * Signature: ([B)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1complete
+  (JNIEnv *, jclass, jbyteArray);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_config
+ * Signature: (I)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1config__I
+  (JNIEnv *, jclass, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_config
+ * Signature: (Lorg/sqlite/jni/capi/ConfigSqllogCallback;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1config__Lorg_sqlite_jni_capi_ConfigSqllogCallback_2
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_config
+ * Signature: (Lorg/sqlite/jni/capi/ConfigLogCallback;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1config__Lorg_sqlite_jni_capi_ConfigLogCallback_2
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_context_db_handle
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;)Lorg/sqlite/jni/capi/sqlite3;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1context_1db_1handle
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_create_collation
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;Ljava/lang/String;ILorg/sqlite/jni/capi/CollationCallback;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1create_1collation
+  (JNIEnv *, jclass, jobject, jstring, jint, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_create_function
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;Ljava/lang/String;IILorg/sqlite/jni/capi/SQLFunction;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1create_1function
+  (JNIEnv *, jclass, jobject, jstring, jint, jint, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_data_count
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1data_1count
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_db_config
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;IILorg/sqlite/jni/capi/OutputPointer/Int32;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1db_1config__Lorg_sqlite_jni_capi_sqlite3_2IILorg_sqlite_jni_capi_OutputPointer_Int32_2
+  (JNIEnv *, jclass, jobject, jint, jint, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_db_config
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;ILjava/lang/String;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1db_1config__Lorg_sqlite_jni_capi_sqlite3_2ILjava_lang_String_2
+  (JNIEnv *, jclass, jobject, jint, jstring);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_db_name
+ * Signature: (JI)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1db_1name
+  (JNIEnv *, jclass, jlong, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_db_filename
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;Ljava/lang/String;)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1db_1filename
+  (JNIEnv *, jclass, jobject, jstring);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_db_handle
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_stmt;)Lorg/sqlite/jni/capi/sqlite3;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1db_1handle
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_db_readonly
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;Ljava/lang/String;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1db_1readonly
+  (JNIEnv *, jclass, jobject, jstring);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_db_release_memory
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1db_1release_1memory
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_db_status
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;ILorg/sqlite/jni/capi/OutputPointer/Int32;Lorg/sqlite/jni/capi/OutputPointer/Int32;Z)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1db_1status
+  (JNIEnv *, jclass, jobject, jint, jobject, jobject, jboolean);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_errcode
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1errcode
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_errmsg
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1errmsg
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_error_offset
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1error_1offset
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_errstr
+ * Signature: (I)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1errstr
+  (JNIEnv *, jclass, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_expanded_sql
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_stmt;)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1expanded_1sql
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_extended_errcode
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1extended_1errcode
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_extended_result_codes
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;Z)Z
+ */
+JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1extended_1result_1codes
+  (JNIEnv *, jclass, jobject, jboolean);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_get_autocommit
+ * Signature: (J)Z
+ */
+JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1get_1autocommit
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_get_auxdata
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;I)Ljava/lang/Object;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1get_1auxdata
+  (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_finalize
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1finalize
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_initialize
+ * Signature: ()I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1initialize
+  (JNIEnv *, jclass);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_interrupt
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1interrupt
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_is_interrupted
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;)Z
+ */
+JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1is_1interrupted
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_keyword_check
+ * Signature: (Ljava/lang/String;)Z
+ */
+JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1keyword_1check
+  (JNIEnv *, jclass, jstring);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_keyword_count
+ * Signature: ()I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1keyword_1count
+  (JNIEnv *, jclass);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_keyword_name
+ * Signature: (I)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1keyword_1name
+  (JNIEnv *, jclass, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_last_insert_rowid
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;)J
+ */
+JNIEXPORT jlong JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1last_1insert_1rowid
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_libversion
+ * Signature: ()Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1libversion
+  (JNIEnv *, jclass);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_libversion_number
+ * Signature: ()I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1libversion_1number
+  (JNIEnv *, jclass);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_limit
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;II)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1limit
+  (JNIEnv *, jclass, jobject, jint, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_normalized_sql
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_stmt;)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1normalized_1sql
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_open
+ * Signature: (Ljava/lang/String;Lorg/sqlite/jni/capi/OutputPointer/sqlite3;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1open
+  (JNIEnv *, jclass, jstring, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_open_v2
+ * Signature: (Ljava/lang/String;Lorg/sqlite/jni/capi/OutputPointer/sqlite3;ILjava/lang/String;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1open_1v2
+  (JNIEnv *, jclass, jstring, jobject, jint, jstring);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_prepare
+ * Signature: (J[BILorg/sqlite/jni/capi/OutputPointer/sqlite3_stmt;Lorg/sqlite/jni/capi/OutputPointer/Int32;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1prepare
+  (JNIEnv *, jclass, jlong, jbyteArray, jint, jobject, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_prepare_v2
+ * Signature: (J[BILorg/sqlite/jni/capi/OutputPointer/sqlite3_stmt;Lorg/sqlite/jni/capi/OutputPointer/Int32;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1prepare_1v2
+  (JNIEnv *, jclass, jlong, jbyteArray, jint, jobject, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_prepare_v3
+ * Signature: (J[BIILorg/sqlite/jni/capi/OutputPointer/sqlite3_stmt;Lorg/sqlite/jni/capi/OutputPointer/Int32;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1prepare_1v3
+  (JNIEnv *, jclass, jlong, jbyteArray, jint, jint, jobject, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_preupdate_blobwrite
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1preupdate_1blobwrite
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_preupdate_count
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1preupdate_1count
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_preupdate_depth
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1preupdate_1depth
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_preupdate_hook
+ * Signature: (JLorg/sqlite/jni/capi/PreupdateHookCallback;)Lorg/sqlite/jni/capi/PreupdateHookCallback;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1preupdate_1hook
+  (JNIEnv *, jclass, jlong, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_preupdate_new
+ * Signature: (JILorg/sqlite/jni/capi/OutputPointer/sqlite3_value;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1preupdate_1new
+  (JNIEnv *, jclass, jlong, jint, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_preupdate_old
+ * Signature: (JILorg/sqlite/jni/capi/OutputPointer/sqlite3_value;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1preupdate_1old
+  (JNIEnv *, jclass, jlong, jint, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_progress_handler
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;ILorg/sqlite/jni/capi/ProgressHandlerCallback;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1progress_1handler
+  (JNIEnv *, jclass, jobject, jint, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_randomness
+ * Signature: ([B)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1randomness
+  (JNIEnv *, jclass, jbyteArray);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_release_memory
+ * Signature: (I)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1release_1memory
+  (JNIEnv *, jclass, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_reset
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_stmt;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1reset
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_reset_auto_extension
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1reset_1auto_1extension
+  (JNIEnv *, jclass);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_result_double
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;D)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1double
+  (JNIEnv *, jclass, jobject, jdouble);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_result_error
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;[BI)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1error
+  (JNIEnv *, jclass, jobject, jbyteArray, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_result_error_toobig
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1error_1toobig
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_result_error_nomem
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1error_1nomem
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_result_error_code
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;I)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1error_1code
+  (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_result_null
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1null
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_result_int
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;I)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1int
+  (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_result_int64
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;J)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1int64
+  (JNIEnv *, jclass, jobject, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_result_java_object
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;Ljava/lang/Object;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1java_1object
+  (JNIEnv *, jclass, jobject, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_result_value
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;Lorg/sqlite/jni/capi/sqlite3_value;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1value
+  (JNIEnv *, jclass, jobject, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_result_zeroblob
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;I)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1zeroblob
+  (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_result_zeroblob64
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1zeroblob64
+  (JNIEnv *, jclass, jobject, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_result_blob
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;[BI)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1blob
+  (JNIEnv *, jclass, jobject, jbyteArray, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_result_blob64
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;[BJ)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1blob64
+  (JNIEnv *, jclass, jobject, jbyteArray, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_result_text
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;[BI)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1text
+  (JNIEnv *, jclass, jobject, jbyteArray, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_result_text64
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;[BJI)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1text64
+  (JNIEnv *, jclass, jobject, jbyteArray, jlong, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_rollback_hook
+ * Signature: (JLorg/sqlite/jni/capi/RollbackHookCallback;)Lorg/sqlite/jni/capi/RollbackHookCallback;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1rollback_1hook
+  (JNIEnv *, jclass, jlong, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_set_authorizer
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;Lorg/sqlite/jni/capi/AuthorizerCallback;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1set_1authorizer
+  (JNIEnv *, jclass, jobject, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_set_auxdata
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;ILjava/lang/Object;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1set_1auxdata
+  (JNIEnv *, jclass, jobject, jint, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_set_last_insert_rowid
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;J)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1set_1last_1insert_1rowid
+  (JNIEnv *, jclass, jobject, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_shutdown
+ * Signature: ()I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1shutdown
+  (JNIEnv *, jclass);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_sleep
+ * Signature: (I)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1sleep
+  (JNIEnv *, jclass, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_sourceid
+ * Signature: ()Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1sourceid
+  (JNIEnv *, jclass);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_sql
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_stmt;)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1sql
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_status
+ * Signature: (ILorg/sqlite/jni/capi/OutputPointer/Int32;Lorg/sqlite/jni/capi/OutputPointer/Int32;Z)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1status
+  (JNIEnv *, jclass, jint, jobject, jobject, jboolean);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_status64
+ * Signature: (ILorg/sqlite/jni/capi/OutputPointer/Int64;Lorg/sqlite/jni/capi/OutputPointer/Int64;Z)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1status64
+  (JNIEnv *, jclass, jint, jobject, jobject, jboolean);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_step
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_stmt;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1step
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_stmt_busy
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_stmt;)Z
+ */
+JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1stmt_1busy
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_stmt_explain
+ * Signature: (JI)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1stmt_1explain
+  (JNIEnv *, jclass, jlong, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_stmt_isexplain
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1stmt_1isexplain
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_stmt_readonly
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_stmt;)Z
+ */
+JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1stmt_1readonly
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_stmt_status
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_stmt;IZ)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1stmt_1status
+  (JNIEnv *, jclass, jobject, jint, jboolean);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_strglob
+ * Signature: ([B[B)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1strglob
+  (JNIEnv *, jclass, jbyteArray, jbyteArray);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_strlike
+ * Signature: ([B[BI)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1strlike
+  (JNIEnv *, jclass, jbyteArray, jbyteArray, jint);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_system_errno
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1system_1errno
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_table_column_metadata
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lorg/sqlite/jni/capi/OutputPointer/String;Lorg/sqlite/jni/capi/OutputPointer/String;Lorg/sqlite/jni/capi/OutputPointer/Bool;Lorg/sqlite/jni/capi/OutputPointer/Bool;Lorg/sqlite/jni/capi/OutputPointer/Bool;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1table_1column_1metadata
+  (JNIEnv *, jclass, jobject, jstring, jstring, jstring, jobject, jobject, jobject, jobject, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_threadsafe
+ * Signature: ()I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1threadsafe
+  (JNIEnv *, jclass);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_total_changes
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1total_1changes
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_total_changes64
+ * Signature: (J)J
+ */
+JNIEXPORT jlong JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1total_1changes64
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_trace_v2
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;ILorg/sqlite/jni/capi/TraceV2Callback;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1trace_1v2
+  (JNIEnv *, jclass, jobject, jint, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_txn_state
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;Ljava/lang/String;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1txn_1state
+  (JNIEnv *, jclass, jobject, jstring);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_update_hook
+ * Signature: (JLorg/sqlite/jni/capi/UpdateHookCallback;)Lorg/sqlite/jni/capi/UpdateHookCallback;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1update_1hook
+  (JNIEnv *, jclass, jlong, jobject);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_value_blob
+ * Signature: (J)[B
+ */
+JNIEXPORT jbyteArray JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1value_1blob
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_value_bytes
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1value_1bytes
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_value_bytes16
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1value_1bytes16
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_value_double
+ * Signature: (J)D
+ */
+JNIEXPORT jdouble JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1value_1double
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_value_dup
+ * Signature: (J)Lorg/sqlite/jni/capi/sqlite3_value;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1value_1dup
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_value_encoding
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1value_1encoding
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_value_free
+ * Signature: (J)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1value_1free
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_value_frombind
+ * Signature: (J)Z
+ */
+JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1value_1frombind
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_value_int
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1value_1int
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_value_int64
+ * Signature: (J)J
+ */
+JNIEXPORT jlong JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1value_1int64
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_value_java_object
+ * Signature: (J)Ljava/lang/Object;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1value_1java_1object
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_value_nochange
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1value_1nochange
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_value_numeric_type
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1value_1numeric_1type
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_value_subtype
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1value_1subtype
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_value_text
+ * Signature: (J)[B
+ */
+JNIEXPORT jbyteArray JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1value_1text
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_value_text16
+ * Signature: (J)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1value_1text16
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_value_type
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1value_1type
+  (JNIEnv *, jclass, jlong);
+
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_jni_internal_details
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1jni_1internal_1details
+  (JNIEnv *, jclass);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include 
+/* Header for class org_sqlite_jni_capi_SQLTester */
+
+#ifndef _Included_org_sqlite_jni_capi_SQLTester
+#define _Included_org_sqlite_jni_capi_SQLTester
+#ifdef __cplusplus
+extern "C" {
+#endif
+/*
+ * Class:     org_sqlite_jni_capi_SQLTester
+ * Method:    strglob
+ * Signature: ([B[B)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_SQLTester_strglob
+  (JNIEnv *, jclass, jbyteArray, jbyteArray);
+
+/*
+ * Class:     org_sqlite_jni_capi_SQLTester
+ * Method:    installCustomExtensions
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_SQLTester_installCustomExtensions
+  (JNIEnv *, jclass);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include 
+/* Header for class org_sqlite_jni_fts5_Fts5ExtensionApi */
+
+#ifndef _Included_org_sqlite_jni_fts5_Fts5ExtensionApi
+#define _Included_org_sqlite_jni_fts5_Fts5ExtensionApi
+#ifdef __cplusplus
+extern "C" {
+#endif
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    getInstance
+ * Signature: ()Lorg/sqlite/jni/fts5/Fts5ExtensionApi;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_getInstance
+  (JNIEnv *, jclass);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xColumnCount
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xColumnCount
+  (JNIEnv *, jobject, jobject);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xColumnSize
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;ILorg/sqlite/jni/capi/OutputPointer/Int32;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xColumnSize
+  (JNIEnv *, jobject, jobject, jint, jobject);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xColumnText
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;ILorg/sqlite/jni/capi/OutputPointer/String;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xColumnText
+  (JNIEnv *, jobject, jobject, jint, jobject);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xColumnTotalSize
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;ILorg/sqlite/jni/capi/OutputPointer/Int64;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xColumnTotalSize
+  (JNIEnv *, jobject, jobject, jint, jobject);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xGetAuxdata
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;Z)Ljava/lang/Object;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xGetAuxdata
+  (JNIEnv *, jobject, jobject, jboolean);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xInst
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;ILorg/sqlite/jni/capi/OutputPointer/Int32;Lorg/sqlite/jni/capi/OutputPointer/Int32;Lorg/sqlite/jni/capi/OutputPointer/Int32;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xInst
+  (JNIEnv *, jobject, jobject, jint, jobject, jobject, jobject);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xInstCount
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;Lorg/sqlite/jni/capi/OutputPointer/Int32;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xInstCount
+  (JNIEnv *, jobject, jobject, jobject);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xPhraseCount
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xPhraseCount
+  (JNIEnv *, jobject, jobject);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xPhraseFirst
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;ILorg/sqlite/jni/fts5/Fts5PhraseIter;Lorg/sqlite/jni/capi/OutputPointer/Int32;Lorg/sqlite/jni/capi/OutputPointer/Int32;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xPhraseFirst
+  (JNIEnv *, jobject, jobject, jint, jobject, jobject, jobject);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xPhraseFirstColumn
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;ILorg/sqlite/jni/fts5/Fts5PhraseIter;Lorg/sqlite/jni/capi/OutputPointer/Int32;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xPhraseFirstColumn
+  (JNIEnv *, jobject, jobject, jint, jobject, jobject);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xPhraseNext
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;Lorg/sqlite/jni/fts5/Fts5PhraseIter;Lorg/sqlite/jni/capi/OutputPointer/Int32;Lorg/sqlite/jni/capi/OutputPointer/Int32;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xPhraseNext
+  (JNIEnv *, jobject, jobject, jobject, jobject, jobject);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xPhraseNextColumn
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;Lorg/sqlite/jni/fts5/Fts5PhraseIter;Lorg/sqlite/jni/capi/OutputPointer/Int32;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xPhraseNextColumn
+  (JNIEnv *, jobject, jobject, jobject, jobject);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xPhraseSize
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;I)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xPhraseSize
+  (JNIEnv *, jobject, jobject, jint);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xQueryPhrase
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;ILorg/sqlite/jni/fts5/Fts5ExtensionApi/XQueryPhraseCallback;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xQueryPhrase
+  (JNIEnv *, jobject, jobject, jint, jobject);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xRowCount
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;Lorg/sqlite/jni/capi/OutputPointer/Int64;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xRowCount
+  (JNIEnv *, jobject, jobject, jobject);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xRowid
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;)J
+ */
+JNIEXPORT jlong JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xRowid
+  (JNIEnv *, jobject, jobject);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xSetAuxdata
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;Ljava/lang/Object;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xSetAuxdata
+  (JNIEnv *, jobject, jobject, jobject);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xTokenize
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;[BLorg/sqlite/jni/fts5/XTokenizeCallback;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xTokenize
+  (JNIEnv *, jobject, jobject, jbyteArray, jobject);
+
+/*
+ * Class:     org_sqlite_jni_fts5_Fts5ExtensionApi
+ * Method:    xUserData
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Context;)Ljava/lang/Object;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_fts5_Fts5ExtensionApi_xUserData
+  (JNIEnv *, jobject, jobject);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include 
+/* Header for class org_sqlite_jni_fts5_fts5_api */
+
+#ifndef _Included_org_sqlite_jni_fts5_fts5_api
+#define _Included_org_sqlite_jni_fts5_fts5_api
+#ifdef __cplusplus
+extern "C" {
+#endif
+#undef org_sqlite_jni_fts5_fts5_api_iVersion
+#define org_sqlite_jni_fts5_fts5_api_iVersion 2L
+/*
+ * Class:     org_sqlite_jni_fts5_fts5_api
+ * Method:    getInstanceForDb
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3;)Lorg/sqlite/jni/fts5/fts5_api;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_fts5_fts5_1api_getInstanceForDb
+  (JNIEnv *, jclass, jobject);
+
+/*
+ * Class:     org_sqlite_jni_fts5_fts5_api
+ * Method:    xCreateFunction
+ * Signature: (Ljava/lang/String;Ljava/lang/Object;Lorg/sqlite/jni/fts5/fts5_extension_function;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_fts5_fts5_1api_xCreateFunction
+  (JNIEnv *, jobject, jstring, jobject, jobject);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include 
+/* Header for class org_sqlite_jni_fts5_fts5_tokenizer */
+
+#ifndef _Included_org_sqlite_jni_fts5_fts5_tokenizer
+#define _Included_org_sqlite_jni_fts5_fts5_tokenizer
+#ifdef __cplusplus
+extern "C" {
+#endif
+/*
+ * Class:     org_sqlite_jni_fts5_fts5_tokenizer
+ * Method:    xTokenize
+ * Signature: (Lorg/sqlite/jni/fts5/Fts5Tokenizer;I[BLorg/sqlite/jni/fts5/XTokenizeCallback;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_fts5_fts5_1tokenizer_xTokenize
+  (JNIEnv *, jobject, jobject, jint, jbyteArray, jobject);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/annotation/NotNull.java b/sqlite/ext/jni/src/org/sqlite/jni/annotation/NotNull.java
new file mode 100644
index 00000000..3b4c1c7a
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/annotation/NotNull.java
@@ -0,0 +1,59 @@
+/*
+** 2023-09-27
+**
+** 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 houses the NotNull annotaion for the sqlite3 C API.
+*/
+package org.sqlite.jni.annotation;
+
+/**
+   This annotation is for flagging parameters which may not legally be
+   null or point to closed/finalized C-side resources.
+
+   In the case of Java types which map directly to C struct types
+   (e.g. {@link org.sqlite.jni.sqlite3}, {@link
+   org.sqlite.jni.sqlite3_stmt}, and {@link
+   org.sqlite.jni.sqlite3_context}), a closed/finalized resource is
+   also considered to be null for purposes this annotation because the
+   C-side effect of passing such a handle is the same as if null is
+   passed.
+
+   When used in the context of Java interfaces which are called
+   from the C APIs, this annotation communicates that the C API will
+   never pass a null value to the callback for that parameter.
+
+   Passing a null, for this annotation's definition of null, for
+   any parameter marked with this annoation specifically invokes
+   undefined behavior.
+
+   Passing 0 (i.e. C NULL) or a negative value for any long-type
+   parameter marked with this annoation specifically invokes undefined
+   behavior. Such values are treated as C pointers in the JNI
+   layer.
+
+   Note that the C-style API does not throw any exceptions on its
+   own because it has a no-throw policy in order to retain its C-style
+   semantics, but it may trigger NullPointerExceptions (or similar) if
+   passed a null for a parameter flagged with this annotation.
+
+   This annotation is informational only. No policy is in place to
+   programmatically ensure that NotNull is conformed to in client
+   code.
+
+   This annotation is solely for the use by the classes in the
+   org.sqlite package and subpackages, but is made public so that
+   javadoc will link to it from the annotated functions. It is not
+   part of the public API and client-level code must not rely on
+   it.
+*/
+@java.lang.annotation.Documented
+@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+@java.lang.annotation.Target(java.lang.annotation.ElementType.PARAMETER)
+public @interface NotNull{}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/annotation/Nullable.java b/sqlite/ext/jni/src/org/sqlite/jni/annotation/Nullable.java
new file mode 100644
index 00000000..ddc8502d
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/annotation/Nullable.java
@@ -0,0 +1,32 @@
+/*
+** 2023-09-27
+**
+** 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 houses the Nullable annotaion for the sqlite3 C API.
+*/
+package org.sqlite.jni.annotation;
+
+/**
+   This annotation is for flagging parameters which may legally be
+   null, noting that they may behave differently if passed null but
+   are prepared to expect null as a value. When used in the context of
+   callback methods which are called into from the C APIs, this
+   annotation communicates that the C API may pass a null value to the
+   callback.
+
+   This annotation is solely for the use by the classes in this
+   package but is made public so that javadoc will link to it from the
+   annotated functions. It is not part of the public API and
+   client-level code must not rely on it.
+*/
+@java.lang.annotation.Documented
+@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+@java.lang.annotation.Target(java.lang.annotation.ElementType.PARAMETER)
+public @interface Nullable{}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/annotation/package-info.java b/sqlite/ext/jni/src/org/sqlite/jni/annotation/package-info.java
new file mode 100644
index 00000000..20ac7a30
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/annotation/package-info.java
@@ -0,0 +1,17 @@
+/*
+** 2023-09-27
+**
+** 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 package houses annotations specific to the JNI bindings of the
+   SQLite3 C API.
+*/
+package org.sqlite.jni.annotation;
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/AbstractCollationCallback.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/AbstractCollationCallback.java
new file mode 100644
index 00000000..92553663
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/AbstractCollationCallback.java
@@ -0,0 +1,34 @@
+/*
+** 2023-08-25
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+import org.sqlite.jni.annotation.NotNull;
+
+/**
+   An implementation of {@link CollationCallback} which provides a
+   no-op xDestroy() method.
+*/
+public abstract class AbstractCollationCallback
+  implements CollationCallback, XDestroyCallback {
+  /**
+     Must compare the given byte arrays and return the result using
+     {@code memcmp()} semantics.
+  */
+  public abstract int call(@NotNull byte[] lhs, @NotNull byte[] rhs);
+
+  /**
+     Optionally override to be notified when the UDF is finalized by
+     SQLite. This implementation does nothing.
+  */
+  public void xDestroy(){}
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/AggregateFunction.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/AggregateFunction.java
new file mode 100644
index 00000000..89c4f274
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/AggregateFunction.java
@@ -0,0 +1,72 @@
+/*
+** 2023-08-25
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+
+/**
+   A SQLFunction implementation for aggregate functions.  Its T is the
+   data type of its "accumulator" state, an instance of which is
+   intended to be be managed using the getAggregateState() and
+   takeAggregateState() methods.
+*/
+public abstract class AggregateFunction implements SQLFunction {
+
+  /**
+     As for the xStep() argument of the C API's
+     sqlite3_create_function().  If this function throws, the
+     exception is not propagated and a warning might be emitted to a
+     debugging channel.
+  */
+  public abstract void xStep(sqlite3_context cx, sqlite3_value[] args);
+
+  /**
+     As for the xFinal() argument of the C API's sqlite3_create_function().
+     If this function throws, it is translated into an sqlite3_result_error().
+  */
+  public abstract void xFinal(sqlite3_context cx);
+
+  /**
+     Optionally override to be notified when the UDF is finalized by
+     SQLite.
+  */
+  public void xDestroy() {}
+
+  /** Per-invocation state for the UDF. */
+  private final SQLFunction.PerContextState map =
+    new SQLFunction.PerContextState<>();
+
+  /**
+     To be called from the implementation's xStep() method, as well
+     as the xValue() and xInverse() methods of the {@link WindowFunction}
+     subclass, to fetch the current per-call UDF state. On the
+     first call to this method for any given sqlite3_context
+     argument, the context is set to the given initial value. On all other
+     calls, the 2nd argument is ignored.
+
+     @see SQLFunction.PerContextState#getAggregateState
+  */
+  protected final ValueHolder getAggregateState(sqlite3_context cx, T initialValue){
+    return map.getAggregateState(cx, initialValue);
+  }
+
+  /**
+     To be called from the implementation's xFinal() method to fetch
+     the final state of the UDF and remove its mapping.
+
+     see SQLFunction.PerContextState#takeAggregateState
+  */
+  protected final T takeAggregateState(sqlite3_context cx){
+    return map.takeAggregateState(cx);
+  }
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/AuthorizerCallback.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/AuthorizerCallback.java
new file mode 100644
index 00000000..ce7c6fca
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/AuthorizerCallback.java
@@ -0,0 +1,28 @@
+/*
+** 2023-08-25
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+import org.sqlite.jni.annotation.*;
+
+/**
+   Callback for use with {@link CApi#sqlite3_set_authorizer}.
+*/
+public interface AuthorizerCallback extends CallbackProxy {
+  /**
+     Must function as described for the C-level
+     sqlite3_set_authorizer() callback.
+  */
+  int call(int opId, @Nullable String s1, @Nullable String s2,
+           @Nullable String s3, @Nullable String s4);
+
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/AutoExtensionCallback.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/AutoExtensionCallback.java
new file mode 100644
index 00000000..7a54132d
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/AutoExtensionCallback.java
@@ -0,0 +1,40 @@
+/*
+** 2023-08-25
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   Callback for use with the {@link CApi#sqlite3_auto_extension}
+   family of APIs.
+*/
+public interface AutoExtensionCallback extends CallbackProxy {
+  /**
+     Must function as described for a C-level
+     sqlite3_auto_extension() callback.
+
+     This callback may throw and the exception's error message will
+     be set as the db's error string.
+
+     
Tips for implementations:
+
+     
- Opening a database from an auto-extension handler will lead to
+     an endless recursion of the auto-handler triggering itself
+     indirectly for each newly-opened database.
+
+     
- If this routine is stateful, it may be useful to make the
+     overridden method synchronized.
+
+     
- Results are undefined if the given db is closed by an auto-extension.
+  */
+  int call(sqlite3 db);
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/BusyHandlerCallback.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/BusyHandlerCallback.java
new file mode 100644
index 00000000..00223f0b
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/BusyHandlerCallback.java
@@ -0,0 +1,26 @@
+/*
+** 2023-08-25
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   Callback for use with {@link CApi#sqlite3_busy_handler}.
+*/
+public interface BusyHandlerCallback extends CallbackProxy {
+  /**
+     Must function as documented for the C-level
+     sqlite3_busy_handler() callback argument, minus the (void*)
+     argument the C-level function requires.
+  */
+  int call(int n);
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/CApi.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/CApi.java
new file mode 100644
index 00000000..302cdb76
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/CApi.java
@@ -0,0 +1,2449 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file declares JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+import java.nio.charset.StandardCharsets;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import org.sqlite.jni.annotation.*;
+import java.util.Arrays;
+
+/**
+  This class contains the entire C-style sqlite3 JNI API binding,
+  minus a few bits and pieces declared in other files. For client-side
+  use, a static import is recommended:
+
+  
{@code
+  import static org.sqlite.jni.capi.CApi.*;
+  }
+
+  The C-side part can be found in sqlite3-jni.c.
+
+  
This class is package-private in order to keep Java clients from
+  having direct access to the low-level C-style APIs, a design
+  decision made by Java developers based on the C-style API being
+  riddled with opportunities for Java developers to proverbially shoot
+  themselves in the foot with. Third-party copies of this code may
+  eliminate that guard by simply changing this class from
+  package-private to public. Its methods which are intended to be
+  exposed that way are all public.
+
+  
Only functions which materially differ from their C counterparts
+  are documented here, and only those material differences are
+  documented. The C documentation is otherwise applicable for these
+  APIs:
+
+  
https://sqlite.org/c3ref/intro.html
+
+  
A handful of Java-specific APIs have been added which are
+  documented here. A number of convenience overloads are provided
+  which are not documented but whose semantics map 1-to-1 in an
+  intuitive manner. e.g. {@link
+  #sqlite3_result_set(sqlite3_context,int)} is equivalent to {@link
+  #sqlite3_result_int}, and sqlite3_result_set() has many
+  type-specific overloads.
+
+  
Notes regarding Java's Modified UTF-8 vs standard UTF-8:
+
+  
SQLite internally uses UTF-8 encoding, whereas Java natively uses
+  UTF-16.  Java JNI has routines for converting to and from UTF-8,
+  but JNI uses what its docs call modified UTF-8 (see links below)
+  Care must be taken when converting Java strings to or from standard
+  UTF-8 to ensure that the proper conversion is performed. In short,
+  Java's {@code String.getBytes(StandardCharsets.UTF_8)} performs the proper
+  conversion in Java, and there are no JNI C APIs for that conversion
+  (JNI's {@code NewStringUTF()} requires its input to be in MUTF-8).
+
+  
The known consequences and limitations this discrepancy places on
+  the SQLite3 JNI binding include:
+
+  
+
+  - C functions which take C-style strings without a length argument
+  require special care when taking input from Java. In particular,
+  Java strings converted to byte arrays for encoding purposes are not
+  NUL-terminated, and conversion to a Java byte array must sometimes
+  be careful to add one. Functions which take a length do not require
+  this so long as the length is provided. Search the CApi class
+  for "\0" for many examples.
+
+  
+
+Further reading:
+
+  
https://stackoverflow.com/questions/57419723
+  
https://stackoverflow.com/questions/7921016
+  
https://itecnote.com/tecnote/java-getting-true-utf-8-characters-in-java-jni/
+  
https://docs.oracle.com/javase/8/docs/api/java/lang/Character.html#unicode
+  
https://docs.oracle.com/javase/8/docs/api/java/io/DataInput.html#modified-utf-8
+
+*/
+public final class CApi {
+  static {
+    System.loadLibrary("sqlite3-jni");
+  }
+  //! Not used
+  private CApi(){}
+  //! Called from static init code.
+  private static native void init();
+
+  /**
+     Returns a nul-terminated copy of s as a UTF-8-encoded byte array,
+     or null if s is null.
+  */
+  private static byte[] nulTerminateUtf8(String s){
+    return null==s ? null : (s+"\0").getBytes(StandardCharsets.UTF_8);
+  }
+
+  /**
+     Each thread which uses the SQLite3 JNI APIs should call
+     sqlite3_jni_uncache_thread() when it is done with the library -
+     either right before it terminates or when it finishes using the
+     SQLite API.  This will clean up any cached per-thread info.
+
+     
This process does not close any databases or finalize
+     any prepared statements because their ownership does not depend on
+     a given thread.  For proper library behavior, and to
+     avoid C-side leaks, be sure to finalize all statements and close
+     all databases before calling this function.
+
+     
Calling this from the main application thread is not strictly
+     required. Additional threads must call this before ending or they
+     will leak cache entries in the C heap, which in turn may keep
+     numerous Java-side global references active.
+
+     
This routine returns false without side effects if the current
+     JNIEnv is not cached, else returns true, but this information is
+     primarily for testing of the JNI bindings and is not information
+     which client-level code can use to make any informed decisions.
+  */
+  public static native boolean sqlite3_java_uncache_thread();
+
+  //////////////////////////////////////////////////////////////////////
+  // Maintenance reminder: please keep the sqlite3_.... functions
+  // alphabetized.  The SQLITE_... values. on the other hand, are
+  // grouped by category.
+
+  /**
+     Functions exactly like the native form except that (A) the 2nd
+     argument is a boolean instead of an int and (B) the returned
+     value is not a pointer address and is only intended for use as a
+     per-UDF-call lookup key in a higher-level data structure.
+
+     
Passing a true second argument is analogous to passing some
+     unspecified small, non-0 positive value to the C API and passing
+     false is equivalent to passing 0 to the C API.
+
+     
Like the C API, it returns 0 if allocation fails or if
+     initialize is false and no prior aggregate context was allocated
+     for cx.  If initialize is true then it returns 0 only on
+     allocation error. In all casses, 0 is considered the sentinel
+     "not a key" value.
+  */
+  public static native long sqlite3_aggregate_context(sqlite3_context cx, boolean initialize);
+
+  /**
+     Functions almost as documented for the C API, with these
+     exceptions:
+
+     
- The callback interface is shorter because of
+     cross-language differences. Specifically, 3rd argument to the C
+     auto-extension callback interface is unnecessary here.
+
+     
The C API docs do not specifically say so, but if the list of
+     auto-extensions is manipulated from an auto-extension, it is
+     undefined which, if any, auto-extensions will subsequently
+     execute for the current database. That is, doing so will result
+     in unpredictable, but not undefined, behavior.
+
+     
See the AutoExtension class docs for more information.
+  */
+  public static native int sqlite3_auto_extension(@NotNull AutoExtensionCallback callback);
+
+  static native int sqlite3_backup_finish(@NotNull long ptrToBackup);
+
+  public static int sqlite3_backup_finish(@NotNull sqlite3_backup b){
+    return sqlite3_backup_finish(b.clearNativePointer());
+  }
+
+  static native sqlite3_backup sqlite3_backup_init(
+    @NotNull long ptrToDbDest, @NotNull String destTableName,
+    @NotNull long ptrToDbSrc, @NotNull String srcTableName
+  );
+
+  public static sqlite3_backup sqlite3_backup_init(
+    @NotNull sqlite3 dbDest, @NotNull String destTableName,
+    @NotNull sqlite3 dbSrc, @NotNull String srcTableName
+  ){
+    return sqlite3_backup_init( dbDest.getNativePointer(), destTableName,
+                                dbSrc.getNativePointer(), srcTableName );
+  }
+
+  static native int sqlite3_backup_pagecount(@NotNull long ptrToBackup);
+
+  public static int sqlite3_backup_pagecount(@NotNull sqlite3_backup b){
+    return sqlite3_backup_pagecount(b.getNativePointer());
+  }
+
+  static native int sqlite3_backup_remaining(@NotNull long ptrToBackup);
+
+  public static int sqlite3_backup_remaining(@NotNull sqlite3_backup b){
+    return sqlite3_backup_remaining(b.getNativePointer());
+  }
+
+  static native int sqlite3_backup_step(@NotNull long ptrToBackup, int nPage);
+
+  public static int sqlite3_backup_step(@NotNull sqlite3_backup b, int nPage){
+    return sqlite3_backup_step(b.getNativePointer(), nPage);
+  }
+
+  static native int sqlite3_bind_blob(
+    @NotNull long ptrToStmt, int ndx, @Nullable byte[] data, int n
+  );
+
+  /**
+     If n is negative, SQLITE_MISUSE is returned. If n>data.length
+     then n is silently truncated to data.length.
+  */
+  static int sqlite3_bind_blob(
+    @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data, int n
+  ){
+    return sqlite3_bind_blob(stmt.getNativePointer(), ndx, data, n);
+  }
+
+  public static int sqlite3_bind_blob(
+    @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data
+  ){
+    return (null==data)
+      ? sqlite3_bind_null(stmt.getNativePointer(), ndx)
+      : sqlite3_bind_blob(stmt.getNativePointer(), ndx, data, data.length);
+  }
+
+  static native int sqlite3_bind_double(
+    @NotNull long ptrToStmt, int ndx, double v
+  );
+
+  public static int sqlite3_bind_double(
+    @NotNull sqlite3_stmt stmt, int ndx, double v
+  ){
+    return sqlite3_bind_double(stmt.getNativePointer(), ndx, v);
+  }
+
+  static native int sqlite3_bind_int(
+    @NotNull long ptrToStmt, int ndx, int v
+  );
+
+  public static int sqlite3_bind_int(
+    @NotNull sqlite3_stmt stmt, int ndx, int v
+  ){
+    return sqlite3_bind_int(stmt.getNativePointer(), ndx, v);
+  }
+
+  static native int sqlite3_bind_int64(
+    @NotNull long ptrToStmt, int ndx, long v
+  );
+
+  public static int sqlite3_bind_int64(@NotNull sqlite3_stmt stmt, int ndx, long v){
+    return sqlite3_bind_int64( stmt.getNativePointer(), ndx, v );
+  }
+
+  static native int sqlite3_bind_java_object(
+    @NotNull long ptrToStmt, int ndx, @Nullable Object o
+  );
+
+  /**
+     Binds the given object at the given index. If o is null then this behaves like
+     sqlite3_bind_null().
+
+     @see #sqlite3_result_java_object
+  */
+  public static int sqlite3_bind_java_object(
+    @NotNull sqlite3_stmt stmt, int ndx, @Nullable Object o
+  ){
+    return sqlite3_bind_java_object(stmt.getNativePointer(), ndx, o);
+  }
+
+  static native int sqlite3_bind_null(@NotNull long ptrToStmt, int ndx);
+
+  public static int sqlite3_bind_null(@NotNull sqlite3_stmt stmt, int ndx){
+    return sqlite3_bind_null(stmt.getNativePointer(), ndx);
+  }
+
+  static native int sqlite3_bind_parameter_count(@NotNull long ptrToStmt);
+
+  public static int sqlite3_bind_parameter_count(@NotNull sqlite3_stmt stmt){
+    return sqlite3_bind_parameter_count(stmt.getNativePointer());
+  }
+
+  /**
+     Requires that paramName be a NUL-terminated UTF-8 string.
+
+     This overload is private because: (A) to keep users from
+     inadvertently passing non-NUL-terminated byte arrays (an easy
+     thing to do). (B) it is cheaper to NUL-terminate the
+     String-to-byte-array conversion in the public-facing Java-side
+     overload than to do that in C, so that signature is the
+     public-facing one.
+  */
+  private static native int sqlite3_bind_parameter_index(
+    @NotNull long ptrToStmt, @NotNull byte[] paramName
+  );
+
+  public static int sqlite3_bind_parameter_index(
+    @NotNull sqlite3_stmt stmt, @NotNull String paramName
+  ){
+    final byte[] utf8 = nulTerminateUtf8(paramName);
+    return null==utf8 ? 0 : sqlite3_bind_parameter_index(stmt.getNativePointer(), utf8);
+  }
+
+  static native String sqlite3_bind_parameter_name(
+    @NotNull long ptrToStmt, int index
+  );
+
+  public static String sqlite3_bind_parameter_name(@NotNull sqlite3_stmt stmt, int index){
+    return sqlite3_bind_parameter_name(stmt.getNativePointer(), index);
+  }
+
+  static native int sqlite3_bind_text(
+    @NotNull long ptrToStmt, int ndx, @Nullable byte[] utf8, int maxBytes
+  );
+
+  /**
+     Works like the C-level sqlite3_bind_text() but assumes
+     SQLITE_TRANSIENT for the final C API parameter. The byte array is
+     assumed to be in UTF-8 encoding.
+
+     
If data is not null and maxBytes>utf8.length then maxBytes is
+     silently truncated to utf8.length. If maxBytes is negative then
+     results are undefined if data is not null and does not contain a
+     NUL byte.
+  */
+  static int sqlite3_bind_text(
+    @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] utf8, int maxBytes
+  ){
+    return sqlite3_bind_text(stmt.getNativePointer(), ndx, utf8, maxBytes);
+  }
+
+  /**
+     Converts data, if not null, to a UTF-8-encoded byte array and
+     binds it as such, returning the result of the C-level
+     sqlite3_bind_null() or sqlite3_bind_text().
+  */
+  public static int sqlite3_bind_text(
+    @NotNull sqlite3_stmt stmt, int ndx, @Nullable String data
+  ){
+    if( null==data ) return sqlite3_bind_null(stmt.getNativePointer(), ndx);
+    final byte[] utf8 = data.getBytes(StandardCharsets.UTF_8);
+    return sqlite3_bind_text(stmt.getNativePointer(), ndx, utf8, utf8.length);
+  }
+
+  /**
+     Requires that utf8 be null or in UTF-8 encoding.
+  */
+  public static int sqlite3_bind_text(
+    @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] utf8
+  ){
+    return ( null==utf8 )
+      ? sqlite3_bind_null(stmt.getNativePointer(), ndx)
+      : sqlite3_bind_text(stmt.getNativePointer(), ndx, utf8, utf8.length);
+  }
+
+  static native int sqlite3_bind_text16(
+    @NotNull long ptrToStmt, int ndx, @Nullable byte[] data, int maxBytes
+  );
+
+  /**
+     Identical to the sqlite3_bind_text() overload with the same
+     signature but requires that its input be encoded in UTF-16 in
+     platform byte order.
+  */
+  static int sqlite3_bind_text16(
+    @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data, int maxBytes
+  ){
+    return sqlite3_bind_text16(stmt.getNativePointer(), ndx, data, maxBytes);
+  }
+
+  /**
+     Converts its string argument to UTF-16 and binds it as such, returning
+     the result of the C-side function of the same name. The 3rd argument
+     may be null.
+  */
+  public static int sqlite3_bind_text16(
+    @NotNull sqlite3_stmt stmt, int ndx, @Nullable String data
+  ){
+    if(null == data) return sqlite3_bind_null(stmt, ndx);
+    final byte[] bytes = data.getBytes(StandardCharsets.UTF_16);
+    return sqlite3_bind_text16(stmt.getNativePointer(), ndx, bytes, bytes.length);
+  }
+
+  /**
+     Requires that data be null or in UTF-16 encoding in platform byte
+     order. Returns the result of the C-level sqlite3_bind_null() or
+     sqlite3_bind_text16().
+  */
+  public static int sqlite3_bind_text16(
+    @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data
+  ){
+    return (null == data)
+      ? sqlite3_bind_null(stmt.getNativePointer(), ndx)
+      : sqlite3_bind_text16(stmt.getNativePointer(), ndx, data, data.length);
+  }
+
+  static native int sqlite3_bind_value(@NotNull long ptrToStmt, int ndx, long ptrToValue);
+
+  /**
+     Functions like the C-level sqlite3_bind_value(), or
+     sqlite3_bind_null() if val is null.
+  */
+  public static int sqlite3_bind_value(@NotNull sqlite3_stmt stmt, int ndx, sqlite3_value val){
+    return sqlite3_bind_value(stmt.getNativePointer(), ndx,
+                              null==val ? 0L : val.getNativePointer());
+  }
+
+  static native int sqlite3_bind_zeroblob(@NotNull long ptrToStmt, int ndx, int n);
+
+  public static int sqlite3_bind_zeroblob(@NotNull sqlite3_stmt stmt, int ndx, int n){
+    return sqlite3_bind_zeroblob(stmt.getNativePointer(), ndx, n);
+  }
+
+  static native int sqlite3_bind_zeroblob64(
+    @NotNull long ptrToStmt, int ndx, long n
+  );
+
+  public static int sqlite3_bind_zeroblob64(@NotNull sqlite3_stmt stmt, int ndx, long n){
+    return sqlite3_bind_zeroblob64(stmt.getNativePointer(), ndx, n);
+  }
+
+  static native int sqlite3_blob_bytes(@NotNull long ptrToBlob);
+
+  public static int sqlite3_blob_bytes(@NotNull sqlite3_blob blob){
+    return sqlite3_blob_bytes(blob.getNativePointer());
+  }
+
+  static native int sqlite3_blob_close(@Nullable long ptrToBlob);
+
+  public static int sqlite3_blob_close(@Nullable sqlite3_blob blob){
+    return sqlite3_blob_close(blob.clearNativePointer());
+  }
+
+  static native int sqlite3_blob_open(
+    @NotNull long ptrToDb, @NotNull String dbName,
+    @NotNull String tableName, @NotNull String columnName,
+    long iRow, int flags, @NotNull OutputPointer.sqlite3_blob out
+  );
+
+  public static int sqlite3_blob_open(
+    @NotNull sqlite3 db, @NotNull String dbName,
+    @NotNull String tableName, @NotNull String columnName,
+    long iRow, int flags, @NotNull OutputPointer.sqlite3_blob out
+  ){
+    return sqlite3_blob_open(db.getNativePointer(), dbName, tableName,
+                             columnName, iRow, flags, out);
+  }
+
+  /**
+     Convenience overload.
+  */
+  public static sqlite3_blob sqlite3_blob_open(
+    @NotNull sqlite3 db, @NotNull String dbName,
+    @NotNull String tableName, @NotNull String columnName,
+    long iRow, int flags ){
+    final OutputPointer.sqlite3_blob out = new OutputPointer.sqlite3_blob();
+    sqlite3_blob_open(db.getNativePointer(), dbName, tableName, columnName,
+                      iRow, flags, out);
+    return out.take();
+  };
+
+  static native int sqlite3_blob_read(
+    @NotNull long ptrToBlob, @NotNull byte[] target, int iOffset
+  );
+
+  public static int sqlite3_blob_read(
+    @NotNull sqlite3_blob b, @NotNull byte[] target, int iOffset
+  ){
+    return sqlite3_blob_read(b.getNativePointer(), target, iOffset);
+  }
+
+  static native int sqlite3_blob_reopen(
+    @NotNull long ptrToBlob, long newRowId
+  );
+
+  public static int sqlite3_blob_reopen(@NotNull sqlite3_blob b, long newRowId){
+    return sqlite3_blob_reopen(b.getNativePointer(), newRowId);
+  }
+
+  static native int sqlite3_blob_write(
+    @NotNull long ptrToBlob, @NotNull byte[] bytes, int iOffset
+  );
+
+  public static int sqlite3_blob_write(
+    @NotNull sqlite3_blob b, @NotNull byte[] bytes, int iOffset
+  ){
+    return sqlite3_blob_write(b.getNativePointer(), bytes, iOffset);
+  }
+
+  static native int sqlite3_busy_handler(
+    @NotNull long ptrToDb, @Nullable BusyHandlerCallback handler
+  );
+
+  /**
+     As for the C-level function of the same name, with a
+     BusyHandlerCallback instance in place of a callback
+     function. Pass it a null handler to clear the busy handler.
+  */
+  public static int sqlite3_busy_handler(
+    @NotNull sqlite3 db, @Nullable BusyHandlerCallback handler
+  ){
+    return sqlite3_busy_handler(db.getNativePointer(), handler);
+  }
+
+  static native int sqlite3_busy_timeout(@NotNull long ptrToDb, int ms);
+
+  public static int sqlite3_busy_timeout(@NotNull sqlite3 db, int ms){
+    return sqlite3_busy_timeout(db.getNativePointer(), ms);
+  }
+
+  public static native boolean sqlite3_cancel_auto_extension(
+    @NotNull AutoExtensionCallback ax
+  );
+
+  static native int sqlite3_changes(@NotNull long ptrToDb);
+
+  public static int sqlite3_changes(@NotNull sqlite3 db){
+    return sqlite3_changes(db.getNativePointer());
+  }
+
+  static native long sqlite3_changes64(@NotNull long ptrToDb);
+
+  public static long sqlite3_changes64(@NotNull sqlite3 db){
+    return sqlite3_changes64(db.getNativePointer());
+  }
+
+  static native int sqlite3_clear_bindings(@NotNull long ptrToStmt);
+
+  public static int sqlite3_clear_bindings(@NotNull sqlite3_stmt stmt){
+    return sqlite3_clear_bindings(stmt.getNativePointer());
+  }
+
+  static native int sqlite3_close(@Nullable long ptrToDb);
+
+  public static int sqlite3_close(@Nullable sqlite3 db){
+    int rc = 0;
+    if( null!=db ){
+      rc = sqlite3_close(db.getNativePointer());
+      if( 0==rc ) db.clearNativePointer();
+    }
+    return rc;
+  }
+
+  static native int sqlite3_close_v2(@Nullable long ptrToDb);
+
+  public static int sqlite3_close_v2(@Nullable sqlite3 db){
+    return db==null ? 0 : sqlite3_close_v2(db.clearNativePointer());
+  }
+
+  public static native byte[] sqlite3_column_blob(
+    @NotNull sqlite3_stmt stmt, int ndx
+  );
+
+  static native int sqlite3_column_bytes(@NotNull long ptrToStmt, int ndx);
+
+  public static int sqlite3_column_bytes(@NotNull sqlite3_stmt stmt, int ndx){
+    return sqlite3_column_bytes(stmt.getNativePointer(), ndx);
+  }
+
+  static native int sqlite3_column_bytes16(@NotNull long ptrToStmt, int ndx);
+
+  public static int sqlite3_column_bytes16(@NotNull sqlite3_stmt stmt, int ndx){
+    return sqlite3_column_bytes16(stmt.getNativePointer(), ndx);
+  }
+
+  static native int sqlite3_column_count(@NotNull long ptrToStmt);
+
+  public static int sqlite3_column_count(@NotNull sqlite3_stmt stmt){
+    return sqlite3_column_count(stmt.getNativePointer());
+  }
+
+  static native String sqlite3_column_decltype(@NotNull long ptrToStmt, int ndx);
+
+  public static String sqlite3_column_decltype(@NotNull sqlite3_stmt stmt, int ndx){
+    return sqlite3_column_decltype(stmt.getNativePointer(), ndx);
+  }
+
+  public static native double sqlite3_column_double(
+    @NotNull sqlite3_stmt stmt, int ndx
+  );
+
+  public static native int sqlite3_column_int(
+    @NotNull sqlite3_stmt stmt, int ndx
+  );
+
+  public static native long sqlite3_column_int64(
+    @NotNull sqlite3_stmt stmt, int ndx
+  );
+
+  static native String sqlite3_column_name(@NotNull long ptrToStmt, int ndx);
+
+  public static String sqlite3_column_name(@NotNull sqlite3_stmt stmt, int ndx){
+    return sqlite3_column_name(stmt.getNativePointer(), ndx);
+  }
+
+  static native String sqlite3_column_database_name(@NotNull long ptrToStmt, int ndx);
+
+  public static String sqlite3_column_database_name(@NotNull sqlite3_stmt stmt, int ndx){
+    return sqlite3_column_database_name(stmt.getNativePointer(), ndx);
+  }
+
+  static native String sqlite3_column_origin_name(@NotNull long ptrToStmt, int ndx);
+
+  public static String sqlite3_column_origin_name(@NotNull sqlite3_stmt stmt, int ndx){
+    return sqlite3_column_origin_name(stmt.getNativePointer(), ndx);
+  }
+
+  static native String sqlite3_column_table_name(@NotNull long ptrToStmt, int ndx);
+
+  public static String sqlite3_column_table_name(@NotNull sqlite3_stmt stmt, int ndx){
+    return sqlite3_column_table_name(stmt.getNativePointer(), ndx);
+  }
+
+  /**
+     Functions identially to the C API, and this note is just to
+     stress that the returned bytes are encoded as UTF-8. It returns
+     null if the underlying C-level sqlite3_column_text() returns NULL
+     or on allocation error.
+
+     @see #sqlite3_column_text16(sqlite3_stmt,int)
+  */
+  public static native byte[] sqlite3_column_text(
+    @NotNull sqlite3_stmt stmt, int ndx
+  );
+
+  public static native String sqlite3_column_text16(
+    @NotNull sqlite3_stmt stmt, int ndx
+  );
+
+  // The real utility of this function is questionable.
+  // /**
+  //    Returns a Java value representation based on the value of
+  //    sqlite_value_type(). For integer types it returns either Integer
+  //    or Long, depending on whether the value will fit in an
+  //    Integer. For floating-point values it always returns type Double.
+
+  //    If the column was bound using sqlite3_result_java_object() then
+  //    that value, as an Object, is returned.
+  // */
+  // public static Object sqlite3_column_to_java(@NotNull sqlite3_stmt stmt,
+  //                                             int ndx){
+  //   sqlite3_value v = sqlite3_column_value(stmt, ndx);
+  //   Object rv = null;
+  //   if(null == v) return v;
+  //   v = sqlite3_value_dup(v)/*need a protected value*/;
+  //   if(null == v) return v /* OOM error in C */;
+  //   if(112/* 'p' */ == sqlite3_value_subtype(v)){
+  //     rv = sqlite3_value_java_object(v);
+  //   }else{
+  //     switch(sqlite3_value_type(v)){
+  //       case SQLITE_INTEGER: {
+  //         final long i = sqlite3_value_int64(v);
+  //         rv = (i<=0x7fffffff && i>=-0x7fffffff-1)
+  //           ? new Integer((int)i) : new Long(i);
+  //         break;
+  //       }
+  //       case SQLITE_FLOAT: rv = new Double(sqlite3_value_double(v)); break;
+  //       case SQLITE_BLOB: rv = sqlite3_value_blob(v); break;
+  //       case SQLITE_TEXT: rv = sqlite3_value_text16(v); break;
+  //       default: break;
+  //     }
+  //   }
+  //   sqlite3_value_free(v);
+  //   return rv;
+  // }
+
+  static native int sqlite3_column_type(@NotNull long ptrToStmt, int ndx);
+
+  public static int sqlite3_column_type(@NotNull sqlite3_stmt stmt, int ndx){
+    return sqlite3_column_type(stmt.getNativePointer(), ndx);
+  }
+
+  public static native sqlite3_value sqlite3_column_value(
+    @NotNull sqlite3_stmt stmt, int ndx
+  );
+
+  static native int sqlite3_collation_needed(
+    @NotNull long ptrToDb, @Nullable CollationNeededCallback callback
+  );
+
+  /**
+     This functions like C's sqlite3_collation_needed16() because
+     Java's string type is inherently compatible with that interface.
+  */
+  public static int sqlite3_collation_needed(
+    @NotNull sqlite3 db, @Nullable CollationNeededCallback callback
+  ){
+    return sqlite3_collation_needed(db.getNativePointer(), callback);
+  }
+
+  static native CommitHookCallback sqlite3_commit_hook(
+    @NotNull long ptrToDb, @Nullable CommitHookCallback hook
+  );
+
+  public static CommitHookCallback sqlite3_commit_hook(
+    @NotNull sqlite3 db, @Nullable CommitHookCallback hook
+  ){
+    return sqlite3_commit_hook(db.getNativePointer(), hook);
+  }
+
+  public static native String sqlite3_compileoption_get(int n);
+
+  public static native boolean sqlite3_compileoption_used(String optName);
+
+  /**
+     This implementation is private because it's too easy to pass it
+     non-NUL-terminated byte arrays from client code.
+  */
+  private static native int sqlite3_complete(
+    @NotNull byte[] nulTerminatedUtf8Sql
+  );
+
+  /**
+     Unlike the C API, this returns SQLITE_MISUSE if its argument is
+     null (as opposed to invoking UB).
+  */
+  public static int sqlite3_complete(@NotNull String sql){
+    return sqlite3_complete( nulTerminateUtf8(sql) );
+  }
+
+
+  /**
+     
Works like in the C API with the exception that it only supports
+     the following subset of configution flags:
+
+     
SQLITE_CONFIG_SINGLETHREAD
+     SQLITE_CONFIG_MULTITHREAD
+     SQLITE_CONFIG_SERIALIZED
+
+     
Others may be added in the future. It returns SQLITE_MISUSE if
+     given an argument it does not handle.
+
+     
Note that sqlite3_config() is not threadsafe with regards to
+     the rest of the library. This must not be called when any other
+     library APIs are being called.
+  */
+  public static native int sqlite3_config(int op);
+
+  /**
+     If the native library was built with SQLITE_ENABLE_SQLLOG defined
+     then this acts as a proxy for C's
+     sqlite3_config(SQLITE_ENABLE_SQLLOG,...). This sets or clears the
+     logger. If installation of a logger fails, any previous logger is
+     retained.
+
+     
If not built with SQLITE_ENABLE_SQLLOG defined, this returns
+     SQLITE_MISUSE.
+
+     
Note that sqlite3_config() is not threadsafe with regards to
+     the rest of the library. This must not be called when any other
+     library APIs are being called.
+  */
+  public static native int sqlite3_config( @Nullable ConfigSqllogCallback logger );
+
+  /**
+     The sqlite3_config() overload for handling the SQLITE_CONFIG_LOG
+     option.
+  */
+  public static native int sqlite3_config( @Nullable ConfigLogCallback logger );
+
+  /**
+     Unlike the C API, this returns null if its argument is
+     null (as opposed to invoking UB).
+  */
+  public static native sqlite3 sqlite3_context_db_handle(
+    @NotNull sqlite3_context cx
+  );
+
+  public static native int sqlite3_create_collation(
+    @NotNull sqlite3 db, @NotNull String name, int eTextRep,
+    @NotNull CollationCallback col
+  );
+
+  /**
+     The Java counterpart to the C-native sqlite3_create_function(),
+     sqlite3_create_function_v2(), and
+     sqlite3_create_window_function(). Which one it behaves like
+     depends on which methods the final argument implements. See
+     SQLFunction's subclasses (ScalarFunction, AggregateFunction,
+     and WindowFunction) for details.
+
+     Unlike the C API, this returns SQLITE_MISUSE null if its db or
+     functionName arguments are null (as opposed to invoking UB).
+  */
+  public static native int sqlite3_create_function(
+    @NotNull sqlite3 db, @NotNull String functionName,
+    int nArg, int eTextRep, @NotNull SQLFunction func
+  );
+
+  static native int sqlite3_data_count(@NotNull long ptrToStmt);
+
+  public static int sqlite3_data_count(@NotNull sqlite3_stmt stmt){
+    return sqlite3_data_count(stmt.getNativePointer());
+  }
+
+  /**
+     Overload for sqlite3_db_config() calls which take (int,int*)
+     variadic arguments. Returns SQLITE_MISUSE if op is not one of the
+     SQLITE_DBCONFIG_... options which uses this call form.
+
+     
Unlike the C API, this returns SQLITE_MISUSE if its db argument
+     are null (as opposed to invoking UB).
+  */
+  public static native int sqlite3_db_config(
+    @NotNull sqlite3 db, int op, int onOff, @Nullable OutputPointer.Int32 out
+  );
+
+  /**
+     Overload for sqlite3_db_config() calls which take a (const char*)
+     variadic argument. As of SQLite3 v3.43 the only such option is
+     SQLITE_DBCONFIG_MAINDBNAME. Returns SQLITE_MISUSE if op is not
+     SQLITE_DBCONFIG_MAINDBNAME, but that set of options may be
+     extended in future versions.
+  */
+  public static native int sqlite3_db_config(
+    @NotNull sqlite3 db, int op, @NotNull String val
+  );
+
+  private static native String sqlite3_db_name(@NotNull long ptrToDb, int ndx);
+
+  public static String sqlite3_db_name(@NotNull sqlite3 db, int ndx){
+    return null==db ? null : sqlite3_db_name(db.getNativePointer(), ndx);
+  }
+
+
+  public static native String sqlite3_db_filename(
+    @NotNull sqlite3 db, @NotNull String dbName
+  );
+
+  public static native sqlite3 sqlite3_db_handle(@NotNull sqlite3_stmt stmt);
+
+  public static native int sqlite3_db_readonly(@NotNull sqlite3 db, String dbName);
+
+  public static native int sqlite3_db_release_memory(sqlite3 db);
+
+  public static native int sqlite3_db_status(
+    @NotNull sqlite3 db, int op, @NotNull OutputPointer.Int32 pCurrent,
+    @NotNull OutputPointer.Int32 pHighwater, boolean reset
+  );
+
+  public static native int sqlite3_errcode(@NotNull sqlite3 db);
+
+  public static native String sqlite3_errmsg(@NotNull sqlite3 db);
+
+  static native int sqlite3_error_offset(@NotNull long ptrToDb);
+
+  /**
+     Note that the returned byte offset values assume UTF-8-encoded
+     inputs, so won't always match character offsets in Java Strings.
+  */
+  public static int sqlite3_error_offset(@NotNull sqlite3 db){
+    return sqlite3_error_offset(db.getNativePointer());
+  }
+
+  public static native String sqlite3_errstr(int resultCode);
+
+  public static native String sqlite3_expanded_sql(@NotNull sqlite3_stmt stmt);
+
+  static native int sqlite3_extended_errcode(@NotNull long ptrToDb);
+
+  public static int sqlite3_extended_errcode(@NotNull sqlite3 db){
+    return sqlite3_extended_errcode(db.getNativePointer());
+  }
+
+  public static native boolean sqlite3_extended_result_codes(
+    @NotNull sqlite3 db, boolean onoff
+  );
+
+  static native boolean sqlite3_get_autocommit(@NotNull long ptrToDb);
+
+  public static boolean sqlite3_get_autocommit(@NotNull sqlite3 db){
+    return sqlite3_get_autocommit(db.getNativePointer());
+  }
+
+  public static native Object sqlite3_get_auxdata(
+    @NotNull sqlite3_context cx, int n
+  );
+
+  static native int sqlite3_finalize(long ptrToStmt);
+
+  public static int sqlite3_finalize(@NotNull sqlite3_stmt stmt){
+    return null==stmt ? 0 : sqlite3_finalize(stmt.clearNativePointer());
+  }
+
+  public static native int sqlite3_initialize();
+
+  public static native void sqlite3_interrupt(@NotNull sqlite3 db);
+
+  public static native boolean sqlite3_is_interrupted(@NotNull sqlite3 db);
+
+  public static native boolean sqlite3_keyword_check(@NotNull String word);
+
+  public static native int sqlite3_keyword_count();
+
+  public static native String sqlite3_keyword_name(int index);
+
+
+  public static native long sqlite3_last_insert_rowid(@NotNull sqlite3 db);
+
+  public static native String sqlite3_libversion();
+
+  public static native int sqlite3_libversion_number();
+
+  public static native int sqlite3_limit(@NotNull sqlite3 db, int id, int newVal);
+
+  /**
+     Only available if built with SQLITE_ENABLE_NORMALIZE. If not, it always
+     returns null.
+  */
+  public static native String sqlite3_normalized_sql(@NotNull sqlite3_stmt stmt);
+
+  /**
+     Works like its C counterpart and makes the native pointer of the
+     underling (sqlite3*) object available via
+     ppDb.getNativePointer(). That pointer is necessary for looking up
+     the JNI-side native, but clients need not pay it any
+     heed. Passing the object to sqlite3_close() or sqlite3_close_v2()
+     will clear that pointer mapping.
+
+     
Recall that even if opening fails, the output pointer might be
+     non-null. Any error message about the failure will be in that
+     object and it is up to the caller to sqlite3_close() that
+     db handle.
+  */
+  public static native int sqlite3_open(
+    @Nullable String filename, @NotNull OutputPointer.sqlite3 ppDb
+  );
+
+  /**
+     Convenience overload which returns its db handle directly. The returned
+     object might not have been successfully opened: use sqlite3_errcode() to
+     check whether it is in an error state.
+
+     
Ownership of the returned value is passed to the caller, who must eventually
+     pass it to sqlite3_close() or sqlite3_close_v2().
+  */
+  public static sqlite3 sqlite3_open(@Nullable String filename){
+    final OutputPointer.sqlite3 out = new OutputPointer.sqlite3();
+    sqlite3_open(filename, out);
+    return out.take();
+  };
+
+  public static native int sqlite3_open_v2(
+    @Nullable String filename, @NotNull OutputPointer.sqlite3 ppDb,
+    int flags, @Nullable String zVfs
+  );
+
+  /**
+     Has the same semantics as the sqlite3-returning sqlite3_open()
+     but uses sqlite3_open_v2() instead of sqlite3_open().
+  */
+  public static sqlite3 sqlite3_open_v2(@Nullable String filename, int flags,
+                                        @Nullable String zVfs){
+    final OutputPointer.sqlite3 out = new OutputPointer.sqlite3();
+    sqlite3_open_v2(filename, out, flags, zVfs);
+    return out.take();
+  };
+
+  /**
+     The sqlite3_prepare() family of functions require slightly
+     different signatures than their native counterparts, but (A) they
+     retain functionally equivalent semantics and (B) overloading
+     allows us to install several convenience forms.
+
+     
All of them which take their SQL in the form of a byte[] require
+     that it be in UTF-8 encoding unless explicitly noted otherwise.
+
+     
The forms which take a "tail" output pointer return (via that
+     output object) the index into their SQL byte array at which the
+     end of the first SQL statement processed by the call was
+     found. That's fundamentally how the C APIs work but making use of
+     that value requires more copying of the input SQL into
+     consecutively smaller arrays in order to consume all of
+     it. (There is an example of doing that in this project's Tester1
+     class.) For that vast majority of uses, that capability is not
+     necessary, however, and overloads are provided which gloss over
+     that.
+
+     
Results are undefined if maxBytes>sqlUtf8.length.
+
+     
This routine is private because its maxBytes value is not
+     strictly necessary in the Java interface, as sqlUtf8.length tells
+     us the information we need. Making this public would give clients
+     more ways to shoot themselves in the foot without providing any
+     real utility.
+  */
+  private static native int sqlite3_prepare(
+    @NotNull long ptrToDb, @NotNull byte[] sqlUtf8, int maxBytes,
+    @NotNull OutputPointer.sqlite3_stmt outStmt,
+    @Nullable OutputPointer.Int32 pTailOffset
+  );
+
+  /**
+     Works like the canonical sqlite3_prepare() but its "tail" output
+     argument is returned as the index offset into the given
+     UTF-8-encoded byte array at which SQL parsing stopped. The
+     semantics are otherwise identical to the C API counterpart.
+
+     
Several overloads provided simplified call signatures.
+  */
+  public static int sqlite3_prepare(
+    @NotNull sqlite3 db, @NotNull byte[] sqlUtf8,
+    @NotNull OutputPointer.sqlite3_stmt outStmt,
+    @Nullable OutputPointer.Int32 pTailOffset
+  ){
+    return sqlite3_prepare(db.getNativePointer(), sqlUtf8, sqlUtf8.length,
+                           outStmt, pTailOffset);
+  }
+
+  public static int sqlite3_prepare(
+    @NotNull sqlite3 db, @NotNull byte[] sqlUtf8,
+    @NotNull OutputPointer.sqlite3_stmt outStmt
+  ){
+    return sqlite3_prepare(db.getNativePointer(), sqlUtf8, sqlUtf8.length,
+                           outStmt, null);
+  }
+
+  public static int sqlite3_prepare(
+    @NotNull sqlite3 db, @NotNull String sql,
+    @NotNull OutputPointer.sqlite3_stmt outStmt
+  ){
+    final byte[] utf8 = sql.getBytes(StandardCharsets.UTF_8);
+    return sqlite3_prepare(db.getNativePointer(), utf8, utf8.length,
+                           outStmt, null);
+  }
+
+  /**
+     Convenience overload which returns its statement handle directly,
+     or null on error or when reading only whitespace or
+     comments. sqlite3_errcode() can be used to determine whether
+     there was an error or the input was empty. Ownership of the
+     returned object is passed to the caller, who must eventually pass
+     it to sqlite3_finalize().
+  */
+  public static sqlite3_stmt sqlite3_prepare(
+    @NotNull sqlite3 db, @NotNull String sql
+  ){
+    final OutputPointer.sqlite3_stmt out = new OutputPointer.sqlite3_stmt();
+    sqlite3_prepare(db, sql, out);
+    return out.take();
+  }
+  /**
+     @see #sqlite3_prepare
+  */
+  private static native int sqlite3_prepare_v2(
+    @NotNull long ptrToDb, @NotNull byte[] sqlUtf8, int maxBytes,
+    @NotNull OutputPointer.sqlite3_stmt outStmt,
+    @Nullable OutputPointer.Int32 pTailOffset
+  );
+
+  /**
+     Works like the canonical sqlite3_prepare_v2() but its "tail"
+     output paramter is returned as the index offset into the given
+     byte array at which SQL parsing stopped.
+  */
+  public static int sqlite3_prepare_v2(
+    @NotNull sqlite3 db, @NotNull byte[] sqlUtf8,
+    @NotNull OutputPointer.sqlite3_stmt outStmt,
+    @Nullable OutputPointer.Int32 pTailOffset
+  ){
+    return sqlite3_prepare_v2(db.getNativePointer(), sqlUtf8, sqlUtf8.length,
+                              outStmt, pTailOffset);
+  }
+
+  public static int sqlite3_prepare_v2(
+    @NotNull sqlite3 db, @NotNull byte[] sqlUtf8,
+    @NotNull OutputPointer.sqlite3_stmt outStmt
+  ){
+    return sqlite3_prepare_v2(db.getNativePointer(), sqlUtf8, sqlUtf8.length,
+                              outStmt, null);
+  }
+
+  public static int sqlite3_prepare_v2(
+    @NotNull sqlite3 db, @NotNull String sql,
+    @NotNull OutputPointer.sqlite3_stmt outStmt
+  ){
+    final byte[] utf8 = sql.getBytes(StandardCharsets.UTF_8);
+    return sqlite3_prepare_v2(db.getNativePointer(), utf8, utf8.length,
+                              outStmt, null);
+  }
+
+  /**
+     Works identically to the sqlite3_stmt-returning sqlite3_prepare()
+     but uses sqlite3_prepare_v2().
+  */
+  public static sqlite3_stmt sqlite3_prepare_v2(
+    @NotNull sqlite3 db, @NotNull String sql
+  ){
+    final OutputPointer.sqlite3_stmt out = new OutputPointer.sqlite3_stmt();
+    sqlite3_prepare_v2(db, sql, out);
+    return out.take();
+  }
+
+  /**
+     @see #sqlite3_prepare
+  */
+  private static native int sqlite3_prepare_v3(
+    @NotNull long ptrToDb, @NotNull byte[] sqlUtf8, int maxBytes,
+    int prepFlags, @NotNull OutputPointer.sqlite3_stmt outStmt,
+    @Nullable OutputPointer.Int32 pTailOffset
+  );
+
+  /**
+     Works like the canonical sqlite3_prepare_v2() but its "tail"
+     output paramter is returned as the index offset into the given
+     byte array at which SQL parsing stopped.
+  */
+  public static int sqlite3_prepare_v3(
+    @NotNull sqlite3 db, @NotNull byte[] sqlUtf8, int prepFlags,
+    @NotNull OutputPointer.sqlite3_stmt outStmt,
+    @Nullable OutputPointer.Int32 pTailOffset
+  ){
+    return sqlite3_prepare_v3(db.getNativePointer(), sqlUtf8, sqlUtf8.length,
+                              prepFlags, outStmt, pTailOffset);
+  }
+
+  /**
+     Convenience overload which elides the seldom-used pTailOffset
+     parameter.
+  */
+  public static int sqlite3_prepare_v3(
+    @NotNull sqlite3 db, @NotNull byte[] sqlUtf8, int prepFlags,
+    @NotNull OutputPointer.sqlite3_stmt outStmt
+  ){
+    return sqlite3_prepare_v3(db.getNativePointer(), sqlUtf8, sqlUtf8.length,
+                              prepFlags, outStmt, null);
+  }
+
+  /**
+     Convenience overload which elides the seldom-used pTailOffset
+     parameter and converts the given string to UTF-8 before passing
+     it on.
+  */
+  public static int sqlite3_prepare_v3(
+    @NotNull sqlite3 db, @NotNull String sql, int prepFlags,
+    @NotNull OutputPointer.sqlite3_stmt outStmt
+  ){
+    final byte[] utf8 = sql.getBytes(StandardCharsets.UTF_8);
+    return sqlite3_prepare_v3(db.getNativePointer(), utf8, utf8.length,
+                              prepFlags, outStmt, null);
+  }
+
+  /**
+     Works identically to the sqlite3_stmt-returning sqlite3_prepare()
+     but uses sqlite3_prepare_v3().
+  */
+  public static sqlite3_stmt sqlite3_prepare_v3(
+    @NotNull sqlite3 db, @NotNull String sql, int prepFlags
+  ){
+    final OutputPointer.sqlite3_stmt out = new OutputPointer.sqlite3_stmt();
+    sqlite3_prepare_v3(db, sql, prepFlags, out);
+    return out.take();
+  }
+
+  /**
+     A convenience wrapper around sqlite3_prepare_v3() which accepts
+     an arbitrary amount of input provided as a UTF-8-encoded byte
+     array.  It loops over the input bytes looking for
+     statements. Each one it finds is passed to p.call(), passing
+     ownership of it to that function. If p.call() returns 0, looping
+     continues, else the loop stops.
+
+     
If p.call() throws, the exception is propagated.
+
+     
How each statement is handled, including whether it is finalized
+     or not, is up to the callback object. e.g. the callback might
+     collect them for later use. If it does not collect them then it
+     must finalize them. See PrepareMultiCallback.Finalize for a
+     simple proxy which does that.
+  */
+  public static int sqlite3_prepare_multi(
+    @NotNull sqlite3 db, @NotNull byte[] sqlUtf8,
+    int preFlags,
+    @NotNull PrepareMultiCallback p){
+    final OutputPointer.Int32 oTail = new OutputPointer.Int32();
+    int pos = 0, n = 1;
+    byte[] sqlChunk = sqlUtf8;
+    int rc = 0;
+    final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt();
+    while(0==rc && pos 0){
+        sqlChunk = Arrays.copyOfRange(sqlChunk, pos,
+                                      sqlChunk.length);
+      }
+      if( 0==sqlChunk.length ) break;
+      rc = sqlite3_prepare_v3(db, sqlChunk, preFlags, outStmt, oTail);
+      if( 0!=rc ) break;
+      pos = oTail.value;
+      stmt = outStmt.take();
+      if( null == stmt ){
+        // empty statement was parsed.
+        continue;
+      }
+      rc = p.call(stmt);
+    }
+    return rc;
+  }
+
+  /**
+     Convenience overload which accepts its SQL as a String and uses
+     no statement-preparation flags.
+  */
+  public static int sqlite3_prepare_multi(
+    @NotNull sqlite3 db, @NotNull byte[] sqlUtf8,
+    @NotNull PrepareMultiCallback p){
+    return sqlite3_prepare_multi(db, sqlUtf8, 0, p);
+  }
+
+  /**
+     Convenience overload which accepts its SQL as a String.
+  */
+  public static int sqlite3_prepare_multi(
+    @NotNull sqlite3 db, @NotNull String sql, int prepFlags,
+    @NotNull PrepareMultiCallback p){
+    return sqlite3_prepare_multi(
+      db, sql.getBytes(StandardCharsets.UTF_8), prepFlags, p
+    );
+  }
+
+  /**
+     Convenience overload which accepts its SQL as a String and uses
+     no statement-preparation flags.
+  */
+  public static int sqlite3_prepare_multi(
+    @NotNull sqlite3 db, @NotNull String sql,
+    @NotNull PrepareMultiCallback p){
+    return sqlite3_prepare_multi(db, sql, 0, p);
+  }
+
+  /**
+     Convenience overload which accepts its SQL as a String
+     array. They will be concatenated together as-is, with no
+     separator, and passed on to one of the other overloads.
+  */
+  public static int sqlite3_prepare_multi(
+    @NotNull sqlite3 db, @NotNull String[] sql, int prepFlags,
+    @NotNull PrepareMultiCallback p){
+    return sqlite3_prepare_multi(db, String.join("",sql), prepFlags, p);
+  }
+
+  /**
+     Convenience overload which uses no statement-preparation flags.
+  */
+  public static int sqlite3_prepare_multi(
+    @NotNull sqlite3 db, @NotNull String[] sql,
+    @NotNull PrepareMultiCallback p){
+    return sqlite3_prepare_multi(db, sql, 0, p);
+  }
+
+  static native int sqlite3_preupdate_blobwrite(@NotNull long ptrToDb);
+
+  /**
+     If the C API was built with SQLITE_ENABLE_PREUPDATE_HOOK defined, this
+     acts as a proxy for C's sqlite3_preupdate_blobwrite(), else it returns
+     SQLITE_MISUSE with no side effects.
+  */
+  public static int sqlite3_preupdate_blobwrite(@NotNull sqlite3 db){
+    return sqlite3_preupdate_blobwrite(db.getNativePointer());
+  }
+
+  static native int sqlite3_preupdate_count(@NotNull long ptrToDb);
+
+  /**
+     If the C API was built with SQLITE_ENABLE_PREUPDATE_HOOK defined, this
+     acts as a proxy for C's sqlite3_preupdate_count(), else it returns
+     SQLITE_MISUSE with no side effects.
+  */
+  public static int sqlite3_preupdate_count(@NotNull sqlite3 db){
+    return sqlite3_preupdate_count(db.getNativePointer());
+  }
+
+  static native int sqlite3_preupdate_depth(@NotNull long ptrToDb);
+
+  /**
+     If the C API was built with SQLITE_ENABLE_PREUPDATE_HOOK defined, this
+     acts as a proxy for C's sqlite3_preupdate_depth(), else it returns
+     SQLITE_MISUSE with no side effects.
+  */
+  public static int sqlite3_preupdate_depth(@NotNull sqlite3 db){
+    return sqlite3_preupdate_depth(db.getNativePointer());
+  }
+
+  static native PreupdateHookCallback sqlite3_preupdate_hook(
+    @NotNull long ptrToDb, @Nullable PreupdateHookCallback hook
+  );
+
+  /**
+     If the C API was built with SQLITE_ENABLE_PREUPDATE_HOOK defined, this
+     acts as a proxy for C's sqlite3_preupdate_hook(), else it returns null
+     with no side effects.
+  */
+  public static PreupdateHookCallback sqlite3_preupdate_hook(
+    @NotNull sqlite3 db, @Nullable PreupdateHookCallback hook
+  ){
+    return sqlite3_preupdate_hook(db.getNativePointer(), hook);
+  }
+
+  static native int sqlite3_preupdate_new(@NotNull long ptrToDb, int col,
+                                                 @NotNull OutputPointer.sqlite3_value out);
+
+  /**
+     If the C API was built with SQLITE_ENABLE_PREUPDATE_HOOK defined,
+     this acts as a proxy for C's sqlite3_preupdate_new(), else it
+     returns SQLITE_MISUSE with no side effects.
+  */
+  public static int sqlite3_preupdate_new(@NotNull sqlite3 db, int col,
+                                          @NotNull OutputPointer.sqlite3_value out){
+    return sqlite3_preupdate_new(db.getNativePointer(), col, out);
+  }
+
+  /**
+     Convenience wrapper for the 3-arg sqlite3_preupdate_new() which returns
+     null on error.
+  */
+  public static sqlite3_value sqlite3_preupdate_new(@NotNull sqlite3 db, int col){
+    final OutputPointer.sqlite3_value out = new OutputPointer.sqlite3_value();
+    sqlite3_preupdate_new(db.getNativePointer(), col, out);
+    return out.take();
+  }
+
+  static native int sqlite3_preupdate_old(@NotNull long ptrToDb, int col,
+                                                 @NotNull OutputPointer.sqlite3_value out);
+
+  /**
+     If the C API was built with SQLITE_ENABLE_PREUPDATE_HOOK defined,
+     this acts as a proxy for C's sqlite3_preupdate_old(), else it
+     returns SQLITE_MISUSE with no side effects.
+  */
+  public static int sqlite3_preupdate_old(@NotNull sqlite3 db, int col,
+                                          @NotNull OutputPointer.sqlite3_value out){
+    return sqlite3_preupdate_old(db.getNativePointer(), col, out);
+  }
+
+  /**
+     Convenience wrapper for the 3-arg sqlite3_preupdate_old() which returns
+     null on error.
+  */
+  public static sqlite3_value sqlite3_preupdate_old(@NotNull sqlite3 db, int col){
+    final OutputPointer.sqlite3_value out = new OutputPointer.sqlite3_value();
+    sqlite3_preupdate_old(db.getNativePointer(), col, out);
+    return out.take();
+  }
+
+  public static native void sqlite3_progress_handler(
+    @NotNull sqlite3 db, int n, @Nullable ProgressHandlerCallback h
+  );
+
+  public static native void sqlite3_randomness(byte[] target);
+
+  public static native int sqlite3_release_memory(int n);
+
+  public static native int sqlite3_reset(@NotNull sqlite3_stmt stmt);
+
+  /**
+     Works like the C API except that it has no side effects if auto
+     extensions are currently running. (The JNI-level list of
+     extensions cannot be manipulated while it is being traversed.)
+  */
+  public static native void sqlite3_reset_auto_extension();
+
+  public static native void sqlite3_result_double(
+    @NotNull sqlite3_context cx, double v
+  );
+
+  /**
+     The main sqlite3_result_error() impl of which all others are
+     proxies. eTextRep must be one of SQLITE_UTF8 or SQLITE_UTF16 and
+     msg must be encoded correspondingly. Any other eTextRep value
+     results in the C-level sqlite3_result_error() being called with a
+     complaint about the invalid argument.
+  */
+  static native void sqlite3_result_error(
+    @NotNull sqlite3_context cx, @NotNull byte[] msg, int eTextRep
+  );
+
+  public static void sqlite3_result_error(
+    @NotNull sqlite3_context cx, @NotNull byte[] utf8
+  ){
+    sqlite3_result_error(cx, utf8, SQLITE_UTF8);
+  }
+
+  public static void sqlite3_result_error(
+    @NotNull sqlite3_context cx, @NotNull String msg
+  ){
+    final byte[] utf8 = msg.getBytes(StandardCharsets.UTF_8);
+    sqlite3_result_error(cx, utf8, SQLITE_UTF8);
+  }
+
+  public static void sqlite3_result_error16(
+    @NotNull sqlite3_context cx, @NotNull byte[] utf16
+  ){
+    sqlite3_result_error(cx, utf16, SQLITE_UTF16);
+  }
+
+  public static void sqlite3_result_error16(
+    @NotNull sqlite3_context cx, @NotNull String msg
+  ){
+    final byte[] utf16 = msg.getBytes(StandardCharsets.UTF_16);
+    sqlite3_result_error(cx, utf16, SQLITE_UTF16);
+  }
+
+  /**
+     Equivalent to passing e.toString() to {@link
+     #sqlite3_result_error(sqlite3_context,String)}.  Note that
+     toString() is used instead of getMessage() because the former
+     prepends the exception type name to the message.
+  */
+  public static void sqlite3_result_error(
+    @NotNull sqlite3_context cx, @NotNull Exception e
+  ){
+    sqlite3_result_error(cx, e.toString());
+  }
+
+  public static native void sqlite3_result_error_toobig(
+    @NotNull sqlite3_context cx
+  );
+
+  public static native void sqlite3_result_error_nomem(
+    @NotNull sqlite3_context cx
+  );
+
+  public static native void sqlite3_result_error_code(
+    @NotNull sqlite3_context cx, int c
+  );
+
+  public static native void sqlite3_result_null(
+    @NotNull sqlite3_context cx
+  );
+
+  public static native void sqlite3_result_int(
+    @NotNull sqlite3_context cx, int v
+  );
+
+  public static native void sqlite3_result_int64(
+    @NotNull sqlite3_context cx, long v
+  );
+
+  /**
+     Binds the SQL result to the given object, or {@link
+     #sqlite3_result_null} if {@code o} is null. Use {@link
+     #sqlite3_value_java_object} to fetch it.
+
+     This is implemented in terms of C's sqlite3_result_pointer(),
+     but that function is not exposed to JNI because (A)
+     cross-language semantic mismatch and (B) Java doesn't need that
+     argument for its intended purpose (type safety).
+
+     
Note that there is no sqlite3_column_java_object(), as the
+     C-level API has no sqlite3_column_pointer() to proxy.
+
+     @see #sqlite3_value_java_object
+     @see #sqlite3_bind_java_object
+  */
+  public static native void sqlite3_result_java_object(
+    @NotNull sqlite3_context cx, @NotNull Object o
+  );
+
+  public static void sqlite3_result_set(
+    @NotNull sqlite3_context cx, @NotNull Boolean v
+  ){
+    sqlite3_result_int(cx, v ? 1 : 0);
+  }
+
+  public static void sqlite3_result_set(
+    @NotNull sqlite3_context cx, boolean v
+  ){
+    sqlite3_result_int(cx, v ? 1 : 0);
+  }
+
+  public static void sqlite3_result_set(
+    @NotNull sqlite3_context cx, @NotNull Double v
+  ){
+    sqlite3_result_double(cx, v);
+  }
+
+  public static void sqlite3_result_set(
+    @NotNull sqlite3_context cx, double v
+  ){
+    sqlite3_result_double(cx, v);
+  }
+
+  public static void sqlite3_result_set(
+    @NotNull sqlite3_context cx, @NotNull Integer v
+  ){
+    sqlite3_result_int(cx, v);
+  }
+
+  public static void sqlite3_result_set(@NotNull sqlite3_context cx, int v){
+    sqlite3_result_int(cx, v);
+  }
+
+  public static void sqlite3_result_set(
+    @NotNull sqlite3_context cx, @NotNull Long v
+  ){
+    sqlite3_result_int64(cx, v);
+  }
+
+  public static void sqlite3_result_set(
+    @NotNull sqlite3_context cx, long v
+  ){
+    sqlite3_result_int64(cx, v);
+  }
+
+  public static void sqlite3_result_set(
+    @NotNull sqlite3_context cx, @Nullable String v
+  ){
+    if( null==v ) sqlite3_result_null(cx);
+    else sqlite3_result_text(cx, v);
+  }
+
+  public static void sqlite3_result_set(
+    @NotNull sqlite3_context cx, @Nullable byte[] blob
+  ){
+    if( null==blob ) sqlite3_result_null(cx);
+    else sqlite3_result_blob(cx, blob, blob.length);
+  }
+
+  public static native void sqlite3_result_value(
+    @NotNull sqlite3_context cx, @NotNull sqlite3_value v
+  );
+
+  public static native void sqlite3_result_zeroblob(
+    @NotNull sqlite3_context cx, int n
+  );
+
+  public static native int sqlite3_result_zeroblob64(
+    @NotNull sqlite3_context cx, long n
+  );
+
+  /**
+     This overload is private because its final parameter is arguably
+     unnecessary in Java.
+  */
+  private static native void sqlite3_result_blob(
+    @NotNull sqlite3_context cx, @Nullable byte[] blob, int maxLen
+  );
+
+  public static void sqlite3_result_blob(
+    @NotNull sqlite3_context cx, @Nullable byte[] blob
+  ){
+    sqlite3_result_blob(cx, blob, (int)(null==blob ? 0 : blob.length));
+  }
+
+  /**
+     Binds the given text using C's sqlite3_result_blob64() unless:
+
+     
+
+     - @param blob is null: translates to sqlite3_result_null()+
+
- @param blob is too large: translates to
+     sqlite3_result_error_toobig()+
+
+
+If @param maxLen is larger than blob.length, it is truncated
+     to that value. If it is negative, results are undefined.
+
+     This overload is private because its final parameter is
+     arguably unnecessary in Java.
+  */
+  private static native void sqlite3_result_blob64(
+    @NotNull sqlite3_context cx, @Nullable byte[] blob, long maxLen
+  );
+
+  public static void sqlite3_result_blob64(
+    @NotNull sqlite3_context cx, @Nullable byte[] blob
+  ){
+    sqlite3_result_blob64(cx, blob, (long)(null==blob ? 0 : blob.length));
+  }
+
+  /**
+     This overload is private because its final parameter is
+     arguably unnecessary in Java.
+  */
+  private static native void sqlite3_result_text(
+    @NotNull sqlite3_context cx, @Nullable byte[] utf8, int maxLen
+  );
+
+  public static void sqlite3_result_text(
+    @NotNull sqlite3_context cx, @Nullable byte[] utf8
+  ){
+    sqlite3_result_text(cx, utf8, null==utf8 ? 0 : utf8.length);
+  }
+
+  public static void sqlite3_result_text(
+    @NotNull sqlite3_context cx, @Nullable String text
+  ){
+    if(null == text) sqlite3_result_null(cx);
+    else{
+      final byte[] utf8 = text.getBytes(StandardCharsets.UTF_8);
+      sqlite3_result_text(cx, utf8, utf8.length);
+    }
+  }
+
+  /**
+     Binds the given text using C's sqlite3_result_text64() unless:
+
+     
+
+     - text is null: translates to a call to sqlite3_result_null()+
+
- text is too large: translates to a call to
+     {@link #sqlite3_result_error_toobig}+
+
- The @param encoding argument has an invalid value: translates to
+     {@link sqlite3_result_error_code} with code SQLITE_FORMAT.+
+
+
+     If maxLength (in bytes, not characters) is larger than
+     text.length, it is silently truncated to text.length. If it is
+     negative, results are undefined. If text is null, the subsequent
+     arguments are ignored.
+
+     This overload is private because its maxLength parameter is
+     arguably unnecessary in Java.
+  */
+  private static native void sqlite3_result_text64(
+    @NotNull sqlite3_context cx, @Nullable byte[] text,
+    long maxLength, int encoding
+  );
+
+  /**
+     Sets the current UDF result to the given bytes, which are assumed
+     be encoded in UTF-16 using the platform's byte order.
+  */
+  public static void sqlite3_result_text16(
+    @NotNull sqlite3_context cx, @Nullable byte[] utf16
+  ){
+    if(null == utf16) sqlite3_result_null(cx);
+    else sqlite3_result_text64(cx, utf16, utf16.length, SQLITE_UTF16);
+  }
+
+  public static void sqlite3_result_text16(
+    @NotNull sqlite3_context cx, @Nullable String text
+  ){
+    if(null == text) sqlite3_result_null(cx);
+    else{
+      final byte[] b = text.getBytes(StandardCharsets.UTF_16);
+      sqlite3_result_text64(cx, b, b.length, SQLITE_UTF16);
+    }
+  }
+
+  static native RollbackHookCallback sqlite3_rollback_hook(
+    @NotNull long ptrToDb, @Nullable RollbackHookCallback hook
+  );
+
+  public static RollbackHookCallback sqlite3_rollback_hook(
+    @NotNull sqlite3 db, @Nullable RollbackHookCallback hook
+  ){
+    return sqlite3_rollback_hook(db.getNativePointer(), hook);
+  }
+
+  public static native int sqlite3_set_authorizer(
+    @NotNull sqlite3 db, @Nullable AuthorizerCallback auth
+  );
+
+  public static native void sqlite3_set_auxdata(
+    @NotNull sqlite3_context cx, int n, @Nullable Object data
+  );
+
+  public static native void sqlite3_set_last_insert_rowid(
+    @NotNull sqlite3 db, long rowid
+  );
+
+
+  /**
+     In addition to calling the C-level sqlite3_shutdown(), the JNI
+     binding also cleans up all stale per-thread state managed by the
+     library, as well as any registered auto-extensions, and frees up
+     various bits of memory. Calling this while database handles or
+     prepared statements are still active will leak resources. Trying
+     to use those objects after this routine is called invoked
+     undefined behavior.
+  */
+  public static synchronized native int sqlite3_shutdown();
+
+  public static native int sqlite3_sleep(int ms);
+
+  public static native String sqlite3_sourceid();
+
+  public static native String sqlite3_sql(@NotNull sqlite3_stmt stmt);
+
+  //! Consider removing this. We can use sqlite3_status64() instead,
+  // or use that one's impl with this one's name.
+  public static native int sqlite3_status(
+    int op, @NotNull OutputPointer.Int32 pCurrent,
+    @NotNull OutputPointer.Int32 pHighwater, boolean reset
+  );
+
+  public static native int sqlite3_status64(
+    int op, @NotNull OutputPointer.Int64 pCurrent,
+    @NotNull OutputPointer.Int64 pHighwater, boolean reset
+  );
+
+  public static native int sqlite3_step(@NotNull sqlite3_stmt stmt);
+
+  public static native boolean sqlite3_stmt_busy(@NotNull sqlite3_stmt stmt);
+
+  static native int sqlite3_stmt_explain(@NotNull long ptrToStmt, int op);
+
+  public static int sqlite3_stmt_explain(@NotNull sqlite3_stmt stmt, int op){
+    return sqlite3_stmt_explain(stmt.getNativePointer(), op);
+  }
+
+  static native int sqlite3_stmt_isexplain(@NotNull long ptrToStmt);
+
+  public static int sqlite3_stmt_isexplain(@NotNull sqlite3_stmt stmt){
+    return sqlite3_stmt_isexplain(stmt.getNativePointer());
+  }
+
+  public static native boolean sqlite3_stmt_readonly(@NotNull sqlite3_stmt stmt);
+
+  public static native int sqlite3_stmt_status(
+    @NotNull sqlite3_stmt stmt, int op, boolean reset
+  );
+
+  /**
+     Internal impl of the public sqlite3_strglob() method. Neither
+     argument may be null and both must be NUL-terminated UTF-8.
+
+     This overload is private because: (A) to keep users from
+     inadvertently passing non-NUL-terminated byte arrays (an easy
+     thing to do). (B) it is cheaper to NUL-terminate the
+     String-to-byte-array conversion in the Java implementation
+     (sqlite3_strglob(String,String)) than to do that in C, so that
+     signature is the public-facing one.
+  */
+  private static native int sqlite3_strglob(
+    @NotNull byte[] glob, @NotNull byte[] nullTerminatedUtf8
+  );
+
+  public static int sqlite3_strglob(
+    @NotNull String glob, @NotNull String txt
+  ){
+    return sqlite3_strglob(nulTerminateUtf8(glob),
+                           nulTerminateUtf8(txt));
+  }
+
+  /**
+     The LIKE counterpart of the private sqlite3_strglob() method.
+  */
+  private static native int sqlite3_strlike(
+    @NotNull byte[] glob, @NotNull byte[] nullTerminatedUtf8,
+    int escChar
+  );
+
+  public static int sqlite3_strlike(
+    @NotNull String glob, @NotNull String txt, char escChar
+  ){
+    return sqlite3_strlike(nulTerminateUtf8(glob),
+                           nulTerminateUtf8(txt),
+                           (int)escChar);
+  }
+
+  static native int sqlite3_system_errno(@NotNull long ptrToDb);
+
+  public static int sqlite3_system_errno(@NotNull sqlite3 db){
+    return sqlite3_system_errno(db.getNativePointer());
+  }
+
+  public static native int sqlite3_table_column_metadata(
+    @NotNull sqlite3 db, @NotNull String zDbName,
+    @NotNull String zTableName, @NotNull String zColumnName,
+    @Nullable OutputPointer.String pzDataType,
+    @Nullable OutputPointer.String pzCollSeq,
+    @Nullable OutputPointer.Bool pNotNull,
+    @Nullable OutputPointer.Bool pPrimaryKey,
+    @Nullable OutputPointer.Bool pAutoinc
+  );
+
+  /**
+     Convenience overload which returns its results via a single
+     output object. If this function returns non-0 (error), the the
+     contents of the output object are not modified.
+  */
+  public static int sqlite3_table_column_metadata(
+    @NotNull sqlite3 db, @NotNull String zDbName,
+    @NotNull String zTableName, @NotNull String zColumnName,
+    @NotNull TableColumnMetadata out){
+    return sqlite3_table_column_metadata(
+      db, zDbName, zTableName, zColumnName,
+      out.pzDataType, out.pzCollSeq, out.pNotNull,
+      out.pPrimaryKey, out.pAutoinc);
+  }
+
+  /**
+     Convenience overload which returns the column metadata object on
+     success and null on error.
+  */
+  public static TableColumnMetadata sqlite3_table_column_metadata(
+    @NotNull sqlite3 db, @NotNull String zDbName,
+    @NotNull String zTableName, @NotNull String zColumnName){
+    final TableColumnMetadata out = new TableColumnMetadata();
+    return 0==sqlite3_table_column_metadata(
+      db, zDbName, zTableName, zColumnName, out
+    ) ? out : null;
+  }
+
+  public static native int sqlite3_threadsafe();
+
+  static native int sqlite3_total_changes(@NotNull long ptrToDb);
+
+  public static int sqlite3_total_changes(@NotNull sqlite3 db){
+    return sqlite3_total_changes(db.getNativePointer());
+  }
+
+  static native long sqlite3_total_changes64(@NotNull long ptrToDb);
+
+  public static long sqlite3_total_changes64(@NotNull sqlite3 db){
+    return sqlite3_total_changes64(db.getNativePointer());
+  }
+
+  /**
+     Works like C's sqlite3_trace_v2() except that the 3rd argument to that
+     function is elided here because the roles of that functions' 3rd and 4th
+     arguments are encapsulated in the final argument to this function.
+
+Unlike the C API, which is documented as always returning 0,
+     this implementation returns non-0 if initialization of the tracer
+     mapping state fails (e.g. on OOM).
+  */
+  public static native int sqlite3_trace_v2(
+    @NotNull sqlite3 db, int traceMask, @Nullable TraceV2Callback tracer
+  );
+
+  public static native int sqlite3_txn_state(
+    @NotNull sqlite3 db, @Nullable String zSchema
+  );
+
+  static native UpdateHookCallback sqlite3_update_hook(
+    @NotNull long ptrToDb, @Nullable UpdateHookCallback hook
+  );
+
+  public static UpdateHookCallback sqlite3_update_hook(
+    @NotNull sqlite3 db, @Nullable UpdateHookCallback hook
+  ){
+    return sqlite3_update_hook(db.getNativePointer(), hook);
+  }
+
+  /*
+     Note that:
+
+     void * sqlite3_user_data(sqlite3_context*)
+
+     Is not relevant in the JNI binding, as its feature is replaced by
+     the ability to pass an object, including any relevant state, to
+     sqlite3_create_function().
+  */
+
+  static native byte[] sqlite3_value_blob(@NotNull long ptrToValue);
+
+  public static byte[] sqlite3_value_blob(@NotNull sqlite3_value v){
+    return sqlite3_value_blob(v.getNativePointer());
+  }
+
+  static native int sqlite3_value_bytes(@NotNull long ptrToValue);
+
+  public static int sqlite3_value_bytes(@NotNull sqlite3_value v){
+    return sqlite3_value_bytes(v.getNativePointer());
+  }
+
+  static native int sqlite3_value_bytes16(@NotNull long ptrToValue);
+
+  public static int sqlite3_value_bytes16(@NotNull sqlite3_value v){
+    return sqlite3_value_bytes16(v.getNativePointer());
+  }
+
+  static native double sqlite3_value_double(@NotNull long ptrToValue);
+
+  public static double sqlite3_value_double(@NotNull sqlite3_value v){
+    return sqlite3_value_double(v.getNativePointer());
+  }
+
+  static native sqlite3_value sqlite3_value_dup(@NotNull long ptrToValue);
+
+  public static sqlite3_value sqlite3_value_dup(@NotNull sqlite3_value v){
+    return sqlite3_value_dup(v.getNativePointer());
+  }
+
+  static native int sqlite3_value_encoding(@NotNull long ptrToValue);
+
+  public static int sqlite3_value_encoding(@NotNull sqlite3_value v){
+    return sqlite3_value_encoding(v.getNativePointer());
+  }
+
+  static native void sqlite3_value_free(@Nullable long ptrToValue);
+
+  public static void sqlite3_value_free(@Nullable sqlite3_value v){
+    sqlite3_value_free(v.getNativePointer());
+  }
+
+  static native boolean sqlite3_value_frombind(@NotNull long ptrToValue);
+
+  public static boolean sqlite3_value_frombind(@NotNull sqlite3_value v){
+    return sqlite3_value_frombind(v.getNativePointer());
+  }
+
+  static native int sqlite3_value_int(@NotNull long ptrToValue);
+
+  public static int sqlite3_value_int(@NotNull sqlite3_value v){
+    return sqlite3_value_int(v.getNativePointer());
+  }
+
+  static native long sqlite3_value_int64(@NotNull long ptrToValue);
+
+  public static long sqlite3_value_int64(@NotNull sqlite3_value v){
+    return sqlite3_value_int64(v.getNativePointer());
+  }
+
+  static native Object sqlite3_value_java_object(@NotNull long ptrToValue);
+
+  /**
+     If the given value was set using {@link
+     #sqlite3_result_java_object} then this function returns that
+     object, else it returns null.
+
+     
It is up to the caller to inspect the object to determine its
+     type, and cast it if necessary.
+  */
+  public static Object sqlite3_value_java_object(@NotNull sqlite3_value v){
+    return sqlite3_value_java_object(v.getNativePointer());
+  }
+
+  /**
+     A variant of sqlite3_value_java_object() which returns the
+     fetched object cast to T if the object is an instance of the
+     given Class, else it returns null.
+  */
+  @SuppressWarnings("unchecked")
+  public static  T sqlite3_value_java_casted(@NotNull sqlite3_value v,
+                                                @NotNull Class type){
+    final Object o = sqlite3_value_java_object(v);
+    return type.isInstance(o) ? (T)o : null;
+  }
+
+  static native int sqlite3_value_nochange(@NotNull long ptrToValue);
+
+  public static int sqlite3_value_nochange(@NotNull sqlite3_value v){
+    return sqlite3_value_nochange(v.getNativePointer());
+  }
+
+  static native int sqlite3_value_numeric_type(@NotNull long ptrToValue);
+
+  public static int sqlite3_value_numeric_type(@NotNull sqlite3_value v){
+    return sqlite3_value_numeric_type(v.getNativePointer());
+  }
+
+  static native int sqlite3_value_subtype(@NotNull long ptrToValue);
+
+  public static int sqlite3_value_subtype(@NotNull sqlite3_value v){
+    return sqlite3_value_subtype(v.getNativePointer());
+  }
+
+  static native byte[] sqlite3_value_text(@NotNull long ptrToValue);
+
+  /**
+     Functions identially to the C API, and this note is just to
+     stress that the returned bytes are encoded as UTF-8. It returns
+     null if the underlying C-level sqlite3_value_text() returns NULL
+     or on allocation error.
+  */
+  public static byte[] sqlite3_value_text(@NotNull sqlite3_value v){
+    return sqlite3_value_text(v.getNativePointer());
+  }
+
+  static native String sqlite3_value_text16(@NotNull long ptrToValue);
+
+  public static String sqlite3_value_text16(@NotNull sqlite3_value v){
+    return sqlite3_value_text16(v.getNativePointer());
+  }
+
+  static native int sqlite3_value_type(@NotNull long ptrToValue);
+
+  public static int sqlite3_value_type(@NotNull sqlite3_value v){
+    return sqlite3_value_type(v.getNativePointer());
+  }
+
+  /**
+     This is NOT part of the public API. It exists solely as a place
+     for this code's developers to collect internal metrics and such.
+     It has no stable interface. It may go way or change behavior at
+     any time.
+  */
+  public static native void sqlite3_jni_internal_details();
+
+  //////////////////////////////////////////////////////////////////////
+  // SQLITE_... constants follow...
+
+  // version info
+  public static final int SQLITE_VERSION_NUMBER = sqlite3_libversion_number();
+  public static final String SQLITE_VERSION = sqlite3_libversion();
+  public static final String SQLITE_SOURCE_ID = sqlite3_sourceid();
+
+  // access
+  public static final int SQLITE_ACCESS_EXISTS = 0;
+  public static final int SQLITE_ACCESS_READWRITE = 1;
+  public static final int SQLITE_ACCESS_READ = 2;
+
+  // authorizer
+  public static final int SQLITE_DENY = 1;
+  public static final int SQLITE_IGNORE = 2;
+  public static final int SQLITE_CREATE_INDEX = 1;
+  public static final int SQLITE_CREATE_TABLE = 2;
+  public static final int SQLITE_CREATE_TEMP_INDEX = 3;
+  public static final int SQLITE_CREATE_TEMP_TABLE = 4;
+  public static final int SQLITE_CREATE_TEMP_TRIGGER = 5;
+  public static final int SQLITE_CREATE_TEMP_VIEW = 6;
+  public static final int SQLITE_CREATE_TRIGGER = 7;
+  public static final int SQLITE_CREATE_VIEW = 8;
+  public static final int SQLITE_DELETE = 9;
+  public static final int SQLITE_DROP_INDEX = 10;
+  public static final int SQLITE_DROP_TABLE = 11;
+  public static final int SQLITE_DROP_TEMP_INDEX = 12;
+  public static final int SQLITE_DROP_TEMP_TABLE = 13;
+  public static final int SQLITE_DROP_TEMP_TRIGGER = 14;
+  public static final int SQLITE_DROP_TEMP_VIEW = 15;
+  public static final int SQLITE_DROP_TRIGGER = 16;
+  public static final int SQLITE_DROP_VIEW = 17;
+  public static final int SQLITE_INSERT = 18;
+  public static final int SQLITE_PRAGMA = 19;
+  public static final int SQLITE_READ = 20;
+  public static final int SQLITE_SELECT = 21;
+  public static final int SQLITE_TRANSACTION = 22;
+  public static final int SQLITE_UPDATE = 23;
+  public static final int SQLITE_ATTACH = 24;
+  public static final int SQLITE_DETACH = 25;
+  public static final int SQLITE_ALTER_TABLE = 26;
+  public static final int SQLITE_REINDEX = 27;
+  public static final int SQLITE_ANALYZE = 28;
+  public static final int SQLITE_CREATE_VTABLE = 29;
+  public static final int SQLITE_DROP_VTABLE = 30;
+  public static final int SQLITE_FUNCTION = 31;
+  public static final int SQLITE_SAVEPOINT = 32;
+  public static final int SQLITE_RECURSIVE = 33;
+
+  // blob finalizers: these should, because they are treated as
+  // special pointer values in C, ideally have the same sizeof() as
+  // the platform's (void*), but we can't know that size from here.
+  public static final long SQLITE_STATIC = 0;
+  public static final long SQLITE_TRANSIENT = -1;
+
+  // changeset
+  public static final int SQLITE_CHANGESETSTART_INVERT = 2;
+  public static final int SQLITE_CHANGESETAPPLY_NOSAVEPOINT = 1;
+  public static final int SQLITE_CHANGESETAPPLY_INVERT = 2;
+  public static final int SQLITE_CHANGESETAPPLY_IGNORENOOP = 4;
+  public static final int SQLITE_CHANGESET_DATA = 1;
+  public static final int SQLITE_CHANGESET_NOTFOUND = 2;
+  public static final int SQLITE_CHANGESET_CONFLICT = 3;
+  public static final int SQLITE_CHANGESET_CONSTRAINT = 4;
+  public static final int SQLITE_CHANGESET_FOREIGN_KEY = 5;
+  public static final int SQLITE_CHANGESET_OMIT = 0;
+  public static final int SQLITE_CHANGESET_REPLACE = 1;
+  public static final int SQLITE_CHANGESET_ABORT = 2;
+
+  // config
+  public static final int SQLITE_CONFIG_SINGLETHREAD = 1;
+  public static final int SQLITE_CONFIG_MULTITHREAD = 2;
+  public static final int SQLITE_CONFIG_SERIALIZED = 3;
+  public static final int SQLITE_CONFIG_MALLOC = 4;
+  public static final int SQLITE_CONFIG_GETMALLOC = 5;
+  public static final int SQLITE_CONFIG_SCRATCH = 6;
+  public static final int SQLITE_CONFIG_PAGECACHE = 7;
+  public static final int SQLITE_CONFIG_HEAP = 8;
+  public static final int SQLITE_CONFIG_MEMSTATUS = 9;
+  public static final int SQLITE_CONFIG_MUTEX = 10;
+  public static final int SQLITE_CONFIG_GETMUTEX = 11;
+  public static final int SQLITE_CONFIG_LOOKASIDE = 13;
+  public static final int SQLITE_CONFIG_PCACHE = 14;
+  public static final int SQLITE_CONFIG_GETPCACHE = 15;
+  public static final int SQLITE_CONFIG_LOG = 16;
+  public static final int SQLITE_CONFIG_URI = 17;
+  public static final int SQLITE_CONFIG_PCACHE2 = 18;
+  public static final int SQLITE_CONFIG_GETPCACHE2 = 19;
+  public static final int SQLITE_CONFIG_COVERING_INDEX_SCAN = 20;
+  public static final int SQLITE_CONFIG_SQLLOG = 21;
+  public static final int SQLITE_CONFIG_MMAP_SIZE = 22;
+  public static final int SQLITE_CONFIG_WIN32_HEAPSIZE = 23;
+  public static final int SQLITE_CONFIG_PCACHE_HDRSZ = 24;
+  public static final int SQLITE_CONFIG_PMASZ = 25;
+  public static final int SQLITE_CONFIG_STMTJRNL_SPILL = 26;
+  public static final int SQLITE_CONFIG_SMALL_MALLOC = 27;
+  public static final int SQLITE_CONFIG_SORTERREF_SIZE = 28;
+  public static final int SQLITE_CONFIG_MEMDB_MAXSIZE = 29;
+
+  // data types
+  public static final int SQLITE_INTEGER = 1;
+  public static final int SQLITE_FLOAT = 2;
+  public static final int SQLITE_TEXT = 3;
+  public static final int SQLITE_BLOB = 4;
+  public static final int SQLITE_NULL = 5;
+
+  // db config
+  public static final int SQLITE_DBCONFIG_MAINDBNAME = 1000;
+  public static final int SQLITE_DBCONFIG_LOOKASIDE = 1001;
+  public static final int SQLITE_DBCONFIG_ENABLE_FKEY = 1002;
+  public static final int SQLITE_DBCONFIG_ENABLE_TRIGGER = 1003;
+  public static final int SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER = 1004;
+  public static final int SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION = 1005;
+  public static final int SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE = 1006;
+  public static final int SQLITE_DBCONFIG_ENABLE_QPSG = 1007;
+  public static final int SQLITE_DBCONFIG_TRIGGER_EQP = 1008;
+  public static final int SQLITE_DBCONFIG_RESET_DATABASE = 1009;
+  public static final int SQLITE_DBCONFIG_DEFENSIVE = 1010;
+  public static final int SQLITE_DBCONFIG_WRITABLE_SCHEMA = 1011;
+  public static final int SQLITE_DBCONFIG_LEGACY_ALTER_TABLE = 1012;
+  public static final int SQLITE_DBCONFIG_DQS_DML = 1013;
+  public static final int SQLITE_DBCONFIG_DQS_DDL = 1014;
+  public static final int SQLITE_DBCONFIG_ENABLE_VIEW = 1015;
+  public static final int SQLITE_DBCONFIG_LEGACY_FILE_FORMAT = 1016;
+  public static final int SQLITE_DBCONFIG_TRUSTED_SCHEMA = 1017;
+  public static final int SQLITE_DBCONFIG_STMT_SCANSTATUS = 1018;
+  public static final int SQLITE_DBCONFIG_REVERSE_SCANORDER = 1019;
+  public static final int SQLITE_DBCONFIG_MAX = 1019;
+
+  // db status
+  public static final int SQLITE_DBSTATUS_LOOKASIDE_USED = 0;
+  public static final int SQLITE_DBSTATUS_CACHE_USED = 1;
+  public static final int SQLITE_DBSTATUS_SCHEMA_USED = 2;
+  public static final int SQLITE_DBSTATUS_STMT_USED = 3;
+  public static final int SQLITE_DBSTATUS_LOOKASIDE_HIT = 4;
+  public static final int SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE = 5;
+  public static final int SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL = 6;
+  public static final int SQLITE_DBSTATUS_CACHE_HIT = 7;
+  public static final int SQLITE_DBSTATUS_CACHE_MISS = 8;
+  public static final int SQLITE_DBSTATUS_CACHE_WRITE = 9;
+  public static final int SQLITE_DBSTATUS_DEFERRED_FKS = 10;
+  public static final int SQLITE_DBSTATUS_CACHE_USED_SHARED = 11;
+  public static final int SQLITE_DBSTATUS_CACHE_SPILL = 12;
+  public static final int SQLITE_DBSTATUS_MAX = 12;
+
+  // encodings
+  public static final int SQLITE_UTF8 = 1;
+  public static final int SQLITE_UTF16LE = 2;
+  public static final int SQLITE_UTF16BE = 3;
+  public static final int SQLITE_UTF16 = 4;
+  public static final int SQLITE_UTF16_ALIGNED = 8;
+
+  // fcntl
+  public static final int SQLITE_FCNTL_LOCKSTATE = 1;
+  public static final int SQLITE_FCNTL_GET_LOCKPROXYFILE = 2;
+  public static final int SQLITE_FCNTL_SET_LOCKPROXYFILE = 3;
+  public static final int SQLITE_FCNTL_LAST_ERRNO = 4;
+  public static final int SQLITE_FCNTL_SIZE_HINT = 5;
+  public static final int SQLITE_FCNTL_CHUNK_SIZE = 6;
+  public static final int SQLITE_FCNTL_FILE_POINTER = 7;
+  public static final int SQLITE_FCNTL_SYNC_OMITTED = 8;
+  public static final int SQLITE_FCNTL_WIN32_AV_RETRY = 9;
+  public static final int SQLITE_FCNTL_PERSIST_WAL = 10;
+  public static final int SQLITE_FCNTL_OVERWRITE = 11;
+  public static final int SQLITE_FCNTL_VFSNAME = 12;
+  public static final int SQLITE_FCNTL_POWERSAFE_OVERWRITE = 13;
+  public static final int SQLITE_FCNTL_PRAGMA = 14;
+  public static final int SQLITE_FCNTL_BUSYHANDLER = 15;
+  public static final int SQLITE_FCNTL_TEMPFILENAME = 16;
+  public static final int SQLITE_FCNTL_MMAP_SIZE = 18;
+  public static final int SQLITE_FCNTL_TRACE = 19;
+  public static final int SQLITE_FCNTL_HAS_MOVED = 20;
+  public static final int SQLITE_FCNTL_SYNC = 21;
+  public static final int SQLITE_FCNTL_COMMIT_PHASETWO = 22;
+  public static final int SQLITE_FCNTL_WIN32_SET_HANDLE = 23;
+  public static final int SQLITE_FCNTL_WAL_BLOCK = 24;
+  public static final int SQLITE_FCNTL_ZIPVFS = 25;
+  public static final int SQLITE_FCNTL_RBU = 26;
+  public static final int SQLITE_FCNTL_VFS_POINTER = 27;
+  public static final int SQLITE_FCNTL_JOURNAL_POINTER = 28;
+  public static final int SQLITE_FCNTL_WIN32_GET_HANDLE = 29;
+  public static final int SQLITE_FCNTL_PDB = 30;
+  public static final int SQLITE_FCNTL_BEGIN_ATOMIC_WRITE = 31;
+  public static final int SQLITE_FCNTL_COMMIT_ATOMIC_WRITE = 32;
+  public static final int SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE = 33;
+  public static final int SQLITE_FCNTL_LOCK_TIMEOUT = 34;
+  public static final int SQLITE_FCNTL_DATA_VERSION = 35;
+  public static final int SQLITE_FCNTL_SIZE_LIMIT = 36;
+  public static final int SQLITE_FCNTL_CKPT_DONE = 37;
+  public static final int SQLITE_FCNTL_RESERVE_BYTES = 38;
+  public static final int SQLITE_FCNTL_CKPT_START = 39;
+  public static final int SQLITE_FCNTL_EXTERNAL_READER = 40;
+  public static final int SQLITE_FCNTL_CKSM_FILE = 41;
+  public static final int SQLITE_FCNTL_RESET_CACHE = 42;
+
+  // flock
+  public static final int SQLITE_LOCK_NONE = 0;
+  public static final int SQLITE_LOCK_SHARED = 1;
+  public static final int SQLITE_LOCK_RESERVED = 2;
+  public static final int SQLITE_LOCK_PENDING = 3;
+  public static final int SQLITE_LOCK_EXCLUSIVE = 4;
+
+  // iocap
+  public static final int SQLITE_IOCAP_ATOMIC = 1;
+  public static final int SQLITE_IOCAP_ATOMIC512 = 2;
+  public static final int SQLITE_IOCAP_ATOMIC1K = 4;
+  public static final int SQLITE_IOCAP_ATOMIC2K = 8;
+  public static final int SQLITE_IOCAP_ATOMIC4K = 16;
+  public static final int SQLITE_IOCAP_ATOMIC8K = 32;
+  public static final int SQLITE_IOCAP_ATOMIC16K = 64;
+  public static final int SQLITE_IOCAP_ATOMIC32K = 128;
+  public static final int SQLITE_IOCAP_ATOMIC64K = 256;
+  public static final int SQLITE_IOCAP_SAFE_APPEND = 512;
+  public static final int SQLITE_IOCAP_SEQUENTIAL = 1024;
+  public static final int SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN = 2048;
+  public static final int SQLITE_IOCAP_POWERSAFE_OVERWRITE = 4096;
+  public static final int SQLITE_IOCAP_IMMUTABLE = 8192;
+  public static final int SQLITE_IOCAP_BATCH_ATOMIC = 16384;
+
+  // limits
+  public static final int SQLITE_LIMIT_LENGTH = 0;
+  public static final int SQLITE_LIMIT_SQL_LENGTH = 1;
+  public static final int SQLITE_LIMIT_COLUMN = 2;
+  public static final int SQLITE_LIMIT_EXPR_DEPTH = 3;
+  public static final int SQLITE_LIMIT_COMPOUND_SELECT = 4;
+  public static final int SQLITE_LIMIT_VDBE_OP = 5;
+  public static final int SQLITE_LIMIT_FUNCTION_ARG = 6;
+  public static final int SQLITE_LIMIT_ATTACHED = 7;
+  public static final int SQLITE_LIMIT_LIKE_PATTERN_LENGTH = 8;
+  public static final int SQLITE_LIMIT_VARIABLE_NUMBER = 9;
+  public static final int SQLITE_LIMIT_TRIGGER_DEPTH = 10;
+  public static final int SQLITE_LIMIT_WORKER_THREADS = 11;
+
+  // open flags
+
+  public static final int SQLITE_OPEN_READONLY     = 0x00000001  /* Ok for sqlite3_open_v2() */;
+  public static final int SQLITE_OPEN_READWRITE    = 0x00000002  /* Ok for sqlite3_open_v2() */;
+  public static final int SQLITE_OPEN_CREATE       = 0x00000004  /* Ok for sqlite3_open_v2() */;
+  //public static final int SQLITE_OPEN_DELETEONCLOSE  = 0x00000008  /* VFS only */;
+  //public static final int SQLITE_OPEN_EXCLUSIVE  = 0x00000010  /* VFS only */;
+  //public static final int SQLITE_OPEN_AUTOPROXY  = 0x00000020  /* VFS only */;
+  public static final int SQLITE_OPEN_URI          = 0x00000040  /* Ok for sqlite3_open_v2() */;
+  public static final int SQLITE_OPEN_MEMORY       = 0x00000080  /* Ok for sqlite3_open_v2() */;
+  //public static final int SQLITE_OPEN_MAIN_DB    = 0x00000100  /* VFS only */;
+  //public static final int SQLITE_OPEN_TEMP_DB    = 0x00000200  /* VFS only */;
+  //public static final int SQLITE_OPEN_TRANSIENT_DB  = 0x00000400  /* VFS only */;
+  //public static final int SQLITE_OPEN_MAIN_JOURNAL  = 0x00000800  /* VFS only */;
+  //public static final int SQLITE_OPEN_TEMP_JOURNAL  = 0x00001000  /* VFS only */;
+  //public static final int SQLITE_OPEN_SUBJOURNAL    = 0x00002000  /* VFS only */;
+  //public static final int SQLITE_OPEN_SUPER_JOURNAL = 0x00004000  /* VFS only */;
+  public static final int SQLITE_OPEN_NOMUTEX       = 0x00008000  /* Ok for sqlite3_open_v2() */;
+  public static final int SQLITE_OPEN_FULLMUTEX     = 0x00010000  /* Ok for sqlite3_open_v2() */;
+  public static final int SQLITE_OPEN_SHAREDCACHE   = 0x00020000  /* Ok for sqlite3_open_v2() */;
+  public static final int SQLITE_OPEN_PRIVATECACHE  = 0x00040000  /* Ok for sqlite3_open_v2() */;
+  //public static final int SQLITE_OPEN_WAL         = 0x00080000  /* VFS only */;
+  public static final int SQLITE_OPEN_NOFOLLOW      = 0x01000000  /* Ok for sqlite3_open_v2() */;
+  public static final int SQLITE_OPEN_EXRESCODE     = 0x02000000  /* Extended result codes */;
+
+  // prepare flags
+  public static final int SQLITE_PREPARE_PERSISTENT = 1;
+  public static final int SQLITE_PREPARE_NORMALIZE = 2;
+  public static final int SQLITE_PREPARE_NO_VTAB = 4;
+
+  // result codes
+  public static final int SQLITE_OK = 0;
+  public static final int SQLITE_ERROR = 1;
+  public static final int SQLITE_INTERNAL = 2;
+  public static final int SQLITE_PERM = 3;
+  public static final int SQLITE_ABORT = 4;
+  public static final int SQLITE_BUSY = 5;
+  public static final int SQLITE_LOCKED = 6;
+  public static final int SQLITE_NOMEM = 7;
+  public static final int SQLITE_READONLY = 8;
+  public static final int SQLITE_INTERRUPT = 9;
+  public static final int SQLITE_IOERR = 10;
+  public static final int SQLITE_CORRUPT = 11;
+  public static final int SQLITE_NOTFOUND = 12;
+  public static final int SQLITE_FULL = 13;
+  public static final int SQLITE_CANTOPEN = 14;
+  public static final int SQLITE_PROTOCOL = 15;
+  public static final int SQLITE_EMPTY = 16;
+  public static final int SQLITE_SCHEMA = 17;
+  public static final int SQLITE_TOOBIG = 18;
+  public static final int SQLITE_CONSTRAINT = 19;
+  public static final int SQLITE_MISMATCH = 20;
+  public static final int SQLITE_MISUSE = 21;
+  public static final int SQLITE_NOLFS = 22;
+  public static final int SQLITE_AUTH = 23;
+  public static final int SQLITE_FORMAT = 24;
+  public static final int SQLITE_RANGE = 25;
+  public static final int SQLITE_NOTADB = 26;
+  public static final int SQLITE_NOTICE = 27;
+  public static final int SQLITE_WARNING = 28;
+  public static final int SQLITE_ROW = 100;
+  public static final int SQLITE_DONE = 101;
+  public static final int SQLITE_ERROR_MISSING_COLLSEQ = 257;
+  public static final int SQLITE_ERROR_RETRY = 513;
+  public static final int SQLITE_ERROR_SNAPSHOT = 769;
+  public static final int SQLITE_IOERR_READ = 266;
+  public static final int SQLITE_IOERR_SHORT_READ = 522;
+  public static final int SQLITE_IOERR_WRITE = 778;
+  public static final int SQLITE_IOERR_FSYNC = 1034;
+  public static final int SQLITE_IOERR_DIR_FSYNC = 1290;
+  public static final int SQLITE_IOERR_TRUNCATE = 1546;
+  public static final int SQLITE_IOERR_FSTAT = 1802;
+  public static final int SQLITE_IOERR_UNLOCK = 2058;
+  public static final int SQLITE_IOERR_RDLOCK = 2314;
+  public static final int SQLITE_IOERR_DELETE = 2570;
+  public static final int SQLITE_IOERR_BLOCKED = 2826;
+  public static final int SQLITE_IOERR_NOMEM = 3082;
+  public static final int SQLITE_IOERR_ACCESS = 3338;
+  public static final int SQLITE_IOERR_CHECKRESERVEDLOCK = 3594;
+  public static final int SQLITE_IOERR_LOCK = 3850;
+  public static final int SQLITE_IOERR_CLOSE = 4106;
+  public static final int SQLITE_IOERR_DIR_CLOSE = 4362;
+  public static final int SQLITE_IOERR_SHMOPEN = 4618;
+  public static final int SQLITE_IOERR_SHMSIZE = 4874;
+  public static final int SQLITE_IOERR_SHMLOCK = 5130;
+  public static final int SQLITE_IOERR_SHMMAP = 5386;
+  public static final int SQLITE_IOERR_SEEK = 5642;
+  public static final int SQLITE_IOERR_DELETE_NOENT = 5898;
+  public static final int SQLITE_IOERR_MMAP = 6154;
+  public static final int SQLITE_IOERR_GETTEMPPATH = 6410;
+  public static final int SQLITE_IOERR_CONVPATH = 6666;
+  public static final int SQLITE_IOERR_VNODE = 6922;
+  public static final int SQLITE_IOERR_AUTH = 7178;
+  public static final int SQLITE_IOERR_BEGIN_ATOMIC = 7434;
+  public static final int SQLITE_IOERR_COMMIT_ATOMIC = 7690;
+  public static final int SQLITE_IOERR_ROLLBACK_ATOMIC = 7946;
+  public static final int SQLITE_IOERR_DATA = 8202;
+  public static final int SQLITE_IOERR_CORRUPTFS = 8458;
+  public static final int SQLITE_LOCKED_SHAREDCACHE = 262;
+  public static final int SQLITE_LOCKED_VTAB = 518;
+  public static final int SQLITE_BUSY_RECOVERY = 261;
+  public static final int SQLITE_BUSY_SNAPSHOT = 517;
+  public static final int SQLITE_BUSY_TIMEOUT = 773;
+  public static final int SQLITE_CANTOPEN_NOTEMPDIR = 270;
+  public static final int SQLITE_CANTOPEN_ISDIR = 526;
+  public static final int SQLITE_CANTOPEN_FULLPATH = 782;
+  public static final int SQLITE_CANTOPEN_CONVPATH = 1038;
+  public static final int SQLITE_CANTOPEN_SYMLINK = 1550;
+  public static final int SQLITE_CORRUPT_VTAB = 267;
+  public static final int SQLITE_CORRUPT_SEQUENCE = 523;
+  public static final int SQLITE_CORRUPT_INDEX = 779;
+  public static final int SQLITE_READONLY_RECOVERY = 264;
+  public static final int SQLITE_READONLY_CANTLOCK = 520;
+  public static final int SQLITE_READONLY_ROLLBACK = 776;
+  public static final int SQLITE_READONLY_DBMOVED = 1032;
+  public static final int SQLITE_READONLY_CANTINIT = 1288;
+  public static final int SQLITE_READONLY_DIRECTORY = 1544;
+  public static final int SQLITE_ABORT_ROLLBACK = 516;
+  public static final int SQLITE_CONSTRAINT_CHECK = 275;
+  public static final int SQLITE_CONSTRAINT_COMMITHOOK = 531;
+  public static final int SQLITE_CONSTRAINT_FOREIGNKEY = 787;
+  public static final int SQLITE_CONSTRAINT_FUNCTION = 1043;
+  public static final int SQLITE_CONSTRAINT_NOTNULL = 1299;
+  public static final int SQLITE_CONSTRAINT_PRIMARYKEY = 1555;
+  public static final int SQLITE_CONSTRAINT_TRIGGER = 1811;
+  public static final int SQLITE_CONSTRAINT_UNIQUE = 2067;
+  public static final int SQLITE_CONSTRAINT_VTAB = 2323;
+  public static final int SQLITE_CONSTRAINT_ROWID = 2579;
+  public static final int SQLITE_CONSTRAINT_PINNED = 2835;
+  public static final int SQLITE_CONSTRAINT_DATATYPE = 3091;
+  public static final int SQLITE_NOTICE_RECOVER_WAL = 283;
+  public static final int SQLITE_NOTICE_RECOVER_ROLLBACK = 539;
+  public static final int SQLITE_WARNING_AUTOINDEX = 284;
+  public static final int SQLITE_AUTH_USER = 279;
+  public static final int SQLITE_OK_LOAD_PERMANENTLY = 256;
+
+  // serialize
+  public static final int SQLITE_SERIALIZE_NOCOPY = 1;
+  public static final int SQLITE_DESERIALIZE_FREEONCLOSE = 1;
+  public static final int SQLITE_DESERIALIZE_READONLY = 4;
+  public static final int SQLITE_DESERIALIZE_RESIZEABLE = 2;
+
+  // session
+  public static final int SQLITE_SESSION_CONFIG_STRMSIZE = 1;
+  public static final int SQLITE_SESSION_OBJCONFIG_SIZE = 1;
+
+  // sqlite3 status
+  public static final int SQLITE_STATUS_MEMORY_USED = 0;
+  public static final int SQLITE_STATUS_PAGECACHE_USED = 1;
+  public static final int SQLITE_STATUS_PAGECACHE_OVERFLOW = 2;
+  public static final int SQLITE_STATUS_MALLOC_SIZE = 5;
+  public static final int SQLITE_STATUS_PARSER_STACK = 6;
+  public static final int SQLITE_STATUS_PAGECACHE_SIZE = 7;
+  public static final int SQLITE_STATUS_MALLOC_COUNT = 9;
+
+  // stmt status
+  public static final int SQLITE_STMTSTATUS_FULLSCAN_STEP = 1;
+  public static final int SQLITE_STMTSTATUS_SORT = 2;
+  public static final int SQLITE_STMTSTATUS_AUTOINDEX = 3;
+  public static final int SQLITE_STMTSTATUS_VM_STEP = 4;
+  public static final int SQLITE_STMTSTATUS_REPREPARE = 5;
+  public static final int SQLITE_STMTSTATUS_RUN = 6;
+  public static final int SQLITE_STMTSTATUS_FILTER_MISS = 7;
+  public static final int SQLITE_STMTSTATUS_FILTER_HIT = 8;
+  public static final int SQLITE_STMTSTATUS_MEMUSED = 99;
+
+  // sync flags
+  public static final int SQLITE_SYNC_NORMAL = 2;
+  public static final int SQLITE_SYNC_FULL = 3;
+  public static final int SQLITE_SYNC_DATAONLY = 16;
+
+  // tracing flags
+  public static final int SQLITE_TRACE_STMT = 1;
+  public static final int SQLITE_TRACE_PROFILE = 2;
+  public static final int SQLITE_TRACE_ROW = 4;
+  public static final int SQLITE_TRACE_CLOSE = 8;
+
+  // transaction state
+  public static final int SQLITE_TXN_NONE = 0;
+  public static final int SQLITE_TXN_READ = 1;
+  public static final int SQLITE_TXN_WRITE = 2;
+
+  // udf flags
+  public static final int SQLITE_DETERMINISTIC = 0x000000800;
+  public static final int SQLITE_DIRECTONLY    = 0x000080000;
+  public static final int SQLITE_INNOCUOUS     = 0x000200000;
+
+  // virtual tables
+  public static final int SQLITE_INDEX_SCAN_UNIQUE = 1;
+  public static final int SQLITE_INDEX_CONSTRAINT_EQ = 2;
+  public static final int SQLITE_INDEX_CONSTRAINT_GT = 4;
+  public static final int SQLITE_INDEX_CONSTRAINT_LE = 8;
+  public static final int SQLITE_INDEX_CONSTRAINT_LT = 16;
+  public static final int SQLITE_INDEX_CONSTRAINT_GE = 32;
+  public static final int SQLITE_INDEX_CONSTRAINT_MATCH = 64;
+  public static final int SQLITE_INDEX_CONSTRAINT_LIKE = 65;
+  public static final int SQLITE_INDEX_CONSTRAINT_GLOB = 66;
+  public static final int SQLITE_INDEX_CONSTRAINT_REGEXP = 67;
+  public static final int SQLITE_INDEX_CONSTRAINT_NE = 68;
+  public static final int SQLITE_INDEX_CONSTRAINT_ISNOT = 69;
+  public static final int SQLITE_INDEX_CONSTRAINT_ISNOTNULL = 70;
+  public static final int SQLITE_INDEX_CONSTRAINT_ISNULL = 71;
+  public static final int SQLITE_INDEX_CONSTRAINT_IS = 72;
+  public static final int SQLITE_INDEX_CONSTRAINT_LIMIT = 73;
+  public static final int SQLITE_INDEX_CONSTRAINT_OFFSET = 74;
+  public static final int SQLITE_INDEX_CONSTRAINT_FUNCTION = 150;
+  public static final int SQLITE_VTAB_CONSTRAINT_SUPPORT = 1;
+  public static final int SQLITE_VTAB_INNOCUOUS = 2;
+  public static final int SQLITE_VTAB_DIRECTONLY = 3;
+  public static final int SQLITE_VTAB_USES_ALL_SCHEMAS = 4;
+  public static final int SQLITE_ROLLBACK = 1;
+  public static final int SQLITE_FAIL = 3;
+  public static final int SQLITE_REPLACE = 5;
+  static {
+    // This MUST come after the SQLITE_MAX_... values or else
+    // attempting to modify them silently fails.
+    init();
+  }
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/CallbackProxy.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/CallbackProxy.java
new file mode 100644
index 00000000..04957025
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/CallbackProxy.java
@@ -0,0 +1,44 @@
+/*
+** 2023-08-25
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+/**
+   This marker interface exists soley for use as a documentation and
+   class-grouping tool. It should be applied to interfaces or
+   classes which have a call() method implementing some specific
+   callback interface on behalf of the C library.
+
+   Unless very explicitely documented otherwise, callbacks must
+   never throw. Any which do throw but should not might trigger debug
+   output regarding the error, but the exception will not be
+   propagated.  For callback interfaces which support returning error
+   info to the core, the JNI binding will convert any exceptions to
+   C-level error information. For callback interfaces which do not
+   support, all exceptions will necessarily be suppressed in order to
+   retain the C-style no-throw semantics.
+
+   
Callbacks of this style follow a common naming convention:
+
+   
1) They use the UpperCamelCase form of the C function they're
+   proxying for, minus the {@code sqlite3_} prefix, plus a {@code
+   Callback} suffix. e.g. {@code sqlite3_busy_handler()}'s callback is
+   named {@code BusyHandlerCallback}. Exceptions are made where that
+   would potentially be ambiguous, e.g. {@link ConfigSqllogCallback}
+   instead of {@code ConfigCallback} because the {@code
+   sqlite3_config()} interface may need to support more callback types
+   in the future.
+
+   
2) They all have a {@code call()} method but its signature is
+   callback-specific.
+*/
+public interface CallbackProxy {}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/CollationCallback.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/CollationCallback.java
new file mode 100644
index 00000000..ed8bd094
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/CollationCallback.java
@@ -0,0 +1,35 @@
+/*
+** 2023-08-25
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+import org.sqlite.jni.annotation.NotNull;
+
+/**
+   Callback for use with {@link CApi#sqlite3_create_collation}.
+
+   @see AbstractCollationCallback
+*/
+public interface CollationCallback
+  extends CallbackProxy, XDestroyCallback {
+  /**
+     Must compare the given byte arrays and return the result using
+     {@code memcmp()} semantics.
+  */
+  int call(@NotNull byte[] lhs, @NotNull byte[] rhs);
+
+  /**
+     Called by SQLite when the collation is destroyed. If a collation
+     requires custom cleanup, override this method.
+  */
+  void xDestroy();
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/CollationNeededCallback.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/CollationNeededCallback.java
new file mode 100644
index 00000000..fe61fe50
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/CollationNeededCallback.java
@@ -0,0 +1,28 @@
+/*
+** 2023-08-25
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   Callback for use with {@link CApi#sqlite3_collation_needed}.
+*/
+public interface CollationNeededCallback extends CallbackProxy {
+  /**
+     Has the same semantics as the C-level sqlite3_create_collation()
+     callback.
+
+     
If it throws, the exception message is passed on to the db and
+     the exception is suppressed.
+  */
+  int call(sqlite3 db, int eTextRep, String collationName);
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/CommitHookCallback.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/CommitHookCallback.java
new file mode 100644
index 00000000..24373bdf
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/CommitHookCallback.java
@@ -0,0 +1,25 @@
+/*
+** 2023-08-25
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   Callback for use with {@link CApi#sqlite3_commit_hook}.
+*/
+public interface CommitHookCallback extends CallbackProxy {
+  /**
+     Works as documented for the C-level sqlite3_commit_hook()
+     callback.  Must not throw.
+  */
+  int call();
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/ConfigLogCallback.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/ConfigLogCallback.java
new file mode 100644
index 00000000..6513b073
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/ConfigLogCallback.java
@@ -0,0 +1,25 @@
+/*
+** 2023-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.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   A callback for use with sqlite3_config().
+*/
+public interface ConfigLogCallback {
+  /**
+     Must function as described for a C-level callback for
+     {@link CApi#sqlite3_config(ConfigLogCallback)}, with the slight signature change.
+  */
+  void call(int errCode, String msg);
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/ConfigSqllogCallback.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/ConfigSqllogCallback.java
new file mode 100644
index 00000000..df753e65
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/ConfigSqllogCallback.java
@@ -0,0 +1,25 @@
+/*
+** 2023-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.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   A callback for use with sqlite3_config().
+*/
+public interface ConfigSqllogCallback {
+  /**
+     Must function as described for a C-level callback for
+     {@link CApi#sqlite3_config(ConfigSqllogCallback)}, with the slight signature change.
+  */
+  void call(sqlite3 db, String msg, int msgType );
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/NativePointerHolder.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/NativePointerHolder.java
new file mode 100644
index 00000000..e82909e4
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/NativePointerHolder.java
@@ -0,0 +1,46 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   A helper for passing pointers between JNI C code and Java, in
+   particular for output pointers of high-level object types in the
+   sqlite3 C API, e.g. (sqlite3**) and (sqlite3_stmt**).  This is
+   intended to be subclassed and the ContextType is intended to be the
+   class which is doing the subclassing. The intent of the ContextType
+   is strictly to provide some level of type safety by avoiding that
+   NativePointerHolder is not inadvertently passed to an incompatible
+   function signature.
+
+   These objects do not own the pointer they refer to.  They are
+   intended simply to communicate that pointer between C and Java.
+*/
+public class NativePointerHolder {
+  //! Only set from JNI, where access permissions don't matter.
+  private volatile long nativePointer = 0;
+  /**
+     For use ONLY by package-level APIs which act as proxies for
+     close/finalize operations. Such ops must call this to zero out
+     the pointer so that this object is not carrying a stale
+     pointer. This function returns the prior value of the pointer and
+     sets it to 0.
+  */
+  final long clearNativePointer() {
+    final long rv = nativePointer;
+    nativePointer= 0;
+    return rv;
+  }
+
+  public final long getNativePointer(){ return nativePointer; }
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/OutputPointer.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/OutputPointer.java
new file mode 100644
index 00000000..60b90253
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/OutputPointer.java
@@ -0,0 +1,231 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   Helper classes for handling JNI output pointers.
+
+   We do not use a generic OutputPointer because working with those
+   from the native JNI code is unduly quirky due to a lack of
+   autoboxing at that level.
+
+   The usage is similar for all of thes types:
+
+   
{@code
+   OutputPointer.sqlite3 out = new OutputPointer.sqlite3();
+   assert( null==out.get() );
+   int rc = sqlite3_open(":memory:", out);
+   if( 0!=rc ) ... error;
+   assert( null!=out.get() );
+   sqlite3 db = out.take();
+   assert( null==out.get() );
+   }
+
+   With the minor exception that the primitive types permit direct
+   access to the object's value via the `value` property, whereas the
+   JNI-level opaque types do not permit client-level code to set that
+   property.
+
+   
Warning: do not share instances of these classes across
+   threads. Doing so may lead to corrupting sqlite3-internal state.
+*/
+public final class OutputPointer {
+
+  /**
+     Output pointer for use with routines, such as sqlite3_open(),
+     which return a database handle via an output pointer. These
+     pointers can only be set by the JNI layer, not by client-level
+     code.
+  */
+  public static final class sqlite3 {
+    private org.sqlite.jni.capi.sqlite3 value;
+    /** Initializes with a null value. */
+    public sqlite3(){value = null;}
+    /** Sets the current value to null. */
+    public void clear(){value = null;}
+    /** Returns the current value. */
+    public final org.sqlite.jni.capi.sqlite3 get(){return value;}
+    /** Equivalent to calling get() then clear(). */
+    public final org.sqlite.jni.capi.sqlite3 take(){
+      final org.sqlite.jni.capi.sqlite3 v = value;
+      value = null;
+      return v;
+    }
+  }
+
+  /**
+     Output pointer for sqlite3_blob_open(). These
+     pointers can only be set by the JNI layer, not by client-level
+     code.
+  */
+  public static final class sqlite3_blob {
+    private org.sqlite.jni.capi.sqlite3_blob value;
+    /** Initializes with a null value. */
+    public sqlite3_blob(){value = null;}
+    /** Sets the current value to null. */
+    public void clear(){value = null;}
+    /** Returns the current value. */
+    public final org.sqlite.jni.capi.sqlite3_blob get(){return value;}
+    /** Equivalent to calling get() then clear(). */
+    public final org.sqlite.jni.capi.sqlite3_blob take(){
+      final org.sqlite.jni.capi.sqlite3_blob v = value;
+      value = null;
+      return v;
+    }
+  }
+
+  /**
+     Output pointer for use with routines, such as sqlite3_prepare(),
+     which return a statement handle via an output pointer. These
+     pointers can only be set by the JNI layer, not by client-level
+     code.
+  */
+  public static final class sqlite3_stmt {
+    private org.sqlite.jni.capi.sqlite3_stmt value;
+    /** Initializes with a null value. */
+    public sqlite3_stmt(){value = null;}
+    /** Sets the current value to null. */
+    public void clear(){value = null;}
+    /** Returns the current value. */
+    public final org.sqlite.jni.capi.sqlite3_stmt get(){return value;}
+    /** Equivalent to calling get() then clear(). */
+    public final org.sqlite.jni.capi.sqlite3_stmt take(){
+      final org.sqlite.jni.capi.sqlite3_stmt v = value;
+      value = null;
+      return v;
+    }
+  }
+
+  /**
+     Output pointer for use with routines, such as sqlite3_prepupdate_new(),
+     which return a sqlite3_value handle via an output pointer. These
+     pointers can only be set by the JNI layer, not by client-level
+     code.
+  */
+  public static final class sqlite3_value {
+    private org.sqlite.jni.capi.sqlite3_value value;
+    /** Initializes with a null value. */
+    public sqlite3_value(){value = null;}
+    /** Sets the current value to null. */
+    public void clear(){value = null;}
+    /** Returns the current value. */
+    public final org.sqlite.jni.capi.sqlite3_value get(){return value;}
+    /** Equivalent to calling get() then clear(). */
+    public final org.sqlite.jni.capi.sqlite3_value take(){
+      final org.sqlite.jni.capi.sqlite3_value v = value;
+      value = null;
+      return v;
+    }
+  }
+
+  /**
+     Output pointer for use with native routines which return booleans
+     via integer output pointers.
+  */
+  public static final class Bool {
+    /**
+       This is public for ease of use. Accessors are provided for
+       consistency with the higher-level types.
+    */
+    public boolean value;
+    /** Initializes with the value 0. */
+    public Bool(){this(false);}
+    /** Initializes with the value v. */
+    public Bool(boolean v){value = v;}
+    /** Returns the current value. */
+    public final boolean get(){return value;}
+    /** Sets the current value to v. */
+    public final void set(boolean v){value = v;}
+  }
+
+  /**
+     Output pointer for use with native routines which return integers via
+     output pointers.
+  */
+  public static final class Int32 {
+    /**
+       This is public for ease of use. Accessors are provided for
+       consistency with the higher-level types.
+    */
+    public int value;
+    /** Initializes with the value 0. */
+    public Int32(){this(0);}
+    /** Initializes with the value v. */
+    public Int32(int v){value = v;}
+    /** Returns the current value. */
+    public final int get(){return value;}
+    /** Sets the current value to v. */
+    public final void set(int v){value = v;}
+  }
+
+  /**
+     Output pointer for use with native routines which return 64-bit integers
+     via output pointers.
+  */
+  public static final class Int64 {
+    /**
+       This is public for ease of use. Accessors are provided for
+       consistency with the higher-level types.
+    */
+    public long value;
+    /** Initializes with the value 0. */
+    public Int64(){this(0);}
+    /** Initializes with the value v. */
+    public Int64(long v){value = v;}
+    /** Returns the current value. */
+    public final long get(){return value;}
+    /** Sets the current value. */
+    public final void set(long v){value = v;}
+  }
+
+  /**
+     Output pointer for use with native routines which return strings via
+     output pointers.
+  */
+  public static final class String {
+    /**
+       This is public for ease of use. Accessors are provided for
+       consistency with the higher-level types.
+    */
+    public java.lang.String value;
+    /** Initializes with a null value. */
+    public String(){this(null);}
+    /** Initializes with the value v. */
+    public String(java.lang.String v){value = v;}
+    /** Returns the current value. */
+    public final java.lang.String get(){return value;}
+    /** Sets the current value. */
+    public final void set(java.lang.String v){value = v;}
+  }
+
+  /**
+     Output pointer for use with native routines which return byte
+     arrays via output pointers.
+  */
+  public static final class ByteArray {
+    /**
+       This is public for ease of use. Accessors are provided for
+       consistency with the higher-level types.
+    */
+    public byte[] value;
+    /** Initializes with the value null. */
+    public ByteArray(){this(null);}
+    /** Initializes with the value v. */
+    public ByteArray(byte[] v){value = v;}
+    /** Returns the current value. */
+    public final byte[] get(){return value;}
+    /** Sets the current value. */
+    public final void set(byte[] v){value = v;}
+  }
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/PrepareMultiCallback.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/PrepareMultiCallback.java
new file mode 100644
index 00000000..1c805a9b
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/PrepareMultiCallback.java
@@ -0,0 +1,78 @@
+/*
+** 2023-09-13
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   Callback for use with {@link CApi#sqlite3_prepare_multi}.
+*/
+public interface PrepareMultiCallback extends CallbackProxy {
+
+  /**
+     Gets passed a sqlite3_stmt which it may handle in arbitrary ways,
+     transfering ownership of it to this function.
+
+     sqlite3_prepare_multi() will _not_ finalize st - it is up
+     to the call() implementation how st is handled.
+
+     Must return 0 on success or an SQLITE_... code on error.
+
+     See the {@link Finalize} class for a wrapper which finalizes the
+     statement after calling a proxy PrepareMultiCallback.
+  */
+  int call(sqlite3_stmt st);
+
+  /**
+     A PrepareMultiCallback impl which wraps a separate impl and finalizes
+     any sqlite3_stmt passed to its callback.
+  */
+  public static final class Finalize implements PrepareMultiCallback {
+    private PrepareMultiCallback p;
+    /**
+       p is the proxy to call() when this.call() is called.
+    */
+    public Finalize( PrepareMultiCallback p ){
+      this.p = p;
+    }
+    /**
+       Calls the call() method of the proxied callback and either returns its
+       result or propagates an exception. Either way, it passes its argument to
+       sqlite3_finalize() before returning.
+    */
+    @Override public int call(sqlite3_stmt st){
+      try {
+        return this.p.call(st);
+      }finally{
+        CApi.sqlite3_finalize(st);
+      }
+    }
+  }
+
+  /**
+     A PrepareMultiCallback impl which steps entirely through a result set,
+     ignoring all non-error results.
+  */
+  public static final class StepAll implements PrepareMultiCallback {
+    public StepAll(){}
+    /**
+       Calls sqlite3_step() on st until it returns something other than
+       SQLITE_ROW. If the final result is SQLITE_DONE then 0 is returned,
+       else the result of the final step is returned.
+    */
+    @Override public int call(sqlite3_stmt st){
+      int rc = CApi.SQLITE_DONE;
+      while( CApi.SQLITE_ROW == (rc = CApi.sqlite3_step(st)) ){}
+      return CApi.SQLITE_DONE==rc ? 0 : rc;
+    }
+  }
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/PreupdateHookCallback.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/PreupdateHookCallback.java
new file mode 100644
index 00000000..99d3fb03
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/PreupdateHookCallback.java
@@ -0,0 +1,26 @@
+/*
+** 2023-08-25
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   Callback for use with {@link CApi#sqlite3_preupdate_hook}.
+*/
+public interface PreupdateHookCallback extends CallbackProxy {
+  /**
+     Must function as described for the C-level sqlite3_preupdate_hook()
+     callback.
+  */
+  void call(sqlite3 db, int op, String dbName, String dbTable,
+            long iKey1, long iKey2 );
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/ProgressHandlerCallback.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/ProgressHandlerCallback.java
new file mode 100644
index 00000000..464baa2e
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/ProgressHandlerCallback.java
@@ -0,0 +1,27 @@
+/*
+** 2023-08-25
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   Callback for use with {@link CApi#sqlite3_progress_handler}.
+*/
+public interface ProgressHandlerCallback extends CallbackProxy {
+  /**
+     Works as documented for the C-level sqlite3_progress_handler() callback.
+
+     
If it throws, the exception message is passed on to the db and
+     the exception is suppressed.
+  */
+  int call();
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/ResultCode.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/ResultCode.java
new file mode 100644
index 00000000..5a8b2e6a
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/ResultCode.java
@@ -0,0 +1,155 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   This enum contains all of the core and "extended" result codes used
+   by the sqlite3 library. It is provided not for use with the C-style
+   API (with which it won't work) but for higher-level code which may
+   find it useful to map SQLite result codes to human-readable names.
+*/
+public enum ResultCode {
+  SQLITE_OK(CApi.SQLITE_OK),
+  SQLITE_ERROR(CApi.SQLITE_ERROR),
+  SQLITE_INTERNAL(CApi.SQLITE_INTERNAL),
+  SQLITE_PERM(CApi.SQLITE_PERM),
+  SQLITE_ABORT(CApi.SQLITE_ABORT),
+  SQLITE_BUSY(CApi.SQLITE_BUSY),
+  SQLITE_LOCKED(CApi.SQLITE_LOCKED),
+  SQLITE_NOMEM(CApi.SQLITE_NOMEM),
+  SQLITE_READONLY(CApi.SQLITE_READONLY),
+  SQLITE_INTERRUPT(CApi.SQLITE_INTERRUPT),
+  SQLITE_IOERR(CApi.SQLITE_IOERR),
+  SQLITE_CORRUPT(CApi.SQLITE_CORRUPT),
+  SQLITE_NOTFOUND(CApi.SQLITE_NOTFOUND),
+  SQLITE_FULL(CApi.SQLITE_FULL),
+  SQLITE_CANTOPEN(CApi.SQLITE_CANTOPEN),
+  SQLITE_PROTOCOL(CApi.SQLITE_PROTOCOL),
+  SQLITE_EMPTY(CApi.SQLITE_EMPTY),
+  SQLITE_SCHEMA(CApi.SQLITE_SCHEMA),
+  SQLITE_TOOBIG(CApi.SQLITE_TOOBIG),
+  SQLITE_CONSTRAINT(CApi.SQLITE_CONSTRAINT),
+  SQLITE_MISMATCH(CApi.SQLITE_MISMATCH),
+  SQLITE_MISUSE(CApi.SQLITE_MISUSE),
+  SQLITE_NOLFS(CApi.SQLITE_NOLFS),
+  SQLITE_AUTH(CApi.SQLITE_AUTH),
+  SQLITE_FORMAT(CApi.SQLITE_FORMAT),
+  SQLITE_RANGE(CApi.SQLITE_RANGE),
+  SQLITE_NOTADB(CApi.SQLITE_NOTADB),
+  SQLITE_NOTICE(CApi.SQLITE_NOTICE),
+  SQLITE_WARNING(CApi.SQLITE_WARNING),
+  SQLITE_ROW(CApi.SQLITE_ROW),
+  SQLITE_DONE(CApi.SQLITE_DONE),
+  SQLITE_ERROR_MISSING_COLLSEQ(CApi.SQLITE_ERROR_MISSING_COLLSEQ),
+  SQLITE_ERROR_RETRY(CApi.SQLITE_ERROR_RETRY),
+  SQLITE_ERROR_SNAPSHOT(CApi.SQLITE_ERROR_SNAPSHOT),
+  SQLITE_IOERR_READ(CApi.SQLITE_IOERR_READ),
+  SQLITE_IOERR_SHORT_READ(CApi.SQLITE_IOERR_SHORT_READ),
+  SQLITE_IOERR_WRITE(CApi.SQLITE_IOERR_WRITE),
+  SQLITE_IOERR_FSYNC(CApi.SQLITE_IOERR_FSYNC),
+  SQLITE_IOERR_DIR_FSYNC(CApi.SQLITE_IOERR_DIR_FSYNC),
+  SQLITE_IOERR_TRUNCATE(CApi.SQLITE_IOERR_TRUNCATE),
+  SQLITE_IOERR_FSTAT(CApi.SQLITE_IOERR_FSTAT),
+  SQLITE_IOERR_UNLOCK(CApi.SQLITE_IOERR_UNLOCK),
+  SQLITE_IOERR_RDLOCK(CApi.SQLITE_IOERR_RDLOCK),
+  SQLITE_IOERR_DELETE(CApi.SQLITE_IOERR_DELETE),
+  SQLITE_IOERR_BLOCKED(CApi.SQLITE_IOERR_BLOCKED),
+  SQLITE_IOERR_NOMEM(CApi.SQLITE_IOERR_NOMEM),
+  SQLITE_IOERR_ACCESS(CApi.SQLITE_IOERR_ACCESS),
+  SQLITE_IOERR_CHECKRESERVEDLOCK(CApi.SQLITE_IOERR_CHECKRESERVEDLOCK),
+  SQLITE_IOERR_LOCK(CApi.SQLITE_IOERR_LOCK),
+  SQLITE_IOERR_CLOSE(CApi.SQLITE_IOERR_CLOSE),
+  SQLITE_IOERR_DIR_CLOSE(CApi.SQLITE_IOERR_DIR_CLOSE),
+  SQLITE_IOERR_SHMOPEN(CApi.SQLITE_IOERR_SHMOPEN),
+  SQLITE_IOERR_SHMSIZE(CApi.SQLITE_IOERR_SHMSIZE),
+  SQLITE_IOERR_SHMLOCK(CApi.SQLITE_IOERR_SHMLOCK),
+  SQLITE_IOERR_SHMMAP(CApi.SQLITE_IOERR_SHMMAP),
+  SQLITE_IOERR_SEEK(CApi.SQLITE_IOERR_SEEK),
+  SQLITE_IOERR_DELETE_NOENT(CApi.SQLITE_IOERR_DELETE_NOENT),
+  SQLITE_IOERR_MMAP(CApi.SQLITE_IOERR_MMAP),
+  SQLITE_IOERR_GETTEMPPATH(CApi.SQLITE_IOERR_GETTEMPPATH),
+  SQLITE_IOERR_CONVPATH(CApi.SQLITE_IOERR_CONVPATH),
+  SQLITE_IOERR_VNODE(CApi.SQLITE_IOERR_VNODE),
+  SQLITE_IOERR_AUTH(CApi.SQLITE_IOERR_AUTH),
+  SQLITE_IOERR_BEGIN_ATOMIC(CApi.SQLITE_IOERR_BEGIN_ATOMIC),
+  SQLITE_IOERR_COMMIT_ATOMIC(CApi.SQLITE_IOERR_COMMIT_ATOMIC),
+  SQLITE_IOERR_ROLLBACK_ATOMIC(CApi.SQLITE_IOERR_ROLLBACK_ATOMIC),
+  SQLITE_IOERR_DATA(CApi.SQLITE_IOERR_DATA),
+  SQLITE_IOERR_CORRUPTFS(CApi.SQLITE_IOERR_CORRUPTFS),
+  SQLITE_LOCKED_SHAREDCACHE(CApi.SQLITE_LOCKED_SHAREDCACHE),
+  SQLITE_LOCKED_VTAB(CApi.SQLITE_LOCKED_VTAB),
+  SQLITE_BUSY_RECOVERY(CApi.SQLITE_BUSY_RECOVERY),
+  SQLITE_BUSY_SNAPSHOT(CApi.SQLITE_BUSY_SNAPSHOT),
+  SQLITE_BUSY_TIMEOUT(CApi.SQLITE_BUSY_TIMEOUT),
+  SQLITE_CANTOPEN_NOTEMPDIR(CApi.SQLITE_CANTOPEN_NOTEMPDIR),
+  SQLITE_CANTOPEN_ISDIR(CApi.SQLITE_CANTOPEN_ISDIR),
+  SQLITE_CANTOPEN_FULLPATH(CApi.SQLITE_CANTOPEN_FULLPATH),
+  SQLITE_CANTOPEN_CONVPATH(CApi.SQLITE_CANTOPEN_CONVPATH),
+  SQLITE_CANTOPEN_SYMLINK(CApi.SQLITE_CANTOPEN_SYMLINK),
+  SQLITE_CORRUPT_VTAB(CApi.SQLITE_CORRUPT_VTAB),
+  SQLITE_CORRUPT_SEQUENCE(CApi.SQLITE_CORRUPT_SEQUENCE),
+  SQLITE_CORRUPT_INDEX(CApi.SQLITE_CORRUPT_INDEX),
+  SQLITE_READONLY_RECOVERY(CApi.SQLITE_READONLY_RECOVERY),
+  SQLITE_READONLY_CANTLOCK(CApi.SQLITE_READONLY_CANTLOCK),
+  SQLITE_READONLY_ROLLBACK(CApi.SQLITE_READONLY_ROLLBACK),
+  SQLITE_READONLY_DBMOVED(CApi.SQLITE_READONLY_DBMOVED),
+  SQLITE_READONLY_CANTINIT(CApi.SQLITE_READONLY_CANTINIT),
+  SQLITE_READONLY_DIRECTORY(CApi.SQLITE_READONLY_DIRECTORY),
+  SQLITE_ABORT_ROLLBACK(CApi.SQLITE_ABORT_ROLLBACK),
+  SQLITE_CONSTRAINT_CHECK(CApi.SQLITE_CONSTRAINT_CHECK),
+  SQLITE_CONSTRAINT_COMMITHOOK(CApi.SQLITE_CONSTRAINT_COMMITHOOK),
+  SQLITE_CONSTRAINT_FOREIGNKEY(CApi.SQLITE_CONSTRAINT_FOREIGNKEY),
+  SQLITE_CONSTRAINT_FUNCTION(CApi.SQLITE_CONSTRAINT_FUNCTION),
+  SQLITE_CONSTRAINT_NOTNULL(CApi.SQLITE_CONSTRAINT_NOTNULL),
+  SQLITE_CONSTRAINT_PRIMARYKEY(CApi.SQLITE_CONSTRAINT_PRIMARYKEY),
+  SQLITE_CONSTRAINT_TRIGGER(CApi.SQLITE_CONSTRAINT_TRIGGER),
+  SQLITE_CONSTRAINT_UNIQUE(CApi.SQLITE_CONSTRAINT_UNIQUE),
+  SQLITE_CONSTRAINT_VTAB(CApi.SQLITE_CONSTRAINT_VTAB),
+  SQLITE_CONSTRAINT_ROWID(CApi.SQLITE_CONSTRAINT_ROWID),
+  SQLITE_CONSTRAINT_PINNED(CApi.SQLITE_CONSTRAINT_PINNED),
+  SQLITE_CONSTRAINT_DATATYPE(CApi.SQLITE_CONSTRAINT_DATATYPE),
+  SQLITE_NOTICE_RECOVER_WAL(CApi.SQLITE_NOTICE_RECOVER_WAL),
+  SQLITE_NOTICE_RECOVER_ROLLBACK(CApi.SQLITE_NOTICE_RECOVER_ROLLBACK),
+  SQLITE_WARNING_AUTOINDEX(CApi.SQLITE_WARNING_AUTOINDEX),
+  SQLITE_AUTH_USER(CApi.SQLITE_AUTH_USER),
+  SQLITE_OK_LOAD_PERMANENTLY(CApi.SQLITE_OK_LOAD_PERMANENTLY);
+
+  public final int value;
+
+  ResultCode(int rc){
+    value = rc;
+    ResultCodeMap.set(rc, this);
+  }
+
+  /**
+     Returns the entry from this enum for the given result code, or
+     null if no match is found.
+  */
+  public static ResultCode getEntryForInt(int rc){
+    return ResultCodeMap.get(rc);
+  }
+
+  /**
+     Internal level of indirection required because we cannot initialize
+     static enum members in an enum before the enum constructor is
+     invoked.
+  */
+  private static final class ResultCodeMap {
+    private static final java.util.Map i2e
+      = new java.util.HashMap<>();
+    private static void set(int rc, ResultCode e){ i2e.put(rc, e); }
+    private static ResultCode get(int rc){ return i2e.get(rc); }
+  }
+
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/RollbackHookCallback.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/RollbackHookCallback.java
new file mode 100644
index 00000000..5ce17e71
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/RollbackHookCallback.java
@@ -0,0 +1,25 @@
+/*
+** 2023-08-25
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   Callback for use with {@link CApi#sqlite3_rollback_hook}.
+*/
+public interface RollbackHookCallback extends CallbackProxy {
+  /**
+     Works as documented for the C-level sqlite3_rollback_hook()
+     callback.
+  */
+  void call();
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/SQLFunction.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/SQLFunction.java
new file mode 100644
index 00000000..4806e2fc
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/SQLFunction.java
@@ -0,0 +1,103 @@
+/*
+** 2023-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 part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   SQLFunction is used in conjunction with the
+   sqlite3_create_function() JNI-bound API to give that native code
+   access to the callback functions needed in order to implement SQL
+   functions in Java.
+
+   
+
+   This class is not used by itself, but is a marker base class. The
+   three UDF types are modelled by the inner classes Scalar,
+   Aggregate, and Window. Most simply, clients may subclass
+   those, or create anonymous classes from them, to implement
+   UDFs. Clients are free to create their own classes for use with
+   UDFs, so long as they conform to the public interfaces defined by
+   those three classes. The JNI layer only actively relies on the
+   SQLFunction base class and the method names and signatures used by
+   the UDF callback interfaces.
+*/
+public interface SQLFunction {
+
+  /**
+     PerContextState assists aggregate and window functions in
+     managing their accumulator state across calls to the UDF's
+     callbacks.
+
+     T must be of a type which can be legally stored as a value in
+     java.util.HashMap.
+
+     If a given aggregate or window function is called multiple times
+     in a single SQL statement, e.g. SELECT MYFUNC(A), MYFUNC(B)...,
+     then the clients need some way of knowing which call is which so
+     that they can map their state between their various UDF callbacks
+     and reset it via xFinal(). This class takes care of such
+     mappings.
+
+     
This class works by mapping
+     sqlite3_context.getAggregateContext() to a single piece of
+     state, of a client-defined type (the T part of this class), which
+     persists across a "matching set" of the UDF's callbacks.
+
+     
This class is a helper providing commonly-needed functionality
+     - it is not required for use with aggregate or window functions.
+     Client UDFs are free to perform such mappings using custom
+     approaches. The provided {@link AggregateFunction} and {@link
+     WindowFunction} classes use this.
+  */
+  public static final class PerContextState {
+    private final java.util.Map> map
+      = new java.util.HashMap<>();
+
+    /**
+       Should be called from a UDF's xStep(), xValue(), and xInverse()
+       methods, passing it that method's first argument and an initial
+       value for the persistent state. If there is currently no
+       mapping for the given context within the map, one is created
+       using the given initial value, else the existing one is used
+       and the 2nd argument is ignored.  It returns a ValueHolder
+       which can be used to modify that state directly without
+       requiring that the client update the underlying map's entry.
+
+       The caller is obligated to eventually call
+       takeAggregateState() to clear the mapping.
+    */
+    public ValueHolder getAggregateState(sqlite3_context cx, T initialValue){
+      final Long key = cx.getAggregateContext(true);
+      ValueHolder rc = null==key ? null : map.get(key);
+      if( null==rc ){
+        map.put(key, rc = new ValueHolder<>(initialValue));
+      }
+      return rc;
+    }
+
+    /**
+       Should be called from a UDF's xFinal() method and passed that
+       method's first argument. This function removes the value
+       associated with cx.getAggregateContext() from the map and
+       returns it, returning null if no other UDF method has been
+       called to set up such a mapping. The latter condition will be
+       the case if a UDF is used in a statement which has no result
+       rows.
+    */
+    public T takeAggregateState(sqlite3_context cx){
+      final ValueHolder h = map.remove(cx.getAggregateContext(false));
+      return null==h ? null : h.value;
+    }
+  }
+
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/SQLTester.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/SQLTester.java
new file mode 100644
index 00000000..81d6106b
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/SQLTester.java
@@ -0,0 +1,1433 @@
+/*
+** 2023-08-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.
+**
+*************************************************************************
+** This file contains the main application entry pointer for the
+** SQLTester framework.
+*/
+package org.sqlite.jni.capi;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.*;
+import static org.sqlite.jni.capi.CApi.*;
+
+/**
+   Modes for how to escape (or not) column values and names from
+   SQLTester.execSql() to the result buffer output.
+*/
+enum ResultBufferMode {
+  //! Do not append to result buffer
+  NONE,
+  //! Append output escaped.
+  ESCAPED,
+  //! Append output as-is
+  ASIS
+};
+
+/**
+   Modes to specify how to emit multi-row output from
+   SQLTester.execSql() to the result buffer.
+*/
+enum ResultRowMode {
+  //! Keep all result rows on one line, space-separated.
+  ONELINE,
+  //! Add a newline between each result row.
+  NEWLINE
+};
+
+/**
+   Base exception type for test-related failures.
+*/
+class SQLTesterException extends RuntimeException {
+  private boolean bFatal = false;
+
+  SQLTesterException(String msg){
+    super(msg);
+  }
+
+  protected SQLTesterException(String msg, boolean fatal){
+    super(msg);
+    bFatal = fatal;
+  }
+
+  /**
+     Indicates whether the framework should consider this exception
+     type as immediately fatal to the test run or not.
+  */
+  final boolean isFatal(){ return bFatal; }
+}
+
+class DbException extends SQLTesterException {
+  DbException(sqlite3 db, int rc, boolean closeDb){
+    super("DB error #"+rc+": "+sqlite3_errmsg(db),true);
+    if( closeDb ) sqlite3_close_v2(db);
+  }
+  DbException(sqlite3 db, int rc){
+    this(db, rc, false);
+  }
+}
+
+/**
+   Generic test-failed exception.
+ */
+class TestScriptFailed extends SQLTesterException {
+  public TestScriptFailed(TestScript ts, String msg){
+    super(ts.getOutputPrefix()+": "+msg, true);
+  }
+}
+
+/**
+   Thrown when an unknown test command is encountered in a script.
+*/
+class UnknownCommand extends SQLTesterException {
+  public UnknownCommand(TestScript ts, String cmd){
+    super(ts.getOutputPrefix()+": unknown command: "+cmd, false);
+  }
+}
+
+/**
+   Thrown when an "incompatible directive" is found in a script.  This
+   can be the presence of a C-preprocessor construct, specific
+   metadata tags within a test script's header, or specific test
+   constructs which are incompatible with this particular
+   implementation.
+*/
+class IncompatibleDirective extends SQLTesterException {
+  public IncompatibleDirective(TestScript ts, String line){
+    super(ts.getOutputPrefix()+": incompatible directive: "+line, false);
+  }
+}
+
+/**
+   Console output utility class.
+*/
+class Outer {
+  private int verbosity = 0;
+
+  static void out(Object val){
+    System.out.print(val);
+  }
+
+  Outer out(Object... vals){
+    for(Object v : vals) out(v);
+    return this;
+  }
+
+  Outer outln(Object... vals){
+    out(vals).out("\n");
+    return this;
+  }
+
+  Outer verbose(Object... vals){
+    if(verbosity>0){
+      out("VERBOSE",(verbosity>1 ? "+: " : ": ")).outln(vals);
+    }
+    return this;
+  }
+
+  void setVerbosity(int level){
+    verbosity = level;
+  }
+
+  int getVerbosity(){
+    return verbosity;
+  }
+
+  public boolean isVerbose(){return verbosity > 0;}
+
+}
+
+/**
+   This class provides an application which aims to implement the
+   rudimentary SQL-driven test tool described in the accompanying
+   {@code test-script-interpreter.md}.
+
+   
This class is an internal testing tool, not part of the public
+   interface but is (A) in the same package as the library because
+   access permissions require it to be so and (B) the JDK8 javadoc
+   offers no way to filter individual classes out of the doc
+   generation process (it can only exclude packages, but see (A)).
+
+   
An instance of this application provides a core set of services
+   which TestScript instances use for processing testing logic.
+   TestScripts, in turn, delegate the concrete test work to Command
+   objects, which the TestScript parses on their behalf.
+*/
+public class SQLTester {
+  //! List of input script files.
+  private final java.util.List listInFiles = new ArrayList<>();
+  //! Console output utility.
+  private final Outer outer = new Outer();
+  //! Test input buffer.
+  private final StringBuilder inputBuffer = new StringBuilder();
+  //! Test result buffer.
+  private final StringBuilder resultBuffer = new StringBuilder();
+  //! Buffer for REQUIRED_PROPERTIES pragmas.
+  private final StringBuilder dbInitSql = new StringBuilder();
+  //! Output representation of SQL NULL.
+  private String nullView = "nil";
+  //! Total tests run.
+  private int nTotalTest = 0;
+  //! Total test script files run.
+  private int nTestFile = 0;
+  //! Number of scripts which were aborted.
+  private int nAbortedScript = 0;
+  //! Incremented by test case handlers
+  private int nTest = 0;
+  //! True to enable column name output from execSql()
+  private boolean emitColNames;
+  //! True to keep going regardless of how a test fails.
+  private boolean keepGoing = false;
+  //! The list of available db handles.
+  private final sqlite3[] aDb = new sqlite3[7];
+  //! Index into aDb of the current db.
+  private int iCurrentDb = 0;
+  //! Name of the default db, re-created for each script.
+  private final String initialDbName = "test.db";
+
+
+  public SQLTester(){
+    reset();
+  }
+
+  void setVerbosity(int level){
+    this.outer.setVerbosity( level );
+  }
+  int getVerbosity(){
+    return this.outer.getVerbosity();
+  }
+  boolean isVerbose(){
+    return this.outer.isVerbose();
+  }
+
+  void outputColumnNames(boolean b){ emitColNames = b; }
+
+  void verbose(Object... vals){
+    outer.verbose(vals);
+  }
+
+  void outln(Object... vals){
+    outer.outln(vals);
+  }
+
+  void out(Object... vals){
+    outer.out(vals);
+  }
+
+  //! Adds the given test script to the to-test list.
+  public void addTestScript(String filename){
+    listInFiles.add(filename);
+    //verbose("Added file ",filename);
+  }
+
+  private void setupInitialDb() throws DbException {
+    if( null==aDb[0] ){
+      Util.unlink(initialDbName);
+      openDb(0, initialDbName, true);
+    }else{
+      outln("WARNING: setupInitialDb() unexpectedly ",
+            "triggered while it is opened.");
+    }
+  }
+
+  static final String[] startEmoji = {
+    "🚴", "🏄", "🏇", "🤸", "⛹", "🏊", "⛷", "🧗", "🏋"
+  };
+  static final int nStartEmoji = startEmoji.length;
+  static int iStartEmoji = 0;
+
+  private static String nextStartEmoji(){
+    return startEmoji[iStartEmoji++ % nStartEmoji];
+  }
+
+  public void runTests() throws Exception {
+    final long tStart = System.currentTimeMillis();
+    for(String f : listInFiles){
+      reset();
+      ++nTestFile;
+      final TestScript ts = new TestScript(f);
+      outln(nextStartEmoji(), " starting [",f,"]");
+      boolean threw = false;
+      final long timeStart = System.currentTimeMillis();
+      try{
+        ts.run(this);
+      }catch(SQLTesterException e){
+        threw = true;
+        outln("🔥EXCEPTION: ",e.getClass().getSimpleName(),": ",e.getMessage());
+        ++nAbortedScript;
+        if( keepGoing ) outln("Continuing anyway becaure of the keep-going option.");
+        else if( e.isFatal() ) throw e;
+      }finally{
+        final long timeEnd = System.currentTimeMillis();
+        outln("🏁",(threw ? "❌" : "✅")," ",nTest," test(s) in ",
+              (timeEnd-timeStart),"ms.");
+      }
+    }
+    final long tEnd = System.currentTimeMillis();
+    outln("Total run-time: ",(tEnd-tStart),"ms");
+    Util.unlink(initialDbName);
+  }
+
+  private StringBuilder clearBuffer(StringBuilder b){
+    b.setLength(0);;
+    return b;
+  }
+
+  StringBuilder clearInputBuffer(){
+    return clearBuffer(inputBuffer);
+  }
+
+  StringBuilder clearResultBuffer(){
+    return clearBuffer(resultBuffer);
+  }
+
+  StringBuilder getInputBuffer(){ return inputBuffer; }
+
+  void appendInput(String n, boolean addNL){
+    inputBuffer.append(n);
+    if(addNL) inputBuffer.append('\n');
+  }
+
+  void appendResult(String n, boolean addNL){
+    resultBuffer.append(n);
+    if(addNL) resultBuffer.append('\n');
+  }
+
+  void appendDbInitSql(String n) throws DbException {
+    dbInitSql.append(n).append('\n');
+    if( null!=getCurrentDb() ){
+      //outln("RUNNING DB INIT CODE: ",n);
+      execSql(null, true, ResultBufferMode.NONE, null, n);
+    }
+  }
+  String getDbInitSql(){ return dbInitSql.toString(); }
+
+  String getInputText(){ return inputBuffer.toString(); }
+
+  String getResultText(){ return resultBuffer.toString(); }
+
+  private String takeBuffer(StringBuilder b){
+    final String rc = b.toString();
+    clearBuffer(b);
+    return rc;
+  }
+
+  String takeInputBuffer(){ return takeBuffer(inputBuffer); }
+
+  String takeResultBuffer(){ return takeBuffer(resultBuffer); }
+
+  int getCurrentDbId(){ return iCurrentDb; }
+
+  SQLTester affirmDbId(int n) throws IndexOutOfBoundsException {
+    if(n<0 || n>=aDb.length){
+      throw new IndexOutOfBoundsException("illegal db number: "+n);
+    }
+    return this;
+  }
+
+  sqlite3 setCurrentDb(int n) throws Exception{
+    affirmDbId(n);
+    iCurrentDb = n;
+    return this.aDb[n];
+  }
+
+  sqlite3 getCurrentDb(){ return aDb[iCurrentDb]; }
+
+  sqlite3 getDbById(int id) throws Exception{
+    return affirmDbId(id).aDb[id];
+  }
+
+  void closeDb(int id) {
+    final sqlite3 db = affirmDbId(id).aDb[id];
+    if( null != db ){
+      sqlite3_close_v2(db);
+      aDb[id] = null;
+    }
+  }
+
+  void closeDb() { closeDb(iCurrentDb); }
+
+  void closeAllDbs(){
+    for(int i = 0; i 0){
+      //outln("RUNNING DB INIT CODE: ",dbInitSql.toString());
+      rc = execSql(db, false, ResultBufferMode.NONE,
+                   null, dbInitSql.toString());
+    }
+    if( 0!=rc ){
+      throw new DbException(db, rc, true);
+    }
+    return aDb[iCurrentDb] = db;
+  }
+
+  sqlite3 openDb(int slot, String name, boolean createIfNeeded) throws DbException {
+    affirmDbId(slot);
+    iCurrentDb = slot;
+    return openDb(name, createIfNeeded);
+  }
+
+  /**
+     Resets all tester context state except for that related to
+     tracking running totals.
+  */
+  void reset(){
+    clearInputBuffer();
+    clearResultBuffer();
+    clearBuffer(dbInitSql);
+    closeAllDbs();
+    nTest = 0;
+    nullView = "nil";
+    emitColNames = false;
+    iCurrentDb = 0;
+    //dbInitSql.append("SELECT 1;");
+  }
+
+  void setNullValue(String v){nullView = v;}
+
+  /**
+     If true, encountering an unknown command in a script causes the
+     remainder of the script to be skipped, rather than aborting the
+     whole script run.
+  */
+  boolean skipUnknownCommands(){
+    // Currently hard-coded. Potentially a flag someday.
+    return true;
+  }
+
+  void incrementTestCounter(){ ++nTest; ++nTotalTest; }
+
+  //! "Special" characters - we have to escape output if it contains any.
+  static final Pattern patternSpecial = Pattern.compile(
+    "[\\x00-\\x20\\x22\\x5c\\x7b\\x7d]"
+  );
+  //! Either of '{' or '}'.
+  static final Pattern patternSquiggly = Pattern.compile("[{}]");
+
+  /**
+     Returns v or some escaped form of v, as defined in the tester's
+     spec doc.
+  */
+  String escapeSqlValue(String v){
+    if( "".equals(v) ) return "{}";
+    Matcher m = patternSpecial.matcher(v);
+    if( !m.find() ){
+      return v  /* no escaping needed */;
+    }
+    m = patternSquiggly.matcher(v);
+    if( !m.find() ){
+      return "{"+v+"}";
+    }
+    final StringBuilder sb = new StringBuilder("\"");
+    final int n = v.length();
+    for(int i = 0; i < n; ++i){
+      final char ch = v.charAt(i);
+      switch(ch){
+        case '\\': sb.append("\\\\"); break;
+        case '"': sb.append("\\\""); break;
+        default:
+          //verbose("CHAR ",(int)ch," ",ch," octal=",String.format("\\%03o", (int)ch));
+          if( (int)ch < 32 ) sb.append(String.format("\\%03o", (int)ch));
+          else sb.append(ch);
+          break;
+      }
+    }
+    sb.append("\"");
+    return sb.toString();
+  }
+
+  private void appendDbErr(sqlite3 db, StringBuilder sb, int rc){
+    sb.append(org.sqlite.jni.capi.ResultCode.getEntryForInt(rc)).append(' ');
+    final String msg = escapeSqlValue(sqlite3_errmsg(db));
+    if( '{' == msg.charAt(0) ){
+      sb.append(msg);
+    }else{
+      sb.append('{').append(msg).append('}');
+    }
+  }
+
+  /**
+     Runs SQL on behalf of test commands and outputs the results following
+     the very specific rules of the test framework.
+
+     If db is null, getCurrentDb() is assumed. If throwOnError is true then
+     any db-side error will result in an exception, else they result in
+     the db's result code.
+
+     appendMode specifies how/whether to append results to the result
+     buffer. rowMode specifies whether to output all results in a
+     single line or one line per row. If appendMode is
+     ResultBufferMode.NONE then rowMode is ignored and may be null.
+  */
+  public int execSql(sqlite3 db, boolean throwOnError,
+                     ResultBufferMode appendMode, ResultRowMode rowMode,
+                     String sql) throws SQLTesterException {
+    if( null==db && null==aDb[0] ){
+      // Delay opening of the initial db to enable tests to change its
+      // name and inject on-connect code via, e.g., the MEMDB
+      // directive.  this setup as the potential to misinteract with
+      // auto-extension timing and must be done carefully.
+      setupInitialDb();
+    }
+    final OutputPointer.Int32 oTail = new OutputPointer.Int32();
+    final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt();
+    final byte[] sqlUtf8 = sql.getBytes(StandardCharsets.UTF_8);
+    if( null==db ) db = getCurrentDb();
+    int pos = 0, n = 1;
+    byte[] sqlChunk = sqlUtf8;
+    int rc = 0;
+    sqlite3_stmt stmt = null;
+    int spacing = 0 /* emit a space for --result if>0 */ ;
+    final StringBuilder sb = (ResultBufferMode.NONE==appendMode)
+      ? null : resultBuffer;
+    //outln("sqlChunk len= = ",sqlChunk.length);
+    try{
+      while(pos < sqlChunk.length){
+        if(pos > 0){
+          sqlChunk = Arrays.copyOfRange(sqlChunk, pos,
+                                        sqlChunk.length);
+        }
+        if( 0==sqlChunk.length ) break;
+        rc = sqlite3_prepare_v2(db, sqlChunk, outStmt, oTail);
+        /*outln("PREPARE rc ",rc," oTail=",oTail.get(),": ",
+          new String(sqlChunk,StandardCharsets.UTF_8),"\n");*/
+        if( 0!=rc ){
+          if(throwOnError){
+            throw new DbException(db, rc);
+          }else if( null!=sb ){
+            appendDbErr(db, sb, rc);
+          }
+          break;
+        }
+        pos = oTail.value;
+        stmt = outStmt.take();
+        if( null == stmt ){
+          // empty statement was parsed.
+          continue;
+        }
+        if( null!=sb ){
+          // Add the output to the result buffer...
+          final int nCol = sqlite3_column_count(stmt);
+          String colName = null, val = null;
+          while( SQLITE_ROW == (rc = sqlite3_step(stmt)) ){
+            for(int i = 0; i < nCol; ++i){
+              if( spacing++ > 0 ) sb.append(' ');
+              if( emitColNames ){
+                colName = sqlite3_column_name(stmt, i);
+                switch(appendMode){
+                  case ASIS:
+                    sb.append( colName );
+                    break;
+                  case ESCAPED:
+                    sb.append( escapeSqlValue(colName) );
+                    break;
+                  default:
+                    throw new SQLTesterException("Unhandled ResultBufferMode: "+appendMode);
+                }
+                sb.append(' ');
+              }
+              val = sqlite3_column_text16(stmt, i);
+              if( null==val ){
+                sb.append( nullView );
+                continue;
+              }
+              switch(appendMode){
+                case ASIS:
+                  sb.append( val );
+                  break;
+                case ESCAPED:
+                  sb.append( escapeSqlValue(val) );
+                  break;
+                default:
+                  throw new SQLTesterException("Unhandled ResultBufferMode: "+appendMode);
+              }
+            }
+            if( ResultRowMode.NEWLINE == rowMode ){
+              spacing = 0;
+              sb.append('\n');
+            }
+          }
+        }else{
+          while( SQLITE_ROW == (rc = sqlite3_step(stmt)) ){}
+        }
+        sqlite3_finalize(stmt);
+        stmt = null;
+        if(SQLITE_ROW==rc || SQLITE_DONE==rc) rc = 0;
+        else if( rc!=0 ){
+          if( null!=sb ){
+            appendDbErr(db, sb, rc);
+          }
+          break;
+        }
+      }
+    }finally{
+      sqlite3_reset(stmt
+        /* In order to trigger an exception in the
+           INSERT...RETURNING locking scenario:
+           https://sqlite.org/forum/forumpost/36f7a2e7494897df */);
+      sqlite3_finalize(stmt);
+    }
+    if( 0!=rc && throwOnError ){
+      throw new DbException(db, rc);
+    }
+    return rc;
+  }
+
+  public static void main(String[] argv) throws Exception{
+    installCustomExtensions();
+    boolean dumpInternals = false;
+    final SQLTester t = new SQLTester();
+    for(String a : argv){
+      if(a.startsWith("-")){
+        final String flag = a.replaceFirst("-+","");
+        if( flag.equals("verbose") ){
+          // Use --verbose up to 3 times
+          t.setVerbosity(t.getVerbosity() + 1);
+        }else if( flag.equals("keep-going") ){
+          t.keepGoing = true;
+        }else if( flag.equals("internals") ){
+          dumpInternals = true;
+        }else{
+          throw new IllegalArgumentException("Unhandled flag: "+flag);
+        }
+        continue;
+      }
+      t.addTestScript(a);
+    }
+    final AutoExtensionCallback ax = new AutoExtensionCallback() {
+        private final SQLTester tester = t;
+        @Override public int call(sqlite3 db){
+          final String init = tester.getDbInitSql();
+          if( !init.isEmpty() ){
+            tester.execSql(db, true, ResultBufferMode.NONE, null, init);
+          }
+          return 0;
+        }
+      };
+    sqlite3_auto_extension(ax);
+    try {
+      t.runTests();
+    }finally{
+      sqlite3_cancel_auto_extension(ax);
+      t.outln("Processed ",t.nTotalTest," test(s) in ",t.nTestFile," file(s).");
+      if( t.nAbortedScript > 0 ){
+        t.outln("Aborted ",t.nAbortedScript," script(s).");
+      }
+      if( dumpInternals ){
+        sqlite3_jni_internal_details();
+      }
+    }
+  }
+
+  /**
+     Internal impl of the public strglob() method. Neither argument
+     may be NULL and both _MUST_ be NUL-terminated.
+  */
+  private static native int strglob(byte[] glob, byte[] txt);
+
+  /**
+     Works essentially the same as sqlite3_strglob() except that the
+     glob character '#' matches a sequence of one or more digits.  It
+     does not match when it appears at the start or middle of a series
+     of digits, e.g. "#23" or "1#3", but will match at the end,
+     e.g. "12#".
+  */
+  static int strglob(String glob, String txt){
+    return strglob(
+      (glob+"\0").getBytes(StandardCharsets.UTF_8),
+      (txt+"\0").getBytes(StandardCharsets.UTF_8)
+    );
+  }
+
+  /**
+     Sets up C-side components needed by the test framework. This must
+     not be called until main() is triggered so that it does not
+     interfere with library clients who don't use this class.
+  */
+  static native void installCustomExtensions();
+  static {
+    System.loadLibrary("sqlite3-jni")
+      /* Interestingly, when SQLTester is the main app, we have to
+         load that lib from here. The same load from CApi does
+         not happen early enough. Without this,
+         installCustomExtensions() is an unresolved symbol. */;
+  }
+
+}
+
+/**
+   General utilities for the SQLTester bits.
+*/
+final class Util {
+
+  //! Throws a new T, appending all msg args into a string for the message.
+  static void toss(Class extends Exception> errorType, Object... msg) throws Exception {
+    StringBuilder sb = new StringBuilder();
+    for(Object s : msg) sb.append(s);
+    final java.lang.reflect.Constructor extends Exception> ctor =
+      errorType.getConstructor(String.class);
+    throw ctor.newInstance(sb.toString());
+  }
+
+  static void toss(Object... msg) throws Exception{
+    toss(RuntimeException.class, msg);
+  }
+
+  //! Tries to delete the given file, silently ignoring failure.
+  static void unlink(String filename){
+    try{
+      final java.io.File f = new java.io.File(filename);
+      f.delete();
+    }catch(Exception e){
+      /* ignore */
+    }
+  }
+
+  /**
+     Appends all entries in argv[1..end] into a space-separated
+     string, argv[0] is not included because it's expected to be a
+     command name.
+  */
+  static String argvToString(String[] argv){
+    StringBuilder sb = new StringBuilder();
+    for(int i = 1; i < argv.length; ++i ){
+      if( i>1 ) sb.append(" ");
+      sb.append( argv[i] );
+    }
+    return sb.toString();
+  }
+
+}
+
+/**
+   Base class for test script commands. It provides a set of utility
+   APIs for concrete command implementations.
+
+   Each subclass must have a public no-arg ctor and must implement
+   the process() method which is abstract in this class.
+
+   Commands are intended to be stateless, except perhaps for counters
+   and similar internals. Specifically, no state which changes the
+   behavior between any two invocations of process() should be
+   retained.
+*/
+abstract class Command {
+  protected Command(){}
+
+  /**
+     Must process one command-unit of work and either return
+     (on success) or throw (on error).
+
+     The first two arguments specify the context of the test. The TestScript
+     provides the content of the test and the SQLTester providers the sandbox
+     in which that script is being evaluated.
+
+     argv is a list with the command name followed by any arguments to
+     that command. The argcCheck() method from this class provides
+     very basic argc validation.
+  */
+  public abstract void process(
+    SQLTester st, TestScript ts, String[] argv
+  ) throws Exception;
+
+  /**
+     If argv.length-1 (-1 because the command's name is in argv[0]) does not
+     fall in the inclusive range (min,max) then this function throws. Use
+     a max value of -1 to mean unlimited.
+  */
+  protected final void argcCheck(TestScript ts, String[] argv, int min, int max) throws Exception{
+    int argc = argv.length-1;
+    if(argc=0 && argc>max)){
+      if( min==max ){
+        ts.toss(argv[0]," requires exactly ",min," argument(s)");
+      }else if(max>0){
+        ts.toss(argv[0]," requires ",min,"-",max," arguments.");
+      }else{
+        ts.toss(argv[0]," requires at least ",min," arguments.");
+      }
+    }
+  }
+
+  /**
+     Equivalent to argcCheck(argv,argc,argc).
+  */
+  protected final void argcCheck(TestScript ts, String[] argv, int argc) throws Exception{
+    argcCheck(ts, argv, argc, argc);
+  }
+}
+
+//! --close command
+class CloseDbCommand extends Command {
+  public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+    argcCheck(ts,argv,0,1);
+    Integer id;
+    if(argv.length>1){
+      String arg = argv[1];
+      if("all".equals(arg)){
+        t.closeAllDbs();
+        return;
+      }
+      else{
+        id = Integer.parseInt(arg);
+      }
+    }else{
+      id = t.getCurrentDbId();
+    }
+    t.closeDb(id);
+  }
+}
+
+//! --column-names command
+class ColumnNamesCommand extends Command {
+  public void process(
+    SQLTester st, TestScript ts, String[] argv
+  ) throws Exception{
+    argcCheck(ts,argv,1);
+    st.outputColumnNames( Integer.parseInt(argv[1])!=0 );
+  }
+}
+
+//! --db command
+class DbCommand extends Command {
+  public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+    argcCheck(ts,argv,1);
+    t.setCurrentDb( Integer.parseInt(argv[1]) );
+  }
+}
+
+//! --glob command
+class GlobCommand extends Command {
+  private boolean negate = false;
+  public GlobCommand(){}
+  protected GlobCommand(boolean negate){ this.negate = negate; }
+
+  public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+    argcCheck(ts,argv,1,-1);
+    t.incrementTestCounter();
+    final String sql = t.takeInputBuffer();
+    int rc = t.execSql(null, true, ResultBufferMode.ESCAPED,
+                       ResultRowMode.ONELINE, sql);
+    final String result = t.getResultText();
+    final String sArgs = Util.argvToString(argv);
+    //t2.verbose2(argv[0]," rc = ",rc," result buffer:\n", result,"\nargs:\n",sArgs);
+    final String glob = Util.argvToString(argv);
+    rc = SQLTester.strglob(glob, result);
+    if( (negate && 0==rc) || (!negate && 0!=rc) ){
+      ts.toss(argv[0], " mismatch: ", glob," vs input: ",result);
+    }
+  }
+}
+
+//! --json command
+class JsonCommand extends ResultCommand {
+  public JsonCommand(){ super(ResultBufferMode.ASIS); }
+}
+
+//! --json-block command
+class JsonBlockCommand extends TableResultCommand {
+  public JsonBlockCommand(){ super(true); }
+}
+
+//! --new command
+class NewDbCommand extends OpenDbCommand {
+  public NewDbCommand(){ super(true); }
+}
+
+//! Placeholder dummy/no-op commands
+class NoopCommand extends Command {
+  public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+  }
+}
+
+//! --notglob command
+class NotGlobCommand extends GlobCommand {
+  public NotGlobCommand(){
+    super(true);
+  }
+}
+
+//! --null command
+class NullCommand extends Command {
+  public void process(
+    SQLTester st, TestScript ts, String[] argv
+  ) throws Exception{
+    argcCheck(ts,argv,1);
+    st.setNullValue( argv[1] );
+  }
+}
+
+//! --open command
+class OpenDbCommand extends Command {
+  private boolean createIfNeeded = false;
+  public OpenDbCommand(){}
+  protected OpenDbCommand(boolean c){createIfNeeded = c;}
+  public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+    argcCheck(ts,argv,1);
+    t.openDb(argv[1], createIfNeeded);
+  }
+}
+
+//! --print command
+class PrintCommand extends Command {
+  public void process(
+    SQLTester st, TestScript ts, String[] argv
+  ) throws Exception{
+    st.out(ts.getOutputPrefix(),": ");
+    if( 1==argv.length ){
+      st.out( st.getInputText() );
+    }else{
+      st.outln( Util.argvToString(argv) );
+    }
+  }
+}
+
+//! --result command
+class ResultCommand extends Command {
+  private final ResultBufferMode bufferMode;
+  protected ResultCommand(ResultBufferMode bm){ bufferMode = bm; }
+  public ResultCommand(){ this(ResultBufferMode.ESCAPED); }
+  public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+    argcCheck(ts,argv,0,-1);
+    t.incrementTestCounter();
+    final String sql = t.takeInputBuffer();
+    //ts.verbose2(argv[0]," SQL =\n",sql);
+    int rc = t.execSql(null, false, bufferMode, ResultRowMode.ONELINE, sql);
+    final String result = t.getResultText().trim();
+    final String sArgs = argv.length>1 ? Util.argvToString(argv) : "";
+    if( !result.equals(sArgs) ){
+      t.outln(argv[0]," FAILED comparison. Result buffer:\n",
+              result,"\nExpected result:\n",sArgs);
+      ts.toss(argv[0]+" comparison failed.");
+    }
+  }
+}
+
+//! --run command
+class RunCommand extends Command {
+  public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+    argcCheck(ts,argv,0,1);
+    final sqlite3 db = (1==argv.length)
+      ? t.getCurrentDb() : t.getDbById( Integer.parseInt(argv[1]) );
+    final String sql = t.takeInputBuffer();
+    final int rc = t.execSql(db, false, ResultBufferMode.NONE,
+                             ResultRowMode.ONELINE, sql);
+    if( 0!=rc && t.isVerbose() ){
+      String msg = sqlite3_errmsg(db);
+      ts.verbose1(argv[0]," non-fatal command error #",rc,": ",
+                  msg,"\nfor SQL:\n",sql);
+    }
+  }
+}
+
+//! --tableresult command
+class TableResultCommand extends Command {
+  private final boolean jsonMode;
+  protected TableResultCommand(boolean jsonMode){ this.jsonMode = jsonMode; }
+  public TableResultCommand(){ this(false); }
+  public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+    argcCheck(ts,argv,0);
+    t.incrementTestCounter();
+    String 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.");
+    }else{
+      body = body.substring(0, body.length()-6);
+    }
+    final String[] globs = body.split("\\s*\\n\\s*");
+    if( globs.length < 1 ){
+      ts.toss(argv[0], " requires 1 or more ",
+              (jsonMode ? "json snippets" : "globs"),".");
+    }
+    final String sql = t.takeInputBuffer();
+    t.execSql(null, true,
+              jsonMode ? ResultBufferMode.ASIS : ResultBufferMode.ESCAPED,
+              ResultRowMode.NEWLINE, sql);
+    final String rbuf = t.getResultText();
+    final String[] res = rbuf.split("\n");
+    if( res.length != globs.length ){
+      ts.toss(argv[0], " failure: input has ", res.length,
+              " row(s) but expecting ",globs.length);
+    }
+    for(int i = 0; i < res.length; ++i){
+      final String glob = globs[i].replaceAll("\\s+"," ").trim();
+      //ts.verbose2(argv[0]," <<",glob,">> vs <<",res[i],">>");
+      if( jsonMode ){
+        if( !glob.equals(res[i]) ){
+          ts.toss(argv[0], " json <<",glob, ">> does not match: <<",
+                  res[i],">>");
+        }
+      }else if( 0 != SQLTester.strglob(glob, res[i]) ){
+        ts.toss(argv[0], " glob <<",glob,">> does not match: <<",res[i],">>");
+      }
+    }
+  }
+}
+
+//! --testcase command
+class TestCaseCommand extends Command {
+  public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+    argcCheck(ts,argv,1);
+    ts.setTestCaseName(argv[1]);
+    t.clearResultBuffer();
+    t.clearInputBuffer();
+  }
+}
+
+//! --verbosity command
+class VerbosityCommand extends Command {
+  public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+    argcCheck(ts,argv,1);
+    ts.setVerbosity( Integer.parseInt(argv[1]) );
+  }
+}
+
+class CommandDispatcher {
+
+  private static java.util.Map commandMap =
+    new java.util.HashMap<>();
+
+  /**
+     Returns a (cached) instance mapped to name, or null if no match
+     is found.
+  */
+  static Command getCommandByName(String name){
+    Command rv = commandMap.get(name);
+    if( null!=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;
+      default: rv = null; break;
+    }
+    if( null!=rv ) commandMap.put(name, rv);
+    return rv;
+  }
+
+  /**
+     Treats argv[0] as a command name, looks it up with
+     getCommandByName(), and calls process() on that instance, passing
+     it arguments given to this function.
+  */
+  static void dispatch(SQLTester tester, TestScript ts, String[] argv) throws Exception{
+    final Command cmd = getCommandByName(argv[0]);
+    if(null == cmd){
+      throw new UnknownCommand(ts, argv[0]);
+    }
+    cmd.process(tester, ts, argv);
+  }
+}
+
+
+/**
+   This class represents a single test script. It handles (or
+   delegates) its the reading-in and parsing, but the details of
+   evaluation are delegated elsewhere.
+*/
+class TestScript {
+  //! input file
+  private String filename = null;
+  //! Name pulled from the SCRIPT_MODULE_NAME directive of the file
+  private String moduleName = null;
+  //! Current test case name.
+  private String testCaseName = null;
+  //! Content buffer state.
+  private final Cursor cur = new Cursor();
+  //! Utility for console output.
+  private final Outer outer = new Outer();
+
+  //! File content and parse state.
+  private static final class Cursor {
+    private final StringBuilder sb = new StringBuilder();
+    byte[] src = null;
+    //! Current position in this.src.
+    int pos = 0;
+    //! Current line number. Starts at 0 for internal reasons and will
+    // line up with 1-based reality once parsing starts.
+    int lineNo = 0 /* yes, zero */;
+    //! Putback value for this.pos.
+    int putbackPos = 0;
+    //! Putback line number
+    int putbackLineNo = 0;
+    //! Peeked-to pos, used by peekLine() and consumePeeked().
+    int peekedPos = 0;
+    //! Peeked-to line number.
+    int peekedLineNo = 0;
+
+    //! Restore parsing state to the start of the stream.
+    void rewind(){
+      sb.setLength(0);
+      pos = lineNo = putbackPos = putbackLineNo = peekedPos = peekedLineNo = 0
+        /* kinda missing memset() about now. */;
+    }
+  }
+
+  private byte[] readFile(String filename) throws Exception {
+    return java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(filename));
+  }
+
+  /**
+     Initializes the script with the content of the given file.
+     Throws if it cannot read the file.
+  */
+  public TestScript(String filename) throws Exception{
+    this.filename = filename;
+    setVerbosity(2);
+    cur.src = readFile(filename);
+  }
+
+  public String getFilename(){
+    return filename;
+  }
+
+  public String getModuleName(){
+    return moduleName;
+  }
+
+  /**
+     Verbosity level 0 produces no debug/verbose output. Level 1 produces
+     some and level 2 produces more.
+   */
+  public void setVerbosity(int level){
+    outer.setVerbosity(level);
+  }
+
+  public String getOutputPrefix(){
+    String rc = "["+(moduleName==null ? "" : moduleName)+"]";
+    if( null!=testCaseName ) rc += "["+testCaseName+"]";
+    if( null!=filename ) rc += "["+filename+"]";
+    return rc + " line "+ cur.lineNo;
+  }
+
+  static final String[] verboseLabel = {"🔈",/*"🔉",*/"🔊","📢"};
+  //! Output vals only if level<=current verbosity level.
+  private TestScript verboseN(int level, Object... vals){
+    final int verbosity = outer.getVerbosity();
+    if(verbosity>=level){
+      outer.out( verboseLabel[level-1], getOutputPrefix(), " ",level,": "
+      ).outln(vals);
+    }
+    return this;
+  }
+
+  TestScript verbose1(Object... vals){return verboseN(1,vals);}
+  TestScript verbose2(Object... vals){return verboseN(2,vals);}
+  TestScript verbose3(Object... vals){return verboseN(3,vals);}
+
+  private void reset(){
+    testCaseName = null;
+    cur.rewind();
+  }
+
+  void setTestCaseName(String n){ testCaseName = n; }
+
+  /**
+     Returns the next line from the buffer, minus the trailing EOL.
+
+     Returns null when all input is consumed. Throws if it reads
+     illegally-encoded input, e.g. (non-)characters in the range
+     128-256.
+  */
+  String getLine(){
+    if( cur.pos==cur.src.length ){
+      return null /* EOF */;
+    }
+    cur.putbackPos = cur.pos;
+    cur.putbackLineNo = cur.lineNo;
+    cur.sb.setLength(0);
+    final boolean skipLeadingWs = false;
+    byte b = 0, prevB = 0;
+    int i = cur.pos;
+    if(skipLeadingWs) {
+      /* Skip any leading spaces, including newlines. This will eliminate
+         blank lines. */
+      for(; i < cur.src.length; ++i, prevB=b){
+        b = cur.src[i];
+        switch((int)b){
+          case 32/*space*/: case 9/*tab*/: case 13/*CR*/: continue;
+          case 10/*NL*/: ++cur.lineNo; continue;
+          default: break;
+        }
+        break;
+      }
+      if( i==cur.src.length ){
+        return null /* EOF */;
+      }
+    }
+    boolean doBreak = false;
+    final byte[] aChar = {0,0,0,0} /* multi-byte char buffer */;
+    int nChar = 0 /* number of bytes in the char */;
+    for(; i < cur.src.length && !doBreak; ++i){
+      b = cur.src[i];
+      switch( (int)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. Appending individual bytes to the StringBuffer
+             appends their integer value. */
+          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 (#"+(int)b+").");
+              break;
+          }
+          if( 1==nChar ){
+            cur.sb.append((char)b);
+          }else{
+            for(int x = 0; x < nChar; ++x) aChar[x] = cur.src[i+x];
+            cur.sb.append(new String(Arrays.copyOf(aChar, nChar),
+                                      StandardCharsets.UTF_8));
+            i += nChar-1;
+          }
+          break;
+      }
+    }
+    cur.pos = i;
+    final String rv = cur.sb.toString();
+    if( i==cur.src.length && 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.
+  */
+  String peekLine(){
+    final int oldPos = cur.pos;
+    final int oldPB = cur.putbackPos;
+    final int oldPBL = cur.putbackLineNo;
+    final int oldLine = cur.lineNo;
+    try{ return 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().
+  */
+  void consumePeeked(){
+    cur.pos = cur.peekedPos;
+    cur.lineNo = cur.peekedLineNo;
+  }
+
+  /**
+     Restores the cursor to the position it had before the previous
+     call to getLine().
+  */
+  void putbackLine(){
+    cur.pos = cur.putbackPos;
+    cur.lineNo = cur.putbackLineNo;
+  }
+
+  private boolean checkRequiredProperties(SQLTester t, String[] props) throws SQLTesterException{
+    if( true ) return false;
+    int nOk = 0;
+    for(String rp : props){
+      verbose1("REQUIRED_PROPERTIES: ",rp);
+      switch(rp){
+        case "RECURSIVE_TRIGGERS":
+          t.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 */
+          t.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 */
+          t.appendDbInitSql("pragma temp_store=0;");
+          ++nOk;
+          break;
+        case "AUTOVACUUM":
+          t.appendDbInitSql("pragma auto_vacuum=full;");
+          ++nOk;
+        case "INCRVACUUM":
+          t.appendDbInitSql("pragma auto_vacuum=incremental;");
+          ++nOk;
+        default:
+          break;
+      }
+    }
+    return props.length == nOk;
+  }
+
+  private static final Pattern patternRequiredProperties =
+    Pattern.compile(" REQUIRED_PROPERTIES:[ \\t]*(\\S.*)\\s*$");
+  private static final Pattern patternScriptModuleName =
+    Pattern.compile(" SCRIPT_MODULE_NAME:[ \\t]*(\\S+)\\s*$");
+  private static final Pattern patternMixedModuleName =
+    Pattern.compile(" ((MIXED_)?MODULE_NAME):[ \\t]*(\\S+)\\s*$");
+  private static final Pattern patternCommand =
+    Pattern.compile("^--(([a-z-]+)( .*)?)$");
+
+  /**
+     Looks for "directives." If a compatible one is found, it is
+     processed and this function returns. If an incompatible one is found,
+     a description of it is returned and processing of the test must
+     end immediately.
+  */
+  private void checkForDirective(
+    SQLTester tester, String line
+  ) throws IncompatibleDirective {
+    if(line.startsWith("#")){
+      throw new IncompatibleDirective(this, "C-preprocessor input: "+line);
+    }else if(line.startsWith("---")){
+      new IncompatibleDirective(this, "triple-dash: "+line);
+    }
+    Matcher m = patternScriptModuleName.matcher(line);
+    if( m.find() ){
+      moduleName = m.group(1);
+      return;
+    }
+    m = patternRequiredProperties.matcher(line);
+    if( m.find() ){
+      final String rp = m.group(1);
+      if( ! checkRequiredProperties( tester, rp.split("\\s+") ) ){
+        throw new IncompatibleDirective(this, "REQUIRED_PROPERTIES: "+rp);
+      }
+    }
+    m = patternMixedModuleName.matcher(line);
+    if( m.find() ){
+      throw new IncompatibleDirective(this, m.group(1)+": "+m.group(3));
+    }
+    if( line.indexOf("\n|")>=0 ){
+      throw new IncompatibleDirective(this, "newline-pipe combination.");
+    }
+    return;
+  }
+
+  boolean isCommandLine(String line, boolean checkForImpl){
+    final Matcher m = patternCommand.matcher(line);
+    boolean rc = m.find();
+    if( rc && checkForImpl ){
+      rc = null!=CommandDispatcher.getCommandByName(m.group(2));
+    }
+    return rc;
+  }
+
+  /**
+     If line looks like a command, returns an argv for that command
+     invocation, else returns null.
+  */
+  String[] getCommandArgv(String line){
+    final Matcher m = patternCommand.matcher(line);
+    return m.find() ? m.group(1).trim().split("\\s+") : null;
+  }
+
+  /**
+     Fetches lines until the next recognized command. Throws if
+     checkForDirective() does.  Returns null if there is no input or
+     it's only whitespace. The returned string retains all whitespace.
+
+     Note that "subcommands", --command-like constructs in the body
+     which do not match a known command name are considered to be
+     content, not commands.
+  */
+  String fetchCommandBody(SQLTester tester){
+    final StringBuilder sb = new StringBuilder();
+    String line;
+    while( (null != (line = peekLine())) ){
+      checkForDirective(tester, line);
+      if( isCommandLine(line, true) ) break;
+      else {
+        sb.append(line).append("\n");
+        consumePeeked();
+      }
+    }
+    line = sb.toString();
+    return line.trim().isEmpty() ? null : line;
+  }
+
+  private void processCommand(SQLTester t, String[] argv) throws Exception{
+    verbose1("running command: ",argv[0], " ", Util.argvToString(argv));
+    if(outer.getVerbosity()>1){
+      final String input = t.getInputText();
+      if( !input.isEmpty() ) verbose3("Input buffer = ",input);
+    }
+    CommandDispatcher.dispatch(t, this, argv);
+  }
+
+  void toss(Object... msg) throws TestScriptFailed {
+    StringBuilder sb = new StringBuilder();
+    for(Object s : msg) sb.append(s);
+    throw new TestScriptFailed(this, sb.toString());
+  }
+
+  /**
+     Runs this test script in the context of the given tester object.
+  */
+  public boolean run(SQLTester tester) throws Exception {
+    reset();
+    setVerbosity(tester.getVerbosity());
+    String line, directive;
+    String[] argv;
+    while( null != (line = getLine()) ){
+      verbose3("input line: ",line);
+      checkForDirective(tester, line);
+      argv = getCommandArgv(line);
+      if( null!=argv ){
+        processCommand(tester, argv);
+        continue;
+      }
+      tester.appendInput(line,true);
+    }
+    return true;
+  }
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/ScalarFunction.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/ScalarFunction.java
new file mode 100644
index 00000000..95541bdc
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/ScalarFunction.java
@@ -0,0 +1,33 @@
+/*
+** 2023-08-25
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+
+/**
+   A SQLFunction implementation for scalar functions.
+*/
+public abstract class ScalarFunction implements SQLFunction {
+  /**
+     As for the xFunc() argument of the C API's
+     sqlite3_create_function(). If this function throws, it is
+     translated into an sqlite3_result_error().
+  */
+  public abstract void xFunc(sqlite3_context cx, sqlite3_value[] args);
+
+  /**
+     Optionally override to be notified when the UDF is finalized by
+     SQLite. This default implementation does nothing.
+  */
+  public void xDestroy() {}
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/TableColumnMetadata.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/TableColumnMetadata.java
new file mode 100644
index 00000000..d8b6226a
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/TableColumnMetadata.java
@@ -0,0 +1,35 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   A wrapper object for use with sqlite3_table_column_metadata().
+   They are populated only via that interface.
+*/
+public final class TableColumnMetadata {
+  OutputPointer.Bool pNotNull = new OutputPointer.Bool();
+  OutputPointer.Bool pPrimaryKey = new OutputPointer.Bool();
+  OutputPointer.Bool pAutoinc = new OutputPointer.Bool();
+  OutputPointer.String pzCollSeq = new OutputPointer.String();
+  OutputPointer.String pzDataType = new OutputPointer.String();
+
+  public TableColumnMetadata(){
+  }
+
+  public String getDataType(){ return pzDataType.value; }
+  public String getCollation(){ return pzCollSeq.value; }
+  public boolean isNotNull(){ return pNotNull.value; }
+  public boolean isPrimaryKey(){ return pPrimaryKey.value; }
+  public boolean isAutoincrement(){ return pAutoinc.value; }
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/Tester1.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/Tester1.java
new file mode 100644
index 00000000..6fb28e65
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/Tester1.java
@@ -0,0 +1,1976 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file contains a set of tests for the sqlite3 JNI bindings.
+*/
+package org.sqlite.jni.capi;
+import static org.sqlite.jni.capi.CApi.*;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+/**
+   An annotation for Tester1 tests which we do not want to run in
+   reflection-driven test mode because either they are not suitable
+   for multi-threaded threaded mode or we have to control their execution
+   order.
+*/
+@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
+@java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD})
+@interface ManualTest{}
+/**
+   Annotation for Tester1 tests which mark those which must be skipped
+   in multi-threaded mode.
+*/
+@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
+@java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD})
+@interface SingleThreadOnly{}
+
+public class Tester1 implements Runnable {
+  //! True when running in multi-threaded mode.
+  private static boolean mtMode = false;
+  //! True to sleep briefly between tests.
+  private static boolean takeNaps = false;
+  //! True to shuffle the order of the tests.
+  private static boolean shuffle = false;
+  //! True to dump the list of to-run tests to stdout.
+  private static boolean listRunTests = false;
+  //! True to squelch all out() and outln() output.
+  private static boolean quietMode = false;
+  //! Total number of runTests() calls.
+  private static int nTestRuns = 0;
+  //! List of test*() methods to run.
+  private static List testMethods = null;
+  //! List of exceptions collected by run()
+  private static List listErrors = new ArrayList<>();
+  private static final class Metrics {
+    //! Number of times createNewDb() (or equivalent) is invoked.
+    volatile int dbOpen = 0;
+  }
+
+  private Integer tId;
+
+  Tester1(Integer id){
+    tId = id;
+  }
+
+  static final Metrics metrics = new Metrics();
+
+  public static synchronized void outln(){
+    if( !quietMode ){
+      System.out.println("");
+    }
+  }
+
+  public static synchronized void outPrefix(){
+    if( !quietMode ){
+      System.out.print(Thread.currentThread().getName()+": ");
+    }
+  }
+
+  public static synchronized void outln(Object val){
+    if( !quietMode ){
+      outPrefix();
+      System.out.println(val);
+    }
+  }
+
+  public static synchronized void out(Object val){
+    if( !quietMode ){
+      System.out.print(val);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  public static synchronized void out(Object... vals){
+    if( !quietMode ){
+      outPrefix();
+      for(Object v : vals) out(v);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  public static synchronized void outln(Object... vals){
+    if( !quietMode ){
+      out(vals); out("\n");
+    }
+  }
+
+  static volatile int affirmCount = 0;
+  public static synchronized int affirm(Boolean v, String comment){
+    ++affirmCount;
+    if( false ) assert( v /* prefer assert over exception if it's enabled because
+                 the JNI layer sometimes has to suppress exceptions,
+                 so they might be squelched on their way back to the
+                 top. */);
+    if( !v ) throw new RuntimeException(comment);
+    return affirmCount;
+  }
+
+  public static void affirm(Boolean v){
+    affirm(v, "Affirmation failed.");
+  }
+
+  @SingleThreadOnly /* because it's thread-agnostic */
+  private void test1(){
+    affirm(sqlite3_libversion_number() == SQLITE_VERSION_NUMBER);
+  }
+
+  public static sqlite3 createNewDb(){
+    final OutputPointer.sqlite3 out = new OutputPointer.sqlite3();
+    int rc = sqlite3_open(":memory:", out);
+    ++metrics.dbOpen;
+    sqlite3 db = out.take();
+    if( 0!=rc ){
+      final String msg =
+        null==db ? sqlite3_errstr(rc) : sqlite3_errmsg(db);
+      sqlite3_close(db);
+      throw new RuntimeException("Opening db failed: "+msg);
+    }
+    affirm( null == out.get() );
+    affirm( 0 != db.getNativePointer() );
+    rc = sqlite3_busy_timeout(db, 2000);
+    affirm( 0 == rc );
+    return db;
+  }
+
+  public static void execSql(sqlite3 db, String[] sql){
+    execSql(db, String.join("", sql));
+  }
+
+  public static int execSql(sqlite3 db, boolean throwOnError, String sql){
+    OutputPointer.Int32 oTail = new OutputPointer.Int32();
+    final byte[] sqlUtf8 = sql.getBytes(StandardCharsets.UTF_8);
+    int pos = 0, n = 1;
+    byte[] sqlChunk = sqlUtf8;
+    int rc = 0;
+    sqlite3_stmt stmt = null;
+    final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt();
+    while(pos < sqlChunk.length){
+      if(pos > 0){
+        sqlChunk = Arrays.copyOfRange(sqlChunk, pos,
+                                      sqlChunk.length);
+      }
+      if( 0==sqlChunk.length ) break;
+      rc = sqlite3_prepare_v2(db, sqlChunk, outStmt, oTail);
+      if(throwOnError) affirm(0 == rc);
+      else if( 0!=rc ) break;
+      pos = oTail.value;
+      stmt = outStmt.take();
+      if( null == stmt ){
+        // empty statement was parsed.
+        continue;
+      }
+      affirm(0 != stmt.getNativePointer());
+      while( SQLITE_ROW == (rc = sqlite3_step(stmt)) ){
+      }
+      sqlite3_finalize(stmt);
+      affirm(0 == stmt.getNativePointer());
+      if(0!=rc && SQLITE_ROW!=rc && SQLITE_DONE!=rc){
+        break;
+      }
+    }
+    sqlite3_finalize(stmt);
+    if(SQLITE_ROW==rc || SQLITE_DONE==rc) rc = 0;
+    if( 0!=rc && throwOnError){
+      throw new RuntimeException("db op failed with rc="
+                                 +rc+": "+sqlite3_errmsg(db));
+    }
+    return rc;
+  }
+
+  public static void execSql(sqlite3 db, String sql){
+    execSql(db, true, sql);
+  }
+
+  public static sqlite3_stmt prepare(sqlite3 db, boolean throwOnError, String sql){
+    final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt();
+    int rc = sqlite3_prepare_v2(db, sql, outStmt);
+    if( throwOnError ){
+      affirm( 0 == rc );
+    }
+    final sqlite3_stmt rv = outStmt.take();
+    affirm( null == outStmt.get() );
+    if( throwOnError ){
+      affirm( 0 != rv.getNativePointer() );
+    }
+    return rv;
+  }
+
+  public static sqlite3_stmt prepare(sqlite3 db, String sql){
+    return prepare(db, true, sql);
+  }
+
+  private void showCompileOption(){
+    int i = 0;
+    String optName;
+    outln("compile options:");
+    for( ; null != (optName = sqlite3_compileoption_get(i)); ++i){
+      outln("\t"+optName+"\t (used="+
+            sqlite3_compileoption_used(optName)+")");
+    }
+  }
+
+  private void testCompileOption(){
+    int i = 0;
+    String optName;
+    for( ; null != (optName = sqlite3_compileoption_get(i)); ++i){
+    }
+    affirm( i > 10 );
+    affirm( null==sqlite3_compileoption_get(-1) );
+  }
+
+  private void testOpenDb1(){
+    final OutputPointer.sqlite3 out = new OutputPointer.sqlite3();
+    int rc = sqlite3_open(":memory:", out);
+    ++metrics.dbOpen;
+    sqlite3 db = out.get();
+    affirm(0 == rc);
+    affirm(db.getNativePointer()!=0);
+    sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 1, null)
+      /* This function has different mangled names in jdk8 vs jdk19,
+         and this call is here to ensure that the build fails
+         if it cannot find both names. */;
+
+    affirm( 0==sqlite3_db_readonly(db,"main") );
+    affirm( 0==sqlite3_db_readonly(db,null) );
+    affirm( 0>sqlite3_db_readonly(db,"nope") );
+    affirm( 0>sqlite3_db_readonly(null,null) );
+    affirm( 0==sqlite3_last_insert_rowid(null) );
+
+    // These interrupt checks are only to make sure that the JNI binding
+    // has the proper exported symbol names. They don't actually test
+    // anything useful.
+    affirm( !sqlite3_is_interrupted(db) );
+    sqlite3_interrupt(db);
+    affirm( sqlite3_is_interrupted(db) );
+    sqlite3_close_v2(db);
+    affirm(0 == db.getNativePointer());
+  }
+
+  private void testOpenDb2(){
+    final OutputPointer.sqlite3 out = new OutputPointer.sqlite3();
+    int rc = sqlite3_open_v2(":memory:", out,
+                             SQLITE_OPEN_READWRITE
+                             | SQLITE_OPEN_CREATE, null);
+    ++metrics.dbOpen;
+    affirm(0 == rc);
+    sqlite3 db = out.get();
+    affirm(0 != db.getNativePointer());
+    sqlite3_close_v2(db);
+    affirm(0 == db.getNativePointer());
+  }
+
+  private void testPrepare123(){
+    sqlite3 db = createNewDb();
+    int rc;
+    final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt();
+    rc = sqlite3_prepare(db, "CREATE TABLE t1(a);", outStmt);
+    affirm(0 == rc);
+    sqlite3_stmt stmt = outStmt.take();
+    affirm(0 != stmt.getNativePointer());
+    affirm( !sqlite3_stmt_readonly(stmt) );
+    affirm( db == sqlite3_db_handle(stmt) );
+    rc = sqlite3_step(stmt);
+    affirm(SQLITE_DONE == rc);
+    sqlite3_finalize(stmt);
+    affirm( null == sqlite3_db_handle(stmt) );
+    affirm(0 == stmt.getNativePointer());
+
+    { /* Demonstrate how to use the "zTail" option of
+         sqlite3_prepare() family of functions. */
+      OutputPointer.Int32 oTail = new OutputPointer.Int32();
+      final byte[] sqlUtf8 =
+        "CREATE TABLE t2(a); INSERT INTO t2(a) VALUES(1),(2),(3)"
+        .getBytes(StandardCharsets.UTF_8);
+      int pos = 0, n = 1;
+      byte[] sqlChunk = sqlUtf8;
+      while(pos < sqlChunk.length){
+        if(pos > 0){
+          sqlChunk = Arrays.copyOfRange(sqlChunk, pos, sqlChunk.length);
+        }
+        //outln("SQL chunk #"+n+" length = "+sqlChunk.length+", pos = "+pos);
+        if( 0==sqlChunk.length ) break;
+        rc = sqlite3_prepare_v2(db, sqlChunk, outStmt, oTail);
+        affirm(0 == rc);
+        stmt = outStmt.get();
+        pos = oTail.value;
+        /*outln("SQL tail pos = "+pos+". Chunk = "+
+              (new String(Arrays.copyOfRange(sqlChunk,0,pos),
+              StandardCharsets.UTF_8)));*/
+        switch(n){
+          case 1: affirm(19 == pos); break;
+          case 2: affirm(36 == pos); break;
+          default: affirm( false /* can't happen */ );
+
+        }
+        ++n;
+        affirm(0 != stmt.getNativePointer());
+        rc = sqlite3_step(stmt);
+        affirm(SQLITE_DONE == rc);
+        sqlite3_finalize(stmt);
+        affirm(0 == stmt.getNativePointer());
+      }
+    }
+
+
+    rc = sqlite3_prepare_v3(db, "INSERT INTO t2(a) VALUES(1),(2),(3)",
+                            SQLITE_PREPARE_NORMALIZE, outStmt);
+    affirm(0 == rc);
+    stmt = outStmt.get();
+    affirm(0 != stmt.getNativePointer());
+    sqlite3_finalize(stmt);
+    affirm(0 == stmt.getNativePointer() );
+
+    affirm( 0==sqlite3_errcode(db) );
+    stmt = sqlite3_prepare(db, "intentional error");
+    affirm( null==stmt );
+    affirm( 0!=sqlite3_errcode(db) );
+    affirm( 0==sqlite3_errmsg(db).indexOf("near \"intentional\"") );
+    sqlite3_finalize(stmt);
+    stmt = sqlite3_prepare(db, "/* empty input*/\n-- comments only");
+    affirm( null==stmt );
+    affirm( 0==sqlite3_errcode(db) );
+    sqlite3_close_v2(db);
+  }
+
+  private void testBindFetchInt(){
+    sqlite3 db = createNewDb();
+    execSql(db, "CREATE TABLE t(a)");
+
+    sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(:a);");
+    affirm(1 == sqlite3_bind_parameter_count(stmt));
+    final int paramNdx = sqlite3_bind_parameter_index(stmt, ":a");
+    affirm(1 == paramNdx);
+    affirm( ":a".equals(sqlite3_bind_parameter_name(stmt, paramNdx)));
+    int total1 = 0;
+    long rowid = -1;
+    int changes = sqlite3_changes(db);
+    int changesT = sqlite3_total_changes(db);
+    long changes64 = sqlite3_changes64(db);
+    long changesT64 = sqlite3_total_changes64(db);
+    int rc;
+    for(int i = 99; i < 102; ++i ){
+      total1 += i;
+      rc = sqlite3_bind_int(stmt, paramNdx, i);
+      affirm(0 == rc);
+      rc = sqlite3_step(stmt);
+      sqlite3_reset(stmt);
+      affirm(SQLITE_DONE == rc);
+      long x = sqlite3_last_insert_rowid(db);
+      affirm(x > rowid);
+      rowid = x;
+    }
+    sqlite3_finalize(stmt);
+    affirm(300 == total1);
+    affirm(sqlite3_changes(db) > changes);
+    affirm(sqlite3_total_changes(db) > changesT);
+    affirm(sqlite3_changes64(db) > changes64);
+    affirm(sqlite3_total_changes64(db) > changesT64);
+    stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;");
+    affirm( sqlite3_stmt_readonly(stmt) );
+    affirm( !sqlite3_stmt_busy(stmt) );
+    int total2 = 0;
+    while( SQLITE_ROW == sqlite3_step(stmt) ){
+      affirm( sqlite3_stmt_busy(stmt) );
+      total2 += sqlite3_column_int(stmt, 0);
+      sqlite3_value sv = sqlite3_column_value(stmt, 0);
+      affirm( null != sv );
+      affirm( 0 != sv.getNativePointer() );
+      affirm( SQLITE_INTEGER == sqlite3_value_type(sv) );
+    }
+    affirm( !sqlite3_stmt_busy(stmt) );
+    sqlite3_finalize(stmt);
+    affirm(total1 == total2);
+
+    // sqlite3_value_frombind() checks...
+    stmt = prepare(db, "SELECT 1, ?");
+    sqlite3_bind_int(stmt, 1, 2);
+    rc = sqlite3_step(stmt);
+    affirm( SQLITE_ROW==rc );
+    affirm( !sqlite3_value_frombind(sqlite3_column_value(stmt, 0)) );
+    affirm( sqlite3_value_frombind(sqlite3_column_value(stmt, 1)) );
+    sqlite3_finalize(stmt);
+
+    sqlite3_close_v2(db);
+    affirm(0 == db.getNativePointer());
+  }
+
+  private void testBindFetchInt64(){
+    try (sqlite3 db = createNewDb()){
+      execSql(db, "CREATE TABLE t(a)");
+      sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);");
+      long total1 = 0;
+      for(long i = 0xffffffff; i < 0xffffffff + 3; ++i ){
+        total1 += i;
+        sqlite3_bind_int64(stmt, 1, i);
+        sqlite3_step(stmt);
+        sqlite3_reset(stmt);
+      }
+      sqlite3_finalize(stmt);
+      stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;");
+      long total2 = 0;
+      while( SQLITE_ROW == sqlite3_step(stmt) ){
+        total2 += sqlite3_column_int64(stmt, 0);
+      }
+      sqlite3_finalize(stmt);
+      affirm(total1 == total2);
+      //sqlite3_close_v2(db);
+    }
+  }
+
+  private void testBindFetchDouble(){
+    try (sqlite3 db = createNewDb()){
+      execSql(db, "CREATE TABLE t(a)");
+      sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);");
+      double total1 = 0;
+      for(double i = 1.5; i < 5.0; i = i + 1.0 ){
+        total1 += i;
+        sqlite3_bind_double(stmt, 1, i);
+        sqlite3_step(stmt);
+        sqlite3_reset(stmt);
+      }
+      sqlite3_finalize(stmt);
+      stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;");
+      double total2 = 0;
+      int counter = 0;
+      while( SQLITE_ROW == sqlite3_step(stmt) ){
+        ++counter;
+        total2 += sqlite3_column_double(stmt, 0);
+      }
+      affirm(4 == counter);
+      sqlite3_finalize(stmt);
+      affirm(total2<=total1+0.01 && total2>=total1-0.01);
+      //sqlite3_close_v2(db);
+    }
+  }
+
+  private void testBindFetchText(){
+    sqlite3 db = createNewDb();
+    execSql(db, "CREATE TABLE t(a)");
+    sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);");
+    String[] list1 = { "hell🤩", "w😃rld", "!🤩" };
+    int rc;
+    int n = 0;
+    for( String e : list1 ){
+      rc = (0==n)
+        ? sqlite3_bind_text(stmt, 1, e)
+        : sqlite3_bind_text16(stmt, 1, e);
+      affirm(0 == rc);
+      rc = sqlite3_step(stmt);
+      affirm(SQLITE_DONE==rc);
+      sqlite3_reset(stmt);
+    }
+    sqlite3_finalize(stmt);
+    stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;");
+    StringBuilder sbuf = new StringBuilder();
+    n = 0;
+    while( SQLITE_ROW == sqlite3_step(stmt) ){
+      final sqlite3_value sv = sqlite3_value_dup(sqlite3_column_value(stmt,0));
+      final String txt = sqlite3_column_text16(stmt, 0);
+      sbuf.append( txt );
+      affirm( txt.equals(new String(
+                           sqlite3_column_text(stmt, 0),
+                           StandardCharsets.UTF_8
+                         )) );
+      affirm( txt.length() < sqlite3_value_bytes(sv) );
+      affirm( txt.equals(new String(
+                           sqlite3_value_text(sv),
+                           StandardCharsets.UTF_8)) );
+      affirm( txt.length() == sqlite3_value_bytes16(sv)/2 );
+      affirm( txt.equals(sqlite3_value_text16(sv)) );
+      sqlite3_value_free(sv);
+      ++n;
+    }
+    sqlite3_finalize(stmt);
+    affirm(3 == n);
+    affirm("w😃rldhell🤩!🤩".equals(sbuf.toString()));
+
+    try( sqlite3_stmt stmt2 = prepare(db, "SELECT ?, ?") ){
+      rc = sqlite3_bind_text(stmt2, 1, "");
+      affirm( 0==rc );
+      rc = sqlite3_bind_text(stmt2, 2, (String)null);
+      affirm( 0==rc );
+      rc = sqlite3_step(stmt2);
+      affirm( SQLITE_ROW==rc );
+      byte[] colBa = sqlite3_column_text(stmt2, 0);
+      affirm( 0==colBa.length );
+      colBa = sqlite3_column_text(stmt2, 1);
+      affirm( null==colBa );
+      //sqlite3_finalize(stmt);
+    }
+
+    if(true){
+      sqlite3_close_v2(db);
+    }else{
+      // Let the Object.finalize() override deal with it.
+    }
+  }
+
+  private void testBindFetchBlob(){
+    sqlite3 db = createNewDb();
+    execSql(db, "CREATE TABLE t(a)");
+    sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);");
+    byte[] list1 = { 0x32, 0x33, 0x34 };
+    int rc = sqlite3_bind_blob(stmt, 1, list1);
+    affirm( 0==rc );
+    rc = sqlite3_step(stmt);
+    affirm(SQLITE_DONE == rc);
+    sqlite3_finalize(stmt);
+    stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;");
+    int n = 0;
+    int total = 0;
+    while( SQLITE_ROW == sqlite3_step(stmt) ){
+      byte[] blob = sqlite3_column_blob(stmt, 0);
+      affirm(3 == blob.length);
+      int i = 0;
+      for(byte b : blob){
+        affirm(b == list1[i++]);
+        total += b;
+      }
+      ++n;
+    }
+    sqlite3_finalize(stmt);
+    affirm(1 == n);
+    affirm(total == 0x32 + 0x33 + 0x34);
+    sqlite3_close_v2(db);
+  }
+
+  private void testSql(){
+    sqlite3 db = createNewDb();
+    sqlite3_stmt stmt = prepare(db, "SELECT 1");
+    affirm( "SELECT 1".equals(sqlite3_sql(stmt)) );
+    sqlite3_finalize(stmt);
+    stmt = prepare(db, "SELECT ?");
+    sqlite3_bind_text(stmt, 1, "hell😃");
+    final String expect = "SELECT 'hell😃'";
+    affirm( expect.equals(sqlite3_expanded_sql(stmt)) );
+    String n = sqlite3_normalized_sql(stmt);
+    affirm( null==n || "SELECT?;".equals(n) );
+    sqlite3_finalize(stmt);
+    sqlite3_close(db);
+  }
+
+  private void testCollation(){
+    final sqlite3 db = createNewDb();
+    execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')");
+    final ValueHolder xDestroyCalled = new ValueHolder<>(0);
+    final CollationCallback myCollation = new CollationCallback() {
+        private String myState =
+          "this is local state. There is much like it, but this is mine.";
+        @Override
+        // Reverse-sorts its inputs...
+        public int call(byte[] lhs, byte[] rhs){
+          int len = lhs.length > rhs.length ? rhs.length : lhs.length;
+          int c = 0, i = 0;
+          for(i = 0; i < len; ++i){
+            c = lhs[i] - rhs[i];
+            if(0 != c) break;
+          }
+          if(0==c){
+            if(i < lhs.length) c = 1;
+            else if(i < rhs.length) c = -1;
+          }
+          return -c;
+        }
+        @Override
+        public void xDestroy() {
+          // Just demonstrates that xDestroy is called.
+          ++xDestroyCalled.value;
+        }
+      };
+    final CollationNeededCallback collLoader = new CollationNeededCallback(){
+        @Override
+        public int call(sqlite3 dbArg, int eTextRep, String collationName){
+          affirm(dbArg == db/* as opposed to a temporary object*/);
+          return sqlite3_create_collation(dbArg, "reversi", eTextRep, myCollation);
+        }
+      };
+    int rc = sqlite3_collation_needed(db, collLoader);
+    affirm( 0 == rc );
+    rc = sqlite3_collation_needed(db, collLoader);
+    affirm( 0 == rc /* Installing the same object again is a no-op */);
+    sqlite3_stmt stmt = prepare(db, "SELECT a FROM t ORDER BY a COLLATE reversi");
+    int counter = 0;
+    while( SQLITE_ROW == sqlite3_step(stmt) ){
+      final String val = sqlite3_column_text16(stmt, 0);
+      ++counter;
+      //outln("REVERSI'd row#"+counter+": "+val);
+      switch(counter){
+        case 1: affirm("c".equals(val)); break;
+        case 2: affirm("b".equals(val)); break;
+        case 3: affirm("a".equals(val)); break;
+      }
+    }
+    affirm(3 == counter);
+    sqlite3_finalize(stmt);
+    stmt = prepare(db, "SELECT a FROM t ORDER BY a");
+    counter = 0;
+    while( SQLITE_ROW == sqlite3_step(stmt) ){
+      final String val = sqlite3_column_text16(stmt, 0);
+      ++counter;
+      //outln("Non-REVERSI'd row#"+counter+": "+val);
+      switch(counter){
+        case 3: affirm("c".equals(val)); break;
+        case 2: affirm("b".equals(val)); break;
+        case 1: affirm("a".equals(val)); break;
+      }
+    }
+    affirm(3 == counter);
+    sqlite3_finalize(stmt);
+    affirm( 0 == xDestroyCalled.value );
+    rc = sqlite3_collation_needed(db, null);
+    affirm( 0 == rc );
+    sqlite3_close_v2(db);
+    affirm( 0 == db.getNativePointer() );
+    affirm( 1 == xDestroyCalled.value );
+  }
+
+  @SingleThreadOnly /* because it's thread-agnostic */
+  private void testToUtf8(){
+    /**
+       https://docs.oracle.com/javase/8/docs/api/java/nio/charset/Charset.html
+
+       Let's ensure that we can convert to standard UTF-8 in Java code
+       (noting that the JNI native API has no way to do this).
+    */
+    final byte[] ba = "a \0 b".getBytes(StandardCharsets.UTF_8);
+    affirm( 5 == ba.length /* as opposed to 6 in modified utf-8 */);
+  }
+
+  private void testStatus(){
+    final OutputPointer.Int64 cur64 = new OutputPointer.Int64();
+    final OutputPointer.Int64 high64 = new OutputPointer.Int64();
+    final OutputPointer.Int32 cur32 = new OutputPointer.Int32();
+    final OutputPointer.Int32 high32 = new OutputPointer.Int32();
+    final sqlite3 db = createNewDb();
+    execSql(db, "create table t(a); insert into t values(1),(2),(3)");
+
+    int rc = sqlite3_status(SQLITE_STATUS_MEMORY_USED, cur32, high32, false);
+    affirm( 0 == rc );
+    affirm( cur32.value > 0 );
+    affirm( high32.value >= cur32.value );
+
+    rc = sqlite3_status64(SQLITE_STATUS_MEMORY_USED, cur64, high64, false);
+    affirm( 0 == rc );
+    affirm( cur64.value > 0 );
+    affirm( high64.value >= cur64.value );
+
+    cur32.value = 0;
+    high32.value = 1;
+    rc = sqlite3_db_status(db, SQLITE_DBSTATUS_SCHEMA_USED, cur32, high32, false);
+    affirm( 0 == rc );
+    affirm( cur32.value > 0 );
+    affirm( high32.value == 0 /* always 0 for SCHEMA_USED */ );
+
+    sqlite3_close_v2(db);
+  }
+
+  private void testUdf1(){
+    final sqlite3 db = createNewDb();
+    // These ValueHolders are just to confirm that the func did what we want...
+    final ValueHolder xDestroyCalled = new ValueHolder<>(false);
+    final ValueHolder xFuncAccum = new ValueHolder<>(0);
+    final ValueHolder neverEverDoThisInClientCode = new ValueHolder<>(null);
+    final ValueHolder neverEverDoThisInClientCode2 = new ValueHolder<>(null);
+
+    // Create an SQLFunction instance using one of its 3 subclasses:
+    // Scalar, Aggregate, or Window:
+    SQLFunction func =
+      // Each of the 3 subclasses requires a different set of
+      // functions, all of which must be implemented.  Anonymous
+      // classes are a convenient way to implement these.
+      new ScalarFunction(){
+        public void xFunc(sqlite3_context cx, sqlite3_value[] args){
+          affirm(db == sqlite3_context_db_handle(cx));
+          if( null==neverEverDoThisInClientCode.value ){
+            /* !!!NEVER!!! hold a reference to an sqlite3_value or
+               sqlite3_context object like this in client code! They
+               are ONLY legal for the duration of their single
+               call. We do it here ONLY to test that the defenses
+               against clients doing this are working. */
+            neverEverDoThisInClientCode2.value = cx;
+            neverEverDoThisInClientCode.value = args;
+          }
+          int result = 0;
+          for( sqlite3_value v : args ) result += sqlite3_value_int(v);
+          xFuncAccum.value += result;// just for post-run testing
+          sqlite3_result_int(cx, result);
+        }
+        /* OPTIONALLY override xDestroy... */
+        public void xDestroy(){
+          xDestroyCalled.value = true;
+        }
+      };
+
+    // Register and use the function...
+    int rc = sqlite3_create_function(db, "myfunc", -1, SQLITE_UTF8, func);
+    affirm(0 == rc);
+    affirm(0 == xFuncAccum.value);
+    final sqlite3_stmt stmt = prepare(db, "SELECT myfunc(1,2,3)");
+    int n = 0;
+    while( SQLITE_ROW == sqlite3_step(stmt) ){
+      affirm( 6 == sqlite3_column_int(stmt, 0) );
+      ++n;
+    }
+    sqlite3_finalize(stmt);
+    affirm(1 == n);
+    affirm(6 == xFuncAccum.value);
+    affirm( !xDestroyCalled.value );
+    affirm( null!=neverEverDoThisInClientCode.value );
+    affirm( null!=neverEverDoThisInClientCode2.value );
+    affirm( 0 xFuncAccum = new ValueHolder<>(0);
+
+    SQLFunction funcAgg = new AggregateFunction(){
+        @Override public void xStep(sqlite3_context cx, sqlite3_value[] args){
+          /** Throwing from here should emit loud noise on stdout or stderr
+              but the exception is supressed because we have no way to inform
+              sqlite about it from these callbacks. */
+          //throw new RuntimeException("Throwing from an xStep");
+        }
+        @Override public void xFinal(sqlite3_context cx){
+          throw new RuntimeException("Throwing from an xFinal");
+        }
+      };
+    int rc = sqlite3_create_function(db, "myagg", 1, SQLITE_UTF8, funcAgg);
+    affirm(0 == rc);
+    affirm(0 == xFuncAccum.value);
+    sqlite3_stmt stmt = prepare(db, "SELECT myagg(1)");
+    rc = sqlite3_step(stmt);
+    sqlite3_finalize(stmt);
+    affirm( 0 != rc );
+    affirm( sqlite3_errmsg(db).indexOf("an xFinal") > 0 );
+
+    SQLFunction funcSc = new ScalarFunction(){
+        @Override public void xFunc(sqlite3_context cx, sqlite3_value[] args){
+          throw new RuntimeException("Throwing from an xFunc");
+        }
+      };
+    rc = sqlite3_create_function(db, "mysca", 0, SQLITE_UTF8, funcSc);
+    affirm(0 == rc);
+    affirm(0 == xFuncAccum.value);
+    stmt = prepare(db, "SELECT mysca()");
+    rc = sqlite3_step(stmt);
+    sqlite3_finalize(stmt);
+    affirm( 0 != rc );
+    affirm( sqlite3_errmsg(db).indexOf("an xFunc") > 0 );
+    rc = sqlite3_create_function(db, "mysca", 1, -1, funcSc);
+    affirm( SQLITE_FORMAT==rc, "invalid encoding value." );
+    sqlite3_close_v2(db);
+  }
+
+  @SingleThreadOnly
+  private void testUdfJavaObject(){
+    affirm( !mtMode );
+    final sqlite3 db = createNewDb();
+    final ValueHolder testResult = new ValueHolder<>(db);
+    final ValueHolder boundObj = new ValueHolder<>(42);
+    final SQLFunction func = new ScalarFunction(){
+        public void xFunc(sqlite3_context cx, sqlite3_value args[]){
+          sqlite3_result_java_object(cx, testResult.value);
+          affirm( sqlite3_value_java_object(args[0]) == boundObj );
+        }
+      };
+    int rc = sqlite3_create_function(db, "myfunc", -1, SQLITE_UTF8, func);
+    affirm(0 == rc);
+    sqlite3_stmt stmt = prepare(db, "select myfunc(?)");
+    affirm( 0 != stmt.getNativePointer() );
+    affirm( testResult.value == db );
+    rc = sqlite3_bind_java_object(stmt, 1, boundObj);
+    affirm( 0==rc );
+    int n = 0;
+    if( SQLITE_ROW == sqlite3_step(stmt) ){
+      final sqlite3_value v = sqlite3_column_value(stmt, 0);
+      affirm( testResult.value == sqlite3_value_java_object(v) );
+      affirm( testResult.value == sqlite3_value_java_casted(v, sqlite3.class) );
+      affirm( testResult.value ==
+              sqlite3_value_java_casted(v, testResult.value.getClass()) );
+      affirm( testResult.value == sqlite3_value_java_casted(v, Object.class) );
+      affirm( null == sqlite3_value_java_casted(v, String.class) );
+      ++n;
+    }
+    sqlite3_finalize(stmt);
+    affirm( 1 == n );
+    affirm( 0==sqlite3_db_release_memory(db) );
+    sqlite3_close_v2(db);
+  }
+
+  private void testUdfAggregate(){
+    final sqlite3 db = createNewDb();
+    final ValueHolder xFinalNull =
+      // To confirm that xFinal() is called with no aggregate state
+      // when the corresponding result set is empty.
+      new ValueHolder<>(false);
+    SQLFunction func = new AggregateFunction(){
+        @Override
+        public void xStep(sqlite3_context cx, sqlite3_value[] args){
+          final ValueHolder agg = this.getAggregateState(cx, 0);
+          agg.value += sqlite3_value_int(args[0]);
+          affirm( agg == this.getAggregateState(cx, 0) );
+        }
+        @Override
+        public void xFinal(sqlite3_context cx){
+          final Integer v = this.takeAggregateState(cx);
+          if(null == v){
+            xFinalNull.value = true;
+            sqlite3_result_null(cx);
+          }else{
+            sqlite3_result_int(cx, v);
+          }
+        }
+      };
+    execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES(1),(2),(3)");
+    int rc = sqlite3_create_function(db, "myfunc", 1, SQLITE_UTF8, func);
+    affirm(0 == rc);
+    sqlite3_stmt stmt = prepare(db, "select myfunc(a), myfunc(a+10) from t");
+    affirm( 0==sqlite3_stmt_status(stmt, SQLITE_STMTSTATUS_RUN, false) );
+    int n = 0;
+    if( SQLITE_ROW == sqlite3_step(stmt) ){
+      int v = sqlite3_column_int(stmt, 0);
+      affirm( 6 == v );
+      int v2 = sqlite3_column_int(stmt, 1);
+      affirm( 30+v == v2 );
+      ++n;
+    }
+    affirm( 1==n );
+    affirm(!xFinalNull.value);
+    sqlite3_reset(stmt);
+    affirm( 1==sqlite3_stmt_status(stmt, SQLITE_STMTSTATUS_RUN, false) );
+    // Ensure that the accumulator is reset on subsequent calls...
+    n = 0;
+    if( SQLITE_ROW == sqlite3_step(stmt) ){
+      final int v = sqlite3_column_int(stmt, 0);
+      affirm( 6 == v );
+      ++n;
+    }
+    sqlite3_finalize(stmt);
+    affirm( 1==n );
+
+    stmt = prepare(db, "select myfunc(a), myfunc(a+a) from t order by a");
+    n = 0;
+    while( SQLITE_ROW == sqlite3_step(stmt) ){
+      final int c0 = sqlite3_column_int(stmt, 0);
+      final int c1 = sqlite3_column_int(stmt, 1);
+      ++n;
+      affirm( 6 == c0 );
+      affirm( 12 == c1 );
+    }
+    sqlite3_finalize(stmt);
+    affirm( 1 == n );
+    affirm(!xFinalNull.value);
+
+    execSql(db, "SELECT myfunc(1) WHERE 0");
+    affirm(xFinalNull.value);
+    sqlite3_close_v2(db);
+  }
+
+  private void testUdfWindow(){
+    final sqlite3 db = createNewDb();
+    /* Example window function, table, and results taken from:
+       https://sqlite.org/windowfunctions.html#udfwinfunc */
+    final SQLFunction func = new WindowFunction(){
+
+        private void xStepInverse(sqlite3_context cx, int v){
+          this.getAggregateState(cx,0).value += v;
+        }
+        @Override public void xStep(sqlite3_context cx, sqlite3_value[] args){
+          this.xStepInverse(cx, sqlite3_value_int(args[0]));
+        }
+        @Override public void xInverse(sqlite3_context cx, sqlite3_value[] args){
+          this.xStepInverse(cx, -sqlite3_value_int(args[0]));
+        }
+
+        private void xFinalValue(sqlite3_context cx, Integer v){
+          if(null == v) sqlite3_result_null(cx);
+          else sqlite3_result_int(cx, v);
+        }
+        @Override public void xFinal(sqlite3_context cx){
+          xFinalValue(cx, this.takeAggregateState(cx));
+        }
+        @Override public void xValue(sqlite3_context cx){
+          xFinalValue(cx, this.getAggregateState(cx,null).value);
+        }
+      };
+    int rc = sqlite3_create_function(db, "winsumint", 1, SQLITE_UTF8, func);
+    affirm( 0 == rc );
+    execSql(db, new String[] {
+        "CREATE TEMP TABLE twin(x, y); INSERT INTO twin VALUES",
+        "('a', 4),('b', 5),('c', 3),('d', 8),('e', 1)"
+      });
+    final sqlite3_stmt stmt = prepare(db,
+                         "SELECT x, winsumint(y) OVER ("+
+                         "ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING"+
+                         ") AS sum_y "+
+                         "FROM twin ORDER BY x;");
+    affirm( 0 == rc );
+    int n = 0;
+    while( SQLITE_ROW == sqlite3_step(stmt) ){
+      final String s = sqlite3_column_text16(stmt, 0);
+      final int i = sqlite3_column_int(stmt, 1);
+      switch(++n){
+        case 1: affirm( "a".equals(s) && 9==i ); break;
+        case 2: affirm( "b".equals(s) && 12==i ); break;
+        case 3: affirm( "c".equals(s) && 16==i ); break;
+        case 4: affirm( "d".equals(s) && 12==i ); break;
+        case 5: affirm( "e".equals(s) && 9==i ); break;
+        default: affirm( false /* cannot happen */ );
+      }
+    }
+    sqlite3_finalize(stmt);
+    affirm( 5 == n );
+    sqlite3_close_v2(db);
+  }
+
+  private void listBoundMethods(){
+    if(false){
+      final java.lang.reflect.Field[] declaredFields =
+        CApi.class.getDeclaredFields();
+      outln("Bound constants:\n");
+      for(java.lang.reflect.Field field : declaredFields) {
+        if(java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
+          outln("\t",field.getName());
+        }
+      }
+    }
+    final java.lang.reflect.Method[] declaredMethods =
+      CApi.class.getDeclaredMethods();
+    final java.util.List funcList = new java.util.ArrayList<>();
+    for(java.lang.reflect.Method m : declaredMethods){
+      if((m.getModifiers() & java.lang.reflect.Modifier.STATIC) != 0){
+        final String name = m.getName();
+        if(name.startsWith("sqlite3_")){
+          funcList.add(name);
+        }
+      }
+    }
+    int count = 0;
+    java.util.Collections.sort(funcList);
+    for(String n : funcList){
+      ++count;
+      outln("\t",n,"()");
+    }
+    outln(count," functions named sqlite3_*.");
+  }
+
+  private void testTrace(){
+    final sqlite3 db = createNewDb();
+    final ValueHolder counter = new ValueHolder<>(0);
+    /* Ensure that characters outside of the UTF BMP survive the trip
+       from Java to sqlite3 and back to Java. (At no small efficiency
+       penalty.) */
+    final String nonBmpChar = "😃";
+    int rc = sqlite3_trace_v2(
+      db, SQLITE_TRACE_STMT | SQLITE_TRACE_PROFILE
+          | SQLITE_TRACE_ROW | SQLITE_TRACE_CLOSE,
+      new TraceV2Callback(){
+        @Override public int call(int traceFlag, Object pNative, Object x){
+          ++counter.value;
+          //outln("TRACE "+traceFlag+" pNative = "+pNative.getClass().getName());
+          switch(traceFlag){
+            case SQLITE_TRACE_STMT:
+              affirm(pNative instanceof sqlite3_stmt);
+              //outln("TRACE_STMT sql = "+x);
+              affirm(x instanceof String);
+              affirm( ((String)x).indexOf(nonBmpChar) > 0 );
+              break;
+            case SQLITE_TRACE_PROFILE:
+              affirm(pNative instanceof sqlite3_stmt);
+              affirm(x instanceof Long);
+              //outln("TRACE_PROFILE time = "+x);
+              break;
+            case SQLITE_TRACE_ROW:
+              affirm(pNative instanceof sqlite3_stmt);
+              affirm(null == x);
+              //outln("TRACE_ROW = "+sqlite3_column_text16((sqlite3_stmt)pNative, 0));
+              break;
+            case SQLITE_TRACE_CLOSE:
+              affirm(pNative instanceof sqlite3);
+              affirm(null == x);
+              break;
+            default:
+              affirm(false /*cannot happen*/);
+              break;
+          }
+          return 0;
+        }
+      });
+    affirm( 0==rc );
+    execSql(db, "SELECT coalesce(null,null,'"+nonBmpChar+"'); "+
+            "SELECT 'w"+nonBmpChar+"orld'");
+    affirm( 6 == counter.value );
+    sqlite3_close_v2(db);
+    affirm( 7 == counter.value );
+  }
+
+  @SingleThreadOnly /* because threads inherently break this test */
+  private static void testBusy(){
+    final String dbName = "_busy-handler.db";
+    final OutputPointer.sqlite3 outDb = new OutputPointer.sqlite3();
+    final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt();
+
+    int rc = sqlite3_open(dbName, outDb);
+    ++metrics.dbOpen;
+    affirm( 0 == rc );
+    final sqlite3 db1 = outDb.get();
+    execSql(db1, "CREATE TABLE IF NOT EXISTS t(a)");
+    rc = sqlite3_open(dbName, outDb);
+    ++metrics.dbOpen;
+    affirm( 0 == rc );
+    affirm( outDb.get() != db1 );
+    final sqlite3 db2 = outDb.get();
+
+    affirm( "main".equals( sqlite3_db_name(db1, 0) ) );
+    rc = sqlite3_db_config(db1, SQLITE_DBCONFIG_MAINDBNAME, "foo");
+    affirm( sqlite3_db_filename(db1, "foo").endsWith(dbName) );
+    affirm( "foo".equals( sqlite3_db_name(db1, 0) ) );
+
+    final ValueHolder xBusyCalled = new ValueHolder<>(0);
+    BusyHandlerCallback handler = new BusyHandlerCallback(){
+        @Override public int call(int n){
+          //outln("busy handler #"+n);
+          return n > 2 ? 0 : ++xBusyCalled.value;
+        }
+      };
+    rc = sqlite3_busy_handler(db2, handler);
+    affirm(0 == rc);
+
+    // Force a locked condition...
+    execSql(db1, "BEGIN EXCLUSIVE");
+    rc = sqlite3_prepare_v2(db2, "SELECT * from t", outStmt);
+    affirm( SQLITE_BUSY == rc);
+    affirm( null == outStmt.get() );
+    affirm( 3 == xBusyCalled.value );
+    sqlite3_close_v2(db1);
+    sqlite3_close_v2(db2);
+    try{
+      final java.io.File f = new java.io.File(dbName);
+      f.delete();
+    }catch(Exception e){
+      /* ignore */
+    }
+  }
+
+  private void testProgress(){
+    final sqlite3 db = createNewDb();
+    final ValueHolder counter = new ValueHolder<>(0);
+    sqlite3_progress_handler(db, 1, new ProgressHandlerCallback(){
+        @Override public int call(){
+          ++counter.value;
+          return 0;
+        }
+      });
+    execSql(db, "SELECT 1; SELECT 2;");
+    affirm( counter.value > 0 );
+    int nOld = counter.value;
+    sqlite3_progress_handler(db, 0, null);
+    execSql(db, "SELECT 1; SELECT 2;");
+    affirm( nOld == counter.value );
+    sqlite3_close_v2(db);
+  }
+
+  private void testCommitHook(){
+    final sqlite3 db = createNewDb();
+    final ValueHolder counter = new ValueHolder<>(0);
+    final ValueHolder hookResult = new ValueHolder<>(0);
+    final CommitHookCallback theHook = new CommitHookCallback(){
+        @Override public int call(){
+          ++counter.value;
+          return hookResult.value;
+        }
+      };
+    CommitHookCallback oldHook = sqlite3_commit_hook(db, theHook);
+    affirm( null == oldHook );
+    execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')");
+    affirm( 2 == counter.value );
+    execSql(db, "BEGIN; SELECT 1; SELECT 2; COMMIT;");
+    affirm( 2 == counter.value /* NOT invoked if no changes are made */ );
+    execSql(db, "BEGIN; update t set a='d' where a='c'; COMMIT;");
+    affirm( 3 == counter.value );
+    oldHook = sqlite3_commit_hook(db, theHook);
+    affirm( theHook == oldHook );
+    execSql(db, "BEGIN; update t set a='e' where a='d'; COMMIT;");
+    affirm( 4 == counter.value );
+    oldHook = sqlite3_commit_hook(db, null);
+    affirm( theHook == oldHook );
+    execSql(db, "BEGIN; update t set a='f' where a='e'; COMMIT;");
+    affirm( 4 == counter.value );
+    oldHook = sqlite3_commit_hook(db, null);
+    affirm( null == oldHook );
+    execSql(db, "BEGIN; update t set a='g' where a='f'; COMMIT;");
+    affirm( 4 == counter.value );
+
+    final CommitHookCallback newHook = new CommitHookCallback(){
+        @Override public int call(){return 0;}
+      };
+    oldHook = sqlite3_commit_hook(db, newHook);
+    affirm( null == oldHook );
+    execSql(db, "BEGIN; update t set a='h' where a='g'; COMMIT;");
+    affirm( 4 == counter.value );
+    oldHook = sqlite3_commit_hook(db, theHook);
+    affirm( newHook == oldHook );
+    execSql(db, "BEGIN; update t set a='i' where a='h'; COMMIT;");
+    affirm( 5 == counter.value );
+    hookResult.value = SQLITE_ERROR;
+    int rc = execSql(db, false, "BEGIN; update t set a='j' where a='i'; COMMIT;");
+    affirm( SQLITE_CONSTRAINT == rc );
+    affirm( 6 == counter.value );
+    sqlite3_close_v2(db);
+  }
+
+  private void testUpdateHook(){
+    final sqlite3 db = createNewDb();
+    final ValueHolder counter = new ValueHolder<>(0);
+    final ValueHolder expectedOp = new ValueHolder<>(0);
+    final UpdateHookCallback theHook = new UpdateHookCallback(){
+        @Override
+        public void call(int opId, String dbName, String tableName, long rowId){
+          ++counter.value;
+          if( 0!=expectedOp.value ){
+            affirm( expectedOp.value == opId );
+          }
+        }
+      };
+    UpdateHookCallback oldHook = sqlite3_update_hook(db, theHook);
+    affirm( null == oldHook );
+    expectedOp.value = SQLITE_INSERT;
+    execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')");
+    affirm( 3 == counter.value );
+    expectedOp.value = SQLITE_UPDATE;
+    execSql(db, "update t set a='d' where a='c';");
+    affirm( 4 == counter.value );
+    oldHook = sqlite3_update_hook(db, theHook);
+    affirm( theHook == oldHook );
+    expectedOp.value = SQLITE_DELETE;
+    execSql(db, "DELETE FROM t where a='d'");
+    affirm( 5 == counter.value );
+    oldHook = sqlite3_update_hook(db, null);
+    affirm( theHook == oldHook );
+    execSql(db, "update t set a='e' where a='b';");
+    affirm( 5 == counter.value );
+    oldHook = sqlite3_update_hook(db, null);
+    affirm( null == oldHook );
+
+    final UpdateHookCallback newHook = new UpdateHookCallback(){
+        @Override public void call(int opId, String dbName, String tableName, long rowId){
+        }
+      };
+    oldHook = sqlite3_update_hook(db, newHook);
+    affirm( null == oldHook );
+    execSql(db, "update t set a='h' where a='a'");
+    affirm( 5 == counter.value );
+    oldHook = sqlite3_update_hook(db, theHook);
+    affirm( newHook == oldHook );
+    expectedOp.value = SQLITE_UPDATE;
+    execSql(db, "update t set a='i' where a='h'");
+    affirm( 6 == counter.value );
+    sqlite3_close_v2(db);
+  }
+
+  /**
+     This test is functionally identical to testUpdateHook(), only with a
+     different callback type.
+  */
+  private void testPreUpdateHook(){
+    if( !sqlite3_compileoption_used("ENABLE_PREUPDATE_HOOK") ){
+      //outln("Skipping testPreUpdateHook(): no pre-update hook support.");
+      return;
+    }
+    final sqlite3 db = createNewDb();
+    final ValueHolder counter = new ValueHolder<>(0);
+    final ValueHolder expectedOp = new ValueHolder<>(0);
+    final PreupdateHookCallback theHook = new PreupdateHookCallback(){
+        @Override
+        public void call(sqlite3 db, int opId, String dbName, String dbTable,
+                         long iKey1, long iKey2 ){
+          ++counter.value;
+          switch( opId ){
+            case SQLITE_UPDATE:
+              affirm( 0 < sqlite3_preupdate_count(db) );
+              affirm( null != sqlite3_preupdate_new(db, 0) );
+              affirm( null != sqlite3_preupdate_old(db, 0) );
+              break;
+            case SQLITE_INSERT:
+              affirm( null != sqlite3_preupdate_new(db, 0) );
+              break;
+            case SQLITE_DELETE:
+              affirm( null != sqlite3_preupdate_old(db, 0) );
+              break;
+            default:
+              break;
+          }
+          if( 0!=expectedOp.value ){
+            affirm( expectedOp.value == opId );
+          }
+        }
+      };
+    PreupdateHookCallback oldHook = sqlite3_preupdate_hook(db, theHook);
+    affirm( null == oldHook );
+    expectedOp.value = SQLITE_INSERT;
+    execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')");
+    affirm( 3 == counter.value );
+    expectedOp.value = SQLITE_UPDATE;
+    execSql(db, "update t set a='d' where a='c';");
+    affirm( 4 == counter.value );
+    oldHook = sqlite3_preupdate_hook(db, theHook);
+    affirm( theHook == oldHook );
+    expectedOp.value = SQLITE_DELETE;
+    execSql(db, "DELETE FROM t where a='d'");
+    affirm( 5 == counter.value );
+    oldHook = sqlite3_preupdate_hook(db, null);
+    affirm( theHook == oldHook );
+    execSql(db, "update t set a='e' where a='b';");
+    affirm( 5 == counter.value );
+    oldHook = sqlite3_preupdate_hook(db, null);
+    affirm( null == oldHook );
+
+    final PreupdateHookCallback newHook = new PreupdateHookCallback(){
+        @Override
+        public void call(sqlite3 db, int opId, String dbName,
+                         String tableName, long iKey1, long iKey2){
+        }
+      };
+    oldHook = sqlite3_preupdate_hook(db, newHook);
+    affirm( null == oldHook );
+    execSql(db, "update t set a='h' where a='a'");
+    affirm( 5 == counter.value );
+    oldHook = sqlite3_preupdate_hook(db, theHook);
+    affirm( newHook == oldHook );
+    expectedOp.value = SQLITE_UPDATE;
+    execSql(db, "update t set a='i' where a='h'");
+    affirm( 6 == counter.value );
+
+    sqlite3_close_v2(db);
+  }
+
+  private void testRollbackHook(){
+    final sqlite3 db = createNewDb();
+    final ValueHolder counter = new ValueHolder<>(0);
+    final RollbackHookCallback theHook = new RollbackHookCallback(){
+        @Override public void call(){
+          ++counter.value;
+        }
+      };
+    RollbackHookCallback oldHook = sqlite3_rollback_hook(db, theHook);
+    affirm( null == oldHook );
+    execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')");
+    affirm( 0 == counter.value );
+    execSql(db, false, "BEGIN; SELECT 1; SELECT 2; ROLLBACK;");
+    affirm( 1 == counter.value /* contra to commit hook, is invoked if no changes are made */ );
+
+    final RollbackHookCallback newHook = new RollbackHookCallback(){
+        @Override public void call(){return;}
+      };
+    oldHook = sqlite3_rollback_hook(db, newHook);
+    affirm( theHook == oldHook );
+    execSql(db, false, "BEGIN; SELECT 1; ROLLBACK;");
+    affirm( 1 == counter.value );
+    oldHook = sqlite3_rollback_hook(db, theHook);
+    affirm( newHook == oldHook );
+    execSql(db, false, "BEGIN; SELECT 1; ROLLBACK;");
+    affirm( 2 == counter.value );
+    int rc = execSql(db, false, "BEGIN; SELECT 1; ROLLBACK;");
+    affirm( 0 == rc );
+    affirm( 3 == counter.value );
+    sqlite3_close_v2(db);
+  }
+
+  /**
+     If FTS5 is available, runs FTS5 tests, else returns with no side
+     effects. If it is available but loading of the FTS5 bits fails,
+     it throws.
+  */
+  @SuppressWarnings("unchecked")
+  @SingleThreadOnly /* because the Fts5 parts are not yet known to be
+                       thread-safe */
+  private void testFts5() throws Exception {
+    if( !sqlite3_compileoption_used("ENABLE_FTS5") ){
+      //outln("SQLITE_ENABLE_FTS5 is not set. Skipping FTS5 tests.");
+      return;
+    }
+    Exception err = null;
+    try {
+      Class t = Class.forName("org.sqlite.jni.fts5.TesterFts5");
+      java.lang.reflect.Constructor ctor = t.getConstructor();
+      ctor.setAccessible(true);
+      final long timeStart = System.currentTimeMillis();
+      ctor.newInstance() /* will run all tests */;
+      final long timeEnd = System.currentTimeMillis();
+      outln("FTS5 Tests done in ",(timeEnd - timeStart),"ms");
+    }catch(ClassNotFoundException e){
+      outln("FTS5 classes not loaded.");
+      err = e;
+    }catch(NoSuchMethodException e){
+      outln("FTS5 tester ctor not found.");
+      err = e;
+    }catch(Exception e){
+      outln("Instantiation of FTS5 tester threw.");
+      err = e;
+    }
+    if( null != err ){
+      outln("Exception: "+err);
+      err.printStackTrace();
+      throw err;
+    }
+  }
+
+  private void testAuthorizer(){
+    final sqlite3 db = createNewDb();
+    final ValueHolder counter = new ValueHolder<>(0);
+    final ValueHolder authRc = new ValueHolder<>(0);
+    final AuthorizerCallback auth = new AuthorizerCallback(){
+        public int call(int op, String s0, String s1, String s2, String s3){
+          ++counter.value;
+          //outln("xAuth(): "+s0+" "+s1+" "+s2+" "+s3);
+          return authRc.value;
+        }
+      };
+    execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')");
+    sqlite3_set_authorizer(db, auth);
+    execSql(db, "UPDATE t SET a=1");
+    affirm( 1 == counter.value );
+    authRc.value = SQLITE_DENY;
+    int rc = execSql(db, false, "UPDATE t SET a=2");
+    affirm( SQLITE_AUTH==rc );
+    // TODO: expand these tests considerably
+    sqlite3_close(db);
+  }
+
+  @SingleThreadOnly /* because multiple threads legitimately make these
+                       results unpredictable */
+  private synchronized void testAutoExtension(){
+    final ValueHolder val = new ValueHolder<>(0);
+    final ValueHolder toss = new ValueHolder<>(null);
+    final AutoExtensionCallback ax = new AutoExtensionCallback(){
+        @Override public int call(sqlite3 db){
+          ++val.value;
+          if( null!=toss.value ){
+            throw new RuntimeException(toss.value);
+          }
+          return 0;
+        }
+      };
+    int rc = sqlite3_auto_extension( ax );
+    affirm( 0==rc );
+    sqlite3_close(createNewDb());
+    affirm( 1==val.value );
+    sqlite3_close(createNewDb());
+    affirm( 2==val.value );
+    sqlite3_reset_auto_extension();
+    sqlite3_close(createNewDb());
+    affirm( 2==val.value );
+    rc = sqlite3_auto_extension( ax );
+    affirm( 0==rc );
+    // Must not add a new entry
+    rc = sqlite3_auto_extension( ax );
+    affirm( 0==rc );
+    sqlite3_close( createNewDb() );
+    affirm( 3==val.value );
+
+    sqlite3 db = createNewDb();
+    affirm( 4==val.value );
+    execSql(db, "ATTACH ':memory:' as foo");
+    affirm( 4==val.value, "ATTACH uses the same connection, not sub-connections." );
+    sqlite3_close(db);
+    db = null;
+
+    affirm( sqlite3_cancel_auto_extension(ax) );
+    affirm( !sqlite3_cancel_auto_extension(ax) );
+    sqlite3_close(createNewDb());
+    affirm( 4==val.value );
+    rc = sqlite3_auto_extension( ax );
+    affirm( 0==rc );
+    Exception err = null;
+    toss.value = "Throwing from auto_extension.";
+    try{
+      sqlite3_close(createNewDb());
+    }catch(Exception e){
+      err = e;
+    }
+    affirm( err!=null );
+    affirm( err.getMessage().indexOf(toss.value)>0 );
+    toss.value = null;
+
+    val.value = 0;
+    final AutoExtensionCallback ax2 = new AutoExtensionCallback(){
+        @Override public synchronized int call(sqlite3 db){
+          ++val.value;
+          return 0;
+        }
+      };
+    rc = sqlite3_auto_extension( ax2 );
+    affirm( 0 == rc );
+    sqlite3_close(createNewDb());
+    affirm( 2 == val.value );
+    affirm( sqlite3_cancel_auto_extension(ax) );
+    affirm( !sqlite3_cancel_auto_extension(ax) );
+    sqlite3_close(createNewDb());
+    affirm( 3 == val.value );
+    rc = sqlite3_auto_extension( ax );
+    affirm( 0 == rc );
+    sqlite3_close(createNewDb());
+    affirm( 5 == val.value );
+    affirm( sqlite3_cancel_auto_extension(ax2) );
+    affirm( !sqlite3_cancel_auto_extension(ax2) );
+    sqlite3_close(createNewDb());
+    affirm( 6 == val.value );
+    rc = sqlite3_auto_extension( ax2 );
+    affirm( 0 == rc );
+    sqlite3_close(createNewDb());
+    affirm( 8 == val.value );
+
+    sqlite3_reset_auto_extension();
+    sqlite3_close(createNewDb());
+    affirm( 8 == val.value );
+    affirm( !sqlite3_cancel_auto_extension(ax) );
+    affirm( !sqlite3_cancel_auto_extension(ax2) );
+    sqlite3_close(createNewDb());
+    affirm( 8 == val.value );
+  }
+
+
+  private void testColumnMetadata(){
+    final sqlite3 db = createNewDb();
+    execSql(db, new String[] {
+        "CREATE TABLE t(a duck primary key not null collate noCase); ",
+        "INSERT INTO t(a) VALUES(1),(2),(3);"
+      });
+    OutputPointer.Bool bNotNull = new OutputPointer.Bool();
+    OutputPointer.Bool bPrimaryKey = new OutputPointer.Bool();
+    OutputPointer.Bool bAutoinc = new OutputPointer.Bool();
+    OutputPointer.String zCollSeq = new OutputPointer.String();
+    OutputPointer.String zDataType = new OutputPointer.String();
+    int rc = sqlite3_table_column_metadata(
+      db, "main", "t", "a", zDataType, zCollSeq,
+      bNotNull, bPrimaryKey, bAutoinc);
+    affirm( 0==rc );
+    affirm( bPrimaryKey.value );
+    affirm( !bAutoinc.value );
+    affirm( bNotNull.value );
+    affirm( "noCase".equals(zCollSeq.value) );
+    affirm( "duck".equals(zDataType.value) );
+
+    TableColumnMetadata m =
+      sqlite3_table_column_metadata(db, "main", "t", "a");
+    affirm( null != m );
+    affirm( bPrimaryKey.value == m.isPrimaryKey() );
+    affirm( bAutoinc.value == m.isAutoincrement() );
+    affirm( bNotNull.value == m.isNotNull() );
+    affirm( zCollSeq.value.equals(m.getCollation()) );
+    affirm( zDataType.value.equals(m.getDataType()) );
+
+    affirm( null == sqlite3_table_column_metadata(db, "nope", "t", "a") );
+    affirm( null == sqlite3_table_column_metadata(db, "main", "nope", "a") );
+
+    m = sqlite3_table_column_metadata(db, "main", "t", null)
+      /* Check only for existence of table */;
+    affirm( null != m );
+    affirm( m.isPrimaryKey() );
+    affirm( !m.isAutoincrement() );
+    affirm( !m.isNotNull() );
+    affirm( "BINARY".equalsIgnoreCase(m.getCollation()) );
+    affirm( "INTEGER".equalsIgnoreCase(m.getDataType()) );
+
+    sqlite3_close_v2(db);
+  }
+
+  private void testTxnState(){
+    final sqlite3 db = createNewDb();
+    affirm( SQLITE_TXN_NONE == sqlite3_txn_state(db, null) );
+    affirm( sqlite3_get_autocommit(db) );
+    execSql(db, "BEGIN;");
+    affirm( !sqlite3_get_autocommit(db) );
+    affirm( SQLITE_TXN_NONE == sqlite3_txn_state(db, null) );
+    execSql(db, "SELECT * FROM sqlite_schema;");
+    affirm( SQLITE_TXN_READ == sqlite3_txn_state(db, "main") );
+    execSql(db, "CREATE TABLE t(a);");
+    affirm( SQLITE_TXN_WRITE ==  sqlite3_txn_state(db, null) );
+    execSql(db, "ROLLBACK;");
+    affirm( SQLITE_TXN_NONE == sqlite3_txn_state(db, null) );
+    sqlite3_close_v2(db);
+  }
+
+
+  private void testExplain(){
+    final sqlite3 db = createNewDb();
+    sqlite3_stmt stmt = prepare(db,"SELECT 1");
+
+    affirm( 0 == sqlite3_stmt_isexplain(stmt) );
+    int rc = sqlite3_stmt_explain(stmt, 1);
+    affirm( 1 == sqlite3_stmt_isexplain(stmt) );
+    rc = sqlite3_stmt_explain(stmt, 2);
+    affirm( 2 == sqlite3_stmt_isexplain(stmt) );
+    sqlite3_finalize(stmt);
+    sqlite3_close_v2(db);
+  }
+
+  private void testLimit(){
+    final sqlite3 db = createNewDb();
+    int v;
+
+    v = sqlite3_limit(db, SQLITE_LIMIT_LENGTH, -1);
+    affirm( v > 0 );
+    affirm( v == sqlite3_limit(db, SQLITE_LIMIT_LENGTH, v-1) );
+    affirm( v-1 == sqlite3_limit(db, SQLITE_LIMIT_LENGTH, -1) );
+    sqlite3_close_v2(db);
+  }
+
+  private void testComplete(){
+    affirm( 0==sqlite3_complete("select 1") );
+    affirm( 0!=sqlite3_complete("select 1;") );
+    affirm( 0!=sqlite3_complete("nope 'nope' 'nope' 1;"), "Yup" );
+  }
+
+  private void testKeyword(){
+    final int n = sqlite3_keyword_count();
+    affirm( n>0 );
+    affirm( !sqlite3_keyword_check("_nope_") );
+    affirm( sqlite3_keyword_check("seLect") );
+    affirm( null!=sqlite3_keyword_name(0) );
+    affirm( null!=sqlite3_keyword_name(n-1) );
+    affirm( null==sqlite3_keyword_name(n) );
+  }
+
+  private void testBackup(){
+    final sqlite3 dbDest = createNewDb();
+
+    try (sqlite3 dbSrc = createNewDb()) {
+      execSql(dbSrc, new String[]{
+          "pragma page_size=512; VACUUM;",
+          "create table t(a);",
+          "insert into t(a) values(1),(2),(3);"
+        });
+      affirm( null==sqlite3_backup_init(dbSrc,"main",dbSrc,"main") );
+      try (sqlite3_backup b = sqlite3_backup_init(dbDest,"main",dbSrc,"main")) {
+        affirm( null!=b );
+        affirm( b.getNativePointer()!=0 );
+        int rc;
+        while( SQLITE_DONE!=(rc = sqlite3_backup_step(b, 1)) ){
+          affirm( 0==rc );
+        }
+        affirm( sqlite3_backup_pagecount(b) > 0 );
+        rc = sqlite3_backup_finish(b);
+        affirm( 0==rc );
+        affirm( b.getNativePointer()==0 );
+      }
+    }
+
+    try (sqlite3_stmt stmt = prepare(dbDest,"SELECT sum(a) from t")) {
+      sqlite3_step(stmt);
+      affirm( sqlite3_column_int(stmt,0) == 6 );
+    }
+    sqlite3_close_v2(dbDest);
+  }
+
+  private void testRandomness(){
+    byte[] foo = new byte[20];
+    int i = 0;
+    for( byte b : foo ){
+      i += b;
+    }
+    affirm( i==0 );
+    sqlite3_randomness(foo);
+    for( byte b : foo ){
+      if(b!=0) ++i;
+    }
+    affirm( i!=0, "There's a very slight chance that 0 is actually correct." );
+  }
+
+  private void testBlobOpen(){
+    final sqlite3 db = createNewDb();
+
+    execSql(db, "CREATE TABLE T(a BLOB);"
+            +"INSERT INTO t(rowid,a) VALUES(1, 'def'),(2, 'XYZ');"
+    );
+    final OutputPointer.sqlite3_blob pOut = new OutputPointer.sqlite3_blob();
+    int rc = sqlite3_blob_open(db, "main", "t", "a",
+                               sqlite3_last_insert_rowid(db), 1, pOut);
+    affirm( 0==rc );
+    sqlite3_blob b = pOut.take();
+    affirm( null!=b );
+    affirm( 0!=b.getNativePointer() );
+    affirm( 3==sqlite3_blob_bytes(b) );
+    rc = sqlite3_blob_write( b, new byte[] {100, 101, 102 /*"DEF"*/}, 0);
+    affirm( 0==rc );
+    rc = sqlite3_blob_close(b);
+    affirm( 0==rc );
+    rc = sqlite3_blob_close(b);
+    affirm( 0!=rc );
+    affirm( 0==b.getNativePointer() );
+    sqlite3_stmt stmt = prepare(db,"SELECT length(a), a FROM t ORDER BY a");
+    affirm( SQLITE_ROW == sqlite3_step(stmt) );
+    affirm( 3 == sqlite3_column_int(stmt,0) );
+    affirm( "def".equals(sqlite3_column_text16(stmt,1)) );
+    sqlite3_finalize(stmt);
+
+    b = sqlite3_blob_open(db, "main", "t", "a",
+                          sqlite3_last_insert_rowid(db), 1);
+    affirm( null!=b );
+    rc = sqlite3_blob_reopen(b, 2);
+    affirm( 0==rc );
+    final byte[] tgt = new byte[3];
+    rc = sqlite3_blob_read(b, tgt, 0);
+    affirm( 0==rc );
+    affirm( 100==tgt[0] && 101==tgt[1] && 102==tgt[2], "DEF" );
+    rc = sqlite3_blob_close(b);
+    affirm( 0==rc );
+    sqlite3_close_v2(db);
+  }
+
+  private void testPrepareMulti(){
+    final sqlite3 db = createNewDb();
+    final String[] sql = {
+      "create table t(","a)",
+      "; insert into t(a) values(1),(2),(3);",
+      "select a from t;"
+    };
+    final List liStmt = new ArrayList();
+    final PrepareMultiCallback proxy = new PrepareMultiCallback.StepAll();
+    PrepareMultiCallback m = new PrepareMultiCallback() {
+        @Override public int call(sqlite3_stmt st){
+          liStmt.add(st);
+          return proxy.call(st);
+        }
+      };
+    int rc = sqlite3_prepare_multi(db, sql, m);
+    affirm( 0==rc );
+    affirm( liStmt.size() == 3 );
+    for( sqlite3_stmt st : liStmt ){
+      sqlite3_finalize(st);
+    }
+    sqlite3_close_v2(db);
+  }
+
+  /* Copy/paste/rename this to add new tests. */
+  private void _testTemplate(){
+    final sqlite3 db = createNewDb();
+    sqlite3_stmt stmt = prepare(db,"SELECT 1");
+    sqlite3_finalize(stmt);
+    sqlite3_close_v2(db);
+  }
+
+
+  @ManualTest /* we really only want to run this test manually */
+  private void testSleep(){
+    out("Sleeping briefly... ");
+    sqlite3_sleep(600);
+    outln("Woke up.");
+  }
+
+  private void nap() throws InterruptedException {
+    if( takeNaps ){
+      Thread.sleep(java.util.concurrent.ThreadLocalRandom.current().nextInt(3, 17), 0);
+    }
+  }
+
+  @ManualTest /* because we only want to run this test on demand */
+  private void testFail(){
+    affirm( false, "Intentional failure." );
+  }
+
+  private void runTests(boolean fromThread) throws Exception {
+    if(false) showCompileOption();
+    List mlist = testMethods;
+    affirm( null!=mlist );
+    if( shuffle ){
+      mlist = new ArrayList<>( testMethods.subList(0, testMethods.size()) );
+      java.util.Collections.shuffle(mlist);
+    }
+    if( listRunTests ){
+      synchronized(this.getClass()){
+        if( !fromThread ){
+          out("Initial test"," list: ");
+          for(java.lang.reflect.Method m : testMethods){
+            out(m.getName()+" ");
+          }
+          outln();
+          outln("(That list excludes some which are hard-coded to run.)");
+        }
+        out("Running"," tests: ");
+        for(java.lang.reflect.Method m : mlist){
+          out(m.getName()+" ");
+        }
+        outln();
+      }
+    }
+    for(java.lang.reflect.Method m : mlist){
+      nap();
+      try{
+        m.invoke(this);
+      }catch(java.lang.reflect.InvocationTargetException e){
+        outln("FAILURE: ",m.getName(),"(): ", e.getCause());
+        throw e;
+      }
+    }
+    synchronized( this.getClass() ){
+      ++nTestRuns;
+    }
+  }
+
+  public void run() {
+    try {
+      runTests(0!=this.tId);
+    }catch(Exception e){
+      synchronized( listErrors ){
+        listErrors.add(e);
+      }
+    }finally{
+      affirm( sqlite3_java_uncache_thread() );
+      affirm( !sqlite3_java_uncache_thread() );
+    }
+  }
+
+  /**
+     Runs the basic sqlite3 JNI binding sanity-check suite.
+
+     CLI flags:
+
+     -q|-quiet: disables most test output.
+
+     -t|-thread N: runs the tests in N threads
+      concurrently. Default=1.
+
+     -r|-repeat N: repeats the tests in a loop N times, each one
+      consisting of the -thread value's threads.
+
+     -shuffle: randomizes the order of most of the test functions.
+
+     -naps: sleep small random intervals between tests in order to add
+     some chaos for cross-thread contention.
+
+     -list-tests: outputs the list of tests being run, minus some
+      which are hard-coded. This is noisy in multi-threaded mode.
+
+     -fail: forces an exception to be thrown during the test run.  Use
+     with -shuffle to make its appearance unpredictable.
+
+     -v: emit some developer-mode info at the end.
+  */
+  public static void main(String[] args) throws Exception {
+    Integer nThread = 1;
+    boolean doSomethingForDev = false;
+    Integer nRepeat = 1;
+    boolean forceFail = false;
+    boolean sqlLog = false;
+    boolean configLog = false;
+    boolean squelchTestOutput = false;
+    for( int i = 0; i < args.length; ){
+      String arg = args[i++];
+      if(arg.startsWith("-")){
+        arg = arg.replaceFirst("-+","");
+        if(arg.equals("v")){
+          doSomethingForDev = true;
+          //listBoundMethods();
+        }else if(arg.equals("t") || arg.equals("thread")){
+          nThread = Integer.parseInt(args[i++]);
+        }else if(arg.equals("r") || arg.equals("repeat")){
+          nRepeat = Integer.parseInt(args[i++]);
+        }else if(arg.equals("shuffle")){
+          shuffle = true;
+        }else if(arg.equals("list-tests")){
+          listRunTests = true;
+        }else if(arg.equals("fail")){
+          forceFail = true;
+        }else if(arg.equals("sqllog")){
+          sqlLog = true;
+        }else if(arg.equals("configlog")){
+          configLog = true;
+        }else if(arg.equals("naps")){
+          takeNaps = true;
+        }else if(arg.equals("q") || arg.equals("quiet")){
+          squelchTestOutput = true;
+        }else{
+          throw new IllegalArgumentException("Unhandled flag:"+arg);
+        }
+      }
+    }
+
+    if( sqlLog ){
+      if( sqlite3_compileoption_used("ENABLE_SQLLOG") ){
+        final ConfigSqllogCallback log = new ConfigSqllogCallback() {
+            @Override public void call(sqlite3 db, String msg, int op){
+              switch(op){
+                case 0: outln("Opening db: ",db); break;
+                case 1: outln("SQL ",db,": ",msg); break;
+                case 2: outln("Closing db: ",db); break;
+              }
+            }
+          };
+        int rc = sqlite3_config( log );
+        affirm( 0==rc );
+        rc = sqlite3_config( (ConfigSqllogCallback)null );
+        affirm( 0==rc );
+        rc = sqlite3_config( log );
+        affirm( 0==rc );
+      }else{
+        outln("WARNING: -sqllog is not active because library was built ",
+              "without SQLITE_ENABLE_SQLLOG.");
+      }
+    }
+    if( configLog ){
+      final ConfigLogCallback log = new ConfigLogCallback() {
+          @Override public void call(int code, String msg){
+            outln("ConfigLogCallback: ",ResultCode.getEntryForInt(code),": ", msg);
+          };
+        };
+      int rc = sqlite3_config( log );
+      affirm( 0==rc );
+      rc = sqlite3_config( (ConfigLogCallback)null );
+      affirm( 0==rc );
+      rc = sqlite3_config( log );
+      affirm( 0==rc );
+    }
+
+    quietMode = squelchTestOutput;
+    outln("If you just saw warning messages regarding CallStaticObjectMethod, ",
+          "you are very likely seeing the side effects of a known openjdk8 ",
+          "bug. It is unsightly but does not affect the library.");
+
+    {
+      // Build list of tests to run from the methods named test*().
+      testMethods = new ArrayList<>();
+      int nSkipped = 0;
+      for(final java.lang.reflect.Method m : Tester1.class.getDeclaredMethods()){
+        final String name = m.getName();
+        if( name.equals("testFail") ){
+          if( forceFail ){
+            testMethods.add(m);
+          }
+        }else if( !m.isAnnotationPresent( ManualTest.class ) ){
+          if( nThread>1 && m.isAnnotationPresent( SingleThreadOnly.class ) ){
+            if( 0==nSkipped++ ){
+              out("Skipping tests in multi-thread mode:");
+            }
+            out(" "+name+"()");
+          }else if( name.startsWith("test") ){
+            testMethods.add(m);
+          }
+        }
+      }
+      if( nSkipped>0 ) out("\n");
+    }
+
+    final long timeStart = System.currentTimeMillis();
+    int nLoop = 0;
+    switch( sqlite3_threadsafe() ){ /* Sanity checking */
+      case 0:
+        affirm( SQLITE_ERROR==sqlite3_config( SQLITE_CONFIG_SINGLETHREAD ),
+                "Could not switch to single-thread mode." );
+        affirm( SQLITE_ERROR==sqlite3_config( SQLITE_CONFIG_MULTITHREAD ),
+                "Could switch to multithread mode."  );
+        affirm( SQLITE_ERROR==sqlite3_config( SQLITE_CONFIG_SERIALIZED ),
+                "Could not switch to serialized threading mode."  );
+        outln("This is a single-threaded build. Not using threads.");
+        nThread = 1;
+        break;
+      case 1:
+      case 2:
+        affirm( 0==sqlite3_config( SQLITE_CONFIG_SINGLETHREAD ),
+                "Could not switch to single-thread mode." );
+        affirm( 0==sqlite3_config( SQLITE_CONFIG_MULTITHREAD ),
+                "Could not switch to multithread mode."  );
+        affirm( 0==sqlite3_config( SQLITE_CONFIG_SERIALIZED ),
+                "Could not switch to serialized threading mode."  );
+        break;
+      default:
+        affirm( false, "Unhandled SQLITE_THREADSAFE value." );
+    }
+    outln("libversion_number: ",
+          sqlite3_libversion_number(),"\n",
+          sqlite3_libversion(),"\n",SQLITE_SOURCE_ID,"\n",
+          "SQLITE_THREADSAFE=",sqlite3_threadsafe());
+    final boolean showLoopCount = (nRepeat>1 && nThread>1);
+    if( showLoopCount ){
+      outln("Running ",nRepeat," loop(s) with ",nThread," thread(s) each.");
+    }
+    if( takeNaps ) outln("Napping between tests is enabled.");
+    for( int n = 0; n < nRepeat; ++n ){
+      ++nLoop;
+      if( showLoopCount ) out((1==nLoop ? "" : " ")+nLoop);
+      if( nThread<=1 ){
+        new Tester1(0).runTests(false);
+        continue;
+      }
+      Tester1.mtMode = true;
+      final ExecutorService ex = Executors.newFixedThreadPool( nThread );
+      for( int i = 0; i < nThread; ++i ){
+        ex.submit( new Tester1(i), i );
+      }
+      ex.shutdown();
+      try{
+        ex.awaitTermination(nThread*200, java.util.concurrent.TimeUnit.MILLISECONDS);
+        ex.shutdownNow();
+      }catch (InterruptedException ie){
+        ex.shutdownNow();
+        Thread.currentThread().interrupt();
+      }
+      if( !listErrors.isEmpty() ){
+        quietMode = false;
+        outln("TEST ERRORS:");
+        Exception err = null;
+        for( Exception e : listErrors ){
+          e.printStackTrace();
+          if( null==err ) err = e;
+        }
+        if( null!=err ) throw err;
+      }
+    }
+    if( showLoopCount ) outln();
+    quietMode = false;
+
+    final long timeEnd = System.currentTimeMillis();
+    outln("Tests done. Metrics across ",nTestRuns," total iteration(s):");
+    outln("\tAssertions checked: ",affirmCount);
+    outln("\tDatabases opened: ",metrics.dbOpen);
+    if( doSomethingForDev ){
+      sqlite3_jni_internal_details();
+    }
+    affirm( 0==sqlite3_release_memory(1) );
+    sqlite3_shutdown();
+    int nMethods = 0;
+    int nNatives = 0;
+    final java.lang.reflect.Method[] declaredMethods =
+      CApi.class.getDeclaredMethods();
+    for(java.lang.reflect.Method m : declaredMethods){
+      final int mod = m.getModifiers();
+      if( 0!=(mod & java.lang.reflect.Modifier.STATIC) ){
+        final String name = m.getName();
+        if(name.startsWith("sqlite3_")){
+          ++nMethods;
+          if( 0!=(mod & java.lang.reflect.Modifier.NATIVE) ){
+            ++nNatives;
+          }
+        }
+      }
+    }
+    outln("\tCApi.sqlite3_*() methods: "+
+          nMethods+" total, with "+
+          nNatives+" native, "+
+          (nMethods - nNatives)+" Java"
+    );
+    outln("\tTotal test time = "
+          +(timeEnd - timeStart)+"ms");
+  }
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/TraceV2Callback.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/TraceV2Callback.java
new file mode 100644
index 00000000..56465a2c
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/TraceV2Callback.java
@@ -0,0 +1,50 @@
+/*
+** 2023-08-25
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+import org.sqlite.jni.annotation.Nullable;
+
+/**
+   Callback for use with {@link CApi#sqlite3_trace_v2}.
+*/
+public interface TraceV2Callback extends CallbackProxy {
+  /**
+     Called by sqlite3 for various tracing operations, as per
+     sqlite3_trace_v2(). Note that this interface elides the 2nd
+     argument to the native trace callback, as that role is better
+     filled by instance-local state.
+
+     These callbacks may throw, in which case their exceptions are
+     converted to C-level error information.
+
+     
The 2nd argument to this function, if non-null, will be a an
+     sqlite3 or sqlite3_stmt object, depending on the first argument
+     (see below).
+
+     
The final argument to this function is the "X" argument
+     documented for sqlite3_trace() and sqlite3_trace_v2(). Its type
+     depends on value of the first argument:
+
+     
- SQLITE_TRACE_STMT: pNative is a sqlite3_stmt. pX is a String
+     containing the prepared SQL.
+
+     
- SQLITE_TRACE_PROFILE: pNative is a sqlite3_stmt. pX is a Long
+     holding an approximate number of nanoseconds the statement took
+     to run.
+
+     
- SQLITE_TRACE_ROW: pNative is a sqlite3_stmt. pX is null.
+
+     
- SQLITE_TRACE_CLOSE: pNative is a sqlite3. pX is null.
+  */
+  int call(int traceFlag, Object pNative, @Nullable Object pX);
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/UpdateHookCallback.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/UpdateHookCallback.java
new file mode 100644
index 00000000..33d72a5d
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/UpdateHookCallback.java
@@ -0,0 +1,25 @@
+/*
+** 2023-08-25
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   Callback for use with {@link CApi#sqlite3_update_hook}.
+*/
+public interface UpdateHookCallback extends CallbackProxy {
+  /**
+     Must function as described for the C-level sqlite3_update_hook()
+     callback.
+  */
+  void call(int opId, String dbName, String tableName, long rowId);
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/ValueHolder.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/ValueHolder.java
new file mode 100644
index 00000000..b3f03ac8
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/ValueHolder.java
@@ -0,0 +1,25 @@
+/*
+** 2023-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.
+**
+*************************************************************************
+** This file contains a set of tests for the sqlite3 JNI bindings.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   A helper class which simply holds a single value. Its primary use
+   is for communicating values out of anonymous classes, as doing so
+   requires a "final" reference.
+*/
+public class ValueHolder {
+  public T value;
+  public ValueHolder(){}
+  public ValueHolder(T v){value = v;}
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/WindowFunction.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/WindowFunction.java
new file mode 100644
index 00000000..eaf1bb9a
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/WindowFunction.java
@@ -0,0 +1,39 @@
+/*
+** 2023-08-25
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+
+/**
+   A SQLFunction implementation for window functions.  Note that
+   WindowFunction inherits from {@link AggregateFunction} and each
+   instance is required to implement the inherited abstract methods
+   from that class. See {@link AggregateFunction} for information on
+   managing the UDF's invocation-specific state.
+*/
+public abstract class WindowFunction extends AggregateFunction {
+
+  /**
+     As for the xInverse() argument of the C API's
+     sqlite3_create_window_function(). If this function throws, the
+     exception is not propagated and a warning might be emitted
+     to a debugging channel.
+  */
+  public abstract void xInverse(sqlite3_context cx, sqlite3_value[] args);
+
+  /**
+     As for the xValue() argument of the C API's sqlite3_create_window_function().
+     See xInverse() for the fate of any exceptions this throws.
+  */
+  public abstract void xValue(sqlite3_context cx);
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/XDestroyCallback.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/XDestroyCallback.java
new file mode 100644
index 00000000..372e4ec8
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/XDestroyCallback.java
@@ -0,0 +1,37 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file declares JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   Callback for a hook called by SQLite when certain client-provided
+   state are destroyed. It gets its name from the pervasive use of
+   the symbol name xDestroy() for this purpose in the C API
+   documentation.
+*/
+public interface XDestroyCallback {
+  /**
+     Must perform any cleanup required by this object. Must not
+     throw. Must not call back into the sqlite3 API, else it might
+     invoke a deadlock.
+
+     WARNING: as a rule, it is never safe to register individual
+     instances with this interface multiple times in the
+     library. e.g., do not register the same CollationCallback with
+     multiple arities or names using sqlite3_create_collation().  If
+     this rule is violated, the library will eventually try to free
+     each individual reference, leading to memory corruption or a
+     crash via duplicate free().
+  */
+  public void xDestroy();
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/package-info.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/package-info.java
new file mode 100644
index 00000000..127f3806
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/package-info.java
@@ -0,0 +1,89 @@
+/**
+   This package houses a JNI binding to the SQLite3 C API.
+
+   The primary interfaces are in {@link
+   org.sqlite.jni.capi.CApi}.
+
+   API Goals and Requirements
+
+   
+
+     - A 1-to-1(-ish) mapping of the C API to Java via JNI, insofar
+     as cross-language semantics allow for. A closely-related goal is
+     that the C
+     documentation should be usable as-is, insofar as possible,
+     for most of the JNI binding. As a rule, undocumented symbols in
+     the Java interface behave as documented for their C API
+     counterpart. Only semantic differences and Java-specific features
+     are documented here.+
+
- Support Java as far back as version 8 (2014).+
+
- Environment-independent. Should work everywhere both Java and
+     SQLite3 do.+
+
- No 3rd-party dependencies beyond the JDK. That includes no
+     build-level dependencies for specific IDEs and toolchains.  We
+     welcome the addition of build files for arbitrary environments
+     insofar as they neither interfere with each other nor become a
+     maintenance burden for the sqlite developers.+
+
+
+Non-Goals
+
+  
+
+    - Creation of high-level OO wrapper APIs. Clients are free to
+    create them off of the C-style API.+
+
- Support for mixed-mode operation, where client code accesses
+    SQLite both via the Java-side API and the C API via their own
+    native code. In such cases, proxy functionalities (primarily
+    callback handler wrappers of all sorts) may fail because the
+    C-side use of the SQLite APIs will bypass those proxies.+
+
+
+State of this API
+
+   As of version 3.43, this software is in "tech preview" form. We
+   tentatively plan to stamp it as stable with the 3.44 release.
+
+   Threading Considerations
+
+   This API is, if built with SQLITE_THREADSAFE set to 1 or 2,
+   thread-safe, insofar as the C API guarantees, with some addenda:
+
+   
+
+     - It is not legal to use Java-facing SQLite3 resource handles
+     (sqlite3, sqlite3_stmt, etc) from multiple threads concurrently,
+     nor to use any database-specific resources concurrently in a
+     thread separate from the one the database is currently in use
+     in. i.e. do not use a sqlite3_stmt in thread #2 when thread #1 is
+     using the database which prepared that handle.
+
+     
 Violating this will eventually corrupt the JNI-level bindings
+     between Java's and C's view of the database. This is a limitation
+     of the JNI bindings, not the lower-level library.
+
+
+- It is legal to use a given handle, and database-specific
+     resources, across threads, so long as no two threads pass
+     resources owned by the same database into the library
+     concurrently.
+     +
+
+
+Any number of threads may, of course, create and use any number
+   of database handles they wish. Care only needs to be taken when
+   those handles or their associated resources cross threads, or...
+
+   When built with SQLITE_THREADSAFE=0 then no threading guarantees
+   are provided and multi-threaded use of the library will provoke
+   undefined behavior.
+
+*/
+package org.sqlite.jni.capi;
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/sqlite3.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/sqlite3.java
new file mode 100644
index 00000000..901317f0
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/sqlite3.java
@@ -0,0 +1,43 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   A wrapper for communicating C-level (sqlite3*) instances with
+   Java. These wrappers do not own their associated pointer, they
+   simply provide a type-safe way to communicate it between Java
+   and C via JNI.
+*/
+public final class sqlite3 extends NativePointerHolder
+ implements AutoCloseable {
+
+  // Only invoked from JNI
+  private sqlite3(){}
+
+  public String toString(){
+    final long ptr = getNativePointer();
+    if( 0==ptr ){
+      return sqlite3.class.getSimpleName()+"@null";
+    }
+    final String fn = CApi.sqlite3_db_filename(this, "main");
+    return sqlite3.class.getSimpleName()
+      +"@"+String.format("0x%08x",ptr)
+      +"["+((null == fn) ? "" : fn)+"]"
+      ;
+  }
+
+  @Override public void close(){
+    CApi.sqlite3_close_v2(this.clearNativePointer());
+  }
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/sqlite3_backup.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/sqlite3_backup.java
new file mode 100644
index 00000000..0ef75c17
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/sqlite3_backup.java
@@ -0,0 +1,31 @@
+/*
+** 2023-09-03
+**
+** 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 part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   A wrapper for passing C-level (sqlite3_backup*) instances around in
+   Java. These wrappers do not own their associated pointer, they
+   simply provide a type-safe way to communicate it between Java and C
+   via JNI.
+*/
+public final class sqlite3_backup extends NativePointerHolder
+  implements AutoCloseable {
+  // Only invoked from JNI.
+  private sqlite3_backup(){}
+
+  @Override public void close(){
+    CApi.sqlite3_backup_finish(this);
+  }
+
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/sqlite3_blob.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/sqlite3_blob.java
new file mode 100644
index 00000000..1b96c18b
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/sqlite3_blob.java
@@ -0,0 +1,31 @@
+/*
+** 2023-09-03
+**
+** 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 part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   A wrapper for passing C-level (sqlite3_blob*) instances around in
+   Java. These wrappers do not own their associated pointer, they
+   simply provide a type-safe way to communicate it between Java and C
+   via JNI.
+*/
+public final class sqlite3_blob extends NativePointerHolder
+  implements AutoCloseable {
+  // Only invoked from JNI.
+  private sqlite3_blob(){}
+
+  @Override public void close(){
+    CApi.sqlite3_blob_close(this.clearNativePointer());
+  }
+
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/sqlite3_context.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/sqlite3_context.java
new file mode 100644
index 00000000..82ec49af
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/sqlite3_context.java
@@ -0,0 +1,79 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   sqlite3_context instances are used in conjunction with user-defined
+   SQL functions (a.k.a. UDFs).
+*/
+public final class sqlite3_context extends NativePointerHolder {
+  private Long aggregateContext = null;
+
+  /**
+     getAggregateContext() corresponds to C's
+     sqlite3_aggregate_context(), with a slightly different interface
+     to account for cross-language differences. It serves the same
+     purposes in a slightly different way: it provides a key which is
+     stable across invocations of a UDF's callbacks, such that all
+     calls into those callbacks can determine which "set" of those
+     calls they belong to.
+
+     Note that use of this method is not a requirement for proper use
+     of this class. sqlite3_aggregate_context() can also be used.
+
+     
If the argument is true and the aggregate context has not yet
+     been set up, it will be initialized and fetched on demand, else it
+     won't. The intent is that xStep(), xValue(), and xInverse()
+     methods pass true and xFinal() methods pass false.
+
+     
This function treats numeric 0 as null, always returning null instead
+     of 0.
+
+     
If this object is being used in the context of an aggregate or
+     window UDF, this function returns a non-0 value which is distinct
+     for each set of UDF callbacks from a single invocation of the
+     UDF, otherwise it returns 0. The returned value is only only
+     valid within the context of execution of a single SQL statement,
+     and must not be re-used by future invocations of the UDF in
+     different SQL statements.
+
+     
Consider this SQL, where MYFUNC is a user-defined aggregate function:
+
+     
{@code
+     SELECT MYFUNC(A), MYFUNC(B) FROM T;
+     }
+
+     The xStep() and xFinal() methods of the callback need to be able
+     to differentiate between those two invocations in order to
+     perform their work properly. The value returned by
+     getAggregateContext() will be distinct for each of those
+     invocations of MYFUNC() and is intended to be used as a lookup
+     key for mapping callback invocations to whatever client-defined
+     state is needed by the UDF.
+
+     
There is one case where this will return null in the context
+     of an aggregate or window function: if the result set has no
+     rows, the UDF's xFinal() will be called without any other x...()
+     members having been called. In that one case, no aggregate
+     context key will have been generated. xFinal() implementations
+     need to be prepared to accept that condition as legal.
+  */
+  public synchronized Long getAggregateContext(boolean initIfNeeded){
+      if( aggregateContext==null ){
+        aggregateContext = CApi.sqlite3_aggregate_context(this, initIfNeeded);
+        if( !initIfNeeded && null==aggregateContext ) aggregateContext = 0L;
+      }
+      return (null==aggregateContext || 0!=aggregateContext) ? aggregateContext : null;
+  }
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/sqlite3_stmt.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/sqlite3_stmt.java
new file mode 100644
index 00000000..3b8b71f8
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/sqlite3_stmt.java
@@ -0,0 +1,30 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+   A wrapper for communicating C-level (sqlite3_stmt*) instances with
+   Java. These wrappers do not own their associated pointer, they
+   simply provide a type-safe way to communicate it between Java and C
+   via JNI.
+*/
+public final class sqlite3_stmt extends NativePointerHolder
+  implements AutoCloseable {
+  // Only invoked from JNI.
+  private sqlite3_stmt(){}
+
+  @Override public void close(){
+    CApi.sqlite3_finalize(this.clearNativePointer());
+  }
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/capi/sqlite3_value.java b/sqlite/ext/jni/src/org/sqlite/jni/capi/sqlite3_value.java
new file mode 100644
index 00000000..a4772f0f
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/capi/sqlite3_value.java
@@ -0,0 +1,19 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+public final class sqlite3_value extends NativePointerHolder {
+  //! Invoked only from JNI.
+  private sqlite3_value(){}
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/fts5/Fts5.java b/sqlite/ext/jni/src/org/sqlite/jni/fts5/Fts5.java
new file mode 100644
index 00000000..0dceeafd
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/fts5/Fts5.java
@@ -0,0 +1,32 @@
+/*
+** 2023-08-05
+**
+** 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 part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.fts5;
+
+/**
+   INCOMPLETE AND COMPLETELY UNTESTED.
+
+   A utility object for holding FTS5-specific types and constants
+   which are used by multiple FTS5 classes.
+*/
+public final class Fts5 {
+  /* Not used */
+  private Fts5(){}
+
+
+  public static final int FTS5_TOKENIZE_QUERY    = 0x0001;
+  public static final int FTS5_TOKENIZE_PREFIX   = 0x0002;
+  public static final int FTS5_TOKENIZE_DOCUMENT = 0x0004;
+  public static final int FTS5_TOKENIZE_AUX      = 0x0008;
+  public static final int FTS5_TOKEN_COLOCATED   = 0x0001;
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/fts5/Fts5Context.java b/sqlite/ext/jni/src/org/sqlite/jni/fts5/Fts5Context.java
new file mode 100644
index 00000000..439b4779
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/fts5/Fts5Context.java
@@ -0,0 +1,24 @@
+/*
+** 2023-08-04
+**
+** 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 part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.fts5;
+import org.sqlite.jni.capi.*;
+
+/**
+   A wrapper for communicating C-level (Fts5Context*) instances with
+   Java. These wrappers do not own their associated pointer, they
+   simply provide a type-safe way to communicate it between Java and C
+   via JNI.
+*/
+public final class Fts5Context extends NativePointerHolder {
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/fts5/Fts5ExtensionApi.java b/sqlite/ext/jni/src/org/sqlite/jni/fts5/Fts5ExtensionApi.java
new file mode 100644
index 00000000..594f3eaa
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/fts5/Fts5ExtensionApi.java
@@ -0,0 +1,97 @@
+/*
+** 2023-08-04
+**
+** 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 part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.fts5;
+import java.nio.charset.StandardCharsets;
+import org.sqlite.jni.capi.*;
+import org.sqlite.jni.annotation.*;
+
+/**
+*/
+public final class Fts5ExtensionApi extends NativePointerHolder {
+  //! Only called from JNI
+  private Fts5ExtensionApi(){}
+  private final int iVersion = 2;
+
+  /* Callback type for used by xQueryPhrase(). */
+  public static interface XQueryPhraseCallback {
+    int call(Fts5ExtensionApi fapi, Fts5Context cx);
+  }
+
+  /**
+     Returns the singleton instance of this class.
+  */
+  public static native Fts5ExtensionApi getInstance();
+
+  public native int xColumnCount(@NotNull Fts5Context fcx);
+
+  public native int xColumnSize(@NotNull Fts5Context cx, int iCol,
+                                @NotNull OutputPointer.Int32 pnToken);
+
+  public native int xColumnText(@NotNull Fts5Context cx, int iCol,
+                                @NotNull OutputPointer.String txt);
+
+  public native int xColumnTotalSize(@NotNull Fts5Context fcx, int iCol,
+                                     @NotNull OutputPointer.Int64 pnToken);
+
+  public native Object xGetAuxdata(@NotNull Fts5Context cx, boolean clearIt);
+
+  public native int xInst(@NotNull Fts5Context cx, int iIdx,
+                          @NotNull OutputPointer.Int32 piPhrase,
+                          @NotNull OutputPointer.Int32 piCol,
+                          @NotNull OutputPointer.Int32 piOff);
+
+  public native int xInstCount(@NotNull Fts5Context fcx,
+                               @NotNull OutputPointer.Int32 pnInst);
+
+  public native int xPhraseCount(@NotNull Fts5Context fcx);
+
+  public native int xPhraseFirst(@NotNull Fts5Context cx, int iPhrase,
+                                 @NotNull Fts5PhraseIter iter,
+                                 @NotNull OutputPointer.Int32 iCol,
+                                 @NotNull OutputPointer.Int32 iOff);
+
+  public native int xPhraseFirstColumn(@NotNull Fts5Context cx, int iPhrase,
+                                       @NotNull Fts5PhraseIter iter,
+                                       @NotNull OutputPointer.Int32 iCol);
+  public native void xPhraseNext(@NotNull Fts5Context cx,
+                                 @NotNull Fts5PhraseIter iter,
+                                 @NotNull OutputPointer.Int32 iCol,
+                                 @NotNull OutputPointer.Int32 iOff);
+  public native void xPhraseNextColumn(@NotNull Fts5Context cx,
+                                       @NotNull Fts5PhraseIter iter,
+                                       @NotNull OutputPointer.Int32 iCol);
+  public native int xPhraseSize(@NotNull Fts5Context fcx, int iPhrase);
+
+  public native int xQueryPhrase(@NotNull Fts5Context cx, int iPhrase,
+                                 @NotNull XQueryPhraseCallback callback);
+  public native int xRowCount(@NotNull Fts5Context fcx,
+                              @NotNull OutputPointer.Int64 nRow);
+
+  public native long xRowid(@NotNull Fts5Context cx);
+  /* Note that the JNI binding lacks the C version's xDelete()
+     callback argument. Instead, if pAux has an xDestroy() method, it
+     is called if the FTS5 API finalizes the aux state (including if
+     allocation of storage for the auxdata fails). Any reference to
+     pAux held by the JNI layer will be relinquished regardless of
+     whether pAux has an xDestroy() method. */
+
+  public native int xSetAuxdata(@NotNull Fts5Context cx, @Nullable Object pAux);
+
+  public native int xTokenize(@NotNull Fts5Context cx, @NotNull byte[] pText,
+                              @NotNull XTokenizeCallback callback);
+
+  public native Object xUserData(Fts5Context cx);
+  //^^^ returns the pointer passed as the 3rd arg to the C-level
+  // fts5_api::xCreateFunction().
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/fts5/Fts5PhraseIter.java b/sqlite/ext/jni/src/org/sqlite/jni/fts5/Fts5PhraseIter.java
new file mode 100644
index 00000000..5774eb59
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/fts5/Fts5PhraseIter.java
@@ -0,0 +1,25 @@
+/*
+** 2023-08-04
+**
+** 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 part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.fts5;
+import org.sqlite.jni.capi.NativePointerHolder;
+
+/**
+   A wrapper for C-level Fts5PhraseIter. They are only modified and
+   inspected by native-level code.
+*/
+public final class Fts5PhraseIter extends NativePointerHolder {
+  //! Updated and used only by native code.
+  private long a;
+  private long b;
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/fts5/Fts5Tokenizer.java b/sqlite/ext/jni/src/org/sqlite/jni/fts5/Fts5Tokenizer.java
new file mode 100644
index 00000000..b72e5d0f
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/fts5/Fts5Tokenizer.java
@@ -0,0 +1,31 @@
+/*
+** 2023-08-05x
+**
+** 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 part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.fts5;
+import org.sqlite.jni.capi.NativePointerHolder;
+
+/**
+   INCOMPLETE AND COMPLETELY UNTESTED.
+
+   A wrapper for communicating C-level (Fts5Tokenizer*) instances with
+   Java. These wrappers do not own their associated pointer, they
+   simply provide a type-safe way to communicate it between Java and C
+   via JNI.
+
+   At the C level, the Fts5Tokenizer type is essentially a void
+   pointer used specifically for tokenizers.
+*/
+public final class Fts5Tokenizer extends NativePointerHolder {
+  //! Only called from JNI.
+  private Fts5Tokenizer(){}
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/fts5/TesterFts5.java b/sqlite/ext/jni/src/org/sqlite/jni/fts5/TesterFts5.java
new file mode 100644
index 00000000..c4264c54
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/fts5/TesterFts5.java
@@ -0,0 +1,832 @@
+/*
+** 2023-08-04
+**
+** 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 set of tests for the sqlite3 JNI bindings.
+*/
+package org.sqlite.jni.fts5;
+import static org.sqlite.jni.capi.CApi.*;
+import static org.sqlite.jni.capi.Tester1.*;
+import org.sqlite.jni.capi.*;
+import org.sqlite.jni.fts5.*;
+
+import java.util.*;
+
+public class TesterFts5 {
+
+  private static void test1(){
+    final Fts5ExtensionApi fea = Fts5ExtensionApi.getInstance();
+    affirm( null != fea );
+    affirm( fea.getNativePointer() != 0 );
+    affirm( fea == Fts5ExtensionApi.getInstance() )/*singleton*/;
+
+    sqlite3 db = createNewDb();
+    fts5_api fApi = fts5_api.getInstanceForDb(db);
+    affirm( fApi != null );
+    affirm( fApi == fts5_api.getInstanceForDb(db) /* singleton per db */ );
+
+    execSql(db, new String[] {
+        "CREATE VIRTUAL TABLE ft USING fts5(a, b);",
+        "INSERT INTO ft(rowid, a, b) VALUES(1, 'X Y', 'Y Z');",
+        "INSERT INTO ft(rowid, a, b) VALUES(2, 'A Z', 'Y Y');"
+      });
+
+    final String pUserData = "This is pUserData";
+    final int outputs[] = {0, 0};
+    final fts5_extension_function func = new fts5_extension_function(){
+        @Override public void call(Fts5ExtensionApi ext, Fts5Context fCx,
+                                   sqlite3_context pCx, sqlite3_value argv[]){
+          final int nCols = ext.xColumnCount(fCx);
+          affirm( 2 == nCols );
+          affirm( nCols == argv.length );
+          affirm( ext.xUserData(fCx) == pUserData );
+          final OutputPointer.String op = new OutputPointer.String();
+          final OutputPointer.Int32 colsz = new OutputPointer.Int32();
+          final OutputPointer.Int64 colTotalSz = new OutputPointer.Int64();
+          for(int i = 0; i < nCols; ++i ){
+            int rc = ext.xColumnText(fCx, i, op);
+            affirm( 0 == rc );
+            final String val = op.value;
+            affirm( val.equals(sqlite3_value_text16(argv[i])) );
+            rc = ext.xColumnSize(fCx, i, colsz);
+            affirm( 0==rc );
+            affirm( 3==sqlite3_value_bytes(argv[i]) );
+            rc = ext.xColumnTotalSize(fCx, i, colTotalSz);
+            affirm( 0==rc );
+          }
+          ++outputs[0];
+        }
+        public void xDestroy(){
+          outputs[1] = 1;
+        }
+      };
+
+    int rc = fApi.xCreateFunction("myaux", pUserData, func);
+    affirm( 0==rc );
+
+    affirm( 0==outputs[0] );
+    execSql(db, "select myaux(ft,a,b) from ft;");
+    affirm( 2==outputs[0] );
+    affirm( 0==outputs[1] );
+    sqlite3_close_v2(db);
+    affirm( 1==outputs[1] );
+  }
+
+  /* 
+  ** Argument sql is a string containing one or more SQL statements
+  ** separated by ";" characters. This function executes each of these
+  ** statements against the database passed as the first argument. If
+  ** no error occurs, the results of the SQL script are returned as
+  ** an array of strings. If an error does occur, a RuntimeException is 
+  ** thrown.
+  */
+  private static String[] sqlite3_exec(sqlite3 db, String sql) {
+    List aOut = new ArrayList();
+
+    /* Iterate through the list of SQL statements. For each, step through
+    ** it and add any results to the aOut[] array.  */
+    int rc = sqlite3_prepare_multi(db, sql, new PrepareMultiCallback() {
+      @Override public int call(sqlite3_stmt pStmt){
+        while( SQLITE_ROW==sqlite3_step(pStmt) ){
+          int ii;
+          for(ii=0; ii, );
+    */
+    class fts5_aux implements fts5_extension_function {
+      @Override public void call(
+          Fts5ExtensionApi ext, 
+          Fts5Context fCx,
+          sqlite3_context pCx, 
+          sqlite3_value argv[]
+      ){
+        if( argv.length>1 ){
+          throw new RuntimeException("fts5_aux: wrong number of args");
+        }
+
+        boolean bClear = (argv.length==1);
+        Object obj = ext.xGetAuxdata(fCx, bClear);
+        if( obj instanceof String ){
+          sqlite3_result_text16(pCx, (String)obj);
+        }
+
+        if( argv.length==1 ){
+          String val = sqlite3_value_text16(argv[0]);
+          if( !val.equals("") ){
+            ext.xSetAuxdata(fCx, val);
+          }
+        }
+      }
+      public void xDestroy(){ }
+    };
+
+    /*
+    ** fts5_inst();
+    **
+    ** This is used to test the xInstCount() and xInst() APIs. It returns a
+    ** text value containing a Tcl list with xInstCount() elements. Each
+    ** element is itself a list of 3 integers - the phrase number, column
+    ** number and token offset returned by each call to xInst().
+    */
+    fts5_extension_function fts5_inst = new fts5_extension_function(){
+      @Override public void call(
+          Fts5ExtensionApi ext, 
+          Fts5Context fCx,
+          sqlite3_context pCx, 
+          sqlite3_value argv[]
+      ){
+        if( argv.length!=0 ){
+          throw new RuntimeException("fts5_inst: wrong number of args");
+        }
+
+        OutputPointer.Int32 pnInst = new OutputPointer.Int32();
+        OutputPointer.Int32 piPhrase = new OutputPointer.Int32();
+        OutputPointer.Int32 piCol = new OutputPointer.Int32();
+        OutputPointer.Int32 piOff = new OutputPointer.Int32();
+        String ret = new String();
+
+        int rc = ext.xInstCount(fCx, pnInst);
+        int nInst = pnInst.get();
+        int ii;
+
+        for(ii=0; rc==SQLITE_OK && ii0 ) ret += " ";
+          ret += "{"+piPhrase.get()+" "+piCol.get()+" "+piOff.get()+"}";
+        }
+
+        sqlite3_result_text(pCx, ret);
+      }
+      public void xDestroy(){ }
+    };
+
+    /*
+    ** fts5_pinst();
+    **
+    ** Like SQL function fts5_inst(), except using the following
+    **
+    **     xPhraseCount
+    **     xPhraseFirst
+    **     xPhraseNext
+    */
+    fts5_extension_function fts5_pinst = new fts5_extension_function(){
+      @Override public void call(
+          Fts5ExtensionApi ext, 
+          Fts5Context fCx,
+          sqlite3_context pCx, 
+          sqlite3_value argv[]
+      ){
+        if( argv.length!=0 ){
+          throw new RuntimeException("fts5_pinst: wrong number of args");
+        }
+
+        OutputPointer.Int32 piCol = new OutputPointer.Int32();
+        OutputPointer.Int32 piOff = new OutputPointer.Int32();
+        String ret = new String();
+        int rc = SQLITE_OK;
+
+        int nPhrase = ext.xPhraseCount(fCx);
+        int ii;
+
+        for(ii=0; rc==SQLITE_OK && ii=0;
+              ext.xPhraseNext(fCx, pIter, piCol, piOff)
+          ){
+            if( !ret.equals("") ) ret += " ";
+            ret += "{"+ii+" "+piCol.get()+" "+piOff.get()+"}";
+          }
+        }
+
+        if( rc!=SQLITE_OK ){
+          throw new RuntimeException("fts5_pinst: rc=" + rc);
+        }else{
+          sqlite3_result_text(pCx, ret);
+        }
+      }
+      public void xDestroy(){ }
+    };
+
+    /*
+    ** fts5_pcolinst();
+    **
+    ** Like SQL function fts5_pinst(), except using the following
+    **
+    **     xPhraseFirstColumn
+    **     xPhraseNextColumn
+    */
+    fts5_extension_function fts5_pcolinst = new fts5_extension_function(){
+      @Override public void call(
+          Fts5ExtensionApi ext, 
+          Fts5Context fCx,
+          sqlite3_context pCx, 
+          sqlite3_value argv[]
+      ){
+        if( argv.length!=0 ){
+          throw new RuntimeException("fts5_pcolinst: wrong number of args");
+        }
+
+        OutputPointer.Int32 piCol = new OutputPointer.Int32();
+        String ret = new String();
+        int rc = SQLITE_OK;
+
+        int nPhrase = ext.xPhraseCount(fCx);
+        int ii;
+
+        for(ii=0; rc==SQLITE_OK && ii=0;
+              ext.xPhraseNextColumn(fCx, pIter, piCol)
+          ){
+            if( !ret.equals("") ) ret += " ";
+            ret += "{"+ii+" "+piCol.get()+"}";
+          }
+        }
+
+        if( rc!=SQLITE_OK ){
+          throw new RuntimeException("fts5_pcolinst: rc=" + rc);
+        }else{
+          sqlite3_result_text(pCx, ret);
+        }
+      }
+      public void xDestroy(){ }
+    };
+
+    /*
+    ** fts5_rowcount();
+    */
+    fts5_extension_function fts5_rowcount = new fts5_extension_function(){
+      @Override public void call(
+          Fts5ExtensionApi ext, 
+          Fts5Context fCx,
+          sqlite3_context pCx, 
+          sqlite3_value argv[]
+      ){
+        if( argv.length!=0 ){
+          throw new RuntimeException("fts5_rowcount: wrong number of args");
+        }
+        OutputPointer.Int64 pnRow = new OutputPointer.Int64();
+
+        int rc = ext.xRowCount(fCx, pnRow);
+        if( rc==SQLITE_OK ){
+          sqlite3_result_int64(pCx, pnRow.get());
+        }else{
+          throw new RuntimeException("fts5_rowcount: rc=" + rc);
+        }
+      }
+      public void xDestroy(){ }
+    };
+
+    /*
+    ** fts5_phrasesize();
+    */
+    fts5_extension_function fts5_phrasesize = new fts5_extension_function(){
+      @Override public void call(
+          Fts5ExtensionApi ext, 
+          Fts5Context fCx,
+          sqlite3_context pCx, 
+          sqlite3_value argv[]
+      ){
+        if( argv.length!=1 ){
+          throw new RuntimeException("fts5_phrasesize: wrong number of args");
+        }
+        int iPhrase = sqlite3_value_int(argv[0]);
+
+        int sz = ext.xPhraseSize(fCx, iPhrase);
+        sqlite3_result_int(pCx, sz);
+      }
+      public void xDestroy(){ }
+    };
+
+    /*
+    ** fts5_phrasehits(, );
+    **
+    ** Use the xQueryPhrase() API to determine how many hits, in total,
+    ** there are for phrase  in the database.
+    */
+    fts5_extension_function fts5_phrasehits = new fts5_extension_function(){
+      @Override public void call(
+          Fts5ExtensionApi ext, 
+          Fts5Context fCx,
+          sqlite3_context pCx, 
+          sqlite3_value argv[]
+      ){
+        if( argv.length!=1 ){
+          throw new RuntimeException("fts5_phrasesize: wrong number of args");
+        }
+        int iPhrase = sqlite3_value_int(argv[0]);
+        int rc = SQLITE_OK;
+
+        class MyCallback implements Fts5ExtensionApi.XQueryPhraseCallback {
+          public int nRet = 0;
+          public int getRet() { return nRet; }
+
+          @Override
+          public int call(Fts5ExtensionApi fapi, Fts5Context cx){
+            OutputPointer.Int32 pnInst = new OutputPointer.Int32();
+            int rc = fapi.xInstCount(cx, pnInst);
+            nRet += pnInst.get();
+            return rc;
+          }
+        };
+
+        MyCallback xCall = new MyCallback();
+        rc = ext.xQueryPhrase(fCx, iPhrase, xCall);
+        if( rc!=SQLITE_OK ){
+          throw new RuntimeException("fts5_phrasehits: rc=" + rc);
+        }
+        sqlite3_result_int(pCx, xCall.getRet());
+      }
+      public void xDestroy(){ }
+    };
+
+    /*
+    ** fts5_tokenize(, )
+    */
+    fts5_extension_function fts5_tokenize = new fts5_extension_function(){
+      @Override public void call(
+          Fts5ExtensionApi ext, 
+          Fts5Context fCx,
+          sqlite3_context pCx, 
+          sqlite3_value argv[]
+      ){
+        if( argv.length!=1 ){
+          throw new RuntimeException("fts5_tokenize: wrong number of args");
+        }
+        byte[] utf8 = sqlite3_value_text(argv[0]);
+        int rc = SQLITE_OK;
+
+        class MyCallback implements XTokenizeCallback {
+          private List myList = new ArrayList();
+
+          public String getval() {
+            return String.join("+", myList);
+          }
+
+          @Override
+          public int call(int tFlags, byte[] txt, int iStart, int iEnd){
+            try {
+              String str = new String(txt, "UTF-8");
+              myList.add(str);
+            } catch (Exception e) {
+            }
+            return SQLITE_OK;
+          }
+        };
+
+        MyCallback xCall = new MyCallback();
+        ext.xTokenize(fCx, utf8, xCall);
+        sqlite3_result_text16(pCx, xCall.getval());
+
+        if( rc!=SQLITE_OK ){
+          throw new RuntimeException("fts5_tokenize: rc=" + rc);
+        }
+      }
+      public void xDestroy(){ }
+    };
+
+    fts5_api api = fts5_api.getInstanceForDb(db);
+    api.xCreateFunction("fts5_rowid", fts5_rowid);
+    api.xCreateFunction("fts5_columncount", fts5_columncount);
+    api.xCreateFunction("fts5_columnsize", fts5_columnsize);
+    api.xCreateFunction("fts5_columntext", fts5_columntext);
+    api.xCreateFunction("fts5_columntotalsize", fts5_columntsize);
+
+    api.xCreateFunction("fts5_aux1", new fts5_aux());
+    api.xCreateFunction("fts5_aux2", new fts5_aux());
+
+    api.xCreateFunction("fts5_inst", fts5_inst);
+    api.xCreateFunction("fts5_pinst", fts5_pinst);
+    api.xCreateFunction("fts5_pcolinst", fts5_pcolinst);
+    api.xCreateFunction("fts5_rowcount", fts5_rowcount);
+    api.xCreateFunction("fts5_phrasesize", fts5_phrasesize);
+    api.xCreateFunction("fts5_phrasehits", fts5_phrasehits);
+    api.xCreateFunction("fts5_tokenize", fts5_tokenize);
+  }
+  /* 
+  ** Test of various Fts5ExtensionApi methods 
+  */
+  private static void test2(){
+
+    /* Open db and populate an fts5 table */
+    sqlite3 db = createNewDb();
+    do_execsql_test(db, 
+      "CREATE VIRTUAL TABLE ft USING fts5(a, b);" +
+      "INSERT INTO ft(rowid, a, b) VALUES(-9223372036854775808, 'x', 'x');" +
+      "INSERT INTO ft(rowid, a, b) VALUES(0, 'x', 'x');" +
+      "INSERT INTO ft(rowid, a, b) VALUES(1, 'x y z', 'x y z');" +
+      "INSERT INTO ft(rowid, a, b) VALUES(2, 'x y z', 'x z');" +
+      "INSERT INTO ft(rowid, a, b) VALUES(3, 'x y z', 'x y z');" +
+      "INSERT INTO ft(rowid, a, b) VALUES(9223372036854775807, 'x', 'x');"
+    );
+
+    create_test_functions(db);
+
+    /* Test that fts5_rowid() seems to work */
+    do_execsql_test(db, 
+      "SELECT rowid==fts5_rowid(ft) FROM ft('x')",
+      "[1, 1, 1, 1, 1, 1]"
+    );
+
+    /* Test fts5_columncount() */
+    do_execsql_test(db, 
+      "SELECT fts5_columncount(ft) FROM ft('x')",
+      "[2, 2, 2, 2, 2, 2]"
+    );
+
+    /* Test fts5_columnsize() */
+    do_execsql_test(db, 
+      "SELECT fts5_columnsize(ft, 0) FROM ft('x') ORDER BY rowid",
+      "[1, 1, 3, 3, 3, 1]"
+    );
+    do_execsql_test(db, 
+      "SELECT fts5_columnsize(ft, 1) FROM ft('x') ORDER BY rowid",
+      "[1, 1, 3, 2, 3, 1]"
+    );
+    do_execsql_test(db, 
+      "SELECT fts5_columnsize(ft, -1) FROM ft('x') ORDER BY rowid",
+      "[2, 2, 6, 5, 6, 2]"
+    );
+
+    /* Test that xColumnSize() returns SQLITE_RANGE if the column number
+    ** is out-of range */
+    try {
+      do_execsql_test(db, 
+        "SELECT fts5_columnsize(ft, 2) FROM ft('x') ORDER BY rowid"
+      );
+    } catch( RuntimeException e ){
+      affirm( e.getMessage().matches(".*column index out of range") );
+    }
+
+    /* Test fts5_columntext() */
+    do_execsql_test(db, 
+      "SELECT fts5_columntext(ft, 0) FROM ft('x') ORDER BY rowid",
+      "[x, x, x y z, x y z, x y z, x]"
+    );
+    do_execsql_test(db, 
+      "SELECT fts5_columntext(ft, 1) FROM ft('x') ORDER BY rowid",
+      "[x, x, x y z, x z, x y z, x]"
+    );
+    do_execsql_test(db, 
+      "SELECT fts5_columntext(ft, 2) FROM ft('x') ORDER BY rowid",
+      "[null, null, null, null, null, null]"
+    );
+
+    /* Test fts5_columntotalsize() */
+    do_execsql_test(db, 
+      "SELECT fts5_columntotalsize(ft, 0) FROM ft('x') ORDER BY rowid",
+      "[12, 12, 12, 12, 12, 12]"
+    );
+    do_execsql_test(db, 
+      "SELECT fts5_columntotalsize(ft, 1) FROM ft('x') ORDER BY rowid",
+      "[11, 11, 11, 11, 11, 11]"
+    );
+    do_execsql_test(db, 
+      "SELECT fts5_columntotalsize(ft, -1) FROM ft('x') ORDER BY rowid",
+      "[23, 23, 23, 23, 23, 23]"
+    );
+
+    /* Test that xColumnTotalSize() returns SQLITE_RANGE if the column 
+    ** number is out-of range */
+    try {
+      do_execsql_test(db, 
+        "SELECT fts5_columntotalsize(ft, 2) FROM ft('x') ORDER BY rowid"
+      );
+    } catch( RuntimeException e ){
+      affirm( e.getMessage().matches(".*column index out of range") );
+    }
+
+    do_execsql_test(db, 
+      "SELECT rowid, fts5_rowcount(ft) FROM ft('z')",
+      "[1, 6, 2, 6, 3, 6]"
+    );
+
+    sqlite3_close_v2(db);
+  }
+
+  /* 
+  ** Test of various Fts5ExtensionApi methods 
+  */
+  private static void test3(){
+
+    /* Open db and populate an fts5 table */
+    sqlite3 db = createNewDb();
+    do_execsql_test(db, 
+      "CREATE VIRTUAL TABLE ft USING fts5(a, b);" +
+      "INSERT INTO ft(a, b) VALUES('the one', 1);" +
+      "INSERT INTO ft(a, b) VALUES('the two', 2);" +
+      "INSERT INTO ft(a, b) VALUES('the three', 3);" +
+      "INSERT INTO ft(a, b) VALUES('the four', '');"
+    );
+    create_test_functions(db);
+
+    /* Test fts5_aux1() + fts5_aux2() - users of xGetAuxdata and xSetAuxdata */
+    do_execsql_test(db,
+      "SELECT fts5_aux1(ft, a) FROM ft('the')",
+      "[null, the one, the two, the three]"
+    );
+    do_execsql_test(db,
+      "SELECT fts5_aux2(ft, b) FROM ft('the')",
+      "[null, 1, 2, 3]"
+    );
+    do_execsql_test(db,
+      "SELECT fts5_aux1(ft, a), fts5_aux2(ft, b) FROM ft('the')",
+      "[null, null, the one, 1, the two, 2, the three, 3]"
+    );
+    do_execsql_test(db,
+      "SELECT fts5_aux1(ft, b), fts5_aux1(ft) FROM ft('the')",
+      "[null, 1, 1, 2, 2, 3, 3, null]"
+    );
+  }
+
+  /* 
+  ** Test of various Fts5ExtensionApi methods 
+  */
+  private static void test4(){
+
+    /* Open db and populate an fts5 table */
+    sqlite3 db = createNewDb();
+    create_test_functions(db);
+    do_execsql_test(db, 
+      "CREATE VIRTUAL TABLE ft USING fts5(a, b);" +
+      "INSERT INTO ft(a, b) VALUES('one two three', 'two three four');" +
+      "INSERT INTO ft(a, b) VALUES('two three four', 'three four five');" +
+      "INSERT INTO ft(a, b) VALUES('three four five', 'four five six');" 
+    );
+
+
+    do_execsql_test(db,
+      "SELECT fts5_inst(ft) FROM ft('two')",
+      "[{0 0 1} {0 1 0}, {0 0 0}]"
+    );
+    do_execsql_test(db,
+      "SELECT fts5_inst(ft) FROM ft('four')",
+      "[{0 1 2}, {0 0 2} {0 1 1}, {0 0 1} {0 1 0}]"
+    );
+
+    do_execsql_test(db,
+      "SELECT fts5_inst(ft) FROM ft('a OR b OR four')",
+      "[{2 1 2}, {2 0 2} {2 1 1}, {2 0 1} {2 1 0}]"
+    );
+    do_execsql_test(db,
+      "SELECT fts5_inst(ft) FROM ft('two four')",
+      "[{0 0 1} {0 1 0} {1 1 2}, {0 0 0} {1 0 2} {1 1 1}]"
+    );
+
+    do_execsql_test(db,
+      "SELECT fts5_pinst(ft) FROM ft('two')",
+      "[{0 0 1} {0 1 0}, {0 0 0}]"
+    );
+    do_execsql_test(db,
+      "SELECT fts5_pinst(ft) FROM ft('four')",
+      "[{0 1 2}, {0 0 2} {0 1 1}, {0 0 1} {0 1 0}]"
+    );
+    do_execsql_test(db,
+      "SELECT fts5_pinst(ft) FROM ft('a OR b OR four')",
+      "[{2 1 2}, {2 0 2} {2 1 1}, {2 0 1} {2 1 0}]"
+    );
+    do_execsql_test(db,
+      "SELECT fts5_pinst(ft) FROM ft('two four')",
+      "[{0 0 1} {0 1 0} {1 1 2}, {0 0 0} {1 0 2} {1 1 1}]"
+    );
+
+    do_execsql_test(db,
+      "SELECT fts5_pcolinst(ft) FROM ft('two')",
+      "[{0 0} {0 1}, {0 0}]"
+    );
+    do_execsql_test(db,
+      "SELECT fts5_pcolinst(ft) FROM ft('four')",
+      "[{0 1}, {0 0} {0 1}, {0 0} {0 1}]"
+    );
+    do_execsql_test(db,
+      "SELECT fts5_pcolinst(ft) FROM ft('a OR b OR four')",
+      "[{2 1}, {2 0} {2 1}, {2 0} {2 1}]"
+    );
+    do_execsql_test(db,
+      "SELECT fts5_pcolinst(ft) FROM ft('two four')",
+      "[{0 0} {0 1} {1 1}, {0 0} {1 0} {1 1}]"
+    );
+
+    do_execsql_test(db,
+      "SELECT fts5_phrasesize(ft, 0) FROM ft('four five six') LIMIT 1;",
+      "[1]"
+    );
+    do_execsql_test(db,
+      "SELECT fts5_phrasesize(ft, 0) FROM ft('four + five + six') LIMIT 1;",
+      "[3]"
+    );
+
+
+    sqlite3_close_v2(db);
+  }
+
+  private static void test5(){
+    /* Open db and populate an fts5 table */
+    sqlite3 db = createNewDb();
+    create_test_functions(db);
+    do_execsql_test(db, 
+      "CREATE VIRTUAL TABLE ft USING fts5(x, b);" +
+      "INSERT INTO ft(x) VALUES('one two three four five six seven eight');" +
+      "INSERT INTO ft(x) VALUES('one two one four one six one eight');" +
+      "INSERT INTO ft(x) VALUES('one two three four five six seven eight');"
+    );
+
+    do_execsql_test(db,
+      "SELECT fts5_phrasehits(ft, 0) FROM ft('one') LIMIT 1",
+      "[6]"
+    );
+
+    sqlite3_close_v2(db);
+  }
+
+  private static void test6(){
+    sqlite3 db = createNewDb();
+    create_test_functions(db);
+    do_execsql_test(db, 
+      "CREATE VIRTUAL TABLE ft USING fts5(x, b);" +
+      "INSERT INTO ft(x) VALUES('one two three four five six seven eight');" 
+    );
+
+    do_execsql_test(db,
+      "SELECT fts5_tokenize(ft, 'abc def ghi') FROM ft('one')",
+      "[abc+def+ghi]"
+    );
+    do_execsql_test(db,
+      "SELECT fts5_tokenize(ft, 'it''s BEEN a...') FROM ft('one')",
+      "[it+s+been+a]"
+    );
+
+    sqlite3_close_v2(db);
+  }
+
+  private static synchronized void runTests(){
+    test1();
+    test2();
+    test3();
+    test4();
+    test5();
+    test6();
+  }
+
+  public TesterFts5(){
+    runTests();
+  }
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/fts5/XTokenizeCallback.java b/sqlite/ext/jni/src/org/sqlite/jni/fts5/XTokenizeCallback.java
new file mode 100644
index 00000000..3aa514f3
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/fts5/XTokenizeCallback.java
@@ -0,0 +1,22 @@
+/*
+** 2023-08-04
+**
+** 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 part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.fts5;
+
+
+/**
+   Callback type for use with xTokenize() variants.
+*/
+public interface XTokenizeCallback {
+  int call(int tFlags, byte[] txt, int iStart, int iEnd);
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/fts5/fts5_api.java b/sqlite/ext/jni/src/org/sqlite/jni/fts5/fts5_api.java
new file mode 100644
index 00000000..d7d2da43
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/fts5/fts5_api.java
@@ -0,0 +1,76 @@
+/*
+** 2023-08-05
+**
+** 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 part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.fts5;
+import org.sqlite.jni.annotation.*;
+import org.sqlite.jni.capi.*;
+
+/**
+   A wrapper for communicating C-level (fts5_api*) instances with
+   Java. These wrappers do not own their associated pointer, they
+   simply provide a type-safe way to communicate it between Java and C
+   via JNI.
+*/
+public final class fts5_api extends NativePointerHolder {
+  /* Only invoked from JNI */
+  private fts5_api(){}
+
+  public static final int iVersion = 2;
+
+  /**
+     Returns the fts5_api instance associated with the given db, or
+     null if something goes horribly wrong.
+  */
+  public static synchronized native fts5_api getInstanceForDb(@NotNull sqlite3 db);
+
+  public synchronized native int xCreateFunction(@NotNull String name,
+                                                 @Nullable Object userData,
+                                                 @NotNull fts5_extension_function xFunction);
+
+  /**
+     Convenience overload which passes null as the 2nd argument to the
+     3-parameter form.
+  */
+  public int xCreateFunction(@NotNull String name,
+                             @NotNull fts5_extension_function xFunction){
+    return xCreateFunction(name, null, xFunction);
+  }
+
+  // /* Create a new auxiliary function */
+  // int (*xCreateFunction)(
+  //   fts5_api *pApi,
+  //   const char *zName,
+  //   void *pContext,
+  //   fts5_extension_function xFunction,
+  //   void (*xDestroy)(void*)
+  // );
+
+  // Still potentially todo:
+
+  // int (*xCreateTokenizer)(
+  //   fts5_api *pApi,
+  //   const char *zName,
+  //   void *pContext,
+  //   fts5_tokenizer *pTokenizer,
+  //   void (*xDestroy)(void*)
+  // );
+
+  // /* Find an existing tokenizer */
+  // int (*xFindTokenizer)(
+  //   fts5_api *pApi,
+  //   const char *zName,
+  //   void **ppContext,
+  //   fts5_tokenizer *pTokenizer
+  // );
+
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/fts5/fts5_extension_function.java b/sqlite/ext/jni/src/org/sqlite/jni/fts5/fts5_extension_function.java
new file mode 100644
index 00000000..5e47633b
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/fts5/fts5_extension_function.java
@@ -0,0 +1,47 @@
+/*
+** 2023-08-05
+**
+** 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 part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.fts5;
+import org.sqlite.jni.capi.sqlite3_context;
+import org.sqlite.jni.capi.sqlite3_value;
+
+/**
+   JNI-level wrapper for C's fts5_extension_function type.
+*/
+public interface fts5_extension_function {
+  // typedef void (*fts5_extension_function)(
+  //   const Fts5ExtensionApi *pApi,   /* API offered by current FTS version */
+  //   Fts5Context *pFts,              /* First arg to pass to pApi functions */
+  //   sqlite3_context *pCtx,          /* Context for returning result/error */
+  //   int nVal,                       /* Number of values in apVal[] array */
+  //   sqlite3_value **apVal           /* Array of trailing arguments */
+  // );
+
+  /**
+     The callback implementation, corresponding to the xFunction
+     argument of C's fts5_api::xCreateFunction().
+  */
+  void call(Fts5ExtensionApi ext, Fts5Context fCx,
+            sqlite3_context pCx, sqlite3_value argv[]);
+  /**
+     Is called when this function is destroyed by sqlite3. Typically
+     this function will be empty.
+  */
+  void xDestroy();
+
+  public static abstract class Abstract implements fts5_extension_function {
+    @Override public abstract void call(Fts5ExtensionApi ext, Fts5Context fCx,
+                                        sqlite3_context pCx, sqlite3_value argv[]);
+    @Override public void xDestroy(){}
+  }
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/fts5/fts5_tokenizer.java b/sqlite/ext/jni/src/org/sqlite/jni/fts5/fts5_tokenizer.java
new file mode 100644
index 00000000..f4ada4dc
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/fts5/fts5_tokenizer.java
@@ -0,0 +1,49 @@
+/*
+** 2023-08-05
+**
+** 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 part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.fts5;
+import org.sqlite.jni.capi.NativePointerHolder;
+import org.sqlite.jni.annotation.NotNull;
+
+/**
+   A wrapper for communicating C-level (fts5_tokenizer*) instances with
+   Java. These wrappers do not own their associated pointer, they
+   simply provide a type-safe way to communicate it between Java and C
+   via JNI.
+*/
+public final class fts5_tokenizer extends NativePointerHolder {
+  /* Only invoked by JNI */
+  private fts5_tokenizer(){}
+
+  // int (*xCreate)(void*, const char **azArg, int nArg, Fts5Tokenizer **ppOut);
+  // void (*xDelete)(Fts5Tokenizer*);
+
+  public native int xTokenize(@NotNull Fts5Tokenizer t, int tokFlags,
+                              @NotNull byte pText[],
+                              @NotNull XTokenizeCallback callback);
+
+
+  // int (*xTokenize)(Fts5Tokenizer*,
+  //     void *pCtx,
+  //     int flags,            /* Mask of FTS5_TOKENIZE_* flags */
+  //     const char *pText, int nText,
+  //     int (*xToken)(
+  //       void *pCtx,         /* Copy of 2nd argument to xTokenize() */
+  //       int tflags,         /* Mask of FTS5_TOKEN_* flags */
+  //       const char *pToken, /* Pointer to buffer containing token */
+  //       int nToken,         /* Size of token in bytes */
+  //       int iStart,         /* Byte offset of token within input text */
+  //       int iEnd            /* Byte offset of end of token within input text */
+  //     )
+  // );
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/test-script-interpreter.md b/sqlite/ext/jni/src/org/sqlite/jni/test-script-interpreter.md
new file mode 100644
index 00000000..a51d64d1
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/test-script-interpreter.md
@@ -0,0 +1,269 @@
+# Specifications For A Rudimentary SQLite Test Script Interpreter
+
+## Overview
+
+The purpose of the Test Script Interpreter is to read and interpret
+script files that contain SQL commands and desired results.  The
+interpreter will check results and report an discrepencies found.
+
+The test script files are ASCII text files.  The filename always ends with
+".test".  Each script is evaluated independently; context does not carry
+forward from one script to the next.  So, for example, the --null command
+run in one test script does not cause any changes in the behavior of
+subsequent test scripts.  All open database connections are closed
+at the end of each test script.  All database files created by a test
+script are deleted when the script finishes.
+
+## Parsing Rules:
+
+  1.   The test script is read line by line, where a line is a sequence of
+       characters that runs up to the next '\\n' (0x0a) character or until
+       the end of the file.  There is never a need to read ahead past the
+       end of the current line.
+
+  2.   If any line contains the string " MODULE_NAME:" (with a space before
+       the initial "M") or "MIXED_MODULE_NAME:" then that test script is
+       incompatible with this spec.  Processing of the test script should
+       end immediately.  There is no need to read any more of the file.
+       In verbose mode, the interpreter might choose to emit an informational
+       messages saying that the test script was abandoned due to an
+       incompatible module type.
+
+  3.   If any line contains the string "SCRIPT_MODULE_NAME:" then the input
+       script is known to be of the correct type for this specification and
+       processing may continue.  The "MODULE_NAME" checking in steps 2 and 3
+       may optionally be discontinued after sighting a "SCRIPT_MODULE_NAME".
+
+  4.   If any line contains "REQUIRED_PROPERTIES:" and that substring is followed
+       by any non-whitespace text, then the script is not compatible with this
+       spec.  Processing should stop immediately.  In verbose mode, the
+       interpreter might choose to emit an information message saying that the
+       test script was abandoned due to unsupported requirement properties.
+
+  5.   If any line begins with the "\|" (0x7c) character, that indicates that
+       the input script is not compatible with this specification.  Processing
+       of the script should stop immediately.  In verbose mode, the interpreter
+       might choose to emit an informational message indicating that the
+       test script was abandoned because it contained "a dbtotxt format database
+       specification".
+
+  6.   Any line that begins with "#" is a C-preprocessor line.  The interpreter
+       described by this spec does not know how to deal with C-preprocessor lines.
+       Hence, processing should be abandoned.  In verbose mode, the interpreter
+       might emit an informational message similar to
+       "script NAME abandoned due to C-preprocessor line: ..."
+
+  7.   If a line begins with exactly two minus signs followed by a
+       lowercase letter, that is a command.  Process commands as described
+       below.
+
+  8.   All other lines should be accumulated into the "input buffer".
+       The various commands will have access to this input buffer.
+       Some commands will reset the buffer.
+
+## Initialization
+
+The initial state of the interpreter at the start of processing each script
+is as if the following command sequence had been run:
+
+> ~~~
+--close all
+--db 0
+--new test.db
+--null nil
+~~~
+
+In words, all database connections are closed except for connection 0 (the
+default) which is open on an empty database named "test.db".  The string
+"nil" is displayed for NULL column values.
+
+The only context carried forward after the evaluation of one test script
+into the evaluation of the next test script is the count of the number of
+tests run and the number of failures seen.
+
+## Commands:
+
+Each command looks like an SQL comment.  The command begins at the left
+margin (no leading space) and starts with exactly 2 minus signs ("-").
+The command name consists of lowercase letters and maybe a "-" or two.
+Some commands have arguments.
+The arguments are separated from the command name by one or more spaces.
+
+Commands have access to the input buffer and might reset the input buffer.
+The command can also optionally read (and consume) additional text from
+script that comes after the command.
+
+Unknown or unrecognized commands indicate that the script contains features
+that are not (yet) supported by this specification.  Processing of the
+script should terminate immediately.  When this happens and when the
+interpreter is in a "verbose" mode, the interpreter might choose to emit
+an informational message along the lines of "test script NAME abandoned
+due to unsupported command: --whatever".
+
+The initial implemention will only recognize a few commands.  Other
+commands may be added later.  The following is the initial set of
+commands:
+
+### The --testcase command
+
+Every test case starts with a --testcase command.  The --testcase
+command resets both the "input buffer" and the "result buffer".  The
+argument to the --testcase command is the name of the test case.  That
+test case name is used for logging and debugging and when printing
+errors. The input buffer is set to the body of the test case.
+
+### The --result command
+
+The --result command tries to execute the text in the input buffer as SQL.
+For each row of result coming out of this SQL, the text of that result is
+appended to the "result buffer".  If a result row contains multiple columns,
+the columns are processed from left to right.  For each column, text is
+appended to the result buffer according to the following rules:
+
+  *   If the result buffer already contains some text, append a space.
+      (In this way, all column values and all row values are separated from
+      each other by a single space.)
+
+  *   If sqlite3_column_text() returns NULL, then append "nil" - or
+      some other text that is specified by the --null command - and skip
+      all subsequent rules.
+
+  *   If sqlite3_column_text() is an empty string, append `{}` to the
+      result buffer and skip all subsequent rules.
+
+  *   If sqlite3_column_text() does not contain any special
+      characters, append it to the result buffer without any
+      formatting and skip all subsequent rules. Special characters are:
+      0x00 to 0x20 (inclusive), double-quote (0x22), backslash (0x5c),
+      curly braces (0x7b and 0x7d).
+
+  *   If sqlite3_column_text() does not contains curly braces, then put
+      the text inside of `{...}` and append it and skip all subsequent rules.
+
+  *   Append the text within double-quotes (`"..."`) and within the text
+      escape '"' and '\\' by prepending a single '\\' and escape any
+      control characters (characters less than 0x20) using octal notation:
+      '\\NNN'.
+
+If an error is encountered while running the SQL, then append the
+symbolic C-preprocessor name for the error
+code (ex: "SQLITE_CONSTRAINT") as if it were a column value.  Then append
+the error message text as if it where a column value.  Then stop processing.
+
+After the SQL text has been run, compare the content of the result buffer
+against the argument to the --result command and report a testing error if
+there are any differences.
+
+The --result command resets the input buffer, but it does not reset
+the result buffer.  This distinction does not matter for the --result
+command itself, but it is important for related commands like --glob
+and --notglob.  Sometimes test cases will contains a bunch of SQL
+followed by multiple --glob and/or --notglob statements.  All of the
+globs should be evaluted agains the result buffer correct, but the SQL
+should only be run once.  This is accomplished by resetting the input
+buffer but not the result buffer.
+
+### The --glob command
+
+The --glob command works just like --result except that the argument to
+--glob is interpreted as a TEST-GLOB pattern and the results are compared
+using that glob pattern rather than using strcmp().  Other than that,
+the two operate the same.
+
+The TEST-GLOB pattern is slightly different for a standard GLOB:
+
+   *    The '*' character matches zero or more characters.
+
+   *    The '?' character matches any single character
+
+   *    The '[...]' character sequence machines a single character
+        in between the brackets.
+
+   *    The '#' character matches one or more digits  (This is the main
+        difference between standard unix-glob and TEST-GLOB.  unix-glob
+        does not have this feature.  It was added to because it comes
+        up a lot during SQLite testing.)
+
+### The --notglob command
+
+The --notglob command works just like --glob except that it reports an
+error if the GLOB does match, rather than if the GLOB does not matches.
+
+### The --oom command
+
+This command is to be used for out-of-memory testing.  It means that
+OOM errors should be simulated to ensure that SQLite is able to deal with
+them.  This command can be silently ignored for now.  We might add support
+for this later.
+
+### The --tableresult command
+
+The --tableresult command works like --glob except that the GLOB pattern
+to be matched is taken from subsequent lines of the input script up to
+the next --end.  Every span of one or more whitespace characters in this
+pattern text is collapsed into a single space (0x20).
+Leading and trailing whitespace are removed from the pattern.
+The --end that ends the GLOB pattern is not part of the GLOB pattern, but
+the --end is consumed from the script input.
+
+### The --new and --open commands
+
+The --new and --open commands cause a database file to be opened.
+The name of the file is the argument to the command.  The --new command
+opens an initially empty database (it deletes the file before opening it)
+whereas the --open command opens an existing database if it already
+exists.
+
+### The --db command
+
+The script interpreter can have up to 7 different SQLite database
+connections open at a time.  The --db command is used to switch between
+them.  The argument to --db is an integer between 0 and 6 that selects
+which database connection to use moving forward.
+
+### The --close command
+
+The --close command causes an existing database connection to close.
+This command is a no-op if the database connection is not currently
+open.  There can be up to 7 different database connections, numbered 0
+through 6.  The number of the database connection to close is an
+argument to the --close command, which will fail if an out-of-range
+value is provided.  Or if the argument to --close is "all" then all
+open database connections are closed. If passed no argument, the
+currently-active database is assumed.
+
+### The --null command
+
+The NULL command changes the text that is used to represent SQL NULL
+values in the result buffer.
+
+### The --run command
+
+The --run command executes text in the input buffer as if it where SQL.
+However, nothing is added to the result buffer.  Any output from the SQL
+is silently ignored. Errors in the SQL are silently ignored.
+
+The --run command normally executes the SQL in the current database
+connection.  However, if --run has an argument that is an integer between
+0 and 6 then the SQL is run in the alternative database connection specified
+by that argument.
+
+### The --json and --json-block commands
+
+The --json and --json-block commands work like --result and --tableresult,
+respectively.  The difference is that column values are appended to the
+result buffer literally, without ever enclosing the values in `{...}` or
+`"..."` and without escaping any characters in the column value and comparison
+is always an exact strcmp() not a GLOB.
+
+### The --print command
+
+The --print command emits both its arguments and its body (if any) to
+stdout, indenting each line of output.
+
+### The --column-names command
+
+The --column-names command requires 0 or 1 as an argument, to disable
+resp.  enable it, and modifies SQL execution to include column names
+in output. When this option is on, each column value emitted gets
+prefixed by its column name, with a single space between them.
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/AggregateFunction.java b/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/AggregateFunction.java
new file mode 100644
index 00000000..173d775e
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/AggregateFunction.java
@@ -0,0 +1,82 @@
+/*
+** 2023-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.
+**
+*************************************************************************
+** This file is part of the wrapper1 interface for sqlite3.
+*/
+package org.sqlite.jni.wrapper1;
+import org.sqlite.jni.capi.CApi;
+import org.sqlite.jni.annotation.*;
+import org.sqlite.jni.capi.sqlite3_context;
+import org.sqlite.jni.capi.sqlite3_value;
+
+/**
+   EXPERIMENTAL/INCOMPLETE/UNTESTED
+
+   A SqlFunction implementation for aggregate functions. The T type
+   represents the type of data accumulated by this aggregate while it
+   works. e.g. a SUM()-like UDF might use Integer or Long and a
+   CONCAT()-like UDF might use a StringBuilder or a List.
+*/
+public abstract class AggregateFunction implements SqlFunction  {
+
+  /**
+     As for the xStep() argument of the C API's
+     sqlite3_create_function().  If this function throws, the
+     exception is reported via sqlite3_result_error().
+  */
+  public abstract void xStep(SqlFunction.Arguments args);
+
+  /**
+     As for the xFinal() argument of the C API's
+     sqlite3_create_function(). If this function throws, it is
+     translated into sqlite3_result_error().
+
+     Note that the passed-in object will not actually contain any
+     arguments for xFinal() but will contain the context object needed
+     for setting the call's result or error state.
+  */
+  public abstract void xFinal(SqlFunction.Arguments args);
+
+  /**
+     Optionally override to be notified when the UDF is finalized by
+     SQLite.
+  */
+  public void xDestroy() {}
+
+  /** Per-invocation state for the UDF. */
+  private final SqlFunction.PerContextState map =
+    new SqlFunction.PerContextState<>();
+
+  /**
+     To be called from the implementation's xStep() method, as well
+     as the xValue() and xInverse() methods of the {@link WindowFunction}
+     subclass, to fetch the current per-call UDF state. On the
+     first call to this method for any given sqlite3_context
+     argument, the context is set to the given initial value. On all other
+     calls, the 2nd argument is ignored.
+
+     @see SQLFunction.PerContextState#getAggregateState
+  */
+  protected final ValueHolder getAggregateState(SqlFunction.Arguments args, T initialValue){
+    return map.getAggregateState(args, initialValue);
+  }
+
+  /**
+     To be called from the implementation's xFinal() method to fetch
+     the final state of the UDF and remove its mapping.
+
+     see SQLFunction.PerContextState#takeAggregateState
+  */
+  protected final T takeAggregateState(SqlFunction.Arguments args){
+    return map.takeAggregateState(args);
+  }
+
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/ScalarFunction.java b/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/ScalarFunction.java
new file mode 100644
index 00000000..067a6983
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/ScalarFunction.java
@@ -0,0 +1,37 @@
+/*
+** 2023-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.
+**
+*************************************************************************
+** This file is part of the wrapper1 interface for sqlite3.
+*/
+package org.sqlite.jni.wrapper1;
+import org.sqlite.jni.capi.CApi;
+import org.sqlite.jni.annotation.*;
+import org.sqlite.jni.capi.sqlite3_context;
+import org.sqlite.jni.capi.sqlite3_value;
+
+/**
+   The SqlFunction type for scalar SQL functions.
+*/
+public abstract class ScalarFunction implements SqlFunction  {
+  /**
+     As for the xFunc() argument of the C API's
+     sqlite3_create_function(). If this function throws, it is
+     translated into an sqlite3_result_error().
+  */
+  public abstract void xFunc(SqlFunction.Arguments args);
+
+  /**
+     Optionally override to be notified when the UDF is finalized by
+     SQLite. This default implementation does nothing.
+  */
+  public void xDestroy() {}
+
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/SqlFunction.java b/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/SqlFunction.java
new file mode 100644
index 00000000..d6acda5a
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/SqlFunction.java
@@ -0,0 +1,301 @@
+/*
+** 2023-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.
+**
+*************************************************************************
+** This file is part of the wrapper1 interface for sqlite3.
+*/
+package org.sqlite.jni.wrapper1;
+import org.sqlite.jni.capi.CApi;
+import org.sqlite.jni.capi.sqlite3_context;
+import org.sqlite.jni.capi.sqlite3_value;
+
+/**
+   Base marker interface for SQLite's three types of User-Defined SQL
+   Functions (UDFs): Scalar, Aggregate, and Window functions.
+*/
+public interface SqlFunction  {
+
+  /**
+     The Arguments type is an abstraction on top of the lower-level
+     UDF function argument types. It provides _most_ of the functionality
+     of the lower-level interface, insofar as possible without "leaking"
+     those types into this API.
+  */
+  public final static class Arguments implements Iterable{
+    private final sqlite3_context cx;
+    private final sqlite3_value args[];
+    public final int length;
+
+    /**
+       Must be passed the context and arguments for the UDF call this
+       object is wrapping. Intended to be used by internal proxy
+       classes which "convert" the lower-level interface into this
+       package's higher-level interface, e.g. ScalarAdapter and
+       AggregateAdapter.
+
+       Passing null for the args is equivalent to passing a length-0
+       array.
+    */
+    Arguments(sqlite3_context cx, sqlite3_value args[]){
+      this.cx = cx;
+      this.args = args==null ? new sqlite3_value[0] : args;;
+      this.length = this.args.length;
+    }
+
+    /**
+       Wrapper for a single SqlFunction argument. Primarily intended
+       for use with the Arguments class's Iterable interface.
+    */
+    public final static class Arg {
+      private final Arguments a;
+      private final int ndx;
+      /* Only for use by the Arguments class. */
+      private Arg(Arguments a, int ndx){
+        this.a = a;
+        this.ndx = ndx;
+      }
+      /** Returns this argument's index in its parent argument list. */
+      public int getIndex(){return ndx;}
+      public int getInt(){return a.getInt(ndx);}
+      public long getInt64(){return a.getInt64(ndx);}
+      public double getDouble(){return a.getDouble(ndx);}
+      public byte[] getBlob(){return a.getBlob(ndx);}
+      public byte[] getText(){return a.getText(ndx);}
+      public String getText16(){return a.getText16(ndx);}
+      public int getBytes(){return a.getBytes(ndx);}
+      public int getBytes16(){return a.getBytes16(ndx);}
+      public Object getObject(){return a.getObject(ndx);}
+      public  T getObjectCasted(Class type){ return a.getObjectCasted(ndx, type); }
+      public int getType(){return a.getType(ndx);}
+      public Object getAuxData(){return a.getAuxData(ndx);}
+      public void setAuxData(Object o){a.setAuxData(ndx, o);}
+    }
+
+    @Override
+    public java.util.Iterator iterator(){
+      final Arg[] proxies = new Arg[args.length];
+      for( int i = 0; i < args.length; ++i ){
+        proxies[i] = new Arg(this, i);
+      }
+      return java.util.Arrays.stream(proxies).iterator();
+    }
+
+    /**
+       Returns the sqlite3_value at the given argument index or throws
+       an IllegalArgumentException exception if ndx is out of range.
+    */
+    private sqlite3_value valueAt(int ndx){
+      if(ndx<0 || ndx>=args.length){
+        throw new IllegalArgumentException(
+          "SQL function argument index "+ndx+" is out of range."
+        );
+      }
+      return args[ndx];
+    }
+
+    sqlite3_context getContext(){return cx;}
+
+    public int getArgCount(){ return args.length; }
+
+    public int getInt(int arg){return CApi.sqlite3_value_int(valueAt(arg));}
+    public long getInt64(int arg){return CApi.sqlite3_value_int64(valueAt(arg));}
+    public double getDouble(int arg){return CApi.sqlite3_value_double(valueAt(arg));}
+    public byte[] getBlob(int arg){return CApi.sqlite3_value_blob(valueAt(arg));}
+    public byte[] getText(int arg){return CApi.sqlite3_value_text(valueAt(arg));}
+    public String getText16(int arg){return CApi.sqlite3_value_text16(valueAt(arg));}
+    public int getBytes(int arg){return CApi.sqlite3_value_bytes(valueAt(arg));}
+    public int getBytes16(int arg){return CApi.sqlite3_value_bytes16(valueAt(arg));}
+    public Object getObject(int arg){return CApi.sqlite3_value_java_object(valueAt(arg));}
+    public  T getObjectCasted(int arg, Class type){
+      return CApi.sqlite3_value_java_casted(valueAt(arg), type);
+    }
+
+    public int getType(int arg){return CApi.sqlite3_value_type(valueAt(arg));}
+    public int getSubtype(int arg){return CApi.sqlite3_value_subtype(valueAt(arg));}
+    public int getNumericType(int arg){return CApi.sqlite3_value_numeric_type(valueAt(arg));}
+    public int getNoChange(int arg){return CApi.sqlite3_value_nochange(valueAt(arg));}
+    public boolean getFromBind(int arg){return CApi.sqlite3_value_frombind(valueAt(arg));}
+    public int getEncoding(int arg){return CApi.sqlite3_value_encoding(valueAt(arg));}
+
+    public void resultInt(int v){ CApi.sqlite3_result_int(cx, v); }
+    public void resultInt64(long v){ CApi.sqlite3_result_int64(cx, v); }
+    public void resultDouble(double v){ CApi.sqlite3_result_double(cx, v); }
+    public void resultError(String msg){CApi.sqlite3_result_error(cx, msg);}
+    public void resultError(Exception e){CApi.sqlite3_result_error(cx, e);}
+    public void resultErrorTooBig(){CApi.sqlite3_result_error_toobig(cx);}
+    public void resultErrorCode(int rc){CApi.sqlite3_result_error_code(cx, rc);}
+    public void resultObject(Object o){CApi.sqlite3_result_java_object(cx, o);}
+    public void resultNull(){CApi.sqlite3_result_null(cx);}
+    public void resultArg(int argNdx){CApi.sqlite3_result_value(cx, valueAt(argNdx));}
+    public void resultZeroBlob(long n){
+      // Throw on error? If n is too big,
+      // sqlite3_result_error_toobig() is automatically called.
+      CApi.sqlite3_result_zeroblob64(cx, n);
+    }
+
+    public void resultBlob(byte[] blob){CApi.sqlite3_result_blob(cx, blob);}
+    public void resultText(byte[] utf8){CApi.sqlite3_result_text(cx, utf8);}
+    public void resultText(String txt){CApi.sqlite3_result_text(cx, txt);}
+    public void resultText16(byte[] utf16){CApi.sqlite3_result_text16(cx, utf16);}
+    public void resultText16(String txt){CApi.sqlite3_result_text16(cx, txt);}
+
+    public void setAuxData(int arg, Object o){
+      /* From the API docs: https://www.sqlite.org/c3ref/get_auxdata.html
+
+         The value of the N parameter to these interfaces should be
+         non-negative. Future enhancements may make use of negative N
+         values to define new kinds of function caching behavior.
+      */
+      valueAt(arg);
+      CApi.sqlite3_set_auxdata(cx, arg, o);
+    }
+
+    public Object getAuxData(int arg){
+      valueAt(arg);
+      return CApi.sqlite3_get_auxdata(cx, arg);
+    }
+  }
+
+  /**
+     PerContextState assists aggregate and window functions in
+     managing their accumulator state across calls to the UDF's
+     callbacks.
+
+     T must be of a type which can be legally stored as a value in
+     java.util.HashMap.
+
+     If a given aggregate or window function is called multiple times
+     in a single SQL statement, e.g. SELECT MYFUNC(A), MYFUNC(B)...,
+     then the clients need some way of knowing which call is which so
+     that they can map their state between their various UDF callbacks
+     and reset it via xFinal(). This class takes care of such
+     mappings.
+
+     
This class works by mapping
+     sqlite3_context.getAggregateContext() to a single piece of
+     state, of a client-defined type (the T part of this class), which
+     persists across a "matching set" of the UDF's callbacks.
+
+     
This class is a helper providing commonly-needed functionality
+     - it is not required for use with aggregate or window functions.
+     Client UDFs are free to perform such mappings using custom
+     approaches. The provided {@link AggregateFunction} and {@link
+     WindowFunction} classes use this.
+  */
+  public static final class PerContextState {
+    private final java.util.Map> map
+      = new java.util.HashMap<>();
+
+    /**
+       Should be called from a UDF's xStep(), xValue(), and xInverse()
+       methods, passing it that method's first argument and an initial
+       value for the persistent state. If there is currently no
+       mapping for the given context within the map, one is created
+       using the given initial value, else the existing one is used
+       and the 2nd argument is ignored.  It returns a ValueHolder
+       which can be used to modify that state directly without
+       requiring that the client update the underlying map's entry.
+
+       The caller is obligated to eventually call
+       takeAggregateState() to clear the mapping.
+    */
+    public ValueHolder getAggregateState(SqlFunction.Arguments args, T initialValue){
+      final Long key = args.getContext().getAggregateContext(true);
+      ValueHolder rc = null==key ? null : map.get(key);
+      if( null==rc ){
+        map.put(key, rc = new ValueHolder<>(initialValue));
+      }
+      return rc;
+    }
+
+    /**
+       Should be called from a UDF's xFinal() method and passed that
+       method's first argument. This function removes the value
+       associated with with the arguments' aggregate context from the
+       map and returns it, returning null if no other UDF method has
+       been called to set up such a mapping. The latter condition will
+       be the case if a UDF is used in a statement which has no result
+       rows.
+    */
+    public T takeAggregateState(SqlFunction.Arguments args){
+      final ValueHolder h = map.remove(args.getContext().getAggregateContext(false));
+      return null==h ? null : h.value;
+    }
+  }
+
+  /**
+     Internal-use adapter for wrapping this package's ScalarFunction
+     for use with the org.sqlite.jni.capi.ScalarFunction interface.
+  */
+  static final class ScalarAdapter extends org.sqlite.jni.capi.ScalarFunction {
+    final ScalarFunction impl;
+    ScalarAdapter(ScalarFunction impl){
+      this.impl = impl;
+    }
+    /**
+       Proxies this.impl.xFunc(), adapting the call arguments to that
+       function's signature. If the proxy throws, it's translated to
+       sqlite_result_error() with the exception's message.
+    */
+    public void xFunc(sqlite3_context cx, sqlite3_value[] args){
+      try{
+        impl.xFunc( new SqlFunction.Arguments(cx, args) );
+      }catch(Exception e){
+        CApi.sqlite3_result_error(cx, e);
+      }
+    }
+
+    public void xDestroy(){
+      impl.xDestroy();
+    }
+  }
+
+  /**
+     Internal-use adapter for wrapping this package's AggregateFunction
+     for use with the org.sqlite.jni.capi.AggregateFunction interface.
+  */
+  static final class AggregateAdapter extends org.sqlite.jni.capi.AggregateFunction {
+    final AggregateFunction impl;
+    AggregateAdapter(AggregateFunction impl){
+      this.impl = impl;
+    }
+
+    /**
+       Proxies this.impl.xStep(), adapting the call arguments to that
+       function's signature. If the proxied function throws, it is
+       translated to sqlite_result_error() with the exception's
+       message.
+    */
+    public void xStep(sqlite3_context cx, sqlite3_value[] args){
+      try{
+        impl.xStep( new SqlFunction.Arguments(cx, args) );
+      }catch(Exception e){
+        CApi.sqlite3_result_error(cx, e);
+      }
+    }
+
+    /**
+       As for the xFinal() argument of the C API's sqlite3_create_function().
+       If the proxied function throws, it is translated into a sqlite3_result_error().
+    */
+    public void xFinal(sqlite3_context cx){
+      try{
+        impl.xFinal( new SqlFunction.Arguments(cx, null) );
+      }catch(Exception e){
+        CApi.sqlite3_result_error(cx, e);
+      }
+    }
+
+    public void xDestroy(){
+      impl.xDestroy();
+    }
+  }
+
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/Sqlite.java b/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/Sqlite.java
new file mode 100644
index 00000000..bcf97b23
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/Sqlite.java
@@ -0,0 +1,218 @@
+/*
+** 2023-10-09
+**
+** 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 part of the wrapper1 interface for sqlite3.
+*/
+package org.sqlite.jni.wrapper1;
+import java.nio.charset.StandardCharsets;
+import static org.sqlite.jni.capi.CApi.*;
+import org.sqlite.jni.capi.CApi;
+import org.sqlite.jni.capi.sqlite3;
+import org.sqlite.jni.capi.sqlite3_stmt;
+import org.sqlite.jni.capi.OutputPointer;
+
+/**
+   This class represents a database connection, analog to the C-side
+   sqlite3 class but with added argument validation, exceptions, and
+   similar "smoothing of sharp edges" to make the API safe to use from
+   Java. It also acts as a namespace for other types for which
+   individual instances are tied to a specific database connection.
+*/
+public final class Sqlite implements AutoCloseable  {
+  private sqlite3 db;
+
+  //! Used only by the open() factory functions.
+  private Sqlite(sqlite3 db){
+    this.db = db;
+  }
+
+  /**
+     Returns a newly-opened db connection or throws SqliteException if
+     opening fails. All arguments are as documented for
+     sqlite3_open_v2().
+
+     Design question: do we want static factory functions or should
+     this be reformulated as a constructor?
+  */
+  public static Sqlite open(String filename, int flags, String vfsName){
+    final OutputPointer.sqlite3 out = new OutputPointer.sqlite3();
+    final int rc = sqlite3_open_v2(filename, out, flags, vfsName);
+    final sqlite3 n = out.take();
+    if( 0!=rc ){
+      if( null==n ) throw new SqliteException(rc);
+      final SqliteException ex = new SqliteException(n);
+      n.close();
+      throw ex;
+    }
+    return new Sqlite(n);
+  }
+
+  public static Sqlite open(String filename, int flags){
+    return open(filename, flags, null);
+  }
+
+  public static Sqlite open(String filename){
+    return open(filename, SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE, null);
+  }
+
+  @Override public void close(){
+    if(null!=this.db){
+      this.db.close();
+      this.db = null;
+    }
+  }
+
+  /**
+     Returns this object's underlying native db handle, or null if
+     this instance has been closed. This is very specifically not
+     public.
+  */
+  sqlite3 nativeHandle(){ return this.db; }
+
+  private sqlite3 affirmOpen(){
+    if( null==db || 0==db.getNativePointer() ){
+      throw new IllegalArgumentException("This database instance is closed.");
+    }
+    return this.db;
+  }
+
+  // private byte[] stringToUtf8(String s){
+  //   return s==null ? null : s.getBytes(StandardCharsets.UTF_8);
+  // }
+
+  private void affirmRcOk(int rc){
+    if( 0!=rc ){
+      throw new SqliteException(db);
+    }
+  }
+
+  /**
+     Corresponds to the sqlite3_stmt class. Use Sqlite.prepare() to
+     create new instances.
+  */
+  public final class Stmt implements AutoCloseable {
+    private Sqlite _db = null;
+    private sqlite3_stmt stmt = null;
+    /** Only called by the prepare() factory functions. */
+    Stmt(Sqlite db, sqlite3_stmt stmt){
+      this._db = db;
+      this.stmt = stmt;
+    }
+
+    sqlite3_stmt nativeHandle(){
+      return stmt;
+    }
+
+    private sqlite3_stmt affirmOpen(){
+      if( null==stmt || 0==stmt.getNativePointer() ){
+        throw new IllegalArgumentException("This Stmt has been finalized.");
+      }
+      return stmt;
+    }
+
+    /**
+       Corresponds to sqlite3_finalize(), but we cannot override the
+       name finalize() here because this one requires a different
+       signature. It does not throw on error here because "destructors
+       do not throw." If it returns non-0, the object is still
+       finalized.
+    */
+    public int finalizeStmt(){
+      int rc = 0;
+      if( null!=stmt ){
+        sqlite3_finalize(stmt);
+        stmt = null;
+      }
+      return rc;
+    }
+
+    @Override public void close(){
+      finalizeStmt();
+    }
+
+    /**
+       Throws if rc is any value other than 0, SQLITE_ROW, or
+       SQLITE_DONE, else returns rc.
+    */
+    private int checkRc(int rc){
+      switch(rc){
+        case 0:
+        case SQLITE_ROW:
+        case SQLITE_DONE: return rc;
+        default:
+          throw new SqliteException(this);
+      }
+    }
+
+    /**
+       Works like sqlite3_step() but throws SqliteException for any
+       result other than 0, SQLITE_ROW, or SQLITE_DONE.
+    */
+    public int step(){
+      return checkRc(sqlite3_step(affirmOpen()));
+    }
+
+    public Sqlite db(){ return this._db; }
+
+    /**
+       Works like sqlite3_reset() but throws on error.
+    */
+    public void reset(){
+      checkRc(sqlite3_reset(affirmOpen()));
+    }
+
+    public void clearBindings(){
+      sqlite3_clear_bindings( affirmOpen() );
+    }
+  }
+
+
+  /**
+     prepare() TODOs include:
+
+     - overloads taking byte[] and ByteBuffer.
+
+     - multi-statement processing, like CApi.sqlite3_prepare_multi()
+     but using a callback specific to the higher-level Stmt class
+     rather than the sqlite3_stmt class.
+  */
+  public Stmt prepare(String sql, int prepFlags){
+    final OutputPointer.sqlite3_stmt out = new OutputPointer.sqlite3_stmt();
+    final int rc = sqlite3_prepare_v3(affirmOpen(), sql, prepFlags, out);
+    affirmRcOk(rc);
+    return new Stmt(this, out.take());
+  }
+
+  public Stmt prepare(String sql){
+    return prepare(sql, 0);
+  }
+
+  public void createFunction(String name, int nArg, int eTextRep, ScalarFunction f ){
+    int rc = CApi.sqlite3_create_function(affirmOpen(), name, nArg, eTextRep,
+                                           new SqlFunction.ScalarAdapter(f));
+    if( 0!=rc ) throw new SqliteException(db);
+  }
+
+  public void createFunction(String name, int nArg, ScalarFunction f){
+    this.createFunction(name, nArg, CApi.SQLITE_UTF8, f);
+  }
+
+  public void createFunction(String name, int nArg, int eTextRep, AggregateFunction f ){
+    int rc = CApi.sqlite3_create_function(affirmOpen(), name, nArg, eTextRep,
+                                           new SqlFunction.AggregateAdapter(f));
+    if( 0!=rc ) throw new SqliteException(db);
+  }
+
+  public void createFunction(String name, int nArg, AggregateFunction f){
+    this.createFunction(name, nArg, CApi.SQLITE_UTF8, f);
+  }
+
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/SqliteException.java b/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/SqliteException.java
new file mode 100644
index 00000000..111f004d
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/SqliteException.java
@@ -0,0 +1,82 @@
+/*
+** 2023-10-09
+**
+** 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 part of the wrapper1 interface for sqlite3.
+*/
+package org.sqlite.jni.wrapper1;
+import static org.sqlite.jni.capi.CApi.*;
+import org.sqlite.jni.capi.sqlite3;
+
+/**
+   A wrapper for communicating C-level (sqlite3*) instances with
+   Java. These wrappers do not own their associated pointer, they
+   simply provide a type-safe way to communicate it between Java
+   and C via JNI.
+*/
+public final class SqliteException extends java.lang.RuntimeException {
+  int errCode = SQLITE_ERROR;
+  int xerrCode = SQLITE_ERROR;
+  int errOffset = -1;
+  int sysErrno = 0;
+
+  /**
+     Records the given error string and uses SQLITE_ERROR for both the
+     error code and extended error code.
+  */
+  public SqliteException(String msg){
+    super(msg);
+  }
+
+  /**
+     Uses sqlite3_errstr(sqlite3ResultCode) for the error string and
+     sets both the error code and extended error code to the given
+     value.
+  */
+  public SqliteException(int sqlite3ResultCode){
+    super(sqlite3_errstr(sqlite3ResultCode));
+    errCode = xerrCode = sqlite3ResultCode;
+  }
+
+  /**
+     Records the current error state of db (which must not be null and
+     must refer to an opened db object). Note that this does NOT close
+     the db.
+
+     Design note: closing the db on error is likely only useful during
+     a failed db-open operation, and the place(s) where that can
+     happen are inside this library, not client-level code.
+  */
+  SqliteException(sqlite3 db){
+    super(sqlite3_errmsg(db));
+    errCode = sqlite3_errcode(db);
+    xerrCode = sqlite3_extended_errcode(db);
+    errOffset = sqlite3_error_offset(db);
+    sysErrno = sqlite3_system_errno(db);
+  }
+
+  /**
+     Records the current error state of db (which must not be null and
+     must refer to an open database).
+  */
+  public SqliteException(Sqlite db){
+    this(db.nativeHandle());
+  }
+
+  public SqliteException(Sqlite.Stmt stmt){
+    this( stmt.db() );
+  }
+
+  public int errcode(){ return errCode; }
+  public int extendedErrcode(){ return xerrCode; }
+  public int errorOffset(){ return errOffset; }
+  public int systemErrno(){ return sysErrno; }
+
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/Tester2.java b/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/Tester2.java
new file mode 100644
index 00000000..f5fd5f84
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/Tester2.java
@@ -0,0 +1,584 @@
+/*
+** 2023-10-09
+**
+** 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 set of tests for the sqlite3 JNI bindings.
+*/
+package org.sqlite.jni.wrapper1;
+//import static org.sqlite.jni.capi.CApi.*;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.sqlite.jni.capi.*;
+
+/**
+   An annotation for Tester2 tests which we do not want to run in
+   reflection-driven test mode because either they are not suitable
+   for multi-threaded threaded mode or we have to control their execution
+   order.
+*/
+@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
+@java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD})
+@interface ManualTest{}
+/**
+   Annotation for Tester2 tests which mark those which must be skipped
+   in multi-threaded mode.
+*/
+@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
+@java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD})
+@interface SingleThreadOnly{}
+
+public class Tester2 implements Runnable {
+  //! True when running in multi-threaded mode.
+  private static boolean mtMode = false;
+  //! True to sleep briefly between tests.
+  private static boolean takeNaps = false;
+  //! True to shuffle the order of the tests.
+  private static boolean shuffle = false;
+  //! True to dump the list of to-run tests to stdout.
+  private static boolean listRunTests = false;
+  //! True to squelch all out() and outln() output.
+  private static boolean quietMode = false;
+  //! Total number of runTests() calls.
+  private static int nTestRuns = 0;
+  //! List of test*() methods to run.
+  private static List testMethods = null;
+  //! List of exceptions collected by run()
+  private static List listErrors = new ArrayList<>();
+  private static final class Metrics {
+    //! Number of times createNewDb() (or equivalent) is invoked.
+    volatile int dbOpen = 0;
+  }
+
+  //! Instance ID.
+  private Integer tId;
+
+  Tester2(Integer id){
+    tId = id;
+  }
+
+  static final Metrics metrics = new Metrics();
+
+  public static synchronized void outln(){
+    if( !quietMode ){
+      System.out.println("");
+    }
+  }
+
+  public static synchronized void outPrefix(){
+    if( !quietMode ){
+      System.out.print(Thread.currentThread().getName()+": ");
+    }
+  }
+
+  public static synchronized void outln(Object val){
+    if( !quietMode ){
+      outPrefix();
+      System.out.println(val);
+    }
+  }
+
+  public static synchronized void out(Object val){
+    if( !quietMode ){
+      System.out.print(val);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  public static synchronized void out(Object... vals){
+    if( !quietMode ){
+      outPrefix();
+      for(Object v : vals) out(v);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  public static synchronized void outln(Object... vals){
+    if( !quietMode ){
+      out(vals); out("\n");
+    }
+  }
+
+  static volatile int affirmCount = 0;
+  public static synchronized int affirm(Boolean v, String comment){
+    ++affirmCount;
+    if( false ) assert( v /* prefer assert over exception if it's enabled because
+                 the JNI layer sometimes has to suppress exceptions,
+                 so they might be squelched on their way back to the
+                 top. */);
+    if( !v ) throw new RuntimeException(comment);
+    return affirmCount;
+  }
+
+  public static void affirm(Boolean v){
+    affirm(v, "Affirmation failed.");
+  }
+
+
+  public static void execSql(Sqlite db, String[] sql){
+    execSql(db, String.join("", sql));
+  }
+
+  public static int execSql(Sqlite dbw, boolean throwOnError, String sql){
+    final sqlite3 db = dbw.nativeHandle();
+    OutputPointer.Int32 oTail = new OutputPointer.Int32();
+    final byte[] sqlUtf8 = sql.getBytes(StandardCharsets.UTF_8);
+    int pos = 0, n = 1;
+    byte[] sqlChunk = sqlUtf8;
+    int rc = 0;
+    sqlite3_stmt stmt = null;
+    final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt();
+    while(pos < sqlChunk.length){
+      if(pos > 0){
+        sqlChunk = Arrays.copyOfRange(sqlChunk, pos,
+                                      sqlChunk.length);
+      }
+      if( 0==sqlChunk.length ) break;
+      rc = CApi.sqlite3_prepare_v2(db, sqlChunk, outStmt, oTail);
+      if(throwOnError) affirm(0 == rc);
+      else if( 0!=rc ) break;
+      pos = oTail.value;
+      stmt = outStmt.take();
+      if( null == stmt ){
+        // empty statement was parsed.
+        continue;
+      }
+      affirm(0 != stmt.getNativePointer());
+      while( CApi.SQLITE_ROW == (rc = CApi.sqlite3_step(stmt)) ){
+      }
+      CApi.sqlite3_finalize(stmt);
+      affirm(0 == stmt.getNativePointer());
+      if(0!=rc && CApi.SQLITE_ROW!=rc && CApi.SQLITE_DONE!=rc){
+        break;
+      }
+    }
+    CApi.sqlite3_finalize(stmt);
+    if(CApi.SQLITE_ROW==rc || CApi.SQLITE_DONE==rc) rc = 0;
+    if( 0!=rc && throwOnError){
+      throw new SqliteException(db);
+    }
+    return rc;
+  }
+
+  static void execSql(Sqlite db, String sql){
+    execSql(db, true, sql);
+  }
+
+  @SingleThreadOnly /* because it's thread-agnostic */
+  private void test1(){
+    affirm(CApi.sqlite3_libversion_number() == CApi.SQLITE_VERSION_NUMBER);
+  }
+
+  /* Copy/paste/rename this to add new tests. */
+  private void _testTemplate(){
+    //final sqlite3 db = createNewDb();
+    //sqlite3_stmt stmt = prepare(db,"SELECT 1");
+    //sqlite3_finalize(stmt);
+    //sqlite3_close_v2(db);
+  }
+
+  private void nap() throws InterruptedException {
+    if( takeNaps ){
+      Thread.sleep(java.util.concurrent.ThreadLocalRandom.current().nextInt(3, 17), 0);
+    }
+  }
+
+  Sqlite openDb(String name){
+    final Sqlite db = Sqlite.open(name, CApi.SQLITE_OPEN_READWRITE|
+                                  CApi.SQLITE_OPEN_CREATE|
+                                  CApi.SQLITE_OPEN_EXRESCODE);
+    ++metrics.dbOpen;
+    return db;
+  }
+
+  Sqlite openDb(){ return openDb(":memory:"); }
+
+  void testOpenDb1(){
+    Sqlite db = openDb();
+    affirm( 0!=db.nativeHandle().getNativePointer() );
+    db.close();
+    affirm( null==db.nativeHandle() );
+
+    SqliteException ex = null;
+    try {
+      db = openDb("/no/such/dir/.../probably");
+    }catch(SqliteException e){
+      ex = e;
+    }
+    affirm( ex!=null );
+    affirm( ex.errcode() != 0 );
+    affirm( ex.extendedErrcode() != 0 );
+    affirm( ex.errorOffset() < 0 );
+    // there's no reliable way to predict what ex.systemErrno() might be
+  }
+
+  void testPrepare1(){
+    try (Sqlite db = openDb()) {
+      Sqlite.Stmt stmt = db.prepare("SELECT 1");
+      affirm( null!=stmt.nativeHandle() );
+      affirm( CApi.SQLITE_ROW == stmt.step() );
+      affirm( CApi.SQLITE_DONE == stmt.step() );
+      stmt.reset();
+      affirm( CApi.SQLITE_ROW == stmt.step() );
+      affirm( CApi.SQLITE_DONE == stmt.step() );
+      affirm( 0 == stmt.finalizeStmt() );
+      affirm( null==stmt.nativeHandle() );
+
+      stmt = db.prepare("SELECT 1");
+      affirm( CApi.SQLITE_ROW == stmt.step() );
+      affirm( 0 == stmt.finalizeStmt() )
+        /* getting a non-0 out of sqlite3_finalize() is tricky */;
+      affirm( null==stmt.nativeHandle() );
+    }
+  }
+
+  void testUdfScalar(){
+    final ValueHolder xDestroyCalled = new ValueHolder<>(0);
+    try (Sqlite db = openDb()) {
+      execSql(db, "create table t(a); insert into t(a) values(1),(2),(3)");
+      final ValueHolder vh = new ValueHolder<>(0);
+      final ScalarFunction f = new ScalarFunction(){
+          public void xFunc(SqlFunction.Arguments args){
+            for( SqlFunction.Arguments.Arg arg : args ){
+              vh.value += arg.getInt();
+            }
+          }
+          public void xDestroy(){
+            ++xDestroyCalled.value;
+          }
+        };
+      db.createFunction("myfunc", -1, f);
+      execSql(db, "select myfunc(1,2,3)");
+      affirm( 6 == vh.value );
+      vh.value = 0;
+      execSql(db, "select myfunc(-1,-2,-3)");
+      affirm( -6 == vh.value );
+      affirm( 0 == xDestroyCalled.value );
+    }
+    affirm( 1 == xDestroyCalled.value );
+  }
+
+  void testUdfAggregate(){
+    final ValueHolder xDestroyCalled = new ValueHolder<>(0);
+    final ValueHolder vh = new ValueHolder<>(0);
+    try (Sqlite db = openDb()) {
+      execSql(db, "create table t(a); insert into t(a) values(1),(2),(3)");
+      final AggregateFunction f = new AggregateFunction(){
+          public void xStep(SqlFunction.Arguments args){
+            final ValueHolder agg = this.getAggregateState(args, 0);
+            for( SqlFunction.Arguments.Arg arg : args ){
+              agg.value += arg.getInt();
+            }
+          }
+          public void xFinal(SqlFunction.Arguments args){
+            final Integer v = this.takeAggregateState(args);
+            if( null==v ) args.resultNull();
+            else args.resultInt(v);
+            vh.value = v;
+          }
+          public void xDestroy(){
+            ++xDestroyCalled.value;
+          }
+        };
+      db.createFunction("myagg", -1, f);
+      execSql(db, "select myagg(a) from t");
+      affirm( 6 == vh.value );
+      affirm( 0 == xDestroyCalled.value );
+    }
+    affirm( 1 == xDestroyCalled.value );
+  }
+
+  private void runTests(boolean fromThread) throws Exception {
+    List mlist = testMethods;
+    affirm( null!=mlist );
+    if( shuffle ){
+      mlist = new ArrayList<>( testMethods.subList(0, testMethods.size()) );
+      java.util.Collections.shuffle(mlist);
+    }
+    if( listRunTests ){
+      synchronized(this.getClass()){
+        if( !fromThread ){
+          out("Initial test"," list: ");
+          for(java.lang.reflect.Method m : testMethods){
+            out(m.getName()+" ");
+          }
+          outln();
+          outln("(That list excludes some which are hard-coded to run.)");
+        }
+        out("Running"," tests: ");
+        for(java.lang.reflect.Method m : mlist){
+          out(m.getName()+" ");
+        }
+        outln();
+      }
+    }
+    for(java.lang.reflect.Method m : mlist){
+      nap();
+      try{
+        m.invoke(this);
+      }catch(java.lang.reflect.InvocationTargetException e){
+        outln("FAILURE: ",m.getName(),"(): ", e.getCause());
+        throw e;
+      }
+    }
+    synchronized( this.getClass() ){
+      ++nTestRuns;
+    }
+  }
+
+  public void run() {
+    try {
+      runTests(0!=this.tId);
+    }catch(Exception e){
+      synchronized( listErrors ){
+        listErrors.add(e);
+      }
+    }finally{
+      affirm( CApi.sqlite3_java_uncache_thread() );
+      affirm( !CApi.sqlite3_java_uncache_thread() );
+    }
+  }
+
+  /**
+     Runs the basic sqlite3 JNI binding sanity-check suite.
+
+     CLI flags:
+
+     -q|-quiet: disables most test output.
+
+     -t|-thread N: runs the tests in N threads
+      concurrently. Default=1.
+
+     -r|-repeat N: repeats the tests in a loop N times, each one
+      consisting of the -thread value's threads.
+
+     -shuffle: randomizes the order of most of the test functions.
+
+     -naps: sleep small random intervals between tests in order to add
+     some chaos for cross-thread contention.
+
+     -list-tests: outputs the list of tests being run, minus some
+      which are hard-coded. This is noisy in multi-threaded mode.
+
+     -fail: forces an exception to be thrown during the test run.  Use
+     with -shuffle to make its appearance unpredictable.
+
+     -v: emit some developer-mode info at the end.
+  */
+  public static void main(String[] args) throws Exception {
+    Integer nThread = 1;
+    boolean doSomethingForDev = false;
+    Integer nRepeat = 1;
+    boolean forceFail = false;
+    boolean sqlLog = false;
+    boolean configLog = false;
+    boolean squelchTestOutput = false;
+    for( int i = 0; i < args.length; ){
+      String arg = args[i++];
+      if(arg.startsWith("-")){
+        arg = arg.replaceFirst("-+","");
+        if(arg.equals("v")){
+          doSomethingForDev = true;
+          //listBoundMethods();
+        }else if(arg.equals("t") || arg.equals("thread")){
+          nThread = Integer.parseInt(args[i++]);
+        }else if(arg.equals("r") || arg.equals("repeat")){
+          nRepeat = Integer.parseInt(args[i++]);
+        }else if(arg.equals("shuffle")){
+          shuffle = true;
+        }else if(arg.equals("list-tests")){
+          listRunTests = true;
+        }else if(arg.equals("fail")){
+          forceFail = true;
+        }else if(arg.equals("sqllog")){
+          sqlLog = true;
+        }else if(arg.equals("configlog")){
+          configLog = true;
+        }else if(arg.equals("naps")){
+          takeNaps = true;
+        }else if(arg.equals("q") || arg.equals("quiet")){
+          squelchTestOutput = true;
+        }else{
+          throw new IllegalArgumentException("Unhandled flag:"+arg);
+        }
+      }
+    }
+
+    if( sqlLog ){
+      if( CApi.sqlite3_compileoption_used("ENABLE_SQLLOG") ){
+        final ConfigSqllogCallback log = new ConfigSqllogCallback() {
+            @Override public void call(sqlite3 db, String msg, int op){
+              switch(op){
+                case 0: outln("Opening db: ",db); break;
+                case 1: outln("SQL ",db,": ",msg); break;
+                case 2: outln("Closing db: ",db); break;
+              }
+            }
+          };
+        int rc = CApi.sqlite3_config( log );
+        affirm( 0==rc );
+        rc = CApi.sqlite3_config( (ConfigSqllogCallback)null );
+        affirm( 0==rc );
+        rc = CApi.sqlite3_config( log );
+        affirm( 0==rc );
+      }else{
+        outln("WARNING: -sqllog is not active because library was built ",
+              "without SQLITE_ENABLE_SQLLOG.");
+      }
+    }
+    if( configLog ){
+      final ConfigLogCallback log = new ConfigLogCallback() {
+          @Override public void call(int code, String msg){
+            outln("ConfigLogCallback: ",ResultCode.getEntryForInt(code),": ", msg);
+          };
+        };
+      int rc = CApi.sqlite3_config( log );
+      affirm( 0==rc );
+      rc = CApi.sqlite3_config( (ConfigLogCallback)null );
+      affirm( 0==rc );
+      rc = CApi.sqlite3_config( log );
+      affirm( 0==rc );
+    }
+
+    quietMode = squelchTestOutput;
+    outln("If you just saw warning messages regarding CallStaticObjectMethod, ",
+          "you are very likely seeing the side effects of a known openjdk8 ",
+          "bug. It is unsightly but does not affect the library.");
+
+    {
+      // Build list of tests to run from the methods named test*().
+      testMethods = new ArrayList<>();
+      int nSkipped = 0;
+      for(final java.lang.reflect.Method m : Tester2.class.getDeclaredMethods()){
+        final String name = m.getName();
+        if( name.equals("testFail") ){
+          if( forceFail ){
+            testMethods.add(m);
+          }
+        }else if( !m.isAnnotationPresent( ManualTest.class ) ){
+          if( nThread>1 && m.isAnnotationPresent( SingleThreadOnly.class ) ){
+            if( 0==nSkipped++ ){
+              out("Skipping tests in multi-thread mode:");
+            }
+            out(" "+name+"()");
+          }else if( name.startsWith("test") ){
+            testMethods.add(m);
+          }
+        }
+      }
+      if( nSkipped>0 ) out("\n");
+    }
+
+    final long timeStart = System.currentTimeMillis();
+    int nLoop = 0;
+    switch( CApi.sqlite3_threadsafe() ){ /* Sanity checking */
+      case 0:
+        affirm( CApi.SQLITE_ERROR==CApi.sqlite3_config( CApi.SQLITE_CONFIG_SINGLETHREAD ),
+                "Could not switch to single-thread mode." );
+        affirm( CApi.SQLITE_ERROR==CApi.sqlite3_config( CApi.SQLITE_CONFIG_MULTITHREAD ),
+                "Could switch to multithread mode."  );
+        affirm( CApi.SQLITE_ERROR==CApi.sqlite3_config( CApi.SQLITE_CONFIG_SERIALIZED ),
+                "Could not switch to serialized threading mode."  );
+        outln("This is a single-threaded build. Not using threads.");
+        nThread = 1;
+        break;
+      case 1:
+      case 2:
+        affirm( 0==CApi.sqlite3_config( CApi.SQLITE_CONFIG_SINGLETHREAD ),
+                "Could not switch to single-thread mode." );
+        affirm( 0==CApi.sqlite3_config( CApi.SQLITE_CONFIG_MULTITHREAD ),
+                "Could not switch to multithread mode."  );
+        affirm( 0==CApi.sqlite3_config( CApi.SQLITE_CONFIG_SERIALIZED ),
+                "Could not switch to serialized threading mode."  );
+        break;
+      default:
+        affirm( false, "Unhandled SQLITE_THREADSAFE value." );
+    }
+    outln("libversion_number: ",
+          CApi.sqlite3_libversion_number(),"\n",
+          CApi.sqlite3_libversion(),"\n",CApi.SQLITE_SOURCE_ID,"\n",
+          "SQLITE_THREADSAFE=",CApi.sqlite3_threadsafe());
+    final boolean showLoopCount = (nRepeat>1 && nThread>1);
+    if( showLoopCount ){
+      outln("Running ",nRepeat," loop(s) with ",nThread," thread(s) each.");
+    }
+    if( takeNaps ) outln("Napping between tests is enabled.");
+    for( int n = 0; n < nRepeat; ++n ){
+      ++nLoop;
+      if( showLoopCount ) out((1==nLoop ? "" : " ")+nLoop);
+      if( nThread<=1 ){
+        new Tester2(0).runTests(false);
+        continue;
+      }
+      Tester2.mtMode = true;
+      final ExecutorService ex = Executors.newFixedThreadPool( nThread );
+      for( int i = 0; i < nThread; ++i ){
+        ex.submit( new Tester2(i), i );
+      }
+      ex.shutdown();
+      try{
+        ex.awaitTermination(nThread*200, java.util.concurrent.TimeUnit.MILLISECONDS);
+        ex.shutdownNow();
+      }catch (InterruptedException ie){
+        ex.shutdownNow();
+        Thread.currentThread().interrupt();
+      }
+      if( !listErrors.isEmpty() ){
+        quietMode = false;
+        outln("TEST ERRORS:");
+        Exception err = null;
+        for( Exception e : listErrors ){
+          e.printStackTrace();
+          if( null==err ) err = e;
+        }
+        if( null!=err ) throw err;
+      }
+    }
+    if( showLoopCount ) outln();
+    quietMode = false;
+
+    final long timeEnd = System.currentTimeMillis();
+    outln("Tests done. Metrics across ",nTestRuns," total iteration(s):");
+    outln("\tAssertions checked: ",affirmCount);
+    outln("\tDatabases opened: ",metrics.dbOpen);
+    if( doSomethingForDev ){
+      CApi.sqlite3_jni_internal_details();
+    }
+    affirm( 0==CApi.sqlite3_release_memory(1) );
+    CApi.sqlite3_shutdown();
+    int nMethods = 0;
+    int nNatives = 0;
+    int nCanonical = 0;
+    final java.lang.reflect.Method[] declaredMethods =
+      CApi.class.getDeclaredMethods();
+    for(java.lang.reflect.Method m : declaredMethods){
+      final int mod = m.getModifiers();
+      if( 0!=(mod & java.lang.reflect.Modifier.STATIC) ){
+        final String name = m.getName();
+        if(name.startsWith("sqlite3_")){
+          ++nMethods;
+          if( 0!=(mod & java.lang.reflect.Modifier.NATIVE) ){
+            ++nNatives;
+          }
+        }
+      }
+    }
+    outln("\tCApi.sqlite3_*() methods: "+
+          nMethods+" total, with "+
+          nNatives+" native, "+
+          (nMethods - nNatives)+" Java"
+    );
+    outln("\tTotal test time = "
+          +(timeEnd - timeStart)+"ms");
+  }
+}
diff --git a/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/ValueHolder.java b/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/ValueHolder.java
new file mode 100644
index 00000000..009936a4
--- /dev/null
+++ b/sqlite/ext/jni/src/org/sqlite/jni/wrapper1/ValueHolder.java
@@ -0,0 +1,25 @@
+/*
+** 2023-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.
+**
+*************************************************************************
+** This file contains a set of tests for the sqlite3 JNI bindings.
+*/
+package org.sqlite.jni.wrapper1;
+
+/**
+   A helper class which simply holds a single value. Its primary use
+   is for communicating values out of anonymous classes, as doing so
+   requires a "final" reference.
+*/
+public class ValueHolder {
+  public T value;
+  public ValueHolder(){}
+  public ValueHolder(T v){value = v;}
+}
diff --git a/sqlite/ext/jni/src/tests/000-000-sanity.test b/sqlite/ext/jni/src/tests/000-000-sanity.test
new file mode 100644
index 00000000..4ccbece3
--- /dev/null
+++ b/sqlite/ext/jni/src/tests/000-000-sanity.test
@@ -0,0 +1,53 @@
+/*
+** This is a comment. There are many like it but this one is mine.
+**
+** SCRIPT_MODULE_NAME:      sanity-check
+** 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
+**
+*/
+--print starting up 😃
+--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
+--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 reached the end 😃
diff --git a/sqlite/ext/jni/src/tests/000-001-ignored.test b/sqlite/ext/jni/src/tests/000-001-ignored.test
new file mode 100644
index 00000000..5af852e1
--- /dev/null
+++ b/sqlite/ext/jni/src/tests/000-001-ignored.test
@@ -0,0 +1,9 @@
+/*
+** This script must be marked as ignored because it contains
+** content which triggers that condition.
+**
+** SCRIPT_MODULE_NAME:        ignored
+**
+*/
+
+|
diff --git a/sqlite/ext/jni/src/tests/900-001-fts.test b/sqlite/ext/jni/src/tests/900-001-fts.test
new file mode 100644
index 00000000..65285e86
--- /dev/null
+++ b/sqlite/ext/jni/src/tests/900-001-fts.test
@@ -0,0 +1,12 @@
+/*
+** SCRIPT_MODULE_NAME:      fts5-sanity-checks
+** xREQUIRED_PROPERTIES:     FTS5
+**
+*/
+
+--testcase 1.0
+CREATE VIRTUAL TABLE email USING fts5(sender, title, body);
+insert into email values('fred','Help!','Dear Sir...');
+insert into email values('barney','Assistance','Dear Madam...');
+select * from email where email match 'assistance';
+--result barney Assistance {Dear Madam...}
diff --git a/sqlite/ext/lsm1/lsm_vtab.c b/sqlite/ext/lsm1/lsm_vtab.c
index bb146029..8c21923e 100644
--- a/sqlite/ext/lsm1/lsm_vtab.c
+++ b/sqlite/ext/lsm1/lsm_vtab.c
@@ -1061,6 +1061,11 @@ static sqlite3_module lsm1Module = {
   lsm1Rollback,            /* xRollback */
   0,                       /* xFindMethod */
   0,                       /* xRename */
+  0,                       /* xSavepoint */
+  0,                       /* xRelease */
+  0,                       /* xRollbackTo */
+  0,                       /* xShadowName */
+  0                        /* xIntegrity */
 };
 
 
diff --git a/sqlite/ext/misc/amatch.c b/sqlite/ext/misc/amatch.c
index bafa4328..dd9bee53 100644
--- a/sqlite/ext/misc/amatch.c
+++ b/sqlite/ext/misc/amatch.c
@@ -1475,7 +1475,8 @@ static sqlite3_module amatchModule = {
   0,                      /* xSavepoint */
   0,                      /* xRelease */
   0,                      /* xRollbackTo */
-  0                       /* xShadowName */
+  0,                      /* xShadowName */
+  0                       /* xIntegrity */
 };
 
 #endif /* SQLITE_OMIT_VIRTUALTABLE */
diff --git a/sqlite/ext/misc/btreeinfo.c b/sqlite/ext/misc/btreeinfo.c
index 22f82681..02f8c031 100644
--- a/sqlite/ext/misc/btreeinfo.c
+++ b/sqlite/ext/misc/btreeinfo.c
@@ -411,7 +411,8 @@ int sqlite3BinfoRegister(sqlite3 *db){
     0,                           /* xSavepoint */
     0,                           /* xRelease */
     0,                           /* xRollbackTo */
-    0                            /* xShadowName */
+    0,                           /* xShadowName */
+    0                            /* xIntegrity */
   };
   return sqlite3_create_module(db, "sqlite_btreeinfo", &binfo_module, 0);
 }
diff --git a/sqlite/ext/misc/carray.c b/sqlite/ext/misc/carray.c
index 709c894f..b1caa98c 100644
--- a/sqlite/ext/misc/carray.c
+++ b/sqlite/ext/misc/carray.c
@@ -409,6 +409,11 @@ static sqlite3_module carrayModule = {
   0,                         /* xRollback */
   0,                         /* xFindMethod */
   0,                         /* xRename */
+  0,                         /* xSavepoint */
+  0,                         /* xRelease */
+  0,                         /* xRollbackTo */
+  0,                         /* xShadow */
+  0                          /* xIntegrity */
 };
 
 /*
diff --git a/sqlite/ext/misc/closure.c b/sqlite/ext/misc/closure.c
index db9b2b73..79a5a21d 100644
--- a/sqlite/ext/misc/closure.c
+++ b/sqlite/ext/misc/closure.c
@@ -939,7 +939,8 @@ static sqlite3_module closureModule = {
   0,                      /* xSavepoint */
   0,                      /* xRelease */
   0,                      /* xRollbackTo */
-  0                       /* xShadowName */
+  0,                      /* xShadowName */
+  0                       /* xIntegrity */
 };
 
 #endif /* SQLITE_OMIT_VIRTUALTABLE */
diff --git a/sqlite/ext/misc/completion.c b/sqlite/ext/misc/completion.c
index d9e7b859..987595a3 100644
--- a/sqlite/ext/misc/completion.c
+++ b/sqlite/ext/misc/completion.c
@@ -470,7 +470,8 @@ static sqlite3_module completionModule = {
   0,                         /* xSavepoint */
   0,                         /* xRelease */
   0,                         /* xRollbackTo */
-  0                          /* xShadowName */
+  0,                         /* xShadowName */
+  0                          /* xIntegrity */
 };
 
 #endif /* SQLITE_OMIT_VIRTUALTABLE */
diff --git a/sqlite/ext/misc/csv.c b/sqlite/ext/misc/csv.c
index 870a0cf6..b38500f4 100644
--- a/sqlite/ext/misc/csv.c
+++ b/sqlite/ext/misc/csv.c
@@ -897,6 +897,11 @@ static sqlite3_module CsvModule = {
   0,                       /* xRollback */
   0,                       /* xFindMethod */
   0,                       /* xRename */
+  0,                       /* xSavepoint */
+  0,                       /* xRelease */
+  0,                       /* xRollbackTo */
+  0,                       /* xShadowName */
+  0                        /* xIntegrity */
 };
 
 #ifdef SQLITE_TEST
@@ -929,6 +934,11 @@ static sqlite3_module CsvModuleFauxWrite = {
   0,                       /* xRollback */
   0,                       /* xFindMethod */
   0,                       /* xRename */
+  0,                       /* xSavepoint */
+  0,                       /* xRelease */
+  0,                       /* xRollbackTo */
+  0,                       /* xShadowName */
+  0                        /* xIntegrity */
 };
 #endif /* SQLITE_TEST */
 
diff --git a/sqlite/ext/misc/decimal.c b/sqlite/ext/misc/decimal.c
index 4495cae4..9365ae68 100644
--- a/sqlite/ext/misc/decimal.c
+++ b/sqlite/ext/misc/decimal.c
@@ -58,41 +58,24 @@ static void decimal_free(Decimal *p){
 }
 
 /*
-** Allocate a new Decimal object.  Initialize it to the number given
-** by the input string.
+** Allocate a new Decimal object initialized to the text in zIn[].
+** Return NULL if any kind of error occurs.
 */
-static Decimal *decimal_new(
-  sqlite3_context *pCtx,
-  sqlite3_value *pIn,
-  int nAlt,
-  const unsigned char *zAlt
-){
-  Decimal *p;
-  int n, i;
-  const unsigned char *zIn;
+static Decimal *decimalNewFromText(const char *zIn, int n){
+  Decimal *p = 0;
+  int i;
   int iExp = 0;
+
   p = sqlite3_malloc( sizeof(*p) );
-  if( p==0 ) goto new_no_mem;
+  if( p==0 ) goto new_from_text_failed;
   p->sign = 0;
   p->oom = 0;
   p->isInit = 1;
   p->isNull = 0;
   p->nDigit = 0;
   p->nFrac = 0;
-  if( zAlt ){
-    n = nAlt,
-    zIn = zAlt;
-  }else{
-    if( sqlite3_value_type(pIn)==SQLITE_NULL ){
-      p->a = 0;
-      p->isNull = 1;
-      return p;
-    }
-    n = sqlite3_value_bytes(pIn);
-    zIn = sqlite3_value_text(pIn);
-  }
   p->a = sqlite3_malloc64( n+1 );
-  if( p->a==0 ) goto new_no_mem;
+  if( p->a==0 ) goto new_from_text_failed;
   for(i=0; isspace(zIn[i]); i++){}
   if( zIn[i]=='-' ){
     p->sign = 1;
@@ -143,7 +126,7 @@ static Decimal *decimal_new(
     }
     if( iExp>0 ){   
       p->a = sqlite3_realloc64(p->a, p->nDigit + iExp + 1 );
-      if( p->a==0 ) goto new_no_mem;
+      if( p->a==0 ) goto new_from_text_failed;
       memset(p->a+p->nDigit, 0, iExp);
       p->nDigit += iExp;
     }
@@ -162,7 +145,7 @@ static Decimal *decimal_new(
     }
     if( iExp>0 ){
       p->a = sqlite3_realloc64(p->a, p->nDigit + iExp + 1 );
-      if( p->a==0 ) goto new_no_mem;
+      if( p->a==0 ) goto new_from_text_failed;
       memmove(p->a+iExp, p->a, p->nDigit);
       memset(p->a, 0, iExp);
       p->nDigit += iExp;
@@ -171,7 +154,76 @@ static Decimal *decimal_new(
   }
   return p;
 
-new_no_mem:
+new_from_text_failed:
+  if( p ){
+    if( p->a ) sqlite3_free(p->a);
+    sqlite3_free(p);
+  }
+  return 0;
+}
+
+/* Forward reference */
+static Decimal *decimalFromDouble(double);
+
+/*
+** Allocate a new Decimal object from an sqlite3_value.  Return a pointer
+** to the new object, or NULL if there is an error.  If the pCtx argument
+** is not NULL, then errors are reported on it as well.
+**
+** If the pIn argument is SQLITE_TEXT or SQLITE_INTEGER, it is converted
+** directly into a Decimal.  For SQLITE_FLOAT or for SQLITE_BLOB of length
+** 8 bytes, the resulting double value is expanded into its decimal equivalent.
+** If pIn is NULL or if it is a BLOB that is not exactly 8 bytes in length,
+** then NULL is returned.
+*/
+static Decimal *decimal_new(
+  sqlite3_context *pCtx,       /* Report error here, if not null */
+  sqlite3_value *pIn,          /* Construct the decimal object from this */
+  int bTextOnly                /* Always interpret pIn as text if true */
+){
+  Decimal *p = 0;
+  int eType = sqlite3_value_type(pIn);
+  if( bTextOnly && (eType==SQLITE_FLOAT || eType==SQLITE_BLOB) ){
+    eType = SQLITE_TEXT;
+  }
+  switch( eType ){
+    case SQLITE_TEXT:
+    case SQLITE_INTEGER: {
+      const char *zIn = (const char*)sqlite3_value_text(pIn);
+      int n = sqlite3_value_bytes(pIn);
+      p = decimalNewFromText(zIn, n);
+      if( p==0 ) goto new_failed;
+      break;
+    }
+
+    case SQLITE_FLOAT: {
+      p = decimalFromDouble(sqlite3_value_double(pIn));
+      break;
+    }
+
+    case SQLITE_BLOB: {
+      const unsigned char *x;
+      unsigned int i;
+      sqlite3_uint64 v = 0;
+      double r;
+
+      if( sqlite3_value_bytes(pIn)!=sizeof(r) ) break;
+      x = sqlite3_value_blob(pIn);
+      for(i=0; ioom ){
+    sqlite3_result_error_nomem(pCtx);
+    return;
+  }
+  if( p->isNull ){
+    sqlite3_result_null(pCtx);
+    return;
+  }
+  for(nDigit=p->nDigit; nDigit>0 && p->a[nDigit-1]==0; nDigit--){}
+  for(nZero=0; nZeroa[nZero]==0; nZero++){}
+  nFrac = p->nFrac + (nDigit - p->nDigit);
+  nDigit -= nZero;
+  z = sqlite3_malloc( nDigit+20 );
+  if( z==0 ){
+    sqlite3_result_error_nomem(pCtx);
+    return;
+  }
+  if( nDigit==0 ){
+    zero = 0;
+    a = &zero;
+    nDigit = 1;
+    nFrac = 0;
+  }else{
+    a = &p->a[nZero];
+  }
+  if( p->sign && nDigit>0 ){
+    z[0] = '-';
+  }else{
+    z[0] = '+';
+  }
+  z[1] = a[0]+'0';
+  z[2] = '.';
+  if( nDigit==1 ){
+    z[3] = '0';
+    i = 4;
+  }else{
+    for(i=1; iisNull ) goto cmp_done;
-  pB = decimal_new(context, argv[1], 0, 0);
+  pB = decimal_new(context, argv[1], 1);
   if( pB==0 || pB->isNull ) goto cmp_done;
   rc = decimal_cmp(pA, pB);
   if( rc<0 ) rc = -1;
@@ -338,7 +435,7 @@ static void decimal_expand(Decimal *p, int nDigit, int nFrac){
 }
 
 /*
-** Add the value pB into pA.
+** Add the value pB into pA.   A := A + B.
 **
 ** Both pA and pB might become denormalized by this routine.
 */
@@ -407,6 +504,172 @@ static void decimal_add(Decimal *pA, Decimal *pB){
   }
 }
 
+/*
+** Multiply A by B.   A := A * B
+**
+** All significant digits after the decimal point are retained.
+** Trailing zeros after the decimal point are omitted as long as
+** the number of digits after the decimal point is no less than
+** either the number of digits in either input.
+*/
+static void decimalMul(Decimal *pA, Decimal *pB){
+  signed char *acc = 0;
+  int i, j, k;
+  int minFrac;
+
+  if( pA==0 || pA->oom || pA->isNull
+   || pB==0 || pB->oom || pB->isNull 
+  ){
+    goto mul_end;
+  }
+  acc = sqlite3_malloc64( pA->nDigit + pB->nDigit + 2 );
+  if( acc==0 ){
+    pA->oom = 1;
+    goto mul_end;
+  }
+  memset(acc, 0, pA->nDigit + pB->nDigit + 2);
+  minFrac = pA->nFrac;
+  if( pB->nFracnFrac;
+  for(i=pA->nDigit-1; i>=0; i--){
+    signed char f = pA->a[i];
+    int carry = 0, x;
+    for(j=pB->nDigit-1, k=i+j+3; j>=0; j--, k--){
+      x = acc[k] + f*pB->a[j] + carry;
+      acc[k] = x%10;
+      carry = x/10;
+    }
+    x = acc[k] + carry;
+    acc[k] = x%10;
+    acc[k-1] += x/10;
+  }
+  sqlite3_free(pA->a);
+  pA->a = acc;
+  acc = 0;
+  pA->nDigit += pB->nDigit + 2;
+  pA->nFrac += pB->nFrac;
+  pA->sign ^= pB->sign;
+  while( pA->nFrac>minFrac && pA->a[pA->nDigit-1]==0 ){
+    pA->nFrac--;
+    pA->nDigit--;
+  }
+
+mul_end:
+  sqlite3_free(acc);
+}
+
+/*
+** Create a new Decimal object that contains an integer power of 2.
+*/
+static Decimal *decimalPow2(int N){
+  Decimal *pA = 0;      /* The result to be returned */
+  Decimal *pX = 0;      /* Multiplier */
+  if( N<-20000 || N>20000 ) goto pow2_fault;
+  pA = decimalNewFromText("1.0", 3);
+  if( pA==0 || pA->oom ) goto pow2_fault;
+  if( N==0 ) return pA;
+  if( N>0 ){
+    pX = decimalNewFromText("2.0", 3);
+  }else{
+    N = -N;
+    pX = decimalNewFromText("0.5", 3);
+  }
+  if( pX==0 || pX->oom ) goto pow2_fault;
+  while( 1 /* Exit by break */ ){
+    if( N & 1 ){
+      decimalMul(pA, pX);
+      if( pA->oom ) goto pow2_fault;
+    }
+    N >>= 1;
+    if( N==0 ) break;
+    decimalMul(pX, pX);
+  }
+  decimal_free(pX);
+  return pA;
+
+pow2_fault:
+  decimal_free(pA);
+  decimal_free(pX);
+  return 0;
+}
+
+/*
+** Use an IEEE754 binary64 ("double") to generate a new Decimal object.
+*/
+static Decimal *decimalFromDouble(double r){
+  sqlite3_int64 m, a;
+  int e;
+  int isNeg;
+  Decimal *pA;
+  Decimal *pX;
+  char zNum[100];
+  if( r<0.0 ){
+    isNeg = 1;
+    r = -r;
+  }else{
+    isNeg = 0;
+  }
+  memcpy(&a,&r,sizeof(a));
+  if( a==0 ){
+    e = 0;
+    m = 0;
+  }else{
+    e = a>>52;
+    m = a & ((((sqlite3_int64)1)<<52)-1);
+    if( e==0 ){
+      m <<= 1;
+    }else{
+      m |= ((sqlite3_int64)1)<<52;
+    }
+    while( e<1075 && m>0 && (m&1)==0 ){
+      m >>= 1;
+      e++;
+    }
+    if( isNeg ) m = -m;
+    e = e - 1075;
+    if( e>971 ){
+      return 0;  /* A NaN or an Infinity */
+    }
+  }
+
+  /* At this point m is the integer significand and e is the exponent */
+  sqlite3_snprintf(sizeof(zNum), zNum, "%lld", m);
+  pA = decimalNewFromText(zNum, (int)strlen(zNum));
+  pX = decimalPow2(e);
+  decimalMul(pA, pX);
+  decimal_free(pX);
+  return pA;
+}
+
+/*
+** SQL Function:   decimal(X)
+** OR:             decimal_exp(X)
+**
+** Convert input X into decimal and then back into text.
+**
+** If X is originally a float, then a full decimal expansion of that floating
+** point value is done.  Or if X is an 8-byte blob, it is interpreted
+** as a float and similarly expanded.
+**
+** The decimal_exp(X) function returns the result in exponential notation.
+** decimal(X) returns a complete decimal, without the e+NNN at the end.
+*/
+static void decimalFunc(
+  sqlite3_context *context,
+  int argc,
+  sqlite3_value **argv
+){
+  Decimal *p =  decimal_new(context, argv[0], 0);
+  UNUSED_PARAMETER(argc);
+  if( p ){
+    if( sqlite3_user_data(context)!=0 ){
+      decimal_result_sci(context, p);
+    }else{
+      decimal_result(context, p);
+    }
+    decimal_free(p);
+  }
+}
+
 /*
 ** Compare text in decimal order.
 */
@@ -417,8 +680,8 @@ static int decimalCollFunc(
 ){
   const unsigned char *zA = (const unsigned char*)pKey1;
   const unsigned char *zB = (const unsigned char*)pKey2;
-  Decimal *pA = decimal_new(0, 0, nKey1, zA);
-  Decimal *pB = decimal_new(0, 0, nKey2, zB);
+  Decimal *pA = decimalNewFromText((const char*)zA, nKey1);
+  Decimal *pB = decimalNewFromText((const char*)zB, nKey2);
   int rc;
   UNUSED_PARAMETER(notUsed);
   if( pA==0 || pB==0 ){
@@ -443,8 +706,8 @@ static void decimalAddFunc(
   int argc,
   sqlite3_value **argv
 ){
-  Decimal *pA = decimal_new(context, argv[0], 0, 0);
-  Decimal *pB = decimal_new(context, argv[1], 0, 0);
+  Decimal *pA = decimal_new(context, argv[0], 1);
+  Decimal *pB = decimal_new(context, argv[1], 1);
   UNUSED_PARAMETER(argc);
   decimal_add(pA, pB);
   decimal_result(context, pA);
@@ -456,8 +719,8 @@ static void decimalSubFunc(
   int argc,
   sqlite3_value **argv
 ){
-  Decimal *pA = decimal_new(context, argv[0], 0, 0);
-  Decimal *pB = decimal_new(context, argv[1], 0, 0);
+  Decimal *pA = decimal_new(context, argv[0], 1);
+  Decimal *pB = decimal_new(context, argv[1], 1);
   UNUSED_PARAMETER(argc);
   if( pB ){
     pB->sign = !pB->sign;
@@ -495,7 +758,7 @@ static void decimalSumStep(
     p->nFrac = 0;
   }
   if( sqlite3_value_type(argv[0])==SQLITE_NULL ) return;
-  pArg = decimal_new(context, argv[0], 0, 0);
+  pArg = decimal_new(context, argv[0], 1);
   decimal_add(p, pArg);
   decimal_free(pArg);
 }
@@ -510,7 +773,7 @@ static void decimalSumInverse(
   p = sqlite3_aggregate_context(context, sizeof(*p));
   if( p==0 ) return;
   if( sqlite3_value_type(argv[0])==SQLITE_NULL ) return;
-  pArg = decimal_new(context, argv[0], 0, 0);
+  pArg = decimal_new(context, argv[0], 1);
   if( pArg ) pArg->sign = !pArg->sign;
   decimal_add(p, pArg);
   decimal_free(pArg);
@@ -531,66 +794,49 @@ static void decimalSumFinalize(sqlite3_context *context){
 ** SQL Function:   decimal_mul(X, Y)
 **
 ** Return the product of X and Y.
-**
-** All significant digits after the decimal point are retained.
-** Trailing zeros after the decimal point are omitted as long as
-** the number of digits after the decimal point is no less than
-** either the number of digits in either input.
 */
 static void decimalMulFunc(
   sqlite3_context *context,
   int argc,
   sqlite3_value **argv
 ){
-  Decimal *pA = decimal_new(context, argv[0], 0, 0);
-  Decimal *pB = decimal_new(context, argv[1], 0, 0);
-  signed char *acc = 0;
-  int i, j, k;
-  int minFrac;
+  Decimal *pA = decimal_new(context, argv[0], 1);
+  Decimal *pB = decimal_new(context, argv[1], 1);
   UNUSED_PARAMETER(argc);
   if( pA==0 || pA->oom || pA->isNull
    || pB==0 || pB->oom || pB->isNull 
   ){
     goto mul_end;
   }
-  acc = sqlite3_malloc64( pA->nDigit + pB->nDigit + 2 );
-  if( acc==0 ){
-    sqlite3_result_error_nomem(context);
+  decimalMul(pA, pB);
+  if( pA->oom ){
     goto mul_end;
   }
-  memset(acc, 0, pA->nDigit + pB->nDigit + 2);
-  minFrac = pA->nFrac;
-  if( pB->nFracnFrac;
-  for(i=pA->nDigit-1; i>=0; i--){
-    signed char f = pA->a[i];
-    int carry = 0, x;
-    for(j=pB->nDigit-1, k=i+j+3; j>=0; j--, k--){
-      x = acc[k] + f*pB->a[j] + carry;
-      acc[k] = x%10;
-      carry = x/10;
-    }
-    x = acc[k] + carry;
-    acc[k] = x%10;
-    acc[k-1] += x/10;
-  }
-  sqlite3_free(pA->a);
-  pA->a = acc;
-  acc = 0;
-  pA->nDigit += pB->nDigit + 2;
-  pA->nFrac += pB->nFrac;
-  pA->sign ^= pB->sign;
-  while( pA->nFrac>minFrac && pA->a[pA->nDigit-1]==0 ){
-    pA->nFrac--;
-    pA->nDigit--;
-  }
   decimal_result(context, pA);
 
 mul_end:
-  sqlite3_free(acc);
   decimal_free(pA);
   decimal_free(pB);
 }
 
+/*
+** SQL Function:   decimal_pow2(N)
+**
+** Return the N-th power of 2.  N must be an integer.
+*/
+static void decimalPow2Func(
+  sqlite3_context *context,
+  int argc,
+  sqlite3_value **argv
+){
+  UNUSED_PARAMETER(argc);
+  if( sqlite3_value_type(argv[0])==SQLITE_INTEGER ){
+    Decimal *pA = decimalPow2(sqlite3_value_int(argv[0]));
+    decimal_result_sci(context, pA);
+    decimal_free(pA);
+  }
+}
+
 #ifdef _WIN32
 __declspec(dllexport)
 #endif
@@ -603,13 +849,16 @@ int sqlite3_decimal_init(
   static const struct {
     const char *zFuncName;
     int nArg;
+    int iArg;
     void (*xFunc)(sqlite3_context*,int,sqlite3_value**);
   } aFunc[] = {
-    { "decimal",       1,   decimalFunc        },
-    { "decimal_cmp",   2,   decimalCmpFunc     },
-    { "decimal_add",   2,   decimalAddFunc     },
-    { "decimal_sub",   2,   decimalSubFunc     },
-    { "decimal_mul",   2,   decimalMulFunc     },
+    { "decimal",       1, 0,  decimalFunc        },
+    { "decimal_exp",   1, 1,  decimalFunc        },
+    { "decimal_cmp",   2, 0,  decimalCmpFunc     },
+    { "decimal_add",   2, 0,  decimalAddFunc     },
+    { "decimal_sub",   2, 0,  decimalSubFunc     },
+    { "decimal_mul",   2, 0,  decimalMulFunc     },
+    { "decimal_pow2",  1, 0,  decimalPow2Func    },
   };
   unsigned int i;
   (void)pzErrMsg;  /* Unused parameter */
@@ -619,7 +868,7 @@ int sqlite3_decimal_init(
   for(i=0; i<(int)(sizeof(aFunc)/sizeof(aFunc[0])) && rc==SQLITE_OK; i++){
     rc = sqlite3_create_function(db, aFunc[i].zFuncName, aFunc[i].nArg,
                    SQLITE_UTF8|SQLITE_INNOCUOUS|SQLITE_DETERMINISTIC,
-                   0, aFunc[i].xFunc, 0, 0);
+                   aFunc[i].iArg ? db : 0, aFunc[i].xFunc, 0, 0);
   }
   if( rc==SQLITE_OK ){
     rc = sqlite3_create_window_function(db, "decimal_sum", 1,
diff --git a/sqlite/ext/misc/explain.c b/sqlite/ext/misc/explain.c
index 00951945..726af76b 100644
--- a/sqlite/ext/misc/explain.c
+++ b/sqlite/ext/misc/explain.c
@@ -293,6 +293,7 @@ static sqlite3_module explainModule = {
   0,                         /* xRelease */
   0,                         /* xRollbackTo */
   0,                         /* xShadowName */
+  0                          /* xIntegrity */
 };
 
 #endif /* SQLITE_OMIT_VIRTUALTABLE */
diff --git a/sqlite/ext/misc/fileio.c b/sqlite/ext/misc/fileio.c
index 7cdbd596..70546adf 100644
--- a/sqlite/ext/misc/fileio.c
+++ b/sqlite/ext/misc/fileio.c
@@ -983,6 +983,7 @@ static int fsdirRegister(sqlite3 *db){
     0,                         /* xRelease */
     0,                         /* xRollbackTo */
     0,                         /* xShadowName */
+    0                          /* xIntegrity */
   };
 
   int rc = sqlite3_create_module(db, "fsdir", &fsdirModule, 0);
diff --git a/sqlite/ext/misc/fossildelta.c b/sqlite/ext/misc/fossildelta.c
index 6a597e0d..e638737d 100644
--- a/sqlite/ext/misc/fossildelta.c
+++ b/sqlite/ext/misc/fossildelta.c
@@ -1058,7 +1058,8 @@ static sqlite3_module deltaparsevtabModule = {
   /* xSavepoint  */ 0,
   /* xRelease    */ 0,
   /* xRollbackTo */ 0,
-  /* xShadowName */ 0
+  /* xShadowName */ 0,
+  /* xIntegrity  */ 0
 };
 
 
diff --git a/sqlite/ext/misc/fuzzer.c b/sqlite/ext/misc/fuzzer.c
index 65d9d8df..92b7c0da 100644
--- a/sqlite/ext/misc/fuzzer.c
+++ b/sqlite/ext/misc/fuzzer.c
@@ -1165,6 +1165,11 @@ static sqlite3_module fuzzerModule = {
   0,                           /* xRollback */
   0,                           /* xFindMethod */
   0,                           /* xRename */
+  0,                           /* xSavepoint */
+  0,                           /* xRelease */
+  0,                           /* xRollbackTo */
+  0,                           /* xShadowName */
+  0                            /* xIntegrity */
 };
 
 #endif /* SQLITE_OMIT_VIRTUALTABLE */
diff --git a/sqlite/ext/misc/ieee754.c b/sqlite/ext/misc/ieee754.c
index ff5d2d33..99489fe9 100644
--- a/sqlite/ext/misc/ieee754.c
+++ b/sqlite/ext/misc/ieee754.c
@@ -256,6 +256,37 @@ static void ieee754func_to_blob(
   }
 }
 
+/*
+** SQL Function:   ieee754_inc(r,N)
+**
+** Move the floating point value r by N quantums and return the new
+** values.
+**
+** Behind the scenes: this routine merely casts r into a 64-bit unsigned
+** integer, adds N, then casts the value back into float.
+**
+** Example:  To find the smallest positive number:
+**
+**     SELECT ieee754_inc(0.0,+1);
+*/
+static void ieee754inc(
+  sqlite3_context *context,
+  int argc,
+  sqlite3_value **argv
+){
+  double r;
+  sqlite3_int64 N;
+  sqlite3_uint64 m1, m2;
+  double r2;
+  UNUSED_PARAMETER(argc);
+  r = sqlite3_value_double(argv[0]);
+  N = sqlite3_value_int64(argv[1]);
+  memcpy(&m1, &r, 8);
+  m2 = m1 + N;
+  memcpy(&r2, &m2, 8);
+  sqlite3_result_double(context, r2);
+}
+
 
 #ifdef _WIN32
 __declspec(dllexport)
@@ -277,7 +308,7 @@ int sqlite3_ieee_init(
     { "ieee754_exponent",  1,   2, ieee754func },
     { "ieee754_to_blob",   1,   0, ieee754func_to_blob },
     { "ieee754_from_blob", 1,   0, ieee754func_from_blob },
-
+    { "ieee754_inc",       2,   0, ieee754inc  },
   };
   unsigned int i;
   int rc = SQLITE_OK;
diff --git a/sqlite/ext/misc/memstat.c b/sqlite/ext/misc/memstat.c
index 800a86e7..c56af9f2 100644
--- a/sqlite/ext/misc/memstat.c
+++ b/sqlite/ext/misc/memstat.c
@@ -396,6 +396,7 @@ static sqlite3_module memstatModule = {
   0,                         /* xRelease */
   0,                         /* xRollbackTo */
   0,                         /* xShadowName */
+  0                          /* xIntegrity */
 };
 
 #endif /* SQLITE_OMIT_VIRTUALTABLE */
diff --git a/sqlite/ext/misc/mmapwarm.c b/sqlite/ext/misc/mmapwarm.c
index 5afa47bf..851f8b0e 100644
--- a/sqlite/ext/misc/mmapwarm.c
+++ b/sqlite/ext/misc/mmapwarm.c
@@ -38,7 +38,7 @@ int sqlite3_mmap_warm(sqlite3 *db, const char *zDb){
   int rc = SQLITE_OK;
   char *zSql = 0;
   int pgsz = 0;
-  int nTotal = 0;
+  unsigned int nTotal = 0;
 
   if( 0==sqlite3_get_autocommit(db) ) return SQLITE_MISUSE;
 
@@ -86,8 +86,8 @@ int sqlite3_mmap_warm(sqlite3 *db, const char *zDb){
         rc = p->xFetch(pFd, pgsz*iPg, pgsz, (void**)&pMap);
         if( rc!=SQLITE_OK || pMap==0 ) break;
 
-        nTotal += pMap[0];
-        nTotal += pMap[pgsz-1];
+        nTotal += (unsigned int)pMap[0];
+        nTotal += (unsigned int)pMap[pgsz-1];
 
         rc = p->xUnfetch(pFd, pgsz*iPg, (void*)pMap);
         if( rc!=SQLITE_OK ) break;
@@ -103,5 +103,6 @@ int sqlite3_mmap_warm(sqlite3 *db, const char *zDb){
     if( rc==SQLITE_OK ) rc = rc2;
   }
 
+  (void)nTotal;
   return rc;
 }
diff --git a/sqlite/ext/misc/pcachetrace.c b/sqlite/ext/misc/pcachetrace.c
new file mode 100644
index 00000000..3757d8d4
--- /dev/null
+++ b/sqlite/ext/misc/pcachetrace.c
@@ -0,0 +1,179 @@
+/*
+** 2023-06-21
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+**
+** This file implements an extension that uses the SQLITE_CONFIG_PCACHE2
+** mechanism to add a tracing layer on top of pluggable page cache of
+** SQLite.  If this extension is registered prior to sqlite3_initialize(),
+** it will cause all page cache activities to be logged on standard output,
+** or to some other FILE specified by the initializer.
+**
+** This file needs to be compiled into the application that uses it.
+**
+** This extension is used to implement the --pcachetrace option of the
+** command-line shell.
+*/
+#include 
+#include 
+#include 
+
+/* The original page cache routines */
+static sqlite3_pcache_methods2 pcacheBase;
+static FILE *pcachetraceOut;
+
+/* Methods that trace pcache activity */
+static int pcachetraceInit(void *pArg){
+  int nRes;
+  if( pcachetraceOut ){
+    fprintf(pcachetraceOut, "PCACHETRACE: xInit(%p)\n", pArg);
+  }
+  nRes = pcacheBase.xInit(pArg);
+  if( pcachetraceOut ){
+    fprintf(pcachetraceOut, "PCACHETRACE: xInit(%p) -> %d\n", pArg, nRes);
+  }
+  return nRes;
+}
+static void pcachetraceShutdown(void *pArg){
+  if( pcachetraceOut ){
+    fprintf(pcachetraceOut, "PCACHETRACE: xShutdown(%p)\n", pArg);
+  }
+  pcacheBase.xShutdown(pArg);
+}
+static sqlite3_pcache *pcachetraceCreate(int szPage, int szExtra, int bPurge){
+  sqlite3_pcache *pRes;
+  if( pcachetraceOut ){
+    fprintf(pcachetraceOut, "PCACHETRACE: xCreate(%d,%d,%d)\n",
+            szPage, szExtra, bPurge);
+  }
+  pRes = pcacheBase.xCreate(szPage, szExtra, bPurge);
+  if( pcachetraceOut ){
+    fprintf(pcachetraceOut, "PCACHETRACE: xCreate(%d,%d,%d) -> %p\n",
+            szPage, szExtra, bPurge, pRes);
+  }
+  return pRes;
+}
+static void pcachetraceCachesize(sqlite3_pcache *p, int nCachesize){
+  if( pcachetraceOut ){
+    fprintf(pcachetraceOut, "PCACHETRACE: xCachesize(%p, %d)\n", p, nCachesize);
+  }
+  pcacheBase.xCachesize(p, nCachesize);
+}
+static int pcachetracePagecount(sqlite3_pcache *p){
+  int nRes;
+  if( pcachetraceOut ){
+    fprintf(pcachetraceOut, "PCACHETRACE: xPagecount(%p)\n", p);
+  }
+  nRes = pcacheBase.xPagecount(p);
+  if( pcachetraceOut ){
+    fprintf(pcachetraceOut, "PCACHETRACE: xPagecount(%p) -> %d\n", p, nRes);
+  }
+  return nRes;
+}
+static sqlite3_pcache_page *pcachetraceFetch(
+  sqlite3_pcache *p,
+  unsigned key,
+  int crFg
+){
+  sqlite3_pcache_page *pRes;
+  if( pcachetraceOut ){
+    fprintf(pcachetraceOut, "PCACHETRACE: xFetch(%p,%u,%d)\n", p, key, crFg);
+  }
+  pRes = pcacheBase.xFetch(p, key, crFg);
+  if( pcachetraceOut ){
+    fprintf(pcachetraceOut, "PCACHETRACE: xFetch(%p,%u,%d) -> %p\n",
+            p, key, crFg, pRes);
+  }
+  return pRes;
+}
+static void pcachetraceUnpin(
+  sqlite3_pcache *p,
+  sqlite3_pcache_page *pPg,
+  int bDiscard
+){
+  if( pcachetraceOut ){
+    fprintf(pcachetraceOut, "PCACHETRACE: xUnpin(%p, %p, %d)\n",
+            p, pPg, bDiscard);
+  }
+  pcacheBase.xUnpin(p, pPg, bDiscard);
+}
+static void pcachetraceRekey(
+  sqlite3_pcache *p,
+  sqlite3_pcache_page *pPg,
+  unsigned oldKey,
+  unsigned newKey
+){
+  if( pcachetraceOut ){
+    fprintf(pcachetraceOut, "PCACHETRACE: xRekey(%p, %p, %u, %u)\n",
+        p, pPg, oldKey, newKey);
+  }
+  pcacheBase.xRekey(p, pPg, oldKey, newKey);
+}
+static void pcachetraceTruncate(sqlite3_pcache *p, unsigned n){
+  if( pcachetraceOut ){
+    fprintf(pcachetraceOut, "PCACHETRACE: xTruncate(%p, %u)\n", p, n);
+  }
+  pcacheBase.xTruncate(p, n);
+}
+static void pcachetraceDestroy(sqlite3_pcache *p){
+  if( pcachetraceOut ){
+    fprintf(pcachetraceOut, "PCACHETRACE: xDestroy(%p)\n", p);
+  }
+  pcacheBase.xDestroy(p);
+}
+static void pcachetraceShrink(sqlite3_pcache *p){
+  if( pcachetraceOut ){
+    fprintf(pcachetraceOut, "PCACHETRACE: xShrink(%p)\n", p);
+  }
+  pcacheBase.xShrink(p);
+}
+
+/* The substitute pcache methods */
+static sqlite3_pcache_methods2 ersaztPcacheMethods = {
+  0,
+  0,
+  pcachetraceInit,
+  pcachetraceShutdown,
+  pcachetraceCreate,
+  pcachetraceCachesize,
+  pcachetracePagecount,
+  pcachetraceFetch,
+  pcachetraceUnpin,
+  pcachetraceRekey,
+  pcachetraceTruncate,
+  pcachetraceDestroy,
+  pcachetraceShrink
+};
+
+/* Begin tracing memory allocations to out. */
+int sqlite3PcacheTraceActivate(FILE *out){
+  int rc = SQLITE_OK;
+  if( pcacheBase.xFetch==0 ){
+    rc = sqlite3_config(SQLITE_CONFIG_GETPCACHE2, &pcacheBase);
+    if( rc==SQLITE_OK ){
+      rc = sqlite3_config(SQLITE_CONFIG_PCACHE2, &ersaztPcacheMethods);
+    }
+  }
+  pcachetraceOut = out;
+  return rc;
+}
+
+/* Deactivate memory tracing */
+int sqlite3PcacheTraceDeactivate(void){
+  int rc = SQLITE_OK;
+  if( pcacheBase.xFetch!=0 ){
+    rc = sqlite3_config(SQLITE_CONFIG_PCACHE2, &pcacheBase);
+    if( rc==SQLITE_OK ){
+      memset(&pcacheBase, 0, sizeof(pcacheBase));
+    }
+  }
+  pcachetraceOut = 0;
+  return rc;
+}
diff --git a/sqlite/ext/misc/prefixes.c b/sqlite/ext/misc/prefixes.c
index 3f053b7f..e6517e71 100644
--- a/sqlite/ext/misc/prefixes.c
+++ b/sqlite/ext/misc/prefixes.c
@@ -248,7 +248,8 @@ static sqlite3_module prefixesModule = {
   /* xSavepoint  */ 0,
   /* xRelease    */ 0,
   /* xRollbackTo */ 0,
-  /* xShadowName */ 0
+  /* xShadowName */ 0,
+  /* xIntegrity  */ 0
 };
 
 /*
diff --git a/sqlite/ext/misc/qpvtab.c b/sqlite/ext/misc/qpvtab.c
index fb0c155a..b7c2a051 100644
--- a/sqlite/ext/misc/qpvtab.c
+++ b/sqlite/ext/misc/qpvtab.c
@@ -439,7 +439,8 @@ static sqlite3_module qpvtabModule = {
   /* xSavepoint  */ 0,
   /* xRelease    */ 0,
   /* xRollbackTo */ 0,
-  /* xShadowName */ 0
+  /* xShadowName */ 0,
+  /* xIntegrity  */ 0
 };
 #endif /* SQLITE_OMIT_VIRTUALTABLE */
 
diff --git a/sqlite/ext/misc/series.c b/sqlite/ext/misc/series.c
index 0deabf95..abd6af7a 100644
--- a/sqlite/ext/misc/series.c
+++ b/sqlite/ext/misc/series.c
@@ -330,6 +330,10 @@ static int seriesColumn(
   return SQLITE_OK;
 }
 
+#ifndef LARGEST_UINT64
+#define LARGEST_UINT64 (0xffffffff|(((sqlite3_uint64)0xffffffff)<<32))
+#endif
+
 /*
 ** Return the rowid for the current row, logically equivalent to n+1 where
 ** "n" is the ascending integer in the aforesaid production definition.
@@ -337,7 +341,7 @@ static int seriesColumn(
 static int seriesRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){
   series_cursor *pCur = (series_cursor*)cur;
   sqlite3_uint64 n = pCur->ss.uSeqIndexNow;
-  *pRowid = (sqlite3_int64)((n<0xffffffffffffffff)? n+1 : 0);
+  *pRowid = (sqlite3_int64)((n
 #include 
 #include 
+#include 
 
 #include 
 
@@ -2200,7 +2201,8 @@ static int zipfileRegister(sqlite3 *db){
     0,                         /* xSavepoint */
     0,                         /* xRelease */
     0,                         /* xRollback */
-    0                          /* xShadowName */
+    0,                         /* xShadowName */
+    0                          /* xIntegrity */
   };
 
   int rc = sqlite3_create_module(db, "zipfile"  , &zipfileModule, 0);
diff --git a/sqlite/ext/recover/dbdata.c b/sqlite/ext/recover/dbdata.c
index 878a61f1..25a6e9fd 100644
--- a/sqlite/ext/recover/dbdata.c
+++ b/sqlite/ext/recover/dbdata.c
@@ -73,13 +73,12 @@
 */
 
 #if !defined(SQLITEINT_H) 
-#include "sqlite3ext.h"
+#include "sqlite3.h"
 
 typedef unsigned char u8;
 typedef unsigned int u32;
 
 #endif
-SQLITE_EXTENSION_INIT1
 #include 
 #include 
 
@@ -664,8 +663,14 @@ static int dbdataNext(sqlite3_vtab_cursor *pCursor){
           if( pCsr->pHdrPtr>&pCsr->pRec[pCsr->nRec] ){
             bNextPage = 1;
           }else{
+            int szField = 0;
             pCsr->pHdrPtr += dbdataGetVarintU32(pCsr->pHdrPtr, &iType);
-            pCsr->pPtr += dbdataValueBytes(iType);
+            szField = dbdataValueBytes(iType);
+            if( (pCsr->nRec - (pCsr->pPtr - pCsr->pRec))pPtr = &pCsr->pRec[pCsr->nRec];
+            }else{
+              pCsr->pPtr += szField;
+            }
           }
         }
       }
@@ -928,7 +933,8 @@ static int sqlite3DbdataRegister(sqlite3 *db){
     0,                            /* xSavepoint */
     0,                            /* xRelease */
     0,                            /* xRollbackTo */
-    0                             /* xShadowName */
+    0,                            /* xShadowName */
+    0                             /* xIntegrity */
   };
 
   int rc = sqlite3_create_module(db, "sqlite_dbdata", &dbdata_module, 0);
@@ -938,15 +944,11 @@ static int sqlite3DbdataRegister(sqlite3 *db){
   return rc;
 }
 
-#ifdef _WIN32
-__declspec(dllexport)
-#endif
 int sqlite3_dbdata_init(
   sqlite3 *db, 
   char **pzErrMsg, 
   const sqlite3_api_routines *pApi
 ){
-  SQLITE_EXTENSION_INIT2(pApi);
   (void)pzErrMsg;
   return sqlite3DbdataRegister(db);
 }
diff --git a/sqlite/ext/recover/recovercorrupt2.test b/sqlite/ext/recover/recovercorrupt2.test
index 7147c67e..29acc27a 100644
--- a/sqlite/ext/recover/recovercorrupt2.test
+++ b/sqlite/ext/recover/recovercorrupt2.test
@@ -285,5 +285,244 @@ do_test 5.1 {
   list [catch { $R finish } msg] $msg
 } {0 {}}
 
+#-------------------------------------------------------------------------
+#
+reset_db
+do_test 6.0 {
+  sqlite3 db {}
+  db deserialize [decode_hexdb {
+| size 8192 pagesize 4096 filename abc.db
+| page 1 offset 0
+|      0: 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00   SQLite format 3.
+|     16: 10 00 01 01 00 40 20 20 00 00 00 02 00 00 00 02   .....@  ........
+|     32: 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 04   ................
+|     48: 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00   ................
+|     80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02   ................
+|     96: 00 2e 6e b8 0d 00 00 00 01 0f dc 00 0f dc 00 00   ..n.............
+|   4048: 00 00 00 00 00 00 00 00 00 00 00 00 22 01 06 17   ................
+|   4064: 11 11 01 31 74 61 62 6c 65 74 31 74 31 02 43 52   ...1tablet1t1.CR
+|   4080: 45 41 54 45 20 54 41 42 4c 45 20 74 31 28 78 29   EATE TABLE t1(x)
+| page 2 offset 4096
+|      0: 0d 00 00 00 01 0f e2 00 0f e2 00 00 00 00 00 00   ................
+|   4064: 00 00 1c 01 02 41 61 62 63 64 65 66 67 68 69 6a   .....Aabcdefghij
+|   4080: 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a   klmnopqrstuvwxyz
+| end abc.db
+}]} {}
+do_test 6.1 {
+  set R [sqlite3_recover_init db main test.db2]
+  catch { $R run }
+  list [catch { $R finish } msg] $msg
+} {0 {}}
+
+reset_db
+breakpoint
+do_test 6.2 {
+  sqlite3 db {}
+  db deserialize [decode_hexdb {
+| size 8192 pagesize 4096 filename abc.db
+| page 1 offset 0
+|      0: 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00   SQLite format 3.
+|     16: 10 00 01 01 00 40 20 20 00 00 00 02 00 00 00 02   .....@  ........
+|     32: 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 04   ................
+|     48: 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00   ................
+|     80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02   ................
+|     96: 00 2e 6e b8 0d 00 00 00 01 0f dc 00 0f dc 00 00   ..n.............
+|   4048: 00 00 00 00 00 00 00 00 00 00 00 00 22 01 06 17   ................
+|   4064: 11 11 01 31 74 61 62 6c 65 74 31 74 31 02 43 52   ...1tablet1t1.CR
+|   4080: 45 41 54 45 20 54 41 42 4c 45 20 74 31 28 78 29   EATE TABLE t1(x)
+| page 2 offset 4096
+|      0: 0d 00 00 00 01 0f e2 00 0f e2 00 00 00 00 00 00   ................
+|   4064: 00 00 1c 01 02 8F FF FF FF 7E 65 66 67 68 69 6a   .....Aabcdefghij
+|   4080: 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a   klmnopqrstuvwxyz
+| end abc.db
+}]} {}
+do_test 6.3 {
+  set R [sqlite3_recover_init db main test.db2]
+  catch { $R run }
+  list [catch { $R finish } msg] $msg
+} {0 {}}
+
+reset_db
+breakpoint
+do_test 7.0 {
+  sqlite3 db {}
+  db deserialize [decode_hexdb {
+| size 4108 pagesize 4096 filename x1.db
+| page 1 offset 0
+|      0: 02 01 00 00 00 00 14 15 40 00 00 00 00 00 00 00   ........@.......
+|     16: 33 3a 6d 65 6d 6f 72 79 3a 02 02 02 02 02 02 02   3:memory:.......
+|     32: 02 02 02 02 02 02 12 02 02 02 63 6f 6c 6f 72 20   ..........color 
+|     48: 73 70 61 63 00 f3 a0 81 a1 00 00 a0 02 02 02 02   spac............
+|     64: 69 95 73 6f 36 00 ff 0d 00 97 8c 90 3f 0a 70 02   i.so6.......?.p.
+|     80: 02 02 02 02 02 02 02 02 02 02 02 02 02 01 00 00   ................
+|     96: 06 02 02 02 02 5f 02 02 02 2c 02 02 02 02 02 02   ....._...,......
+|    112: 02 02 02 02 02 02 02 02 02 12 02 02 02 63 6f 6c   .............col
+|    128: 6f 72 20 73 70 61 63 00 f3 a0 81 a1 00 00 a0 02   or spac.........
+|    144: 02 02 02 69 95 73 6f 36 00 ff 0d 00 97 8c 90 3f   ...i.so6.......?
+|    160: 0a 70 02 02 02 02 02 02 02 02 02 02 02 02 02 02   .p..............
+|    176: 01 00 00 06 02 02 02 02 5f 02 02 02 2c 02 02 00   ........_...,...
+|    192: 00 01 00 01 00 00 00 01 00 02 fe 00 00 03 00 01   ................
+|    208: 00 00 00 01 c5 04 00 00 00 01 00 01 00 00 00 01   ................
+|    224: 00 fa 02 00 00 00 03 00 01 00 00 00 81 00 04 00   ................
+|    240: 00 00 01 00 01 00 00 00 01 00 02 00 fe 00 03 00   ................
+|    256: 01 00 00 00 01 00 04 00 00 00 01 00 01 00 00 00   ................
+|    272: 01 00 02 00 00 00 03 00 01 00 00 00 01 00 04 00   ................
+|    288: 00 00 01 00 01 00 00 00 01 00 02 00 00 00 03 00   ................
+|    304: 01 00 00 00 01 00 04 00 00 00 01 00 01 00 00 00   ................
+|    320: 01 00 02 00 00 00 03 00 01 00 00 00 40 00 84 00   ............@...
+|    336: 84 00 84 00 01 00 00 00 09 00 06 00 f5 00 01 00   ................
+|    352: 08 01 03 00 03 00 62 00 62 00 23 00 01 00 62 00   ......b.b.#...b.
+|    368: 04 00 1e 00 62 00 62 00 62 00 01 00 00 00 0a 00   ....b.b.b.......
+|    384: 01 00 03 00 01 00 03 00 04 00 02 00 01 00 01 00   ................
+|    400: 08 00 01 00 31 c6 00 03 00 0c 00 12 00 18 00 02   ....1...........
+|    416: 00 05 00 08 00 02 00 06 00 08 00 02 00 07 00 08   ................
+|    432: 00 02 00 01 00 01 00 08 00 01 00 0c 00 03 00 16   ................
+|    448: 00 1c 00 22 00 01 00 03 00 05 00 06 00 07 00 02   ................
+|    464: 00 05 00 09 00 02 00 06 00 09 00 02 00 07 00 09   ................
+|    480: 00 00 00 00 01 00 05 00 00 00 01 00 01 00 00 00   ................
+|    496: 01 00 02 00 00 00 03 00 01 00 00 00 01 00 04 00   ................
+|    512: 00 00 01 00 01 00 00 00 01 00 02 00 00 00 03 00   ................
+|    528: 01 00 00 00 01 00 04 00 00 00 01 00 01 00 00 00   ................
+|    544: 01 00 02 00 00 f6 03 00 00 02 00 00 01 00 04 00   ................
+|    560: 00 00 01 00 01 00 00 00 01 00 02 00 00 00 03 00   ................
+|    576: 01 00 00 00 01 00 04 00 00 00 01 00 01 00 00 00   ................
+|    592: 01 00 02 00 00 00 03 00 01 00 00 00 01 00 04 00   ................
+|    608: 00 00 01 00 01 00 00 00 01 00 02 00 00 00 03 00   ................
+|    624: 01 00 00 00 01 00 04 00 00 00 01 00 01 00 00 00   ................
+|    640: 01 3d 02 00 00 00 03 00 06 00 00 00 01 00 01 00   .=..............
+|    656: 00 00 01 00 01 00 00 00 01 00 02 00 00 00 03 00   ................
+|    672: 01 00 00 00 01 00 04 00 00 00 01 00 01 00 00 00   ................
+|    688: 01 00 02 00 00 00 55 52 4c 52 65 71 75 65 73 74   ......URLRequest
+|    704: 43 6f 6e 00 00 00 01 01 0e d4 00 04 00 00 00 01   Con.............
+|    720: 0e f8 00 04 00 00 00 01 0f 1c 00 04 00 00 00 01   ................
+|    736: 0f 00 00 01 00 00 00 01 0f 86 00 01 00 00 00 01   ................
+|    752: 0f 84 00 01 00 00 00 01 00 00 01 0f c0 00 01 00   ................
+|    768: 00 00 01 0f e8 00 d6 0f 00 01 6f 00 02 0f d6 00   ..........o.....
+|    784: 02 34 03 03 03 00 01 00 00 00 01 00 05 00 00 00   .4..............
+|    800: 01 00 01 00 00 00 01 00 02 00 00 00 03 00 01 00   ................
+|    816: 00 00 01 00 04 00 00 00 01 00 01 00 00 00 01 00   ................
+|    832: 02 00 00 00 03 00 01 00 10 00 01 00 04 00 00 00   ................
+|    848: 01 00 01 00 00 00 01 00 02 00 00 00 03 00 00 02   ................
+|    864: 00 00 01 40 04 00 00 03 01 00 01 00 00 00 01 00   ...@............
+|    880: 02 00 00 00 03 00 01 00 00 00 00 00 01 0e f8 00   ................
+|    896: 04 77 4f 46 32 73 40 23 70 00 00 00 70 00 1f 00   .wOF2s@#p...p...
+|    912: 00 00 d8 00 00 00 ff ff 00 00 00 00 43 00 00 00   ............C...
+|    928: 00 00 ff ff ff ff ff 00 00 a8 00 00 0c 00 00 00   ................
+|   1024: 00 00 00 00 00 00 00 00 00 00 10 22 00 22 0f 00   ................
+|   1040: 00 00 00 00 00 00 10 22 00 00 70 00 1f 00 00 0f   ..........p.....
+|   1056: d8 00 00 00 00 00 00 00 00 00 03 00 00 00 00 00   ................
+|   1072: 00 01 00 00 00 3f 23 70 00 00 00 01 0f 1c 00 04   .....?#p........
+|   1088: 00 00 00 01 0f 40 00 01 00 00 00 01 0f 86 00 01   .....@..........
+|   1104: 00 00 00 01 0f 84 00 01 00 00 00 01 00 00 01 0f   ................
+|   1120: c0 00 01 00 00 00 01 0f e8 00 01 0f d6 00 6f 00   ..............o.
+|   1136: 02 0f d6 00 03 02 31 03 2b 03 2a f2 00 0f d4 00   ......1.+.*.....
+|   1152: 01 00 08 00 01 00 04 03 2b 00 02 02 32 00 01 0f   ........+...2...
+|   1168: c8 01 15 00 02 20 c8 00 02 12 ad 02 00 24 06 c0   ..... .......$..
+|   1184: 00 00 00 03 00 00 01 24 00 2a 06 e4 00 00 00 03   .......$.*......
+|   1200: 00 00 01 25 00 38 07 0e 00 00 00 03 00 00 01 26   ...%.8.........&
+|   1216: 00 34 07 46 00 00 00 03 00 00 01 27 00 1c 07 7a   .4.F.......'...z
+|   1232: 00 00 00 03 00 00 01 28 00 2a 07 96 00 00 00 03   .......(.*......
+|   1248: 00 e5 01 29 00 34 07 c0 00 00 00 03 00 00 01 2a   ...).4.........*
+|   1264: 67 34 07 f4 00 00 00 03 00 00 01 2b 00 22 08 28   g4.........+...(
+|   1280: 00 00 00 00 01 00 01 00 00 00 01 00 02 00 00 00   ................
+|   1296: 03 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00   ................
+|   1312: 01 00 00 00 01 00 02 00 00 00 03 00 00 02 00 00   ................
+|   1328: 01 00 04 00 00 00 01 00 01 00 00 00 01 00 02 00   ................
+|   1344: 00 00 03 00 01 00 00 00 01 00 04 00 00 00 01 00   ................
+|   1360: 01 00 00 00 01 00 02 00 00 00 03 00 01 00 00 21   ...............!
+|   1376: 04 00 01 00 00 00 00 00 01 00 00 00 01 00 02 00   ................
+|   1392: 00 00 03 00 01 00 00 00 01 00 04 00 00 00 01 00   ................
+|   1408: 01 00 00 00 01 00 02 00 00 00 03 00 01 00 00 00   ................
+|   1424: 01 00 05 00 00 00 01 00 01 00 00 01 00 02 02 02   ................
+|   1440: 12 02 02 02 63 6f 6c 6f 72 20 73 70 61 63 00 f3   ....color spac..
+|   1456: a0 81 a1 00 00 a0 02 02 02 02 69 95 73 6f 36 00   ..........i.so6.
+|   1472: ff 0d 00 97 8c 90 3f 0a 70 02 02 02 02 02 02 02   ......?.p.......
+|   1488: 02 02 02 02 02 02 02 01 00 00 06 02 02 02 02 5f   ..............._
+|   1504: 02 02 02 2c 02 02 00 00 01 00 01 00 00 00 01 00   ...,............
+|   1520: 02 fe 00 00 03 00 01 00 00 00 01 c5 04 00 00 00   ................
+|   1536: 01 00 01 00 00 00 01 00 02 00 00 00 03 00 01 00   ................
+|   1552: 00 00 81 00 04 00 00 00 01 00 01 00 00 00 01 00   ................
+|   1568: 02 00 fe 00 03 00 01 00 00 00 01 00 04 00 00 00   ................
+|   1584: 01 00 01 00 00 00 01 00 02 00 00 00 03 00 01 00   ................
+|   1600: 00 00 01 00 04 00 00 00 01 00 01 00 00 00 01 00   ................
+|   1616: 02 00 00 00 03 00 01 00 00 00 01 00 04 00 00 00   ................
+|   1632: 01 00 01 00 00 00 01 00 02 00 00 00 03 00 01 00   ................
+|   1648: 00 00 40 00 84 00 84 00 84 00 01 00 00 00 09 00   ..@.............
+|   1664: 06 00 f5 00 01 00 08 01 03 15 15 15 15 15 15 15   ................
+|   1680: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15   ................
+|   1696: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15   ................
+|   1712: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15   ................
+|   1728: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15   ................
+|   1744: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15   ................
+|   1760: 15 15 15 15 15 15 15 15 15 15 15 00 03 00 62 00   ..............b.
+|   1776: 62 00 23 00 01 00 62 00 04 00 1e 00 62 00 62 00   b.#...b.....b.b.
+|   1792: 62 00 01 00 00 00 0a 00 01 00 03 00 01 00 03 00   b...............
+|   1808: 04 00 02 00 01 00 01 00 08 00 01 00 31 c6 00 03   ............1...
+|   1824: 00 0c 00 12 00 18 00 02 00 05 00 08 00 02 00 06   ................
+|   1840: 00 08 00 02 00 07 00 08 00 02 00 01 00 01 00 08   ................
+|   1856: 00 01 00 0c 00 03 00 16 00 1c 00 22 00 01 00 03   ................
+|   1872: 00 05 00 06 00 07 00 02 00 05 00 09 00 02 00 06   ................
+|   1888: 00 09 00 02 00 07 00 09 00 00 00 00 01 00 05 00   ................
+|   1904: 00 00 01 00 01 00 00 00 01 00 02 00 00 00 03 00   ................
+|   1920: 01 00 00 00 01 00 04 00 00 00 01 00 01 00 00 00   ................
+|   1936: 01 00 02 00 00 00 03 00 01 00 00 00 01 00 04 00   ................
+|   1952: 00 00 01 0f d6 00 02 34 03 03 03 00 01 00 00 00   .......4........
+|   1968: 01 00 05 00 00 00 01 00 01 00 00 00 01 00 02 00   ................
+|   1984: 00 00 03 00 01 00 00 00 01 00 04 00 00 00 01 00   ................
+|   2000: 01 00 00 00 01 00 02 fc 42 dc 19 5c 74 23 18 cd   ........B...t#..
+|   2016: b3 a5 a8 7a 90 40 1d 66 12 5d e5 4f 85 00 68 f4   ...z.@.f.].O..h.
+|   2032: 05 98 86 25 24 dd bc c2 f6 f6 4e a3 e2 61 d2 c6   ...%$.....N..a..
+|   2048: aa c1 56 50 d4 80 82 35 f1 e2 59 41 50 a6 da 51   ..VP...5..YAP..Q
+|   2064: d4 62 9c 19 94 58 aa 31 30 8a 22 c2 5f 33 2b c9   .b...X.10..._3+.
+|   2080: b6 e6 b4 11 4e 51 82 c4 d8 b6 d8 b4 06 04 fb 68   ....NQ.........h
+|   2096: f4 d2 6f e7 cb 8a a8 82 d5 74 00 00 00 00 00 00   ..o......t......
+|   2368: 00 00 00 00 00 00 00 00 00 08 00 00 00 00 00 00   ................
+|   2432: 00 00 00 00 00 03 00 01 00 10 00 01 00 04 00 00   ................
+|   2448: 00 01 00 01 00 00 00 01 00 02 00 00 00 03 00 00   ................
+|   2464: 02 00 00 01 40 04 00 00 03 01 00 01 00 00 00 01   ....@...........
+|   2480: 00 02 00 00 00 03 00 01 00 00 00 00 00 01 0e f8   ................
+|   2496: 00 04 77 4f 46 32 73 40 23 70 00 00 00 70 00 1f   ..wOF2s@#p...p..
+|   2512: 00 00 00 d8 00 00 00 ff ff 00 00 00 00 43 00 00   .............C..
+|   2528: 00 00 00 ff ff ff ff ff 00 00 a8 00 00 0c 00 00   ................
+|   2624: 00 00 00 00 00 00 00 00 00 00 00 10 22 00 22 0f   ................
+|   2640: 00 00 00 00 00 00 00 10 22 00 00 70 00 1f 00 00   ...........p....
+|   2656: 0f d8 00 00 00 00 00 00 00 00 00 03 00 00 00 00   ................
+|   2672: 00 00 01 00 00 00 3f 23 70 00 00 00 01 0f 1c 00   ......?#p.......
+|   2688: 04 00 00 00 01 0f 40 00 01 00 00 00 01 0f 86 00   ......@.........
+|   2704: 01 00 00 00 01 0f 84 00 01 00 00 00 01 00 00 01   ................
+|   2720: 0f c0 00 01 00 00 00 01 0f e8 00 01 0f d6 00 6f   ...............o
+|   2736: 00 02 0f d6 00 03 02 31 03 2b 03 2a f2 00 0f d4   .......1.+.*....
+|   2752: 00 01 00 08 00 01 00 04 03 2b 00 02 02 32 00 01   .........+...2..
+|   2768: 0f c8 01 15 00 02 20 c8 00 02 12 ad 02 00 24 06   ...... .......$.
+|   2784: c0 00 00 5a 03 00 00 01 24 00 2a 06 e4 00 00 00   ...Z....$.*.....
+|   2800: 03 00 00 01 25 00 38 07 0e 00 00 00 03 00 00 01   ....%.8.........
+|   2816: 26 00 34 07 46 00 00 00 03 00 00 01 27 00 1c 07   &.4.F.......'...
+|   2832: 7a 00 00 00 03 00 00 01 28 00 2a 07 96 00 00 00   z.......(.*.....
+|   2848: 03 00 e5 01 29 00 34 07 c0 00 00 00 03 00 00 01   ....).4.........
+|   2864: 2a 67 34 07 f4 00 00 00 03 00 00 01 2b 00 22 08   *g4.........+...
+|   2880: 28 00 00 00 00 01 00 01 00 00 00 01 00 02 00 00   (...............
+|   2896: 00 03 00 01 00 00 00 01 00 00 00 01 00 00 00 01   ................
+|   2912: 00 01 00 00 00 01 00 02 00 00 00 03 00 00 02 00   ................
+|   2928: 00 01 00 04 00 00 00 01 00 01 00 00 00 00 00 00   ................
+|   2992: 00 48 00 00 00 00 00 00 00 00 00 00 00 00 00 00   .H..............
+|   3504: 00 00 00 00 00 00 00 00 00 00 00 97 00 00 00 00   ................
+|   3904: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 09   ................
+|   3920: 03 fe 00 00 01 36 00 3c 0a 38 00 00 00 03 00 00   .....6.<.8......
+|   3936: 01 37 00 20 0a 74 00 00 00 fb ff ff 00 38 00 2a   .7. .t.......8.*
+|   3952: 0a 94 00 00 00 03 00 00 01 39 4f 54 54 4f 00 0e   .........9OTTO..
+|   3968: 00 80 00 03 00 60 43 46 46 20 e3 ae 89 2a 00 00   .....`CFF ...*..
+|   3984: 02 b0 00 00 02 76 42 50 4f 53 00 15 00 0a 00 00   .....vBPOS......
+|   4000: 05 28 00 00 00 0c 54 53 55 42 c9 70 c3 06 00 00   .(....TSUB.p....
+|   4016: 05 34 1f 00 40 00 48 00 00 00 00 00 00 00 00 00   .4..@.H.........
+|   4064: 00 00 00 00 00 08 00 01 00 01 00 01 00 01 00 06   ................
+|   4080: 00 02 00 08 00 01 00 01 00 01 00 01 00 00 00 00   ................
+| end x1.db
+}]} {}
+do_test 7.1 {
+  set R [sqlite3_recover_init db main test.db2]
+  catch { $R run }
+  list [catch { $R finish } msg] $msg
+} {1 {file is not a database}}
+
 finish_test
 
diff --git a/sqlite/ext/recover/sqlite3recover.c b/sqlite/ext/recover/sqlite3recover.c
index 29fff0e7..c445c517 100644
--- a/sqlite/ext/recover/sqlite3recover.c
+++ b/sqlite/ext/recover/sqlite3recover.c
@@ -2103,7 +2103,7 @@ static int recoverIsValidPage(u8 *aTmp, const u8 *a, int n){
     if( iFree>(n-4) ) return 0;
     iNext = recoverGetU16(&a[iFree]);
     nByte = recoverGetU16(&a[iFree+2]);
-    if( iFree+nByte>n ) return 0;
+    if( iFree+nByte>n || nByte<4 ) return 0;
     if( iNext && iNextnBusy = 1;
   pRtree->base.pModule = &rtreeModule;
   pRtree->zDb = (char *)&pRtree[1];
   pRtree->zName = &pRtree->zDb[nDb+1];
+  pRtree->zNodeName = &pRtree->zName[nName+1];
   pRtree->eCoordType = RTREE_COORD_REAL32;
   pRtree->nDim = 2;
   pRtree->nDim2 = 4;
   memcpy(pRtree->zDb, argv[1], nDb);
   memcpy(pRtree->zName, argv[2], nName);
+  memcpy(pRtree->zNodeName, argv[2], nName);
+  memcpy(&pRtree->zNodeName[nName], "_node", 6);
 
 
   /* Create/Connect to the underlying relational database schema. If
@@ -1683,7 +1687,6 @@ static int geopolyUpdate(
     }
     if( rc==SQLITE_OK ){
       int rc2;
-      pRtree->iReinsertHeight = -1;
       rc = rtreeInsertCell(pRtree, pLeaf, &cell, 0);
       rc2 = nodeRelease(pRtree, pLeaf);
       if( rc==SQLITE_OK ){
@@ -1780,7 +1783,8 @@ static sqlite3_module geopolyModule = {
   rtreeSavepoint,             /* xSavepoint */
   0,                          /* xRelease */
   0,                          /* xRollbackTo */
-  rtreeShadowName             /* xShadowName */
+  rtreeShadowName,            /* xShadowName */
+  rtreeIntegrity              /* xIntegrity */
 };
 
 static int sqlite3_geopoly_init(sqlite3 *db){
diff --git a/sqlite/ext/rtree/rtree.c b/sqlite/ext/rtree/rtree.c
index da5e2a97..1705abd5 100644
--- a/sqlite/ext/rtree/rtree.c
+++ b/sqlite/ext/rtree/rtree.c
@@ -96,6 +96,11 @@ typedef unsigned int u32;
 #endif
 #endif /* !defined(SQLITE_AMALGAMATION) */
 
+/* Macro to check for 4-byte alignment.  Only used inside of assert() */
+#ifdef SQLITE_DEBUG
+# define FOUR_BYTE_ALIGNED(X)  ((((char*)(X) - (char*)0) & 3)==0)
+#endif
+
 #include 
 #include 
 #include 
@@ -161,6 +166,7 @@ struct Rtree {
   int iDepth;                 /* Current depth of the r-tree structure */
   char *zDb;                  /* Name of database containing r-tree table */
   char *zName;                /* Name of r-tree table */ 
+  char *zNodeName;            /* Name of the %_node table */
   u32 nBusy;                  /* Current number of users of this structure */
   i64 nRowEst;                /* Estimated number of rows in this table */
   u32 nCursor;                /* Number of open cursors */
@@ -173,7 +179,6 @@ struct Rtree {
   ** headed by the node (leaf nodes have RtreeNode.iNode==0).
   */
   RtreeNode *pDeleted;
-  int iReinsertHeight;        /* Height of sub-trees Reinsert() has run on */
 
   /* Blob I/O on xxx_node */
   sqlite3_blob *pNodeBlob;
@@ -195,7 +200,7 @@ struct Rtree {
   /* Statement for writing to the "aux:" fields, if there are any */
   sqlite3_stmt *pWriteAux;
 
-  RtreeNode *aHash[HASHSIZE]; /* Hash table of in-memory nodes. */ 
+  RtreeNode *aHash[HASHSIZE]; /* Hash table of in-memory nodes. */
 };
 
 /* Possible values for Rtree.eCoordType: */
@@ -470,15 +475,20 @@ struct RtreeMatchArg {
 ** -DSQLITE_RUNTIME_BYTEORDER=1 is set, then byte-order is determined
 ** at run-time.
 */
-#ifndef SQLITE_BYTEORDER
-# if defined(i386)      || defined(__i386__)      || defined(_M_IX86) ||    \
+#ifndef SQLITE_BYTEORDER /* Replicate changes at tag-20230904a */
+# if defined(__BYTE_ORDER__) && __BYTE_ORDER__==__ORDER_BIG_ENDIAN__
+#   define SQLITE_BYTEORDER 4321
+# elif defined(__BYTE_ORDER__) && __BYTE_ORDER__==__ORDER_LITTLE_ENDIAN__
+#   define SQLITE_BYTEORDER 1234
+# elif defined(__BIG_ENDIAN__) && __BIG_ENDIAN__==1
+#   define SQLITE_BYTEORDER 4321
+# elif defined(i386)    || defined(__i386__)      || defined(_M_IX86) ||    \
      defined(__x86_64)  || defined(__x86_64__)    || defined(_M_X64)  ||    \
      defined(_M_AMD64)  || defined(_M_ARM)        || defined(__x86)   ||    \
      defined(__ARMEL__) || defined(__AARCH64EL__) || defined(_M_ARM64)
-#   define SQLITE_BYTEORDER    1234
-# elif defined(sparc)     || defined(__ppc__) || \
-       defined(__ARMEB__) || defined(__AARCH64EB__)
-#   define SQLITE_BYTEORDER    4321
+#   define SQLITE_BYTEORDER 1234
+# elif defined(sparc)   || defined(__ARMEB__)     || defined(__AARCH64EB__)
+#   define SQLITE_BYTEORDER 4321
 # else
 #   define SQLITE_BYTEORDER 0
 # endif
@@ -502,7 +512,7 @@ static int readInt16(u8 *p){
   return (p[0]<<8) + p[1];
 }
 static void readCoord(u8 *p, RtreeCoord *pCoord){
-  assert( (((sqlite3_uint64)p)&3)==0 );  /* p is always 4-byte aligned */
+  assert( FOUR_BYTE_ALIGNED(p) );
 #if SQLITE_BYTEORDER==1234 && MSVC_VERSION>=1300
   pCoord->u = _byteswap_ulong(*(u32*)p);
 #elif SQLITE_BYTEORDER==1234 && GCC_VERSION>=4003000
@@ -556,7 +566,7 @@ static void writeInt16(u8 *p, int i){
 }
 static int writeCoord(u8 *p, RtreeCoord *pCoord){
   u32 i;
-  assert( (((sqlite3_uint64)p)&3)==0 );  /* p is always 4-byte aligned */
+  assert( FOUR_BYTE_ALIGNED(p) );
   assert( sizeof(RtreeCoord)==4 );
   assert( sizeof(u32)==4 );
 #if SQLITE_BYTEORDER==1234 && GCC_VERSION>=4003000
@@ -727,11 +737,9 @@ static int nodeAcquire(
     }
   }
   if( pRtree->pNodeBlob==0 ){
-    char *zTab = sqlite3_mprintf("%s_node", pRtree->zName);
-    if( zTab==0 ) return SQLITE_NOMEM;
-    rc = sqlite3_blob_open(pRtree->db, pRtree->zDb, zTab, "data", iNode, 0,
+    rc = sqlite3_blob_open(pRtree->db, pRtree->zDb, pRtree->zNodeName,
+                           "data", iNode, 0,
                            &pRtree->pNodeBlob);
-    sqlite3_free(zTab);
   }
   if( rc ){
     nodeBlobReset(pRtree);
@@ -1284,7 +1292,7 @@ static void rtreeNonleafConstraint(
   assert(p->op==RTREE_LE || p->op==RTREE_LT || p->op==RTREE_GE 
       || p->op==RTREE_GT || p->op==RTREE_EQ || p->op==RTREE_TRUE
       || p->op==RTREE_FALSE );
-  assert( (((sqlite3_uint64)pCellData)&3)==0 );  /* 4-byte aligned */
+  assert( FOUR_BYTE_ALIGNED(pCellData) );
   switch( p->op ){
     case RTREE_TRUE:  return;   /* Always satisfied */
     case RTREE_FALSE: break;    /* Never satisfied */
@@ -1337,7 +1345,7 @@ static void rtreeLeafConstraint(
       || p->op==RTREE_GT || p->op==RTREE_EQ || p->op==RTREE_TRUE
       || p->op==RTREE_FALSE );
   pCellData += 8 + p->iCoord*4;
-  assert( (((sqlite3_uint64)pCellData)&3)==0 );  /* 4-byte aligned */
+  assert( FOUR_BYTE_ALIGNED(pCellData) );
   RTREE_DECODE_COORD(eInt, pCellData, xN);
   switch( p->op ){
     case RTREE_TRUE:  return;   /* Always satisfied */
@@ -1907,7 +1915,20 @@ static int rtreeFilter(
             p->pInfo->nCoord = pRtree->nDim2;
             p->pInfo->anQueue = pCsr->anQueue;
             p->pInfo->mxLevel = pRtree->iDepth + 1;
-          }else if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){
+          }else if( eType==SQLITE_INTEGER ){
+            sqlite3_int64 iVal = sqlite3_value_int64(argv[ii]);
+#ifdef SQLITE_RTREE_INT_ONLY
+            p->u.rValue = iVal;
+#else
+            p->u.rValue = (double)iVal;
+            if( iVal>=((sqlite3_int64)1)<<48
+             || iVal<=-(((sqlite3_int64)1)<<48)
+            ){
+              if( p->op==RTREE_LT ) p->op = RTREE_LE;
+              if( p->op==RTREE_GT ) p->op = RTREE_GE;
+            }
+#endif
+          }else if( eType==SQLITE_FLOAT ){
 #ifdef SQLITE_RTREE_INT_ONLY
             p->u.rValue = sqlite3_value_int64(argv[ii]);
 #else
@@ -2038,11 +2059,12 @@ static int rtreeBestIndex(sqlite3_vtab *tab, sqlite3_index_info *pIdxInfo){
         || p->op==SQLITE_INDEX_CONSTRAINT_MATCH)
     ){
       u8 op;
+      u8 doOmit = 1;
       switch( p->op ){
-        case SQLITE_INDEX_CONSTRAINT_EQ:    op = RTREE_EQ;    break;
-        case SQLITE_INDEX_CONSTRAINT_GT:    op = RTREE_GT;    break;
+        case SQLITE_INDEX_CONSTRAINT_EQ:    op = RTREE_EQ;    doOmit = 0; break;
+        case SQLITE_INDEX_CONSTRAINT_GT:    op = RTREE_GT;    doOmit = 0; break;
         case SQLITE_INDEX_CONSTRAINT_LE:    op = RTREE_LE;    break;
-        case SQLITE_INDEX_CONSTRAINT_LT:    op = RTREE_LT;    break;
+        case SQLITE_INDEX_CONSTRAINT_LT:    op = RTREE_LT;    doOmit = 0; break;
         case SQLITE_INDEX_CONSTRAINT_GE:    op = RTREE_GE;    break;
         case SQLITE_INDEX_CONSTRAINT_MATCH: op = RTREE_MATCH; break;
         default:                            op = 0;           break;
@@ -2051,15 +2073,19 @@ static int rtreeBestIndex(sqlite3_vtab *tab, sqlite3_index_info *pIdxInfo){
         zIdxStr[iIdx++] = op;
         zIdxStr[iIdx++] = (char)(p->iColumn - 1 + '0');
         pIdxInfo->aConstraintUsage[ii].argvIndex = (iIdx/2);
-        pIdxInfo->aConstraintUsage[ii].omit = 1;
+        pIdxInfo->aConstraintUsage[ii].omit = doOmit;
       }
     }
   }
 
   pIdxInfo->idxNum = 2;
   pIdxInfo->needToFreeIdxStr = 1;
-  if( iIdx>0 && 0==(pIdxInfo->idxStr = sqlite3_mprintf("%s", zIdxStr)) ){
-    return SQLITE_NOMEM;
+  if( iIdx>0 ){
+    pIdxInfo->idxStr = sqlite3_malloc( iIdx+1 );
+    if( pIdxInfo->idxStr==0 ){
+      return SQLITE_NOMEM;
+    }
+    memcpy(pIdxInfo->idxStr, zIdxStr, iIdx+1);
   }
 
   nRow = pRtree->nRowEst >> (iIdx/2);
@@ -2138,31 +2164,22 @@ static void cellUnion(Rtree *pRtree, RtreeCell *p1, RtreeCell *p2){
 */
 static int cellContains(Rtree *pRtree, RtreeCell *p1, RtreeCell *p2){
   int ii;
-  int isInt = (pRtree->eCoordType==RTREE_COORD_INT32);
-  for(ii=0; iinDim2; ii+=2){
-    RtreeCoord *a1 = &p1->aCoord[ii];
-    RtreeCoord *a2 = &p2->aCoord[ii];
-    if( (!isInt && (a2[0].fa1[1].f)) 
-     || ( isInt && (a2[0].ia1[1].i)) 
-    ){
-      return 0;
+  if( pRtree->eCoordType==RTREE_COORD_INT32 ){
+    for(ii=0; iinDim2; ii+=2){
+      RtreeCoord *a1 = &p1->aCoord[ii];
+      RtreeCoord *a2 = &p2->aCoord[ii];
+      if( a2[0].ia1[1].i ) return 0;
+    }
+  }else{
+    for(ii=0; iinDim2; ii+=2){
+      RtreeCoord *a1 = &p1->aCoord[ii];
+      RtreeCoord *a2 = &p2->aCoord[ii];
+      if( a2[0].fa1[1].f ) return 0;
     }
   }
   return 1;
 }
 
-/*
-** Return the amount cell p would grow by if it were unioned with pCell.
-*/
-static RtreeDValue cellGrowth(Rtree *pRtree, RtreeCell *p, RtreeCell *pCell){
-  RtreeDValue area;
-  RtreeCell cell;
-  memcpy(&cell, p, sizeof(RtreeCell));
-  area = cellArea(pRtree, &cell);
-  cellUnion(pRtree, &cell, pCell);
-  return (cellArea(pRtree, &cell)-area);
-}
-
 static RtreeDValue cellOverlap(
   Rtree *pRtree, 
   RtreeCell *p, 
@@ -2209,38 +2226,52 @@ static int ChooseLeaf(
   for(ii=0; rc==SQLITE_OK && ii<(pRtree->iDepth-iHeight); ii++){
     int iCell;
     sqlite3_int64 iBest = 0;
-
+    int bFound = 0;
     RtreeDValue fMinGrowth = RTREE_ZERO;
     RtreeDValue fMinArea = RTREE_ZERO;
-
     int nCell = NCELL(pNode);
-    RtreeCell cell;
     RtreeNode *pChild = 0;
 
-    RtreeCell *aCell = 0;
-
-    /* Select the child node which will be enlarged the least if pCell
-    ** is inserted into it. Resolve ties by choosing the entry with
-    ** the smallest area.
+    /* First check to see if there is are any cells in pNode that completely
+    ** contains pCell.  If two or more cells in pNode completely contain pCell
+    ** then pick the smallest.
     */
     for(iCell=0; iCell1 ){
-    int iLeft = 0;
-    int iRight = 0;
-
-    int nLeft = nIdx/2;
-    int nRight = nIdx-nLeft;
-    int *aLeft = aIdx;
-    int *aRight = &aIdx[nLeft];
-
-    SortByDistance(aLeft, nLeft, aDistance, aSpare);
-    SortByDistance(aRight, nRight, aDistance, aSpare);
-
-    memcpy(aSpare, aLeft, sizeof(int)*nLeft);
-    aLeft = aSpare;
-
-    while( iLeftnDim; iDim++){
-      aCenterCoord[iDim] += DCOORD(aCell[ii].aCoord[iDim*2]);
-      aCenterCoord[iDim] += DCOORD(aCell[ii].aCoord[iDim*2+1]);
-    }
-  }
-  for(iDim=0; iDimnDim; iDim++){
-    aCenterCoord[iDim] = (aCenterCoord[iDim]/(nCell*(RtreeDValue)2));
-  }
-
-  for(ii=0; iinDim; iDim++){
-      RtreeDValue coord = (DCOORD(aCell[ii].aCoord[iDim*2+1]) - 
-                               DCOORD(aCell[ii].aCoord[iDim*2]));
-      aDistance[ii] += (coord-aCenterCoord[iDim])*(coord-aCenterCoord[iDim]);
-    }
-  }
-
-  SortByDistance(aOrder, nCell, aDistance, aSpare);
-  nodeZero(pRtree, pNode);
-
-  for(ii=0; rc==SQLITE_OK && ii<(nCell-(RTREE_MINCELLS(pRtree)+1)); ii++){
-    RtreeCell *p = &aCell[aOrder[ii]];
-    nodeInsertCell(pRtree, pNode, p);
-    if( p->iRowid==pCell->iRowid ){
-      if( iHeight==0 ){
-        rc = rowidWrite(pRtree, p->iRowid, pNode->iNode);
-      }else{
-        rc = parentWrite(pRtree, p->iRowid, pNode->iNode);
-      }
-    }
-  }
-  if( rc==SQLITE_OK ){
-    rc = fixBoundingBox(pRtree, pNode);
-  }
-  for(; rc==SQLITE_OK && iiiNode currently contains
-    ** the height of the sub-tree headed by the cell.
-    */
-    RtreeNode *pInsert;
-    RtreeCell *p = &aCell[aOrder[ii]];
-    rc = ChooseLeaf(pRtree, p, iHeight, &pInsert);
-    if( rc==SQLITE_OK ){
-      int rc2;
-      rc = rtreeInsertCell(pRtree, pInsert, p, iHeight);
-      rc2 = nodeRelease(pRtree, pInsert);
-      if( rc==SQLITE_OK ){
-        rc = rc2;
-      }
-    }
-  }
-
-  sqlite3_free(aCell);
-  return rc;
-}
-
+	
 /*
 ** Insert cell pCell into node pNode. Node pNode is the head of a 
 ** subtree iHeight high (leaf nodes have iHeight==0).
@@ -2989,12 +2848,7 @@ static int rtreeInsertCell(
     }
   }
   if( nodeInsertCell(pRtree, pNode, pCell) ){
-    if( iHeight<=pRtree->iReinsertHeight || pNode->iNode==1){
-      rc = SplitNode(pRtree, pNode, pCell, iHeight);
-    }else{
-      pRtree->iReinsertHeight = iHeight;
-      rc = Reinsert(pRtree, pNode, pCell, iHeight);
-    }
+    rc = SplitNode(pRtree, pNode, pCell, iHeight);
   }else{
     rc = AdjustTree(pRtree, pNode, pCell);
     if( ALWAYS(rc==SQLITE_OK) ){
@@ -3337,7 +3191,6 @@ static int rtreeUpdate(
     }
     if( rc==SQLITE_OK ){
       int rc2;
-      pRtree->iReinsertHeight = -1;
       rc = rtreeInsertCell(pRtree, pLeaf, &cell, 0);
       rc2 = nodeRelease(pRtree, pLeaf);
       if( rc==SQLITE_OK ){
@@ -3478,8 +3331,11 @@ static int rtreeShadowName(const char *zName){
   return 0;
 }
 
+/* Forward declaration */
+static int rtreeIntegrity(sqlite3_vtab*, const char*, const char*, int, char**);
+
 static sqlite3_module rtreeModule = {
-  3,                          /* iVersion */
+  4,                          /* iVersion */
   rtreeCreate,                /* xCreate - create a table */
   rtreeConnect,               /* xConnect - connect to an existing table */
   rtreeBestIndex,             /* xBestIndex - Determine search strategy */
@@ -3502,7 +3358,8 @@ static sqlite3_module rtreeModule = {
   rtreeSavepoint,             /* xSavepoint */
   0,                          /* xRelease */
   0,                          /* xRollbackTo */
-  rtreeShadowName             /* xShadowName */
+  rtreeShadowName,            /* xShadowName */
+  rtreeIntegrity              /* xIntegrity */
 };
 
 static int rtreeSqlInit(
@@ -3758,22 +3615,27 @@ static int rtreeInit(
   }
 
   sqlite3_vtab_config(db, SQLITE_VTAB_CONSTRAINT_SUPPORT, 1);
+  sqlite3_vtab_config(db, SQLITE_VTAB_INNOCUOUS);
+
 
   /* Allocate the sqlite3_vtab structure */
   nDb = (int)strlen(argv[1]);
   nName = (int)strlen(argv[2]);
-  pRtree = (Rtree *)sqlite3_malloc64(sizeof(Rtree)+nDb+nName+2);
+  pRtree = (Rtree *)sqlite3_malloc64(sizeof(Rtree)+nDb+nName*2+8);
   if( !pRtree ){
     return SQLITE_NOMEM;
   }
-  memset(pRtree, 0, sizeof(Rtree)+nDb+nName+2);
+  memset(pRtree, 0, sizeof(Rtree)+nDb+nName*2+8);
   pRtree->nBusy = 1;
   pRtree->base.pModule = &rtreeModule;
   pRtree->zDb = (char *)&pRtree[1];
   pRtree->zName = &pRtree->zDb[nDb+1];
+  pRtree->zNodeName = &pRtree->zName[nName+1];
   pRtree->eCoordType = (u8)eCoordType;
   memcpy(pRtree->zDb, argv[1], nDb);
   memcpy(pRtree->zName, argv[2], nName);
+  memcpy(pRtree->zNodeName, argv[2], nName);
+  memcpy(&pRtree->zNodeName[nName], "_node", 6);
 
 
   /* Create/Connect to the underlying relational database schema. If
@@ -4270,7 +4132,6 @@ static int rtreeCheckTable(
 ){
   RtreeCheck check;               /* Common context for various routines */
   sqlite3_stmt *pStmt = 0;        /* Used to find column count of rtree table */
-  int bEnd = 0;                   /* True if transaction should be closed */
   int nAux = 0;                   /* Number of extra columns. */
 
   /* Initialize the context object */
@@ -4279,14 +4140,6 @@ static int rtreeCheckTable(
   check.zDb = zDb;
   check.zTab = zTab;
 
-  /* If there is not already an open transaction, open one now. This is
-  ** to ensure that the queries run as part of this integrity-check operate
-  ** on a consistent snapshot.  */
-  if( sqlite3_get_autocommit(db) ){
-    check.rc = sqlite3_exec(db, "BEGIN", 0, 0, 0);
-    bEnd = 1;
-  }
-
   /* Find the number of auxiliary columns */
   if( check.rc==SQLITE_OK ){
     pStmt = rtreeCheckPrepare(&check, "SELECT * FROM %Q.'%q_rowid'", zDb, zTab);
@@ -4327,15 +4180,34 @@ static int rtreeCheckTable(
   sqlite3_finalize(check.aCheckMapping[0]);
   sqlite3_finalize(check.aCheckMapping[1]);
 
-  /* If one was opened, close the transaction */
-  if( bEnd ){
-    int rc = sqlite3_exec(db, "END", 0, 0, 0);
-    if( check.rc==SQLITE_OK ) check.rc = rc;
-  }
   *pzReport = check.zReport;
   return check.rc;
 }
 
+/*
+** Implementation of the xIntegrity method for Rtree.
+*/
+static int rtreeIntegrity(
+  sqlite3_vtab *pVtab,   /* The virtual table to check */
+  const char *zSchema,   /* Schema in which the virtual table lives */
+  const char *zName,     /* Name of the virtual table */
+  int isQuick,           /* True for a quick_check */
+  char **pzErr           /* Write results here */
+){
+  Rtree *pRtree = (Rtree*)pVtab;
+  int rc;
+  assert( pzErr!=0 && *pzErr==0 );
+  UNUSED_PARAMETER(zSchema);
+  UNUSED_PARAMETER(zName);
+  UNUSED_PARAMETER(isQuick);
+  rc = rtreeCheckTable(pRtree->db, pRtree->zDb, pRtree->zName, pzErr);
+  if( rc==SQLITE_OK && *pzErr ){
+    *pzErr = sqlite3_mprintf("In RTree %s.%s:\n%z",
+                 pRtree->zDb, pRtree->zName, *pzErr);
+  }
+  return rc;
+}
+
 /*
 ** Usage:
 **
diff --git a/sqlite/ext/rtree/rtree1.test b/sqlite/ext/rtree/rtree1.test
index 03415534..61664e15 100644
--- a/sqlite/ext/rtree/rtree1.test
+++ b/sqlite/ext/rtree/rtree1.test
@@ -756,4 +756,45 @@ do_execsql_test 20.4 {
   SELECT * FROM t1 JOIN t0 ON true RIGHT JOIN rt0 ON x0>a WHERE x0 = 0;
 } {- - 0 0.0 0.0}
 
+# 2023-05-19 https://sqlite.org/forum/forumpost/da61c4a1b5b4af19
+# Do not omit constraints that involve equality comparisons of
+# floating-point values.
+#
+reset_db
+do_execsql_test 21.0 {
+  CREATE VIRTUAL TABLE t1 USING rtree(id, x0, x1);
+  INSERT INTO t1 VALUES(0, 1, 9223372036854775807);
+  SELECT count(*) FROM t1 WHERE x1=9223372036854775807;
+} {0}
+do_execsql_test 21.1 {
+ SELECT x1=9223372036854775807 FROM t1;
+} {0}
+
+# 2023-05-22 https://sqlite.org/forum/forumpost/da70ee0d0d
+# Round-off error associated with using large integer constraints on
+# a rtree search.
+#
+if {$tcl_platform(machine)!="i686" || $tcl_platform(os)!="Linux"} {
+  reset_db
+  do_execsql_test 22.0 {
+    CREATE VIRTUAL TABLE t1 USING rtree ( id, x0, x1 );  
+    INSERT INTO t1 VALUES (123, 9223372036854775799, 9223372036854775800);
+    SELECT id FROM t1 WHERE x0 > 9223372036854775807;
+  } {123}
+  do_execsql_test 22.1 {
+    SELECT id, x0 > 9223372036854775807 AS 'a0' FROM t1;
+  } {123 1}
+}
+
+# 2023-10-14 dbsqlfuzz --sql-fuzz find.  rtreecheck() should not call
+# BEGIN/COMMIT because that causes problems with statement transactions,
+# and it is unnecessary.
+#
+reset_db
+do_test 23.0 {
+  db eval {CREATE TABLE t1(a,b,c);}
+  catch {db eval {CREATE TABLE t2 AS SELECT rtreecheck('t1') AS y;}}
+  db eval {PRAGMA integrity_check;}
+} {ok}
+
 finish_test
diff --git a/sqlite/ext/rtree/rtree8.test b/sqlite/ext/rtree/rtree8.test
index 12e75a68..51bd4041 100644
--- a/sqlite/ext/rtree/rtree8.test
+++ b/sqlite/ext/rtree/rtree8.test
@@ -75,7 +75,7 @@ do_test rtree8-1.2.2 { nested_select 1 } {51}
 #
 populate_t1 1500
 do_rtree_integrity_test rtree8-1.3.0 t1
-do_execsql_test rtree8-1.3.1 { SELECT max(nodeno) FROM t1_node } {164}
+do_execsql_test rtree8-1.3.1 { SELECT max(nodeno) FROM t1_node } {183}
 do_test rtree8-1.3.2 {
   set rowids [execsql {SELECT min(rowid) FROM t1_rowid GROUP BY nodeno}]
   set stmt_list [list]
diff --git a/sqlite/ext/rtree/rtreeA.test b/sqlite/ext/rtree/rtreeA.test
index 301cd4fc..0b52070c 100644
--- a/sqlite/ext/rtree/rtreeA.test
+++ b/sqlite/ext/rtree/rtreeA.test
@@ -113,7 +113,7 @@ do_execsql_test rtreeA-1.1.1 {
   SELECT rtreecheck('main', 't1')
 } {{Node 1 missing from database
 Wrong number of entries in %_rowid table - expected 0, actual 500
-Wrong number of entries in %_parent table - expected 0, actual 23}}
+Wrong number of entries in %_parent table - expected 0, actual 25}}
 
 do_execsql_test  rtreeA-1.2.0 { DROP TABLE t1_node } {}
 do_corruption_tests rtreeA-1.2 -error "database disk image is malformed" {
@@ -191,7 +191,7 @@ do_execsql_test rtreeA-3.3.3.4 {
   SELECT rtreecheck('main', 't1')
 } {{Rtree depth out of range (65535)
 Wrong number of entries in %_rowid table - expected 0, actual 499
-Wrong number of entries in %_parent table - expected 0, actual 23}}
+Wrong number of entries in %_parent table - expected 0, actual 25}}
 
 #-------------------------------------------------------------------------
 # Set the "number of entries" field on some nodes incorrectly.
diff --git a/sqlite/ext/rtree/rtree_util.tcl b/sqlite/ext/rtree/rtree_util.tcl
index afa588e4..5640baf4 100644
--- a/sqlite/ext/rtree/rtree_util.tcl
+++ b/sqlite/ext/rtree/rtree_util.tcl
@@ -192,6 +192,6 @@ proc rtree_treedump {db zTab} {
 }
 
 proc do_rtree_integrity_test {tn tbl} {
-  uplevel [list do_execsql_test $tn "SELECT rtreecheck('$tbl')" ok]
+  uplevel [list do_execsql_test $tn.1 "SELECT rtreecheck('$tbl')" ok]
+  uplevel [list do_execsql_test $tn.2 "PRAGMA integrity_check" ok]
 }
-
diff --git a/sqlite/ext/rtree/rtreecheck.test b/sqlite/ext/rtree/rtreecheck.test
index ff5397e1..7a98f9bf 100644
--- a/sqlite/ext/rtree/rtreecheck.test
+++ b/sqlite/ext/rtree/rtreecheck.test
@@ -78,6 +78,11 @@ do_execsql_test 2.3 {
   SELECT rtreecheck('r1') 
 } {{Dimension 0 of cell 0 on node 1 is corrupt
 Dimension 1 of cell 3 on node 1 is corrupt}}
+do_execsql_test 2.3b { 
+  PRAGMA integrity_check;
+} {{In RTree main.r1:
+Dimension 0 of cell 0 on node 1 is corrupt
+Dimension 1 of cell 3 on node 1 is corrupt}}
 
 setup_simple_db
 do_execsql_test 2.4 {
@@ -85,12 +90,21 @@ do_execsql_test 2.4 {
   SELECT rtreecheck('r1') 
 } {{Mapping (3 -> 1) missing from %_rowid table
 Wrong number of entries in %_rowid table - expected 5, actual 4}}
+do_execsql_test 2.4b {
+  PRAGMA integrity_check
+} {{In RTree main.r1:
+Mapping (3 -> 1) missing from %_rowid table
+Wrong number of entries in %_rowid table - expected 5, actual 4}}
 
 setup_simple_db
 do_execsql_test 2.5 {
   UPDATE r1_rowid SET nodeno=2 WHERE rowid=3;
   SELECT rtreecheck('r1') 
 } {{Found (3 -> 2) in %_rowid table, expected (3 -> 1)}}
+do_execsql_test 2.5b {
+  PRAGMA integrity_check
+} {{In RTree main.r1:
+Found (3 -> 2) in %_rowid table, expected (3 -> 1)}}
 
 reset_db
 do_execsql_test 3.0 { 
@@ -104,14 +118,16 @@ do_execsql_test 3.0 {
   INSERT INTO r1 VALUES(7, 5, 0x00000080);
   INSERT INTO r1 VALUES(8, 5, 0x40490fdb);
   INSERT INTO r1 VALUES(9, 0x7f800000, 0x7f900000);
-  SELECT rtreecheck('r1') 
-} {ok}
+  SELECT rtreecheck('r1');
+  PRAGMA integrity_check;
+} {ok ok}
 
 do_execsql_test 3.1 { 
   CREATE VIRTUAL TABLE r2 USING rtree_i32(id, x1, x2);
   INSERT INTO r2 VALUES(2, -1*(1<<31), -1*(1<<31)+5);
-  SELECT rtreecheck('r2') 
-} {ok}
+  SELECT rtreecheck('r2');
+  PRAGMA integrity_check;
+} {ok ok}
 
 sqlite3_db_config db DEFENSIVE 0
 do_execsql_test 3.2 {
@@ -125,6 +141,11 @@ do_execsql_test 3.3 {
   UPDATE r2_node SET data = X'00001234';
   SELECT rtreecheck('r2')!='ok';
 } {1}
+do_execsql_test 3.4 {
+  PRAGMA integrity_check;
+} {{In RTree main.r2:
+Node 1 is too small for cell count of 4660 (4 bytes)
+Wrong number of entries in %_rowid table - expected 0, actual 1}}
 
 do_execsql_test 4.0 {
   CREATE TABLE notanrtree(i);
@@ -181,4 +202,3 @@ if {[permutation]=="inmemory_journal"} {
 }
 
 finish_test
-
diff --git a/sqlite/ext/rtree/rtreedoc.test b/sqlite/ext/rtree/rtreedoc.test
index b64faa2e..4e610db8 100644
--- a/sqlite/ext/rtree/rtreedoc.test
+++ b/sqlite/ext/rtree/rtreedoc.test
@@ -601,11 +601,21 @@ do_execsql_test 2.5.2 {
   SELECT A.id FROM demo_index AS A, demo_index AS B
     WHERE A.maxX>=B.minX AND A.minX<=B.maxX
     AND A.maxY>=B.minY AND A.minY<=B.maxY
-    AND B.id=28269;
+    AND B.id=28269 ORDER BY +A.id;
 } {
-  28293 28216 28322 28286 28269 
-  28215 28336 28262 28291 28320 
-  28313 28298 28287
+  28215
+  28216
+  28262
+  28269
+  28286
+  28287
+  28291
+  28293
+  28298
+  28313
+  28320
+  28322
+  28336
 }
 
 # EVIDENCE-OF: R-02723-34107 Note that it is not necessary for all
@@ -1575,7 +1585,7 @@ execsql BEGIN
 do_test 3.6 {
   execsql { INSERT INTO rt2_parent VALUES(1000, 1000) }
   execsql { SELECT rtreecheck('rt2') }
-} {{Wrong number of entries in %_parent table - expected 9, actual 10}}
+} {{Wrong number of entries in %_parent table - expected 10, actual 11}}
 execsql ROLLBACK
 
 
diff --git a/sqlite/ext/rtree/rtreefuzz001.test b/sqlite/ext/rtree/rtreefuzz001.test
index 58fd179a..c81c41da 100644
--- a/sqlite/ext/rtree/rtreefuzz001.test
+++ b/sqlite/ext/rtree/rtreefuzz001.test
@@ -1043,7 +1043,7 @@ do_test rtreefuzz001-500 {
 | end crash-2e81f5dce5cbd4.db}]
   execsql { PRAGMA writable_schema = 1;}
   catchsql {UPDATE t1 SET ex= ex ISNULL}
-} {1 {database disk image is malformed}}
+} {0 {}}
 
 do_test rtreefuzz001-600 {
   sqlite3 db {}
diff --git a/sqlite/ext/session/session3.test b/sqlite/ext/session/session3.test
index ba316348..ee955f13 100644
--- a/sqlite/ext/session/session3.test
+++ b/sqlite/ext/session/session3.test
@@ -135,8 +135,8 @@ do_test 2.2.2 {
     DROP TABLE t2;
     CREATE TABLE t2(a, b PRIMARY KEY, c, d);
   }
-  list [catch { S changeset } msg] $msg
-} {1 SQLITE_SCHEMA}
+  catch { S changeset } 
+} {0}
 do_test 2.2.3 {
   S delete
   sqlite3session S db main
@@ -167,8 +167,8 @@ do_test 2.2.4 {
     CREATE TABLE t2(a, b PRIMARY KEY, c, d);
     INSERT INTO t2 VALUES(4, 5, 6, 7);
   }
-  list [catch { S changeset } msg] $msg
-} {1 SQLITE_SCHEMA}
+  catch { S changeset }
+} {0}
 
 do_test 2.3 {
   S delete
diff --git a/sqlite/ext/session/sessionalter.test b/sqlite/ext/session/sessionalter.test
new file mode 100644
index 00000000..34424cf2
--- /dev/null
+++ b/sqlite/ext/session/sessionalter.test
@@ -0,0 +1,238 @@
+# 2023 October 02
+#
+# 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 that the sessions module interacts well with
+# the ALTER TABLE ADD COLUMN command.
+#
+
+if {![info exists testdir]} {
+  set testdir [file join [file dirname [info script]] .. .. test]
+} 
+source [file join [file dirname [info script]] session_common.tcl]
+source $testdir/tester.tcl
+
+ifcapable !session {finish_test; return}
+set testprefix sessionalter
+
+
+forcedelete test.db2
+sqlite3 db2 test.db2
+
+do_execsql_test 1.0 {
+  CREATE TABLE t1(a INTEGER PRIMARY KEY, b);
+}
+
+do_execsql_test -db db2 1.1 {
+  CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c DEFAULT 1234);
+}
+
+do_then_apply_sql {
+  INSERT INTO t1 VALUES(1, 'one');
+  INSERT INTO t1 VALUES(2, 'two');
+}
+
+do_execsql_test -db db2 1.2 {
+  SELECT * FROM t1
+} {
+  1 one 1234
+  2 two 1234
+}
+
+do_then_apply_sql {
+  UPDATE t1 SET b='four' WHERE a=2;
+}
+
+do_execsql_test -db db2 1.3 {
+  SELECT * FROM t1
+} {
+  1 one 1234
+  2 four 1234
+}
+
+do_then_apply_sql {
+  DELETE FROM t1 WHERE a=1;
+}
+
+do_execsql_test -db db2 1.4 {
+  SELECT * FROM t1
+} {
+  2 four 1234
+}
+
+db2 close
+
+#--------------------------------------------------------------------------
+reset_db
+
+do_execsql_test 2.0 {
+  CREATE TABLE t1(a INTEGER PRIMARY KEY, b);
+}
+
+do_test 2.1 {
+  sqlite3session S db main
+  S attach t1
+  set {} {}
+} {} 
+do_execsql_test 2.2 {
+  INSERT INTO t1 VALUES(1, 2);
+  ALTER TABLE t1 ADD COLUMN c DEFAULT 'abcd';
+  INSERT INTO t1 VALUES(2, 3, 4);
+}
+do_changeset_test 2.3 S {
+  {INSERT t1 0 X.. {} {i 1 i 2 t abcd}} 
+  {INSERT t1 0 X.. {} {i 2 i 3 i 4}}
+}
+
+do_iterator_test 2.4 {} {
+  DELETE FROM t1 WHERE a=2;
+  ALTER TABLE t1 ADD COLUMN d DEFAULT 'abcd';
+  ALTER TABLE t1 ADD COLUMN e DEFAULT 5;
+  ALTER TABLE t1 ADD COLUMN f DEFAULT 7.2;
+  -- INSERT INTO t1 VALUES(9, 9, 9, 9);
+} {
+  {DELETE t1 0 X..... {i 2 i 3 i 4 t abcd i 5 f 7.2} {}}
+}
+
+#-------------------------------------------------------------------------
+# Tests of the sqlite3changegroup_xxx() APIs.
+#
+reset_db
+do_execsql_test 3.0 {
+  CREATE TABLE t1(x INTEGER PRIMARY KEY, y);
+  CREATE TABLE t2(x PRIMARY KEY, y);
+  CREATE TABLE t3(x, y);
+  CREATE TABLE t4(y PRIMARY KEY, x) WITHOUT ROWID;
+
+  INSERT INTO t1 VALUES(1, 2), (3, 4), (5, 6);
+  INSERT INTO t2 VALUES('one', 'two'), ('three', 'four'), ('five', 'six');
+  INSERT INTO t3 VALUES(1, 2), (3, 4), (5, 6);
+
+  INSERT INTO t4(x, y) VALUES(1, 2), (3, 4), (5, 6);
+}
+
+db_save_and_close
+foreach {tn sql1 at sql2} {
+  1 {
+    INSERT INTO t1(x, y) VALUES(7, 8);
+  } {
+    ALTER TABLE t1 ADD COLUMN z DEFAULT 10;
+  } {
+    UPDATE t1 SET y=11 WHERE x=7;
+  }
+
+  2 {
+    UPDATE t2 SET y='two.two' WHERE x='one';
+    DELETE FROM t2 WHERE x='five';
+    INSERT INTO t2(x, y) VALUES('seven', 'eight');
+  } {
+    ALTER TABLE t2 ADD COLUMN z;
+    ALTER TABLE t2 ADD COLUMN zz;
+  } {
+  }
+
+  3 {
+    DELETE FROM t2 WHERE x='five';
+  } {
+    ALTER TABLE t2 ADD COLUMN z DEFAULT 'xyz';
+  } {
+  }
+
+  4 {
+    UPDATE t2 SET y='two.two' WHERE x='three';
+  } {
+    ALTER TABLE t2 ADD COLUMN z;
+  } {
+    UPDATE t2 SET z='abc' WHERE x='one';
+  }
+
+  5* {
+    UPDATE t2 SET y='two.two' WHERE x='three';
+  } {
+    ALTER TABLE t2 ADD COLUMN z DEFAULT 'defu1';
+  } {
+  }
+
+  6* {
+    INSERT INTO t2(x, y) VALUES('nine', 'ten');
+  } {
+    ALTER TABLE t2 ADD COLUMN z;
+    ALTER TABLE t2 ADD COLUMN a DEFAULT 'eelve';
+    ALTER TABLE t2 ADD COLUMN b DEFAULT x'1234abcd';
+    ALTER TABLE t2 ADD COLUMN c DEFAULT 4.2;
+    ALTER TABLE t2 ADD COLUMN d DEFAULT NULL;
+  } {
+  }
+
+  7 {
+    INSERT INTO t3(x, y) VALUES(7, 8);
+    UPDATE t3 SET y='fourteen' WHERE x=1;
+    DELETE FROM t3 WHERE x=3;
+  } {
+    ALTER TABLE t3 ADD COLUMN c;
+  } {
+    INSERT INTO t3(x, y, c) VALUES(9, 10, 11);
+  }
+
+  8 {
+    INSERT INTO t4(x, y) VALUES(7, 8);
+    UPDATE t4 SET y='fourteen' WHERE x=1;
+    DELETE FROM t4 WHERE x=3;
+  } {
+    ALTER TABLE t4 ADD COLUMN c;
+  } {
+    INSERT INTO t4(x, y, c) VALUES(9, 10, 11);
+  }
+} {
+  foreach {tn2 cmd} {
+    1 changeset_from_sql
+    2 patchset_from_sql
+  } {
+    db_restore_and_reopen
+  
+    set C1 [$cmd $sql1]
+    execsql $at
+    set C2 [$cmd $sql2]
+  
+    sqlite3changegroup grp
+    grp schema db main
+    grp add $C1
+    grp add $C2
+    set T1 [grp output]
+    grp delete
+  
+    db_restore_and_reopen
+    execsql $at
+    set T2 [$cmd "$sql1 ; $sql2"]
+  
+    if {[string range $tn end end]!="*"} {
+      do_test 3.1.$tn.$tn2.1 { changeset_to_list $T1 } [changeset_to_list $T2]
+      set testname "$tn.$tn2"
+    } else {
+      set testname "[string range $tn 0 end-1].$tn2"
+    }
+  
+    db_restore_and_reopen
+    proc xConflict {args} { return "REPLACE" }
+    sqlite3changeset_apply_v2 db $T1 xConflict
+    set S1 [scksum db main]
+  
+    db_restore_and_reopen
+    sqlite3changeset_apply_v2 db $T2 xConflict
+    set S2 [scksum db main]
+  
+    # if { $tn==7 } { puts [changeset_to_list $T1] }
+  
+    do_test 3.1.$tn.2 { set S1 } $S2
+  }
+}
+
+
+finish_test
+
diff --git a/sqlite/ext/session/sessionfault3.test b/sqlite/ext/session/sessionfault3.test
new file mode 100644
index 00000000..af5a4cdb
--- /dev/null
+++ b/sqlite/ext/session/sessionfault3.test
@@ -0,0 +1,59 @@
+# 2016 October 6
+#
+# The author disclaims copyright to this source code.  In place of
+# a legal notice, here is a blessing:
+#
+#    May you do good and not evil.
+#    May you find forgiveness for yourself and forgive others.
+#    May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+# The focus of this file is testing the session module.
+#
+
+if {![info exists testdir]} {
+  set testdir [file join [file dirname [info script]] .. .. test]
+} 
+source [file join [file dirname [info script]] session_common.tcl]
+source $testdir/tester.tcl
+ifcapable !session {finish_test; return}
+set testprefix sessionfault3
+
+do_execsql_test 1.0 {
+  CREATE TABLE t1(a, b, PRIMARY KEY(a));
+  INSERT INTO t1 VALUES(1, 2);
+  INSERT INTO t1 VALUES(3, 4);
+  INSERT INTO t1 VALUES('five', 'six');
+}
+
+set C1 [changeset_from_sql {
+  INSERT INTO t1 VALUES('seven', 'eight');
+  UPDATE t1 SET b=6 WHERE a='five';
+  DELETE FROM t1 WHERE a=1;
+}]
+
+do_execsql_test 1.1 {
+  ALTER TABLE t1 ADD COLUMN d DEFAULT 123;
+  ALTER TABLE t1 ADD COLUMN e DEFAULT 'string';
+}
+
+set C2 [changeset_from_sql {
+  UPDATE t1 SET e='new value' WHERE a='seven';
+  INSERT INTO t1 VALUES(0, 0, 0, 0);
+}]
+
+do_faultsim_test 1 -faults oom* -prep {
+  sqlite3changegroup G
+} -body {
+  G schema db main
+  G add $::C1
+  G add $::C2
+  G output
+  set {} {}
+} -test {
+  catch { G delete }
+  faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
+}
+
+finish_test
diff --git a/sqlite/ext/session/sessionnoact.test b/sqlite/ext/session/sessionnoact.test
new file mode 100644
index 00000000..1274ecb1
--- /dev/null
+++ b/sqlite/ext/session/sessionnoact.test
@@ -0,0 +1,110 @@
+# 2023 October 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 file implements regression tests for SQLite library.
+#
+
+if {![info exists testdir]} {
+  set testdir [file join [file dirname [info script]] .. .. test]
+} 
+source [file join [file dirname [info script]] session_common.tcl]
+source $testdir/tester.tcl
+ifcapable !session {finish_test; return}
+
+set testprefix sessionnoact
+
+do_execsql_test 1.0 {
+  CREATE TABLE p1(a INTEGER PRIMARY KEY, b, c UNIQUE);
+  INSERT INTO p1 VALUES(1, 1, 'one');
+  INSERT INTO p1 VALUES(2, 2, 'two');
+  INSERT INTO p1 VALUES(3, 3, 'three');
+  INSERT INTO p1 VALUES(4, 4, 'four');
+}
+
+db_save
+
+set C [changeset_from_sql {
+  DELETE FROM p1 WHERE a=2;
+  UPDATE p1 SET c='six' WHERE a=3;
+  INSERT INTO p1 VALUES(5, 5, 'two');
+  INSERT INTO p1 VALUES(6, 6, 'three');
+}]
+
+db_restore_and_reopen
+
+do_execsql_test 1.1 {
+  CREATE TABLE c1(x INTEGER PRIMARY KEY, y,
+    FOREIGN KEY(y) REFERENCES p1(c) ON DELETE CASCADE ON UPDATE SET NULL
+  );
+
+  INSERT INTO c1 VALUES(10, 'one');
+  INSERT INTO c1 VALUES(20, 'two');
+  INSERT INTO c1 VALUES(30, 'three');
+  INSERT INTO c1 VALUES(40, 'four');
+}
+
+db_save
+
+do_execsql_test 1.2 {
+  PRAGMA foreign_keys = 1;
+}
+
+set ::nConflict 0
+proc conflict {args} {
+  incr ::nConflict
+  return "OMIT"
+}
+
+sqlite3changeset_apply_v2 db $C conflict
+
+do_execsql_test 1.3 {
+  SELECT * FROM c1
+} {
+  10 one
+  30 {}
+  40 four
+}
+
+db_restore_and_reopen
+
+do_execsql_test 1.4 {
+  PRAGMA foreign_keys = 1;
+}
+
+do_execsql_test 1.5 {
+  UPDATE p1 SET c=12345 WHERE a = 45;
+}
+
+sqlite3changeset_apply_v2 -noaction db $C conflict
+do_execsql_test 1.6 {
+  SELECT * FROM c1
+} {
+  10 one
+  20 two
+  30 three
+  40 four
+}
+
+do_execsql_test 1.7 {
+  PRAGMA foreign_keys = 1;
+  UPDATE p1 SET c = 'ten' WHERE c='two';
+  SELECT * FROM c1;
+} {
+  10 one
+  20 {}
+  30 three
+  40 four
+}
+
+do_execsql_test 1.8 {
+  PRAGMA foreign_key_check
+}
+
+finish_test
diff --git a/sqlite/ext/session/sqlite3session.c b/sqlite/ext/session/sqlite3session.c
index 67940884..4f682a13 100644
--- a/sqlite/ext/session/sqlite3session.c
+++ b/sqlite/ext/session/sqlite3session.c
@@ -119,6 +119,18 @@ struct sqlite3_changeset_iter {
 ** The data associated with each hash-table entry is a structure containing
 ** a subset of the initial values that the modified row contained at the
 ** start of the session. Or no initial values if the row was inserted.
+**
+** pDfltStmt:
+**   This is only used by the sqlite3changegroup_xxx() APIs, not by
+**   regular sqlite3_session objects. It is a SELECT statement that
+**   selects the default value for each table column. For example,
+**   if the table is 
+**
+**      CREATE TABLE xx(a DEFAULT 1, b, c DEFAULT 'abc')
+**
+**   then this variable is the compiled version of:
+**
+**      SELECT 1, NULL, 'abc'
 */
 struct SessionTable {
   SessionTable *pNext;
@@ -127,10 +139,12 @@ struct SessionTable {
   int bStat1;                     /* True if this is sqlite_stat1 */
   int bRowid;                     /* True if this table uses rowid for PK */
   const char **azCol;             /* Column names */
+  const char **azDflt;            /* Default value expressions */
   u8 *abPK;                       /* Array of primary key flags */
   int nEntry;                     /* Total number of entries in hash table */
   int nChange;                    /* Size of apChange[] array */
   SessionChange **apChange;       /* Hash table buckets */
+  sqlite3_stmt *pDfltStmt;
 };
 
 /* 
@@ -299,6 +313,7 @@ struct SessionTable {
 struct SessionChange {
   u8 op;                          /* One of UPDATE, DELETE, INSERT */
   u8 bIndirect;                   /* True if this change is "indirect" */
+  u16 nRecordField;               /* Number of fields in aRecord[] */
   int nMaxSize;                   /* Max size of eventual changeset record */
   int nRecord;                    /* Number of bytes in buffer aRecord[] */
   u8 *aRecord;                    /* Buffer containing old.* record */
@@ -324,7 +339,7 @@ static int sessionVarintLen(int iVal){
 ** Read a varint value from aBuf[] into *piVal. Return the number of 
 ** bytes read.
 */
-static int sessionVarintGet(u8 *aBuf, int *piVal){
+static int sessionVarintGet(const u8 *aBuf, int *piVal){
   return getVarint32(aBuf, *piVal);
 }
 
@@ -587,9 +602,11 @@ static int sessionPreupdateHash(
 ** Return the number of bytes of space occupied by the value (including
 ** the type byte).
 */
-static int sessionSerialLen(u8 *a){
-  int e = *a;
+static int sessionSerialLen(const u8 *a){
+  int e;
   int n;
+  assert( a!=0 );
+  e = *a;
   if( e==0 || e==0xFF ) return 1;
   if( e==SQLITE_NULL ) return 1;
   if( e==SQLITE_INTEGER || e==SQLITE_FLOAT ) return 9;
@@ -891,6 +908,7 @@ static int sessionPreupdateEqual(
         rc = pSession->hook.xOld(pSession->hook.pCtx, iCol, &pVal);
       }
       assert( rc==SQLITE_OK );
+      (void)rc;                   /* Suppress warning about unused variable */
       if( sqlite3_value_type(pVal)!=eType ) return 0;
 
       /* A SessionChange object never has a NULL value in a PK column */
@@ -993,13 +1011,14 @@ static int sessionGrowHash(
 **
 ** For example, if the table is declared as:
 **
-**     CREATE TABLE tbl1(w, x, y, z, PRIMARY KEY(w, z));
+**     CREATE TABLE tbl1(w, x DEFAULT 'abc', y, z, PRIMARY KEY(w, z));
 **
-** Then the four output variables are populated as follows:
+** Then the five output variables are populated as follows:
 **
 **     *pnCol  = 4
 **     *pzTab  = "tbl1"
 **     *pazCol = {"w", "x", "y", "z"}
+**     *pazDflt = {NULL, 'abc', NULL, NULL}
 **     *pabPK  = {1, 0, 0, 1}
 **
 ** All returned buffers are part of the same single allocation, which must
@@ -1013,6 +1032,7 @@ static int sessionTableInfo(
   int *pnCol,                     /* OUT: number of columns */
   const char **pzTab,             /* OUT: Copy of zThis */
   const char ***pazCol,           /* OUT: Array of column names for table */
+  const char ***pazDflt,          /* OUT: Array of default value expressions */
   u8 **pabPK,                     /* OUT: Array of booleans - true for PK col */
   int *pbRowid                    /* OUT: True if only PK is a rowid */
 ){
@@ -1025,11 +1045,18 @@ static int sessionTableInfo(
   int i;
   u8 *pAlloc = 0;
   char **azCol = 0;
+  char **azDflt = 0;
   u8 *abPK = 0;
   int bRowid = 0;                 /* Set to true to use rowid as PK */
 
   assert( pazCol && pabPK );
 
+  *pazCol = 0;
+  *pabPK = 0;
+  *pnCol = 0;
+  if( pzTab ) *pzTab = 0;
+  if( pazDflt ) *pazDflt = 0;
+
   nThis = sqlite3Strlen30(zThis);
   if( nThis==12 && 0==sqlite3_stricmp("sqlite_stat1", zThis) ){
     rc = sqlite3_table_column_metadata(db, zDb, zThis, 0, 0, 0, 0, 0, 0);
@@ -1043,39 +1070,28 @@ static int sessionTableInfo(
     }else if( rc==SQLITE_ERROR ){
       zPragma = sqlite3_mprintf("");
     }else{
-      *pazCol = 0;
-      *pabPK = 0;
-      *pnCol = 0;
-      if( pzTab ) *pzTab = 0;
       return rc;
     }
   }else{
     zPragma = sqlite3_mprintf("PRAGMA '%q'.table_info('%q')", zDb, zThis);
   }
   if( !zPragma ){
-    *pazCol = 0;
-    *pabPK = 0;
-    *pnCol = 0;
-    if( pzTab ) *pzTab = 0;
     return SQLITE_NOMEM;
   }
 
   rc = sqlite3_prepare_v2(db, zPragma, -1, &pStmt, 0);
   sqlite3_free(zPragma);
   if( rc!=SQLITE_OK ){
-    *pazCol = 0;
-    *pabPK = 0;
-    *pnCol = 0;
-    if( pzTab ) *pzTab = 0;
     return rc;
   }
 
   nByte = nThis + 1;
   bRowid = (pbRowid!=0);
   while( SQLITE_ROW==sqlite3_step(pStmt) ){
-    nByte += sqlite3_column_bytes(pStmt, 1);
+    nByte += sqlite3_column_bytes(pStmt, 1);          /* name */
+    nByte += sqlite3_column_bytes(pStmt, 4);          /* dflt_value */
     nDbCol++;
-    if( sqlite3_column_int(pStmt, 5) ) bRowid = 0;
+    if( sqlite3_column_int(pStmt, 5) ) bRowid = 0;    /* pk */
   }
   if( nDbCol==0 ) bRowid = 0;
   nDbCol += bRowid;
@@ -1083,15 +1099,18 @@ static int sessionTableInfo(
   rc = sqlite3_reset(pStmt);
 
   if( rc==SQLITE_OK ){
-    nByte += nDbCol * (sizeof(const char *) + sizeof(u8) + 1);
+    nByte += nDbCol * (sizeof(const char *)*2 + sizeof(u8) + 1 + 1);
     pAlloc = sessionMalloc64(pSession, nByte);
     if( pAlloc==0 ){
       rc = SQLITE_NOMEM;
+    }else{
+      memset(pAlloc, 0, nByte);
     }
   }
   if( rc==SQLITE_OK ){
     azCol = (char **)pAlloc;
-    pAlloc = (u8 *)&azCol[nDbCol];
+    azDflt = (char**)&azCol[nDbCol];
+    pAlloc = (u8 *)&azDflt[nDbCol];
     abPK = (u8 *)pAlloc;
     pAlloc = &abPK[nDbCol];
     if( pzTab ){
@@ -1111,11 +1130,21 @@ static int sessionTableInfo(
     }
     while( SQLITE_ROW==sqlite3_step(pStmt) ){
       int nName = sqlite3_column_bytes(pStmt, 1);
+      int nDflt = sqlite3_column_bytes(pStmt, 4);
       const unsigned char *zName = sqlite3_column_text(pStmt, 1);
+      const unsigned char *zDflt = sqlite3_column_text(pStmt, 4);
+
       if( zName==0 ) break;
       memcpy(pAlloc, zName, nName+1);
       azCol[i] = (char *)pAlloc;
       pAlloc += nName+1;
+      if( zDflt ){
+        memcpy(pAlloc, zDflt, nDflt+1);
+        azDflt[i] = (char *)pAlloc;
+        pAlloc += nDflt+1;
+      }else{
+        azDflt[i] = 0;
+      }
       abPK[i] = sqlite3_column_int(pStmt, 5);
       i++;
     }
@@ -1126,14 +1155,11 @@ static int sessionTableInfo(
   ** free any allocation made. An error code will be returned in this case.
   */
   if( rc==SQLITE_OK ){
-    *pazCol = (const char **)azCol;
+    *pazCol = (const char**)azCol;
+    if( pazDflt ) *pazDflt = (const char**)azDflt;
     *pabPK = abPK;
     *pnCol = nDbCol;
   }else{
-    *pazCol = 0;
-    *pabPK = 0;
-    *pnCol = 0;
-    if( pzTab ) *pzTab = 0;
     sessionFree(pSession, azCol);
   }
   if( pbRowid ) *pbRowid = bRowid;
@@ -1142,10 +1168,9 @@ static int sessionTableInfo(
 }
 
 /*
-** This function is only called from within a pre-update handler for a
-** write to table pTab, part of session pSession. If this is the first
-** write to this table, initalize the SessionTable.nCol, azCol[] and
-** abPK[] arrays accordingly.
+** This function is called to initialize the SessionTable.nCol, azCol[]
+** abPK[] and azDflt[] members of SessionTable object pTab. If these
+** fields are already initilialized, this function is a no-op.
 **
 ** If an error occurs, an error code is stored in sqlite3_session.rc and
 ** non-zero returned. Or, if no error occurs but the table has no primary
@@ -1153,15 +1178,22 @@ static int sessionTableInfo(
 ** indicate that updates on this table should be ignored. SessionTable.abPK 
 ** is set to NULL in this case.
 */
-static int sessionInitTable(sqlite3_session *pSession, SessionTable *pTab){
+static int sessionInitTable(
+  sqlite3_session *pSession,      /* Optional session handle */
+  SessionTable *pTab,             /* Table object to initialize */
+  sqlite3 *db,                    /* Database handle to read schema from */
+  const char *zDb                 /* Name of db - "main", "temp" etc. */
+){
+  int rc = SQLITE_OK;
+
   if( pTab->nCol==0 ){
     u8 *abPK;
     assert( pTab->azCol==0 || pTab->abPK==0 );
-    pSession->rc = sessionTableInfo(pSession, pSession->db, pSession->zDb, 
-        pTab->zName, &pTab->nCol, 0, &pTab->azCol, &abPK,
-        (pSession->bImplicitPK ? &pTab->bRowid : 0)
+    rc = sessionTableInfo(pSession, db, zDb, 
+        pTab->zName, &pTab->nCol, 0, &pTab->azCol, &pTab->azDflt, &abPK,
+        ((pSession==0 || pSession->bImplicitPK) ? &pTab->bRowid : 0)
     );
-    if( pSession->rc==SQLITE_OK ){
+    if( rc==SQLITE_OK ){
       int i;
       for(i=0; inCol; i++){
         if( abPK[i] ){
@@ -1173,14 +1205,321 @@ static int sessionInitTable(sqlite3_session *pSession, SessionTable *pTab){
         pTab->bStat1 = 1;
       }
 
-      if( pSession->bEnableSize ){
+      if( pSession && pSession->bEnableSize ){
         pSession->nMaxChangesetSize += (
           1 + sessionVarintLen(pTab->nCol) + pTab->nCol + strlen(pTab->zName)+1
         );
       }
     }
   }
-  return (pSession->rc || pTab->abPK==0);
+
+  if( pSession ){
+    pSession->rc = rc;
+    return (rc || pTab->abPK==0);
+  }
+  return rc;
+}
+
+/*
+** Re-initialize table object pTab.
+*/
+static int sessionReinitTable(sqlite3_session *pSession, SessionTable *pTab){
+  int nCol = 0;
+  const char **azCol = 0;
+  const char **azDflt = 0;
+  u8 *abPK = 0; 
+  int bRowid = 0;
+
+  assert( pSession->rc==SQLITE_OK );
+
+  pSession->rc = sessionTableInfo(pSession, pSession->db, pSession->zDb, 
+      pTab->zName, &nCol, 0, &azCol, &azDflt, &abPK,
+      (pSession->bImplicitPK ? &bRowid : 0)
+  );
+  if( pSession->rc==SQLITE_OK ){
+    if( pTab->nCol>nCol || pTab->bRowid!=bRowid ){
+      pSession->rc = SQLITE_SCHEMA;
+    }else{
+      int ii;
+      int nOldCol = pTab->nCol;
+      for(ii=0; iinCol ){
+          if( pTab->abPK[ii]!=abPK[ii] ){
+            pSession->rc = SQLITE_SCHEMA;
+          }
+        }else if( abPK[ii] ){
+          pSession->rc = SQLITE_SCHEMA;
+        }
+      }
+
+      if( pSession->rc==SQLITE_OK ){
+        const char **a = pTab->azCol;
+        pTab->azCol = azCol;
+        pTab->nCol = nCol;
+        pTab->azDflt = azDflt;
+        pTab->abPK = abPK;
+        azCol = a;
+      }
+      if( pSession->bEnableSize ){
+        pSession->nMaxChangesetSize += (nCol - nOldCol);
+        pSession->nMaxChangesetSize += sessionVarintLen(nCol);
+        pSession->nMaxChangesetSize -= sessionVarintLen(nOldCol);
+      }
+    }
+  }
+
+  sqlite3_free((char*)azCol);
+  return pSession->rc;
+}
+
+/*
+** Session-change object (*pp) contains an old.* record with fewer than
+** nCol fields. This function updates it with the default values for
+** the missing fields.
+*/
+static void sessionUpdateOneChange(
+  sqlite3_session *pSession,      /* For memory accounting */
+  int *pRc,                       /* IN/OUT: Error code */
+  SessionChange **pp,             /* IN/OUT: Change object to update */
+  int nCol,                       /* Number of columns now in table */
+  sqlite3_stmt *pDflt             /* SELECT  */
+){
+  SessionChange *pOld = *pp;
+
+  while( pOld->nRecordFieldnRecordField;
+    int eType = sqlite3_column_type(pDflt, iField);
+    switch( eType ){
+      case SQLITE_NULL:
+        nIncr = 1;
+        break;
+      case SQLITE_INTEGER:
+      case SQLITE_FLOAT:
+        nIncr = 9;
+        break;
+      default: {
+        int n = sqlite3_column_bytes(pDflt, iField);
+        nIncr = 1 + sessionVarintLen(n) + n;
+        assert( eType==SQLITE_TEXT || eType==SQLITE_BLOB );
+        break;
+      }
+    }
+
+    nByte = nIncr + (sizeof(SessionChange) + pOld->nRecord);
+    pNew = sessionMalloc64(pSession, nByte);
+    if( pNew==0 ){
+      *pRc = SQLITE_NOMEM;
+      return;
+    }else{
+      memcpy(pNew, pOld, sizeof(SessionChange));
+      pNew->aRecord = (u8*)&pNew[1];
+      memcpy(pNew->aRecord, pOld->aRecord, pOld->nRecord);
+      pNew->aRecord[pNew->nRecord++] = (u8)eType;
+      switch( eType ){
+        case SQLITE_INTEGER: {
+          i64 iVal = sqlite3_column_int64(pDflt, iField);
+          sessionPutI64(&pNew->aRecord[pNew->nRecord], iVal); 
+          pNew->nRecord += 8;
+          break;
+        }
+          
+        case SQLITE_FLOAT: {
+          double rVal = sqlite3_column_double(pDflt, iField);
+          i64 iVal = 0;
+          memcpy(&iVal, &rVal, sizeof(rVal));
+          sessionPutI64(&pNew->aRecord[pNew->nRecord], iVal); 
+          pNew->nRecord += 8;
+          break;
+        }
+
+        case SQLITE_TEXT: {
+          int n = sqlite3_column_bytes(pDflt, iField);
+          const char *z = (const char*)sqlite3_column_text(pDflt, iField);
+          pNew->nRecord += sessionVarintPut(&pNew->aRecord[pNew->nRecord], n); 
+          memcpy(&pNew->aRecord[pNew->nRecord], z, n);
+          pNew->nRecord += n;
+          break;
+        }
+
+        case SQLITE_BLOB: {
+          int n = sqlite3_column_bytes(pDflt, iField);
+          const u8 *z = (const u8*)sqlite3_column_blob(pDflt, iField);
+          pNew->nRecord += sessionVarintPut(&pNew->aRecord[pNew->nRecord], n); 
+          memcpy(&pNew->aRecord[pNew->nRecord], z, n);
+          pNew->nRecord += n;
+          break;
+        }
+
+        default:
+          assert( eType==SQLITE_NULL );
+          break;
+      }
+
+      sessionFree(pSession, pOld);
+      *pp = pOld = pNew;
+      pNew->nRecordField++;
+      pNew->nMaxSize += nIncr;
+      if( pSession ){
+        pSession->nMaxChangesetSize += nIncr;
+      }
+    }
+  }
+}
+
+/*
+** Ensure that there is room in the buffer to append nByte bytes of data.
+** If not, use sqlite3_realloc() to grow the buffer so that there is.
+**
+** If successful, return zero. Otherwise, if an OOM condition is encountered,
+** set *pRc to SQLITE_NOMEM and return non-zero.
+*/
+static int sessionBufferGrow(SessionBuffer *p, i64 nByte, int *pRc){
+#define SESSION_MAX_BUFFER_SZ (0x7FFFFF00 - 1) 
+  i64 nReq = p->nBuf + nByte;
+  if( *pRc==SQLITE_OK && nReq>p->nAlloc ){
+    u8 *aNew;
+    i64 nNew = p->nAlloc ? p->nAlloc : 128;
+
+    do {
+      nNew = nNew*2;
+    }while( nNewSESSION_MAX_BUFFER_SZ ){
+      nNew = SESSION_MAX_BUFFER_SZ;
+      if( nNewaBuf, nNew);
+    if( 0==aNew ){
+      *pRc = SQLITE_NOMEM;
+    }else{
+      p->aBuf = aNew;
+      p->nAlloc = nNew;
+    }
+  }
+  return (*pRc!=SQLITE_OK);
+}
+
+
+/*
+** This function is a no-op if *pRc is other than SQLITE_OK when it is 
+** called. Otherwise, append a string to the buffer. All bytes in the string
+** up to (but not including) the nul-terminator are written to the buffer.
+**
+** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before
+** returning.
+*/
+static void sessionAppendStr(
+  SessionBuffer *p, 
+  const char *zStr, 
+  int *pRc
+){
+  int nStr = sqlite3Strlen30(zStr);
+  if( 0==sessionBufferGrow(p, nStr+1, pRc) ){
+    memcpy(&p->aBuf[p->nBuf], zStr, nStr);
+    p->nBuf += nStr;
+    p->aBuf[p->nBuf] = 0x00;
+  }
+}
+
+/*
+** Format a string using printf() style formatting and then append it to the
+** buffer using sessionAppendString().
+*/
+static void sessionAppendPrintf(
+  SessionBuffer *p,               /* Buffer to append to */
+  int *pRc, 
+  const char *zFmt,
+  ...
+){
+  if( *pRc==SQLITE_OK ){
+    char *zApp = 0;
+    va_list ap;
+    va_start(ap, zFmt);
+    zApp = sqlite3_vmprintf(zFmt, ap);
+    if( zApp==0 ){
+      *pRc = SQLITE_NOMEM;
+    }else{
+      sessionAppendStr(p, zApp, pRc);
+    }
+    va_end(ap);
+    sqlite3_free(zApp);
+  }
+}
+
+/*
+** Prepare a statement against database handle db that SELECTs a single
+** row containing the default values for each column in table pTab. For
+** example, if pTab is declared as:
+**
+**   CREATE TABLE pTab(a PRIMARY KEY, b DEFAULT 123, c DEFAULT 'abcd');
+**
+** Then this function prepares and returns the SQL statement:
+**
+**   SELECT NULL, 123, 'abcd';
+*/
+static int sessionPrepareDfltStmt(
+  sqlite3 *db,                    /* Database handle */
+  SessionTable *pTab,             /* Table to prepare statement for */
+  sqlite3_stmt **ppStmt           /* OUT: Statement handle */
+){
+  SessionBuffer sql = {0,0,0};
+  int rc = SQLITE_OK;
+  const char *zSep = " ";
+  int ii = 0;
+
+  *ppStmt = 0;
+  sessionAppendPrintf(&sql, &rc, "SELECT");
+  for(ii=0; iinCol; ii++){
+    const char *zDflt = pTab->azDflt[ii] ? pTab->azDflt[ii] : "NULL";
+    sessionAppendPrintf(&sql, &rc, "%s%s", zSep, zDflt);
+    zSep = ", ";
+  }
+  if( rc==SQLITE_OK ){
+    rc = sqlite3_prepare_v2(db, (const char*)sql.aBuf, -1, ppStmt, 0);
+  }
+  sqlite3_free(sql.aBuf);
+
+  return rc;
+}
+
+/*
+** Table pTab has one or more existing change-records with old.* records
+** with fewer than pTab->nCol columns. This function updates all such 
+** change-records with the default values for the missing columns.
+*/
+static int sessionUpdateChanges(sqlite3_session *pSession, SessionTable *pTab){
+  sqlite3_stmt *pStmt = 0;
+  int rc = pSession->rc;
+
+  rc = sessionPrepareDfltStmt(pSession->db, pTab, &pStmt);
+  if( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){
+    int ii = 0;
+    SessionChange **pp = 0;
+    for(ii=0; iinChange; ii++){
+      for(pp=&pTab->apChange[ii]; *pp; pp=&((*pp)->pNext)){
+        if( (*pp)->nRecordField!=pTab->nCol ){
+          sessionUpdateOneChange(pSession, &rc, pp, pTab->nCol, pStmt);
+        }
+      }
+    }
+  }
+
+  pSession->rc = rc;
+  rc = sqlite3_finalize(pStmt);
+  if( pSession->rc==SQLITE_OK ) pSession->rc = rc;
+  return pSession->rc;
 }
 
 /*
@@ -1343,16 +1682,22 @@ static void sessionPreupdateOneChange(
   int iHash; 
   int bNull = 0; 
   int rc = SQLITE_OK;
+  int nExpect = 0;
   SessionStat1Ctx stat1 = {{0,0,0,0,0},0};
 
   if( pSession->rc ) return;
 
   /* Load table details if required */
-  if( sessionInitTable(pSession, pTab) ) return;
+  if( sessionInitTable(pSession, pTab, pSession->db, pSession->zDb) ) return;
 
   /* Check the number of columns in this xPreUpdate call matches the 
   ** number of columns in the table.  */
-  if( (pTab->nCol-pTab->bRowid)!=pSession->hook.xCount(pSession->hook.pCtx) ){
+  nExpect = pSession->hook.xCount(pSession->hook.pCtx);
+  if( (pTab->nCol-pTab->bRowid)nCol-pTab->bRowid)!=nExpect ){
     pSession->rc = SQLITE_SCHEMA;
     return;
   }
@@ -1429,7 +1774,7 @@ static void sessionPreupdateOneChange(
       }
   
       /* Allocate the change object */
-      pC = (SessionChange *)sessionMalloc64(pSession, nByte);
+      pC = (SessionChange*)sessionMalloc64(pSession, nByte);
       if( !pC ){
         rc = SQLITE_NOMEM;
         goto error_out;
@@ -1462,6 +1807,7 @@ static void sessionPreupdateOneChange(
       if( pSession->bIndirect || pSession->hook.xDepth(pSession->hook.pCtx) ){
         pC->bIndirect = 1;
       }
+      pC->nRecordField = pTab->nCol;
       pC->nRecord = nByte;
       pC->op = op;
       pC->pNext = pTab->apChange[iHash];
@@ -1841,7 +2187,7 @@ int sqlite3session_diff(
     /* Locate and if necessary initialize the target table object */
     rc = sessionFindTable(pSession, zTbl, &pTo);
     if( pTo==0 ) goto diff_out;
-    if( sessionInitTable(pSession, pTo) ){
+    if( sessionInitTable(pSession, pTo, pSession->db, pSession->zDb) ){
       rc = pSession->rc;
       goto diff_out;
     }
@@ -1854,7 +2200,7 @@ int sqlite3session_diff(
       int bRowid = 0;
       u8 *abPK;
       const char **azCol = 0;
-      rc = sessionTableInfo(0, db, zFrom, zTbl, &nCol, 0, &azCol, &abPK, 
+      rc = sessionTableInfo(0, db, zFrom, zTbl, &nCol, 0, &azCol, 0, &abPK, 
           pSession->bImplicitPK ? &bRowid : 0
       );
       if( rc==SQLITE_OK ){
@@ -1969,6 +2315,7 @@ static void sessionDeleteTable(sqlite3_session *pSession, SessionTable *pList){
         sessionFree(pSession, p);
       }
     }
+    sqlite3_finalize(pTab->pDfltStmt);
     sessionFree(pSession, (char*)pTab->azCol);  /* cast works around VC++ bug */
     sessionFree(pSession, pTab->apChange);
     sessionFree(pSession, pTab);
@@ -2003,7 +2350,7 @@ void sqlite3session_delete(sqlite3_session *pSession){
 
   /* Assert that all allocations have been freed and then free the 
   ** session object itself. */
-  assert( pSession->nMalloc==0 );
+  // assert( pSession->nMalloc==0 );
   sqlite3_free(pSession);
 }
 
@@ -2074,48 +2421,6 @@ int sqlite3session_attach(
   return rc;
 }
 
-/*
-** Ensure that there is room in the buffer to append nByte bytes of data.
-** If not, use sqlite3_realloc() to grow the buffer so that there is.
-**
-** If successful, return zero. Otherwise, if an OOM condition is encountered,
-** set *pRc to SQLITE_NOMEM and return non-zero.
-*/
-static int sessionBufferGrow(SessionBuffer *p, i64 nByte, int *pRc){
-#define SESSION_MAX_BUFFER_SZ (0x7FFFFF00 - 1) 
-  i64 nReq = p->nBuf + nByte;
-  if( *pRc==SQLITE_OK && nReq>p->nAlloc ){
-    u8 *aNew;
-    i64 nNew = p->nAlloc ? p->nAlloc : 128;
-
-    do {
-      nNew = nNew*2;
-    }while( nNewSESSION_MAX_BUFFER_SZ ){
-      nNew = SESSION_MAX_BUFFER_SZ;
-      if( nNewaBuf, nNew);
-    if( 0==aNew ){
-      *pRc = SQLITE_NOMEM;
-    }else{
-      p->aBuf = aNew;
-      p->nAlloc = nNew;
-    }
-  }
-  return (*pRc!=SQLITE_OK);
-}
-
 /*
 ** Append the value passed as the second argument to the buffer passed
 ** as the first.
@@ -2184,27 +2489,6 @@ static void sessionAppendBlob(
   }
 }
 
-/*
-** This function is a no-op if *pRc is other than SQLITE_OK when it is 
-** called. Otherwise, append a string to the buffer. All bytes in the string
-** up to (but not including) the nul-terminator are written to the buffer.
-**
-** If an OOM condition is encountered, set *pRc to SQLITE_NOMEM before
-** returning.
-*/
-static void sessionAppendStr(
-  SessionBuffer *p, 
-  const char *zStr, 
-  int *pRc
-){
-  int nStr = sqlite3Strlen30(zStr);
-  if( 0==sessionBufferGrow(p, nStr+1, pRc) ){
-    memcpy(&p->aBuf[p->nBuf], zStr, nStr);
-    p->nBuf += nStr;
-    p->aBuf[p->nBuf] = 0x00;
-  }
-}
-
 /*
 ** This function is a no-op if *pRc is other than SQLITE_OK when it is 
 ** called. Otherwise, append the string representation of integer iVal
@@ -2223,27 +2507,6 @@ static void sessionAppendInteger(
   sessionAppendStr(p, aBuf, pRc);
 }
 
-static void sessionAppendPrintf(
-  SessionBuffer *p,               /* Buffer to append to */
-  int *pRc, 
-  const char *zFmt,
-  ...
-){
-  if( *pRc==SQLITE_OK ){
-    char *zApp = 0;
-    va_list ap;
-    va_start(ap, zFmt);
-    zApp = sqlite3_vmprintf(zFmt, ap);
-    if( zApp==0 ){
-      *pRc = SQLITE_NOMEM;
-    }else{
-      sessionAppendStr(p, zApp, pRc);
-    }
-    va_end(ap);
-    sqlite3_free(zApp);
-  }
-}
-
 /*
 ** This function is a no-op if *pRc is other than SQLITE_OK when it is 
 ** called. Otherwise, append the string zStr enclosed in quotes (") and
@@ -2734,26 +2997,16 @@ static int sessionGenerateChangeset(
   for(pTab=pSession->pTable; rc==SQLITE_OK && pTab; pTab=pTab->pNext){
     if( pTab->nEntry ){
       const char *zName = pTab->zName;
-      int nCol = 0;               /* Number of columns in table */
-      u8 *abPK = 0;               /* Primary key array */
-      const char **azCol = 0;     /* Table columns */
       int i;                      /* Used to iterate through hash buckets */
       sqlite3_stmt *pSel = 0;     /* SELECT statement to query table pTab */
       int nRewind = buf.nBuf;     /* Initial size of write buffer */
       int nNoop;                  /* Size of buffer after writing tbl header */
-      int bRowid = 0;
+      int nOldCol = pTab->nCol;
 
       /* Check the table schema is still Ok. */
-      rc = sessionTableInfo(
-          0, db, pSession->zDb, zName, &nCol, 0, &azCol, &abPK, 
-          (pSession->bImplicitPK ? &bRowid : 0)
-      );
-      if( rc==SQLITE_OK && (
-          pTab->nCol!=nCol 
-       || pTab->bRowid!=bRowid 
-       || memcmp(abPK, pTab->abPK, nCol)
-      )){
-        rc = SQLITE_SCHEMA;
+      rc = sessionReinitTable(pSession, pTab);
+      if( rc==SQLITE_OK && pTab->nCol!=nOldCol ){
+        rc = sessionUpdateChanges(pSession, pTab);
       }
 
       /* Write a table header */
@@ -2761,8 +3014,8 @@ static int sessionGenerateChangeset(
 
       /* Build and compile a statement to execute: */
       if( rc==SQLITE_OK ){
-        rc = sessionSelectStmt(
-            db, 0, pSession->zDb, zName, bRowid, nCol, azCol, abPK, &pSel
+        rc = sessionSelectStmt(db, 0, pSession->zDb, 
+            zName, pTab->bRowid, pTab->nCol, pTab->azCol, pTab->abPK, &pSel
         );
       }
 
@@ -2771,22 +3024,22 @@ static int sessionGenerateChangeset(
         SessionChange *p;         /* Used to iterate through changes */
 
         for(p=pTab->apChange[i]; rc==SQLITE_OK && p; p=p->pNext){
-          rc = sessionSelectBind(pSel, nCol, abPK, p);
+          rc = sessionSelectBind(pSel, pTab->nCol, pTab->abPK, p);
           if( rc!=SQLITE_OK ) continue;
           if( sqlite3_step(pSel)==SQLITE_ROW ){
             if( p->op==SQLITE_INSERT ){
               int iCol;
               sessionAppendByte(&buf, SQLITE_INSERT, &rc);
               sessionAppendByte(&buf, p->bIndirect, &rc);
-              for(iCol=0; iColnCol; iCol++){
                 sessionAppendCol(&buf, pSel, iCol, &rc);
               }
             }else{
-              assert( abPK!=0 );  /* Because sessionSelectStmt() returned ok */
-              rc = sessionAppendUpdate(&buf, bPatchset, pSel, p, abPK);
+              assert( pTab->abPK!=0 );
+              rc = sessionAppendUpdate(&buf, bPatchset, pSel, p, pTab->abPK);
             }
           }else if( p->op!=SQLITE_INSERT ){
-            rc = sessionAppendDelete(&buf, bPatchset, p, nCol, abPK);
+            rc = sessionAppendDelete(&buf, bPatchset, p, pTab->nCol,pTab->abPK);
           }
           if( rc==SQLITE_OK ){
             rc = sqlite3_reset(pSel);
@@ -2811,7 +3064,6 @@ static int sessionGenerateChangeset(
       if( buf.nBuf==nNoop ){
         buf.nBuf = nRewind;
       }
-      sqlite3_free((char*)azCol);  /* cast works around VC++ bug */
     }
   }
 
@@ -3235,15 +3487,19 @@ static int sessionReadRecord(
         }
       }
       if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){
-        sqlite3_int64 v = sessionGetI64(aVal);
-        if( eType==SQLITE_INTEGER ){
-          sqlite3VdbeMemSetInt64(apOut[i], v);
+        if( (pIn->nData-pIn->iNext)<8 ){
+          rc = SQLITE_CORRUPT_BKPT;
         }else{
-          double d;
-          memcpy(&d, &v, 8);
-          sqlite3VdbeMemSetDouble(apOut[i], d);
+          sqlite3_int64 v = sessionGetI64(aVal);
+          if( eType==SQLITE_INTEGER ){
+            sqlite3VdbeMemSetInt64(apOut[i], v);
+          }else{
+            double d;
+            memcpy(&d, &v, 8);
+            sqlite3VdbeMemSetDouble(apOut[i], d);
+          }
+          pIn->iNext += 8;
         }
-        pIn->iNext += 8;
       }
     }
   }
@@ -4936,7 +5192,7 @@ static int sessionChangesetApply(
 
         sqlite3changeset_pk(pIter, &abPK, 0);
         rc = sessionTableInfo(0, db, "main", zNew, 
-            &sApply.nCol, &zTab, &sApply.azCol, &sApply.abPK, &sApply.bRowid
+            &sApply.nCol, &zTab, &sApply.azCol, 0, &sApply.abPK, &sApply.bRowid
         );
         if( rc!=SQLITE_OK ) break;
         for(i=0; iflags & SQLITE_FkNoAction;
+
+  if( flags & SQLITE_CHANGESETAPPLY_FKNOACTION ){
+    db->flags |= ((u64)SQLITE_FkNoAction);
+    db->aDb[0].pSchema->schema_cookie -= 32;
+  }
+
   if( rc==SQLITE_OK ){
     rc = sessionChangesetApply(
         db, pIter, xFilter, xConflict, pCtx, ppRebase, pnRebase, flags
     );
   }
+
+  if( (flags & SQLITE_CHANGESETAPPLY_FKNOACTION) && savedFlag==0 ){
+    assert( db->flags & SQLITE_FkNoAction );
+    db->flags &= ~((u64)SQLITE_FkNoAction);
+    db->aDb[0].pSchema->schema_cookie -= 32;
+  }
   return rc;
 }
 
@@ -5160,6 +5429,9 @@ struct sqlite3_changegroup {
   int rc;                         /* Error code */
   int bPatch;                     /* True to accumulate patchsets */
   SessionTable *pList;            /* List of tables in current patch */
+
+  sqlite3 *db;                    /* Configured by changegroup_schema() */
+  char *zDb;                      /* Configured by changegroup_schema() */
 };
 
 /*
@@ -5180,6 +5452,7 @@ static int sessionChangeMerge(
 ){
   SessionChange *pNew = 0;
   int rc = SQLITE_OK;
+  assert( aRec!=0 );
 
   if( !pExist ){
     pNew = (SessionChange *)sqlite3_malloc64(sizeof(SessionChange) + nRec);
@@ -5345,6 +5618,114 @@ static int sessionChangeMerge(
   return rc;
 }
 
+/*
+** Check if a changeset entry with nCol columns and the PK array passed
+** as the final argument to this function is compatible with SessionTable
+** pTab. If so, return 1. Otherwise, if they are incompatible in some way,
+** return 0.
+*/
+static int sessionChangesetCheckCompat(
+  SessionTable *pTab,
+  int nCol,
+  u8 *abPK
+){
+  if( pTab->azCol && nColnCol ){
+    int ii;
+    for(ii=0; iinCol; ii++){
+      u8 bPK = (ii < nCol) ? abPK[ii] : 0;
+      if( pTab->abPK[ii]!=bPK ) return 0;
+    }
+    return 1;
+  }
+  return (pTab->nCol==nCol && 0==memcmp(abPK, pTab->abPK, nCol));
+}
+
+static int sessionChangesetExtendRecord(
+  sqlite3_changegroup *pGrp,
+  SessionTable *pTab, 
+  int nCol, 
+  int op, 
+  const u8 *aRec, 
+  int nRec, 
+  SessionBuffer *pOut
+){
+  int rc = SQLITE_OK;
+  int ii = 0;
+
+  assert( pTab->azCol );
+  assert( nColnCol );
+
+  pOut->nBuf = 0;
+  if( op==SQLITE_INSERT || (op==SQLITE_DELETE && pGrp->bPatch==0) ){
+    /* Append the missing default column values to the record. */
+    sessionAppendBlob(pOut, aRec, nRec, &rc);
+    if( rc==SQLITE_OK && pTab->pDfltStmt==0 ){
+      rc = sessionPrepareDfltStmt(pGrp->db, pTab, &pTab->pDfltStmt);
+    }
+    for(ii=nCol; rc==SQLITE_OK && iinCol; ii++){
+      int eType = sqlite3_column_type(pTab->pDfltStmt, ii);
+      sessionAppendByte(pOut, eType, &rc);
+      switch( eType ){
+        case SQLITE_FLOAT:
+        case SQLITE_INTEGER: {
+          i64 iVal;
+          if( eType==SQLITE_INTEGER ){
+            iVal = sqlite3_column_int64(pTab->pDfltStmt, ii);
+          }else{
+            double rVal = sqlite3_column_int64(pTab->pDfltStmt, ii);
+            memcpy(&iVal, &rVal, sizeof(i64));
+          }
+          if( SQLITE_OK==sessionBufferGrow(pOut, 8, &rc) ){
+            sessionPutI64(&pOut->aBuf[pOut->nBuf], iVal);
+          }
+          break;
+        }
+
+        case SQLITE_BLOB:
+        case SQLITE_TEXT: {
+          int n = sqlite3_column_bytes(pTab->pDfltStmt, ii);
+          sessionAppendVarint(pOut, n, &rc);
+          if( eType==SQLITE_TEXT ){
+            const u8 *z = (const u8*)sqlite3_column_text(pTab->pDfltStmt, ii);
+            sessionAppendBlob(pOut, z, n, &rc);
+          }else{
+            const u8 *z = (const u8*)sqlite3_column_blob(pTab->pDfltStmt, ii);
+            sessionAppendBlob(pOut, z, n, &rc);
+          }
+          break;
+        }
+
+        default:
+          assert( eType==SQLITE_NULL );
+          break;
+      }
+    }
+  }else if( op==SQLITE_UPDATE ){
+    /* Append missing "undefined" entries to the old.* record. And, if this
+    ** is an UPDATE, to the new.* record as well.  */
+    int iOff = 0;
+    if( pGrp->bPatch==0 ){
+      for(ii=0; iinCol-nCol); ii++){
+        sessionAppendByte(pOut, 0x00, &rc);
+      }
+    }
+
+    sessionAppendBlob(pOut, &aRec[iOff], nRec-iOff, &rc);
+    for(ii=0; ii<(pTab->nCol-nCol); ii++){
+      sessionAppendByte(pOut, 0x00, &rc);
+    }
+  }else{
+    assert( op==SQLITE_DELETE && pGrp->bPatch );
+    sessionAppendBlob(pOut, aRec, nRec, &rc);
+  }
+
+  return rc;
+}
+
 /*
 ** Add all changes in the changeset traversed by the iterator passed as
 ** the first argument to the changegroup hash tables.
@@ -5358,6 +5739,7 @@ static int sessionChangesetToHash(
   int nRec;
   int rc = SQLITE_OK;
   SessionTable *pTab = 0;
+  SessionBuffer rec = {0, 0, 0};
 
   while( SQLITE_ROW==sessionChangesetNext(pIter, &aRec, &nRec, 0) ){
     const char *zNew;
@@ -5369,6 +5751,9 @@ static int sessionChangesetToHash(
     SessionChange *pExist = 0;
     SessionChange **pp;
 
+    /* Ensure that only changesets, or only patchsets, but not a mixture
+    ** of both, are being combined. It is an error to try to combine a
+    ** changeset and a patchset.  */
     if( pGrp->pList==0 ){
       pGrp->bPatch = pIter->bPatchset;
     }else if( pIter->bPatchset!=pGrp->bPatch ){
@@ -5401,18 +5786,38 @@ static int sessionChangesetToHash(
         pTab->zName = (char*)&pTab->abPK[nCol];
         memcpy(pTab->zName, zNew, nNew+1);
 
+        if( pGrp->db ){
+          pTab->nCol = 0;
+          rc = sessionInitTable(0, pTab, pGrp->db, pGrp->zDb);
+          if( rc ){
+            assert( pTab->azCol==0 );
+            sqlite3_free(pTab);
+            break;
+          }
+        }
+
         /* The new object must be linked on to the end of the list, not
         ** simply added to the start of it. This is to ensure that the
         ** tables within the output of sqlite3changegroup_output() are in 
         ** the right order.  */
         for(ppTab=&pGrp->pList; *ppTab; ppTab=&(*ppTab)->pNext);
         *ppTab = pTab;
-      }else if( pTab->nCol!=nCol || memcmp(pTab->abPK, abPK, nCol) ){
+      }
+
+      if( !sessionChangesetCheckCompat(pTab, nCol, abPK) ){
         rc = SQLITE_SCHEMA;
         break;
       }
     }
 
+    if( nColnCol ){
+      assert( pGrp->db );
+      rc = sessionChangesetExtendRecord(pGrp, pTab, nCol, op, aRec, nRec, &rec);
+      if( rc ) break;
+      aRec = rec.aBuf;
+      nRec = rec.nBuf;
+    }
+
     if( sessionGrowHash(0, pIter->bPatchset, pTab) ){
       rc = SQLITE_NOMEM;
       break;
@@ -5450,6 +5855,7 @@ static int sessionChangesetToHash(
     }
   }
 
+  sqlite3_free(rec.aBuf);
   if( rc==SQLITE_OK ) rc = pIter->rc;
   return rc;
 }
@@ -5536,6 +5942,31 @@ int sqlite3changegroup_new(sqlite3_changegroup **pp){
   return rc;
 }
 
+/*
+** Provide a database schema to the changegroup object.
+*/
+int sqlite3changegroup_schema(
+  sqlite3_changegroup *pGrp, 
+  sqlite3 *db, 
+  const char *zDb
+){
+  int rc = SQLITE_OK;
+
+  if( pGrp->pList || pGrp->db ){
+    /* Cannot add a schema after one or more calls to sqlite3changegroup_add(),
+    ** or after sqlite3changegroup_schema() has already been called. */
+    rc = SQLITE_MISUSE;
+  }else{
+    pGrp->zDb = sqlite3_mprintf("%s", zDb);
+    if( pGrp->zDb==0 ){
+      rc = SQLITE_NOMEM;
+    }else{
+      pGrp->db = db;
+    }
+  }
+  return rc;
+}
+
 /*
 ** Add the changeset currently stored in buffer pData, size nData bytes,
 ** to changeset-group p.
@@ -5599,6 +6030,7 @@ int sqlite3changegroup_output_strm(
 */
 void sqlite3changegroup_delete(sqlite3_changegroup *pGrp){
   if( pGrp ){
+    sqlite3_free(pGrp->zDb);
     sessionDeleteTable(0, pGrp->pList);
     sqlite3_free(pGrp);
   }
diff --git a/sqlite/ext/session/sqlite3session.h b/sqlite/ext/session/sqlite3session.h
index 1ea90dce..160ea878 100644
--- a/sqlite/ext/session/sqlite3session.h
+++ b/sqlite/ext/session/sqlite3session.h
@@ -884,6 +884,18 @@ int sqlite3changeset_concat(
 );
 
 
+/*
+** CAPI3REF: Upgrade the Schema of a Changeset/Patchset
+*/
+int sqlite3changeset_upgrade(
+  sqlite3 *db,
+  const char *zDb,
+  int nIn, const void *pIn,       /* Input changeset */
+  int *pnOut, void **ppOut        /* OUT: Inverse of input */
+);
+
+
+
 /*
 ** CAPI3REF: Changegroup Handle
 **
@@ -930,6 +942,38 @@ typedef struct sqlite3_changegroup sqlite3_changegroup;
 */
 int sqlite3changegroup_new(sqlite3_changegroup **pp);
 
+/*
+** CAPI3REF: Add a Schema to a Changegroup
+** METHOD: sqlite3_changegroup_schema
+**
+** This method may be used to optionally enforce the rule that the changesets
+** added to the changegroup handle must match the schema of database zDb
+** ("main", "temp", or the name of an attached database). If
+** sqlite3changegroup_add() is called to add a changeset that is not compatible
+** with the configured schema, SQLITE_SCHEMA is returned and the changegroup
+** object is left in an undefined state.
+**
+** A changeset schema is considered compatible with the database schema in
+** the same way as for sqlite3changeset_apply(). Specifically, for each
+** table in the changeset, there exists a database table with:
+**
+** 
+**   -  The name identified by the changeset, and
+**   
-  at least as many columns as recorded in the changeset, and
+**   
-  the primary key columns in the same position as recorded in 
+**        the changeset.
+** 
+**
+** The output of the changegroup object always has the same schema as the
+** database nominated using this function. In cases where changesets passed
+** to sqlite3changegroup_add() have fewer columns than the corresponding table
+** in the database schema, these are filled in using the default column
+** values from the database schema. This makes it possible to combined 
+** changesets that have different numbers of columns for a single table
+** within a changegroup, provided that they are otherwise compatible.
+*/
+int sqlite3changegroup_schema(sqlite3_changegroup*, sqlite3*, const char *zDb);
+
 /*
 ** CAPI3REF: Add A Changeset To A Changegroup
 ** METHOD: sqlite3_changegroup
@@ -998,13 +1042,18 @@ int sqlite3changegroup_new(sqlite3_changegroup **pp);
 ** If the new changeset contains changes to a table that is already present
 ** in the changegroup, then the number of columns and the position of the
 ** primary key columns for the table must be consistent. If this is not the
-** case, this function fails with SQLITE_SCHEMA. If the input changeset
-** appears to be corrupt and the corruption is detected, SQLITE_CORRUPT is
-** returned. Or, if an out-of-memory condition occurs during processing, this
-** function returns SQLITE_NOMEM. In all cases, if an error occurs the state
-** of the final contents of the changegroup is undefined.
-**
-** If no error occurs, SQLITE_OK is returned.
+** case, this function fails with SQLITE_SCHEMA. Except, if the changegroup
+** object has been configured with a database schema using the
+** sqlite3changegroup_schema() API, then it is possible to combine changesets
+** with different numbers of columns for a single table, provided that
+** they are otherwise compatible.
+**
+** If the input changeset appears to be corrupt and the corruption is
+** detected, SQLITE_CORRUPT is returned. Or, if an out-of-memory condition
+** occurs during processing, this function returns SQLITE_NOMEM. 
+**
+** In all cases, if an error occurs the state of the final contents of the
+** changegroup is undefined. If no error occurs, SQLITE_OK is returned.
 */
 int sqlite3changegroup_add(sqlite3_changegroup*, int nData, void *pData);
 
@@ -1269,10 +1318,17 @@ int sqlite3changeset_apply_v2(
 **    an insert change if all fields of the conflicting row match
 **        the row being inserted.
 **    
+**
+** SQLITE_CHANGESETAPPLY_FKNOACTION 
+**   If this flag it set, then all foreign key constraints in the target
+**   database behave as if they were declared with "ON UPDATE NO ACTION ON
+**   DELETE NO ACTION", even if they are actually CASCADE, RESTRICT, SET NULL
+**   or SET DEFAULT.
 */
 #define SQLITE_CHANGESETAPPLY_NOSAVEPOINT   0x0001
 #define SQLITE_CHANGESETAPPLY_INVERT        0x0002
 #define SQLITE_CHANGESETAPPLY_IGNORENOOP    0x0004
+#define SQLITE_CHANGESETAPPLY_FKNOACTION    0x0008
 
 /* 
 ** CAPI3REF: Constants Passed To The Conflict Handler
diff --git a/sqlite/ext/session/test_session.c b/sqlite/ext/session/test_session.c
index 0836238b..af42351b 100644
--- a/sqlite/ext/session/test_session.c
+++ b/sqlite/ext/session/test_session.c
@@ -392,7 +392,6 @@ static int SQLITE_TCLAPI test_session_cmd(
       };
       size_t sz = sizeof(aOpt[0]);
 
-      int rc;
       int iArg;
       int iOpt;
       if( Tcl_GetIndexFromObjStruct(interp,objv[2],aOpt,sz,"option",0,&iOpt) ){
@@ -812,9 +811,12 @@ static int SQLITE_TCLAPI testSqlite3changesetApply(
     while( objc>1 ){
       const char *z1 = Tcl_GetString(objv[1]);
       int n = strlen(z1);
-      if( n>1 && n<=12 && 0==sqlite3_strnicmp("-nosavepoint", z1, n) ){
+      if( n>3 && n<=12 && 0==sqlite3_strnicmp("-nosavepoint", z1, n) ){
         flags |= SQLITE_CHANGESETAPPLY_NOSAVEPOINT;
       }
+      else if( n>3 && n<=9 && 0==sqlite3_strnicmp("-noaction", z1, n) ){
+        flags |= SQLITE_CHANGESETAPPLY_FKNOACTION;
+      }
       else if( n>2 && n<=7 && 0==sqlite3_strnicmp("-invert", z1, n) ){
         flags |= SQLITE_CHANGESETAPPLY_INVERT;
       }
@@ -1452,12 +1454,144 @@ static int SQLITE_TCLAPI test_sqlite3session_config(
   return TCL_OK;
 }
 
+typedef struct TestChangegroup TestChangegroup;
+struct TestChangegroup {
+  sqlite3_changegroup *pGrp;
+};
+
+/*
+** Destructor for Tcl changegroup command object.
+*/
+static void test_changegroup_del(void *clientData){
+  TestChangegroup *pGrp = (TestChangegroup*)clientData;
+  sqlite3changegroup_delete(pGrp->pGrp);
+  ckfree(pGrp);
+}
+
+/*
+** Tclcmd:  $changegroup schema DB DBNAME
+** Tclcmd:  $changegroup add CHANGESET
+** Tclcmd:  $changegroup output
+** Tclcmd:  $changegroup delete
+*/
+static int SQLITE_TCLAPI test_changegroup_cmd(
+  void * clientData,
+  Tcl_Interp *interp,
+  int objc,
+  Tcl_Obj *CONST objv[]
+){
+  TestChangegroup *p = (TestChangegroup*)clientData;
+  static struct ChangegroupCmd {
+    const char *zSub;
+    int nArg;
+    const char *zMsg;
+    int iSub;
+  } aSub[] = {
+    { "schema",       2, "DB DBNAME",  }, /* 0 */
+    { "add",          1, "CHANGESET",  }, /* 1 */
+    { "output",       0, "",           }, /* 2 */
+    { "delete",       0, "",           }, /* 3 */
+    { 0 }
+  };
+  int rc = TCL_OK;
+  int iSub = 0;
+
+  if( objc<2 ){
+    Tcl_WrongNumArgs(interp, 1, objv, "SUBCOMMAND ...");
+    return TCL_ERROR;
+  }
+  rc = Tcl_GetIndexFromObjStruct(interp, 
+      objv[1], aSub, sizeof(aSub[0]), "sub-command", 0, &iSub
+  );
+  if( rc!=TCL_OK ) return rc;
+  if( objc!=2+aSub[iSub].nArg ){
+    Tcl_WrongNumArgs(interp, 2, objv, aSub[iSub].zMsg);
+    return TCL_ERROR;
+  }
+
+  switch( iSub ){
+    case 0: {      /* schema */
+      sqlite3 *db = 0;
+      const char *zDb = Tcl_GetString(objv[3]);
+      if( dbHandleFromObj(interp, objv[2], &db) ){
+        return TCL_ERROR;
+      }
+      rc = sqlite3changegroup_schema(p->pGrp, db, zDb);
+      if( rc!=SQLITE_OK ) rc = test_session_error(interp, rc, 0);
+      break;
+    };
+
+    case 1: {      /* add */
+      int nByte = 0;
+      const u8 *aByte = Tcl_GetByteArrayFromObj(objv[2], &nByte);
+      rc = sqlite3changegroup_add(p->pGrp, nByte, (void*)aByte);
+      if( rc!=SQLITE_OK ) rc = test_session_error(interp, rc, 0);
+      break;
+    };
+
+    case 2: {      /* output */
+      int nByte = 0;
+      u8 *aByte = 0;
+      rc = sqlite3changegroup_output(p->pGrp, &nByte, (void**)&aByte);
+      if( rc!=SQLITE_OK ){
+        rc = test_session_error(interp, rc, 0);
+      }else{
+        Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(aByte, nByte));
+      }
+      sqlite3_free(aByte);
+      break;
+    };
+
+    default: {     /* delete */
+      assert( iSub==3 );
+      Tcl_DeleteCommand(interp, Tcl_GetString(objv[0]));
+      break;
+    }
+  }
+
+  return rc;
+}
+
+/*
+** Tclcmd:  sqlite3changegroup CMD
+*/
+static int SQLITE_TCLAPI test_sqlite3changegroup(
+  void * clientData,
+  Tcl_Interp *interp,
+  int objc,
+  Tcl_Obj *CONST objv[]
+){
+  int rc;                         /* sqlite3changegroup_new() return code */
+  TestChangegroup *p;             /* New wrapper object */
+
+  if( objc!=2 ){
+    Tcl_WrongNumArgs(interp, 1, objv, "CMD");
+    return TCL_ERROR;
+  }
+
+  p = (TestChangegroup*)ckalloc(sizeof(TestChangegroup));
+  memset(p, 0, sizeof(TestChangegroup));
+  rc = sqlite3changegroup_new(&p->pGrp);
+  if( rc!=SQLITE_OK ){
+    ckfree((char*)p);
+    return test_session_error(interp, rc, 0);
+  }
+
+  Tcl_CreateObjCommand(
+      interp, Tcl_GetString(objv[1]), test_changegroup_cmd, (ClientData)p,
+      test_changegroup_del
+  );
+  Tcl_SetObjResult(interp, objv[1]);
+  return TCL_OK;
+}
+
 int TestSession_Init(Tcl_Interp *interp){
   struct Cmd {
     const char *zCmd;
     Tcl_ObjCmdProc *xProc;
   } aCmd[] = {
     { "sqlite3session", test_sqlite3session },
+    { "sqlite3changegroup", test_sqlite3changegroup },
     { "sqlite3session_foreach", test_sqlite3session_foreach },
     { "sqlite3changeset_invert", test_sqlite3changeset_invert },
     { "sqlite3changeset_concat", test_sqlite3changeset_concat },
diff --git a/sqlite/ext/wasm/GNUmakefile b/sqlite/ext/wasm/GNUmakefile
index a99513bf..c0cab212 100644
--- a/sqlite/ext/wasm/GNUmakefile
+++ b/sqlite/ext/wasm/GNUmakefile
@@ -18,8 +18,6 @@
 #  quick, q = do just a minimal build (sqlite3.js/wasm, tester1) for
 #      faster development-mode turnaround.
 #
-#  qo2, qoz = a combination of quick+o2/oz.
-#
 #  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.
@@ -46,11 +44,12 @@
 #    $(eval), or at least centralize the setup of the numerous vars
 #    related to each build variant $(JS_BUILD_MODES).
 #
+default: all
+#default: quick
 SHELL := $(shell which bash 2>/dev/null)
 MAKEFILE := $(lastword $(MAKEFILE_LIST))
 CLEAN_FILES :=
 DISTCLEAN_FILES := ./--dummy--
-default: all
 release: oz
 # JS_BUILD_MODES exists solely to reduce repetition in documentation
 # below.
@@ -68,13 +67,6 @@ ifeq (,$(emcc.version))
 else
   $(info using emcc version [$(emcc.version)])
 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)))
@@ -159,6 +151,9 @@ endif
 # bundle.
 #
 # 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
@@ -184,14 +179,19 @@ SQLITE_OPT = \
   -DSQLITE_OMIT_DEPRECATED \
   -DSQLITE_OMIT_UTF16 \
   -DSQLITE_OMIT_SHARED_CACHE \
-  -DSQLITE_OMIT_WAL \
   -DSQLITE_THREADSAFE=0 \
-  -DSQLITE_TEMP_STORE=3 \
+  -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.
 
 .NOTPARALLEL: $(sqlite3.h)
 $(sqlite3.h):
@@ -246,10 +246,10 @@ endif
 # 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.wasm)/version-info
-$(bin.version-info): $(dir.wasm)/version-info.c $(sqlite3.h) $(MAKEFILE)
-	$(CC) -O0 -I$(dir $(sqlite3.c)) -o $@ $<
-DISTCLEAN_FILES += $(bin.version-info)
+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
@@ -379,20 +379,17 @@ sqlite3-api.jses += $(dir.api)/sqlite3-api-oo1.js
 sqlite3-api.jses += $(dir.api)/sqlite3-api-worker1.js
 sqlite3-api.jses += $(dir.api)/sqlite3-v-helper.js
 sqlite3-api.jses += $(dir.api)/sqlite3-vfs-opfs.c-pp.js
+sqlite3-api.jses += $(dir.api)/sqlite3-vfs-opfs-sahpool.c-pp.js
 sqlite3-api.jses += $(dir.api)/sqlite3-api-cleanup.js
 
-# "External" API files which are part of our distribution
+# SOAP.js is an external API file which is part of our distribution
 # but not part of the sqlite3-api.js amalgamation.
 SOAP.js := $(dir.api)/sqlite3-opfs-async-proxy.js
-# COPY_XAPI = a $(call)able function to copy $1 to $(dir.dout), where
-# $1 must be one of the "external" JS API files.
-define COPY_XAPI
-sqlite3-api.ext.jses += $$(dir.dout)/$$(notdir $(1))
-$$(dir.dout)/$$(notdir $(1)): $(1) $$(MAKEFILE)
-	cp $$< $$@
-endef
-$(foreach X,$(SOAP.js),\
-  $(eval $(call COPY_XAPI,$(X))))
+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
 
@@ -615,26 +612,40 @@ $(post-js.js.in): $(post-jses.js) $(MAKEFILE)
 
 ########################################################################
 # call-make-pre-post is a $(call)able which creates rules for
-# pre-js-$(1).js. $1 = the base name of the JS file on whose behalf
-# this pre-js is for (one of: sqlite3, sqlite3-wasmfs). $2 is the
-# build mode: one of $(JS_BUILD_MODES).  This
-# sets up --[extern-][pre/post]-js flags in
-# $(pre-post-$(1).flags.$(2)) and dependencies in
-# $(pre-post-$(1).deps.$(2)).
+# pre-js-$(1)-$(2).js. $1 = the base name of the JS file on whose
+# behalf this pre-js is for (one of: sqlite3, sqlite3-wasmfs). $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.
 define call-make-pre-post
-pre-post-$(1).flags.$(2) ?=
-$$(dir.tmp)/pre-js-$(1)-$(2).js: $$(pre-js.js.$(2)) $$(MAKEFILE)
-	cp $$(pre-js.js.$(2)) $$@
+pre-post-$(1)-$(2).flags ?=
+pre-js.js.$(1)-$(2) := $$(dir.tmp)/pre-js.$(1)-$(2).intermediary.js
+$$(eval $$(call C-PP.FILTER,$$(pre-js.js.in),$$(pre-js.js.$(1)-$(2)),$$(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))
+$$(dir.tmp)/pre-js-$(1)-$(2).js: $$(pre-js.js.$(1)-$(2)) $$(MAKEFILE)
+	cp $$(pre-js.js.$(1)-$(2)) $$@
 	@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).deps.$(2) := \
-  $$(pre-post-jses.deps.$(2)) \
+pre-post-$(1)-$(2).deps := \
+  $$(pre-post-jses.$(1)-$(2).deps) \
   $$(dir.tmp)/pre-js-$(1)-$(2).js
-pre-post-$(1).flags.$(2) += \
-  $$(pre-post-common.flags.$(2)) \
+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
@@ -645,7 +656,8 @@ endef
 # https://github.com/emscripten-core/emscripten/issues/14383
 sqlite3.wasm := $(dir.dout)/sqlite3.wasm
 sqlite3-wasm.c := $(dir.api)/sqlite3-wasm.c
-sqlite3-wasm.cses := $(sqlite3-wasm.c) $(sqlite3_wasm_extra_init.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
@@ -655,7 +667,8 @@ sqlite3-wasm.cses := $(sqlite3-wasm.c) $(sqlite3_wasm_extra_init.c)
 # 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) and 0 if not (vanilla).
+# 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:
@@ -669,13 +682,20 @@ sqlite3-wasm.cses := $(sqlite3-wasm.c) $(sqlite3_wasm_extra_init.c)
 #
 # Upstream RFE:
 # https://github.com/emscripten-core/emscripten/issues/18237
+#
+# Maintenance reminder: Mac sed works differently than GNU sed, so
+# don't use sed for this.
 define SQLITE3.xJS.ESM-EXPORT-DEFAULT
 if [ x1 = x$(1) ]; then \
-		echo "Fragile workaround for an Emscripten annoyance. See SQLITE3.xJS.RECIPE."; \
-		sed -i -e '0,/^export default/{/^export default/d;}' $@ || exit $$?; \
-		if ! grep -q '^export default' $@; then \
-			echo "Cannot find export default." 1>&2; \
-			exit 1; \
+		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
@@ -695,53 +715,57 @@ 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 = build mode name: one of $(JS_BUILD_MODES)
-# $2 = 1 for ESM build mode, else 0
-# $3 = resulting sqlite-api JS/MJS file
-# $4 = resulting JS/MJS file
-# $5 = -D... flags for $(bin.c-pp)
-# $6 = emcc -sXYZ flags (CURRENTLY UNUSED - was factored out)
+# $1 = one of: sqlite3, sqlite3-wasmfs
+# $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.$(1) must be set to a value for the -sENVIRONMENT flag.
+# emcc.environment.$(2) must be set to a value for the -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)]: $(4))
-c-pp.D.$(1) := $(5)
-pre-js.js.$(1) := $$(dir.tmp)/pre-js.$(1).js
-$$(eval $$(call C-PP.FILTER,$$(pre-js.js.in),$$(pre-js.js.$(1)),$$(c-pp.D.$(1))))
-post-js.js.$(1) := $$(dir.tmp)/post-js.$(1).js
-$$(eval $$(call C-PP.FILTER,$$(post-js.js.in),$$(post-js.js.$(1)),$$(c-pp.D.$(1))))
-extern-post-js.js.$(1) := $$(dir.tmp)/extern-post-js.$(1).js
-$$(eval $$(call C-PP.FILTER,$$(extern-post-js.js.in),$$(extern-post-js.js.$(1)),$$(c-pp.D.$(1))))
-pre-post-common.flags.$(1) := \
-  $$(pre-post-common.flags) \
-  --post-js=$$(post-js.js.$(1)) \
-  --extern-post-js=$$(extern-post-js.js.$(1))
-pre-post-jses.deps.$(1) := $$(pre-post-jses.deps.common) \
-  $$(post-js.js.$(1)) $$(extern-post-js.js.$(1))
-$$(eval $$(call call-make-pre-post,sqlite3,$(1)))
-emcc.flags.sqlite3.$(1) := $(6)
-$$(eval $$(call C-PP.FILTER, $$(sqlite3-api.js.in), $(3), $(5)))
-$(4): $(3) $$(MAKEFILE) $$(sqlite3-wasm.cses) $$(EXPORTED_FUNCTIONS.api) $$(pre-post-sqlite3.deps.$(1))
+$(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.$(1)) \
-    $$(pre-post-sqlite3.flags.$(1)) $$(emcc.flags.sqlite3.$(1)) \
-    $$(cflags.common) $$(SQLITE_OPT) $$(cflags.wasm_extra_init) $$(sqlite3-wasm.cses)
-	@$$(call SQLITE3.xJS.ESM-EXPORT-DEFAULT,$(2))
-	@case $(1) in \
+    -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 $(3) for sqlite3.wasm..."; \
-      rm -f $$(dir.dout)/sqlite3-$(1).wasm; \
-      sed -i -e 's/sqlite3-$(1).wasm/sqlite3.wasm/g' $$@ || exit $$$$?; \
+      echo "Patching $$@ for $(1).wasm..."; \
+      rm -f $$$$dotwasm; \
+      dotwasm=; \
+      sed -i -e 's/$(1)-$(2).wasm/$(1).wasm/g' $$@ || exit $$$$?; \
       ;; \
-	esac
-	chmod -x $$(sqlite3.wasm)
-	$$(maybe-wasm-strip) $$(sqlite3.wasm)
-	@ls -la $@ $$(sqlite3.wasm)
-all: $(4)
-quick: $(4)
-CLEAN_FILES += $(3) $(4)
+	esac; \
+	ls -la $$$$dotwasm $$@
+all: $(5)
+#quick: $(5)
+CLEAN_FILES += $(4) $(5)
 endef
 # ^^^ /SETUP_LIB_BUILD_MODE
 ########################################################################
@@ -753,21 +777,24 @@ 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
-# Maintenance reminder: careful not to introduce spaces around args $1, $2
-#$(info $(call SETUP_LIB_BUILD_MODE,vanilla,0, $(sqlite3-api.js), $(sqlite3.js)))
-$(eval $(call SETUP_LIB_BUILD_MODE,vanilla,0, $(sqlite3-api.js), $(sqlite3.js)))
-$(eval $(call SETUP_LIB_BUILD_MODE,esm,1, $(sqlite3-api.mjs), $(sqlite3.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, -sEXPORT_ES6 -sUSE_ES6_IMPORT_META))
-$(eval $(call SETUP_LIB_BUILD_MODE,bundler-friendly,1,\
+$(eval $(call SETUP_LIB_BUILD_MODE,sqlite3,bundler-friendly,1,\
        $(sqlite3-api-bundler-friendly.mjs),$(sqlite3-bundler-friendly.mjs),\
-       $(c-pp.D.esm) -Dtarget=es6-bundler-friendly))
-$(eval $(call SETUP_LIB_BUILD_MODE,node,1,\
+       $(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.bundler-friendly) -Dtarget=node))
+       $(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
@@ -811,11 +838,11 @@ sqlite3-worker1-bundler-friendly.js := $(dir.dout)/sqlite3-worker1-bundler-frien
 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.bundler-friendly)))
+    $(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.bundler-friendly)))
+    $(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)
@@ -841,7 +868,7 @@ clean-batch:
 # 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
+#all: batch
 # end batch-runner.js
 ########################################################################
 # Wasmified speedtest1 is our primary benchmarking tool.
@@ -849,7 +876,7 @@ all: batch
 # 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 :=
+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))
@@ -892,22 +919,24 @@ $(EXPORTED_FUNCTIONS.speedtest1): $(EXPORTED_FUNCTIONS.api.main)
 	@{ echo _wasm_main; cat $(EXPORTED_FUNCTIONS.api.main); } > $@
 speedtest1.js := $(dir.dout)/speedtest1.js
 speedtest1.wasm := $(dir.dout)/speedtest1.wasm
-cflags.speedtest1 := $(cflags.common) -DSQLITE_SPEEDTEST1_WASM
-speedtest1.cses := $(speedtest1.c) $(sqlite3-wasm.c)
+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.cses) \
-    $(pre-post-speedtest1.deps.vanilla) \
+$(speedtest1.js): $(MAKEFILE) $(speedtest1.cfiles) \
+    $(pre-post-speedtest1-vanilla.deps) \
     $(EXPORTED_FUNCTIONS.speedtest1)
 	@echo "Building $@ ..."
 	$(emcc.bin) \
-        $(emcc.speedtest1) -I$(dir $(sqlite3.canonical.c)) \
+        $(emcc.speedtest1) \
         $(emcc.speedtest1.common) \
-        $(cflags.speedtest1) $(pre-post-speedtest1.flags.vanilla) \
+        $(emcc.flags.speedtest1-vanilla) $(pre-post-speedtest1-vanilla.flags) \
         $(SQLITE_OPT) \
         -USQLITE_C -DSQLITE_C=$(sqlite3.canonical.c) \
         $(speedtest1.exit-runtime0) \
-        -o $@ $(speedtest1.cses) -lm
+        -o $@ $(speedtest1.cfiles) -lm
 	$(maybe-wasm-strip) $(speedtest1.wasm)
+	chmod -x $(speedtest1.wasm)
 	ls -la $@ $(speedtest1.wasm)
 
 speedtest1: $(speedtest1.js)
@@ -933,13 +962,14 @@ CLEAN_FILES += $(speedtest1.js) $(speedtest1.wasm)
 #
 # 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.esm)))
+$(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.esm)))
+$(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
@@ -959,8 +989,6 @@ o1: clean
 	$(MAKE) -e "emcc_opt=-O1 $(o-xtra)"
 o2: clean
 	$(MAKE) -j2 -e "emcc_opt=-O2 $(o-xtra)"
-qo2: clean
-	$(MAKE) -j2 -e "emcc_opt=-O2 $(o-xtra)" quick
 o3: clean
 	$(MAKE) -e "emcc_opt=-O3 $(o-xtra)"
 os: clean
@@ -968,8 +996,6 @@ os: clean
 	$(MAKE) -e "emcc_opt=-Os $(o-xtra)"
 oz: clean
 	$(MAKE) -j2 -e "emcc_opt=-Oz $(o-xtra)"
-qoz: clean
-	$(MAKE) -j2 -e "emcc_opt=-Oz $(o-xtra)" quick
 
 ########################################################################
 # Sub-makes...
@@ -981,6 +1007,8 @@ include fiddle.make
 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))
@@ -1011,8 +1039,7 @@ endif
 # Push files to public wasm-testing.sqlite.org server
 wasm-testing.include = *.js *.mjs *.html \
   ./tests \
-  batch-runner.list \
-  $(dir.dout) $(dir.sql) $(dir.common) $(dir.fiddle) $(dir.jacc)
+  $(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)
diff --git a/sqlite/ext/wasm/README.md b/sqlite/ext/wasm/README.md
index e8d66865..0c328310 100644
--- a/sqlite/ext/wasm/README.md
+++ b/sqlite/ext/wasm/README.md
@@ -82,7 +82,7 @@ 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 2022-08-18*
+*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
@@ -93,13 +93,14 @@ features in the apps which use them.
   *  Local:  `ssh -L 8180:localhost:8080 remote`
   *  Local:  Point your web-browser at http://localhost:8180/index.html
 
-In order to enable [SharedArrayBuffers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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".
-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.
+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/sqlite/ext/wasm/SQLTester/GNUmakefile b/sqlite/ext/wasm/SQLTester/GNUmakefile
new file mode 100644
index 00000000..8fa12471
--- /dev/null
+++ b/sqlite/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/sqlite/ext/wasm/SQLTester/SQLTester.mjs b/sqlite/ext/wasm/SQLTester/SQLTester.mjs
new file mode 100644
index 00000000..a72399ae
--- /dev/null
+++ b/sqlite/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 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('', ts);
+    }else if('string' === typeof arguments[1]){
+      ts = new TestScript('', 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=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 || '')+"]";
+    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/sqlite/ext/wasm/SQLTester/SQLTester.run.mjs b/sqlite/ext/wasm/SQLTester/SQLTester.run.mjs
new file mode 100644
index 00000000..735fe4dc
--- /dev/null
+++ b/sqlite/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/sqlite/ext/wasm/SQLTester/index.html b/sqlite/ext/wasm/SQLTester/index.html
new file mode 100644
index 00000000..1dffad63
--- /dev/null
+++ b/sqlite/ext/wasm/SQLTester/index.html
@@ -0,0 +1,127 @@
+
+
+  
+