diff --git a/Makefile.in b/Makefile.in index e0d764c5c6..38dd723940 100644 --- a/Makefile.in +++ b/Makefile.in @@ -447,6 +447,7 @@ TESTSRC += \ $(TOP)/ext/expert/test_expert.c \ $(TOP)/ext/misc/amatch.c \ $(TOP)/ext/misc/appendvfs.c \ + $(TOP)/ext/misc/basexx.c \ $(TOP)/ext/misc/carray.c \ $(TOP)/ext/misc/cksumvfs.c \ $(TOP)/ext/misc/closure.c \ @@ -780,6 +781,15 @@ sqlite3.c: .target_source $(TOP)/tool/mksqlite3c.tcl cp tsrc/sqlite3ext.h . cp $(TOP)/ext/session/sqlite3session.h . +sqlite3r.h: sqlite3.h + $(TCLSH_CMD) $(TOP)/tool/mksqlite3h.tcl $(TOP) --enable-recover >sqlite3r.h + +sqlite3r.c: sqlite3.c sqlite3r.h + cp $(TOP)/ext/recover/sqlite3recover.c tsrc/ + cp $(TOP)/ext/recover/sqlite3recover.h tsrc/ + cp $(TOP)/ext/recover/dbdata.c tsrc/ + $(TCLSH_CMD) $(TOP)/tool/mksqlite3c.tcl --enable-recover $(AMALGAMATION_LINE_MACROS) + sqlite3ext.h: .target_source cp tsrc/sqlite3ext.h . diff --git a/Makefile.msc b/Makefile.msc index 191701cdba..0312b0dbaf 100644 --- a/Makefile.msc +++ b/Makefile.msc @@ -1561,6 +1561,7 @@ TESTEXT = \ $(TOP)\ext\expert\test_expert.c \ $(TOP)\ext\misc\amatch.c \ $(TOP)\ext\misc\appendvfs.c \ + $(TOP)\ext\misc\basexx.c \ $(TOP)\ext\misc\carray.c \ $(TOP)\ext\misc\cksumvfs.c \ $(TOP)\ext\misc\closure.c \ diff --git a/VERSION b/VERSION index 7e16c94210..371986f6df 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.40.0 +3.41.0 diff --git a/configure b/configure index 4a32d5e561..75ca8935ff 100755 --- a/configure +++ b/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.40.0. +# Generated by GNU Autoconf 2.69 for sqlite 3.41.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.40.0' -PACKAGE_STRING='sqlite 3.40.0' +PACKAGE_VERSION='3.41.0' +PACKAGE_STRING='sqlite 3.41.0' PACKAGE_BUGREPORT='' PACKAGE_URL='' @@ -1468,7 +1468,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.40.0 to adapt to many kinds of systems. +\`configure' configures sqlite 3.41.0 to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... @@ -1533,7 +1533,7 @@ fi if test -n "$ac_init_help"; then case $ac_init_help in - short | recursive ) echo "Configuration of sqlite 3.40.0:";; + short | recursive ) echo "Configuration of sqlite 3.41.0:";; esac cat <<\_ACEOF @@ -1661,7 +1661,7 @@ fi test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF -sqlite configure 3.40.0 +sqlite configure 3.41.0 generated by GNU Autoconf 2.69 Copyright (C) 2012 Free Software Foundation, Inc. @@ -2080,7 +2080,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.40.0, which was +It was created by sqlite $as_me 3.41.0, which was generated by GNU Autoconf 2.69. Invocation command line was $ $0 $@ @@ -12390,7 +12390,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.40.0, which was +This file was extended by sqlite $as_me 3.41.0, which was generated by GNU Autoconf 2.69. Invocation command line was CONFIG_FILES = $CONFIG_FILES @@ -12456,7 +12456,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.40.0 +sqlite config.status 3.41.0 configured by $0, generated by GNU Autoconf 2.69, with options \\"\$ac_cs_config\\" diff --git a/ext/fts5/fts5_main.c b/ext/fts5/fts5_main.c index 9a8b2fadb4..d4b9fa85c4 100644 --- a/ext/fts5/fts5_main.c +++ b/ext/fts5/fts5_main.c @@ -260,7 +260,7 @@ static void fts5CheckTransactionState(Fts5FullTable *p, int op, int iSavepoint){ break; case FTS5_SYNC: - assert( p->ts.eState==1 ); + assert( p->ts.eState==1 || p->ts.eState==2 ); p->ts.eState = 2; break; @@ -275,21 +275,21 @@ static void fts5CheckTransactionState(Fts5FullTable *p, int op, int iSavepoint){ break; case FTS5_SAVEPOINT: - assert( p->ts.eState==1 ); + assert( p->ts.eState>=1 ); assert( iSavepoint>=0 ); assert( iSavepoint>=p->ts.iSavepoint ); p->ts.iSavepoint = iSavepoint; break; case FTS5_RELEASE: - assert( p->ts.eState==1 ); + assert( p->ts.eState>=1 ); assert( iSavepoint>=0 ); assert( iSavepoint<=p->ts.iSavepoint ); p->ts.iSavepoint = iSavepoint-1; break; case FTS5_ROLLBACKTO: - assert( p->ts.eState==1 ); + assert( p->ts.eState>=1 ); assert( iSavepoint>=-1 ); /* The following assert() can fail if another vtab strikes an error ** within an xSavepoint() call then SQLite calls xRollbackTo() - without diff --git a/ext/fts5/test/fts5misc.test b/ext/fts5/test/fts5misc.test index e354d20e2c..4b9ca2dbb2 100644 --- a/ext/fts5/test/fts5misc.test +++ b/ext/fts5/test/fts5misc.test @@ -352,5 +352,55 @@ do_test 13.3 { sqlite3_errmsg db } {not an error} +#------------------------------------------------------------------------- +reset_db +db close +sqlite3 db test.db -uri 1 + +do_execsql_test 14.0 { + PRAGMA locking_mode=EXCLUSIVE; + BEGIN; + ATTACH 'file:/one?vfs=memdb' AS aux1; + ATTACH 'file:/one?vfs=memdb' AS aux2; + CREATE VIRTUAL TABLE t1 USING fts5(x); +} {exclusive} +do_catchsql_test 14.1 { + ANALYZE; +} {1 {database is locked}} +do_catchsql_test 14.2 { + COMMIT; +} {1 {database is locked}} +do_catchsql_test 14.3 { + COMMIT; +} {1 {database is locked}} +do_catchsql_test 14.4 { + ROLLBACK; +} {0 {}} + +#------------------------------------------------------------------------- +reset_db +sqlite3 db2 test.db + +do_execsql_test 15.0 { + CREATE TABLE t1(a, b); + BEGIN; + SELECT * FROM t1; +} + +do_execsql_test -db db2 15.1 { + BEGIN; + CREATE VIRTUAL TABLE x1 USING fts5(y); +} +do_test 15.2 { + list [catch { db2 eval COMMIT } msg] $msg +} {1 {database is locked}} +do_execsql_test -db db2 15.3 { + SAVEPOINT one; +} {} +do_execsql_test 15.4 END +do_test 15.4 { + list [catch { db2 eval COMMIT } msg] $msg +} {0 {}} + finish_test diff --git a/ext/misc/base64.c b/ext/misc/base64.c new file mode 100755 index 0000000000..81767379b1 --- /dev/null +++ b/ext/misc/base64.c @@ -0,0 +1,270 @@ +/* +** 2022-11-18 +** +** The author disclaims copyright to this source code. In place of +** a legal notice, here is a blessing: +** +** May you do good and not evil. +** May you find forgiveness for yourself and forgive others. +** May you share freely, never taking more than you give. +** +************************************************************************* +** +** This is a SQLite extension for converting in either direction +** between a (binary) blob and base64 text. Base64 can transit a +** sane USASCII channel unmolested. It also plays nicely in CSV or +** written as TCL brace-enclosed literals or SQL string literals, +** and can be used unmodified in XML-like documents. +** +** This is an independent implementation of conversions specified in +** RFC 4648, done on the above date by the author (Larry Brasfield) +** who thereby has the right to put this into the public domain. +** +** The conversions meet RFC 4648 requirements, provided that this +** C source specifies that line-feeds are included in the encoded +** data to limit visible line lengths to 72 characters and to +** terminate any encoded blob having non-zero length. +** +** Length limitations are not imposed except that the runtime +** SQLite string or blob length limits are respected. Otherwise, +** any length binary sequence can be represented and recovered. +** Generated base64 sequences, with their line-feeds included, +** can be concatenated; the result converted back to binary will +** be the concatenation of the represented binary sequences. +** +** This SQLite3 extension creates a function, base64(x), which +** either: converts text x containing base64 to a returned blob; +** or converts a blob x to returned text containing base64. An +** error will be thrown for other input argument types. +** +** This code relies on UTF-8 encoding only with respect to the +** meaning of the first 128 (7-bit) codes matching that of USASCII. +** It will fail miserably if somehow made to try to convert EBCDIC. +** Because it is table-driven, it could be enhanced to handle that, +** but the world and SQLite have moved on from that anachronism. +** +** To build the extension: +** Set shell variable SQDIR= +** *Nix: gcc -O2 -shared -I$SQDIR -fPIC -o base64.so base64.c +** OSX: gcc -O2 -dynamiclib -fPIC -I$SQDIR -o base64.dylib base64.c +** Win32: gcc -O2 -shared -I%SQDIR% -o base64.dll base64.c +** Win32: cl /Os -I%SQDIR% base64.c -link -dll -out:base64.dll +*/ + +#include + +#ifndef SQLITE_SHELL_EXTFUNCS /* Guard for #include as built-in extension. */ +#include "sqlite3ext.h" +#endif + +SQLITE_EXTENSION_INIT1; + +#define PC 0x80 /* pad character */ +#define WS 0x81 /* whitespace */ +#define ND 0x82 /* Not above or digit-value */ +#define PAD_CHAR '=' + +#ifndef UBYTE_TYPEDEF +typedef unsigned char ubyte; +# define UBYTE_TYPEDEF +#endif + +static const ubyte b64DigitValues[128] = { + /* HT LF VT FF CR */ + ND,ND,ND,ND, ND,ND,ND,ND, ND,WS,WS,WS, WS,WS,ND,ND, + /* US */ + ND,ND,ND,ND, ND,ND,ND,ND, ND,ND,ND,ND, ND,ND,ND,ND, + /*sp + / */ + WS,ND,ND,ND, ND,ND,ND,ND, ND,ND,ND,62, ND,ND,ND,63, + /* 0 1 5 9 = */ + 52,53,54,55, 56,57,58,59, 60,61,ND,ND, ND,PC,ND,ND, + /* A O */ + ND, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, + /* P Z */ + 15,16,17,18, 19,20,21,22, 23,24,25,ND, ND,ND,ND,ND, + /* a o */ + ND,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, + /* p z */ + 41,42,43,44, 45,46,47,48, 49,50,51,ND, ND,ND,ND,ND +}; + +static const char b64Numerals[64+1] += "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +#define BX_DV_PROTO(c) \ + ((((ubyte)(c))<0x80)? (ubyte)(b64DigitValues[(ubyte)(c)]) : 0x80) +#define IS_BX_DIGIT(bdp) (((ubyte)(bdp))<0x80) +#define IS_BX_WS(bdp) ((bdp)==WS) +#define IS_BX_PAD(bdp) ((bdp)==PC) +#define BX_NUMERAL(dv) (b64Numerals[(ubyte)(dv)]) +/* Width of base64 lines. Should be an integer multiple of 4. */ +#define B64_DARK_MAX 72 + +/* Encode a byte buffer into base64 text with linefeeds appended to limit +** encoded group lengths to B64_DARK_MAX or to terminate the last group. +*/ +static char* toBase64( ubyte *pIn, int nbIn, char *pOut ){ + int nCol = 0; + while( nbIn >= 3 ){ + /* Do the bit-shuffle, exploiting unsigned input to avoid masking. */ + pOut[0] = BX_NUMERAL(pIn[0]>>2); + pOut[1] = BX_NUMERAL(((pIn[0]<<4)|(pIn[1]>>4))&0x3f); + pOut[2] = BX_NUMERAL(((pIn[1]&0xf)<<2)|(pIn[2]>>6)); + pOut[3] = BX_NUMERAL(pIn[2]&0x3f); + pOut += 4; + nbIn -= 3; + pIn += 3; + if( (nCol += 4)>=B64_DARK_MAX || nbIn<=0 ){ + *pOut++ = '\n'; + nCol = 0; + } + } + if( nbIn > 0 ){ + signed char nco = nbIn+1; + int nbe; + unsigned long qv = *pIn++; + for( nbe=1; nbe<3; ++nbe ){ + qv <<= 8; + if( nbe=0; --nbe ){ + char ce = (nbe>= 6; + pOut[nbe] = ce; + } + pOut += 4; + *pOut++ = '\n'; + } + *pOut = 0; + return pOut; +} + +/* Skip over text which is not base64 numeral(s). */ +static char * skipNonB64( char *s ){ + char c; + while( (c = *s) && !IS_BX_DIGIT(BX_DV_PROTO(c)) ) ++s; + return s; +} + +/* Decode base64 text into a byte buffer. */ +static ubyte* fromBase64( char *pIn, int ncIn, ubyte *pOut ){ + if( ncIn>0 && pIn[ncIn-1]=='\n' ) --ncIn; + while( ncIn>0 && *pIn!=PAD_CHAR ){ + static signed char nboi[] = { 0, 0, 1, 2, 3 }; + char *pUse = skipNonB64(pIn); + unsigned long qv = 0L; + int nti, nbo, nac; + ncIn -= (pUse - pIn); + pIn = pUse; + nti = (ncIn>4)? 4 : ncIn; + ncIn -= nti; + nbo = nboi[nti]; + if( nbo==0 ) break; + for( nac=0; nac<4; ++nac ){ + char c = (nac>8) & 0xff; + case 1: + pOut[0] = (qv>>16) & 0xff; + } + pOut += nbo; + } + return pOut; +} + +/* This function does the work for the SQLite base64(x) UDF. */ +static void base64(sqlite3_context *context, int na, sqlite3_value *av[]){ + int nb, nc, nv = sqlite3_value_bytes(av[0]); + int nvMax = sqlite3_limit(sqlite3_context_db_handle(context), + SQLITE_LIMIT_LENGTH, -1); + char *cBuf; + ubyte *bBuf; + assert(na==1); + switch( sqlite3_value_type(av[0]) ){ + case SQLITE_BLOB: + nb = nv; + nc = 4*(nv+2/3); /* quads needed */ + nc += (nc+(B64_DARK_MAX-1))/B64_DARK_MAX + 1; /* LFs and a 0-terminator */ + if( nvMax < nc ){ + sqlite3_result_error(context, "blob expanded to base64 too big", -1); + return; + } + cBuf = sqlite3_malloc(nc); + if( !cBuf ) goto memFail; + bBuf = (ubyte*)sqlite3_value_blob(av[0]); + nc = (int)(toBase64(bBuf, nb, cBuf) - cBuf); + sqlite3_result_text(context, cBuf, nc, sqlite3_free); + break; + case SQLITE_TEXT: + nc = nv; + nb = 3*((nv+3)/4); /* may overestimate due to LF and padding */ + if( nvMax < nb ){ + sqlite3_result_error(context, "blob from base64 may be too big", -1); + return; + }else if( nb<1 ){ + nb = 1; + } + bBuf = sqlite3_malloc(nb); + if( !bBuf ) goto memFail; + cBuf = (char *)sqlite3_value_text(av[0]); + nb = (int)(fromBase64(cBuf, nc, bBuf) - bBuf); + sqlite3_result_blob(context, bBuf, nb, sqlite3_free); + break; + default: + sqlite3_result_error(context, "base64 accepts only blob or text", -1); + return; + } + return; + memFail: + sqlite3_result_error(context, "base64 OOM", -1); +} + +/* +** Establish linkage to running SQLite library. +*/ +#ifndef SQLITE_SHELL_EXTFUNCS +#ifdef _WIN32 +__declspec(dllexport) +#endif +int sqlite3_base_init +#else +static int sqlite3_base64_init +#endif +(sqlite3 *db, char **pzErr, const sqlite3_api_routines *pApi){ + SQLITE_EXTENSION_INIT2(pApi); + (void)pzErr; + return sqlite3_create_function + (db, "base64", 1, + SQLITE_DETERMINISTIC|SQLITE_INNOCUOUS|SQLITE_DIRECTONLY|SQLITE_UTF8, + 0, base64, 0, 0); +} + +/* +** Define some macros to allow this extension to be built into the shell +** conveniently, in conjunction with use of SQLITE_SHELL_EXTFUNCS. This +** allows shell.c, as distributed, to have this extension built in. +*/ +#define BASE64_INIT(db) sqlite3_base64_init(db, 0, 0) +#define BASE64_EXPOSE(db, pzErr) /* Not needed, ..._init() does this. */ diff --git a/ext/misc/base85.c b/ext/misc/base85.c new file mode 100644 index 0000000000..91e6992faf --- /dev/null +++ b/ext/misc/base85.c @@ -0,0 +1,437 @@ +/* +** 2022-11-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 is a utility for converting binary to base85 or vice-versa. +** It can be built as a standalone program or an SQLite3 extension. +** +** Much like base64 representations, base85 can be sent through a +** sane USASCII channel unmolested. It also plays nicely in CSV or +** written as TCL brace-enclosed literals or SQL string literals. +** It is not suited for unmodified use in XML-like documents. +** +** The encoding used resembles Ascii85, but was devised by the author +** (Larry Brasfield) before Mozilla, Adobe, ZMODEM or other Ascii85 +** variant sources existed, in the 1984 timeframe on a VAX mainframe. +** Further, this is an independent implementation of a base85 system. +** Hence, the author has rightfully put this into the public domain. +** +** Base85 numerals are taken from the set of 7-bit USASCII codes, +** excluding control characters and Space ! " ' ( ) { | } ~ Del +** in code order representing digit values 0 to 84 (base 10.) +** +** Groups of 4 bytes, interpreted as big-endian 32-bit values, +** are represented as 5-digit base85 numbers with MS to LS digit +** order. Groups of 1-3 bytes are represented with 2-4 digits, +** still big-endian but 8-24 bit values. (Using big-endian yields +** the simplest transition to byte groups smaller than 4 bytes. +** These byte groups can also be considered base-256 numbers.) +** Groups of 0 bytes are represented with 0 digits and vice-versa. +** No pad characters are used; Encoded base85 numeral sequence +** (aka "group") length maps 1-to-1 to the decoded binary length. +** +** Any character not in the base85 numeral set delimits groups. +** When base85 is streamed or stored in containers of indefinite +** size, newline is used to separate it into sub-sequences of no +** more than 80 digits so that fgets() can be used to read it. +** +** Length limitations are not imposed except that the runtime +** SQLite string or blob length limits are respected. Otherwise, +** any length binary sequence can be represented and recovered. +** Base85 sequences can be concatenated by separating them with +** a non-base85 character; the conversion to binary will then +** be the concatenation of the represented binary sequences. + +** The standalone program either converts base85 on stdin to create +** a binary file or converts a binary file to base85 on stdout. +** Read or make it blurt its help for invocation details. +** +** The SQLite3 extension creates a function, base85(x), which will +** either convert text base85 to a blob or a blob to text base85 +** and return the result (or throw an error for other types.) +** Unless built with OMIT_BASE85_CHECKER defined, it also creates a +** function, is_base85(t), which returns 1 iff the text t contains +** nothing other than base85 numerals and whitespace, or 0 otherwise. +** +** To build the extension: +** Set shell variable SQDIR= +** and variable OPTS to -DOMIT_BASE85_CHECKER if is_base85() unwanted. +** *Nix: gcc -O2 -shared -I$SQDIR $OPTS -fPIC -o base85.so base85.c +** OSX: gcc -O2 -dynamiclib -fPIC -I$SQDIR $OPTS -o base85.dylib base85.c +** Win32: gcc -O2 -shared -I%SQDIR% %OPTS% -o base85.dll base85.c +** Win32: cl /Os -I%SQDIR% %OPTS% base85.c -link -dll -out:base85.dll +** +** To build the standalone program, define PP symbol BASE85_STANDALONE. Eg. +** *Nix or OSX: gcc -O2 -DBASE85_STANDALONE base85.c -o base85 +** Win32: gcc -O2 -DBASE85_STANDALONE -o base85.exe base85.c +** Win32: cl /Os /MD -DBASE85_STANDALONE base85.c +*/ + +#include +#include +#include +#include +#ifndef OMIT_BASE85_CHECKER +# include +#endif + +#ifndef BASE85_STANDALONE + +#ifndef SQLITE_SHELL_EXTFUNCS /* Guard for #include as built-in extension. */ +# include "sqlite3ext.h" +#endif + +SQLITE_EXTENSION_INIT1; + +#else + +# ifdef _WIN32 +# include +# include +# else +# define setmode(fd,m) +# endif + +static char *zHelp = + "Usage: base85 \n" + " is either -r to read or -w to write ,\n" + " content to be converted to/from base85 on stdout/stdin.\n" + " names a binary file to be rendered or created.\n" + " Or, the name '-' refers to the stdin or stdout stream.\n" + ; + +static void sayHelp(){ + printf("%s", zHelp); +} +#endif + +#ifndef UBYTE_TYPEDEF +typedef unsigned char ubyte; +# define UBYTE_TYPEDEF +#endif + +/* Classify c according to interval within USASCII set w.r.t. base85 + * Values of 1 and 3 are base85 numerals. Values of 0, 2, or 4 are not. + */ +#define B85_CLASS( c ) (((c)>='#')+((c)>'&')+((c)>='*')+((c)>'z')) + +/* Provide digitValue to b85Numeral offset as a function of above class. */ +static ubyte b85_cOffset[] = { 0, '#', 0, '*'-4, 0 }; +#define B85_DNOS( c ) b85_cOffset[B85_CLASS(c)] + +/* Say whether c is a base85 numeral. */ +#define IS_B85( c ) (B85_CLASS(c) & 1) + +#if 0 /* Not used, */ +static ubyte base85DigitValue( char c ){ + ubyte dv = (ubyte)(c - '#'); + if( dv>87 ) return 0xff; + return (dv > 3)? dv-3 : dv; +} +#endif + +/* Width of base64 lines. Should be an integer multiple of 5. */ +#define B85_DARK_MAX 80 + + +static char * skipNonB85( char *s ){ + char c; + while( (c = *s) && !IS_B85(c) ) ++s; + return s; +} + +/* Convert small integer, known to be in 0..84 inclusive, to base85 numeral. + * Do not use the macro form with argument expression having a side-effect.*/ +#if 0 +static char base85Numeral( ubyte b ){ + return (b < 4)? (char)(b + '#') : (char)(b - 4 + '*'); +} +#else +# define base85Numeral( dn )\ + ((char)(((dn) < 4)? (char)((dn) + '#') : (char)((dn) - 4 + '*'))) +#endif + +static char *putcs(char *pc, char *s){ + char c; + while( (c = *s++)!=0 ) *pc++ = c; + return pc; +} + +/* Encode a byte buffer into base85 text. If pSep!=0, it's a C string +** to be appended to encoded groups to limit their length to B85_DARK_MAX +** or to terminate the last group (to aid concatenation.) +*/ +static char* toBase85( ubyte *pIn, int nbIn, char *pOut, char *pSep ){ + int nCol = 0; + while( nbIn >= 4 ){ + int nco = 5; + unsigned long qbv = (pIn[0]<<24)|(pIn[1]<<16)|(pIn[2]<<8)|pIn[3]; + while( nco > 0 ){ + unsigned nqv = (unsigned)(qbv/85UL); + unsigned char dv = qbv - 85UL*nqv; + qbv = nqv; + pOut[--nco] = base85Numeral(dv); + } + nbIn -= 4; + pIn += 4; + pOut += 5; + if( pSep && (nCol += 5)>=B85_DARK_MAX ){ + pOut = putcs(pOut, pSep); + nCol = 0; + } + } + if( nbIn > 0 ){ + int nco = nbIn + 1; + unsigned long qv = *pIn++; + int nbe = 1; + while( nbe++ < nbIn ){ + qv = (qv<<8) | *pIn++; + } + nCol += nco; + while( nco > 0 ){ + ubyte dv = (ubyte)(qv % 85); + qv /= 85; + pOut[--nco] = base85Numeral(dv); + } + pOut += (nbIn+1); + } + if( pSep && nCol>0 ) pOut = putcs(pOut, pSep); + *pOut = 0; + return pOut; +} + +/* Decode base85 text into a byte buffer. */ +static ubyte* fromBase85( char *pIn, int ncIn, ubyte *pOut ){ + if( ncIn>0 && pIn[ncIn-1]=='\n' ) --ncIn; + while( ncIn>0 ){ + static signed char nboi[] = { 0, 0, 1, 2, 3, 4 }; + char *pUse = skipNonB85(pIn); + unsigned long qv = 0L; + int nti, nbo; + ncIn -= (pUse - pIn); + pIn = pUse; + nti = (ncIn>5)? 5 : ncIn; + nbo = nboi[nti]; + if( nbo==0 ) break; + while( nti>0 ){ + char c = *pIn++; + ubyte cdo = B85_DNOS(c); + --ncIn; + if( cdo==0 ) break; + qv = 85 * qv + (c - cdo); + --nti; + } + nbo -= nti; /* Adjust for early (non-digit) end of group. */ + switch( nbo ){ + case 4: + *pOut++ = (qv >> 24)&0xff; + case 3: + *pOut++ = (qv >> 16)&0xff; + case 2: + *pOut++ = (qv >> 8)&0xff; + case 1: + *pOut++ = qv&0xff; + case 0: + break; + } + } + return pOut; +} + +#ifndef OMIT_BASE85_CHECKER +/* Say whether input char sequence is all (base85 and/or whitespace).*/ +static int allBase85( char *p, int len ){ + char c; + while( len-- > 0 && (c = *p++) != 0 ){ + if( !IS_B85(c) && !isspace(c) ) return 0; + } + return 1; +} +#endif + +#ifndef BASE85_STANDALONE + +# ifndef OMIT_BASE85_CHECKER +/* This function does the work for the SQLite is_base85(t) UDF. */ +static void is_base85(sqlite3_context *context, int na, sqlite3_value *av[]){ + assert(na==1); + switch( sqlite3_value_type(av[0]) ){ + case SQLITE_TEXT: + { + int rv = allBase85( (char *)sqlite3_value_text(av[0]), + sqlite3_value_bytes(av[0]) ); + sqlite3_result_int(context, rv); + } + break; + case SQLITE_NULL: + sqlite3_result_null(context); + break; + default: + sqlite3_result_error(context, "is_base85 accepts only text or NULL", -1); + return; + } +} +# endif + +/* This function does the work for the SQLite base85(x) UDF. */ +static void base85(sqlite3_context *context, int na, sqlite3_value *av[]){ + int nb, nc, nv = sqlite3_value_bytes(av[0]); + int nvMax = sqlite3_limit(sqlite3_context_db_handle(context), + SQLITE_LIMIT_LENGTH, -1); + char *cBuf; + ubyte *bBuf; + assert(na==1); + switch( sqlite3_value_type(av[0]) ){ + case SQLITE_BLOB: + nb = nv; + /* ulongs tail newlines tailenc+nul*/ + nc = 5*(nv/4) + nv%4 + nv/64+1 + 2; + if( nvMax < nc ){ + sqlite3_result_error(context, "blob expanded to base85 too big", -1); + return; + } + cBuf = sqlite3_malloc(nc); + if( !cBuf ) goto memFail; + bBuf = (ubyte*)sqlite3_value_blob(av[0]); + nc = (int)(toBase85(bBuf, nb, cBuf, "\n") - cBuf); + sqlite3_result_text(context, cBuf, nc, sqlite3_free); + break; + case SQLITE_TEXT: + nc = nv; + nb = 4*(nv/5) + nv%5; /* may overestimate */ + if( nvMax < nb ){ + sqlite3_result_error(context, "blob from base85 may be too big", -1); + return; + }else if( nb<1 ){ + nb = 1; + } + bBuf = sqlite3_malloc(nb); + if( !bBuf ) goto memFail; + cBuf = (char *)sqlite3_value_text(av[0]); + nb = (int)(fromBase85(cBuf, nc, bBuf) - bBuf); + sqlite3_result_blob(context, bBuf, nb, sqlite3_free); + break; + default: + sqlite3_result_error(context, "base85 accepts only blob or text.", -1); + return; + } + return; + memFail: + sqlite3_result_error(context, "base85 OOM", -1); +} + +/* +** Establish linkage to running SQLite library. +*/ +#ifndef SQLITE_SHELL_EXTFUNCS +#ifdef _WIN32 +__declspec(dllexport) +#endif +int sqlite3_base_init +#else +static int sqlite3_base85_init +#endif +(sqlite3 *db, char **pzErr, const sqlite3_api_routines *pApi){ + SQLITE_EXTENSION_INIT2(pApi); + (void)pzErr; +# ifndef OMIT_BASE85_CHECKER + { + int rc = sqlite3_create_function + (db, "is_base85", 1, + SQLITE_DETERMINISTIC|SQLITE_INNOCUOUS|SQLITE_UTF8, + 0, is_base85, 0, 0); + if( rc!=SQLITE_OK ) return rc; + } +# endif + return sqlite3_create_function + (db, "base85", 1, + SQLITE_DETERMINISTIC|SQLITE_INNOCUOUS|SQLITE_DIRECTONLY|SQLITE_UTF8, + 0, base85, 0, 0); +} + +/* +** Define some macros to allow this extension to be built into the shell +** conveniently, in conjunction with use of SQLITE_SHELL_EXTFUNCS. This +** allows shell.c, as distributed, to have this extension built in. +*/ +# define BASE85_INIT(db) sqlite3_base85_init(db, 0, 0) +# define BASE85_EXPOSE(db, pzErr) /* Not needed, ..._init() does this. */ + +#else /* standalone program */ + +int main(int na, char *av[]){ + int cin; + int rc = 0; + ubyte bBuf[4*(B85_DARK_MAX/5)]; + char cBuf[5*(sizeof(bBuf)/4)+2]; + size_t nio; +# ifndef OMIT_BASE85_CHECKER + int b85Clean = 1; +# endif + char rw; + FILE *fb = 0, *foc = 0; + char fmode[3] = "xb"; + if( na < 3 || av[1][0]!='-' || (rw = av[1][1])==0 || (rw!='r' && rw!='w') ){ + sayHelp(); + return 0; + } + fmode[0] = rw; + if( av[2][0]=='-' && av[2][1]==0 ){ + switch( rw ){ + case 'r': + fb = stdin; + setmode(fileno(stdin), O_BINARY); + break; + case 'w': + fb = stdout; + setmode(fileno(stdout), O_BINARY); + break; + } + }else{ + fb = fopen(av[2], fmode); + foc = fb; + } + if( !fb ){ + fprintf(stderr, "Cannot open %s for %c\n", av[2], rw); + rc = 1; + }else{ + switch( rw ){ + case 'r': + while( (nio = fread( bBuf, 1, sizeof(bBuf), fb))>0 ){ + toBase85( bBuf, (int)nio, cBuf, 0 ); + fprintf(stdout, "%s\n", cBuf); + } + break; + case 'w': + while( 0 != fgets(cBuf, sizeof(cBuf), stdin) ){ + int nc = strlen(cBuf); + size_t nbo = fromBase85( cBuf, nc, bBuf ) - bBuf; + if( 1 != fwrite(bBuf, nbo, 1, fb) ) rc = 1; +# ifndef OMIT_BASE85_CHECKER + b85Clean &= allBase85( cBuf, nc ); +# endif + } + break; + default: + sayHelp(); + rc = 1; + } + if( foc ) fclose(foc); + } +# ifndef OMIT_BASE85_CHECKER + if( !b85Clean ){ + fprintf(stderr, "Base85 input had non-base85 dark or control content.\n"); + } +# endif + return rc; +} + +#endif diff --git a/ext/misc/basexx.c b/ext/misc/basexx.c new file mode 100644 index 0000000000..2ca2906fae --- /dev/null +++ b/ext/misc/basexx.c @@ -0,0 +1,83 @@ +/* +** 2022-11-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 source allows multiple SQLite extensions to be either: combined +** into a single runtime-loadable library; or built into the SQLite shell +** using a preprocessing convention set by src/shell.c.in (and shell.c). +** +** Presently, it combines the base64.c and base85.c extensions. However, +** it can be used as a template for other combinations. +** +** Example usages: +** +** - Build a runtime-loadable extension from SQLite checkout directory: +** *Nix, OSX: gcc -O2 -shared -I. -fPIC -o basexx.so ext/misc/basexx.c +** Win32: cl /Os -I. ext/misc/basexx.c -link -dll -out:basexx.dll +** +** - Incorporate as built-in in sqlite3 shell: +** *Nix, OSX with gcc on a like platform: +** export mop1=-DSQLITE_SHELL_EXTSRC=ext/misc/basexx.c +** export mop2=-DSQLITE_SHELL_EXTFUNCS=BASEXX +** make sqlite3 "OPTS=$mop1 $mop2" +** Win32 with Microsoft toolset on Windows: +** set mop1=-DSQLITE_SHELL_EXTSRC=ext/misc/basexx.c +** set mop2=-DSQLITE_SHELL_EXTFUNCS=BASEXX +** set mops="OPTS=%mop1% %mop2%" +** nmake -f Makefile.msc sqlite3.exe %mops% +*/ + +#ifndef SQLITE_SHELL_EXTFUNCS /* Guard for #include as built-in extension. */ +# include "sqlite3ext.h" +SQLITE_EXTENSION_INIT1; +#endif + +static void init_api_ptr(const sqlite3_api_routines *pApi){ + SQLITE_EXTENSION_INIT2(pApi); +} + +#undef SQLITE_EXTENSION_INIT1 +#define SQLITE_EXTENSION_INIT1 /* */ +#undef SQLITE_EXTENSION_INIT2 +#define SQLITE_EXTENSION_INIT2(v) (void)v + +/* These next 2 undef's are only needed because the entry point names + * collide when formulated per the rules stated for loadable extension + * entry point names that will be deduced from the file basenames. + */ +#undef sqlite3_base_init +#define sqlite3_base_init sqlite3_base64_init +#include "base64.c" + +#undef sqlite3_base_init +#define sqlite3_base_init sqlite3_base85_init +#include "base85.c" + +#ifdef _WIN32 +__declspec(dllexport) +#endif +int sqlite3_basexx_init(sqlite3 *db, char **pzErr, + const sqlite3_api_routines *pApi){ + init_api_ptr(pApi); + int rc1 = BASE64_INIT(db); + int rc2 = BASE85_INIT(db); + + if( rc1==SQLITE_OK && rc2==SQLITE_OK ){ + BASE64_EXPOSE(db, pzErr); + BASE64_EXPOSE(db, pzErr); + return SQLITE_OK; + }else{ + return SQLITE_ERROR; + } +} + +# define BASEXX_INIT(db) sqlite3_basexx_init(db, 0, 0) +# define BASEXX_EXPOSE(db, pzErr) /* Not needed, ..._init() does this. */ diff --git a/ext/misc/regexp.c b/ext/misc/regexp.c index d0c8ee5cfe..086ef564cb 100644 --- a/ext/misc/regexp.c +++ b/ext/misc/regexp.c @@ -185,7 +185,7 @@ static unsigned re_next_char(ReInput *p){ c = (c&0x0f)<<12 | ((p->z[p->i]&0x3f)<<6) | (p->z[p->i+1]&0x3f); p->i += 2; if( c<=0x7ff || (c>=0xd800 && c<=0xdfff) ) c = 0xfffd; - }else if( (c&0xf8)==0xf0 && p->i+3mx && (p->z[p->i]&0xc0)==0x80 + }else if( (c&0xf8)==0xf0 && p->i+2mx && (p->z[p->i]&0xc0)==0x80 && (p->z[p->i+1]&0xc0)==0x80 && (p->z[p->i+2]&0xc0)==0x80 ){ c = (c&0x07)<<18 | ((p->z[p->i]&0x3f)<<12) | ((p->z[p->i+1]&0x3f)<<6) | (p->z[p->i+2]&0x3f); @@ -712,15 +712,15 @@ static const char *re_compile(ReCompiled **ppRe, const char *zIn, int noCase){ ** one or more matching characters, enter those matching characters into ** zInit[]. The re_match() routine can then search ahead in the input ** string looking for the initial match without having to run the whole - ** regex engine over the string. Do not worry able trying to match + ** regex engine over the string. Do not worry about trying to match ** unicode characters beyond plane 0 - those are very rare and this is ** just an optimization. */ if( pRe->aOp[0]==RE_OP_ANYSTAR && !noCase ){ for(j=0, i=1; j<(int)sizeof(pRe->zInit)-2 && pRe->aOp[i]==RE_OP_MATCH; i++){ unsigned x = pRe->aArg[i]; - if( x<=127 ){ + if( x<=0x7f ){ pRe->zInit[j++] = (unsigned char)x; - }else if( x<=0xfff ){ + }else if( x<=0x7ff ){ pRe->zInit[j++] = (unsigned char)(0xc0 | (x>>6)); pRe->zInit[j++] = 0x80 | (x&0x3f); }else if( x<=0xffff ){ diff --git a/ext/misc/shathree.c b/ext/misc/shathree.c index f80dd855da..765c691811 100644 --- a/ext/misc/shathree.c +++ b/ext/misc/shathree.c @@ -531,7 +531,7 @@ static void sha3Func( /* Compute a string using sqlite3_vsnprintf() with a maximum length ** of 50 bytes and add it to the hash. */ -static void hash_step_vformat( +static void sha3_step_vformat( SHA3Context *p, /* Add content to this context */ const char *zFormat, ... @@ -627,7 +627,7 @@ static void sha3QueryFunc( z = sqlite3_sql(pStmt); if( z ){ n = (int)strlen(z); - hash_step_vformat(&cx,"S%d:",n); + sha3_step_vformat(&cx,"S%d:",n); SHA3Update(&cx,(unsigned char*)z,n); } @@ -671,14 +671,14 @@ static void sha3QueryFunc( case SQLITE_TEXT: { int n2 = sqlite3_column_bytes(pStmt, i); const unsigned char *z2 = sqlite3_column_text(pStmt, i); - hash_step_vformat(&cx,"T%d:",n2); + sha3_step_vformat(&cx,"T%d:",n2); SHA3Update(&cx, z2, n2); break; } case SQLITE_BLOB: { int n2 = sqlite3_column_bytes(pStmt, i); const unsigned char *z2 = sqlite3_column_blob(pStmt, i); - hash_step_vformat(&cx,"B%d:",n2); + sha3_step_vformat(&cx,"B%d:",n2); SHA3Update(&cx, z2, n2); break; } diff --git a/ext/rbu/rbuB.test b/ext/rbu/rbuB.test index ece51c39b2..6eb917e57d 100644 --- a/ext/rbu/rbuB.test +++ b/ext/rbu/rbuB.test @@ -48,7 +48,7 @@ do_test 1.2 { do_test 1.3 { set ::errlog -} {SQLITE_NOTICE_RECOVER_WAL SQLITE_INTERNAL} +} {SQLITE_NOTICE_RECOVER_WAL SQLITE_NOTICE_RBU} do_execsql_test 1.4 { SELECT * FROM t1 diff --git a/ext/rbu/sqlite3rbu.c b/ext/rbu/sqlite3rbu.c index 482952006e..04d727a571 100644 --- a/ext/rbu/sqlite3rbu.c +++ b/ext/rbu/sqlite3rbu.c @@ -3031,11 +3031,11 @@ static void rbuSetupCheckpoint(sqlite3rbu *p, RbuState *pState){ ** no-ops. These locks will not be released until the connection ** is closed. ** - ** * Attempting to xSync() the database file causes an SQLITE_INTERNAL + ** * Attempting to xSync() the database file causes an SQLITE_NOTICE ** error. ** ** As a result, unless an error (i.e. OOM or SQLITE_BUSY) occurs, the - ** checkpoint below fails with SQLITE_INTERNAL, and leaves the aFrame[] + ** checkpoint below fails with SQLITE_NOTICE, and leaves the aFrame[] ** array populated with a set of (frame -> page) mappings. Because the ** WRITER, CHECKPOINT and READ0 locks are still held, it is safe to copy ** data from the wal file into the database file according to the @@ -3045,7 +3045,7 @@ static void rbuSetupCheckpoint(sqlite3rbu *p, RbuState *pState){ int rc2; p->eStage = RBU_STAGE_CAPTURE; rc2 = sqlite3_exec(p->dbMain, "PRAGMA main.wal_checkpoint=restart", 0, 0,0); - if( rc2!=SQLITE_INTERNAL ) p->rc = rc2; + if( rc2!=SQLITE_NOTICE ) p->rc = rc2; } if( p->rc==SQLITE_OK && p->nFrame>0 ){ @@ -3091,7 +3091,7 @@ static int rbuCaptureWalRead(sqlite3rbu *pRbu, i64 iOff, int iAmt){ if( pRbu->mLock!=mReq ){ pRbu->rc = SQLITE_BUSY; - return SQLITE_INTERNAL; + return SQLITE_NOTICE_RBU; } pRbu->pgsz = iAmt; @@ -4478,7 +4478,7 @@ void sqlite3rbu_rename_handler( ** database file are recorded. xShmLock() calls to unlock the same ** locks are no-ops (so that once obtained, these locks are never ** relinquished). Finally, calls to xSync() on the target database -** file fail with SQLITE_INTERNAL errors. +** file fail with SQLITE_NOTICE errors. */ static void rbuUnlockShm(rbu_file *p){ @@ -4757,7 +4757,7 @@ static int rbuVfsSync(sqlite3_file *pFile, int flags){ rbu_file *p = (rbu_file *)pFile; if( p->pRbu && p->pRbu->eStage==RBU_STAGE_CAPTURE ){ if( p->openFlags & SQLITE_OPEN_MAIN_DB ){ - return SQLITE_INTERNAL; + return SQLITE_NOTICE_RBU; } return SQLITE_OK; } diff --git a/ext/recover/recover1.test b/ext/recover/recover1.test index 75f5dba1ff..3e8a691492 100644 --- a/ext/recover/recover1.test +++ b/ext/recover/recover1.test @@ -273,5 +273,46 @@ do_execsql_test 15.1 { } {} do_recover_test 15 +#------------------------------------------------------------------------- +reset_db +do_execsql_test 16.1 { + PRAGMA journal_mode = wal; + CREATE TABLE t1(x); + INSERT INTO t1 VALUES(1), (2), (3); +} {wal} +do_test 16.2 { + set R [sqlite3_recover_init db main test.db2] + $R run + $R finish +} {} +do_execsql_test 16.3 { + SELECT * FROM t1; +} {1 2 3} + +do_execsql_test 16.4 { + BEGIN; + SELECT * FROM t1; +} {1 2 3} +do_test 16.5 { + set R [sqlite3_recover_init db main test.db2] + $R run + list [catch { $R finish } msg] $msg +} {1 {cannot start a transaction within a transaction}} +do_execsql_test 16.6 { + SELECT * FROM t1; +} {1 2 3} +do_execsql_test 16.7 { + INSERT INTO t1 VALUES(4); +} +do_test 16.8 { + set R [sqlite3_recover_init db main test.db2] + $R run + list [catch { $R finish } msg] $msg +} {1 {cannot start a transaction within a transaction}} +do_execsql_test 16.9 { + SELECT * FROM t1; + COMMIT; +} {1 2 3 4} + finish_test diff --git a/ext/recover/sqlite3recover.c b/ext/recover/sqlite3recover.c index 30260f014e..e62aba752f 100644 --- a/ext/recover/sqlite3recover.c +++ b/ext/recover/sqlite3recover.c @@ -2017,6 +2017,7 @@ static void recoverFinalCleanup(sqlite3_recover *p){ p->pTblList = 0; sqlite3_finalize(p->pGetPage); p->pGetPage = 0; + sqlite3_file_control(p->dbIn, p->zDb, SQLITE_FCNTL_RESET_CACHE, 0); { #ifndef NDEBUG @@ -2315,6 +2316,7 @@ static int recoverVfsRead(sqlite3_file *pFd, void *aBuf, int nByte, i64 iOff){ ** ** + first freelist page (32-bits at offset 32) ** + size of freelist (32-bits at offset 36) + ** + the wal-mode flags (16-bits at offset 18) ** ** We also try to preserve the auto-vacuum, incr-value, user-version ** and application-id fields - all 32 bit quantities at offsets @@ -2378,7 +2380,8 @@ static int recoverVfsRead(sqlite3_file *pFd, void *aBuf, int nByte, i64 iOff){ if( p->pPage1Cache ){ p->pPage1Disk = &p->pPage1Cache[nByte]; memcpy(p->pPage1Disk, aBuf, nByte); - + aHdr[18] = a[18]; + aHdr[19] = a[19]; recoverPutU32(&aHdr[28], dbsz); recoverPutU32(&aHdr[56], enc); recoverPutU16(&aHdr[105], pgsz-nReserve); @@ -2574,6 +2577,7 @@ static void recoverStep(sqlite3_recover *p){ recoverOpenOutput(p); /* Open transactions on both the input and output databases. */ + sqlite3_file_control(p->dbIn, p->zDb, SQLITE_FCNTL_RESET_CACHE, 0); recoverExec(p, p->dbIn, "PRAGMA writable_schema = on"); recoverExec(p, p->dbIn, "BEGIN"); if( p->errCode==SQLITE_OK ) p->bCloseTransaction = 1; diff --git a/ext/session/sessionat.test b/ext/session/sessionat.test index 8141d92322..e3f9e31ed7 100644 --- a/ext/session/sessionat.test +++ b/ext/session/sessionat.test @@ -243,6 +243,60 @@ eval [string map [list %WR% $trailing] { sqlite3changeset_apply db $cinv xConflict execsql { SELECT * FROM t7 } } {1 1 ccc 2 2 ccc 3 3 ccc} + + #----------------------------------------------------------------------- + reset_test + do_execsql_test $tn.7.0 { + CREATE TABLE t8(a PRIMARY KEY, b, c); + } + do_execsql_test -db db2 $tn.7.1 { + CREATE TABLE t8(a PRIMARY KEY, b, c, d DEFAULT 'D', e DEFAULT 'E'); + } + + do_then_apply_sql { + INSERT INTO t8 VALUES(1, 2, 3); + INSERT INTO t8 VALUES(4, 5, 6); + } + do_execsql_test $tn.7.2.1 { + SELECT * FROM t8 + } {1 2 3 4 5 6} + do_execsql_test -db db2 $tn.7.2.2 { + SELECT * FROM t8 + } {1 2 3 D E 4 5 6 D E} + + do_then_apply_sql { + UPDATE t8 SET c=45 WHERE a=4; + } + do_execsql_test $tn.7.3.1 { + SELECT * FROM t8 + } {1 2 3 4 5 45} + do_execsql_test -db db2 $tn.7.3.2 { + SELECT * FROM t8 + } {1 2 3 D E 4 5 45 D E} + + #----------------------------------------------------------------------- + reset_test + do_execsql_test $tn.8.0 { + CREATE TABLE t9(a PRIMARY KEY, b, c, d, e, f, g, h); + } + do_execsql_test -db db2 $tn.8.1 { + CREATE TABLE t9(a PRIMARY KEY, b, c, d, e, f, g, h, i, j, k, l); + } + do_then_apply_sql { + INSERT INTO t9 VALUES(1, 2, 3, 4, 5, 6, 7, 8); + } + do_then_apply_sql { + UPDATE t9 SET h=450 WHERE a=1 + } + do_execsql_test -db db2 $tn.8.2 { + SELECT * FROM t9 + } {1 2 3 4 5 6 7 450 {} {} {} {}} + do_then_apply_sql { + UPDATE t9 SET h=NULL + } + do_execsql_test -db db2 $tn.8.2 { + SELECT * FROM t9 + } {1 2 3 4 5 6 7 {} {} {} {} {}} }] } diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c index 93a9f2c73d..e8d95553d0 100644 --- a/ext/session/sqlite3session.c +++ b/ext/session/sqlite3session.c @@ -3348,6 +3348,22 @@ static int sessionChangesetNextOne( if( p->op==SQLITE_INSERT ) p->op = SQLITE_DELETE; else if( p->op==SQLITE_DELETE ) p->op = SQLITE_INSERT; } + + /* If this is an UPDATE that is part of a changeset, then check that + ** there are no fields in the old.* record that are not (a) PK fields, + ** or (b) also present in the new.* record. + ** + ** Such records are technically corrupt, but the rebaser was at one + ** point generating them. Under most circumstances this is benign, but + ** can cause spurious SQLITE_RANGE errors when applying the changeset. */ + if( p->bPatchset==0 && p->op==SQLITE_UPDATE){ + for(i=0; inCol; i++){ + if( p->abPK[i]==0 && p->apValue[i+p->nCol]==0 ){ + sqlite3ValueFree(p->apValue[i]); + p->apValue[i] = 0; + } + } + } } return SQLITE_ROW; @@ -5545,7 +5561,7 @@ static void sessionAppendPartialUpdate( if( !pIter->abPK[i] && a1[0] ) bData = 1; memcpy(pOut, a1, n1); pOut += n1; - }else if( a2[0]!=0xFF ){ + }else if( a2[0]!=0xFF && a1[0] ){ bData = 1; memcpy(pOut, a2, n2); pOut += n2; diff --git a/ext/session/test_session.c b/ext/session/test_session.c index 009f2990d0..78fa282a66 100644 --- a/ext/session/test_session.c +++ b/ext/session/test_session.c @@ -98,6 +98,19 @@ int sql_exec_changeset( } /************************************************************************/ + +#ifdef SQLITE_DEBUG +static int sqlite3_test_changeset(int, void *, char **); +static void assert_changeset_is_ok(int n, void *p){ + int rc = 0; + char *z = 0; + rc = sqlite3_test_changeset(n, p, &z); + assert( z==0 ); +} +#else +# define assert_changeset_is_ok(n,p) +#endif + /* ** Tclcmd: sql_exec_changeset DB SQL */ @@ -127,6 +140,7 @@ static int SQLITE_TCLAPI test_sql_exec_changeset( return TCL_ERROR; } + assert_changeset_is_ok(nChangeset, pChangeset); Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(pChangeset, nChangeset)); sqlite3_free(pChangeset); return TCL_OK; @@ -299,6 +313,7 @@ static int SQLITE_TCLAPI test_session_cmd( } } if( rc==SQLITE_OK ){ + assert_changeset_is_ok(o.n, o.p); Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(o.p, o.n)); } sqlite3_free(o.p); @@ -958,6 +973,7 @@ static int SQLITE_TCLAPI test_sqlite3changeset_invert( if( rc!=SQLITE_OK ){ rc = test_session_error(interp, rc, 0); }else{ + assert_changeset_is_ok(sOut.n, sOut.p); Tcl_SetObjResult(interp,Tcl_NewByteArrayObj((unsigned char*)sOut.p,sOut.n)); } sqlite3_free(sOut.p); @@ -1006,6 +1022,7 @@ static int SQLITE_TCLAPI test_sqlite3changeset_concat( if( rc!=SQLITE_OK ){ rc = test_session_error(interp, rc, 0); }else{ + assert_changeset_is_ok(sOut.n, sOut.p); Tcl_SetObjResult(interp,Tcl_NewByteArrayObj((unsigned char*)sOut.p,sOut.n)); } sqlite3_free(sOut.p); @@ -1362,6 +1379,7 @@ static int SQLITE_TCLAPI test_rebaser_cmd( } if( rc==SQLITE_OK ){ + assert_changeset_is_ok(sOut.n, sOut.p); Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(sOut.p, sOut.n)); } sqlite3_free(sOut.p); @@ -1408,6 +1426,107 @@ static int SQLITE_TCLAPI test_sqlite3rebaser_create( return TCL_OK; } +/* +** Run some sanity checks on the changeset in nChangeset byte buffer +** pChangeset. If any fail, return a non-zero value and, optionally, +** set output variable (*pzErr) to point to a buffer containing an +** English language error message describing the problem. In this +** case it is the responsibility of the caller to free the buffer +** using sqlite3_free(). +** +** Or, if the changeset appears to be well-formed, this function +** returns SQLITE_OK and sets (*pzErr) to NULL. +*/ +static int sqlite3_test_changeset( + int nChangeset, + void *pChangeset, + char **pzErr +){ + sqlite3_changeset_iter *pIter = 0; + char *zErr = 0; + int rc = SQLITE_OK; + int bPatch = (nChangeset>0 && ((char*)pChangeset)[0]=='P'); + + rc = sqlite3changeset_start(&pIter, nChangeset, pChangeset); + if( rc==SQLITE_OK ){ + int rc2; + while( rc==SQLITE_OK && SQLITE_ROW==sqlite3changeset_next(pIter) ){ + unsigned char *aPk = 0; + int nCol = 0; + int op = 0; + const char *zTab = 0; + + sqlite3changeset_pk(pIter, &aPk, &nCol); + sqlite3changeset_op(pIter, &zTab, &nCol, &op, 0); + + if( op==SQLITE_UPDATE ){ + int iCol; + for(iCol=0; iCol/dev/null) MAKEFILE := $(lastword $(MAKEFILE_LIST)) @@ -71,17 +87,19 @@ dir.jacc := jaccwabyt dir.common := common dir.fiddle := fiddle dir.tool := $(dir.top)/tool +CLEAN_FILES += *~ $(dir.jacc)/*~ $(dir.api)/*~ $(dir.common)/*~ $(dir.fiddle)/*~ + ######################################################################## # dir.dout = output dir for deliverables. # -# MAINTENANCE REMINDER: the output .js and .wasm files of emcc must be -# in _this_ dir, rather than a subdir, or else parts of the generated -# code get confused and cannot load property. Specifically, when X.js -# loads X.wasm, whether or not X.js uses the correct path for X.wasm -# depends on how it's loaded: an HTML script tag will resolve it -# intuitively, whereas a Worker's call to importScripts() will not. -# That's a fundamental incompatibility with how URL resolution in -# JS happens between those two contexts. See: +# MAINTENANCE REMINDER: the output .js and .wasm files of certain emcc +# buildables must be in _this_ dir, rather than a subdir, or else +# parts of the generated code get confused and cannot load +# property. Specifically, when X.js loads X.wasm, whether or not X.js +# uses the correct path for X.wasm depends on how it's loaded: an HTML +# script tag will resolve it intuitively, whereas a Worker's call to +# importScripts() will not. That's a fundamental incompatibility with +# how URL resolution in JS happens between those two contexts. See: # # https://zzz.buzz/2017/03/14/relative-uris-in-web-development/ # @@ -104,13 +122,12 @@ ifeq (,$(wildcard $(dir.tmp))) dir._tmp := $(shell mkdir -p $(dir.tmp)) endif -cflags.common := -I. -I.. -I$(dir.top) -CLEAN_FILES += *~ $(dir.jacc)/*~ $(dir.api)/*~ $(dir.common)/*~ -emcc.WASM_BIGINT ?= 1 sqlite3.c := $(dir.top)/sqlite3.c sqlite3.h := $(dir.top)/sqlite3.h +# Most SQLITE_OPT flags are set in sqlite3-wasm.c but we need them +# made explicit here for building speedtest1.c. SQLITE_OPT = \ - -DSQLITE_ENABLE_FTS4 \ + -DSQLITE_ENABLE_FTS5 \ -DSQLITE_ENABLE_RTREE \ -DSQLITE_ENABLE_EXPLAIN_COMMENTS \ -DSQLITE_ENABLE_UNKNOWN_SQL_FUNCTION \ @@ -130,16 +147,100 @@ SQLITE_OPT = \ '-DSQLITE_DEFAULT_UNIX_VFS="unix-none"' \ -DSQLITE_USE_URI=1 \ -DSQLITE_WASM_ENABLE_C_TESTS -# ^^^ most flags are set in sqlite3-wasm.c but we need them -# made explicit here for building speedtest1.c. -ifneq (,$(filter release,$(MAKECMDGOALS))) -emcc_opt ?= -Oz -flto +$(sqlite3.c) $(sqlite3.h): + $(MAKE) -C $(dir.top) sqlite3.c + +.PHONY: clean distclean +clean: + -rm -f $(CLEAN_FILES) +distclean: clean + -rm -f $(DISTCLEAN_FILES) + +ifeq (release,$(filter release,$(MAKECMDGOALS))) + ifeq (,$(wasm-strip)) + $(error Cannot make release-quality binary because wasm-strip is not available. \ + See notes in the warning above) + endif else -emcc_opt ?= -O0 -# ^^^^ build times for -O levels higher than 0 are painful at -# dev-time. + $(info Development build. Use '$(MAKE) release' for a smaller release build.) endif + +# bin.version-info = binary to output various sqlite3 version info for +# embedding in the JS files and in building the distribution zip file. +# It must NOT be in $(dir.tmp) because we need it to survive the +# cleanup process for the dist build to work properly. +bin.version-info := $(dir.wasm)/version-info +$(bin.version-info): $(dir.wasm)/version-info.c $(sqlite3.h) $(MAKEFILE) + $(CC) -O0 -I$(dir.top) -o $@ $< +DISTCLEAN_FILES += $(bin.version-info) + +# bin.stripcomments is used for stripping C/C++-style comments from JS +# files. The JS files contain large chunks of documentation which we +# don't need for all builds. That app's -k flag is of particular +# importance here, as it allows us to retain the opening comment +# blocks, which contain the license header and version info. +bin.stripccomments := $(dir.tool)/stripccomments +$(bin.stripccomments): $(bin.stripccomments).c $(MAKEFILE) + $(CC) -o $@ $< +DISTCLEAN_FILES += $(bin.stripccomments) + + +######################################################################## +# C-PP.FILTER: a $(call)able to transform $(1) to $(2) via ./c-pp -f +# $(1) ... +# +# Historical notes: +# +# - We first attempted to use gcc and/or clang to preprocess JS files +# in the same way we would normally do C files, but C-specific quirks +# of each makes that untennable. +# +# - We implemented c-pp.c (the C-Minus Pre-processor) as a custom +# generic/file-format-agnostic preprocessor to enable us to pack +# code for different target builds into the same JS files. Most +# notably, some ES6 module (a.k.a. ESM) features cannot legally be +# referenced at all in non-ESM code, e.g. the "import" and "export" +# keywords. This preprocessing step permits us to swap out sections +# of code where necessary for ESM and non-ESM (a.k.a. vanilla JS) +# require different implementations. The alternative to such +# preprocessing, would be to have separate source files for ES6 +# builds, which would have a higher maintenance burden than c-pp.c +# seems likely to. +# +# c-pp.c was written specifically for the sqlite project's JavaScript +# builds but is maintained as a standalone project: +# https://fossil.wanderinghorse.net/r/c-pp +bin.c-pp := ./c-pp +$(bin.c-pp): c-pp.c $(sqlite3.c) $(MAKEFILE) + $(CC) -O0 -o $@ c-pp.c $(sqlite3.c) '-DCMPP_DEFAULT_DELIM="//#"' -I$(dir.top) +define C-PP.FILTER +# Create $2 from $1 using $(bin.c-pp) +# $1 = Input file: c-pp -f $(1).js +# $2 = Output file: c-pp -o $(2).js +# $3 = optional c-pp -D... flags +$(2): $(1) $$(MAKEFILE) $$(bin.c-pp) + $$(bin.c-pp) -f $(1) -o $$@ $(3) +CLEAN_FILES += $(2) +endef +c-pp.D.vanilla ?= +c-pp.D.esm ?= -Dtarget=es6-module +# /end C-PP.FILTER +######################################################################## + + +# cflags.common = C compiler flags for all builds +cflags.common := -I. -I.. -I$(dir.top) +# emcc.WASM_BIGINT = 1 for BigInt (C int64) support, else 0. The API +# disables certain features if BigInt is not enabled and such builds +# _are not tested_ on any regular basis. +emcc.WASM_BIGINT ?= 1 + +# emcc_opt = optimization-related flags. These are primarily used by +# the various oX targets. build times for -O levels higher than 0 are +# painful at dev-time. +emcc_opt ?= -O0 + # When passing emcc_opt from the CLI, += and re-assignment have no # effect, so emcc_opt+=-g3 doesn't work. So... emcc_opt_full := $(emcc_opt) -g3 @@ -165,50 +266,29 @@ emcc_opt_full := $(emcc_opt) -g3 # speed for the unit tests is all over the place either way so it's # difficult to say whether -Os gives any speed benefit over -Oz. # -# (Much later: -O2 consistently gives the best speeds.) +# Much practice has demonstrated that -O2 consistently gives the best +# runtime speeds, but not by a large enough factor to rule out use of +# -Oz when small deliverable size is a priority. ######################################################################## - -$(sqlite3.c) $(sqlite3.h): - $(MAKE) -C $(dir.top) sqlite3.c - -.PHONY: clean distclean -clean: - -rm -f $(CLEAN_FILES) -distclean: clean - -rm -f $(DISTCLEAN_FILES) - -ifeq (release,$(filter release,$(MAKECMDGOALS))) - ifeq (,$(wasm-strip)) - $(error Cannot make release-quality binary because wasm-strip is not available. \ - See notes in the warning above) - endif -else - $(info Development build. Use '$(MAKE) release' for a smaller release build.) -endif - -bin.version-info := $(dir.wasm)/version-info -# ^^^^ NOT 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.c $(sqlite3.h) $(MAKEFILE) - $(CC) -O0 -I$(dir.top) -o $@ $< -DISTCLEAN_FILES += $(bin.version-info) - -bin.stripccomments := $(dir.tool)/stripccomments -$(bin.stripccomments): $(bin.stripccomments).c $(MAKEFILE) - $(CC) -o $@ $< -DISTCLEAN_FILES += $(bin.stripccomments) - +# EXPORTED_FUNCTIONS.* = files for use with Emscripten's +# -sEXPORTED_FUNCTION flag. EXPORTED_FUNCTIONS.api.in := $(abspath $(dir.api)/EXPORTED_FUNCTIONS.sqlite3-api) EXPORTED_FUNCTIONS.api := $(dir.tmp)/EXPORTED_FUNCTIONS.api $(EXPORTED_FUNCTIONS.api): $(EXPORTED_FUNCTIONS.api.in) $(MAKEFILE) - cat $(EXPORTED_FUNCTIONS.api.in) > $@ + cp $(EXPORTED_FUNCTIONS.api.in) $@ +# sqlite3-license-version.js = generated JS file with the license +# header and version info. sqlite3-license-version.js := $(dir.tmp)/sqlite3-license-version.js +# sqlite3-license-version-header.js = JS file containing only the +# license header. sqlite3-license-version-header.js := $(dir.api)/sqlite3-license-version-header.js +# sqlite3-api-build-version.js = generated JS file which populates the +# sqlite3.version object using $(bin.version-info). sqlite3-api-build-version.js := $(dir.tmp)/sqlite3-api-build-version.js -# sqlite3-api.jses = the list of JS files which make up $(sqlite3-api.js), in -# the order they need to be assembled. +# sqlite3-api.jses = the list of JS files which make up +# $(sqlite3-api.js.in), in the order they need to be assembled. sqlite3-api.jses := $(sqlite3-license-version.js) sqlite3-api.jses += $(dir.api)/sqlite3-api-prologue.js sqlite3-api.jses += $(dir.common)/whwasmutil.js @@ -217,7 +297,8 @@ sqlite3-api.jses += $(dir.api)/sqlite3-api-glue.js sqlite3-api.jses += $(sqlite3-api-build-version.js) sqlite3-api.jses += $(dir.api)/sqlite3-api-oo1.js sqlite3-api.jses += $(dir.api)/sqlite3-api-worker1.js -sqlite3-api.jses += $(dir.api)/sqlite3-api-opfs.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-api-cleanup.js # "External" API files which are part of our distribution @@ -225,17 +306,23 @@ sqlite3-api.jses += $(dir.api)/sqlite3-api-cleanup.js SOAP.js := $(dir.api)/sqlite3-opfs-async-proxy.js sqlite3-worker1.js := $(dir.api)/sqlite3-worker1.js sqlite3-worker1-promiser.js := $(dir.api)/sqlite3-worker1-promiser.js -define CP_XAPI +# 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) $(sqlite3-worker1.js) $(sqlite3-worker1-promiser.js),\ - $(eval $(call CP_XAPI,$(X)))) -all: $(sqlite3-api.ext.jses) + $(eval $(call COPY_XAPI,$(X)))) +all quick: $(sqlite3-api.ext.jses) +q: quick -sqlite3-api.js := $(dir.tmp)/sqlite3-api.js -$(sqlite3-api.js): $(sqlite3-api.jses) $(MAKEFILE) +# sqlite3-api.js.in = the generated sqlite3-api.js before it gets +# preprocessed. It contains all of $(sqlite3-api.jses) but none of the +# Emscripten-specific headers and footers. +sqlite3-api.js.in := $(dir.tmp)/sqlite3-api.c-pp.js +$(sqlite3-api.js.in): $(sqlite3-api.jses) $(MAKEFILE) @echo "Making $@..." @for i in $(sqlite3-api.jses); do \ echo "/* BEGIN FILE: $$i */"; \ @@ -252,32 +339,8 @@ $(sqlite3-api-build-version.js): $(bin.version-info) $(MAKEFILE) echo ';'; \ echo '});'; \ } > $@ - -######################################################################## -# --post-js and --pre-js are emcc flags we use to append/prepend JS to -# the generated emscripten module file. -pre-js.js := $(dir.api)/pre-js.js -post-js.js := $(dir.tmp)/post-js.js -post-jses := \ - $(dir.api)/post-js-header.js \ - $(sqlite3-api.js) \ - $(dir.api)/post-js-footer.js -$(post-js.js): $(post-jses) $(MAKEFILE) - @echo "Making $@..." - @for i in $(post-jses); do \ - echo "/* BEGIN FILE: $$i */"; \ - cat $$i; \ - echo "/* END FILE: $$i */"; \ - done > $@ -extern-post-js.js := $(dir.api)/extern-post-js.js -extern-pre-js.js := $(dir.api)/extern-pre-js.js -pre-post-common.flags := \ - --post-js=$(post-js.js) \ - --extern-post-js=$(extern-post-js.js) \ - --extern-pre-js=$(sqlite3-license-version.js) -pre-post-jses.deps := $(post-js.js) \ - $(extern-post-js.js) $(extern-pre-js.js) $(sqlite3-license-version.js) -$(sqlite3-license-version.js): $(sqlite3.h) $(sqlite3-license-version-header.js) $(MAKEFILE) +$(sqlite3-license-version.js): $(sqlite3.h) $(sqlite3-license-version-header.js) \ + $(MAKEFILE) @echo "Making $@..."; { \ cat $(sqlite3-license-version-header.js); \ echo '/*'; \ @@ -289,51 +352,116 @@ $(sqlite3-license-version.js): $(sqlite3.h) $(sqlite3-license-version-header.js) } > $@ ######################################################################## -# call-make-pre-js creates rules for pre-js-$(1).js. $1 = the base -# name of the JS file on whose behalf this pre-js is for. +# --post-js and --pre-js are emcc flags we use to append/prepend JS to +# the generated emscripten module file. The following rules generate +# various versions of those files for the vanilla and ESM builds. +pre-js.js.in := $(dir.api)/pre-js.c-pp.js +pre-js.js.esm := $(dir.tmp)/pre-js.esm.js +pre-js.js.vanilla := $(dir.tmp)/pre-js.vanilla.js +$(eval $(call C-PP.FILTER,$(pre-js.js.in),$(pre-js.js.vanilla),$(c-pp.D.vanilla))) +$(eval $(call C-PP.FILTER,$(pre-js.js.in),$(pre-js.js.esm),$(c-pp.D.esm))) +post-js.js.in := $(dir.tmp)/post-js.c-pp.js +post-js.js.vanilla := $(dir.tmp)/post-js.vanilla.js +post-js.js.esm := $(dir.tmp)/post-js.esm.js +post-jses.js := \ + $(dir.api)/post-js-header.js \ + $(sqlite3-api.js.in) \ + $(dir.api)/post-js-footer.js +$(post-js.js.in): $(post-jses.js) $(MAKEFILE) + @echo "Making $@..." + @for i in $(post-jses.js); do \ + echo "/* BEGIN FILE: $$i */"; \ + cat $$i; \ + echo "/* END FILE: $$i */"; \ + done > $@ +$(eval $(call C-PP.FILTER,$(post-js.js.in),$(post-js.js.vanilla),$(c-pp.D.vanilla))) +$(eval $(call C-PP.FILTER,$(post-js.js.in),$(post-js.js.esm),$(c-pp.D.esm))) + +# extern-post-js* and extern-pre-js* are files for use with +# Emscripten's --extern-pre-js and --extern-post-js flags. These +# rules make different copies for the vanilla and ESM builds. +extern-post-js.js.in := $(dir.api)/extern-post-js.c-pp.js +extern-post-js.js.vanilla := $(dir.tmp)/extern-post-js.vanilla.js +extern-post-js.js.esm := $(dir.tmp)/extern-post-js.esm.js +$(eval $(call C-PP.FILTER,$(extern-post-js.js.in),$(extern-post-js.js.vanilla),$(c-pp.D.vanilla))) +$(eval $(call C-PP.FILTER,$(extern-post-js.js.in),$(extern-post-js.js.esm),$(c-pp.D.esm))) +extern-pre-js.js := $(dir.api)/extern-pre-js.js + +# Emscripten flags for --[extern-][pre|post]-js=... for the +# various builds. +pre-post-common.flags := \ + --extern-pre-js=$(sqlite3-license-version.js) +pre-post-common.flags.vanilla := \ + $(pre-post-common.flags) \ + --post-js=$(post-js.js.vanilla) \ + --extern-post-js=$(extern-post-js.js.vanilla) +pre-post-common.flags.esm := \ + $(pre-post-common.flags) \ + --post-js=$(post-js.js.esm) \ + --extern-post-js=$(extern-post-js.js.esm) + +# pre-post-jses.deps.* = a list of dependencies for the +# --[extern-][pre/post]-js files. +pre-post-jses.deps.common := $(extern-pre-js.js) $(sqlite3-license-version.js) +pre-post-jses.deps.vanilla := $(pre-post-jses.deps.common) \ + $(post-js.js.vanilla) $(extern-post-js.js.vanilla) +pre-post-jses.deps.esm := $(pre-post-jses.deps.common) \ + $(post-js.js.esm) $(extern-post-js.js.esm) + +######################################################################## +# call-make-pre-js 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. $2 is the build mode: one of (vanilla, esm). +# This sets up --[extern-][pre/post]-js flags in +# $(pre-post-$(1).flags.$(2)) and dependencies in +# $(pre-post-$(1).deps.$(2)). define call-make-pre-js -pre-post-$(1).flags ?= -$$(dir.tmp)/pre-js-$(1).js: $$(pre-js.js) $$(MAKEFILE) - cp $$(pre-js.js) $$@ +pre-post-$(1).flags.$(2) ?= +$$(dir.tmp)/pre-js-$(1)-$(2).js: $$(pre-js.js.$(2)) $$(MAKEFILE) + cp $$(pre-js.js.$(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 := $$(pre-post-jses.deps) $$(dir.tmp)/pre-js-$(1).js -pre-post-$(1).flags += --pre-js=$$(dir.tmp)/pre-js-$(1).js +pre-post-$(1).deps.$(2) := \ + $$(pre-post-jses.deps.$(2)) \ + $$(dir.tmp)/pre-js-$(1)-$(2).js +pre-post-$(1).flags.$(2) += \ + $$(pre-post-common.flags.$(2)) \ + --pre-js=$$(dir.tmp)/pre-js-$(1)-$(2).js endef -#$(error $(call call-make-pre-js,sqlite3-wasmfs)) # /post-js and pre-js ######################################################################## ######################################################################## # emcc flags for .c/.o/.wasm/.js. emcc.flags := -#emcc.flags += -v # _very_ loud but also informative about what it's doing -# -g3 is needed to keep -O2 and higher from creating broken JS via -# minification. +ifeq (1,$(emcc.verbose)) +emcc.flags += -v +# -v is _very_ loud but also informative about what it's doing +endif ######################################################################## # emcc flags for .c/.o. emcc.cflags := emcc.cflags += -std=c99 -fPIC -# -------------^^^^^^^^ we currently need c99 for WASM-specific sqlite3 APIs. +# -------------^^^^^^^^ we need c99 for $(sqlite3-wasm.c). emcc.cflags += -I. -I$(dir.top) ######################################################################## -# emcc flags specific to building the final .js/.wasm file... +# emcc flags specific to building .js/.wasm files... emcc.jsflags := -fPIC emcc.jsflags += --minify 0 emcc.jsflags += --no-entry +emcc.jsflags += -sWASM_BIGINT=$(emcc.WASM_BIGINT) emcc.jsflags += -sMODULARIZE emcc.jsflags += -sSTRICT_JS emcc.jsflags += -sDYNAMIC_EXECUTION=0 emcc.jsflags += -sNO_POLYFILL emcc.jsflags += -sEXPORTED_FUNCTIONS=@$(EXPORTED_FUNCTIONS.api) emcc.exportedRuntimeMethods := \ - -sEXPORTED_RUNTIME_METHODS=FS,wasmMemory - # FS ==> stdio/POSIX I/O proxies + -sEXPORTED_RUNTIME_METHODS=wasmMemory # wasmMemory ==> required by our code for use with -sIMPORTED_MEMORY emcc.jsflags += $(emcc.exportedRuntimeMethods) emcc.jsflags += -sUSE_CLOSURE_COMPILER=0 @@ -366,33 +494,48 @@ emcc.jsflags += -sINITIAL_MEMORY=$(emcc.INITIAL_MEMORY.$(emcc.INITIAL_MEMORY)) ######################################################################## emcc.jsflags += $(emcc.environment) -#emcc.jsflags += -sTOTAL_STACK=4194304 - +emcc.jsflags += -sSTACK_SIZE=512KB +# ^^^ ACHTUNG: emsdk 3.1.27 reduced the default stack size from 5MB to +# a mere 64KB, which leads to silent memory corruption via the kvvfs +# VFS, which requires twice that for its xRead() and xWrite() methods. +######################################################################## +# $(sqlite3.js.init-func) is the name Emscripten assigns our exported +# module init/load function. This symbol name is hard-coded in +# $(extern-post-js.js) as well as in numerous docs. +# +# "sqlite3InitModule" is the symbol we document for client use, so +# that's the symbol name which must be exported, whether it comes from +# Emscripten or our own code in extern-post-js.js. +# +# That said... we can change $(sqlite3.js.init-func) as long as the +# name "sqlite3InitModule" is the one which gets exposed via the +# resulting JS files. That can be accomplished via +# extern-post-js.js. However... using a temporary symbol name here +# and then adding sqlite3InitModule() ourselves results in 2 global +# symbols: we cannot "delete" the Emscripten-defined +# $(sqlite3.js.init-func) because it's declared with "var". sqlite3.js.init-func := sqlite3InitModule -# ^^^^ $(sqlite3.js.init-func) symbol name is hard-coded in -# $(extern-post-js.js) as well as in numerous docs. If changed, it -# needs to be globally modified in *.js and all related documentation. - emcc.jsflags += -sEXPORT_NAME=$(sqlite3.js.init-func) emcc.jsflags += -sGLOBAL_BASE=4096 # HYPOTHETICALLY keep func table indexes from overlapping w/ heap addr. #emcc.jsflags += -sSTRICT # fails due to missing __syscall_...() #emcc.jsflags += -sALLOW_UNIMPLEMENTED_SYSCALLS #emcc.jsflags += -sFILESYSTEM=0 # only for experimentation. sqlite3 needs the FS API -#emcc.jsflags += -sABORTING_MALLOC +#emcc.jsflags += -sABORTING_MALLOC # only for experimentation emcc.jsflags += -sALLOW_TABLE_GROWTH -# -sALLOW_TABLE_GROWTH is required for installing new SQL UDFs +# ^^^^ -sALLOW_TABLE_GROWTH is required for installing new SQL UDFs emcc.jsflags += -Wno-limited-postlink-optimizations -# ^^^^^ it likes to warn when we have "limited optimizations" via the -g3 flag. -#emcc.jsflags += -sSTANDALONE_WASM # causes OOM errors, not sure why -# https://lld.llvm.org/WebAssembly.html -emcc.jsflags += -sERROR_ON_UNDEFINED_SYMBOLS=0 +# ^^^^ emcc likes to warn when we have "limited optimizations" via the +# -g3 flag. +# emcc.jsflags += -sSTANDALONE_WASM # causes OOM errors, not sure why. + +# Re. undefined symbol handling, see: https://lld.llvm.org/WebAssembly.html +emcc.jsflags += -sERROR_ON_UNDEFINED_SYMBOLS=1 emcc.jsflags += -sLLD_REPORT_UNDEFINED #emcc.jsflags += --allow-undefined #emcc.jsflags += --import-undefined #emcc.jsflags += --unresolved-symbols=import-dynamic --experimental-pic #emcc.jsflags += --experimental-pic --unresolved-symbols=ingore-all --import-undefined #emcc.jsflags += --unresolved-symbols=ignore-all -emcc.jsflags += -sWASM_BIGINT=$(emcc.WASM_BIGINT) ######################################################################## # -sMEMORY64=1 fails to load, erroring with: @@ -404,34 +547,28 @@ emcc.jsflags += -sWASM_BIGINT=$(emcc.WASM_BIGINT) # new Uint8Array(wasm.heap8u().buffer, ptr, n) # # because ptr is now a BigInt, so is invalid for passing to arguments -# which have strict must-be-a-Number requirements. +# which have strict must-be-a-Number requirements. That aspect will +# make any eventual port to 64-bit address space extremely painful, as +# such constructs are found all over the place in the source code. ######################################################################## ######################################################################## # -sSINGLE_FILE: -# https://github.com/emscripten-core/emscripten/blob/main/src/settings.js#L1704 -# -sSINGLE_FILE=1 would be really nice but we have to build with -g3 +# https://github.com/emscripten-core/emscripten/blob/main/src/settings.js +# +# -sSINGLE_FILE=1 would be _really_ nice but we have to build with -g3 # for -O2 and higher to work (else minification breaks the code) and # cannot wasm-strip the binary before it gets encoded into the JS -# file. The result is that the generated JS file is, because of the -g3 -# debugging info, _huge_. +# file. The result is that the generated JS file is, because of the +# -g3 debugging info, _huge_. ######################################################################## -######################################################################## -# AN EXPERIMENT: undocumented Emscripten feature: if the target file -# extension is "mjs", it defaults to ES6 module builds: +sqlite3.js := $(dir.dout)/sqlite3.js +sqlite3.mjs := $(dir.dout)/sqlite3.mjs +# Undocumented Emscripten feature: if the target file extension is +# "mjs", it defaults to ES6 module builds: # https://github.com/emscripten-core/emscripten/issues/14383 -ifeq (,$(filter esm,$(MAKECMDGOALS))) -sqlite3.js.ext := js -else -esm.deps := $(filter-out esm,$(MAKECMDGOALS)) -esm: $(if $(esm.deps),$(esm.deps),all) -sqlite3.js.ext := mjs -endif -# /esm -######################################################################## -sqlite3.js := $(dir.dout)/sqlite3.$(sqlite3.js.ext) sqlite3.wasm := $(dir.dout)/sqlite3.wasm sqlite3-wasm.c := $(dir.api)/sqlite3-wasm.c # sqlite3-wasm.o vs sqlite3-wasm.c: building against the latter @@ -439,27 +576,78 @@ sqlite3-wasm.c := $(dir.api)/sqlite3-wasm.c # enough to the target speed requirements that the 500ms makes a # difference. Thus we build all binaries against sqlite3-wasm.c # instead of building a shared copy of sqlite3-wasm.o. -$(eval $(call call-make-pre-js,sqlite3)) -$(sqlite3.js): -$(sqlite3.js): $(MAKEFILE) $(sqlite3.wasm.obj) \ - $(EXPORTED_FUNCTIONS.api) \ - $(pre-post-sqlite3.deps) +$(eval $(call call-make-pre-js,sqlite3,vanilla)) +$(eval $(call call-make-pre-js,sqlite3,esm)) +$(sqlite3.js) $(sqlite3.mjs): $(MAKEFILE) $(sqlite3-wasm.c) \ + $(EXPORTED_FUNCTIONS.api) +$(sqlite3.js): $(pre-post-sqlite3.deps.vanilla) +$(sqlite3.mjs): $(pre-post-sqlite3.deps.esm) +######################################################################## +# SQLITE3.xJS.RECIPE = the $(call)able recipe body for $(sqlite3.js) +# and $(sqlite3.mjs). $1 = one of (vanilla, esm). +# +# Reminder for ESM builds: even if we use -sEXPORT_ES6=0, emcc _still_ +# adds: +# +# export default $(sqlite3.js.init-func); +# +# when building *.mjs, which is bad because we need to export an +# overwritten version of that function and cannot "export default" +# twice. Because of this, we have to sed $(sqlite3.mjs) to remove the +# _first_ instance (only) of /^export default/. +# +# Upstream RFE: +# https://github.com/emscripten-core/emscripten/issues/18237 +######################################################################## +# SQLITE3.xJS.EXPORT-DEFAULT is part of SQLITE3[-WASMFS].xJS.RECIPE, +# factored into a separate piece to avoid code duplication. $1 is +# the build mode: one of (vanilla, esm). +define SQLITE3.xJS.ESM-EXPORT-DEFAULT +if [ esm = $(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; \ + fi; \ +fi +endef +define SQLITE3.xJS.RECIPE @echo "Building $@ ..." $(emcc.bin) -o $@ $(emcc_opt_full) $(emcc.flags) \ - $(emcc.jsflags) $(pre-post-common.flags) $(pre-post-sqlite3.flags) \ + $(emcc.jsflags) \ + $(pre-post-sqlite3.flags.$(1)) $(emcc.flags.sqlite3.$(1)) \ $(cflags.common) $(SQLITE_OPT) $(sqlite3-wasm.c) + @$(call SQLITE3.xJS.ESM-EXPORT-DEFAULT,$(1)) chmod -x $(sqlite3.wasm) $(maybe-wasm-strip) $(sqlite3.wasm) @ls -la $@ $(sqlite3.wasm) +endef +emcc.flags.sqlite3.vanilla := +emcc.flags.sqlite3.esm := -sEXPORT_ES6 -sUSE_ES6_IMPORT_META +$(sqlite3.js): + $(call SQLITE3.xJS.RECIPE,vanilla) +$(sqlite3.mjs): + $(call SQLITE3.xJS.RECIPE,esm) +######################################################################## +# We have to ensure that we do not build both $(sqlite3.js) and +# $(sqlite3.mjs) in parallel because both result in the creation of +# $(sqlite3.wasm). We have no(?) way to build just the .mjs file +# without also building the .wasm file. i.e. we're building +# $(sqlite3.wasm) twice, but that's apparently unavoidable (and +# harmless, just a waste of build time). $(sqlite3.wasm): $(sqlite3.js) -CLEAN_FILES += $(sqlite3.js) $(sqlite3.wasm) -all: $(sqlite3.js) -wasm: $(sqlite3.js) -# End main Emscripten-based module build +$(sqlite3.mjs): $(sqlite3.js) +CLEAN_FILES += $(sqlite3.js) $(sqlite3.mjs) $(sqlite3.wasm) +all: $(sqlite3.js) $(sqlite3.mjs) +quick: $(sqlite3.js) +quick: $(sqlite3.mjs) # for the sake of the snapshot build +# End main $(sqlite3.js) build ######################################################################## ######################################################################## -# batch-runner.js... +# batch-runner.js is part of one of the test apps which reads in SQL +# dumps generated by $(speedtest1) and executes them. dir.sql := sql speedtest1 := ../../speedtest1 speedtest1.c := ../../test/speedtest1.c @@ -481,30 +669,31 @@ batch: batch-runner.list all: batch # end batch-runner.js ######################################################################## -# speedtest1.js... -# speedtest1-common.eflags = emcc flags used by multiple builds of speedtest1 -# speedtest1.eflags = emcc flags used by main build of speedtest1 -speedtest1-common.eflags := $(emcc_opt_full) -speedtest1.eflags := -speedtest1.eflags += -sENVIRONMENT=web -speedtest1.eflags += -sALLOW_MEMORY_GROWTH -speedtest1.eflags += -sINITIAL_MEMORY=$(emcc.INITIAL_MEMORY.$(emcc.INITIAL_MEMORY)) -speedtest1-common.eflags += -sINVOKE_RUN=0 -speedtest1-common.eflags += --no-entry -#speedtest1-common.eflags += -flto -speedtest1-common.eflags += -sABORTING_MALLOC -speedtest1-common.eflags += -sSTRICT_JS -speedtest1-common.eflags += -sMODULARIZE -speedtest1-common.eflags += -Wno-limited-postlink-optimizations +# Wasmified speedtest1 is our primary benchmarking tool. +# +# emcc.speedtest1.common = emcc flags used by multiple builds of speedtest1 +# emcc.speedtest1 = emcc flags used by main build of speedtest1 +emcc.speedtest1.common := $(emcc_opt_full) +emcc.speedtest1 := +emcc.speedtest1 += -sENVIRONMENT=web +emcc.speedtest1 += -sALLOW_MEMORY_GROWTH +emcc.speedtest1 += -sINITIAL_MEMORY=$(emcc.INITIAL_MEMORY.$(emcc.INITIAL_MEMORY)) +emcc.speedtest1.common += -sINVOKE_RUN=0 +emcc.speedtest1.common += --no-entry +#emcc.speedtest1.common += -flto +emcc.speedtest1.common += -sABORTING_MALLOC +emcc.speedtest1.common += -sSTRICT_JS +emcc.speedtest1.common += -sMODULARIZE +emcc.speedtest1.common += -Wno-limited-postlink-optimizations EXPORTED_FUNCTIONS.speedtest1 := $(abspath $(dir.tmp)/EXPORTED_FUNCTIONS.speedtest1) -speedtest1-common.eflags += -sEXPORTED_FUNCTIONS=@$(EXPORTED_FUNCTIONS.speedtest1) -speedtest1-common.eflags += $(emcc.exportedRuntimeMethods) -speedtest1-common.eflags += -sALLOW_TABLE_GROWTH -speedtest1-common.eflags += -sDYNAMIC_EXECUTION=0 -speedtest1-common.eflags += --minify 0 -speedtest1-common.eflags += -sEXPORT_NAME=$(sqlite3.js.init-func) -speedtest1-common.eflags += -sWASM_BIGINT=$(emcc.WASM_BIGINT) -speedtest1-common.eflags += $(pre-post-common.flags) +emcc.speedtest1.common += -sSTACK_SIZE=512KB +emcc.speedtest1.common += -sEXPORTED_FUNCTIONS=@$(EXPORTED_FUNCTIONS.speedtest1) +emcc.speedtest1.common += $(emcc.exportedRuntimeMethods) +emcc.speedtest1.common += -sALLOW_TABLE_GROWTH +emcc.speedtest1.common += -sDYNAMIC_EXECUTION=0 +emcc.speedtest1.common += --minify 0 +emcc.speedtest1.common += -sEXPORT_NAME=$(sqlite3.js.init-func) +emcc.speedtest1.common += -sWASM_BIGINT=$(emcc.WASM_BIGINT) speedtest1.exit-runtime0 := -sEXIT_RUNTIME=0 speedtest1.exit-runtime1 := -sEXIT_RUNTIME=1 # Re -sEXIT_RUNTIME=1 vs 0: if it's 1 and speedtest1 crashes, we get @@ -528,17 +717,17 @@ $(EXPORTED_FUNCTIONS.speedtest1): $(EXPORTED_FUNCTIONS.api) @echo "Making $@ ..." @{ echo _wasm_main; cat $(EXPORTED_FUNCTIONS.api); } > $@ speedtest1.js := $(dir.dout)/speedtest1.js -speedtest1.wasm := $(subst .js,.wasm,$(speedtest1.js)) -speedtest1.cflags := $(cflags.common) -DSQLITE_SPEEDTEST1_WASM +speedtest1.wasm := $(dir.dout)/speedtest1.wasm +cflags.speedtest1 := $(cflags.common) -DSQLITE_SPEEDTEST1_WASM speedtest1.cses := $(speedtest1.c) $(sqlite3-wasm.c) -$(eval $(call call-make-pre-js,speedtest1)) +$(eval $(call call-make-pre-js,speedtest1,vanilla)) $(speedtest1.js): $(MAKEFILE) $(speedtest1.cses) \ - $(pre-post-speedtest1.deps) \ + $(pre-post-speedtest1.deps.vanilla) \ $(EXPORTED_FUNCTIONS.speedtest1) @echo "Building $@ ..." $(emcc.bin) \ - $(speedtest1.eflags) $(speedtest1-common.eflags) $(speedtest1.cflags) \ - $(pre-post-speedtest1.flags) \ + $(emcc.speedtest1) $(emcc.speedtest1.common) \ + $(cflags.speedtest1) $(pre-post-speedtest1.flags.vanilla) \ $(SQLITE_OPT) \ $(speedtest1.exit-runtime0) \ -o $@ $(speedtest1.cses) -lm @@ -551,6 +740,29 @@ CLEAN_FILES += $(speedtest1.js) $(speedtest1.wasm) # end speedtest1.js ######################################################################## +######################################################################## +# tester1 is the main unit and regression test application and needs +# to be able to run in 4 separate modes to cover the primary +# client-side use cases: +# +# 1) Load sqlite3 in the main UI thread of a conventional script. +# 2) Load sqlite3 in a conventional Worker thread. +# 3) Load sqlite3 as an ES6 module (ESM) in the main thread. +# 4) Load sqlite3 as an ESM worker. (Not all browsers support this.) +# +# To that end, we require two separate builds of tester1.js: +# +# tester1.js: cases 1 and 2 +# tester1.mjs: cases 3 and 4 +# +# To create those, we filter tester1.c-pp.js with $(bin.c-pp)... +$(eval $(call C-PP.FILTER,tester1.c-pp.js,tester1.js)) +$(eval $(call C-PP.FILTER,tester1.c-pp.js,tester1.mjs,$(c-pp.D.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))) +tester1: tester1.js tester1.mjs tester1.html tester1-esm.html +all quick: tester1 + ######################################################################## # Convenience rules to rebuild with various -Ox levels. Much # experimentation shows -O2 to be the clear winner in terms of speed. @@ -558,37 +770,47 @@ CLEAN_FILES += $(speedtest1.js) $(speedtest1.wasm) # painful. .PHONY: o0 o1 o2 o3 os oz -o-xtra := -flto +o-xtra := +#o-xtra ?= -flto # ^^^^ -flto can have a considerably performance boost at -O0 but -# doubles the build time and seems to have negligible effect on -# higher optimization levels. +# doubles the build time and seems to have negligible, if any, effect +# on higher optimization levels. o0: clean $(MAKE) -e "emcc_opt=-O0" o1: clean $(MAKE) -e "emcc_opt=-O1 $(o-xtra)" o2: clean - $(MAKE) -e "emcc_opt=-O2 $(o-xtra)" + $(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 @echo "WARNING: -Os can result in a build with mysteriously missing pieces!" $(MAKE) -e "emcc_opt=-Os $(o-xtra)" oz: clean - $(MAKE) -e "emcc_opt=-Oz $(o-xtra)" + $(MAKE) -j2 -e "emcc_opt=-Oz $(o-xtra)" +qoz: clean + $(MAKE) -j2 -e "emcc_opt=-Oz $(o-xtra)" quick ######################################################################## # Sub-makes... +# sqlite.org/fiddle application... include fiddle.make # Only add wasmfs if wasmfs.enable=1 or we're running (dist)clean +ifneq (,$(filter wasmfs,$(MAKECMDGOALS))) +wasmfs.enable ?= 1 +else wasmfs.enable ?= $(if $(filter %clean,$(MAKECMDGOALS)),1,0) +endif ifeq (1,$(wasmfs.enable)) # wasmfs build disabled 2022-10-19 per /chat discussion. # OPFS-over-wasmfs was initially a stopgap measure and a convenient # point of comparison for the OPFS sqlite3_vfs's performance, but it # currently doubles our deliverables and build maintenance burden for -# little, if any, benefit. +# little benefit. # ######################################################################## # Some platforms do not support the WASMFS build. Raspberry Pi OS is one @@ -607,16 +829,12 @@ endif # /wasmfs ######################################################################## -######################################################################## -# Create deliverables: -ifneq (,$(filter dist,$(MAKECMDGOALS))) -include dist.make -endif - ######################################################################## # Push files to public wasm-testing.sqlite.org server -wasm-testing.include = $(dir.dout) *.js *.html \ - batch-runner.list $(dir.sql) $(dir.common) $(dir.fiddle) $(dir.jacc) +wasm-testing.include = *.js *.mjs *.html \ + ./tests \ + batch-runner.list \ + $(dir.dout) $(dir.sql) $(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) @@ -633,23 +851,35 @@ push-testing: ######################################################################## # If we find a copy of the sqlite.org/wasm docs checked out, copy # certain files over to it, noting that some need automatable edits... -WDOCS.home ?= ../../../wdoc +wasm.docs.home ?= ../../../wasm +wasm.docs.found = $(if $(wildcard $(wasm.docs.home)/api-index.md),\ + $(wildcard $(wasm.docs.home)),) .PHONY: update-docs -ifneq (,$(wildcard $(WDOCS.home)/api-index.md)) -WDOCS.jswasm := $(WDOCS.home)/jswasm -update-docs: $(bin.stripccomments) $(sqlite3.js) $(sqlite3.wasm) - @echo "Copying files to the /wasm docs. Be sure to use an -Oz build for this!" - cp $(sqlite3.wasm) $(WDOCS.jswasm)/. - $(bin.stripccomments) -k -k < $(sqlite3.js) \ - | sed -e '/^[ \t]*$$/d' > $(WDOCS.jswasm)/sqlite3.js - cp demo-123.js demo-123.html demo-123-worker.html $(WDOCS.home) - sed -n -e '/EXTRACT_BEGIN/,/EXTRACT_END/p' \ - module-symbols.html > $(WDOCS.home)/module-symbols.html -else +ifeq (,$(wasm.docs.found)) update-docs: @echo "Cannot find wasm docs checkout."; \ - echo "Pass WDOCS.home=/path/to/wasm/docs/checkout or edit this makefile to suit."; \ + echo "Pass wasm.docs.home=/path/to/wasm/docs/checkout or edit this makefile to suit."; \ exit 127 +else +wasm.docs.jswasm := $(wasm.docs.home)/jswasm +update-docs: $(bin.stripccomments) $(sqlite3.js) $(sqlite3.wasm) + @echo "Copying files to the /wasm docs. Be sure to use an -Oz build for this!" + cp $(sqlite3.wasm) $(wasm.docs.jswasm)/. + $(bin.stripccomments) -k -k < $(sqlite3.js) \ + | sed -e '/^[ \t]*$$/d' > $(wasm.docs.jswasm)/sqlite3.js + cp demo-123.js demo-123.html demo-123-worker.html $(wasm.docs.home) + sed -n -e '/EXTRACT_BEGIN/,/EXTRACT_END/p' \ + module-symbols.html > $(wasm.docs.home)/module-symbols.html endif # end /wasm docs ######################################################################## + +######################################################################## +# Create main client downloadable zip file: +ifneq (,$(filter dist snapshot,$(MAKECMDGOALS))) +include dist.make +endif + +# Run local web server for the test/demo pages. +httpd: + althttpd -max-age 1 -enable-sab -page index.html diff --git a/ext/wasm/api/README.md b/ext/wasm/api/README.md index 6440eba996..6618666ae1 100644 --- a/ext/wasm/api/README.md +++ b/ext/wasm/api/README.md @@ -17,19 +17,20 @@ multiple JS files because: to be loaded as JS Workers. Note that the structure described here is the current state of things, -not necessarily the "final" state. +as of this writing, but is not set in stone forever and may change +at any time. The overall idea is that the following files get concatenated together, in the listed order, the resulting file is loaded by a browser client: -- `sqlite3-api-prologue.js`\ +- **`sqlite3-api-prologue.js`**\ Contains the initial bootstrap setup of the sqlite3 API objects. This is exposed as a function, rather than objects, so that the next step can pass in a config object which abstracts away parts of the WASM environment, to facilitate plugging it in to arbitrary WASM toolchains. -- `../common/whwasmutil.js`\ +- **`../common/whwasmutil.js`**\ A semi-third-party collection of JS/WASM utility code intended to replace much of the Emscripten glue. The sqlite3 APIs internally use these APIs instead of their Emscripten counterparts, in order to be @@ -38,47 +39,54 @@ browser client: toolchains. It is "semi-third-party" in that it was created in order to support this tree but is standalone and maintained together with... -- `../jaccwabyt/jaccwabyt.js`\ +- **`../jaccwabyt/jaccwabyt.js`**\ Another semi-third-party API which creates bindings between JS and C structs, such that changes to the struct state from either JS or C are visible to the other end of the connection. This is also an independent spinoff project, conceived for the sqlite3 project but maintained separately. -- `sqlite3-api-glue.js`\ - Invokes functionality exposed by the previous two files to - flesh out low-level parts of `sqlite3-api-prologue.js`. Most of - these pieces related to the `sqlite3.capi.wasm` object. -- `sqlite3-api-build-version.js`\ +- **`sqlite3-api-glue.js`**\ + Invokes functionality exposed by the previous two files to flesh out + low-level parts of `sqlite3-api-prologue.js`. Most of these pieces + related to populating the `sqlite3.capi.wasm` object. This file + also deletes most global-scope symbols the above files create, + effectively moving them into the scope being used for initializing + the API. +- **`/sqlite3-api-build-version.js`**\ Gets created by the build process and populates the `sqlite3.version` object. This part is not critical, but records the version of the library against which this module was built. -- `sqlite3-api-oo1.js`\ +- **`sqlite3-api-oo1.js`**\ Provides a high-level object-oriented wrapper to the lower-level C API, colloquially known as OO API #1. Its API is similar to other high-level sqlite3 JS wrappers and should feel relatively familiar to anyone familiar with such APIs. That said, it is not a "required component" and can be elided from builds which do not want it. -- `sqlite3-api-worker1.js`\ +- **`sqlite3-api-worker1.js`**\ A Worker-thread-based API which uses OO API #1 to provide an interface to a database which can be driven from the main Window thread via the Worker message-passing interface. Like OO API #1, this is an optional component, offering one of any number of potential implementations for such an API. - - `sqlite3-worker1.js`\ + - **`sqlite3-worker1.js`**\ Is not part of the amalgamated sources and is intended to be loaded by a client Worker thread. It loads the sqlite3 module and runs the Worker #1 API which is implemented in `sqlite3-api-worker1.js`. - - `sqlite3-worker1-promiser.js`\ + - **`sqlite3-worker1-promiser.js`**\ Is likewise not part of the amalgamated sources and provides a Promise-based interface into the Worker #1 API. This is a far user-friendlier way to interface with databases running in a Worker thread. -- `sqlite3-api-opfs.js`\ +- **`sqlite3-v-helper.js`**\ + Installs `sqlite3.vfs` and `sqlite3.vtab`, namespaces which contain + helpers for use by downstream code which creates `sqlite3_vfs` + and `sqlite3_module` implementations. +- **`sqlite3-vfs-opfs.c-pp.js`**\ is an sqlite3 VFS implementation which supports Google Chrome's Origin-Private FileSystem (OPFS) as a storage layer to provide persistent storage for database files in a browser. It requires... - - `sqlite3-opfs-async-proxy.js`\ + - **`sqlite3-opfs-async-proxy.js`**\ is the asynchronous backend part of the OPFS proxy. It speaks directly to the (async) OPFS API and channels those results back to its synchronous counterpart. This file, because it must be @@ -95,6 +103,13 @@ browser client: symbol installed. When adapting the API for non-Emscripten toolchains, this "should" be the only file where changes are needed. + +**Files with the extension `.c-pp.js`** are intended [to be processed +with `c-pp`](#c-pp), noting that such preprocessing may be applied +after all of the relevant files are concatenated. That extension is +used primarily to keep the code maintainers cognisant of the fact that +those files contain constructs which will not run as-is in JavaScript. + The build process glues those files together, resulting in `sqlite3-api.js`, which is everything except for the `post-js-*.js` files, and `sqlite3.js`, which is the Emscripten-generated amalgamated @@ -114,7 +129,7 @@ into the build-generated `sqlite3.js` along with `sqlite3-api.js`. Emscripten-specific header for Emscripten's `--extern-pre-js` flag. As of this writing, that file is only used for experimentation purposes and holds no code relevant to the production deliverables. -- `pre-js.js`\ +- `pre-js.c-pp.js`\ Emscripten-specific header for Emscripten's `--pre-js` flag. This file is intended as a place to override certain Emscripten behavior before it starts up, but corner-case Emscripten bugs keep that from @@ -125,9 +140,20 @@ into the build-generated `sqlite3.js` along with `sqlite3-api.js`. - `post-js-footer.js`\ Emscripten-specific footer for the `--post-js` input. This closes off the lexical scope opened by `post-js-header.js`. -- `extern-post-js.js`\ +- `extern-post-js.c-pp.js`\ Emscripten-specific header for Emscripten's `--extern-post-js` flag. This file overwrites the Emscripten-installed `sqlite3InitModule()` function with one which, after the module is loaded, also initializes the asynchronous parts of the sqlite3 module. For example, the OPFS VFS support. + + +Preprocessing of Source Files +------------------------------------------------------------------------ + +Certain files in the build require preprocessing to filter in/out +parts which differ between vanilla JS builds and ES6 Module +(a.k.a. esm) builds. The preprocessor application itself is in +[`c-pp.c`](/file/ext/wasm/c-pp.c) and the complete technical details +of such preprocessing are maintained in +[`GNUMakefile`](/file/ext/wasm/GNUmakefile). diff --git a/ext/wasm/api/extern-post-js.js b/ext/wasm/api/extern-post-js.c-pp.js similarity index 74% rename from ext/wasm/api/extern-post-js.js rename to ext/wasm/api/extern-post-js.c-pp.js index 84b99b53a6..2258697944 100644 --- a/ext/wasm/api/extern-post-js.js +++ b/ext/wasm/api/extern-post-js.c-pp.js @@ -1,21 +1,32 @@ + +/* ^^^^ ACHTUNG: blank line at the start is necessary because + Emscripten will not add a newline in some cases and we need + a blank line for a sed-based kludge for the ES6 build. */ /* extern-post-js.js must be appended to the resulting sqlite3.js file. It gets its name from being used as the value for the --extern-post-js=... Emscripten flag. Note that this code, unlike most of the associated JS code, runs outside of the Emscripten-generated module init scope, in the current global scope. */ +//#if target=es6-module +const toExportForES6 = +//#endif (function(){ /** - In order to hide the sqlite3InitModule()'s resulting Emscripten - module from downstream clients (and simplify our documentation by - being able to elide those details), we rewrite - sqlite3InitModule() to return the sqlite3 object. + In order to hide the sqlite3InitModule()'s resulting + Emscripten module from downstream clients (and simplify our + documentation by being able to elide those details), we hide that + function and expose a hand-written sqlite3InitModule() to return + the sqlite3 object (most of the time). Unfortunately, we cannot modify the module-loader/exporter-based impls which Emscripten installs at some point in the file above this. */ - const originalInit = self.sqlite3InitModule; + const originalInit = + /* Maintenance reminder: DO NOT use `self.` here. It's correct + for non-ES6 Module cases but wrong for ES6 modules because those + resolve this symbol differently. */ sqlite3InitModule; if(!originalInit){ throw new Error("Expecting self.sqlite3InitModule to be defined by the Emscripten build."); } @@ -49,7 +60,7 @@ initModuleState.sqlite3Dir = li.join('/') + '/'; } - self.sqlite3InitModule = (...args)=>{ + self.sqlite3InitModule = function ff(...args){ //console.warn("Using replaced sqlite3InitModule()",self.location); return originalInit(...args).then((EmscriptenModule)=>{ if(self.window!==self && @@ -65,10 +76,12 @@ Emscripten details. */ return EmscriptenModule; } - EmscriptenModule.sqlite3.scriptInfo = initModuleState; - //console.warn("sqlite3.scriptInfo =",EmscriptenModule.sqlite3.scriptInfo); - const f = EmscriptenModule.sqlite3.asyncPostInit; - delete EmscriptenModule.sqlite3.asyncPostInit; + const s = EmscriptenModule.sqlite3; + s.scriptInfo = initModuleState; + //console.warn("sqlite3.scriptInfo =",s.scriptInfo); + if(ff.__isUnderTest) s.__isUnderTest = true; + const f = s.asyncPostInit; + delete s.asyncPostInit; return f(); }).catch((e)=>{ console.error("Exception loading sqlite3 module:",e); @@ -94,10 +107,15 @@ } /* Replace the various module exports performed by the Emscripten glue... */ - if (typeof exports === 'object' && typeof module === 'object') + if (typeof exports === 'object' && typeof module === 'object'){ module.exports = sqlite3InitModule; - else if (typeof exports === 'object') + }else if (typeof exports === 'object'){ exports["sqlite3InitModule"] = sqlite3InitModule; + } /* AMD modules get injected in a way we cannot override, so we can't handle those here. */ + return self.sqlite3InitModule /* required for ESM */; })(); +//#if target=es6-module +export default toExportForES6; +//#endif diff --git a/ext/wasm/api/post-js-header.js b/ext/wasm/api/post-js-header.js index 82a80e5a17..0e27e1fd94 100644 --- a/ext/wasm/api/post-js-header.js +++ b/ext/wasm/api/post-js-header.js @@ -19,7 +19,8 @@ Module.postRun.push(function(Module/*the Emscripten-style module object*/){ - sqlite3-api-glue.js => glues previous parts together - sqlite3-api-oo.js => SQLite3 OO API #1 - sqlite3-api-worker1.js => Worker-based API - - sqlite3-api-opfs.js => OPFS VFS + - sqlite3-vfs-helper.js => Internal-use utilities for... + - sqlite3-vfs-opfs.js => OPFS VFS - sqlite3-api-cleanup.js => final API cleanup - post-js-footer.js => closes this postRun() function */ diff --git a/ext/wasm/api/pre-js.js b/ext/wasm/api/pre-js.c-pp.js similarity index 95% rename from ext/wasm/api/pre-js.js rename to ext/wasm/api/pre-js.c-pp.js index f31dea1794..2e2fe66bc9 100644 --- a/ext/wasm/api/pre-js.js +++ b/ext/wasm/api/pre-js.c-pp.js @@ -29,6 +29,10 @@ sqlite3InitModuleState.debugModule('self.location =',self.location); 4) If none of the above apply, (prefix+path) is returned. */ Module['locateFile'] = function(path, prefix) { +//#if target=es6-module + return new URL(path, import.meta.url).href; +//#else + 'use strict'; let theFile; const up = this.urlParams; if(up.has(path)){ @@ -47,6 +51,7 @@ Module['locateFile'] = function(path, prefix) { "result =", theFile ); return theFile; +//#endif /* SQLITE_JS_EMS */ }.bind(sqlite3InitModuleState); /** @@ -65,7 +70,7 @@ Module[xNameOfInstantiateWasm] = function callee(imports,onSuccess){ const uri = Module.locateFile( callee.uri, ( ('undefined'===typeof scriptDirectory/*var defined by Emscripten glue*/) - ? '' : scriptDirectory) + ? "" : scriptDirectory) ); sqlite3InitModuleState.debugModule( "instantiateWasm() uri =", uri diff --git a/ext/wasm/api/sqlite3-api-cleanup.js b/ext/wasm/api/sqlite3-api-cleanup.js index bef4d91d70..30cd64b053 100644 --- a/ext/wasm/api/sqlite3-api-cleanup.js +++ b/ext/wasm/api/sqlite3-api-cleanup.js @@ -22,12 +22,10 @@ if('undefined' !== typeof Module){ // presumably an Emscripten build */ const SABC = Object.assign( Object.create(null), { - Module: Module /* ==> Currently needs to be exposed here for - test code. NOT part of the public API. */, exports: Module['asm'], memory: Module.wasmMemory /* gets set if built with -sIMPORT_MEMORY */ }, - self.sqlite3ApiConfig || Object.create(null) + self.sqlite3ApiConfig || {} ); /** @@ -58,8 +56,6 @@ if('undefined' !== typeof Module){ // presumably an Emscripten build self.S = sqlite3; } - /* Clean up temporary references to our APIs... */ - delete sqlite3.util /* arguable, but these are (currently) internal-use APIs */; Module.sqlite3 = sqlite3 /* Needed for customized sqlite3InitModule() to be able to pass the sqlite3 object off to the client. */; }else{ diff --git a/ext/wasm/api/sqlite3-api-glue.js b/ext/wasm/api/sqlite3-api-glue.js index 86aa1d1813..59b40786ec 100644 --- a/ext/wasm/api/sqlite3-api-glue.js +++ b/ext/wasm/api/sqlite3-api-glue.js @@ -24,6 +24,259 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ self.WhWasmUtilInstaller(wasm); delete self.WhWasmUtilInstaller; + /** + Signatures for the WASM-exported C-side functions. Each entry + is an array with 2+ elements: + + [ "c-side name", + "result type" (wasm.xWrap() syntax), + [arg types in xWrap() syntax] + // ^^^ this needn't strictly be an array: it can be subsequent + // elements instead: [x,y,z] is equivalent to x,y,z + ] + + Note that support for the API-specific data types in the + result/argument type strings gets plugged in at a later phase in + the API initialization process. + */ + wasm.bindingSignatures = [ + // Please keep these sorted by function name! + ["sqlite3_aggregate_context","void*", "sqlite3_context*", "int"], + ["sqlite3_bind_blob","int", "sqlite3_stmt*", "int", "*", "int", "*" + /* TODO: we should arguably write a custom wrapper which knows + how to handle Blob, TypedArrays, and JS strings. */ + ], + ["sqlite3_bind_double","int", "sqlite3_stmt*", "int", "f64"], + ["sqlite3_bind_int","int", "sqlite3_stmt*", "int", "int"], + ["sqlite3_bind_null",undefined, "sqlite3_stmt*", "int"], + ["sqlite3_bind_parameter_count", "int", "sqlite3_stmt*"], + ["sqlite3_bind_parameter_index","int", "sqlite3_stmt*", "string"], + ["sqlite3_bind_pointer", "int", + "sqlite3_stmt*", "int", "*", "string:static", "*"], + ["sqlite3_bind_text","int", "sqlite3_stmt*", "int", "string", "int", "int" + /* We should arguably create a hand-written binding of + bind_text() which does more flexible text conversion, along + the lines of sqlite3_prepare_v3(). The slightly problematic + part is the final argument (text destructor). */ + ], + //["sqlite3_busy_handler","int", "sqlite3*", "*", "*"], + // ^^^^ TODO: custom binding which auto-converts JS function arg + // to a WASM function, noting that calling it multiple times + // would introduce a leak. + ["sqlite3_busy_timeout","int", "sqlite3*", "int"], + ["sqlite3_close_v2", "int", "sqlite3*"], + ["sqlite3_changes", "int", "sqlite3*"], + ["sqlite3_clear_bindings","int", "sqlite3_stmt*"], + ["sqlite3_collation_needed", "int", "sqlite3*", "*", "*"/*=>v(ppis)*/], + ["sqlite3_column_blob","*", "sqlite3_stmt*", "int"], + ["sqlite3_column_bytes","int", "sqlite3_stmt*", "int"], + ["sqlite3_column_count", "int", "sqlite3_stmt*"], + ["sqlite3_column_double","f64", "sqlite3_stmt*", "int"], + ["sqlite3_column_int","int", "sqlite3_stmt*", "int"], + ["sqlite3_column_name","string", "sqlite3_stmt*", "int"], + ["sqlite3_column_text","string", "sqlite3_stmt*", "int"], + ["sqlite3_column_type","int", "sqlite3_stmt*", "int"], + ["sqlite3_column_value","sqlite3_value*", "sqlite3_stmt*", "int"], + ["sqlite3_compileoption_get", "string", "int"], + ["sqlite3_compileoption_used", "int", "string"], + ["sqlite3_complete", "int", "string:flexible"], + /* sqlite3_create_function(), sqlite3_create_function_v2(), and + sqlite3_create_window_function() use hand-written bindings to + simplify handling of their function-type arguments. */ + /* sqlite3_create_collation() and sqlite3_create_collation_v2() + use hand-written bindings to simplify passing of the callback + function. + ["sqlite3_create_collation", "int", + "sqlite3*", "string", "int",//SQLITE_UTF8 is the only legal value + "*", "*"], + ["sqlite3_create_collation_v2", "int", + "sqlite3*", "string", "int",//SQLITE_UTF8 is the only legal value + "*", "*", "*"], + */ + ["sqlite3_data_count", "int", "sqlite3_stmt*"], + ["sqlite3_db_filename", "string", "sqlite3*", "string"], + ["sqlite3_db_handle", "sqlite3*", "sqlite3_stmt*"], + ["sqlite3_db_name", "string", "sqlite3*", "int"], + ["sqlite3_db_status", "int", "sqlite3*", "int", "*", "*", "int"], + ["sqlite3_errcode", "int", "sqlite3*"], + ["sqlite3_errmsg", "string", "sqlite3*"], + ["sqlite3_error_offset", "int", "sqlite3*"], + ["sqlite3_errstr", "string", "int"], + /*["sqlite3_exec", "int", "sqlite3*", "string", "*", "*", "**" + Handled seperately to perform translation of the callback + into a WASM-usable one. ],*/ + ["sqlite3_expanded_sql", "string", "sqlite3_stmt*"], + ["sqlite3_extended_errcode", "int", "sqlite3*"], + ["sqlite3_extended_result_codes", "int", "sqlite3*", "int"], + ["sqlite3_file_control", "int", "sqlite3*", "string", "int", "*"], + ["sqlite3_finalize", "int", "sqlite3_stmt*"], + ["sqlite3_free", undefined,"*"], + ["sqlite3_get_auxdata", "*", "sqlite3_context*", "int"], + ["sqlite3_initialize", undefined], + /*["sqlite3_interrupt", undefined, "sqlite3*" + ^^^ we cannot actually currently support this because JS is + single-threaded and we don't have a portable way to access a DB + from 2 SharedWorkers concurrently. ],*/ + ["sqlite3_keyword_count", "int"], + ["sqlite3_keyword_name", "int", ["int", "**", "*"]], + ["sqlite3_keyword_check", "int", ["string", "int"]], + ["sqlite3_libversion", "string"], + ["sqlite3_libversion_number", "int"], + ["sqlite3_limit", "int", ["sqlite3*", "int", "int"]], + ["sqlite3_malloc", "*","int"], + ["sqlite3_open", "int", "string", "*"], + ["sqlite3_open_v2", "int", "string", "*", "int", "string"], + /* sqlite3_prepare_v2() and sqlite3_prepare_v3() are handled + separately due to us requiring two different sets of semantics + for those, depending on how their SQL argument is provided. */ + /* sqlite3_randomness() uses a hand-written wrapper to extend + the range of supported argument types. */ + [ + "sqlite3_progress_handler", undefined, [ + "sqlite3*", "int", new wasm.xWrap.FuncPtrAdapter({ + name: 'xProgressHandler', + signature: 'i(p)', + bindScope: 'context', + contextKey: (argIndex,argv)=>'sqlite3@'+argv[0] + }), "*" + ] + ], + ["sqlite3_realloc", "*","*","int"], + ["sqlite3_reset", "int", "sqlite3_stmt*"], + ["sqlite3_result_blob", undefined, "sqlite3_context*", "*", "int", "*"], + ["sqlite3_result_double", undefined, "sqlite3_context*", "f64"], + ["sqlite3_result_error", undefined, "sqlite3_context*", "string", "int"], + ["sqlite3_result_error_code", undefined, "sqlite3_context*", "int"], + ["sqlite3_result_error_nomem", undefined, "sqlite3_context*"], + ["sqlite3_result_error_toobig", undefined, "sqlite3_context*"], + ["sqlite3_result_int", undefined, "sqlite3_context*", "int"], + ["sqlite3_result_null", undefined, "sqlite3_context*"], + ["sqlite3_result_pointer", undefined, + "sqlite3_context*", "*", "string:static", "*"], + ["sqlite3_result_subtype", undefined, "sqlite3_value*", "int"], + ["sqlite3_result_text", undefined, "sqlite3_context*", "string", "int", "*"], + ["sqlite3_result_zeroblob", undefined, "sqlite3_context*", "int"], + ["sqlite3_set_auxdata", undefined, "sqlite3_context*", "int", "*", "*"/* => v(*) */], + ["sqlite3_shutdown", undefined], + ["sqlite3_sourceid", "string"], + ["sqlite3_sql", "string", "sqlite3_stmt*"], + ["sqlite3_status", "int", "int", "*", "*", "int"], + ["sqlite3_step", "int", "sqlite3_stmt*"], + ["sqlite3_stmt_isexplain", "int", ["sqlite3_stmt*"]], + ["sqlite3_stmt_readonly", "int", ["sqlite3_stmt*"]], + ["sqlite3_stmt_status", "int", "sqlite3_stmt*", "int", "int"], + ["sqlite3_strglob", "int", "string","string"], + ["sqlite3_stricmp", "int", "string", "string"], + ["sqlite3_strlike", "int", "string", "string","int"], + ["sqlite3_strnicmp", "int", "string", "string", "int"], + ["sqlite3_table_column_metadata", "int", + "sqlite3*", "string", "string", "string", + "**", "**", "*", "*", "*"], + ["sqlite3_total_changes", "int", "sqlite3*"], + ["sqlite3_trace_v2", "int", "sqlite3*", "int", + new wasm.xWrap.FuncPtrAdapter({ + name: 'sqlite3_trace_v2::callback', + signature: 'i(ippp)', + contextKey: (argIndex, argv)=>'sqlite3@'+argv[0] + }), "*"], + ["sqlite3_txn_state", "int", ["sqlite3*","string"]], + /* Note that sqlite3_uri_...() have very specific requirements for + their first C-string arguments, so we cannot perform any value + conversion on those. */ + ["sqlite3_uri_boolean", "int", "sqlite3_filename", "string", "int"], + ["sqlite3_uri_key", "string", "sqlite3_filename", "int"], + ["sqlite3_uri_parameter", "string", "sqlite3_filename", "string"], + ["sqlite3_user_data","void*", "sqlite3_context*"], + ["sqlite3_value_blob", "*", "sqlite3_value*"], + ["sqlite3_value_bytes","int", "sqlite3_value*"], + ["sqlite3_value_double","f64", "sqlite3_value*"], + ["sqlite3_value_dup", "sqlite3_value*", "sqlite3_value*"], + ["sqlite3_value_free", undefined, "sqlite3_value*"], + ["sqlite3_value_frombind", "int", "sqlite3_value*"], + ["sqlite3_value_int","int", "sqlite3_value*"], + ["sqlite3_value_nochange", "int", "sqlite3_value*"], + ["sqlite3_value_numeric_type", "int", "sqlite3_value*"], + ["sqlite3_value_pointer", "*", "sqlite3_value*", "string:static"], + ["sqlite3_value_subtype", "int", "sqlite3_value*"], + ["sqlite3_value_text", "string", "sqlite3_value*"], + ["sqlite3_value_type", "int", "sqlite3_value*"], + ["sqlite3_vfs_find", "*", "string"], + ["sqlite3_vfs_register", "int", "sqlite3_vfs*", "int"], + ["sqlite3_vfs_unregister", "int", "sqlite3_vfs*"] + ]/*wasm.bindingSignatures*/; + + if(false && wasm.compileOptionUsed('SQLITE_ENABLE_NORMALIZE')){ + /* ^^^ "the problem" is that this is an option feature and the + build-time function-export list does not currently take + optional features into account. */ + wasm.bindingSignatures.push(["sqlite3_normalized_sql", "string", "sqlite3_stmt*"]); + } + + /** + Functions which require BigInt (int64) support are separated from + the others because we need to conditionally bind them or apply + dummy impls, depending on the capabilities of the environment. + + Note that not all of these functions directly require int64 + but are only for use with APIs which require int64. For example, + the vtab-related functions. + */ + wasm.bindingSignatures.int64 = [ + ["sqlite3_bind_int64","int", ["sqlite3_stmt*", "int", "i64"]], + ["sqlite3_changes64","i64", ["sqlite3*"]], + ["sqlite3_column_int64","i64", ["sqlite3_stmt*", "int"]], + ["sqlite3_create_module", "int", + ["sqlite3*","string","sqlite3_module*","*"]], + ["sqlite3_create_module_v2", "int", + ["sqlite3*","string","sqlite3_module*","*","*"]], + ["sqlite3_declare_vtab", "int", ["sqlite3*", "string:flexible"]], + ["sqlite3_deserialize", "int", "sqlite3*", "string", "*", "i64", "i64", "int"] + /* Careful! Short version: de/serialize() are problematic because they + might use a different allocator than the user for managing the + deserialized block. de/serialize() are ONLY safe to use with + sqlite3_malloc(), sqlite3_free(), and its 64-bit variants. */, + ["sqlite3_drop_modules", "int", ["sqlite3*", "**"]], + ["sqlite3_last_insert_rowid", "i64", ["sqlite3*"]], + ["sqlite3_malloc64", "*","i64"], + ["sqlite3_msize", "i64", "*"], + ["sqlite3_overload_function", "int", ["sqlite3*","string","int"]], + ["sqlite3_realloc64", "*","*", "i64"], + ["sqlite3_result_int64", undefined, "*", "i64"], + ["sqlite3_result_zeroblob64", "int", "*", "i64"], + ["sqlite3_serialize","*", "sqlite3*", "string", "*", "int"], + /* sqlite3_set_authorizer() requires a hand-written binding for + string conversions, so is defined elsewhere. */ + ["sqlite3_set_last_insert_rowid", undefined, ["sqlite3*", "i64"]], + ["sqlite3_status64", "int", "int", "*", "*", "int"], + ["sqlite3_total_changes64", "i64", ["sqlite3*"]], + ["sqlite3_uri_int64", "i64", ["sqlite3_filename", "string", "i64"]], + ["sqlite3_value_int64","i64", "sqlite3_value*"], + ["sqlite3_vtab_collation","string","sqlite3_index_info*","int"], + ["sqlite3_vtab_distinct","int", "sqlite3_index_info*"], + ["sqlite3_vtab_in","int", "sqlite3_index_info*", "int", "int"], + ["sqlite3_vtab_in_first", "int", "sqlite3_value*", "**"], + ["sqlite3_vtab_in_next", "int", "sqlite3_value*", "**"], + /*["sqlite3_vtab_config" is variadic and requires a hand-written + proxy.] */ + ["sqlite3_vtab_nochange","int", "sqlite3_context*"], + ["sqlite3_vtab_on_conflict","int", "sqlite3*"], + ["sqlite3_vtab_rhs_value","int", "sqlite3_index_info*", "int", "**"] + ]; + + /** + Functions which are intended solely for API-internal use by the + WASM components, not client code. These get installed into + sqlite3.wasm. Some of them get exposed to clients via variants + named sqlite3_js_...(). + */ + wasm.bindingSignatures.wasm = [ + ["sqlite3_wasm_db_reset", "int", "sqlite3*"], + ["sqlite3_wasm_db_vfs", "sqlite3_vfs*", "sqlite3*","string"], + ["sqlite3_wasm_vfs_create_file", "int", + "sqlite3_vfs*","string","*", "int"], + ["sqlite3_wasm_vfs_unlink", "int", "sqlite3_vfs*","string"] + ]; + /** Install JS<->C struct bindings for the non-opaque struct types we need... */ @@ -31,54 +284,104 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ heap: 0 ? wasm.memory : wasm.heap8u, alloc: wasm.alloc, dealloc: wasm.dealloc, - functionTable: wasm.functionTable, bigIntEnabled: wasm.bigIntEnabled, - memberPrefix: '$' + memberPrefix: /* Never change this: this prefix is baked into any + amount of code and client-facing docs. */ '$' }); delete self.Jaccwabyt; - if(0){ - /* "The problem" is that the following isn't even remotely - type-safe. OTOH, nothing about WASM pointers is. */ - const argPointer = wasm.xWrap.argAdapter('*'); - wasm.xWrap.argAdapter('StructType', (v)=>{ - if(v && v.constructor && v instanceof StructBinder.StructType){ - v = v.pointer; - } - return wasm.isPtr(v) - ? argPointer(v) - : toss("Invalid (object) type for StructType-type argument."); - }); - } - {/* Convert Arrays and certain TypedArrays to strings for - 'flexible-string'-type arguments */ + 'string:flexible'-type arguments */ const xString = wasm.xWrap.argAdapter('string'); wasm.xWrap.argAdapter( - 'flexible-string', (v)=>xString(util.flexibleString(v)) + 'string:flexible', (v)=>xString(util.flexibleString(v)) ); - } + + /** + The 'string:static' argument adapter treats its argument as + either... + + - WASM pointer: assumed to be a long-lived C-string which gets + returned as-is. + + - Anything else: gets coerced to a JS string for use as a map + key. If a matching entry is found (as described next), it is + returned, else wasm.allocCString() is used to create a a new + string, map its pointer to (''+v) for the remainder of the + application's life, and returns that pointer value for this + call and all future calls which are passed a + string-equivalent argument. + + Use case: sqlite3_bind_pointer() and sqlite3_result_pointer() + call for "a static string and preferably a string + literal". This converter is used to ensure that the string + value seen by those functions is long-lived and behaves as they + need it to. + */ + wasm.xWrap.argAdapter( + 'string:static', + function(v){ + if(wasm.isPtr(v)) return v; + v = ''+v; + let rc = this[v]; + return rc || (rc = this[v] = wasm.allocCString(v)); + }.bind(Object.create(null)) + ); + }/* special-case string-type argument conversions */ if(1){// WhWasmUtil.xWrap() bindings... /** Add some descriptive xWrap() aliases for '*' intended to (A) initially improve readability/correctness of capi.signatures - and (B) eventually perhaps provide automatic conversion from - higher-level representations, e.g. capi.sqlite3_vfs to - `sqlite3_vfs*` via capi.sqlite3_vfs.pointer. + and (B) provide automatic conversion from higher-level + representations, e.g. capi.sqlite3_vfs to `sqlite3_vfs*` via + capi.sqlite3_vfs.pointer. */ const aPtr = wasm.xWrap.argAdapter('*'); - wasm.xWrap.argAdapter('sqlite3*', aPtr) - ('sqlite3_stmt*', aPtr) + const nilType = function(){}; + wasm.xWrap.argAdapter('sqlite3_filename', aPtr) ('sqlite3_context*', aPtr) ('sqlite3_value*', aPtr) - ('sqlite3_vfs*', aPtr) - ('void*', aPtr); - wasm.xWrap.resultAdapter('sqlite3*', aPtr) - ('sqlite3_context*', aPtr) - ('sqlite3_stmt*', aPtr) - ('sqlite3_vfs*', aPtr) - ('void*', aPtr); + ('void*', aPtr) + ('sqlite3_stmt*', (v)=> + aPtr((v instanceof (sqlite3?.oo1?.Stmt || nilType)) + ? v.pointer : v)) + ('sqlite3*', (v)=> + aPtr((v instanceof (sqlite3?.oo1?.DB || nilType)) + ? v.pointer : v)) + ('sqlite3_index_info*', (v)=> + aPtr((v instanceof (capi.sqlite3_index_info || nilType)) + ? v.pointer : v)) + ('sqlite3_module*', (v)=> + aPtr((v instanceof (capi.sqlite3_module || nilType)) + ? v.pointer : v)) + /** + `sqlite3_vfs*`: + + - v is-a string: use the result of sqlite3_vfs_find(v) but + throw if it returns 0. + - v is-a capi.sqlite3_vfs: use v.pointer. + - Else return the same as the `'*'` argument conversion. + */ + ('sqlite3_vfs*', (v)=>{ + if('string'===typeof v){ + /* A NULL sqlite3_vfs pointer will be treated as the default + VFS in many contexts. We specifically do not want that + behavior here. */ + return capi.sqlite3_vfs_find(v) + || sqlite3.SQLite3Error.toss("Unknown sqlite3_vfs name:",v); + } + return aPtr((v instanceof (capi.sqlite3_vfs || nilType)) + ? v.pointer : v); + }); + + const rPtr = wasm.xWrap.resultAdapter('*'); + wasm.xWrap.resultAdapter('sqlite3*', rPtr) + ('sqlite3_context*', rPtr) + ('sqlite3_stmt*', rPtr) + ('sqlite3_value*', rPtr) + ('sqlite3_vfs*', rPtr) + ('void*', rPtr); /** Populate api object with sqlite3_...() by binding the "raw" wasm @@ -104,35 +407,48 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ : fI64Disabled(e[0]); } - /* There's no(?) need to expose bindingSignatures to clients, + /* There's no need to expose bindingSignatures to clients, implicitly making it part of the public interface. */ delete wasm.bindingSignatures; if(wasm.exports.sqlite3_wasm_db_error){ - util.sqlite3_wasm_db_error = wasm.xWrap( + const __db_err = wasm.xWrap( 'sqlite3_wasm_db_error', 'int', 'sqlite3*', 'int', 'string' ); + /** + Sets the given db's error state. Accepts: + + - (sqlite3*, int code, string msg) + - (sqlite3*, Error e [,string msg = ''+e]) + + If passed a WasmAllocError, the message is ingored and the + result code is SQLITE_NOMEM. If passed any other Error type, + the result code defaults to SQLITE_ERROR unless the Error + object has a resultCode property, in which case that is used + (e.g. SQLite3Error has that). If passed a non-WasmAllocError + exception, the message string defaults to theError.message. + + Returns the resulting code. Pass (pDb,0,0) to clear the error + state. + */ + util.sqlite3_wasm_db_error = function(pDb, resultCode, message){ + if(resultCode instanceof sqlite3.WasmAllocError){ + resultCode = capi.SQLITE_NOMEM; + message = 0 /*avoid allocating message string*/; + }else if(resultCode instanceof Error){ + message = message || ''+resultCode; + resultCode = (resultCode.resultCode || capi.SQLITE_ERROR); + } + return __db_err(pDb, resultCode, message); + }; }else{ util.sqlite3_wasm_db_error = function(pDb,errCode,msg){ console.warn("sqlite3_wasm_db_error() is not exported.",arguments); return errCode; }; } - }/*xWrap() bindings*/; - /** - When registering a VFS and its related components it may be - necessary to ensure that JS keeps a reference to them to keep - them from getting garbage collected. Simply pass each such value - to this function and a reference will be held to it for the life - of the app. - */ - capi.sqlite3_vfs_register.addReference = function f(...args){ - if(!f._) f._ = []; - f._.push(...args); - }; - /** Internal helper to assist in validating call argument counts in the hand-written sqlite3_xyz() wrappers. We do this only for @@ -144,34 +460,83 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ (1===n?"":'s')+"."); }; - /** - Helper for flexible-string conversions which require a - byte-length counterpart argument. Passed a value and its - ostensible length, this function returns [V,N], where V - is either v or a transformed copy of v and N is either n, - -1, or the byte length of v (if it's a byte array). - */ - const __flexiString = function(v,n){ - if('string'===typeof v){ - n = -1; - }else if(util.isSQLableTypedArray(v)){ - n = v.byteLength; - v = util.typedArrayToString(v); - }else if(Array.isArray(v)){ - v = v.join(""); - n = -1; - } - return [v, n]; - }; + if(1){/* Bindings for sqlite3_create_collation() */ + + const __collationContextKey = (argIndex,argv)=>{ + return 'argv['+argIndex+']:sqlite3@'+argv[0]+ + ':'+wasm.cstrToJs(argv[1]).toLowerCase() + }; + const __ccv2 = wasm.xWrap( + 'sqlite3_create_collation_v2', 'int', + 'sqlite3*','string','int','*', + new wasm.xWrap.FuncPtrAdapter({ + /* int(*xCompare)(void*,int,const void*,int,const void*) */ + name: 'sqlite3_create_collation_v2::xCompare', + signature: 'i(pipip)', + bindScope: 'context', + contextKey: __collationContextKey + }), + new wasm.xWrap.FuncPtrAdapter({ + /* void(*xDestroy(void*) */ + name: 'sqlite3_create_collation_v2::xDestroy', + signature: 'v(p)', + bindScope: 'context', + contextKey: __collationContextKey + }) + ); + + /** + Works exactly like C's sqlite3_create_collation_v2() except that: + + 1) It accepts JS functions for its function-pointer arguments, + for which it will install WASM-bound proxies. The bindings + are "permanent," in that they will stay in the WASM environment + until it shuts down unless the client somehow finds and removes + them. + + 2) It returns capi.SQLITE_FORMAT if the 3rd argument is not + capi.SQLITE_UTF8. No other encodings are supported. + + Returns 0 on success, non-0 on error, in which case the error + state of pDb (of type `sqlite3*` or argument-convertible to it) + may contain more information. + */ + capi.sqlite3_create_collation_v2 = function(pDb,zName,eTextRep,pArg,xCompare,xDestroy){ + if(6!==arguments.length) return __dbArgcMismatch(pDb, 'sqlite3_create_collation_v2', 6); + else if(capi.SQLITE_UTF8!==eTextRep){ + return util.sqlite3_wasm_db_error( + pDb, capi.SQLITE_FORMAT, "SQLITE_UTF8 is the only supported encoding." + ); + } + let rc, pfCompare, pfDestroy; + try{ + rc = __ccv2(pDb, zName, eTextRep, pArg, xCompare, xDestroy); + }catch(e){ + rc = util.sqlite3_wasm_db_error(pDb, e); + } + return rc; + }; + + capi.sqlite3_create_collation = (pDb,zName,eTextRep,pArg,xCompare)=>{ + return (5===arguments.length) + ? capi.sqlite3_create_collation_v2(pDb,zName,eTextRep,pArg,xCompare,0) + : __dbArgcMismatch(pDb, 'sqlite3_create_collation', 5); + }; + + }/*sqlite3_create_collation() and friends*/ if(1){/* Special-case handling of sqlite3_exec() */ const __exec = wasm.xWrap("sqlite3_exec", "int", - ["sqlite3*", "flexible-string", "*", "*", "**"]); + ["sqlite3*", "string:flexible", + new wasm.xWrap.FuncPtrAdapter({ + signature: 'i(pipp)', + bindScope: 'transient' + }), "*", "**"]); /* Documented in the api object's initializer. */ capi.sqlite3_exec = function f(pDb, sql, callback, pVoid, pErrMsg){ if(f.length!==arguments.length){ return __dbArgcMismatch(pDb,"sqlite3_exec",f.length); - }else if('function' !== typeof callback){ + }else if(!(callback instanceof Function)){ return __exec(pDb, sql, callback, pVoid, pErrMsg); } /* Wrap the callback in a WASM-bound function and convert the callback's @@ -181,8 +546,8 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ try { let aVals = [], aNames = [], i = 0, offset = 0; for( ; i < nCols; offset += (wasm.ptrSizeof * ++i) ){ - aVals.push( wasm.cstringToJs(wasm.getPtrValue(pColVals + offset)) ); - aNames.push( wasm.cstringToJs(wasm.getPtrValue(pColNames + offset)) ); + aVals.push( wasm.cstrToJs(wasm.peekPtr(pColVals + offset)) ); + aNames.push( wasm.cstrToJs(wasm.peekPtr(pColNames + offset)) ); } rc = callback(pVoid, nCols, aVals, aNames) | 0; /* The first 2 args of the callback are useless for JS but @@ -196,15 +561,12 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ } return rc; }; - let pFunc, rc; + let rc; try{ - pFunc = wasm.installFunction("ipipp", cbwrap); - rc = __exec(pDb, sql, pFunc, pVoid, pErrMsg); + rc = __exec(pDb, sql, cbwrap, pVoid, pErrMsg); }catch(e){ rc = util.sqlite3_wasm_db_error(pDb, capi.SQLITE_ERROR, - "Error running exec(): "+e.message); - }finally{ - if(pFunc) wasm.uninstallFunction(pFunc); + "Error running exec(): "+e); } return rc; }; @@ -226,132 +588,31 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ "*"/*xInverse*/, "*"/*xDestroy*/] ); - const __udfSetResult = function(pCtx, val){ - //console.warn("udfSetResult",typeof val, val); - switch(typeof val) { - case 'undefined': - /* Assume that the client already called sqlite3_result_xxx(). */ - break; - case 'boolean': - capi.sqlite3_result_int(pCtx, val ? 1 : 0); - break; - case 'bigint': - if(wasm.bigIntEnabled){ - if(util.bigIntFits64(val)) capi.sqlite3_result_int64(pCtx, val); - else toss3("BigInt value",val.toString(),"is too BigInt for int64."); - }else if(util.bigIntFits32(val)){ - capi.sqlite3_result_int(pCtx, Number(val)); - }else if(util.bigIntFitsDouble(val)){ - capi.sqlite3_result_double(pCtx, Number(val)); - }else{ - toss3("BigInt value",val.toString(),"is too BigInt."); - } - break; - case 'number': { - (util.isInt32(val) - ? capi.sqlite3_result_int - : capi.sqlite3_result_double)(pCtx, val); - break; - } - case 'string': - capi.sqlite3_result_text(pCtx, val, -1, capi.SQLITE_TRANSIENT); - break; - case 'object': - if(null===val/*yes, typeof null === 'object'*/) { - capi.sqlite3_result_null(pCtx); - break; - }else if(util.isBindableTypedArray(val)){ - const pBlob = wasm.allocFromTypedArray(val); - capi.sqlite3_result_blob( - pCtx, pBlob, val.byteLength, - wasm.exports[sqlite3.config.deallocExportName] - ); - break; - } - // else fall through - default: - toss3("Don't not how to handle this UDF result value:",(typeof val), val); - }; - }/*__udfSetResult()*/; - - const __udfConvertArgs = function(argc, pArgv){ - let i, pVal, valType, arg; - const tgt = []; - for(i = 0; i < argc; ++i){ - pVal = wasm.getPtrValue(pArgv + (wasm.ptrSizeof * i)); - /** - Curiously: despite ostensibly requiring 8-byte - alignment, the pArgv array is parcelled into chunks of - 4 bytes (1 pointer each). The values those point to - have 8-byte alignment but the individual argv entries - do not. - */ - valType = capi.sqlite3_value_type(pVal); - switch(valType){ - case capi.SQLITE_INTEGER: - if(wasm.bigIntEnabled){ - arg = capi.sqlite3_value_int64(pVal); - if(util.bigIntFitsDouble(arg)) arg = Number(arg); - } - else arg = capi.sqlite3_value_double(pVal)/*yes, double, for larger integers*/; - break; - case capi.SQLITE_FLOAT: - arg = capi.sqlite3_value_double(pVal); - break; - case capi.SQLITE_TEXT: - arg = capi.sqlite3_value_text(pVal); - break; - case capi.SQLITE_BLOB:{ - const n = capi.sqlite3_value_bytes(pVal); - const pBlob = capi.sqlite3_value_blob(pVal); - if(n && !pBlob) sqlite3.WasmAllocError.toss( - "Cannot allocate memory for blob argument of",n,"byte(s)" - ); - arg = n ? wasm.heap8u().slice(pBlob, pBlob + Number(n)) : null; - break; - } - case capi.SQLITE_NULL: - arg = null; break; - default: - toss3("Unhandled sqlite3_value_type()",valType, - "is possibly indicative of incorrect", - "pointer size assumption."); - } - tgt.push(arg); - } - return tgt; - }/*__udfConvertArgs()*/; - - const __udfSetError = (pCtx, e)=>{ - if(e instanceof sqlite3.WasmAllocError){ - capi.sqlite3_result_error_nomem(pCtx); - }else{ - const msg = ('string'===typeof e) ? e : e.message; - capi.sqlite3_result_error(pCtx, msg, -1); - } - }; - const __xFunc = function(callback){ return function(pCtx, argc, pArgv){ - try{ __udfSetResult(pCtx, callback(pCtx, ...__udfConvertArgs(argc, pArgv))) } - catch(e){ + try{ + capi.sqlite3_result_js( + pCtx, + callback(pCtx, ...capi.sqlite3_values_to_js(argc, pArgv)) + ); + }catch(e){ //console.error('xFunc() caught:',e); - __udfSetError(pCtx, e); + capi.sqlite3_result_error_js(pCtx, e); } }; }; const __xInverseAndStep = function(callback){ return function(pCtx, argc, pArgv){ - try{ callback(pCtx, ...__udfConvertArgs(argc, pArgv)) } - catch(e){ __udfSetError(pCtx, e) } + try{ callback(pCtx, ...capi.sqlite3_values_to_js(argc, pArgv)) } + catch(e){ capi.sqlite3_result_error_js(pCtx, e) } }; }; const __xFinalAndValue = function(callback){ return function(pCtx){ - try{ __udfSetResult(pCtx, callback(pCtx)) } - catch(e){ __udfSetError(pCtx, e) } + try{ capi.sqlite3_result_js(pCtx, callback(pCtx)) } + catch(e){ capi.sqlite3_result_error_js(pCtx, e) } }; }; @@ -457,66 +718,53 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ return rc; }; /** - A helper for UDFs implemented in JS and bound to WASM by the - client. Given a JS value, udfSetResult(pCtx,X) calls one of the - sqlite3_result_xyz(pCtx,...) routines, depending on X's data - type: - - - `null`: sqlite3_result_null() - - `boolean`: sqlite3_result_int() - - `number`: sqlite3_result_int() or sqlite3_result_double() - - `string`: sqlite3_result_text() - - Uint8Array or Int8Array: sqlite3_result_blob() - - `undefined`: indicates that the UDF called one of the - `sqlite3_result_xyz()` routines on its own, making this - function a no-op. Results are _undefined_ if this function is - passed the `undefined` value but did _not_ call one of the - `sqlite3_result_xyz()` routines. - - Anything else triggers sqlite3_result_error(). + A _deprecated_ alias for capi.sqlite3_result_js() which + predates the addition of that function in the public API. */ capi.sqlite3_create_function_v2.udfSetResult = capi.sqlite3_create_function.udfSetResult = - capi.sqlite3_create_window_function.udfSetResult = __udfSetResult; + capi.sqlite3_create_window_function.udfSetResult = capi.sqlite3_result_js; /** - A helper for UDFs implemented in JS and bound to WASM by the - client. When passed the - (argc,argv) values from the UDF-related functions which receive - them (xFunc, xStep, xInverse), it creates a JS array - representing those arguments, converting each to JS in a manner - appropriate to its data type: numeric, text, blob - (Uint8Array), or null. - - Results are undefined if it's passed anything other than those - two arguments from those specific contexts. - - Thus an argc of 4 will result in a length-4 array containing - the converted values from the corresponding argv. - - The conversion will throw only on allocation error or an internal - error. + A _deprecated_ alias for capi.sqlite3_values_to_js() which + predates the addition of that function in the public API. */ capi.sqlite3_create_function_v2.udfConvertArgs = capi.sqlite3_create_function.udfConvertArgs = - capi.sqlite3_create_window_function.udfConvertArgs = __udfConvertArgs; + capi.sqlite3_create_window_function.udfConvertArgs = capi.sqlite3_values_to_js; /** - A helper for UDFs implemented in JS and bound to WASM by the - client. It expects to be a passed `(sqlite3_context*, Error)` - (an exception object or message string). And it sets the - current UDF's result to sqlite3_result_error_nomem() or - sqlite3_result_error(), depending on whether the 2nd argument - is a sqlite3.WasmAllocError object or not. + A _deprecated_ alias for capi.sqlite3_result_error_js() which + predates the addition of that function in the public API. */ capi.sqlite3_create_function_v2.udfSetError = capi.sqlite3_create_function.udfSetError = - capi.sqlite3_create_window_function.udfSetError = __udfSetError; + capi.sqlite3_create_window_function.udfSetError = capi.sqlite3_result_error_js; }/*sqlite3_create_function_v2() and sqlite3_create_window_function() proxies*/; if(1){/* Special-case handling of sqlite3_prepare_v2() and sqlite3_prepare_v3() */ + /** + Helper for string:flexible conversions which require a + byte-length counterpart argument. Passed a value and its + ostensible length, this function returns [V,N], where V + is either v or a transformed copy of v and N is either n, + -1, or the byte length of v (if it's a byte array). + */ + const __flexiString = (v,n)=>{ + if('string'===typeof v){ + n = -1; + }else if(util.isSQLableTypedArray(v)){ + n = v.byteLength; + v = util.typedArrayToString(v); + }else if(Array.isArray(v)){ + v = v.join(""); + n = -1; + } + return [v, n]; + }; + /** Scope-local holder of the two impls of sqlite3_prepare_v2/v3(). */ @@ -547,7 +795,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ "int", ["sqlite3*", "*", "int", "int", "**", "**"]); - /* Documented in the api object's initializer. */ + /* Documented in the capi object's initializer. */ capi.sqlite3_prepare_v3 = function f(pDb, sql, sqlLen, prepFlags, ppStmt, pzTail){ if(f.length!==arguments.length){ return __dbArgcMismatch(pDb,"sqlite3_prepare_v3",f.length); @@ -564,7 +812,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ } }; - /* Documented in the api object's initializer. */ + /* Documented in the capi object's initializer. */ capi.sqlite3_prepare_v2 = function f(pDb, sql, sqlLen, ppStmt, pzTail){ return (f.length===arguments.length) ? capi.sqlite3_prepare_v3(pDb, sql, sqlLen, 0, ppStmt, pzTail) @@ -572,23 +820,108 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ }; }/*sqlite3_prepare_v2/v3()*/; + {/* sqlite3_set_authorizer() */ + const __ssa = wasm.xWrap("sqlite3_set_authorizer", 'int', [ + "sqlite3*", + new wasm.xWrap.FuncPtrAdapter({ + name: "sqlite3_set_authorizer::xAuth", + signature: "i(pi"+"ssss)", + contextKey: (argIndex, argv)=>argv[0/*(sqlite3*)*/] + }), + "*" + ]); + capi.sqlite3_set_authorizer = function(pDb, xAuth, pUserData){ + if(3!==arguments.length) return __dbArgcMismatch(pDb, 'sqlite3_set_authorizer', 3); + if(xAuth instanceof Function){ + const xProxy = xAuth; + /* Create a proxy which will receive the C-strings from WASM + and convert them to JS strings for the client-supplied + function. */ + xAuth = function(pV, iCode, s0, s1, s2, s3){ + try{ + s0 = s0 && wasm.cstrToJs(s0); s1 = s1 && wasm.cstrToJs(s1); + s2 = s2 && wasm.cstrToJs(s2); s3 = s3 && wasm.cstrToJs(s3); + return xProxy(pV, iCode, s0, s1, s2, s3) || 0; + }catch(e){ + return util.sqlite3_wasm_db_error(pDb, e); + } + }; + } + return __ssa(pDb, xAuth, pUserData); + }; + }/* sqlite3_set_authorizer() */ + + {/* sqlite3_config() */ + /** + Wraps a small subset of the C API's sqlite3_config() options. + Unsupported options trigger the return of capi.SQLITE_NOTFOUND. + Passing fewer than 2 arguments triggers return of + capi.SQLITE_MISUSE. + */ + capi.sqlite3_config = function(op, ...args){ + if(arguments.length<2) return capi.SQLITE_MISUSE; + switch(op){ + case capi.SQLITE_CONFIG_COVERING_INDEX_SCAN: // 20 /* int */ + case capi.SQLITE_CONFIG_MEMSTATUS:// 9 /* boolean */ + case capi.SQLITE_CONFIG_SMALL_MALLOC: // 27 /* boolean */ + case capi.SQLITE_CONFIG_SORTERREF_SIZE: // 28 /* int nByte */ + case capi.SQLITE_CONFIG_STMTJRNL_SPILL: // 26 /* int nByte */ + case capi.SQLITE_CONFIG_URI:// 17 /* int */ + return wasm.exports.sqlite3_wasm_config_i(op, args[0]); + case capi.SQLITE_CONFIG_LOOKASIDE: // 13 /* int int */ + return wasm.exports.sqlite3_wasm_config_ii(op, args[0], args[1]); + case capi.SQLITE_CONFIG_MEMDB_MAXSIZE: // 29 /* sqlite3_int64 */ + return wasm.exports.sqlite3_wasm_config_j(op, args[0]); + case capi.SQLITE_CONFIG_GETMALLOC: // 5 /* sqlite3_mem_methods* */ + case capi.SQLITE_CONFIG_GETMUTEX: // 11 /* sqlite3_mutex_methods* */ + case capi.SQLITE_CONFIG_GETPCACHE2: // 19 /* sqlite3_pcache_methods2* */ + case capi.SQLITE_CONFIG_GETPCACHE: // 15 /* no-op */ + case capi.SQLITE_CONFIG_HEAP: // 8 /* void*, int nByte, int min */ + case capi.SQLITE_CONFIG_LOG: // 16 /* xFunc, void* */ + case capi.SQLITE_CONFIG_MALLOC:// 4 /* sqlite3_mem_methods* */ + case capi.SQLITE_CONFIG_MMAP_SIZE: // 22 /* sqlite3_int64, sqlite3_int64 */ + case capi.SQLITE_CONFIG_MULTITHREAD: // 2 /* nil */ + case capi.SQLITE_CONFIG_MUTEX: // 10 /* sqlite3_mutex_methods* */ + case capi.SQLITE_CONFIG_PAGECACHE: // 7 /* void*, int sz, int N */ + case capi.SQLITE_CONFIG_PCACHE2: // 18 /* sqlite3_pcache_methods2* */ + case capi.SQLITE_CONFIG_PCACHE: // 14 /* no-op */ + case capi.SQLITE_CONFIG_PCACHE_HDRSZ: // 24 /* int *psz */ + case capi.SQLITE_CONFIG_PMASZ: // 25 /* unsigned int szPma */ + case capi.SQLITE_CONFIG_SERIALIZED: // 3 /* nil */ + case capi.SQLITE_CONFIG_SINGLETHREAD: // 1 /* nil */: + case capi.SQLITE_CONFIG_SQLLOG: // 21 /* xSqllog, void* */ + case capi.SQLITE_CONFIG_WIN32_HEAPSIZE: // 23 /* int nByte */ + default: + return capi.SQLITE_NOTFOUND; + } + }; + }/* sqlite3_config() */ + {/* Import C-level constants and structs... */ const cJson = wasm.xCall('sqlite3_wasm_enum_json'); if(!cJson){ toss("Maintenance required: increase sqlite3_wasm_enum_json()'s", "static buffer size!"); } - wasm.ctype = JSON.parse(wasm.cstringToJs(cJson)); + wasm.ctype = JSON.parse(wasm.cstrToJs(cJson)); //console.debug('wasm.ctype length =',wasm.cstrlen(cJson)); - for(const t of ['access', 'blobFinalizers', 'dataTypes', - 'encodings', 'fcntl', 'flock', 'ioCap', - 'openFlags', 'prepareFlags', 'resultCodes', - 'serialize', 'syncFlags', 'trace', 'udfFlags', - 'version' - ]){ + const defineGroups = ['access', 'authorizer', + 'blobFinalizers', 'config', 'dataTypes', + 'dbConfig', 'dbStatus', + 'encodings', 'fcntl', 'flock', 'ioCap', + 'limits', 'openFlags', + 'prepareFlags', 'resultCodes', + 'serialize', 'sqlite3Status', + 'stmtStatus', 'syncFlags', + 'trace', 'txnState', 'udfFlags', + 'version' ]; + if(wasm.bigIntEnabled){ + defineGroups.push('vtab'); + } + for(const t of defineGroups){ for(const e of Object.entries(wasm.ctype[t])){ - // ^^^ [k,v] there triggers a buggy code transormation via one - // of the Emscripten-driven optimizers. + // ^^^ [k,v] there triggers a buggy code transformation via + // one of the Emscripten-driven optimizers. capi[e[0]] = e[1]; } } @@ -605,19 +938,38 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ capi.sqlite3_js_rc_str = (rc)=>__rcMap[rc]; /* Bind all registered C-side structs... */ const notThese = Object.assign(Object.create(null),{ - // Structs NOT to register - WasmTestStruct: true + // For each struct to NOT register, map its name to true: + WasmTestStruct: true, + /* We unregister the kvvfs VFS from Worker threads below. */ + sqlite3_kvvfs_methods: !util.isUIThread(), + /* sqlite3_index_info and friends require int64: */ + sqlite3_index_info: !wasm.bigIntEnabled, + sqlite3_index_constraint: !wasm.bigIntEnabled, + sqlite3_index_orderby: !wasm.bigIntEnabled, + sqlite3_index_constraint_usage: !wasm.bigIntEnabled }); - if(!util.isUIThread()){ - /* We remove the kvvfs VFS from Worker threads below. */ - notThese.sqlite3_kvvfs_methods = true; - } for(const s of wasm.ctype.structs){ if(!notThese[s.name]){ capi[s.name] = sqlite3.StructBinder(s); } } - }/*end C constant imports*/ + if(capi.sqlite3_index_info){ + /* Move these inner structs into sqlite3_index_info. Binding + ** them to WASM requires that we create global-scope structs to + ** model them with, but those are no longer needed after we've + ** passed them to StructBinder. */ + for(const k of ['sqlite3_index_constraint', + 'sqlite3_index_orderby', + 'sqlite3_index_constraint_usage']){ + capi.sqlite3_index_info[k] = capi[k]; + delete capi[k]; + } + capi.sqlite3_vtab_config = wasm.xWrap( + 'sqlite3_wasm_vtab_config','int',[ + 'sqlite3*', 'int', 'int'] + ); + }/* end vtab-related setup */ + }/*end C constant and struct imports*/ const pKvvfs = capi.sqlite3_vfs_find("kvvfs"); if( pKvvfs ){/* kvvfs-specific glue */ @@ -628,11 +980,10 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ delete capi.sqlite3_kvvfs_methods; const kvvfsMakeKey = wasm.exports.sqlite3_wasm_kvvfsMakeKeyOnPstack, - pstack = wasm.pstack, - pAllocRaw = wasm.exports.sqlite3_wasm_pstack_alloc; + pstack = wasm.pstack; const kvvfsStorage = (zClass)=> - ((115/*=='s'*/===wasm.getMemValue(zClass)) + ((115/*=='s'*/===wasm.peek(zClass)) ? sessionStorage : localStorage); /** @@ -648,7 +999,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ try { const zXKey = kvvfsMakeKey(zClass,zKey); if(!zXKey) return -3/*OOM*/; - const jKey = wasm.cstringToJs(zXKey); + const jKey = wasm.cstrToJs(zXKey); const jV = kvvfsStorage(zClass).getItem(jKey); if(!jV) return -1; const nV = jV.length /* Note that we are relying 100% on v being @@ -656,13 +1007,13 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ C-string's byte length. */; if(nBuf<=0) return nV; else if(1===nBuf){ - wasm.setMemValue(zBuf, 0); + wasm.poke(zBuf, 0); return nV; } const zV = wasm.scopedAllocCString(jV); if(nBuf > nV + 1) nBuf = nV + 1; wasm.heap8u().copyWithin(zBuf, zV, zV + nBuf - 1); - wasm.setMemValue(zBuf + nBuf - 1, 0); + wasm.poke(zBuf + nBuf - 1, 0); return nBuf - 1; }catch(e){ console.error("kvstorageRead()",e); @@ -677,8 +1028,8 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ try { const zXKey = kvvfsMakeKey(zClass,zKey); if(!zXKey) return 1/*OOM*/; - const jKey = wasm.cstringToJs(zXKey); - kvvfsStorage(zClass).setItem(jKey, wasm.cstringToJs(zData)); + const jKey = wasm.cstrToJs(zXKey); + kvvfsStorage(zClass).setItem(jKey, wasm.cstrToJs(zData)); return 0; }catch(e){ console.error("kvstorageWrite()",e); @@ -692,7 +1043,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ try { const zXKey = kvvfsMakeKey(zClass,zKey); if(!zXKey) return 1/*OOM*/; - kvvfsStorage(zClass).removeItem(wasm.cstringToJs(zXKey)); + kvvfsStorage(zClass).removeItem(wasm.cstrToJs(zXKey)); return 0; }catch(e){ console.error("kvstorageDelete()",e); diff --git a/ext/wasm/api/sqlite3-api-oo1.js b/ext/wasm/api/sqlite3-api-oo1.js index 02ce9c0ced..59ecf56b96 100644 --- a/ext/wasm/api/sqlite3-api-oo1.js +++ b/ext/wasm/api/sqlite3-api-oo1.js @@ -55,12 +55,13 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ if(sqliteResultCode){ if(dbPtr instanceof DB) dbPtr = dbPtr.pointer; toss3( - "sqlite result code",sqliteResultCode+":", + "sqlite3 result code",sqliteResultCode+":", (dbPtr ? capi.sqlite3_errmsg(dbPtr) : capi.sqlite3_errstr(sqliteResultCode)) ); } + return arguments[0]; }; /** @@ -72,13 +73,15 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ if(capi.SQLITE_TRACE_STMT===t){ // x == SQL, p == sqlite3_stmt* console.log("SQL TRACE #"+(++this.counter), - wasm.cstringToJs(x)); + wasm.cstrToJs(x)); } }.bind({counter: 0})); /** - A map of sqlite3_vfs pointers to SQL code to run when the DB - constructor opens a database with the given VFS. + A map of sqlite3_vfs pointers to SQL code or a callback function + to run when the DB constructor opens a database with the given + VFS. In the latter case, the call signature is (theDbObject,sqlite3Namespace) + and the callback is expected to throw on error. */ const __vfsPostOpenSql = Object.create(null); @@ -136,7 +139,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ console.error("Invalid DB ctor args",opt,arguments); toss3("Invalid arguments for DB constructor."); } - let fnJs = ('number'===typeof fn) ? wasm.cstringToJs(fn) : fn; + let fnJs = ('number'===typeof fn) ? wasm.cstrToJs(fn) : fn; const vfsCheck = ctor._name2vfs[fnJs]; if(vfsCheck){ vfsName = vfsCheck.vfs; @@ -153,21 +156,13 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ try { const pPtr = wasm.pstack.allocPtr() /* output (sqlite3**) arg */; let rc = capi.sqlite3_open_v2(fn, pPtr, oflags, vfsName || 0); - pDb = wasm.getPtrValue(pPtr); + pDb = wasm.peekPtr(pPtr); checkSqlite3Rc(pDb, rc); + capi.sqlite3_extended_result_codes(pDb, 1); if(flagsStr.indexOf('t')>=0){ capi.sqlite3_trace_v2(pDb, capi.SQLITE_TRACE_STMT, __dbTraceToConsole, 0); } - // Check for per-VFS post-open SQL... - const pVfs = capi.sqlite3_js_db_vfs(pDb); - //console.warn("Opened db",fn,"with vfs",vfsName,pVfs); - if(!pVfs) toss3("Internal error: cannot get VFS for new db handle."); - const postInitSql = __vfsPostOpenSql[pVfs]; - if(postInitSql){ - rc = capi.sqlite3_exec(pDb, postInitSql, 0, 0, 0); - checkSqlite3Rc(pDb, rc); - } }catch( e ){ if( pDb ) capi.sqlite3_close_v2(pDb); throw e; @@ -177,12 +172,34 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ this.filename = fnJs; __ptrMap.set(this, pDb); __stmtMap.set(this, Object.create(null)); + try{ + // Check for per-VFS post-open SQL/callback... + const pVfs = capi.sqlite3_js_db_vfs(pDb); + if(!pVfs) toss3("Internal error: cannot get VFS for new db handle."); + const postInitSql = __vfsPostOpenSql[pVfs]; + if(postInitSql instanceof Function){ + postInitSql(this, sqlite3); + }else if(postInitSql){ + checkSqlite3Rc( + pDb, capi.sqlite3_exec(pDb, postInitSql, 0, 0, 0) + ); + } + }catch(e){ + this.close(); + throw e; + } }; /** Sets SQL which should be exec()'d on a DB instance after it is - opened with the given VFS pointer. This is intended only for use - by DB subclasses or sqlite3_vfs implementations. + opened with the given VFS pointer. The SQL may be any type + supported by the "string:flexible" function argument conversion. + Alternately, the 2nd argument may be a function, in which case it + is called with (theOo1DbObject,sqlite3Namespace) at the end of + the DB() constructor. The function must throw on error, in which + case the db is closed and the exception is propagated. This + function is intended only for use by DB subclasses or sqlite3_vfs + implementations. */ dbCtorHelper.setVfsPostOpenSql = function(pVfs, sql){ __vfsPostOpenSql[pVfs] = sql; @@ -200,9 +217,8 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ */ dbCtorHelper.normalizeArgs = function(filename=':memory:',flags = 'c',vfs = null){ const arg = {}; - if(1===arguments.length && 'object'===typeof arguments[0]){ - const x = arguments[0]; - Object.keys(x).forEach((k)=>arg[k] = x[k]); + if(1===arguments.length && arguments[0] && 'object'===typeof arguments[0]){ + Object.assign(arg, arguments[0]); if(undefined===arg.flags) arg.flags = 'c'; if(undefined===arg.vfs) arg.vfs = null; if(undefined===arg.filename) arg.filename = ':memory:'; @@ -424,9 +440,11 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ out.cbArg = (stmt)=>stmt.get(opt.rowMode); break; }else if('string'===typeof opt.rowMode && opt.rowMode.length>1){ - /* "$X", ":X", and "@X" fetch column named "X" (case-sensitive!) */ - const prefix = opt.rowMode[0]; - if(':'===prefix || '@'===prefix || '$'===prefix){ + /* "$X": fetch column named "X" (case-sensitive!). Prior + to 2022-12-14 ":X" and "@X" were also permitted, but + having so many options is unnecessary and likely to + cause confusion. */ + if('$'===opt.rowMode[0]){ out.cbArg = function(stmt){ const rc = stmt.get(this.obj)[this.colName]; return (undefined===rc) ? toss3("exec(): unknown result column:",this.colName) : rc; @@ -458,18 +476,29 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ return rc; }; + /** + Internal impl of the DB.selectArrays() and + selectObjects() methods. + */ + const __selectAll = + (db, sql, bind, rowMode)=>db.exec({ + sql, bind, rowMode, returnValue: 'resultRows' + }); + /** Expects to be given a DB instance or an `sqlite3*` pointer (may be null) and an sqlite3 API result code. If the result code is not falsy, this function throws an SQLite3Error with an error - message from sqlite3_errmsg(), using dbPtr as the db handle, or - sqlite3_errstr() if dbPtr is falsy. Note that if it's passed a - non-error code like SQLITE_ROW or SQLITE_DONE, it will still - throw but the error string might be "Not an error." The various - non-0 non-error codes need to be checked for in - client code where they are expected. + message from sqlite3_errmsg(), using db (or, if db is-a DB, + db.pointer) as the db handle, or sqlite3_errstr() if db is + falsy. Note that if it's passed a non-error code like SQLITE_ROW + or SQLITE_DONE, it will still throw but the error string might be + "Not an error." The various non-0 non-error codes need to be + checked for in client code where they are expected. + + If it does not throw, it returns its first argument. */ - DB.checkRc = checkSqlite3Rc; + DB.checkRc = (db,resultCode)=>checkSqlite3Rc(db,resultCode); DB.prototype = { /** Returns true if this db handle is open, else false. */ @@ -494,10 +523,12 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ db is closed but before auxiliary state like this.filename is cleared. - Both onclose handlers are passed this object. If this db is not - opened, neither of the handlers are called. Any exceptions the - handlers throw are ignored because "destructors must not - throw." + Both onclose handlers are passed this object, with the onclose + object as their "this," noting that the db will have been + closed when onclose.after is called. If this db is not opened + when close() is called, neither of the handlers are called. Any + exceptions the handlers throw are ignored because "destructors + must not throw." Note that garbage collection of a db handle, if it happens at all, will never trigger close(), so onclose handlers are not a @@ -574,7 +605,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ ); if(pVfs){ const v = new capi.sqlite3_vfs(pVfs); - try{ rc = wasm.cstringToJs(v.$zName) } + try{ rc = wasm.cstrToJs(v.$zName) } finally { v.dispose() } } return rc; @@ -608,7 +639,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ try{ ppStmt = wasm.pstack.alloc(8)/* output (sqlite3_stmt**) arg */; DB.checkRc(this, capi.sqlite3_prepare_v2(this.pointer, sql, -1, ppStmt, null)); - pStmt = wasm.getPtrValue(ppStmt); + pStmt = wasm.peekPtr(ppStmt); } finally { wasm.pstack.restore(stack); @@ -711,17 +742,15 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ row. Only that one single value will be passed on. C) A string with a minimum length of 2 and leading character of - ':', '$', or '@' will fetch the row as an object, extract that - one field, and pass that field's value to the callback. Note - that these keys are case-sensitive so must match the case used - in the SQL. e.g. `"select a A from t"` with a `rowMode` of - `'$A'` would work but `'$a'` would not. A reference to a column - not in the result set will trigger an exception on the first - row (as the check is not performed until rows are fetched). - Note also that `$` is a legal identifier character in JS so - need not be quoted. (Design note: those 3 characters were - chosen because they are the characters support for naming bound - parameters.) + '$' will fetch the row as an object, extract that one field, + and pass that field's value to the callback. Note that these + keys are case-sensitive so must match the case used in the + SQL. e.g. `"select a A from t"` with a `rowMode` of `'$A'` + would work but `'$a'` would not. A reference to a column not in + the result set will trigger an exception on the first row (as + the check is not performed until rows are fetched). Note also + that `$` is a legal identifier character in JS so need not be + quoted. Any other `rowMode` value triggers an exception. @@ -772,6 +801,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ let bind = opt.bind; let evalFirstResult = !!(arg.cbArg || opt.columnNames) /* true to evaluate the first result-returning query */; const stack = wasm.scopedAllocPush(); + const saveSql = Array.isArray(opt.saveSql) ? opt.saveSql : undefined; try{ const isTA = util.isSQLableTypedArray(arg.sql) /* Optimization: if the SQL is a TypedArray we can save some string @@ -788,8 +818,8 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ const pSqlEnd = pSql + sqlByteLen; if(isTA) wasm.heap8().set(arg.sql, pSql); else wasm.jstrcpy(arg.sql, wasm.heap8(), pSql, sqlByteLen, false); - wasm.setMemValue(pSql + sqlByteLen, 0/*NUL terminator*/); - while(pSql && wasm.getMemValue(pSql, 'i8') + wasm.poke(pSql + sqlByteLen, 0/*NUL terminator*/); + while(pSql && wasm.peek(pSql, 'i8') /* Maintenance reminder:^^^ _must_ be 'i8' or else we will very likely cause an endless loop. What that's doing is checking for a terminating NUL byte. If we @@ -797,18 +827,15 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ around the NUL terminator, and get stuck in and endless loop at the end of the SQL, endlessly re-preparing an empty statement. */ ){ - wasm.setPtrValue(ppStmt, 0); - wasm.setPtrValue(pzTail, 0); + wasm.pokePtr([ppStmt, pzTail], 0); DB.checkRc(this, capi.sqlite3_prepare_v3( this.pointer, pSql, sqlByteLen, 0, ppStmt, pzTail )); - const pStmt = wasm.getPtrValue(ppStmt); - pSql = wasm.getPtrValue(pzTail); + const pStmt = wasm.peekPtr(ppStmt); + pSql = wasm.peekPtr(pzTail); sqlByteLen = pSqlEnd - pSql; if(!pStmt) continue; - if(Array.isArray(opt.saveSql)){ - opt.saveSql.push(capi.sqlite3_sql(pStmt).trim()); - } + if(saveSql) saveSql.push(capi.sqlite3_sql(pStmt).trim()); stmt = new Stmt(this, pStmt, BindTypes); if(bind && stmt.parameterCount){ stmt.bind(bind); @@ -1081,6 +1108,26 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ return __selectFirstRow(this, sql, bind, {}); }, + /** + Runs the given SQL and returns an array of all results, with + each row represented as an array, as per the 'array' `rowMode` + option to `exec()`. An empty result set resolves + to an empty array. The second argument, if any, is treated as + the 'bind' option to a call to exec(). + */ + selectArrays: function(sql,bind){ + return __selectAll(this, sql, bind, 'array'); + }, + + /** + Works identically to selectArrays() except that each value + in the returned array is an object, as per the 'object' `rowMode` + option to `exec()`. + */ + selectObjects: function(sql,bind){ + return __selectAll(this, sql, bind, 'object'); + }, + /** Returns the number of currently-opened Stmt handles for this db handle, or 0 if this DB instance is closed. @@ -1130,6 +1177,14 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ this.exec("ROLLBACK to SAVEPOINT oo1; RELEASE SAVEPOINT oo1"); throw e; } + }, + + /** + A convenience form of DB.checkRc(this,resultCode). If it does + not throw, it returns this object. + */ + checkRc: function(resultCode){ + return DB.checkRc(this, resultCode); } }/*DB.prototype*/; @@ -1749,10 +1804,6 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ /** The OO API's public namespace. */ sqlite3.oo1 = { - version: { - lib: capi.sqlite3_libversion(), - ooApi: "0.1" - }, DB, Stmt }/*oo1 object*/; diff --git a/ext/wasm/api/sqlite3-api-prologue.js b/ext/wasm/api/sqlite3-api-prologue.js index fed1c56669..94514e4770 100644 --- a/ext/wasm/api/sqlite3-api-prologue.js +++ b/ext/wasm/api/sqlite3-api-prologue.js @@ -11,76 +11,18 @@ *********************************************************************** This file is intended to be combined at build-time with other - related code, most notably a header and footer which wraps this whole - file into an Emscripten Module.postRun() handler which has a parameter - named "Module" (the Emscripten Module object). The exact requirements, - conventions, and build process are very much under construction and - will be (re)documented once they've stopped fluctuating so much. + related code, most notably a header and footer which wraps this + whole file into an Emscripten Module.postRun() handler which has a + parameter named "Module" (the Emscripten Module object). The sqlite3 + JS API has no hard requirements on Emscripten, and does not expose + any Emscripten APIs to clients. It is structured such that its build + can be tweaked to include it in arbitrary WASM environments which + supply the necessary underlying features (e.g. a POSIX file I/O + layer). - Project home page: https://sqlite.org + Main project home page: https://sqlite.org Documentation home page: https://sqlite.org/wasm - - Specific goals of this subproject: - - - Except where noted in the non-goals, provide a more-or-less - feature-complete wrapper to the sqlite3 C API, insofar as WASM - feature parity with C allows for. In fact, provide at least 4 - APIs... - - 1) 1-to-1 bindings as exported from WASM, with no automatic - type conversions between JS and C. - - 2) A binding of (1) which provides certain JS/C type conversions - to greatly simplify its use. - - 3) A higher-level API, more akin to sql.js and node.js-style - implementations. This one speaks directly to the low-level - API. This API must be used from the same thread as the - low-level API. - - 4) A second higher-level API which speaks to the previous APIs via - worker messages. This one is intended for use in the main - thread, with the lower-level APIs installed in a Worker thread, - and talking to them via Worker messages. Because Workers are - asynchronouns and have only a single message channel, some - acrobatics are needed here to feed async work results back to - the client (as we cannot simply pass around callbacks between - the main and Worker threads). - - - Insofar as possible, support client-side storage using JS - filesystem APIs. As of this writing, such things are still very - much under development. - - Specific non-goals of this project: - - - As WASM is a web-centric technology and UTF-8 is the King of - Encodings in that realm, there are no currently plans to support - the UTF16-related sqlite3 APIs. They would add a complication to - the bindings for no appreciable benefit. Though web-related - implementation details take priority, and the JavaScript - components of the API specifically focus on browser clients, the - lower-level WASM module "should" work in non-web WASM - environments. - - - Supporting old or niche-market platforms. WASM is built for a - modern web and requires modern platforms. - - - Though scalar User-Defined Functions (UDFs) may be created in - JavaScript, there are currently no plans to add support for - aggregate and window functions. - - Attribution: - - This project is endebted to the work of sql.js: - - https://github.com/sql-js/sql.js - - sql.js was an essential stepping stone in this code's development as - it demonstrated how to handle some of the WASM-related voodoo (like - handling pointers-to-pointers and adding JS implementations of - C-bound callback functions). These APIs have a considerably - different shape than sql.js's, however. */ /** @@ -90,6 +32,10 @@ for the current environment, and then optionally be removed from the global object using `delete self.sqlite3ApiBootstrap`. + This function is not intended for client-level use. It is intended + for use in creating bundles configured for specific WASM + environments. + This function expects a configuration object, intended to abstract away details specific to any given WASM environment, primarily so that it can be used without any _direct_ dependency on @@ -119,19 +65,23 @@ - `allocExportName`: the name of the function, in `exports`, of the `malloc(3)`-compatible routine for the WASM environment. Defaults - to `"malloc"`. + to `"sqlite3_malloc"`. Beware that using any allocator other than + sqlite3_malloc() may require care in certain client-side code + regarding which allocator is uses. Notably, sqlite3_deserialize() + and sqlite3_serialize() can only safely use memory from different + allocators under very specific conditions. - `deallocExportName`: the name of the function, in `exports`, of the `free(3)`-compatible routine for the WASM - environment. Defaults to `"free"`. + environment. Defaults to `"sqlite3_free"`. - - `wasmfsOpfsDir`[^1]: if the environment supports persistent - storage, this directory names the "mount point" for that - directory. It must be prefixed by `/` and may contain only a - single directory-name part. Using the root directory name is not - supported by any current persistent backend. This setting is - only used in WASMFS-enabled builds. + - `reallocExportName`: the name of the function, in `exports`, of + the `realloc(3)`-compatible routine for the WASM + environment. Defaults to `"sqlite3_realloc"`. + - `wasmfsOpfsDir`[^1]: As of 2022-12-17, this feature does not + currently work due to incompatible Emscripten-side changes made + in the WASMFS+OPFS combination. This option is currently ignored. [^1] = This property may optionally be a function, in which case this function re-assigns it to the value returned from that function, @@ -158,11 +108,23 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( } return !!self.BigInt64Array; })(), - allocExportName: 'malloc', - deallocExportName: 'free', - wasmfsOpfsDir: '/opfs' + wasmfsOpfsDir: '/opfs', + /** + useStdAlloc is just for testing an allocator discrepancy. The + docs guarantee that this is false in the canonical builds. For + 99% of purposes it doesn't matter which allocators we use, but + it becomes significant with, e.g., sqlite3_deserialize() + and certain wasm.xWrap.resultAdapter()s. + */ + useStdAlloc: false }, apiConfig || {}); + Object.assign(config, { + allocExportName: config.useStdAlloc ? 'malloc' : 'sqlite3_malloc', + deallocExportName: config.useStdAlloc ? 'free' : 'sqlite3_free', + reallocExportName: config.useStdAlloc ? 'realloc' : 'sqlite3_realloc' + }, config); + [ // If any of these config options are functions, replace them with // the result of calling that function... @@ -172,6 +134,10 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( config[k] = config[k](); } }); + config.wasmOpfsDir = + /* 2022-12-17: WASMFS+OPFS can no longer be activated from the + main thread (aborts via a failed assert() if it's attempted), + which eliminates any(?) benefit to supporting it. */ false; /** The main sqlite3 binding API gets installed into this object, @@ -191,11 +157,7 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( const capi = Object.create(null); /** Holds state which are specific to the WASM-related - infrastructure and glue code. It is not expected that client - code will normally need these, but they're exposed here in case - it does. These APIs are _not_ to be considered an - official/stable part of the sqlite3 WASM API. They may change - as the developers' experience suggests appropriate changes. + infrastructure and glue code. Note that a number of members of this object are injected dynamically after the api object is fully constructed, so @@ -223,28 +185,49 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( /** Constructs this object with a message depending on its arguments: - - If it's passed only a single integer argument, it is assumed - to be an sqlite3 C API result code. The message becomes the - result of sqlite3.capi.sqlite3_js_rc_str() or (if that returns - falsy) a synthesized string which contains that integer. + If its first argument is an integer, it is assumed to be + an SQLITE_... result code and it is passed to + sqlite3.capi.sqlite3_js_rc_str() to stringify it. - - If passed 2 arguments and the 2nd is a object, it bevaves - like the Error(string,object) constructor except that the first - argument is subject to the is-integer semantics from the - previous point. + If called with exactly 2 arguments and the 2nd is an object, + that object is treated as the 2nd argument to the parent + constructor. - - Else all arguments are concatenated with a space between each - one, using args.join(' '), to create the error message. + The exception's message is created by concatenating its + arguments with a space between each, except for the + two-args-with-an-objec form and that the first argument will + get coerced to a string, as described above, if it's an + integer. + + If passed an integer first argument, the error object's + `resultCode` member will be set to the given integer value, + else it will be set to capi.SQLITE_ERROR. */ constructor(...args){ - if(1===args.length && __isInt(args[0])){ - super(__rcStr(args[0])); - }else if(2===args.length && 'object'===typeof args){ - if(__isInt(args[0])) super(__rcStr(args[0]), args[1]); - else super(...args); - }else{ - super(args.join(' ')); + let rc; + if(args.length){ + if(__isInt(args[0])){ + rc = args[0]; + if(1===args.length){ + super(__rcStr(args[0])); + }else{ + const rcStr = __rcStr(rc); + if('object'===typeof args[1]){ + super(rcStr,args[1]); + }else{ + args[0] = rcStr+':'; + super(args.join(' ')); + } + } + }else{ + if(2===args.length && 'object'===typeof args[1]){ + super(...args); + }else{ + super(args.join(' ')); + } + } } + this.resultCode = rc || capi.SQLITE_ERROR; this.name = 'SQLite3Error'; } }; @@ -338,12 +321,14 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( }; /** - Returns true if v appears to be one of our bind()-able - TypedArray types: Uint8Array or Int8Array. Support for - TypedArrays with element sizes >1 is TODO. + Returns true if v appears to be one of our bind()-able TypedArray + types: Uint8Array or Int8Array. Support for TypedArrays with + element sizes >1 is a potential TODO just waiting on a use case + to justify them. */ const isBindableTypedArray = (v)=>{ - return v && v.constructor && (1===v.constructor.BYTES_PER_ELEMENT); + return v && (v instanceof Uint8Array || v instanceof Int8Array); + //v && v.constructor && (1===v.constructor.BYTES_PER_ELEMENT); }; /** @@ -356,7 +341,8 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( isSQLableTypedArray() list. */ const isSQLableTypedArray = (v)=>{ - return v && v.constructor && (1===v.constructor.BYTES_PER_ELEMENT); + return v && (v instanceof Uint8Array || v instanceof Int8Array); + //v && v.constructor && (1===v.constructor.BYTES_PER_ELEMENT); }; /** Returns true if isBindableTypedArray(v) does, else throws with a message @@ -383,13 +369,13 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( /** If v is-a Array, its join("") result is returned. If isSQLableTypedArray(v) is true then typedArrayToString(v) is - returned. If it looks like a WASM pointer, wasm.cstringToJs(v) is + returned. If it looks like a WASM pointer, wasm.cstrToJs(v) is returned. Else v is returned as-is. */ const flexibleString = function(v){ if(isSQLableTypedArray(v)) return typedArrayToString(v); else if(Array.isArray(v)) return v.join(""); - else if(wasm.isPtr(v)) v = wasm.cstringToJs(v); + else if(wasm.isPtr(v)) v = wasm.cstrToJs(v); return v; }; @@ -408,7 +394,7 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( message. */ constructor(...args){ - if(2===args.length && 'object'===typeof args){ + if(2===args.length && 'object'===typeof args[1]){ super(...args); }else if(args.length){ super(args.join(' ')); @@ -600,7 +586,7 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( It returns its result and compiled statement as documented in the C API. Fetching the output pointers (5th and 6th - parameters) requires using `capi.wasm.getMemValue()` (or + parameters) requires using `capi.wasm.peek()` (or equivalent) and the `pzTail` will point to an address relative to the `sqlAsPointer` value. @@ -637,7 +623,7 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( If the callback is not a JS function then this binding performs no translation of the callback, but the sql argument is still converted to a WASM string for the call using the - "flexible-string" argument converter. + "string:flexible" argument converter. */ sqlite3_exec: (pDb, sql, callback, pVoid, pErrMsg)=>{}/*installed later*/, @@ -670,8 +656,11 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( isBindableTypedArray, isInt32, isSQLableTypedArray, isTypedArray, typedArrayToString, - isUIThread: ()=>'undefined'===typeof WorkerGlobalScope, + isUIThread: ()=>(self.window===self && !!self.document), + // is this true for ESM?: 'undefined'===typeof WorkerGlobalScope isSharedTypedArray, + toss: function(...args){throw new Error(args.join(' '))}, + toss3, typedArrayPart }; @@ -686,9 +675,7 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( /** The WASM IR (Intermediate Representation) value for pointer-type values. It MUST refer to a value type of the - size described by this.ptrSizeof _or_ it may be any value - which ends in '*', which Emscripten's glue code internally - translates to i32. + size described by this.ptrSizeof. */ ptrIR: config.wasmPtrIR || "i32", /** @@ -717,12 +704,12 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( "or config.memory (imported)."), /** - The API's one single point of access to the WASM-side memory - allocator. Works like malloc(3) (and is likely bound to - malloc()) but throws an WasmAllocError if allocation fails. It is - important that any code which might pass through the sqlite3 C - API NOT throw and must instead return SQLITE_NOMEM (or - equivalent, depending on the context). + The API's primary point of access to the WASM-side memory + allocator. Works like sqlite3_malloc() but throws a + WasmAllocError if allocation fails. It is important that any + code which might pass through the sqlite3 C API NOT throw and + must instead return SQLITE_NOMEM (or equivalent, depending on + the context). Very few cases in the sqlite3 JS APIs can result in client-defined functions propagating exceptions via the C-style @@ -734,7 +721,7 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( catch exceptions and convert them to appropriate error codes. For cases where non-throwing allocation is required, use - sqlite3.wasm.alloc.impl(), which is direct binding of the + this.alloc.impl(), which is direct binding of the underlying C-level allocator. Design note: this function is not named "malloc" primarily @@ -745,9 +732,27 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( alloc: undefined/*installed later*/, /** - The API's one single point of access to the WASM-side memory - deallocator. Works like free(3) (and is likely bound to - free()). + Rarely necessary in JS code, this routine works like + sqlite3_realloc(M,N), where M is either NULL or a pointer + obtained from this function or this.alloc() and N is the number + of bytes to reallocate the block to. Returns a pointer to the + reallocated block or 0 if allocation fails. + + If M is NULL and N is positive, this behaves like + this.alloc(N). If N is 0, it behaves like this.dealloc(). + Results are undefined if N is negative (sqlite3_realloc() + treats that as 0, but if this code is built with a different + allocator it may misbehave with negative values). + + Like this.alloc.impl(), this.realloc.impl() is a direct binding + to the underlying realloc() implementation which does not throw + exceptions, instead returning 0 on allocation error. + */ + realloc: undefined/*installed later*/, + + /** + The API's primary point of access to the WASM-side memory + deallocator. Works like sqlite3_free(). Design note: this function is not named "free" for the same reason that this.alloc() is not called this.malloc(). @@ -764,18 +769,20 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( returned pointer must eventually be passed to wasm.dealloc() to clean it up. + The argument may be a Uint8Array, Int8Array, or ArrayBuffer, + and it throws if passed any other type. + As a special case, to avoid further special cases where this is used, if srcTypedArray.byteLength is 0, it allocates a single byte and sets it to the value 0. Even in such cases, calls must behave as if the allocated memory has exactly srcTypedArray.byteLength bytes. - - ACHTUNG: this currently only works for Uint8Array and - Int8Array types and will throw if srcTypedArray is of - any other type. */ wasm.allocFromTypedArray = function(srcTypedArray){ + if(srcTypedArray instanceof ArrayBuffer){ + srcTypedArray = new Uint8Array(srcTypedArray); + } affirmBindableTypedArray(srcTypedArray); const pRet = wasm.alloc(srcTypedArray.byteLength || 1); wasm.heapForSize(srcTypedArray.constructor).set( @@ -784,24 +791,31 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( return pRet; }; - const keyAlloc = config.allocExportName || 'malloc', - keyDealloc = config.deallocExportName || 'free'; - for(const key of [keyAlloc, keyDealloc]){ - const f = wasm.exports[key]; - if(!(f instanceof Function)) toss3("Missing required exports[",key,"] function."); - } + { + // Set up allocators... + const keyAlloc = config.allocExportName, + keyDealloc = config.deallocExportName, + keyRealloc = config.reallocExportName; + for(const key of [keyAlloc, keyDealloc, keyRealloc]){ + const f = wasm.exports[key]; + if(!(f instanceof Function)) toss3("Missing required exports[",key,"] function."); + } - wasm.alloc = function f(n){ - const m = f.impl(n); - if(!m) throw new WasmAllocError("Failed to allocate",n," bytes."); - return m; - }; - wasm.alloc.impl = wasm.exports[keyAlloc]; - wasm.dealloc = wasm.exports[keyDealloc]; + wasm.alloc = function f(n){ + return f.impl(n) || WasmAllocError.toss("Failed to allocate",n," bytes."); + }; + wasm.alloc.impl = wasm.exports[keyAlloc]; + wasm.realloc = function f(m,n){ + const m2 = f.impl(m,n); + return n ? (m2 || WasmAllocError.toss("Failed to reallocate",n," bytes.")) : 0; + }; + wasm.realloc.impl = wasm.exports[keyRealloc]; + wasm.dealloc = wasm.exports[keyDealloc]; + } /** Reports info about compile-time options using - sqlite_compileoption_get() and sqlite3_compileoption_used(). It + sqlite3_compileoption_get() and sqlite3_compileoption_used(). It has several distinct uses: If optName is an array then it is expected to be a list of @@ -864,165 +878,6 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( ) ? !!capi.sqlite3_compileoption_used(optName) : false; }/*compileOptionUsed()*/; - /** - Signatures for the WASM-exported C-side functions. Each entry - is an array with 2+ elements: - - [ "c-side name", - "result type" (wasm.xWrap() syntax), - [arg types in xWrap() syntax] - // ^^^ this needn't strictly be an array: it can be subsequent - // elements instead: [x,y,z] is equivalent to x,y,z - ] - - Note that support for the API-specific data types in the - result/argument type strings gets plugged in at a later phase in - the API initialization process. - */ - wasm.bindingSignatures = [ - // Please keep these sorted by function name! - ["sqlite3_aggregate_context","void*", "sqlite3_context*", "int"], - ["sqlite3_bind_blob","int", "sqlite3_stmt*", "int", "*", "int", "*" - /* TODO: we should arguably write a custom wrapper which knows - how to handle Blob, TypedArrays, and JS strings. */ - ], - ["sqlite3_bind_double","int", "sqlite3_stmt*", "int", "f64"], - ["sqlite3_bind_int","int", "sqlite3_stmt*", "int", "int"], - ["sqlite3_bind_null",undefined, "sqlite3_stmt*", "int"], - ["sqlite3_bind_parameter_count", "int", "sqlite3_stmt*"], - ["sqlite3_bind_parameter_index","int", "sqlite3_stmt*", "string"], - ["sqlite3_bind_text","int", "sqlite3_stmt*", "int", "string", "int", "int" - /* We should arguably create a hand-written binding of - bind_text() which does more flexible text conversion, along - the lines of sqlite3_prepare_v3(). The slightly problematic - part is the final argument (text destructor). */ - ], - ["sqlite3_close_v2", "int", "sqlite3*"], - ["sqlite3_changes", "int", "sqlite3*"], - ["sqlite3_clear_bindings","int", "sqlite3_stmt*"], - ["sqlite3_column_blob","*", "sqlite3_stmt*", "int"], - ["sqlite3_column_bytes","int", "sqlite3_stmt*", "int"], - ["sqlite3_column_count", "int", "sqlite3_stmt*"], - ["sqlite3_column_double","f64", "sqlite3_stmt*", "int"], - ["sqlite3_column_int","int", "sqlite3_stmt*", "int"], - ["sqlite3_column_name","string", "sqlite3_stmt*", "int"], - ["sqlite3_column_text","string", "sqlite3_stmt*", "int"], - ["sqlite3_column_type","int", "sqlite3_stmt*", "int"], - ["sqlite3_compileoption_get", "string", "int"], - ["sqlite3_compileoption_used", "int", "string"], - /* sqlite3_create_function(), sqlite3_create_function_v2(), and - sqlite3_create_window_function() use hand-written bindings to - simplify handling of their function-type arguments. */ - ["sqlite3_data_count", "int", "sqlite3_stmt*"], - ["sqlite3_db_filename", "string", "sqlite3*", "string"], - ["sqlite3_db_handle", "sqlite3*", "sqlite3_stmt*"], - ["sqlite3_db_name", "string", "sqlite3*", "int"], - ["sqlite3_deserialize", "int", "sqlite3*", "string", "*", "i64", "i64", "int"] - /* Careful! Short version: de/serialize() are problematic because they - might use a different allocator than the user for managing the - deserialized block. de/serialize() are ONLY safe to use with - sqlite3_malloc(), sqlite3_free(), and its 64-bit variants. */, - ["sqlite3_errmsg", "string", "sqlite3*"], - ["sqlite3_error_offset", "int", "sqlite3*"], - ["sqlite3_errstr", "string", "int"], - /*["sqlite3_exec", "int", "sqlite3*", "string", "*", "*", "**" - Handled seperately to perform translation of the callback - into a WASM-usable one. ],*/ - ["sqlite3_expanded_sql", "string", "sqlite3_stmt*"], - ["sqlite3_extended_errcode", "int", "sqlite3*"], - ["sqlite3_extended_result_codes", "int", "sqlite3*", "int"], - ["sqlite3_file_control", "int", "sqlite3*", "string", "int", "*"], - ["sqlite3_finalize", "int", "sqlite3_stmt*"], - ["sqlite3_free", undefined,"*"], - ["sqlite3_initialize", undefined], - /*["sqlite3_interrupt", undefined, "sqlite3*" - ^^^ we cannot actually currently support this because JS is - single-threaded and we don't have a portable way to access a DB - from 2 SharedWorkers concurrently. ],*/ - ["sqlite3_libversion", "string"], - ["sqlite3_libversion_number", "int"], - ["sqlite3_malloc", "*","int"], - ["sqlite3_open", "int", "string", "*"], - ["sqlite3_open_v2", "int", "string", "*", "int", "string"], - /* sqlite3_prepare_v2() and sqlite3_prepare_v3() are handled - separately due to us requiring two different sets of semantics - for those, depending on how their SQL argument is provided. */ - /* sqlite3_randomness() uses a hand-written wrapper to extend - the range of supported argument types. */ - ["sqlite3_realloc", "*","*","int"], - ["sqlite3_reset", "int", "sqlite3_stmt*"], - ["sqlite3_result_blob",undefined, "*", "*", "int", "*"], - ["sqlite3_result_double",undefined, "*", "f64"], - ["sqlite3_result_error",undefined, "*", "string", "int"], - ["sqlite3_result_error_code", undefined, "*", "int"], - ["sqlite3_result_error_nomem", undefined, "*"], - ["sqlite3_result_error_toobig", undefined, "*"], - ["sqlite3_result_int",undefined, "*", "int"], - ["sqlite3_result_null",undefined, "*"], - ["sqlite3_result_text",undefined, "*", "string", "int", "*"], - ["sqlite3_serialize","*", "sqlite3*", "string", "*", "int"], - ["sqlite3_shutdown", undefined], - ["sqlite3_sourceid", "string"], - ["sqlite3_sql", "string", "sqlite3_stmt*"], - ["sqlite3_step", "int", "sqlite3_stmt*"], - ["sqlite3_strglob", "int", "string","string"], - ["sqlite3_strlike", "int", "string","string","int"], - ["sqlite3_trace_v2", "int", "sqlite3*", "int", "*", "*"], - ["sqlite3_total_changes", "int", "sqlite3*"], - ["sqlite3_uri_boolean", "int", "string", "string", "int"], - ["sqlite3_uri_key", "string", "string", "int"], - ["sqlite3_uri_parameter", "string", "string", "string"], - ["sqlite3_user_data","void*", "sqlite3_context*"], - ["sqlite3_value_blob", "*", "sqlite3_value*"], - ["sqlite3_value_bytes","int", "sqlite3_value*"], - ["sqlite3_value_double","f64", "sqlite3_value*"], - ["sqlite3_value_int","int", "sqlite3_value*"], - ["sqlite3_value_text", "string", "sqlite3_value*"], - ["sqlite3_value_type", "int", "sqlite3_value*"], - ["sqlite3_vfs_find", "*", "string"], - ["sqlite3_vfs_register", "int", "sqlite3_vfs*", "int"], - ["sqlite3_vfs_unregister", "int", "sqlite3_vfs*"] - ]/*wasm.bindingSignatures*/; - - if(false && wasm.compileOptionUsed('SQLITE_ENABLE_NORMALIZE')){ - /* ^^^ "the problem" is that this is an option feature and the - build-time function-export list does not currently take - optional features into account. */ - wasm.bindingSignatures.push(["sqlite3_normalized_sql", "string", "sqlite3_stmt*"]); - } - - /** - Functions which require BigInt (int64) support are separated from - the others because we need to conditionally bind them or apply - dummy impls, depending on the capabilities of the environment. - */ - wasm.bindingSignatures.int64 = [ - ["sqlite3_bind_int64","int", ["sqlite3_stmt*", "int", "i64"]], - ["sqlite3_changes64","i64", ["sqlite3*"]], - ["sqlite3_column_int64","i64", ["sqlite3_stmt*", "int"]], - ["sqlite3_malloc64", "*","i64"], - ["sqlite3_msize", "i64", "*"], - ["sqlite3_realloc64", "*","*", "i64"], - ["sqlite3_result_int64",undefined, "*", "i64"], - ["sqlite3_total_changes64", "i64", ["sqlite3*"]], - ["sqlite3_uri_int64", "i64", ["string", "string", "i64"]], - ["sqlite3_value_int64","i64", "sqlite3_value*"], - ]; - - /** - Functions which are intended solely for API-internal use by the - WASM components, not client code. These get installed into - sqlite3.wasm. - */ - wasm.bindingSignatures.wasm = [ - ["sqlite3_wasm_db_reset", "int", "sqlite3*"], - ["sqlite3_wasm_db_vfs", "sqlite3_vfs*", "sqlite3*","string"], - ["sqlite3_wasm_vfs_create_file", "int", - "sqlite3_vfs*","string","*", "int"], - ["sqlite3_wasm_vfs_unlink", "int", "sqlite3_vfs*","string"] - ]; - - /** sqlite3.wasm.pstack (pseudo-stack) holds a special-case stack-style allocator intended only for use with _small_ data of @@ -1052,7 +907,7 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( space managed by Emscripten's stack-management, so does not collide with Emscripten-provided stack allocation APIs. The memory lives in the WASM heap and can be used with routines such - as wasm.setMemValue() and any wasm.heap8u().slice(). + as wasm.poke() and any wasm.heap8u().slice(). */ wasm.pstack = Object.assign(Object.create(null),{ /** @@ -1111,7 +966,7 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( When a returned pointers will refer to a 64-bit value, e.g. a double or int64, and that value must be written or fetched, - e.g. using wasm.setMemValue() or wasm.getMemValue(), it is + e.g. using wasm.poke() or wasm.peek(), it is important that the pointer in question be aligned to an 8-byte boundary or else it will not be fetched or written properly and will corrupt or read neighboring memory. @@ -1195,6 +1050,13 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( /** State for sqlite3_wasmfs_opfs_dir(). */ let __wasmfsOpfsDir = undefined; /** + 2022-12-17: incompatible WASMFS changes have made WASMFS+OPFS + unavailable from the main thread, which eliminates the most + significant benefit of supporting WASMFS. This function is now a + no-op which always returns a falsy value. Before that change, + this function behaved as documented below (and how it will again + if we can find a compelling reason to support it). + If the wasm environment has a WASMFS/OPFS-backed persistent storage directory, its path is returned by this function. If it does not then it returns "" (noting that "" is a falsy value). @@ -1211,6 +1073,8 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( if(undefined !== __wasmfsOpfsDir) return __wasmfsOpfsDir; // If we have no OPFS, there is no persistent dir const pdir = config.wasmfsOpfsDir; + console.error("sqlite3_wasmfs_opfs_dir() can no longer work due "+ + "to incompatible WASMFS changes. It will be removed."); if(!pdir || !self.FileSystemHandle || !self.FileSystemDirectoryHandle @@ -1298,7 +1162,7 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( let pVfs = capi.sqlite3_vfs_find(0); while(pVfs){ const oVfs = new capi.sqlite3_vfs(pVfs); - rc.push(wasm.cstringToJs(oVfs.$zName)); + rc.push(wasm.cstrToJs(oVfs.$zName)); pVfs = oVfs.$pNext; oVfs.dispose(); } @@ -1306,17 +1170,26 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( }; /** - Serializes the given `sqlite3*` pointer to a Uint8Array, as per - sqlite3_serialize(). On success it returns a Uint8Array. On - error it throws with a description of the problem. + A convenience wrapper around sqlite3_serialize() which serializes + the given `sqlite3*` pointer to a Uint8Array. The first argument + may be either an `sqlite3*` or an sqlite3.oo1.DB instance. + + On success it returns a Uint8Array. If the schema is empty, an + empty array is returned. + + `schema` is the schema to serialize. It may be a WASM C-string + pointer or a JS string. If it is falsy, it defaults to `"main"`. + + On error it throws with a description of the problem. */ - capi.sqlite3_js_db_export = function(pDb){ + capi.sqlite3_js_db_export = function(pDb, schema=0){ + pDb = wasm.xWrap.testConvertArg('sqlite3*', pDb); if(!pDb) toss3('Invalid sqlite3* argument.'); if(!wasm.bigIntEnabled) toss3('BigInt64 support is not enabled.'); - const stack = wasm.pstack.pointer; + const scope = wasm.scopedAllocPush(); let pOut; try{ - const pSize = wasm.pstack.alloc(8/*i64*/ + wasm.ptrSizeof); + const pSize = wasm.scopedAlloc(8/*i64*/ + wasm.ptrSizeof); const ppOut = pSize + 8; /** Maintenance reminder, since this cost a full hour of grief @@ -1325,22 +1198,25 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( export reads a garbage size because it's not on an 8-byte memory boundary! */ + const zSchema = schema + ? (wasm.isPtr(schema) ? schema : wasm.scopedAllocCString(''+schema)) + : 0; let rc = wasm.exports.sqlite3_wasm_db_serialize( - pDb, ppOut, pSize, 0 + pDb, zSchema, ppOut, pSize, 0 ); if(rc){ toss3("Database serialization failed with code", sqlite3.capi.sqlite3_js_rc_str(rc)); } - pOut = wasm.getPtrValue(ppOut); - const nOut = wasm.getMemValue(pSize, 'i64'); + pOut = wasm.peekPtr(ppOut); + const nOut = wasm.peek(pSize, 'i64'); rc = nOut ? wasm.heap8u().slice(pOut, pOut + Number(nOut)) : new Uint8Array(); return rc; }finally{ if(pOut) wasm.exports.sqlite3_free(pOut); - wasm.pstack.restore(stack); + wasm.scopedAllocPop(scope); } }; @@ -1366,6 +1242,86 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( "bytes for sqlite3_aggregate_context()") : 0); }; + + /** + Creates a file using the storage appropriate for the given + sqlite3_vfs. The first argument may be a VFS name (JS string + only, NOT a WASM C-string), WASM-managed `sqlite3_vfs*`, or + a capi.sqlite3_vfs instance. Pass 0 (a NULL pointer) to use the + default VFS. If passed a string which does not resolve using + sqlite3_vfs_find(), an exception is thrown. (Note that a WASM + C-string is not accepted because it is impossible to + distinguish from a C-level `sqlite3_vfs*`.) + + The second argument, the filename, must be a JS or WASM C-string. + + The 3rd may either be falsy, a valid WASM memory pointer, an + ArrayBuffer, or a Uint8Array. The 4th must be the length, in + bytes, of the data array to copy. If the 3rd argument is an + ArrayBuffer or Uint8Array and the 4th is not a positive integer + then the 4th defaults to the array's byteLength value. + + If data is falsy then a file is created with dataLen bytes filled + with uninitialized data (whatever truncate() leaves there). If + data is not falsy then a file is created or truncated and it is + filled with the first dataLen bytes of the data source. + + Throws if any arguments are invalid or if creating or writing to + the file fails. + + Note that most VFSes do _not_ automatically create directory + parts of filenames, nor do all VFSes have a concept of + directories. If the given filename is not valid for the given + VFS, an exception will be thrown. This function exists primarily + to assist in implementing file-upload capability, with the caveat + that clients must have some idea of the VFS into which they want + to upload and that VFS must support the operation. + + VFS-specific notes: + + - "memdb": results are undefined. + + - "kvvfs": will fail with an I/O error due to strict internal + requirments of that VFS's xTruncate(). + + - "unix" and related: will use the WASM build's equivalent of the + POSIX I/O APIs. This will work so long as neither a specific + VFS nor the WASM environment imposes requirements which break it. + + - "opfs": uses OPFS storage and creates directory parts of the + filename. + */ + capi.sqlite3_js_vfs_create_file = function(vfs, filename, data, dataLen){ + let pData; + if(data){ + if(wasm.isPtr(data)){ + pData = data; + }else if(data instanceof ArrayBuffer){ + data = new Uint8Array(data); + } + if(data instanceof Uint8Array){ + pData = wasm.allocFromTypedArray(data); + if(arguments.length<4 || !util.isInt32(dataLen) || dataLen<0){ + dataLen = data.byteLength; + } + }else{ + SQLite3Error.toss("Invalid 3rd argument type for sqlite3_js_vfs_create_file()."); + } + }else{ + pData = 0; + } + if(!util.isInt32(dataLen) || dataLen<0){ + wasm.dealloc(pData); + SQLite3Error.toss("Invalid 4th argument for sqlite3_js_vfs_create_file()."); + } + try{ + const rc = wasm.sqlite3_wasm_vfs_create_file(vfs, filename, pData, dataLen); + if(rc) SQLite3Error.toss("Creation of file failed with sqlite3 result code", + capi.sqlite3_js_rc_str(rc)); + }finally{ + wasm.dealloc(pData); + } + }; if( util.isUIThread() ){ /* Features specific to the main window thread... */ @@ -1447,6 +1403,265 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( }/* main-window-only bits */ + /** + Wraps all known variants of the C-side variadic + sqlite3_db_config(). + + Full docs: https://sqlite.org/c3ref/db_config.html + + Returns capi.SQLITE_MISUSE if op is not a valid operation ID. + */ + capi.sqlite3_db_config = function f(pDb, op, ...args){ + if(!this.s){ + this.s = wasm.xWrap('sqlite3_wasm_db_config_s','int', + ['sqlite3*', 'int', 'string:static'] + /* MAINDBNAME requires a static string */); + this.pii = wasm.xWrap('sqlite3_wasm_db_config_pii', 'int', + ['sqlite3*', 'int', '*','int', 'int']); + this.ip = wasm.xWrap('sqlite3_wasm_db_config_ip','int', + ['sqlite3*', 'int', 'int','*']); + } + const c = capi; + switch(op){ + case c.SQLITE_DBCONFIG_ENABLE_FKEY: + case c.SQLITE_DBCONFIG_ENABLE_TRIGGER: + case c.SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER: + case c.SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION: + case c.SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE: + case c.SQLITE_DBCONFIG_ENABLE_QPSG: + case c.SQLITE_DBCONFIG_TRIGGER_EQP: + case c.SQLITE_DBCONFIG_RESET_DATABASE: + case c.SQLITE_DBCONFIG_DEFENSIVE: + case c.SQLITE_DBCONFIG_WRITABLE_SCHEMA: + case c.SQLITE_DBCONFIG_LEGACY_ALTER_TABLE: + case c.SQLITE_DBCONFIG_DQS_DML: + case c.SQLITE_DBCONFIG_DQS_DDL: + case c.SQLITE_DBCONFIG_ENABLE_VIEW: + case c.SQLITE_DBCONFIG_LEGACY_FILE_FORMAT: + case c.SQLITE_DBCONFIG_TRUSTED_SCHEMA: + return this.ip(pDb, op, args[0], args[1] || 0); + case c.SQLITE_DBCONFIG_LOOKASIDE: + return this.pii(pDb, op, args[0], args[1], args[2]); + case c.SQLITE_DBCONFIG_MAINDBNAME: + return this.s(pDb, op, args[0]); + default: + return c.SQLITE_MISUSE; + } + }.bind(Object.create(null)); + + /** + Given a (sqlite3_value*), this function attempts to convert it + to an equivalent JS value with as much fidelity as feasible and + return it. + + By default it throws if it cannot determine any sensible + conversion. If passed a falsy second argument, it instead returns + `undefined` if no suitable conversion is found. Note that there + is no conversion from SQL to JS which results in the `undefined` + value, so `undefined` has an unambiguous meaning here. It will + always throw a WasmAllocError if allocating memory for a + conversion fails. + + Caveats: + + - It does not support sqlite3_value_to_pointer() conversions + because those require a type name string which this function + does not have and cannot sensibly be given at the level of the + API where this is used (e.g. automatically converting UDF + arguments). Clients using sqlite3_value_to_pointer(), and its + related APIs, will need to manage those themselves. + */ + capi.sqlite3_value_to_js = function(pVal,throwIfCannotConvert=true){ + let arg; + const valType = capi.sqlite3_value_type(pVal); + switch(valType){ + case capi.SQLITE_INTEGER: + if(wasm.bigIntEnabled){ + arg = capi.sqlite3_value_int64(pVal); + if(util.bigIntFitsDouble(arg)) arg = Number(arg); + } + else arg = capi.sqlite3_value_double(pVal)/*yes, double, for larger integers*/; + break; + case capi.SQLITE_FLOAT: + arg = capi.sqlite3_value_double(pVal); + break; + case capi.SQLITE_TEXT: + arg = capi.sqlite3_value_text(pVal); + break; + case capi.SQLITE_BLOB:{ + const n = capi.sqlite3_value_bytes(pVal); + const pBlob = capi.sqlite3_value_blob(pVal); + if(n && !pBlob) sqlite3.WasmAllocError.toss( + "Cannot allocate memory for blob argument of",n,"byte(s)" + ); + arg = n ? wasm.heap8u().slice(pBlob, pBlob + Number(n)) : null; + break; + } + case capi.SQLITE_NULL: + arg = null; break; + default: + if(throwIfCannotConvert){ + toss3(capi.SQLITE_MISMATCH, + "Unhandled sqlite3_value_type():",valType); + } + arg = undefined; + } + return arg; + }; + + /** + Requires a C-style array of `sqlite3_value*` objects and the + number of entries in that array. Returns a JS array containing + the results of passing each C array entry to + sqlite3_value_to_js(). The 3rd argument to this function is + passed on as the 2nd argument to that one. + */ + capi.sqlite3_values_to_js = function(argc,pArgv,throwIfCannotConvert=true){ + let i; + const tgt = []; + for(i = 0; i < argc; ++i){ + /** + Curiously: despite ostensibly requiring 8-byte + alignment, the pArgv array is parcelled into chunks of + 4 bytes (1 pointer each). The values those point to + have 8-byte alignment but the individual argv entries + do not. + */ + tgt.push(capi.sqlite3_value_to_js( + wasm.peekPtr(pArgv + (wasm.ptrSizeof * i)) + )); + } + return tgt; + }; + + /** + Calls either sqlite3_result_error_nomem(), if e is-a + WasmAllocError, or sqlite3_result_error(). In the latter case, + the second arugment is coerced to a string to create the error + message. + + The first argument is a (sqlite3_context*). Returns void. + Does not throw. + */ + capi.sqlite3_result_error_js = function(pCtx,e){ + if(e instanceof WasmAllocError){ + capi.sqlite3_result_error_nomem(pCtx); + }else{ + /* Maintenance reminder: ''+e, rather than e.message, + will prefix e.message with e.name, so it includes + the exception's type name in the result. */; + capi.sqlite3_result_error(pCtx, ''+e, -1); + } + }; + + /** + This function passes its 2nd argument to one of the + sqlite3_result_xyz() routines, depending on the type of that + argument: + + - If (val instanceof Error), this function passes it to + sqlite3_result_error_js(). + - `null`: `sqlite3_result_null()` + - `boolean`: `sqlite3_result_int()` with a value of 0 or 1. + - `number`: `sqlite3_result_int()`, `sqlite3_result_int64()`, or + `sqlite3_result_double()`, depending on the range of the number + and whether or not int64 support is enabled. + - `bigint`: similar to `number` but will trigger an error if the + value is too big to store in an int64. + - `string`: `sqlite3_result_text()` + - Uint8Array or Int8Array: `sqlite3_result_blob()` + - `undefined`: is a no-op provided to simplify certain use cases. + + Anything else triggers `sqlite3_result_error()` with a + description of the problem. + + The first argument to this function is a `(sqlite3_context*)`. + Returns void. Does not throw. + */ + capi.sqlite3_result_js = function(pCtx,val){ + if(val instanceof Error){ + capi.sqlite3_result_error_js(pCtx, val); + return; + } + try{ + switch(typeof val) { + case 'undefined': + /* This is a no-op. This routine originated in the create_function() + family of APIs and in that context, passing in undefined indicated + that the caller was responsible for calling sqlite3_result_xxx() + (if needed). */ + break; + case 'boolean': + capi.sqlite3_result_int(pCtx, val ? 1 : 0); + break; + case 'bigint': + if(util.bigIntFits32(val)){ + capi.sqlite3_result_int(pCtx, Number(val)); + }else if(util.bigIntFitsDouble(val)){ + capi.sqlite3_result_double(pCtx, Number(val)); + }else if(wasm.bigIntEnabled){ + if(util.bigIntFits64(val)) capi.sqlite3_result_int64(pCtx, val); + else toss3("BigInt value",val.toString(),"is too BigInt for int64."); + }else{ + toss3("BigInt value",val.toString(),"is too BigInt."); + } + break; + case 'number': { + let f; + if(util.isInt32(val)){ + f = capi.sqlite3_result_int; + }else if(wasm.bigIntEnabled + && Number.isInteger(val) + && util.bigIntFits64(BigInt(val))){ + f = capi.sqlite3_result_int64; + }else{ + f = capi.sqlite3_result_double; + } + f(pCtx, val); + break; + } + case 'string': + capi.sqlite3_result_text(pCtx, val, -1, capi.SQLITE_TRANSIENT); + break; + case 'object': + if(null===val/*yes, typeof null === 'object'*/) { + capi.sqlite3_result_null(pCtx); + break; + }else if(util.isBindableTypedArray(val)){ + const pBlob = wasm.allocFromTypedArray(val); + capi.sqlite3_result_blob( + pCtx, pBlob, val.byteLength, + wasm.exports[sqlite3.config.deallocExportName] + ); + break; + } + // else fall through + default: + toss3("Don't not how to handle this UDF result value:",(typeof val), val); + } + }catch(e){ + capi.sqlite3_result_error_js(pCtx, e); + } + }; + + /** + Returns the result sqlite3_column_value(pStmt,iCol) passed to + sqlite3_value_to_js(). The 3rd argument of this function is + ignored by this function except to pass it on as the second + argument of sqlite3_value_to_js(). If the sqlite3_column_value() + returns NULL (e.g. because the column index is out of range), + this function returns `undefined`, regardless of the 3rd + argument. 3rd argument is falsy and conversion fails, `undefined` + will be returned. + + Note that sqlite3_column_value() returns an "unprotected" value + object, but in a single-threaded environment (like this one) + there is no distinction between protected and unprotected values. + */ + capi.sqlite3_column_js = function(pStmt, iCol, throwIfCannotConvert=true){ + const v = capi.sqlite3_column_value(pStmt, iCol); + return (0===v) ? undefined : capi.sqlite3_value_to_js(v, throwIfCannotConvert); + }; /* The remainder of the API will be set up in later steps. */ const sqlite3 = { @@ -1467,6 +1682,15 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( build-generated object. */ version: Object.create(null), + + /** + The library reserves the 'client' property for client-side use + and promises to never define a property with this name nor to + ever rely on specific contents of it. It makes no such guarantees + for other properties. + */ + client: undefined, + /** Performs any optional asynchronous library-level initialization which might be required. This function returns a Promise which @@ -1496,9 +1720,6 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( let lip = sqlite3ApiBootstrap.initializersAsync; delete sqlite3ApiBootstrap.initializersAsync; if(!lip || !lip.length) return Promise.resolve(sqlite3); - // Is it okay to resolve these in parallel or do we need them - // to resolve in order? We currently only have 1, so it - // makes no difference. lip = lip.map((f)=>{ const p = (f instanceof Promise) ? f : f(sqlite3); return p.catch((e)=>{ @@ -1506,10 +1727,32 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( throw e; }); }); - //let p = lip.shift(); - //while(lip.length) p = p.then(lip.shift()); - //return p.then(()=>sqlite3); - return Promise.all(lip).then(()=>sqlite3); + const postInit = ()=>{ + if(!sqlite3.__isUnderTest){ + /* Delete references to internal-only APIs which are used by + some initializers. Retain them when running in test mode + so that we can add tests for them. */ + delete sqlite3.util; + /* It's conceivable that we might want to expose + StructBinder to client-side code, but it's only useful if + clients build their own sqlite3.wasm which contains their + one C struct types. */ + delete sqlite3.StructBinder; + } + return sqlite3; + }; + if(1){ + /* Run all initializers in sequence. The advantage is that it + allows us to have post-init cleanup defined outside of this + routine at the end of the list and have it run at a + well-defined time. */ + let p = lip.shift(); + while(lip.length) p = p.then(lip.shift()); + return p.then(postInit); + }else{ + /* Run them in an arbitrary order. */ + return Promise.all(lip).then(postInit); + } }, /** scriptInfo ideally gets injected into this object by the diff --git a/ext/wasm/api/sqlite3-opfs-async-proxy.js b/ext/wasm/api/sqlite3-opfs-async-proxy.js index e4657484ef..1456ae08d2 100644 --- a/ext/wasm/api/sqlite3-opfs-async-proxy.js +++ b/ext/wasm/api/sqlite3-opfs-async-proxy.js @@ -13,7 +13,7 @@ A Worker which manages asynchronous OPFS handles on behalf of a synchronous API which controls it via a combination of Worker messages, SharedArrayBuffer, and Atomics. It is the asynchronous - counterpart of the API defined in sqlite3-api-opfs.js. + counterpart of the API defined in sqlite3-vfs-opfs.js. Highly indebted to: @@ -29,802 +29,869 @@ This file represents an implementation detail of a larger piece of code, and not a public interface. Its details may change at any time and are not intended to be used by any client-level code. + + 2022-11-27: Chrome v108 changes some async methods to synchronous, as + documented at: + + https://developer.chrome.com/blog/sync-methods-for-accesshandles/ + + We cannot change to the sync forms at this point without breaking + clients who use Chrome v104-ish or higher. truncate(), getSize(), + flush(), and close() are now (as of v108) synchronous. Calling them + with an "await", as we have to for the async forms, is still legal + with the sync forms but is superfluous. Calling the async forms with + theFunc().then(...) is not compatible with the change to + synchronous, but we do do not use those APIs that way. i.e. we don't + _need_ to change anything for this, but at some point (after Chrome + versions (approximately) 104-107 are extinct) should change our + usage of those methods to remove the "await". */ "use strict"; -const toss = function(...args){throw new Error(args.join(' '))}; -if(self.window === self){ - toss("This code cannot run from the main thread.", - "Load it as a Worker from a separate Worker."); -}else if(!navigator.storage.getDirectory){ - toss("This API requires navigator.storage.getDirectory."); -} - -/** - Will hold state copied to this object from the syncronous side of - this API. -*/ -const state = Object.create(null); - -/** - verbose: - - 0 = no logging output - 1 = only errors - 2 = warnings and errors - 3 = debug, warnings, and errors -*/ -state.verbose = 2; - -const loggers = { - 0:console.error.bind(console), - 1:console.warn.bind(console), - 2:console.log.bind(console) -}; -const logImpl = (level,...args)=>{ - if(state.verbose>level) loggers[level]("OPFS asyncer:",...args); -}; -const log = (...args)=>logImpl(2, ...args); -const warn = (...args)=>logImpl(1, ...args); -const error = (...args)=>logImpl(0, ...args); -const metrics = Object.create(null); -metrics.reset = ()=>{ - let k; - const r = (m)=>(m.count = m.time = m.wait = 0); - for(k in state.opIds){ - r(metrics[k] = Object.create(null)); +const wPost = (type,...args)=>postMessage({type, payload:args}); +const installAsyncProxy = function(self){ + const toss = function(...args){throw new Error(args.join(' '))}; + if(self.window === self){ + toss("This code cannot run from the main thread.", + "Load it as a Worker from a separate Worker."); + }else if(!navigator.storage.getDirectory){ + toss("This API requires navigator.storage.getDirectory."); } - let s = metrics.s11n = Object.create(null); - s = s.serialize = Object.create(null); - s.count = s.time = 0; - s = metrics.s11n.deserialize = Object.create(null); - s.count = s.time = 0; -}; -metrics.dump = ()=>{ - let k, n = 0, t = 0, w = 0; - for(k in state.opIds){ - const m = metrics[k]; - n += m.count; - t += m.time; - w += m.wait; - m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0; - } - console.log(self.location.href, - "metrics for",self.location.href,":\n", - metrics, - "\nTotal of",n,"op(s) for",t,"ms", - "approx",w,"ms spent waiting on OPFS APIs."); - console.log("Serialization metrics:",metrics.s11n); -}; -/** - __openFiles is a map of sqlite3_file pointers (integers) to - metadata related to a given OPFS file handles. The pointers are, in - this side of the interface, opaque file handle IDs provided by the - synchronous part of this constellation. Each value is an object - with a structure demonstrated in the xOpen() impl. -*/ -const __openFiles = Object.create(null); -/** - __autoLocks is a Set of sqlite3_file pointers (integers) which were - "auto-locked". i.e. those for which we obtained a sync access - handle without an explicit xLock() call. Such locks will be - released during db connection idle time, whereas a sync access - handle obtained via xLock(), or subsequently xLock()'d after - auto-acquisition, will not be released until xUnlock() is called. - - Maintenance reminder: if we relinquish auto-locks at the end of the - operation which acquires them, we pay a massive performance - penalty: speedtest1 benchmarks take up to 4x as long. By delaying - the lock release until idle time, the hit is negligible. -*/ -const __autoLocks = new Set(); - -/** - Expects an OPFS file path. It gets resolved, such that ".." - components are properly expanded, and returned. If the 2nd arg is - true, the result is returned as an array of path elements, else an - absolute path string is returned. -*/ -const getResolvedPath = function(filename,splitIt){ - const p = new URL( - filename, 'file://irrelevant' - ).pathname; - return splitIt ? p.split('/').filter((v)=>!!v) : p; -}; - -/** - Takes the absolute path to a filesystem element. Returns an array - of [handleOfContainingDir, filename]. If the 2nd argument is truthy - then each directory element leading to the file is created along - the way. Throws if any creation or resolution fails. -*/ -const getDirForFilename = async function f(absFilename, createDirs = false){ - const path = getResolvedPath(absFilename, true); - const filename = path.pop(); - let dh = state.rootDir; - for(const dirName of path){ - if(dirName){ - dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs}); - } - } - return [dh, filename]; -}; - -/** - An error class specifically for use with getSyncHandle(), the goal - of which is to eventually be able to distinguish unambiguously - between locking-related failures and other types, noting that we - cannot currently do so because createSyncAccessHandle() does not - define its exceptions in the required level of detail. -*/ -class GetSyncHandleError extends Error { - constructor(errorObject, ...msg){ - super(); - this.error = errorObject; - this.message = [ - ...msg, ': Original exception ['+errorObject.name+']:', - errorObject.message - ].join(' '); - this.name = 'GetSyncHandleError'; - } -}; - -/** - Returns the sync access handle associated with the given file - handle object (which must be a valid handle object, as created by - xOpen()), lazily opening it if needed. - - In order to help alleviate cross-tab contention for a dabase, - if an exception is thrown while acquiring the handle, this routine - will wait briefly and try again, up to 3 times. If acquisition - still fails at that point it will give up and propagate the - exception. -*/ -const getSyncHandle = async (fh)=>{ - if(!fh.syncHandle){ - const t = performance.now(); - log("Acquiring sync handle for",fh.filenameAbs); - const maxTries = 4, msBase = 300; - let i = 1, ms = msBase; - for(; true; ms = msBase * ++i){ - try { - //if(i<3) toss("Just testing getSyncHandle() wait-and-retry."); - //TODO? A config option which tells it to throw here - //randomly every now and then, for testing purposes. - fh.syncHandle = await fh.fileHandle.createSyncAccessHandle(); - break; - }catch(e){ - if(i === maxTries){ - throw new GetSyncHandleError( - e, "Error getting sync handle.",maxTries, - "attempts failed.",fh.filenameAbs - ); - } - warn("Error getting sync handle. Waiting",ms, - "ms and trying again.",fh.filenameAbs,e); - Atomics.wait(state.sabOPView, state.opIds.retry, 0, ms); - } - } - log("Got sync handle for",fh.filenameAbs,'in',performance.now() - t,'ms'); - if(!fh.xLock){ - __autoLocks.add(fh.fid); - log("Auto-locked",fh.fid,fh.filenameAbs); - } - } - return fh.syncHandle; -}; - -/** - If the given file-holding object has a sync handle attached to it, - that handle is remove and asynchronously closed. Though it may - sound sensible to continue work as soon as the close() returns - (noting that it's asynchronous), doing so can cause operations - performed soon afterwards, e.g. a call to getSyncHandle() to fail - because they may happen out of order from the close(). OPFS does - not guaranty that the actual order of operations is retained in - such cases. i.e. always "await" on the result of this function. -*/ -const closeSyncHandle = async (fh)=>{ - if(fh.syncHandle){ - log("Closing sync handle for",fh.filenameAbs); - const h = fh.syncHandle; - delete fh.syncHandle; - delete fh.xLock; - __autoLocks.delete(fh.fid); - return h.close(); - } -}; - -/** - A proxy for closeSyncHandle() which is guaranteed to not throw. - - This function is part of a lock/unlock step in functions which - require a sync access handle but may be called without xLock() - having been called first. Such calls need to release that - handle to avoid locking the file for all of time. This is an - _attempt_ at reducing cross-tab contention but it may prove - to be more of a problem than a solution and may need to be - removed. -*/ -const closeSyncHandleNoThrow = async (fh)=>{ - try{await closeSyncHandle(fh)} - catch(e){ - warn("closeSyncHandleNoThrow() ignoring:",e,fh); - } -}; - -/** - Stores the given value at state.sabOPView[state.opIds.rc] and then - Atomics.notify()'s it. -*/ -const storeAndNotify = (opName, value)=>{ - log(opName+"() => notify(",value,")"); - Atomics.store(state.sabOPView, state.opIds.rc, value); - Atomics.notify(state.sabOPView, state.opIds.rc); -}; - -/** - Throws if fh is a file-holding object which is flagged as read-only. -*/ -const affirmNotRO = function(opName,fh){ - if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs); -}; -const affirmLocked = function(opName,fh){ - //if(!fh.syncHandle) toss(opName+"(): File does not have a lock: "+fh.filenameAbs); /** - Currently a no-op, as speedtest1 triggers xRead() without a - lock (that seems like a bug but it's currently uninvestigated). - This means, however, that some OPFS VFS routines may trigger - acquisition of a lock but never let it go until xUnlock() is - called (which it likely won't be if xLock() was not called). + Will hold state copied to this object from the syncronous side of + this API. */ -}; + const state = Object.create(null); -/** - We track 2 different timers: the "metrics" timer records how much - time we spend performing work. The "wait" timer records how much - time we spend waiting on the underlying OPFS timer. See the calls - to mTimeStart(), mTimeEnd(), wTimeStart(), and wTimeEnd() - throughout this file to see how they're used. -*/ -const __mTimer = Object.create(null); -__mTimer.op = undefined; -__mTimer.start = undefined; -const mTimeStart = (op)=>{ - __mTimer.start = performance.now(); - __mTimer.op = op; - //metrics[op] || toss("Maintenance required: missing metrics for",op); - ++metrics[op].count; -}; -const mTimeEnd = ()=>( - metrics[__mTimer.op].time += performance.now() - __mTimer.start -); -const __wTimer = Object.create(null); -__wTimer.op = undefined; -__wTimer.start = undefined; -const wTimeStart = (op)=>{ - __wTimer.start = performance.now(); - __wTimer.op = op; - //metrics[op] || toss("Maintenance required: missing metrics for",op); -}; -const wTimeEnd = ()=>( - metrics[__wTimer.op].wait += performance.now() - __wTimer.start -); + /** + verbose: -/** - Gets set to true by the 'opfs-async-shutdown' command to quit the - wait loop. This is only intended for debugging purposes: we cannot - inspect this file's state while the tight waitLoop() is running and - need a way to stop that loop for introspection purposes. -*/ -let flagAsyncShutdown = false; + 0 = no logging output + 1 = only errors + 2 = warnings and errors + 3 = debug, warnings, and errors + */ + state.verbose = 1; - -/** - Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods - methods, as well as helpers like mkdir(). Maintenance reminder: - members are in alphabetical order to simplify finding them. -*/ -const vfsAsyncImpls = { - 'opfs-async-metrics': async ()=>{ - mTimeStart('opfs-async-metrics'); - metrics.dump(); - storeAndNotify('opfs-async-metrics', 0); - mTimeEnd(); - }, - 'opfs-async-shutdown': async ()=>{ - flagAsyncShutdown = true; - storeAndNotify('opfs-async-shutdown', 0); - }, - mkdir: async (dirname)=>{ - mTimeStart('mkdir'); - let rc = 0; - wTimeStart('mkdir'); - try { - await getDirForFilename(dirname+"/filepart", true); - }catch(e){ - state.s11n.storeException(2,e); - rc = state.sq3Codes.SQLITE_IOERR; - }finally{ - wTimeEnd(); + const loggers = { + 0:console.error.bind(console), + 1:console.warn.bind(console), + 2:console.log.bind(console) + }; + const logImpl = (level,...args)=>{ + if(state.verbose>level) loggers[level]("OPFS asyncer:",...args); + }; + const log = (...args)=>logImpl(2, ...args); + const warn = (...args)=>logImpl(1, ...args); + const error = (...args)=>logImpl(0, ...args); + const metrics = Object.create(null); + metrics.reset = ()=>{ + let k; + const r = (m)=>(m.count = m.time = m.wait = 0); + for(k in state.opIds){ + r(metrics[k] = Object.create(null)); } - storeAndNotify('mkdir', rc); - mTimeEnd(); - }, - xAccess: async (filename)=>{ - mTimeStart('xAccess'); - /* OPFS cannot support the full range of xAccess() queries sqlite3 - calls for. We can essentially just tell if the file is - accessible, but if it is it's automatically writable (unless - it's locked, which we cannot(?) know without trying to open - it). OPFS does not have the notion of read-only. - - The return semantics of this function differ from sqlite3's - xAccess semantics because we are limited in what we can - communicate back to our synchronous communication partner: 0 = - accessible, non-0 means not accessible. - */ - let rc = 0; - wTimeStart('xAccess'); - try{ - const [dh, fn] = await getDirForFilename(filename); - await dh.getFileHandle(fn); - }catch(e){ - state.s11n.storeException(2,e); - rc = state.sq3Codes.SQLITE_IOERR; - }finally{ - wTimeEnd(); + let s = metrics.s11n = Object.create(null); + s = s.serialize = Object.create(null); + s.count = s.time = 0; + s = metrics.s11n.deserialize = Object.create(null); + s.count = s.time = 0; + }; + metrics.dump = ()=>{ + let k, n = 0, t = 0, w = 0; + for(k in state.opIds){ + const m = metrics[k]; + n += m.count; + t += m.time; + w += m.wait; + m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0; } - storeAndNotify('xAccess', rc); - mTimeEnd(); - }, - xClose: async function(fid/*sqlite3_file pointer*/){ - const opName = 'xClose'; - mTimeStart(opName); - __autoLocks.delete(fid); - const fh = __openFiles[fid]; - let rc = 0; - wTimeStart(opName); - if(fh){ - delete __openFiles[fid]; - await closeSyncHandle(fh); - if(fh.deleteOnClose){ - try{ await fh.dirHandle.removeEntry(fh.filenamePart) } - catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) } + console.log(self.location.href, + "metrics for",self.location.href,":\n", + metrics, + "\nTotal of",n,"op(s) for",t,"ms", + "approx",w,"ms spent waiting on OPFS APIs."); + console.log("Serialization metrics:",metrics.s11n); + }; + + /** + __openFiles is a map of sqlite3_file pointers (integers) to + metadata related to a given OPFS file handles. The pointers are, in + this side of the interface, opaque file handle IDs provided by the + synchronous part of this constellation. Each value is an object + with a structure demonstrated in the xOpen() impl. + */ + const __openFiles = Object.create(null); + /** + __implicitLocks is a Set of sqlite3_file pointers (integers) which were + "auto-locked". i.e. those for which we obtained a sync access + handle without an explicit xLock() call. Such locks will be + released during db connection idle time, whereas a sync access + handle obtained via xLock(), or subsequently xLock()'d after + auto-acquisition, will not be released until xUnlock() is called. + + Maintenance reminder: if we relinquish auto-locks at the end of the + operation which acquires them, we pay a massive performance + penalty: speedtest1 benchmarks take up to 4x as long. By delaying + the lock release until idle time, the hit is negligible. + */ + const __implicitLocks = new Set(); + + /** + Expects an OPFS file path. It gets resolved, such that ".." + components are properly expanded, and returned. If the 2nd arg is + true, the result is returned as an array of path elements, else an + absolute path string is returned. + */ + const getResolvedPath = function(filename,splitIt){ + const p = new URL( + filename, 'file://irrelevant' + ).pathname; + return splitIt ? p.split('/').filter((v)=>!!v) : p; + }; + + /** + Takes the absolute path to a filesystem element. Returns an array + of [handleOfContainingDir, filename]. If the 2nd argument is truthy + then each directory element leading to the file is created along + the way. Throws if any creation or resolution fails. + */ + const getDirForFilename = async function f(absFilename, createDirs = false){ + const path = getResolvedPath(absFilename, true); + const filename = path.pop(); + let dh = state.rootDir; + for(const dirName of path){ + if(dirName){ + dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs}); } - }else{ - state.s11n.serialize(); - rc = state.sq3Codes.SQLITE_NOTFOUND; } - wTimeEnd(); - storeAndNotify(opName, rc); - mTimeEnd(); - }, - xDelete: async function(...args){ - mTimeStart('xDelete'); - const rc = await vfsAsyncImpls.xDeleteNoWait(...args); - storeAndNotify('xDelete', rc); - mTimeEnd(); - }, - xDeleteNoWait: async function(filename, syncDir = 0, recursive = false){ - /* The syncDir flag is, for purposes of the VFS API's semantics, - ignored here. However, if it has the value 0x1234 then: after - deleting the given file, recursively try to delete any empty - directories left behind in its wake (ignoring any errors and - stopping at the first failure). + return [dh, filename]; + }; - That said: we don't know for sure that removeEntry() fails if - the dir is not empty because the API is not documented. It has, - however, a "recursive" flag which defaults to false, so - presumably it will fail if the dir is not empty and that flag - is false. - */ - let rc = 0; - wTimeStart('xDelete'); - try { - while(filename){ - const [hDir, filenamePart] = await getDirForFilename(filename, false); - if(!filenamePart) break; - await hDir.removeEntry(filenamePart, {recursive}); - if(0x1234 !== syncDir) break; - recursive = false; - filename = getResolvedPath(filename, true); - filename.pop(); - filename = filename.join('/'); - } - }catch(e){ - state.s11n.storeException(2,e); - rc = state.sq3Codes.SQLITE_IOERR_DELETE; + /** + If the given file-holding object has a sync handle attached to it, + that handle is remove and asynchronously closed. Though it may + sound sensible to continue work as soon as the close() returns + (noting that it's asynchronous), doing so can cause operations + performed soon afterwards, e.g. a call to getSyncHandle() to fail + because they may happen out of order from the close(). OPFS does + not guaranty that the actual order of operations is retained in + such cases. i.e. always "await" on the result of this function. + */ + const closeSyncHandle = async (fh)=>{ + if(fh.syncHandle){ + log("Closing sync handle for",fh.filenameAbs); + const h = fh.syncHandle; + delete fh.syncHandle; + delete fh.xLock; + __implicitLocks.delete(fh.fid); + return h.close(); } - wTimeEnd(); - return rc; - }, - xFileSize: async function(fid/*sqlite3_file pointer*/){ - mTimeStart('xFileSize'); - const fh = __openFiles[fid]; - let rc; - wTimeStart('xFileSize'); - try{ - affirmLocked('xFileSize',fh); - rc = await (await getSyncHandle(fh)).getSize(); - state.s11n.serialize(Number(rc)); - rc = 0; - }catch(e){ - state.s11n.storeException(2,e); - rc = state.sq3Codes.SQLITE_IOERR; + }; + + /** + A proxy for closeSyncHandle() which is guaranteed to not throw. + + This function is part of a lock/unlock step in functions which + require a sync access handle but may be called without xLock() + having been called first. Such calls need to release that + handle to avoid locking the file for all of time. This is an + _attempt_ at reducing cross-tab contention but it may prove + to be more of a problem than a solution and may need to be + removed. + */ + const closeSyncHandleNoThrow = async (fh)=>{ + try{await closeSyncHandle(fh)} + catch(e){ + warn("closeSyncHandleNoThrow() ignoring:",e,fh); } - wTimeEnd(); - storeAndNotify('xFileSize', rc); - mTimeEnd(); - }, - xLock: async function(fid/*sqlite3_file pointer*/, - lockType/*SQLITE_LOCK_...*/){ - mTimeStart('xLock'); - const fh = __openFiles[fid]; - let rc = 0; - const oldLockType = fh.xLock; - fh.xLock = lockType; - if( !fh.syncHandle ){ - wTimeStart('xLock'); - try { - await getSyncHandle(fh); - __autoLocks.delete(fid); - }catch(e){ - state.s11n.storeException(1,e); - rc = state.sq3Codes.SQLITE_IOERR_LOCK; - fh.xLock = oldLockType; + }; + + /* Release all auto-locks. */ + const releaseImplicitLocks = async ()=>{ + if(__implicitLocks.size){ + /* Release all auto-locks. */ + for(const fid of __implicitLocks){ + const fh = __openFiles[fid]; + await closeSyncHandleNoThrow(fh); + log("Auto-unlocked",fid,fh.filenameAbs); } - wTimeEnd(); } - storeAndNotify('xLock',rc); - mTimeEnd(); - }, - xOpen: async function(fid/*sqlite3_file pointer*/, filename, - flags/*SQLITE_OPEN_...*/){ - const opName = 'xOpen'; - mTimeStart(opName); - const deleteOnClose = (state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags); - const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags); - wTimeStart('xOpen'); - try{ - let hDir, filenamePart; - try { - [hDir, filenamePart] = await getDirForFilename(filename, !!create); - }catch(e){ - state.s11n.storeException(1,e); - storeAndNotify(opName, state.sq3Codes.SQLITE_NOTFOUND); - mTimeEnd(); - wTimeEnd(); - return; - } - const hFile = await hDir.getFileHandle(filenamePart, {create}); - /** - wa-sqlite, at this point, grabs a SyncAccessHandle and - assigns it to the syncHandle prop of the file state - object, but only for certain cases and it's unclear why it - places that limitation on it. - */ - wTimeEnd(); - __openFiles[fid] = Object.assign(Object.create(null),{ - fid: fid, - filenameAbs: filename, - filenamePart: filenamePart, - dirHandle: hDir, - fileHandle: hFile, - sabView: state.sabFileBufView, - readOnly: create - ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags), - deleteOnClose: deleteOnClose + }; + + /** + An experiment in improving concurrency by freeing up implicit locks + sooner. This is known to impact performance dramatically but it has + also shown to improve concurrency considerably. + + If fh.releaseImplicitLocks is truthy and fh is in __implicitLocks, + this routine returns closeSyncHandleNoThrow(), else it is a no-op. + */ + const releaseImplicitLock = async (fh)=>{ + if(fh.releaseImplicitLocks && __implicitLocks.has(fh.fid)){ + return closeSyncHandleNoThrow(fh); + } + }; + + /** + An error class specifically for use with getSyncHandle(), the goal + of which is to eventually be able to distinguish unambiguously + between locking-related failures and other types, noting that we + cannot currently do so because createSyncAccessHandle() does not + define its exceptions in the required level of detail. + + 2022-11-29: according to: + + https://github.com/whatwg/fs/pull/21 + + NoModificationAllowedError will be the standard exception thrown + when acquisition of a sync access handle fails due to a locking + error. As of this writing, that error type is not visible in the + dev console in Chrome v109, nor is it documented in MDN, but an + error with that "name" property is being thrown from the OPFS + layer. + */ + class GetSyncHandleError extends Error { + constructor(errorObject, ...msg){ + super([ + ...msg, ': '+errorObject.name+':', + errorObject.message + ].join(' '), { + cause: errorObject }); - storeAndNotify(opName, 0); - }catch(e){ - wTimeEnd(); - error(opName,e); - state.s11n.storeException(1,e); - storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR); + this.name = 'GetSyncHandleError'; } - mTimeEnd(); - }, - xRead: async function(fid/*sqlite3_file pointer*/,n,offset64){ - mTimeStart('xRead'); - let rc = 0, nRead; - const fh = __openFiles[fid]; - try{ - affirmLocked('xRead',fh); - wTimeStart('xRead'); - nRead = (await getSyncHandle(fh)).read( - fh.sabView.subarray(0, n), - {at: Number(offset64)} - ); - wTimeEnd(); - if(nRead < n){/* Zero-fill remaining bytes */ - fh.sabView.fill(0, nRead, n); - rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ; + }; + GetSyncHandleError.convertRc = (e,rc)=>{ + if(1){ + return ( + e instanceof GetSyncHandleError + && ((e.cause.name==='NoModificationAllowedError') + /* Inconsistent exception.name from Chrome/ium with the + same exception.message text: */ + || (e.cause.name==='DOMException' + && 0===e.cause.message.indexOf('Access Handles cannot'))) + ) ? ( + /*console.warn("SQLITE_BUSY",e),*/ + state.sq3Codes.SQLITE_BUSY + ) : rc; + }else{ + return rc; + } + } + /** + Returns the sync access handle associated with the given file + handle object (which must be a valid handle object, as created by + xOpen()), lazily opening it if needed. + + In order to help alleviate cross-tab contention for a dabase, if + an exception is thrown while acquiring the handle, this routine + will wait briefly and try again, up to some fixed number of + times. If acquisition still fails at that point it will give up + and propagate the exception. Client-level code will see that as + an I/O error. + */ + const getSyncHandle = async (fh,opName)=>{ + if(!fh.syncHandle){ + const t = performance.now(); + log("Acquiring sync handle for",fh.filenameAbs); + const maxTries = 6, + msBase = state.asyncIdleWaitTime * 2; + let i = 1, ms = msBase; + for(; true; ms = msBase * ++i){ + try { + //if(i<3) toss("Just testing getSyncHandle() wait-and-retry."); + //TODO? A config option which tells it to throw here + //randomly every now and then, for testing purposes. + fh.syncHandle = await fh.fileHandle.createSyncAccessHandle(); + break; + }catch(e){ + if(i === maxTries){ + throw new GetSyncHandleError( + e, "Error getting sync handle for",opName+"().",maxTries, + "attempts failed.",fh.filenameAbs + ); + } + warn("Error getting sync handle for",opName+"(). Waiting",ms, + "ms and trying again.",fh.filenameAbs,e); + Atomics.wait(state.sabOPView, state.opIds.retry, 0, ms); + } + } + log("Got",opName+"() sync handle for",fh.filenameAbs, + 'in',performance.now() - t,'ms'); + if(!fh.xLock){ + __implicitLocks.add(fh.fid); + log("Acquired implicit lock for",opName+"()",fh.fid,fh.filenameAbs); } - }catch(e){ - if(undefined===nRead) wTimeEnd(); - error("xRead() failed",e,fh); - state.s11n.storeException(1,e); - rc = state.sq3Codes.SQLITE_IOERR_READ; } - storeAndNotify('xRead',rc); - mTimeEnd(); - }, - xSync: async function(fid/*sqlite3_file pointer*/,flags/*ignored*/){ - mTimeStart('xSync'); - const fh = __openFiles[fid]; - let rc = 0; - if(!fh.readOnly && fh.syncHandle){ + return fh.syncHandle; + }; + + /** + Stores the given value at state.sabOPView[state.opIds.rc] and then + Atomics.notify()'s it. + */ + const storeAndNotify = (opName, value)=>{ + log(opName+"() => notify(",value,")"); + Atomics.store(state.sabOPView, state.opIds.rc, value); + Atomics.notify(state.sabOPView, state.opIds.rc); + }; + + /** + Throws if fh is a file-holding object which is flagged as read-only. + */ + const affirmNotRO = function(opName,fh){ + if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs); + }; + + /** + We track 2 different timers: the "metrics" timer records how much + time we spend performing work. The "wait" timer records how much + time we spend waiting on the underlying OPFS timer. See the calls + to mTimeStart(), mTimeEnd(), wTimeStart(), and wTimeEnd() + throughout this file to see how they're used. + */ + const __mTimer = Object.create(null); + __mTimer.op = undefined; + __mTimer.start = undefined; + const mTimeStart = (op)=>{ + __mTimer.start = performance.now(); + __mTimer.op = op; + //metrics[op] || toss("Maintenance required: missing metrics for",op); + ++metrics[op].count; + }; + const mTimeEnd = ()=>( + metrics[__mTimer.op].time += performance.now() - __mTimer.start + ); + const __wTimer = Object.create(null); + __wTimer.op = undefined; + __wTimer.start = undefined; + const wTimeStart = (op)=>{ + __wTimer.start = performance.now(); + __wTimer.op = op; + //metrics[op] || toss("Maintenance required: missing metrics for",op); + }; + const wTimeEnd = ()=>( + metrics[__wTimer.op].wait += performance.now() - __wTimer.start + ); + + /** + Gets set to true by the 'opfs-async-shutdown' command to quit the + wait loop. This is only intended for debugging purposes: we cannot + inspect this file's state while the tight waitLoop() is running and + need a way to stop that loop for introspection purposes. + */ + let flagAsyncShutdown = false; + + /** + Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods + methods, as well as helpers like mkdir(). Maintenance reminder: + members are in alphabetical order to simplify finding them. + */ + const vfsAsyncImpls = { + 'opfs-async-metrics': async ()=>{ + mTimeStart('opfs-async-metrics'); + metrics.dump(); + storeAndNotify('opfs-async-metrics', 0); + mTimeEnd(); + }, + 'opfs-async-shutdown': async ()=>{ + flagAsyncShutdown = true; + storeAndNotify('opfs-async-shutdown', 0); + }, + mkdir: async (dirname)=>{ + mTimeStart('mkdir'); + let rc = 0; + wTimeStart('mkdir'); try { - wTimeStart('xSync'); - await fh.syncHandle.flush(); + await getDirForFilename(dirname+"/filepart", true); }catch(e){ state.s11n.storeException(2,e); - rc = state.sq3Codes.SQLITE_IOERR_FSYNC; + rc = state.sq3Codes.SQLITE_IOERR; + }finally{ + wTimeEnd(); + } + storeAndNotify('mkdir', rc); + mTimeEnd(); + }, + xAccess: async (filename)=>{ + mTimeStart('xAccess'); + /* OPFS cannot support the full range of xAccess() queries + sqlite3 calls for. We can essentially just tell if the file + is accessible, but if it is then it's automatically writable + (unless it's locked, which we cannot(?) know without trying + to open it). OPFS does not have the notion of read-only. + + The return semantics of this function differ from sqlite3's + xAccess semantics because we are limited in what we can + communicate back to our synchronous communication partner: 0 = + accessible, non-0 means not accessible. + */ + let rc = 0; + wTimeStart('xAccess'); + try{ + const [dh, fn] = await getDirForFilename(filename); + await dh.getFileHandle(fn); + }catch(e){ + state.s11n.storeException(2,e); + rc = state.sq3Codes.SQLITE_IOERR; + }finally{ + wTimeEnd(); + } + storeAndNotify('xAccess', rc); + mTimeEnd(); + }, + xClose: async function(fid/*sqlite3_file pointer*/){ + const opName = 'xClose'; + mTimeStart(opName); + __implicitLocks.delete(fid); + const fh = __openFiles[fid]; + let rc = 0; + wTimeStart(opName); + if(fh){ + delete __openFiles[fid]; + await closeSyncHandle(fh); + if(fh.deleteOnClose){ + try{ await fh.dirHandle.removeEntry(fh.filenamePart) } + catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) } + } + }else{ + state.s11n.serialize(); + rc = state.sq3Codes.SQLITE_NOTFOUND; } wTimeEnd(); - } - storeAndNotify('xSync',rc); - mTimeEnd(); - }, - xTruncate: async function(fid/*sqlite3_file pointer*/,size){ - mTimeStart('xTruncate'); - let rc = 0; - const fh = __openFiles[fid]; - wTimeStart('xTruncate'); - try{ - affirmLocked('xTruncate',fh); - affirmNotRO('xTruncate', fh); - await (await getSyncHandle(fh)).truncate(size); - }catch(e){ - error("xTruncate():",e,fh); - state.s11n.storeException(2,e); - rc = state.sq3Codes.SQLITE_IOERR_TRUNCATE; - } - wTimeEnd(); - storeAndNotify('xTruncate',rc); - mTimeEnd(); - }, - xUnlock: async function(fid/*sqlite3_file pointer*/, - lockType/*SQLITE_LOCK_...*/){ - mTimeStart('xUnlock'); - let rc = 0; - const fh = __openFiles[fid]; - if( state.sq3Codes.SQLITE_LOCK_NONE===lockType - && fh.syncHandle ){ - wTimeStart('xUnlock'); - try { await closeSyncHandle(fh) } - catch(e){ + storeAndNotify(opName, rc); + mTimeEnd(); + }, + xDelete: async function(...args){ + mTimeStart('xDelete'); + const rc = await vfsAsyncImpls.xDeleteNoWait(...args); + storeAndNotify('xDelete', rc); + mTimeEnd(); + }, + xDeleteNoWait: async function(filename, syncDir = 0, recursive = false){ + /* The syncDir flag is, for purposes of the VFS API's semantics, + ignored here. However, if it has the value 0x1234 then: after + deleting the given file, recursively try to delete any empty + directories left behind in its wake (ignoring any errors and + stopping at the first failure). + + That said: we don't know for sure that removeEntry() fails if + the dir is not empty because the API is not documented. It has, + however, a "recursive" flag which defaults to false, so + presumably it will fail if the dir is not empty and that flag + is false. + */ + let rc = 0; + wTimeStart('xDelete'); + try { + while(filename){ + const [hDir, filenamePart] = await getDirForFilename(filename, false); + if(!filenamePart) break; + await hDir.removeEntry(filenamePart, {recursive}); + if(0x1234 !== syncDir) break; + recursive = false; + filename = getResolvedPath(filename, true); + filename.pop(); + filename = filename.join('/'); + } + }catch(e){ + state.s11n.storeException(2,e); + rc = state.sq3Codes.SQLITE_IOERR_DELETE; + } + wTimeEnd(); + return rc; + }, + xFileSize: async function(fid/*sqlite3_file pointer*/){ + mTimeStart('xFileSize'); + const fh = __openFiles[fid]; + let rc = 0; + wTimeStart('xFileSize'); + try{ + const sz = await (await getSyncHandle(fh,'xFileSize')).getSize(); + state.s11n.serialize(Number(sz)); + }catch(e){ state.s11n.storeException(1,e); - rc = state.sq3Codes.SQLITE_IOERR_UNLOCK; + rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR); } + await releaseImplicitLock(fh); wTimeEnd(); - } - storeAndNotify('xUnlock',rc); - mTimeEnd(); - }, - xWrite: async function(fid/*sqlite3_file pointer*/,n,offset64){ - mTimeStart('xWrite'); - let rc; - const fh = __openFiles[fid]; - wTimeStart('xWrite'); - try{ - affirmLocked('xWrite',fh); - affirmNotRO('xWrite', fh); - rc = ( - n === (await getSyncHandle(fh)) - .write(fh.sabView.subarray(0, n), - {at: Number(offset64)}) - ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE; - }catch(e){ - error("xWrite():",e,fh); - state.s11n.storeException(1,e); - rc = state.sq3Codes.SQLITE_IOERR_WRITE; - } - wTimeEnd(); - storeAndNotify('xWrite',rc); - mTimeEnd(); - } -}/*vfsAsyncImpls*/; - -const initS11n = ()=>{ - /** - ACHTUNG: this code is 100% duplicated in the other half of this - proxy! The documentation is maintained in the "synchronous half". - */ - if(state.s11n) return state.s11n; - const textDecoder = new TextDecoder(), - textEncoder = new TextEncoder('utf-8'), - viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize), - viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize); - state.s11n = Object.create(null); - const TypeIds = Object.create(null); - TypeIds.number = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' }; - TypeIds.bigint = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' }; - TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' }; - TypeIds.string = { id: 4 }; - const getTypeId = (v)=>( - TypeIds[typeof v] - || toss("Maintenance required: this value type cannot be serialized.",v) - ); - const getTypeIdById = (tid)=>{ - switch(tid){ - case TypeIds.number.id: return TypeIds.number; - case TypeIds.bigint.id: return TypeIds.bigint; - case TypeIds.boolean.id: return TypeIds.boolean; - case TypeIds.string.id: return TypeIds.string; - default: toss("Invalid type ID:",tid); - } - }; - state.s11n.deserialize = function(clear=false){ - ++metrics.s11n.deserialize.count; - const t = performance.now(); - const argc = viewU8[0]; - const rc = argc ? [] : null; - if(argc){ - const typeIds = []; - let offset = 1, i, n, v; - for(i = 0; i < argc; ++i, ++offset){ - typeIds.push(getTypeIdById(viewU8[offset])); - } - for(i = 0; i < argc; ++i){ - const t = typeIds[i]; - if(t.getter){ - v = viewDV[t.getter](offset, state.littleEndian); - offset += t.size; - }else{/*String*/ - n = viewDV.getInt32(offset, state.littleEndian); - offset += 4; - v = textDecoder.decode(viewU8.slice(offset, offset+n)); - offset += n; + storeAndNotify('xFileSize', rc); + mTimeEnd(); + }, + xLock: async function(fid/*sqlite3_file pointer*/, + lockType/*SQLITE_LOCK_...*/){ + mTimeStart('xLock'); + const fh = __openFiles[fid]; + let rc = 0; + const oldLockType = fh.xLock; + fh.xLock = lockType; + if( !fh.syncHandle ){ + wTimeStart('xLock'); + try { + await getSyncHandle(fh,'xLock'); + __implicitLocks.delete(fid); + }catch(e){ + state.s11n.storeException(1,e); + rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_LOCK); + fh.xLock = oldLockType; } - rc.push(v); + wTimeEnd(); } + storeAndNotify('xLock',rc); + mTimeEnd(); + }, + xOpen: async function(fid/*sqlite3_file pointer*/, filename, + flags/*SQLITE_OPEN_...*/, + opfsFlags/*OPFS_...*/){ + const opName = 'xOpen'; + mTimeStart(opName); + const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags); + wTimeStart('xOpen'); + try{ + let hDir, filenamePart; + try { + [hDir, filenamePart] = await getDirForFilename(filename, !!create); + }catch(e){ + state.s11n.storeException(1,e); + storeAndNotify(opName, state.sq3Codes.SQLITE_NOTFOUND); + mTimeEnd(); + wTimeEnd(); + return; + } + const hFile = await hDir.getFileHandle(filenamePart, {create}); + wTimeEnd(); + const fh = Object.assign(Object.create(null),{ + fid: fid, + filenameAbs: filename, + filenamePart: filenamePart, + dirHandle: hDir, + fileHandle: hFile, + sabView: state.sabFileBufView, + readOnly: create + ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags), + deleteOnClose: !!(state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags) + }); + fh.releaseImplicitLocks = + (opfsFlags & state.opfsFlags.OPFS_UNLOCK_ASAP) + || state.opfsFlags.defaultUnlockAsap; + if(0 /* this block is modelled after something wa-sqlite + does but it leads to immediate contention on journal files. + Update: this approach reportedly only works for DELETE journal + mode. */ + && (0===(flags & state.sq3Codes.SQLITE_OPEN_MAIN_DB))){ + /* sqlite does not lock these files, so go ahead and grab an OPFS + lock. */ + fh.xLock = "xOpen"/* Truthy value to keep entry from getting + flagged as auto-locked. String value so + that we can easily distinguish is later + if needed. */; + await getSyncHandle(fh,'xOpen'); + } + __openFiles[fid] = fh; + storeAndNotify(opName, 0); + }catch(e){ + wTimeEnd(); + error(opName,e); + state.s11n.storeException(1,e); + storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR); + } + mTimeEnd(); + }, + xRead: async function(fid/*sqlite3_file pointer*/,n,offset64){ + mTimeStart('xRead'); + let rc = 0, nRead; + const fh = __openFiles[fid]; + try{ + wTimeStart('xRead'); + nRead = (await getSyncHandle(fh,'xRead')).read( + fh.sabView.subarray(0, n), + {at: Number(offset64)} + ); + wTimeEnd(); + if(nRead < n){/* Zero-fill remaining bytes */ + fh.sabView.fill(0, nRead, n); + rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ; + } + }catch(e){ + if(undefined===nRead) wTimeEnd(); + error("xRead() failed",e,fh); + state.s11n.storeException(1,e); + rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_READ); + } + await releaseImplicitLock(fh); + storeAndNotify('xRead',rc); + mTimeEnd(); + }, + xSync: async function(fid/*sqlite3_file pointer*/,flags/*ignored*/){ + mTimeStart('xSync'); + const fh = __openFiles[fid]; + let rc = 0; + if(!fh.readOnly && fh.syncHandle){ + try { + wTimeStart('xSync'); + await fh.syncHandle.flush(); + }catch(e){ + state.s11n.storeException(2,e); + rc = state.sq3Codes.SQLITE_IOERR_FSYNC; + } + wTimeEnd(); + } + storeAndNotify('xSync',rc); + mTimeEnd(); + }, + xTruncate: async function(fid/*sqlite3_file pointer*/,size){ + mTimeStart('xTruncate'); + let rc = 0; + const fh = __openFiles[fid]; + wTimeStart('xTruncate'); + try{ + affirmNotRO('xTruncate', fh); + await (await getSyncHandle(fh,'xTruncate')).truncate(size); + }catch(e){ + error("xTruncate():",e,fh); + state.s11n.storeException(2,e); + rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_TRUNCATE); + } + await releaseImplicitLock(fh); + wTimeEnd(); + storeAndNotify('xTruncate',rc); + mTimeEnd(); + }, + xUnlock: async function(fid/*sqlite3_file pointer*/, + lockType/*SQLITE_LOCK_...*/){ + mTimeStart('xUnlock'); + let rc = 0; + const fh = __openFiles[fid]; + if( state.sq3Codes.SQLITE_LOCK_NONE===lockType + && fh.syncHandle ){ + wTimeStart('xUnlock'); + try { await closeSyncHandle(fh) } + catch(e){ + state.s11n.storeException(1,e); + rc = state.sq3Codes.SQLITE_IOERR_UNLOCK; + } + wTimeEnd(); + } + storeAndNotify('xUnlock',rc); + mTimeEnd(); + }, + xWrite: async function(fid/*sqlite3_file pointer*/,n,offset64){ + mTimeStart('xWrite'); + let rc; + const fh = __openFiles[fid]; + wTimeStart('xWrite'); + try{ + affirmNotRO('xWrite', fh); + rc = ( + n === (await getSyncHandle(fh,'xWrite')) + .write(fh.sabView.subarray(0, n), + {at: Number(offset64)}) + ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE; + }catch(e){ + error("xWrite():",e,fh); + state.s11n.storeException(1,e); + rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_WRITE); + } + await releaseImplicitLock(fh); + wTimeEnd(); + storeAndNotify('xWrite',rc); + mTimeEnd(); } - if(clear) viewU8[0] = 0; - //log("deserialize:",argc, rc); - metrics.s11n.deserialize.time += performance.now() - t; - return rc; - }; - state.s11n.serialize = function(...args){ - const t = performance.now(); - ++metrics.s11n.serialize.count; - if(args.length){ - //log("serialize():",args); - const typeIds = []; - let i = 0, offset = 1; - viewU8[0] = args.length & 0xff /* header = # of args */; - for(; i < args.length; ++i, ++offset){ - /* Write the TypeIds.id value into the next args.length - bytes. */ - typeIds.push(getTypeId(args[i])); - viewU8[offset] = typeIds[i].id; + }/*vfsAsyncImpls*/; + + const initS11n = ()=>{ + /** + ACHTUNG: this code is 100% duplicated in the other half of this + proxy! The documentation is maintained in the "synchronous half". + */ + if(state.s11n) return state.s11n; + const textDecoder = new TextDecoder(), + textEncoder = new TextEncoder('utf-8'), + viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize), + viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize); + state.s11n = Object.create(null); + const TypeIds = Object.create(null); + TypeIds.number = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' }; + TypeIds.bigint = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' }; + TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' }; + TypeIds.string = { id: 4 }; + const getTypeId = (v)=>( + TypeIds[typeof v] + || toss("Maintenance required: this value type cannot be serialized.",v) + ); + const getTypeIdById = (tid)=>{ + switch(tid){ + case TypeIds.number.id: return TypeIds.number; + case TypeIds.bigint.id: return TypeIds.bigint; + case TypeIds.boolean.id: return TypeIds.boolean; + case TypeIds.string.id: return TypeIds.string; + default: toss("Invalid type ID:",tid); } - for(i = 0; i < args.length; ++i) { - /* Deserialize the following bytes based on their - corresponding TypeIds.id from the header. */ - const t = typeIds[i]; - if(t.setter){ - viewDV[t.setter](offset, args[i], state.littleEndian); - offset += t.size; - }else{/*String*/ - const s = textEncoder.encode(args[i]); - viewDV.setInt32(offset, s.byteLength, state.littleEndian); - offset += 4; - viewU8.set(s, offset); - offset += s.byteLength; + }; + state.s11n.deserialize = function(clear=false){ + ++metrics.s11n.deserialize.count; + const t = performance.now(); + const argc = viewU8[0]; + const rc = argc ? [] : null; + if(argc){ + const typeIds = []; + let offset = 1, i, n, v; + for(i = 0; i < argc; ++i, ++offset){ + typeIds.push(getTypeIdById(viewU8[offset])); + } + for(i = 0; i < argc; ++i){ + const t = typeIds[i]; + if(t.getter){ + v = viewDV[t.getter](offset, state.littleEndian); + offset += t.size; + }else{/*String*/ + n = viewDV.getInt32(offset, state.littleEndian); + offset += 4; + v = textDecoder.decode(viewU8.slice(offset, offset+n)); + offset += n; + } + rc.push(v); } } - //log("serialize() result:",viewU8.slice(0,offset)); - }else{ - viewU8[0] = 0; - } - metrics.s11n.serialize.time += performance.now() - t; - }; - - state.s11n.storeException = state.asyncS11nExceptions - ? ((priority,e)=>{ - if(priority<=state.asyncS11nExceptions){ - state.s11n.serialize([e.name,': ',e.message].join("")); - } - }) - : ()=>{}; - - return state.s11n; -}/*initS11n()*/; - -const waitLoop = async function f(){ - const opHandlers = Object.create(null); - for(let k of Object.keys(state.opIds)){ - const vi = vfsAsyncImpls[k]; - if(!vi) continue; - const o = Object.create(null); - opHandlers[state.opIds[k]] = o; - o.key = k; - o.f = vi; - } - /** - waitTime is how long (ms) to wait for each Atomics.wait(). - We need to wake up periodically to give the thread a chance - to do other things. - */ - const waitTime = 500; - while(!flagAsyncShutdown){ - try { - if('timed-out'===Atomics.wait( - state.sabOPView, state.opIds.whichOp, 0, waitTime - )){ - if(__autoLocks.size){ - /* Release all auto-locks. */ - for(const fid of __autoLocks){ - const fh = __openFiles[fid]; - await closeSyncHandleNoThrow(fh); - log("Auto-unlocked",fid,fh.filenameAbs); + if(clear) viewU8[0] = 0; + //log("deserialize:",argc, rc); + metrics.s11n.deserialize.time += performance.now() - t; + return rc; + }; + state.s11n.serialize = function(...args){ + const t = performance.now(); + ++metrics.s11n.serialize.count; + if(args.length){ + //log("serialize():",args); + const typeIds = []; + let i = 0, offset = 1; + viewU8[0] = args.length & 0xff /* header = # of args */; + for(; i < args.length; ++i, ++offset){ + /* Write the TypeIds.id value into the next args.length + bytes. */ + typeIds.push(getTypeId(args[i])); + viewU8[offset] = typeIds[i].id; + } + for(i = 0; i < args.length; ++i) { + /* Deserialize the following bytes based on their + corresponding TypeIds.id from the header. */ + const t = typeIds[i]; + if(t.setter){ + viewDV[t.setter](offset, args[i], state.littleEndian); + offset += t.size; + }else{/*String*/ + const s = textEncoder.encode(args[i]); + viewDV.setInt32(offset, s.byteLength, state.littleEndian); + offset += 4; + viewU8.set(s, offset); + offset += s.byteLength; } } - continue; + //log("serialize() result:",viewU8.slice(0,offset)); + }else{ + viewU8[0] = 0; } - const opId = Atomics.load(state.sabOPView, state.opIds.whichOp); - Atomics.store(state.sabOPView, state.opIds.whichOp, 0); - const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId); - const args = state.s11n.deserialize( - true /* clear s11n to keep the caller from confusing this with - an exception string written by the upcoming - operation */ - ) || []; - //warn("waitLoop() whichOp =",opId, hnd, args); - if(hnd.f) await hnd.f(...args); - else error("Missing callback for opId",opId); - }catch(e){ - error('in waitLoop():',e); - } - } -}; + metrics.s11n.serialize.time += performance.now() - t; + }; -navigator.storage.getDirectory().then(function(d){ - const wMsg = (type)=>postMessage({type}); - state.rootDir = d; - self.onmessage = function({data}){ - switch(data.type){ - case 'opfs-async-init':{ - /* Receive shared state from synchronous partner */ - const opt = data.args; - state.littleEndian = opt.littleEndian; - state.asyncS11nExceptions = opt.asyncS11nExceptions; - state.verbose = opt.verbose ?? 2; - state.fileBufferSize = opt.fileBufferSize; - state.sabS11nOffset = opt.sabS11nOffset; - state.sabS11nSize = opt.sabS11nSize; - state.sabOP = opt.sabOP; - state.sabOPView = new Int32Array(state.sabOP); - state.sabIO = opt.sabIO; - state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize); - state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize); - state.opIds = opt.opIds; - state.sq3Codes = opt.sq3Codes; - Object.keys(vfsAsyncImpls).forEach((k)=>{ - if(!Number.isFinite(state.opIds[k])){ - toss("Maintenance required: missing state.opIds[",k,"]"); - } - }); - initS11n(); - metrics.reset(); - log("init state",state); - wMsg('opfs-async-inited'); - waitLoop(); - break; + state.s11n.storeException = state.asyncS11nExceptions + ? ((priority,e)=>{ + if(priority<=state.asyncS11nExceptions){ + state.s11n.serialize([e.name,': ',e.message].join("")); } - case 'opfs-async-restart': - if(flagAsyncShutdown){ - warn("Restarting after opfs-async-shutdown. Might or might not work."); - flagAsyncShutdown = false; + }) + : ()=>{}; + + return state.s11n; + }/*initS11n()*/; + + const waitLoop = async function f(){ + const opHandlers = Object.create(null); + for(let k of Object.keys(state.opIds)){ + const vi = vfsAsyncImpls[k]; + if(!vi) continue; + const o = Object.create(null); + opHandlers[state.opIds[k]] = o; + o.key = k; + o.f = vi; + } + while(!flagAsyncShutdown){ + try { + if('timed-out'===Atomics.wait( + state.sabOPView, state.opIds.whichOp, 0, state.asyncIdleWaitTime + )){ + await releaseImplicitLocks(); + continue; + } + const opId = Atomics.load(state.sabOPView, state.opIds.whichOp); + Atomics.store(state.sabOPView, state.opIds.whichOp, 0); + const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId); + const args = state.s11n.deserialize( + true /* clear s11n to keep the caller from confusing this with + an exception string written by the upcoming + operation */ + ) || []; + //warn("waitLoop() whichOp =",opId, hnd, args); + if(hnd.f) await hnd.f(...args); + else error("Missing callback for opId",opId); + }catch(e){ + error('in waitLoop():',e); + } + } + }; + + navigator.storage.getDirectory().then(function(d){ + state.rootDir = d; + self.onmessage = function({data}){ + switch(data.type){ + case 'opfs-async-init':{ + /* Receive shared state from synchronous partner */ + const opt = data.args; + for(const k in opt) state[k] = opt[k]; + state.verbose = opt.verbose ?? 1; + state.sabOPView = new Int32Array(state.sabOP); + state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize); + state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize); + Object.keys(vfsAsyncImpls).forEach((k)=>{ + if(!Number.isFinite(state.opIds[k])){ + toss("Maintenance required: missing state.opIds[",k,"]"); + } + }); + initS11n(); + metrics.reset(); + log("init state",state); + wPost('opfs-async-inited'); waitLoop(); + break; } - break; - case 'opfs-async-metrics': - metrics.dump(); - break; - } - }; - wMsg('opfs-async-loaded'); -}).catch((e)=>error("error initializing OPFS asyncer:",e)); + case 'opfs-async-restart': + if(flagAsyncShutdown){ + warn("Restarting after opfs-async-shutdown. Might or might not work."); + flagAsyncShutdown = false; + waitLoop(); + } + break; + case 'opfs-async-metrics': + metrics.dump(); + break; + } + }; + wPost('opfs-async-loaded'); + }).catch((e)=>error("error initializing OPFS asyncer:",e)); +}/*installAsyncProxy()*/; +if(!self.SharedArrayBuffer){ + wPost('opfs-unavailable', "Missing SharedArrayBuffer API.", + "The server must emit the COOP/COEP response headers to enable that."); +}else if(!self.Atomics){ + wPost('opfs-unavailable', "Missing Atomics API.", + "The server must emit the COOP/COEP response headers to enable that."); +}else if(!self.FileSystemHandle || + !self.FileSystemDirectoryHandle || + !self.FileSystemFileHandle || + !self.FileSystemFileHandle.prototype.createSyncAccessHandle || + !navigator.storage.getDirectory){ + wPost('opfs-unavailable',"Missing required OPFS APIs."); +}else{ + installAsyncProxy(self); +} diff --git a/ext/wasm/api/sqlite3-v-helper.js b/ext/wasm/api/sqlite3-v-helper.js new file mode 100644 index 0000000000..10be8ebce4 --- /dev/null +++ b/ext/wasm/api/sqlite3-v-helper.js @@ -0,0 +1,714 @@ +/* +** 2022-11-30 +** +** The author disclaims copyright to this source code. In place of a +** legal notice, here is a blessing: +** +** * May you do good and not evil. +** * May you find forgiveness for yourself and forgive others. +** * May you share freely, never taking more than you give. +*/ + +/** + This file installs sqlite3.vfs, and object which exists to assist + in the creation of JavaScript implementations of sqlite3_vfs, along + with its virtual table counterpart, sqlite3.vtab. +*/ +'use strict'; +self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ + const wasm = sqlite3.wasm, capi = sqlite3.capi, toss = sqlite3.util.toss3; + const vfs = Object.create(null), vtab = Object.create(null); + + sqlite3.vfs = vfs; + sqlite3.vtab = vtab; + + const sii = capi.sqlite3_index_info; + /** + If n is >=0 and less than this.$nConstraint, this function + returns either a WASM pointer to the 0-based nth entry of + this.$aConstraint (if passed a truthy 2nd argument) or an + sqlite3_index_info.sqlite3_index_constraint object wrapping that + address (if passed a falsy value or no 2nd argument). Returns a + falsy value if n is out of range. + */ + sii.prototype.nthConstraint = function(n, asPtr=false){ + if(n<0 || n>=this.$nConstraint) return false; + const ptr = this.$aConstraint + ( + sii.sqlite3_index_constraint.structInfo.sizeof * n + ); + return asPtr ? ptr : new sii.sqlite3_index_constraint(ptr); + }; + + /** + Works identically to nthConstraint() but returns state from + this.$aConstraintUsage, so returns an + sqlite3_index_info.sqlite3_index_constraint_usage instance + if passed no 2nd argument or a falsy 2nd argument. + */ + sii.prototype.nthConstraintUsage = function(n, asPtr=false){ + if(n<0 || n>=this.$nConstraint) return false; + const ptr = this.$aConstraintUsage + ( + sii.sqlite3_index_constraint_usage.structInfo.sizeof * n + ); + return asPtr ? ptr : new sii.sqlite3_index_constraint_usage(ptr); + }; + + /** + If n is >=0 and less than this.$nOrderBy, this function + returns either a WASM pointer to the 0-based nth entry of + this.$aOrderBy (if passed a truthy 2nd argument) or an + sqlite3_index_info.sqlite3_index_orderby object wrapping that + address (if passed a falsy value or no 2nd argument). Returns a + falsy value if n is out of range. + */ + sii.prototype.nthOrderBy = function(n, asPtr=false){ + if(n<0 || n>=this.$nOrderBy) return false; + const ptr = this.$aOrderBy + ( + sii.sqlite3_index_orderby.structInfo.sizeof * n + ); + return asPtr ? ptr : new sii.sqlite3_index_orderby(ptr); + }; + + /** + Installs a StructBinder-bound function pointer member of the + given name and function in the given StructType target object. + + It creates a WASM proxy for the given function and arranges for + that proxy to be cleaned up when tgt.dispose() is called. Throws + on the slightest hint of error, e.g. tgt is-not-a StructType, + name does not map to a struct-bound member, etc. + + As a special case, if the given function is a pointer, then + `wasm.functionEntry()` is used to validate that it is a known + function. If so, it is used as-is with no extra level of proxying + or cleanup, else an exception is thrown. It is legal to pass a + value of 0, indicating a NULL pointer, with the caveat that 0 + _is_ a legal function pointer in WASM but it will not be accepted + as such _here_. (Justification: the function at address zero must + be one which initially came from the WASM module, not a method we + want to bind to a virtual table or VFS.) + + This function returns a proxy for itself which is bound to tgt + and takes 2 args (name,func). That function returns the same + thing as this one, permitting calls to be chained. + + If called with only 1 arg, it has no side effects but returns a + func with the same signature as described above. + + ACHTUNG: because we cannot generically know how to transform JS + exceptions into result codes, the installed functions do no + automatic catching of exceptions. It is critical, to avoid + undefined behavior in the C layer, that methods mapped via + this function do not throw. The exception, as it were, to that + rule is... + + If applyArgcCheck is true then each JS function (as opposed to + function pointers) gets wrapped in a proxy which asserts that it + is passed the expected number of arguments, throwing if the + argument count does not match expectations. That is only intended + for dev-time usage for sanity checking, and will leave the C + environment in an undefined state. + */ + const installMethod = function callee( + tgt, name, func, applyArgcCheck = callee.installMethodArgcCheck + ){ + if(!(tgt instanceof sqlite3.StructBinder.StructType)){ + toss("Usage error: target object is-not-a StructType."); + }else if(!(func instanceof Function) && !wasm.isPtr(func)){ + toss("Usage errror: expecting a Function or WASM pointer to one."); + } + if(1===arguments.length){ + return (n,f)=>callee(tgt, n, f, applyArgcCheck); + } + if(!callee.argcProxy){ + callee.argcProxy = function(tgt, funcName, func,sig){ + return function(...args){ + if(func.length!==arguments.length){ + toss("Argument mismatch for", + tgt.structInfo.name+"::"+funcName + +": Native signature is:",sig); + } + return func.apply(this, args); + } + }; + /* An ondispose() callback for use with + sqlite3.StructBinder-created types. */ + callee.removeFuncList = function(){ + if(this.ondispose.__removeFuncList){ + this.ondispose.__removeFuncList.forEach( + (v,ndx)=>{ + if('number'===typeof v){ + try{wasm.uninstallFunction(v)} + catch(e){/*ignore*/} + } + /* else it's a descriptive label for the next number in + the list. */ + } + ); + delete this.ondispose.__removeFuncList; + } + }; + }/*static init*/ + const sigN = tgt.memberSignature(name); + if(sigN.length<2){ + toss("Member",name,"does not have a function pointer signature:",sigN); + } + const memKey = tgt.memberKey(name); + const fProxy = (applyArgcCheck && !wasm.isPtr(func)) + /** This middle-man proxy is only for use during development, to + confirm that we always pass the proper number of + arguments. We know that the C-level code will always use the + correct argument count. */ + ? callee.argcProxy(tgt, memKey, func, sigN) + : func; + if(wasm.isPtr(fProxy)){ + if(fProxy && !wasm.functionEntry(fProxy)){ + toss("Pointer",fProxy,"is not a WASM function table entry."); + } + tgt[memKey] = fProxy; + }else{ + const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true)); + tgt[memKey] = pFunc; + if(!tgt.ondispose || !tgt.ondispose.__removeFuncList){ + tgt.addOnDispose('ondispose.__removeFuncList handler', + callee.removeFuncList); + tgt.ondispose.__removeFuncList = []; + } + tgt.ondispose.__removeFuncList.push(memKey, pFunc); + } + return (n,f)=>callee(tgt, n, f, applyArgcCheck); + }/*installMethod*/; + installMethod.installMethodArgcCheck = false; + + /** + Installs methods into the given StructType-type instance. Each + entry in the given methods object must map to a known member of + the given StructType, else an exception will be triggered. See + installMethod() for more details, including the semantics of the + 3rd argument. + + As an exception to the above, if any two or more methods in the + 2nd argument are the exact same function, installMethod() is + _not_ called for the 2nd and subsequent instances, and instead + those instances get assigned the same method pointer which is + created for the first instance. This optimization is primarily to + accommodate special handling of sqlite3_module::xConnect and + xCreate methods. + + On success, returns its first argument. Throws on error. + */ + const installMethods = function( + structInstance, methods, applyArgcCheck = installMethod.installMethodArgcCheck + ){ + const seen = new Map /* map of */; + for(const k of Object.keys(methods)){ + const m = methods[k]; + const prior = seen.get(m); + if(prior){ + const mkey = structInstance.memberKey(k); + structInstance[mkey] = structInstance[structInstance.memberKey(prior)]; + }else{ + installMethod(structInstance, k, m, applyArgcCheck); + seen.set(m, k); + } + } + return structInstance; + }; + + /** + Equivalent to calling installMethod(this,...arguments) with a + first argument of this object. If called with 1 or 2 arguments + and the first is an object, it's instead equivalent to calling + installMethods(this,...arguments). + */ + sqlite3.StructBinder.StructType.prototype.installMethod = function callee( + name, func, applyArgcCheck = installMethod.installMethodArgcCheck + ){ + return (arguments.length < 3 && name && 'object'===typeof name) + ? installMethods(this, ...arguments) + : installMethod(this, ...arguments); + }; + + /** + Equivalent to calling installMethods() with a first argument + of this object. + */ + sqlite3.StructBinder.StructType.prototype.installMethods = function( + methods, applyArgcCheck = installMethod.installMethodArgcCheck + ){ + return installMethods(this, methods, applyArgcCheck); + }; + + /** + Uses sqlite3_vfs_register() to register this + sqlite3.capi.sqlite3_vfs. This object must have already been + filled out properly. If the first argument is truthy, the VFS is + registered as the default VFS, else it is not. + + On success, returns this object. Throws on error. + */ + capi.sqlite3_vfs.prototype.registerVfs = function(asDefault=false){ + if(!(this instanceof sqlite3.capi.sqlite3_vfs)){ + toss("Expecting a sqlite3_vfs-type argument."); + } + const rc = capi.sqlite3_vfs_register(this, asDefault ? 1 : 0); + if(rc){ + toss("sqlite3_vfs_register(",this,") failed with rc",rc); + } + if(this.pointer !== capi.sqlite3_vfs_find(this.$zName)){ + toss("BUG: sqlite3_vfs_find(vfs.$zName) failed for just-installed VFS", + this); + } + return this; + }; + + /** + A wrapper for installMethods() or registerVfs() to reduce + installation of a VFS and/or its I/O methods to a single + call. + + Accepts an object which contains the properties "io" and/or + "vfs", each of which is itself an object with following properties: + + - `struct`: an sqlite3.StructType-type struct. This must be a + populated (except for the methods) object of type + sqlite3_io_methods (for the "io" entry) or sqlite3_vfs (for the + "vfs" entry). + + - `methods`: an object mapping sqlite3_io_methods method names + (e.g. 'xClose') to JS implementations of those methods. The JS + implementations must be call-compatible with their native + counterparts. + + For each of those object, this function passes its (`struct`, + `methods`, (optional) `applyArgcCheck`) properties to + installMethods(). + + If the `vfs` entry is set then: + + - Its `struct` property's registerVfs() is called. The + `vfs` entry may optionally have an `asDefault` property, which + gets passed as the argument to registerVfs(). + + - If `struct.$zName` is falsy and the entry has a string-type + `name` property, `struct.$zName` is set to the C-string form of + that `name` value before registerVfs() is called. + + On success returns this object. Throws on error. + */ + vfs.installVfs = function(opt){ + let count = 0; + const propList = ['io','vfs']; + for(const key of propList){ + const o = opt[key]; + if(o){ + ++count; + installMethods(o.struct, o.methods, !!o.applyArgcCheck); + if('vfs'===key){ + if(!o.struct.$zName && 'string'===typeof o.name){ + o.struct.addOnDispose( + o.struct.$zName = wasm.allocCString(o.name) + ); + } + o.struct.registerVfs(!!o.asDefault); + } + } + } + if(!count) toss("Misuse: installVfs() options object requires at least", + "one of:", propList); + return this; + }; + + /** + Internal factory function for xVtab and xCursor impls. + */ + const __xWrapFactory = function(methodName,StructType){ + return function(ptr,removeMapping=false){ + if(0===arguments.length) ptr = new StructType; + if(ptr instanceof StructType){ + //T.assert(!this.has(ptr.pointer)); + this.set(ptr.pointer, ptr); + return ptr; + }else if(!wasm.isPtr(ptr)){ + sqlite3.SQLite3Error.toss("Invalid argument to",methodName+"()"); + } + let rc = this.get(ptr); + if(removeMapping) this.delete(ptr); + return rc; + }.bind(new Map); + }; + + /** + A factory function which implements a simple lifetime manager for + mappings between C struct pointers and their JS-level wrappers. + The first argument must be the logical name of the manager + (e.g. 'xVtab' or 'xCursor'), which is only used for error + reporting. The second must be the capi.XYZ struct-type value, + e.g. capi.sqlite3_vtab or capi.sqlite3_vtab_cursor. + + Returns an object with 4 methods: create(), get(), unget(), and + dispose(), plus a StructType member with the value of the 2nd + argument. The methods are documented in the body of this + function. + */ + const StructPtrMapper = function(name, StructType){ + const __xWrap = __xWrapFactory(name,StructType); + /** + This object houses a small API for managing mappings of (`T*`) + to StructType objects, specifically within the lifetime + requirements of sqlite3_module methods. + */ + return Object.assign(Object.create(null),{ + /** The StructType object for this object's API. */ + StructType, + /** + Creates a new StructType object, writes its `pointer` + value to the given output pointer, and returns that + object. Its intended usage depends on StructType: + + sqlite3_vtab: to be called from sqlite3_module::xConnect() + or xCreate() implementations. + + sqlite3_vtab_cursor: to be called from xOpen(). + + This will throw if allocation of the StructType instance + fails or if ppOut is not a pointer-type value. + */ + create: (ppOut)=>{ + const rc = __xWrap(); + wasm.pokePtr(ppOut, rc.pointer); + return rc; + }, + /** + Returns the StructType object previously mapped to the + given pointer using create(). Its intended usage depends + on StructType: + + sqlite3_vtab: to be called from sqlite3_module methods which + take a (sqlite3_vtab*) pointer _except_ for + xDestroy()/xDisconnect(), in which case unget() or dispose(). + + sqlite3_vtab_cursor: to be called from any sqlite3_module methods + which take a `sqlite3_vtab_cursor*` argument except xClose(), + in which case use unget() or dispose(). + + Rule to remember: _never_ call dispose() on an instance + returned by this function. + */ + get: (pCObj)=>__xWrap(pCObj), + /** + Identical to get() but also disconnects the mapping between the + given pointer and the returned StructType object, such that + future calls to this function or get() with the same pointer + will return the undefined value. Its intended usage depends + on StructType: + + sqlite3_vtab: to be called from sqlite3_module::xDisconnect() or + xDestroy() implementations or in error handling of a failed + xCreate() or xConnect(). + + sqlite3_vtab_cursor: to be called from xClose() or during + cleanup in a failed xOpen(). + + Calling this method obligates the caller to call dispose() on + the returned object when they're done with it. + */ + unget: (pCObj)=>__xWrap(pCObj,true), + /** + Works like unget() plus it calls dispose() on the + StructType object. + */ + dispose: (pCObj)=>{ + const o = __xWrap(pCObj,true); + if(o) o.dispose(); + } + }); + }; + + /** + A lifetime-management object for mapping `sqlite3_vtab*` + instances in sqlite3_module methods to capi.sqlite3_vtab + objects. + + The API docs are in the API-internal StructPtrMapper(). + */ + vtab.xVtab = StructPtrMapper('xVtab', capi.sqlite3_vtab); + + /** + A lifetime-management object for mapping `sqlite3_vtab_cursor*` + instances in sqlite3_module methods to capi.sqlite3_vtab_cursor + objects. + + The API docs are in the API-internal StructPtrMapper(). + */ + vtab.xCursor = StructPtrMapper('xCursor', capi.sqlite3_vtab_cursor); + + /** + Convenience form of creating an sqlite3_index_info wrapper, + intended for use in xBestIndex implementations. Note that the + caller is expected to call dispose() on the returned object + before returning. Though not _strictly_ required, as that object + does not own the pIdxInfo memory, it is nonetheless good form. + */ + vtab.xIndexInfo = (pIdxInfo)=>new capi.sqlite3_index_info(pIdxInfo); + + /** + Given an error object, this function returns + sqlite3.capi.SQLITE_NOMEM if (e instanceof + sqlite3.WasmAllocError), else it returns its + second argument. Its intended usage is in the methods + of a sqlite3_vfs or sqlite3_module: + + ``` + try{ + let rc = ... + return rc; + }catch(e){ + return sqlite3.vtab.exceptionToRc(e, sqlite3.capi.SQLITE_XYZ); + // where SQLITE_XYZ is some call-appropriate result code. + } + ``` + */ + /**vfs.exceptionToRc = vtab.exceptionToRc = + (e, defaultRc=capi.SQLITE_ERROR)=>( + (e instanceof sqlite3.WasmAllocError) + ? capi.SQLITE_NOMEM + : defaultRc + );*/ + + /** + Given an sqlite3_module method name and error object, this + function returns sqlite3.capi.SQLITE_NOMEM if (e instanceof + sqlite3.WasmAllocError), else it returns its second argument. Its + intended usage is in the methods of a sqlite3_vfs or + sqlite3_module: + + ``` + try{ + let rc = ... + return rc; + }catch(e){ + return sqlite3.vtab.xError( + 'xColumn', e, sqlite3.capi.SQLITE_XYZ); + // where SQLITE_XYZ is some call-appropriate result code. + } + ``` + + If no 3rd argument is provided, its default depends on + the error type: + + - An sqlite3.WasmAllocError always resolves to capi.SQLITE_NOMEM. + + - If err is an SQLite3Error then its `resultCode` property + is used. + + - If all else fails, capi.SQLITE_ERROR is used. + + If xError.errorReporter is a function, it is called in + order to report the error, else the error is not reported. + If that function throws, that exception is ignored. + */ + vtab.xError = function f(methodName, err, defaultRc){ + if(f.errorReporter instanceof Function){ + try{f.errorReporter("sqlite3_module::"+methodName+"(): "+err.message);} + catch(e){/*ignored*/} + } + let rc; + if(err instanceof sqlite3.WasmAllocError) rc = capi.SQLITE_NOMEM; + else if(arguments.length>2) rc = defaultRc; + else if(err instanceof sqlite3.SQLite3Error) rc = err.resultCode; + return rc || capi.SQLITE_ERROR; + }; + vtab.xError.errorReporter = 1 ? console.error.bind(console) : false; + + /** + "The problem" with this is that it introduces an outer function with + a different arity than the passed-in method callback. That means we + cannot do argc validation on these. Additionally, some methods (namely + xConnect) may have call-specific error handling. It would be a shame to + hard-coded that per-method support in this function. + */ + /** vtab.methodCatcher = function(methodName, method, defaultErrRc=capi.SQLITE_ERROR){ + return function(...args){ + try { method(...args); } + }catch(e){ return vtab.xError(methodName, e, defaultRc) } + }; + */ + + /** + A helper for sqlite3_vtab::xRowid() and xUpdate() + implementations. It must be passed the final argument to one of + those methods (an output pointer to an int64 row ID) and the + value to store at the output pointer's address. Returns the same + as wasm.poke() and will throw if the 1st or 2nd arguments + are invalid for that function. + + Example xRowid impl: + + ``` + const xRowid = (pCursor, ppRowid64)=>{ + const c = vtab.xCursor(pCursor); + vtab.xRowid(ppRowid64, c.myRowId); + return 0; + }; + ``` + */ + vtab.xRowid = (ppRowid64, value)=>wasm.poke(ppRowid64, value, 'i64'); + + /** + A helper to initialize and set up an sqlite3_module object for + later installation into individual databases using + sqlite3_create_module(). Requires an object with the following + properties: + + - `methods`: an object containing a mapping of properties with + the C-side names of the sqlite3_module methods, e.g. xCreate, + xBestIndex, etc., to JS implementations for those functions. + Certain special-case handling is performed, as described below. + + - `catchExceptions` (default=false): if truthy, the given methods + are not mapped as-is, but are instead wrapped inside wrappers + which translate exceptions into result codes of SQLITE_ERROR or + SQLITE_NOMEM, depending on whether the exception is an + sqlite3.WasmAllocError. In the case of the xConnect and xCreate + methods, the exception handler also sets the output error + string to the exception's error string. + + - OPTIONAL `struct`: a sqlite3.capi.sqlite3_module() instance. If + not set, one will be created automatically. If the current + "this" is-a sqlite3_module then it is unconditionally used in + place of `struct`. + + - OPTIONAL `iVersion`: if set, it must be an integer value and it + gets assigned to the `$iVersion` member of the struct object. + If it's _not_ set, and the passed-in `struct` object's `$iVersion` + is 0 (the default) then this function attempts to define a value + for that property based on the list of methods it has. + + If `catchExceptions` is false, it is up to the client to ensure + that no exceptions escape the methods, as doing so would move + them through the C API, leading to undefined + behavior. (vtab.xError() is intended to assist in reporting + such exceptions.) + + Certain methods may refer to the same implementation. To simplify + the definition of such methods: + + - If `methods.xConnect` is `true` then the value of + `methods.xCreate` is used in its place, and vice versa. sqlite + treats xConnect/xCreate functions specially if they are exactly + the same function (same pointer value). + + - If `methods.xDisconnect` is true then the value of + `methods.xDestroy` is used in its place, and vice versa. + + This is to facilitate creation of those methods inline in the + passed-in object without requiring the client to explicitly get a + reference to one of them in order to assign it to the other + one. + + The `catchExceptions`-installed handlers will account for + identical references to the above functions and will install the + same wrapper function for both. + + The given methods are expected to return integer values, as + expected by the C API. If `catchExceptions` is truthy, the return + value of the wrapped function will be used as-is and will be + translated to 0 if the function returns a falsy value (e.g. if it + does not have an explicit return). If `catchExceptions` is _not_ + active, the method implementations must explicitly return integer + values. + + Throws on error. On success, returns the sqlite3_module object + (`this` or `opt.struct` or a new sqlite3_module instance, + depending on how it's called). + */ + vtab.setupModule = function(opt){ + let createdMod = false; + const mod = (this instanceof capi.sqlite3_module) + ? this : (opt.struct || (createdMod = new capi.sqlite3_module())); + try{ + const methods = opt.methods || toss("Missing 'methods' object."); + for(const e of Object.entries({ + // -----^ ==> [k,v] triggers a broken code transformation in + // some versions of the emsdk toolchain. + xConnect: 'xCreate', xDisconnect: 'xDestroy' + })){ + // Remap X=true to X=Y for certain X/Y combinations + const k = e[0], v = e[1]; + if(true === methods[k]) methods[k] = methods[v]; + else if(true === methods[v]) methods[v] = methods[k]; + } + if(opt.catchExceptions){ + const fwrap = function(methodName, func){ + if(['xConnect','xCreate'].indexOf(methodName) >= 0){ + return function(pDb, pAux, argc, argv, ppVtab, pzErr){ + try{return func(...arguments) || 0} + catch(e){ + if(!(e instanceof sqlite3.WasmAllocError)){ + wasm.dealloc(wasm.peekPtr(pzErr)); + wasm.pokePtr(pzErr, wasm.allocCString(e.message)); + } + return vtab.xError(methodName, e); + } + }; + }else{ + return function(...args){ + try{return func(...args) || 0} + catch(e){ + return vtab.xError(methodName, e); + } + }; + } + }; + const mnames = [ + 'xCreate', 'xConnect', 'xBestIndex', 'xDisconnect', + 'xDestroy', 'xOpen', 'xClose', 'xFilter', 'xNext', + 'xEof', 'xColumn', 'xRowid', 'xUpdate', + 'xBegin', 'xSync', 'xCommit', 'xRollback', + 'xFindFunction', 'xRename', 'xSavepoint', 'xRelease', + 'xRollbackTo', 'xShadowName' + ]; + const remethods = Object.create(null); + for(const k of mnames){ + const m = methods[k]; + if(!(m instanceof Function)) continue; + else if('xConnect'===k && methods.xCreate===m){ + remethods[k] = methods.xCreate; + }else if('xCreate'===k && methods.xConnect===m){ + remethods[k] = methods.xConnect; + }else{ + remethods[k] = fwrap(k, m); + } + } + installMethods(mod, remethods, false); + }else{ + // No automatic exception handling. Trust the client + // to not throw. + installMethods( + mod, methods, !!opt.applyArgcCheck/*undocumented option*/ + ); + } + if(0===mod.$iVersion){ + let v; + if('number'===typeof opt.iVersion) v = opt.iVersion; + else if(mod.$xShadowName) v = 3; + else if(mod.$xSavePoint || mod.$xRelease || mod.$xRollbackTo) v = 2; + else v = 1; + mod.$iVersion = v; + } + }catch(e){ + if(createdMod) createdMod.dispose(); + throw e; + } + return mod; + }/*setupModule()*/; + + /** + Equivalent to calling vtab.setupModule() with this sqlite3_module + object as the call's `this`. + */ + capi.sqlite3_module.prototype.setupModule = function(opt){ + return vtab.setupModule.call(this, opt); + }; +}/*sqlite3ApiBootstrap.initializers.push()*/); diff --git a/ext/wasm/api/sqlite3-api-opfs.js b/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js similarity index 85% rename from ext/wasm/api/sqlite3-api-opfs.js rename to ext/wasm/api/sqlite3-vfs-opfs.c-pp.js index da5496f651..50be31a2bd 100644 --- a/ext/wasm/api/sqlite3-api-opfs.js +++ b/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js @@ -76,15 +76,25 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ `opfs` property, containing several OPFS-specific utilities. */ const installOpfsVfs = function callee(options){ - if(!self.SharedArrayBuffer || - !self.Atomics || - !self.FileSystemHandle || - !self.FileSystemDirectoryHandle || - !self.FileSystemFileHandle || - !self.FileSystemFileHandle.prototype.createSyncAccessHandle || - !navigator.storage.getDirectory){ + if(!self.SharedArrayBuffer + || !self.Atomics){ return Promise.reject( - new Error("This environment does not have OPFS support.") + new Error("Cannot install OPFS: Missing SharedArrayBuffer and/or Atomics. "+ + "The server must emit the COOP/COEP response headers to enable those. "+ + "See https://sqlite.org/wasm/doc/trunk/persistence.md#coop-coep") + ); + }else if(self.window===self && self.document){ + return Promise.reject( + new Error("The OPFS sqlite3_vfs cannot run in the main thread "+ + "because it requires Atomics.wait().") + ); + }else if(!self.FileSystemHandle || + !self.FileSystemDirectoryHandle || + !self.FileSystemFileHandle || + !self.FileSystemFileHandle.prototype.createSyncAccessHandle || + !navigator.storage.getDirectory){ + return Promise.reject( + new Error("Missing required OPFS APIs.") ); } if(!options || 'object'!==typeof options){ @@ -92,7 +102,8 @@ const installOpfsVfs = function callee(options){ } const urlParams = new URL(self.location.href).searchParams; if(undefined===options.verbose){ - options.verbose = urlParams.has('opfs-verbose') ? 3 : 2; + options.verbose = urlParams.has('opfs-verbose') + ? (+urlParams.get('opfs-verbose') || 2) : 1; } if(undefined===options.sanityChecks){ options.sanityChecks = urlParams.has('opfs-sanity-check'); @@ -101,6 +112,8 @@ const installOpfsVfs = function callee(options){ options.proxyUri = callee.defaultProxyUri; } + //console.warn("OPFS options =",options,self.location); + if('function' === typeof options.proxyUri){ options.proxyUri = options.proxyUri(); } @@ -116,7 +129,7 @@ const installOpfsVfs = function callee(options){ const log = (...args)=>logImpl(2, ...args); const warn = (...args)=>logImpl(1, ...args); const error = (...args)=>logImpl(0, ...args); - const toss = function(...args){throw new Error(args.join(' '))}; + const toss = sqlite3.util.toss; const capi = sqlite3.capi; const wasm = sqlite3.wasm; const sqlite3_vfs = capi.sqlite3_vfs; @@ -125,8 +138,24 @@ const installOpfsVfs = function callee(options){ /** Generic utilities for working with OPFS. This will get filled out by the Promise setup and, on success, installed as sqlite3.opfs. + + ACHTUNG: do not rely on these APIs in client code. They are + experimental and subject to change or removal as the + OPFS-specific sqlite3_vfs evolves. */ const opfsUtil = Object.create(null); + + /** + Returns true if _this_ thread has access to the OPFS APIs. + */ + const thisThreadHasOPFS = ()=>{ + return self.FileSystemHandle && + self.FileSystemDirectoryHandle && + self.FileSystemFileHandle && + self.FileSystemFileHandle.prototype.createSyncAccessHandle && + navigator.storage.getDirectory; + }; + /** Not part of the public API. Solely for internal/development use. @@ -162,11 +191,18 @@ const installOpfsVfs = function callee(options){ s.count = s.time = 0; } }/*metrics*/; + const opfsVfs = new sqlite3_vfs(); + const opfsIoMethods = new sqlite3_io_methods(); const promiseReject = function(err){ opfsVfs.dispose(); return promiseReject_(err); }; - const W = new Worker(options.proxyUri); + const W = +//#if target=es6-module + new Worker(new URL(options.proxyUri, import.meta.url)); +//#else + new Worker(options.proxyUri); +//#endif W._originalOnError = W.onerror /* will be restored later */; W.onerror = function(err){ // The error object doesn't contain any useful info when the @@ -178,10 +214,7 @@ const installOpfsVfs = function callee(options){ const dVfs = pDVfs ? new sqlite3_vfs(pDVfs) : null /* dVfs will be null when sqlite3 is built with - SQLITE_OS_OTHER. Though we cannot currently handle - that case, the hope is to eventually be able to. */; - const opfsVfs = new sqlite3_vfs(); - const opfsIoMethods = new sqlite3_io_methods(); + SQLITE_OS_OTHER. */; opfsVfs.$iVersion = 2/*yes, two*/; opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof; opfsVfs.$mxPathname = 1024/*sure, why not?*/; @@ -235,13 +268,23 @@ const installOpfsVfs = function callee(options){ // Int16Array uses the platform's endianness. return new Int16Array(buffer)[0] === 256; })(); + /** + asyncIdleWaitTime is how long (ms) to wait, in the async proxy, + for each Atomics.wait() when waiting on inbound VFS API calls. + We need to wake up periodically to give the thread a chance to + do other things. If this is too high (e.g. 500ms) then even two + workers/tabs can easily run into locking errors. Some multiple + of this value is also used for determining how long to wait on + lock contention to free up. + */ + state.asyncIdleWaitTime = 150; /** Whether the async counterpart should log exceptions to the serialization channel. That produces a great deal of noise for seemingly innocuous things like xAccess() checks for missing files, so this option may have one of 3 values: - 0 = no exception logging + 0 = no exception logging. 1 = only log exceptions for "significant" ops like xOpen(), xRead(), and xWrite(). @@ -257,7 +300,9 @@ const installOpfsVfs = function callee(options){ The size of the block in our SAB for serializing arguments and result values. Needs to be large enough to hold serialized values of any of the proxied APIs. Filenames are the largest - part but are limited to opfsVfs.$mxPathname bytes. + part but are limited to opfsVfs.$mxPathname bytes. We also + store exceptions there, so it needs to be long enough to hold + a reasonably long exception string. */ state.sabS11nSize = opfsVfs.$mxPathname * 2; /** @@ -318,6 +363,7 @@ const installOpfsVfs = function callee(options){ [ 'SQLITE_ACCESS_EXISTS', 'SQLITE_ACCESS_READWRITE', + 'SQLITE_BUSY', 'SQLITE_ERROR', 'SQLITE_IOERR', 'SQLITE_IOERR_ACCESS', @@ -335,16 +381,37 @@ const installOpfsVfs = function callee(options){ 'SQLITE_LOCK_PENDING', 'SQLITE_LOCK_RESERVED', 'SQLITE_LOCK_SHARED', + 'SQLITE_LOCKED', 'SQLITE_MISUSE', 'SQLITE_NOTFOUND', 'SQLITE_OPEN_CREATE', 'SQLITE_OPEN_DELETEONCLOSE', + 'SQLITE_OPEN_MAIN_DB', 'SQLITE_OPEN_READONLY' ].forEach((k)=>{ if(undefined === (state.sq3Codes[k] = capi[k])){ toss("Maintenance required: not found:",k); } }); + state.opfsFlags = Object.assign(Object.create(null),{ + /** + Flag for use with xOpen(). "opfs-unlock-asap=1" enables + this. See defaultUnlockAsap, below. + */ + OPFS_UNLOCK_ASAP: 0x01, + /** + If true, any async routine which implicitly acquires a sync + access handle (i.e. an OPFS lock) will release that locks at + the end of the call which acquires it. If false, such + "autolocks" are not released until the VFS is idle for some + brief amount of time. + + The benefit of enabling this is much higher concurrency. The + down-side is much-reduced performance (as much as a 4x decrease + in speedtest1). + */ + defaultUnlockAsap: false + }); /** Runs the given operation (by name) in the async worker @@ -569,7 +636,13 @@ const installOpfsVfs = function callee(options){ const ndx = Math.random() * (f._n * 64) % f._n | 0; a[i] = f._chars[ndx]; } - return a.join(''); + return a.join(""); + /* + An alternative impl. with an unpredictable length + but much simpler: + + Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36) + */ }; /** @@ -577,77 +650,6 @@ const installOpfsVfs = function callee(options){ */ const __openFiles = Object.create(null); - /** - Installs a StructBinder-bound function pointer member of the - given name and function in the given StructType target object. - It creates a WASM proxy for the given function and arranges for - that proxy to be cleaned up when tgt.dispose() is called. Throws - on the slightest hint of error (e.g. tgt is-not-a StructType, - name does not map to a struct-bound member, etc.). - - Returns a proxy for this function which is bound to tgt and takes - 2 args (name,func). That function returns the same thing, - permitting calls to be chained. - - If called with only 1 arg, it has no side effects but returns a - func with the same signature as described above. - */ - const installMethod = function callee(tgt, name, func){ - if(!(tgt instanceof sqlite3.StructBinder.StructType)){ - toss("Usage error: target object is-not-a StructType."); - } - if(1===arguments.length){ - return (n,f)=>callee(tgt,n,f); - } - if(!callee.argcProxy){ - callee.argcProxy = function(func,sig){ - return function(...args){ - if(func.length!==arguments.length){ - toss("Argument mismatch. Native signature is:",sig); - } - return func.apply(this, args); - } - }; - callee.removeFuncList = function(){ - if(this.ondispose.__removeFuncList){ - this.ondispose.__removeFuncList.forEach( - (v,ndx)=>{ - if('number'===typeof v){ - try{wasm.uninstallFunction(v)} - catch(e){/*ignore*/} - } - /* else it's a descriptive label for the next number in - the list. */ - } - ); - delete this.ondispose.__removeFuncList; - } - }; - }/*static init*/ - const sigN = tgt.memberSignature(name); - if(sigN.length<2){ - toss("Member",name," is not a function pointer. Signature =",sigN); - } - const memKey = tgt.memberKey(name); - const fProxy = 0 - /** This middle-man proxy is only for use during development, to - confirm that we always pass the proper number of - arguments. We know that the C-level code will always use the - correct argument count. */ - ? callee.argcProxy(func, sigN) - : func; - const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true)); - tgt[memKey] = pFunc; - if(!tgt.ondispose) tgt.ondispose = []; - if(!tgt.ondispose.__removeFuncList){ - tgt.ondispose.push('ondispose.__removeFuncList handler', - callee.removeFuncList); - tgt.ondispose.__removeFuncList = []; - } - tgt.ondispose.__removeFuncList.push(memKey, pFunc); - return (n,f)=>callee(tgt, n, f); - }/*installMethod*/; - const opTimer = Object.create(null); opTimer.op = undefined; opTimer.start = undefined; @@ -673,10 +675,10 @@ const installOpfsVfs = function callee(options){ success) release a sync-handle for it, but doing so would involve an inherent race condition. For the time being, pending a better solution, we simply report whether the - given pFile instance has a lock. + given pFile is open. */ const f = __openFiles[pFile]; - wasm.setMemValue(pOut, f.lockMode ? 1 : 0, 'i32'); + wasm.poke(pOut, f.lockType ? 1 : 0, 'i32'); return 0; }, xClose: function(pFile){ @@ -705,10 +707,15 @@ const installOpfsVfs = function callee(options){ }, xFileSize: function(pFile,pSz64){ mTimeStart('xFileSize'); - const rc = opRun('xFileSize', pFile); + let rc = opRun('xFileSize', pFile); if(0==rc){ - const sz = state.s11n.deserialize()[0]; - wasm.setMemValue(pSz64, sz, 'i64'); + try { + const sz = state.s11n.deserialize()[0]; + wasm.poke(pSz64, sz, 'i64'); + }catch(e){ + error("Unexpected error reading xFileSize() result:",e); + rc = state.sq3Codes.SQLITE_IOERR; + } } mTimeEnd(); return rc; @@ -717,7 +724,11 @@ const installOpfsVfs = function callee(options){ mTimeStart('xLock'); const f = __openFiles[pFile]; let rc = 0; - if( capi.SQLITE_LOCK_NONE === f.lockType ) { + /* All OPFS locks are exclusive locks. If xLock() has + previously succeeded, do nothing except record the lock + type. If no lock is active, have the async counterpart + lock the file. */ + if( !f.lockType ) { rc = opRun('xLock', pFile, lockType); if( 0===rc ) f.lockType = lockType; }else{ @@ -792,27 +803,27 @@ const installOpfsVfs = function callee(options){ const vfsSyncWrappers = { xAccess: function(pVfs,zName,flags,pOut){ mTimeStart('xAccess'); - const rc = opRun('xAccess', wasm.cstringToJs(zName)); - wasm.setMemValue( pOut, (rc ? 0 : 1), 'i32' ); + const rc = opRun('xAccess', wasm.cstrToJs(zName)); + wasm.poke( pOut, (rc ? 0 : 1), 'i32' ); mTimeEnd(); return 0; }, xCurrentTime: function(pVfs,pOut){ /* If it turns out that we need to adjust for timezone, see: https://stackoverflow.com/a/11760121/1458521 */ - wasm.setMemValue(pOut, 2440587.5 + (new Date().getTime()/86400000), + wasm.poke(pOut, 2440587.5 + (new Date().getTime()/86400000), 'double'); return 0; }, xCurrentTimeInt64: function(pVfs,pOut){ // TODO: confirm that this calculation is correct - wasm.setMemValue(pOut, (2440587.5 * 86400000) + new Date().getTime(), + wasm.poke(pOut, (2440587.5 * 86400000) + new Date().getTime(), 'i64'); return 0; }, xDelete: function(pVfs, zName, doSyncDir){ mTimeStart('xDelete'); - opRun('xDelete', wasm.cstringToJs(zName), doSyncDir, false); + opRun('xDelete', wasm.cstrToJs(zName), doSyncDir, false); /* We're ignoring errors because we cannot yet differentiate between harmless and non-harmless failures. */ mTimeEnd(); @@ -835,22 +846,28 @@ const installOpfsVfs = function callee(options){ //xSleep is optionally defined below xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){ mTimeStart('xOpen'); + let opfsFlags = 0; if(0===zName){ zName = randomFilename(); }else if('number'===typeof zName){ - zName = wasm.cstringToJs(zName); + if(capi.sqlite3_uri_boolean(zName, "opfs-unlock-asap", 0)){ + /* -----------------------^^^^^ MUST pass the untranslated + C-string here. */ + opfsFlags |= state.opfsFlags.OPFS_UNLOCK_ASAP; + } + zName = wasm.cstrToJs(zName); } const fh = Object.create(null); fh.fid = pFile; fh.filename = zName; fh.sab = new SharedArrayBuffer(state.fileBufferSize); fh.flags = flags; - const rc = opRun('xOpen', pFile, zName, flags); + const rc = opRun('xOpen', pFile, zName, flags, opfsFlags); if(!rc){ /* Recall that sqlite3_vfs::xClose() will be called, even on error, unless pFile->pMethods is NULL. */ if(fh.readOnly){ - wasm.setMemValue(pOutFlags, capi.SQLITE_OPEN_READONLY, 'i32'); + wasm.poke(pOutFlags, capi.SQLITE_OPEN_READONLY, 'i32'); } __openFiles[pFile] = fh; fh.sabView = state.sabFileBufView; @@ -886,14 +903,6 @@ const installOpfsVfs = function callee(options){ }; } - /* Install the vfs/io_methods into their C-level shared instances... */ - for(let k of Object.keys(ioSyncWrappers)){ - installMethod(opfsIoMethods, k, ioSyncWrappers[k]); - } - for(let k of Object.keys(vfsSyncWrappers)){ - installMethod(opfsVfs, k, vfsSyncWrappers[k]); - } - /** Expects an OPFS file path. It gets resolved, such that ".." components are properly expanded, and returned. If the 2nd arg @@ -1033,8 +1042,9 @@ const installOpfsVfs = function callee(options){ Irrevocably deletes _all_ files in the current origin's OPFS. Obviously, this must be used with great caution. It may throw an exception if removal of anything fails (e.g. a file is - locked), but the precise conditions under which it will throw - are not documented (so we cannot tell you what they are). + locked), but the precise conditions under which the underlying + APIs will throw are not documented (so we cannot tell you what + they are). */ opfsUtil.rmfr = async function(){ const dir = opfsUtil.rootDirectory, opt = {recurse: true}; @@ -1136,39 +1146,44 @@ const installOpfsVfs = function callee(options){ //TODO to support fiddle and worker1 db upload: //opfsUtil.createFile = function(absName, content=undefined){...} + //We have sqlite3.wasm.sqlite3_wasm_vfs_create_file() for this + //purpose but its interface and name are still under + //consideration. if(sqlite3.oo1){ - opfsUtil.OpfsDb = function(...args){ + const OpfsDb = function(...args){ const opt = sqlite3.oo1.DB.dbCtorHelper.normalizeArgs(...args); opt.vfs = opfsVfs.$zName; sqlite3.oo1.DB.dbCtorHelper.call(this, opt); }; - opfsUtil.OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype); + OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype); + sqlite3.oo1.OpfsDb = OpfsDb; sqlite3.oo1.DB.dbCtorHelper.setVfsPostOpenSql( opfsVfs.pointer, - [ - /* Truncate journal mode is faster than delete or wal for - this vfs, per speedtest1. */ - "pragma journal_mode=truncate;" - /* - This vfs benefits hugely from cache on moderate/large - speedtest1 --size 50 and --size 100 workloads. We currently - rely on setting a non-default cache size when building - sqlite3.wasm. If that policy changes, the cache can - be set here. - */ - //"pragma cache_size=-8388608;" - ].join('') + function(oo1Db, sqlite3){ + /* Set a relatively high default busy-timeout handler to + help OPFS dbs deal with multi-tab/multi-worker + contention. */ + sqlite3.capi.sqlite3_busy_timeout(oo1Db, 10000); + sqlite3.capi.sqlite3_exec(oo1Db, [ + /* Truncate journal mode is faster than delete for + this vfs, per speedtest1. That gap seems to have closed with + Chrome version 108 or 109, but "persist" is very roughly 5-6% + faster than truncate in initial tests. */ + "pragma journal_mode=persist;", + /* + This vfs benefits hugely from cache on moderate/large + speedtest1 --size 50 and --size 100 workloads. We + currently rely on setting a non-default cache size when + building sqlite3.wasm. If that policy changes, the cache + can be set here. + */ + "pragma cache_size=-16384;" + ], 0, 0, 0); + } ); - } + }/*extend sqlite3.oo1*/ - /** - Potential TODOs: - - - Expose one or both of the Worker objects via opfsUtil and - publish an interface for proxying the higher-level OPFS - features like getting a directory listing. - */ const sanityCheck = function(){ const scope = wasm.scopedAllocPush(); const sq3File = new sqlite3_file(); @@ -1187,7 +1202,7 @@ const installOpfsVfs = function callee(options){ log("deserialize() says:",rc); if("This is ä string."!==rc[0]) toss("String d13n error."); vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut); - rc = wasm.getMemValue(pOut,'i32'); + rc = wasm.peek(pOut,'i32'); log("xAccess(",dbFile,") exists ?=",rc); rc = vfsSyncWrappers.xOpen(opfsVfs.pointer, zDbFile, fid, openFlags, pOut); @@ -1198,22 +1213,22 @@ const installOpfsVfs = function callee(options){ return; } vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut); - rc = wasm.getMemValue(pOut,'i32'); + rc = wasm.peek(pOut,'i32'); if(!rc) toss("xAccess() failed to detect file."); rc = ioSyncWrappers.xSync(sq3File.pointer, 0); if(rc) toss('sync failed w/ rc',rc); rc = ioSyncWrappers.xTruncate(sq3File.pointer, 1024); if(rc) toss('truncate failed w/ rc',rc); - wasm.setMemValue(pOut,0,'i64'); + wasm.poke(pOut,0,'i64'); rc = ioSyncWrappers.xFileSize(sq3File.pointer, pOut); if(rc) toss('xFileSize failed w/ rc',rc); - log("xFileSize says:",wasm.getMemValue(pOut, 'i64')); + log("xFileSize says:",wasm.peek(pOut, 'i64')); rc = ioSyncWrappers.xWrite(sq3File.pointer, zDbFile, 10, 1); if(rc) toss("xWrite() failed!"); const readBuf = wasm.scopedAlloc(16); rc = ioSyncWrappers.xRead(sq3File.pointer, readBuf, 6, 2); - wasm.setMemValue(readBuf+6,0); - let jRead = wasm.cstringToJs(readBuf); + wasm.poke(readBuf+6,0); + let jRead = wasm.cstrToJs(readBuf); log("xRead() got:",jRead); if("sanity"!==jRead) toss("Unexpected xRead() value."); if(vfsSyncWrappers.xSleep){ @@ -1226,7 +1241,7 @@ const installOpfsVfs = function callee(options){ log("Deleting file:",dbFile); vfsSyncWrappers.xDelete(opfsVfs.pointer, zDbFile, 0x1234); vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut); - rc = wasm.getMemValue(pOut,'i32'); + rc = wasm.peek(pOut,'i32'); if(rc) toss("Expecting 0 from xAccess(",dbFile,") after xDelete()."); warn("End of OPFS sanity checks."); }finally{ @@ -1238,6 +1253,11 @@ const installOpfsVfs = function callee(options){ W.onmessage = function({data}){ //log("Worker.onmessage:",data); switch(data.type){ + case 'opfs-unavailable': + /* Async proxy has determined that OPFS is unavailable. There's + nothing more for us to do here. */ + promiseReject(new Error(data.payload.join(' '))); + break; case 'opfs-async-loaded': /*Arrives as soon as the asyc proxy finishes loading. Pass our config and shared state on to the async worker.*/ @@ -1248,14 +1268,10 @@ const installOpfsVfs = function callee(options){ and has finished initializing, so the real work can begin...*/ try { - const rc = capi.sqlite3_vfs_register(opfsVfs.pointer, 0); - if(rc){ - toss("sqlite3_vfs_register(OPFS) failed with rc",rc); - } - if(opfsVfs.pointer !== capi.sqlite3_vfs_find("opfs")){ - toss("BUG: sqlite3_vfs_find() failed for just-installed OPFS VFS"); - } - capi.sqlite3_vfs_register.addReference(opfsVfs, opfsIoMethods); + sqlite3.vfs.installVfs({ + io: {struct: opfsIoMethods, methods: ioSyncWrappers}, + vfs: {struct: opfsVfs, methods: vfsSyncWrappers} + }); state.sabOPView = new Int32Array(state.sabOP); state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize); state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize); @@ -1264,14 +1280,18 @@ const installOpfsVfs = function callee(options){ warn("Running sanity checks because of opfs-sanity-check URL arg..."); sanityCheck(); } - navigator.storage.getDirectory().then((d)=>{ - W.onerror = W._originalOnError; - delete W._originalOnError; - sqlite3.opfs = opfsUtil; - opfsUtil.rootDirectory = d; - log("End of OPFS sqlite3_vfs setup.", opfsVfs); + if(thisThreadHasOPFS()){ + navigator.storage.getDirectory().then((d)=>{ + W.onerror = W._originalOnError; + delete W._originalOnError; + sqlite3.opfs = opfsUtil; + opfsUtil.rootDirectory = d; + log("End of OPFS sqlite3_vfs setup.", opfsVfs); + promiseResolve(sqlite3); + }).catch(promiseReject); + }else{ promiseResolve(sqlite3); - }); + } }catch(e){ error(e); promiseReject(e); @@ -1290,9 +1310,6 @@ const installOpfsVfs = function callee(options){ installOpfsVfs.defaultProxyUri = "sqlite3-opfs-async-proxy.js"; self.sqlite3ApiBootstrap.initializersAsync.push(async (sqlite3)=>{ - if(sqlite3.scriptInfo && !sqlite3.scriptInfo.isWorker){ - return; - } try{ let proxyJs = installOpfsVfs.defaultProxyUri; if(sqlite3.scriptInfo.sqlite3Dir){ diff --git a/ext/wasm/api/sqlite3-wasm.c b/ext/wasm/api/sqlite3-wasm.c index af5ed6bf76..a42ea68d42 100644 --- a/ext/wasm/api/sqlite3-wasm.c +++ b/ext/wasm/api/sqlite3-wasm.c @@ -32,21 +32,16 @@ ** the same db handle as another thread, thus multi-threading support ** is unnecessary in the library. Because the filesystems are virtual ** and local to a given wasm runtime instance, two Workers can never -** access the same db file at once, with the exception of OPFS. As of -** this writing (2022-09-30), OPFS exclusively locks a file when -** opening it, so two Workers can never open the same OPFS-backed file -** at once. That situation will change if and when lower-level locking -** features are added to OPFS (as is currently planned, per folks -** involved with its development). +** access the same db file at once, with the exception of OPFS. ** -** Summary: except for the case of future OPFS, which supports -** locking, and any similar future filesystems, threading and file -** locking support are unnecessary in the wasm build. +** Summary: except for the case of OPFS, which supports locking using +** its own API, threading and file locking support are unnecessary in +** the wasm build. */ /* ** Undefine any SQLITE_... config flags which we specifically do not -** want undefined. Please keep these alphabetized. +** want defined. Please keep these alphabetized. */ #undef SQLITE_OMIT_DESERIALIZE #undef SQLITE_OMIT_MEMORYDB @@ -69,9 +64,17 @@ */ # define SQLITE_DEFAULT_CACHE_SIZE -16384 #endif -#if 0 && !defined(SQLITE_DEFAULT_PAGE_SIZE) -/* TODO: experiment with this. */ -# define SQLITE_DEFAULT_PAGE_SIZE 8192 /*4096*/ +#if !defined(SQLITE_DEFAULT_PAGE_SIZE) +/* +** OPFS performance is improved by approx. 12% with a page size of 8kb +** instead of 4kb. Performance with 16kb is equivalent to 8kb. +** +** Performance difference of kvvfs with a page size of 8kb compared to +** 4kb, as measured by speedtest1 --size 4, is indeterminate: +** measurements are all over the place either way and not +** significantly different. +*/ +# define SQLITE_DEFAULT_PAGE_SIZE 8192 #endif #ifndef SQLITE_DEFAULT_UNIX_VFS # define SQLITE_DEFAULT_UNIX_VFS "unix-none" @@ -109,6 +112,12 @@ # define SQLITE_ENABLE_UNKNOWN_SQL_FUNCTION #endif +/**********************************************************************/ +/* SQLITE_M... */ +#ifndef SQLITE_MAX_ALLOCATION_SIZE +# define SQLITE_MAX_ALLOCATION_SIZE 0x1fffffff +#endif + /**********************************************************************/ /* SQLITE_O... */ #ifndef SQLITE_OMIT_DEPRECATED @@ -359,11 +368,11 @@ void sqlite3_wasm_test_struct(WasmTestStruct * s){ */ SQLITE_WASM_KEEP const char * sqlite3_wasm_enum_json(void){ - static char aBuffer[1024 * 12] = {0} /* where the JSON goes */; + static char aBuffer[1024 * 20] = {0} /* where the JSON goes */; int n = 0, nChildren = 0, nStruct = 0 /* output counters for figuring out where commas go */; char * zPos = &aBuffer[1] /* skip first byte for now to help protect - ** against a small race condition */; + ** against a small race condition */; char const * const zEnd = &aBuffer[0] + sizeof(aBuffer) /* one-past-the-end */; if(aBuffer[0]) return aBuffer; /* Leave aBuffer[0] at 0 until the end to help guard against a tiny @@ -401,12 +410,83 @@ const char * sqlite3_wasm_enum_json(void){ DefInt(SQLITE_ACCESS_READ)/*docs say this is unused*/; } _DefGroup; + DefGroup(authorizer){ + DefInt(SQLITE_DENY); + DefInt(SQLITE_IGNORE); + DefInt(SQLITE_CREATE_INDEX); + DefInt(SQLITE_CREATE_TABLE); + DefInt(SQLITE_CREATE_TEMP_INDEX); + DefInt(SQLITE_CREATE_TEMP_TABLE); + DefInt(SQLITE_CREATE_TEMP_TRIGGER); + DefInt(SQLITE_CREATE_TEMP_VIEW); + DefInt(SQLITE_CREATE_TRIGGER); + DefInt(SQLITE_CREATE_VIEW); + DefInt(SQLITE_DELETE); + DefInt(SQLITE_DROP_INDEX); + DefInt(SQLITE_DROP_TABLE); + DefInt(SQLITE_DROP_TEMP_INDEX); + DefInt(SQLITE_DROP_TEMP_TABLE); + DefInt(SQLITE_DROP_TEMP_TRIGGER); + DefInt(SQLITE_DROP_TEMP_VIEW); + DefInt(SQLITE_DROP_TRIGGER); + DefInt(SQLITE_DROP_VIEW); + DefInt(SQLITE_INSERT); + DefInt(SQLITE_PRAGMA); + DefInt(SQLITE_READ); + DefInt(SQLITE_SELECT); + DefInt(SQLITE_TRANSACTION); + DefInt(SQLITE_UPDATE); + DefInt(SQLITE_ATTACH); + DefInt(SQLITE_DETACH); + DefInt(SQLITE_ALTER_TABLE); + DefInt(SQLITE_REINDEX); + DefInt(SQLITE_ANALYZE); + DefInt(SQLITE_CREATE_VTABLE); + DefInt(SQLITE_DROP_VTABLE); + DefInt(SQLITE_FUNCTION); + DefInt(SQLITE_SAVEPOINT); + //DefInt(SQLITE_COPY) /* No longer used */; + DefInt(SQLITE_RECURSIVE); + } _DefGroup; + DefGroup(blobFinalizers) { /* SQLITE_STATIC/TRANSIENT need to be handled explicitly as ** integers to avoid casting-related warnings. */ out("\"SQLITE_STATIC\":0, \"SQLITE_TRANSIENT\":-1"); } _DefGroup; + DefGroup(config){ + DefInt(SQLITE_CONFIG_SINGLETHREAD); + DefInt(SQLITE_CONFIG_MULTITHREAD); + DefInt(SQLITE_CONFIG_SERIALIZED); + DefInt(SQLITE_CONFIG_MALLOC); + DefInt(SQLITE_CONFIG_GETMALLOC); + DefInt(SQLITE_CONFIG_SCRATCH); + DefInt(SQLITE_CONFIG_PAGECACHE); + DefInt(SQLITE_CONFIG_HEAP); + DefInt(SQLITE_CONFIG_MEMSTATUS); + DefInt(SQLITE_CONFIG_MUTEX); + DefInt(SQLITE_CONFIG_GETMUTEX); +/* previously SQLITE_CONFIG_CHUNKALLOC 12 which is now unused. */ + DefInt(SQLITE_CONFIG_LOOKASIDE); + DefInt(SQLITE_CONFIG_PCACHE); + DefInt(SQLITE_CONFIG_GETPCACHE); + DefInt(SQLITE_CONFIG_LOG); + DefInt(SQLITE_CONFIG_URI); + DefInt(SQLITE_CONFIG_PCACHE2); + DefInt(SQLITE_CONFIG_GETPCACHE2); + DefInt(SQLITE_CONFIG_COVERING_INDEX_SCAN); + DefInt(SQLITE_CONFIG_SQLLOG); + DefInt(SQLITE_CONFIG_MMAP_SIZE); + DefInt(SQLITE_CONFIG_WIN32_HEAPSIZE); + DefInt(SQLITE_CONFIG_PCACHE_HDRSZ); + DefInt(SQLITE_CONFIG_PMASZ); + DefInt(SQLITE_CONFIG_STMTJRNL_SPILL); + DefInt(SQLITE_CONFIG_SMALL_MALLOC); + DefInt(SQLITE_CONFIG_SORTERREF_SIZE); + DefInt(SQLITE_CONFIG_MEMDB_MAXSIZE); + } _DefGroup; + DefGroup(dataTypes) { DefInt(SQLITE_INTEGER); DefInt(SQLITE_FLOAT); @@ -415,6 +495,45 @@ const char * sqlite3_wasm_enum_json(void){ DefInt(SQLITE_NULL); } _DefGroup; + DefGroup(dbConfig){ + DefInt(SQLITE_DBCONFIG_MAINDBNAME); + DefInt(SQLITE_DBCONFIG_LOOKASIDE); + DefInt(SQLITE_DBCONFIG_ENABLE_FKEY); + DefInt(SQLITE_DBCONFIG_ENABLE_TRIGGER); + DefInt(SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER); + DefInt(SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION); + DefInt(SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE); + DefInt(SQLITE_DBCONFIG_ENABLE_QPSG); + DefInt(SQLITE_DBCONFIG_TRIGGER_EQP); + DefInt(SQLITE_DBCONFIG_RESET_DATABASE); + DefInt(SQLITE_DBCONFIG_DEFENSIVE); + DefInt(SQLITE_DBCONFIG_WRITABLE_SCHEMA); + DefInt(SQLITE_DBCONFIG_LEGACY_ALTER_TABLE); + DefInt(SQLITE_DBCONFIG_DQS_DML); + DefInt(SQLITE_DBCONFIG_DQS_DDL); + DefInt(SQLITE_DBCONFIG_ENABLE_VIEW); + DefInt(SQLITE_DBCONFIG_LEGACY_FILE_FORMAT); + DefInt(SQLITE_DBCONFIG_TRUSTED_SCHEMA); + DefInt(SQLITE_DBCONFIG_MAX); + } _DefGroup; + + DefGroup(dbStatus){ + DefInt(SQLITE_DBSTATUS_LOOKASIDE_USED); + DefInt(SQLITE_DBSTATUS_CACHE_USED); + DefInt(SQLITE_DBSTATUS_SCHEMA_USED); + DefInt(SQLITE_DBSTATUS_STMT_USED); + DefInt(SQLITE_DBSTATUS_LOOKASIDE_HIT); + DefInt(SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE); + DefInt(SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL); + DefInt(SQLITE_DBSTATUS_CACHE_HIT); + DefInt(SQLITE_DBSTATUS_CACHE_MISS); + DefInt(SQLITE_DBSTATUS_CACHE_WRITE); + DefInt(SQLITE_DBSTATUS_DEFERRED_FKS); + DefInt(SQLITE_DBSTATUS_CACHE_USED_SHARED); + DefInt(SQLITE_DBSTATUS_CACHE_SPILL); + DefInt(SQLITE_DBSTATUS_MAX); + } _DefGroup; + DefGroup(encodings) { /* Noting that the wasm binding only aims to support UTF-8. */ DefInt(SQLITE_UTF8); @@ -494,6 +613,34 @@ const char * sqlite3_wasm_enum_json(void){ DefInt(SQLITE_IOCAP_BATCH_ATOMIC); } _DefGroup; + DefGroup(limits) { + DefInt(SQLITE_MAX_ALLOCATION_SIZE); + DefInt(SQLITE_LIMIT_LENGTH); + DefInt(SQLITE_MAX_LENGTH); + DefInt(SQLITE_LIMIT_SQL_LENGTH); + DefInt(SQLITE_MAX_SQL_LENGTH); + DefInt(SQLITE_LIMIT_COLUMN); + DefInt(SQLITE_MAX_COLUMN); + DefInt(SQLITE_LIMIT_EXPR_DEPTH); + DefInt(SQLITE_MAX_EXPR_DEPTH); + DefInt(SQLITE_LIMIT_COMPOUND_SELECT); + DefInt(SQLITE_MAX_COMPOUND_SELECT); + DefInt(SQLITE_LIMIT_VDBE_OP); + DefInt(SQLITE_MAX_VDBE_OP); + DefInt(SQLITE_LIMIT_FUNCTION_ARG); + DefInt(SQLITE_MAX_FUNCTION_ARG); + DefInt(SQLITE_LIMIT_ATTACHED); + DefInt(SQLITE_MAX_ATTACHED); + DefInt(SQLITE_LIMIT_LIKE_PATTERN_LENGTH); + DefInt(SQLITE_MAX_LIKE_PATTERN_LENGTH); + DefInt(SQLITE_LIMIT_VARIABLE_NUMBER); + DefInt(SQLITE_MAX_VARIABLE_NUMBER); + DefInt(SQLITE_LIMIT_TRIGGER_DEPTH); + DefInt(SQLITE_MAX_TRIGGER_DEPTH); + DefInt(SQLITE_LIMIT_WORKER_THREADS); + DefInt(SQLITE_MAX_WORKER_THREADS); + } _DefGroup; + DefGroup(openFlags) { /* Noting that not all of these will have any effect in ** WASM-space. */ @@ -644,6 +791,31 @@ const char * sqlite3_wasm_enum_json(void){ DefInt(SQLITE_DESERIALIZE_RESIZEABLE); } _DefGroup; + DefGroup(sqlite3Status){ + DefInt(SQLITE_STATUS_MEMORY_USED); + DefInt(SQLITE_STATUS_PAGECACHE_USED); + DefInt(SQLITE_STATUS_PAGECACHE_OVERFLOW); + //DefInt(SQLITE_STATUS_SCRATCH_USED) /* NOT USED */; + //DefInt(SQLITE_STATUS_SCRATCH_OVERFLOW) /* NOT USED */; + DefInt(SQLITE_STATUS_MALLOC_SIZE); + DefInt(SQLITE_STATUS_PARSER_STACK); + DefInt(SQLITE_STATUS_PAGECACHE_SIZE); + //DefInt(SQLITE_STATUS_SCRATCH_SIZE) /* NOT USED */; + DefInt(SQLITE_STATUS_MALLOC_COUNT); + } _DefGroup; + + DefGroup(stmtStatus){ + DefInt(SQLITE_STMTSTATUS_FULLSCAN_STEP); + DefInt(SQLITE_STMTSTATUS_SORT); + DefInt(SQLITE_STMTSTATUS_AUTOINDEX); + DefInt(SQLITE_STMTSTATUS_VM_STEP); + DefInt(SQLITE_STMTSTATUS_REPREPARE); + DefInt(SQLITE_STMTSTATUS_RUN); + DefInt(SQLITE_STMTSTATUS_FILTER_MISS); + DefInt(SQLITE_STMTSTATUS_FILTER_HIT); + DefInt(SQLITE_STMTSTATUS_MEMUSED); + } _DefGroup; + DefGroup(syncFlags) { DefInt(SQLITE_SYNC_NORMAL); DefInt(SQLITE_SYNC_FULL); @@ -657,6 +829,12 @@ const char * sqlite3_wasm_enum_json(void){ DefInt(SQLITE_TRACE_CLOSE); } _DefGroup; + DefGroup(txnState){ + DefInt(SQLITE_TXN_NONE); + DefInt(SQLITE_TXN_READ); + DefInt(SQLITE_TXN_WRITE); + } _DefGroup; + DefGroup(udfFlags) { DefInt(SQLITE_DETERMINISTIC); DefInt(SQLITE_DIRECTONLY); @@ -668,7 +846,36 @@ const char * sqlite3_wasm_enum_json(void){ DefStr(SQLITE_VERSION); DefStr(SQLITE_SOURCE_ID); } _DefGroup; - + + DefGroup(vtab) { + DefInt(SQLITE_INDEX_SCAN_UNIQUE); + DefInt(SQLITE_INDEX_CONSTRAINT_EQ); + DefInt(SQLITE_INDEX_CONSTRAINT_GT); + DefInt(SQLITE_INDEX_CONSTRAINT_LE); + DefInt(SQLITE_INDEX_CONSTRAINT_LT); + DefInt(SQLITE_INDEX_CONSTRAINT_GE); + DefInt(SQLITE_INDEX_CONSTRAINT_MATCH); + DefInt(SQLITE_INDEX_CONSTRAINT_LIKE); + DefInt(SQLITE_INDEX_CONSTRAINT_GLOB); + DefInt(SQLITE_INDEX_CONSTRAINT_REGEXP); + DefInt(SQLITE_INDEX_CONSTRAINT_NE); + DefInt(SQLITE_INDEX_CONSTRAINT_ISNOT); + DefInt(SQLITE_INDEX_CONSTRAINT_ISNOTNULL); + DefInt(SQLITE_INDEX_CONSTRAINT_ISNULL); + DefInt(SQLITE_INDEX_CONSTRAINT_IS); + DefInt(SQLITE_INDEX_CONSTRAINT_LIMIT); + DefInt(SQLITE_INDEX_CONSTRAINT_OFFSET); + DefInt(SQLITE_INDEX_CONSTRAINT_FUNCTION); + DefInt(SQLITE_VTAB_CONSTRAINT_SUPPORT); + DefInt(SQLITE_VTAB_INNOCUOUS); + DefInt(SQLITE_VTAB_DIRECTONLY); + DefInt(SQLITE_ROLLBACK); + //DefInt(SQLITE_IGNORE); // Also used by sqlite3_authorizer() callback + DefInt(SQLITE_FAIL); + //DefInt(SQLITE_ABORT); // Also an error code + DefInt(SQLITE_REPLACE); + } _DefGroup; + #undef DefGroup #undef DefStr #undef DefInt @@ -695,8 +902,8 @@ const char * sqlite3_wasm_enum_json(void){ /** Macros for emitting StructBinder description. */ #define StructBinder__(TYPE) \ n = 0; \ - outf("%s{", (nStruct++ ? ", " : "")); \ - out("\"name\": \"" # TYPE "\","); \ + outf("%s{", (nStruct++ ? ", " : "")); \ + out("\"name\": \"" # TYPE "\","); \ outf("\"sizeof\": %d", (int)sizeof(TYPE)); \ out(",\"members\": {"); #define StructBinder_(T) StructBinder__(T) @@ -716,78 +923,200 @@ const char * sqlite3_wasm_enum_json(void){ #define CurrentStruct sqlite3_vfs StructBinder { - M(iVersion,"i"); - M(szOsFile,"i"); - M(mxPathname,"i"); - M(pNext,"p"); - M(zName,"s"); - M(pAppData,"p"); - M(xOpen,"i(pppip)"); - M(xDelete,"i(ppi)"); - M(xAccess,"i(ppip)"); - M(xFullPathname,"i(ppip)"); - M(xDlOpen,"p(pp)"); - M(xDlError,"p(pip)"); - M(xDlSym,"p()"); - M(xDlClose,"v(pp)"); - M(xRandomness,"i(pip)"); - M(xSleep,"i(pi)"); - M(xCurrentTime,"i(pp)"); - M(xGetLastError,"i(pip)"); - M(xCurrentTimeInt64,"i(pp)"); - M(xSetSystemCall,"i(ppp)"); - M(xGetSystemCall,"p(pp)"); - M(xNextSystemCall,"p(pp)"); + M(iVersion, "i"); + M(szOsFile, "i"); + M(mxPathname, "i"); + M(pNext, "p"); + M(zName, "s"); + M(pAppData, "p"); + M(xOpen, "i(pppip)"); + M(xDelete, "i(ppi)"); + M(xAccess, "i(ppip)"); + M(xFullPathname, "i(ppip)"); + M(xDlOpen, "p(pp)"); + M(xDlError, "p(pip)"); + M(xDlSym, "p()"); + M(xDlClose, "v(pp)"); + M(xRandomness, "i(pip)"); + M(xSleep, "i(pi)"); + M(xCurrentTime, "i(pp)"); + M(xGetLastError, "i(pip)"); + M(xCurrentTimeInt64, "i(pp)"); + M(xSetSystemCall, "i(ppp)"); + M(xGetSystemCall, "p(pp)"); + M(xNextSystemCall, "p(pp)"); } _StructBinder; #undef CurrentStruct #define CurrentStruct sqlite3_io_methods StructBinder { - M(iVersion,"i"); - M(xClose,"i(p)"); - M(xRead,"i(ppij)"); - M(xWrite,"i(ppij)"); - M(xTruncate,"i(pj)"); - M(xSync,"i(pi)"); - M(xFileSize,"i(pp)"); - M(xLock,"i(pi)"); - M(xUnlock,"i(pi)"); - M(xCheckReservedLock,"i(pp)"); - M(xFileControl,"i(pip)"); - M(xSectorSize,"i(p)"); - M(xDeviceCharacteristics,"i(p)"); - M(xShmMap,"i(piiip)"); - M(xShmLock,"i(piii)"); - M(xShmBarrier,"v(p)"); - M(xShmUnmap,"i(pi)"); - M(xFetch,"i(pjip)"); - M(xUnfetch,"i(pjp)"); + M(iVersion, "i"); + M(xClose, "i(p)"); + M(xRead, "i(ppij)"); + M(xWrite, "i(ppij)"); + M(xTruncate, "i(pj)"); + M(xSync, "i(pi)"); + M(xFileSize, "i(pp)"); + M(xLock, "i(pi)"); + M(xUnlock, "i(pi)"); + M(xCheckReservedLock, "i(pp)"); + M(xFileControl, "i(pip)"); + M(xSectorSize, "i(p)"); + M(xDeviceCharacteristics, "i(p)"); + M(xShmMap, "i(piiip)"); + M(xShmLock, "i(piii)"); + M(xShmBarrier, "v(p)"); + M(xShmUnmap, "i(pi)"); + M(xFetch, "i(pjip)"); + M(xUnfetch, "i(pjp)"); } _StructBinder; #undef CurrentStruct #define CurrentStruct sqlite3_file StructBinder { - M(pMethods,"p"); + M(pMethods, "p"); } _StructBinder; #undef CurrentStruct #define CurrentStruct sqlite3_kvvfs_methods StructBinder { - M(xRead,"i(sspi)"); - M(xWrite,"i(sss)"); - M(xDelete,"i(ss)"); - M(nKeySize,"i"); + M(xRead, "i(sspi)"); + M(xWrite, "i(sss)"); + M(xDelete, "i(ss)"); + M(nKeySize, "i"); + } _StructBinder; +#undef CurrentStruct + + +#define CurrentStruct sqlite3_vtab + StructBinder { + M(pModule, "p"); + M(nRef, "i"); + M(zErrMsg, "p"); + } _StructBinder; +#undef CurrentStruct + +#define CurrentStruct sqlite3_vtab_cursor + StructBinder { + M(pVtab, "p"); + } _StructBinder; +#undef CurrentStruct + +#define CurrentStruct sqlite3_module + StructBinder { + M(iVersion, "i"); + M(xCreate, "i(ppippp)"); + M(xConnect, "i(ppippp)"); + M(xBestIndex, "i(pp)"); + M(xDisconnect, "i(p)"); + M(xDestroy, "i(p)"); + M(xOpen, "i(pp)"); + M(xClose, "i(p)"); + M(xFilter, "i(pisip)"); + M(xNext, "i(p)"); + M(xEof, "i(p)"); + M(xColumn, "i(ppi)"); + M(xRowid, "i(pp)"); + M(xUpdate, "i(pipp)"); + M(xBegin, "i(p)"); + M(xSync, "i(p)"); + M(xCommit, "i(p)"); + M(xRollback, "i(p)"); + M(xFindFunction, "i(pispp)"); + M(xRename, "i(ps)"); + // ^^^ v1. v2+ follows... + M(xSavepoint, "i(pi)"); + M(xRelease, "i(pi)"); + M(xRollbackTo, "i(pi)"); + // ^^^ v2. v3+ follows... + M(xShadowName, "i(s)"); + } _StructBinder; +#undef CurrentStruct + + /** + ** Workaround: in order to map the various inner structs from + ** sqlite3_index_info, we have to uplift those into constructs we + ** can access by type name. These structs _must_ match their + ** in-sqlite3_index_info counterparts byte for byte. + */ + typedef struct { + int iColumn; + unsigned char op; + unsigned char usable; + int iTermOffset; + } sqlite3_index_constraint; + typedef struct { + int iColumn; + unsigned char desc; + } sqlite3_index_orderby; + typedef struct { + int argvIndex; + unsigned char omit; + } sqlite3_index_constraint_usage; + { /* Validate that the above struct sizeof()s match + ** expectations. We could improve upon this by + ** checking the offsetof() for each member. */ + const sqlite3_index_info siiCheck; +#define IndexSzCheck(T,M) \ + (sizeof(T) == sizeof(*siiCheck.M)) + if(!IndexSzCheck(sqlite3_index_constraint,aConstraint) + || !IndexSzCheck(sqlite3_index_orderby,aOrderBy) + || !IndexSzCheck(sqlite3_index_constraint_usage,aConstraintUsage)){ + assert(!"sizeof mismatch in sqlite3_index_... struct(s)"); + return 0; + } +#undef IndexSzCheck + } + +#define CurrentStruct sqlite3_index_constraint + StructBinder { + M(iColumn, "i"); + M(op, "C"); + M(usable, "C"); + M(iTermOffset, "i"); + } _StructBinder; +#undef CurrentStruct + +#define CurrentStruct sqlite3_index_orderby + StructBinder { + M(iColumn, "i"); + M(desc, "C"); + } _StructBinder; +#undef CurrentStruct + +#define CurrentStruct sqlite3_index_constraint_usage + StructBinder { + M(argvIndex, "i"); + M(omit, "C"); + } _StructBinder; +#undef CurrentStruct + +#define CurrentStruct sqlite3_index_info + StructBinder { + M(nConstraint, "i"); + M(aConstraint, "p"); + M(nOrderBy, "i"); + M(aOrderBy, "p"); + M(aConstraintUsage, "p"); + M(idxNum, "i"); + M(idxStr, "p"); + M(needToFreeIdxStr, "i"); + M(orderByConsumed, "i"); + M(estimatedCost, "d"); + M(estimatedRows, "j"); + M(idxFlags, "i"); + M(colUsed, "j"); } _StructBinder; #undef CurrentStruct #if SQLITE_WASM_TESTS #define CurrentStruct WasmTestStruct StructBinder { - M(v4,"i"); - M(cstr,"s"); - M(ppV,"p"); - M(v8,"j"); - M(xFunc,"v(p)"); + M(v4, "i"); + M(cstr, "s"); + M(ppV, "p"); + M(v8, "j"); + M(xFunc, "v(p)"); } _StructBinder; #undef CurrentStruct #endif @@ -820,7 +1149,7 @@ const char * sqlite3_wasm_enum_json(void){ ** call is returned. */ SQLITE_WASM_KEEP -int sqlite3_wasm_vfs_unlink(sqlite3_vfs *pVfs, const char * zName){ +int sqlite3_wasm_vfs_unlink(sqlite3_vfs *pVfs, const char *zName){ int rc = SQLITE_MISUSE /* ??? */; if( 0==pVfs && 0!=zName ) pVfs = sqlite3_vfs_find(0); if( zName && pVfs && pVfs->xDelete ){ @@ -851,23 +1180,33 @@ sqlite3_vfs * sqlite3_wasm_db_vfs(sqlite3 *pDb, const char *zDbName){ ** ** This function resets the given db pointer's database as described at ** -** https://www.sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigresetdatabase +** https://sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigresetdatabase +** +** But beware: virtual tables destroyed that way do not have their +** xDestroy() called, so will leak if they require that function for +** proper cleanup. ** ** Returns 0 on success, an SQLITE_xxx code on error. Returns ** SQLITE_MISUSE if pDb is NULL. */ SQLITE_WASM_KEEP -int sqlite3_wasm_db_reset(sqlite3*pDb){ +int sqlite3_wasm_db_reset(sqlite3 *pDb){ int rc = SQLITE_MISUSE; if( pDb ){ + sqlite3_table_column_metadata(pDb, "main", 0, 0, 0, 0, 0, 0, 0); rc = sqlite3_db_config(pDb, SQLITE_DBCONFIG_RESET_DATABASE, 1, 0); - if( 0==rc ) rc = sqlite3_exec(pDb, "VACUUM", 0, 0, 0); - sqlite3_db_config(pDb, SQLITE_DBCONFIG_RESET_DATABASE, 0, 0); + if( 0==rc ){ + rc = sqlite3_exec(pDb, "VACUUM", 0, 0, 0); + sqlite3_db_config(pDb, SQLITE_DBCONFIG_RESET_DATABASE, 0, 0); + } } return rc; } /* +** This function is NOT part of the sqlite3 public API. It is strictly +** for use by the sqlite project's own JS/WASM bindings. +** ** Uses the given database's VFS xRead to stream the db file's ** contents out to the given callback. The callback gets a single ** chunk of size n (its 2nd argument) on each call and must return 0 @@ -907,7 +1246,7 @@ int sqlite3_wasm_db_export_chunked( sqlite3* pDb, } for( ; 0==rc && nPospMethods->xRead(pFile, buf, nBuf, nPos); - if(SQLITE_IOERR_SHORT_READ == rc){ + if( SQLITE_IOERR_SHORT_READ == rc ){ rc = (nPos + nBuf) < nSize ? rc : 0/*assume EOF*/; } if( 0==rc ) rc = xCallback(buf, nBuf); @@ -916,25 +1255,30 @@ int sqlite3_wasm_db_export_chunked( sqlite3* pDb, } /* -** A proxy for sqlite3_serialize() which serializes the "main" schema +** This function is NOT part of the sqlite3 public API. It is strictly +** for use by the sqlite project's own JS/WASM bindings. +** +** A proxy for sqlite3_serialize() which serializes the schema zSchema ** of pDb, placing the serialized output in pOut and nOut. nOut may be -** NULL. If pDb or pOut are NULL then SQLITE_MISUSE is returned. If -** allocation of the serialized copy fails, SQLITE_NOMEM is returned. -** On success, 0 is returned and `*pOut` will contain a pointer to the -** memory unless mFlags includes SQLITE_SERIALIZE_NOCOPY and the -** database has no contiguous memory representation, in which case -** `*pOut` will be NULL but 0 will be returned. +** NULL. If zSchema is NULL then "main" is assumed. If pDb or pOut are +** NULL then SQLITE_MISUSE is returned. If allocation of the +** serialized copy fails, SQLITE_NOMEM is returned. On success, 0 is +** returned and `*pOut` will contain a pointer to the memory unless +** mFlags includes SQLITE_SERIALIZE_NOCOPY and the database has no +** contiguous memory representation, in which case `*pOut` will be +** NULL but 0 will be returned. ** ** If `*pOut` is not NULL, the caller is responsible for passing it to ** sqlite3_free() to free it. */ SQLITE_WASM_KEEP -int sqlite3_wasm_db_serialize( sqlite3 *pDb, unsigned char **pOut, +int sqlite3_wasm_db_serialize( sqlite3 *pDb, const char *zSchema, + unsigned char **pOut, sqlite3_int64 *nOut, unsigned int mFlags ){ unsigned char * z; if( !pDb || !pOut ) return SQLITE_MISUSE; - if(nOut) *nOut = 0; - z = sqlite3_serialize(pDb, "main", nOut, mFlags); + if( nOut ) *nOut = 0; + z = sqlite3_serialize(pDb, zSchema ? zSchema : "main", nOut, mFlags); if( z || (SQLITE_SERIALIZE_NOCOPY & mFlags) ){ *pOut = z; return 0; @@ -948,14 +1292,17 @@ int sqlite3_wasm_db_serialize( sqlite3 *pDb, unsigned char **pOut, ** for use by the sqlite project's own JS/WASM bindings. ** ** Creates a new file using the I/O API of the given VFS, containing -** the given number of bytes of the given data. If the file exists, -** it is truncated to the given length and populated with the given +** the given number of bytes of the given data. If the file exists, it +** is truncated to the given length and populated with the given ** data. ** ** This function exists so that we can implement the equivalent of ** Emscripten's FS.createDataFile() in a VFS-agnostic way. This ** functionality is intended for use in uploading database files. ** +** Not all VFSes support this functionality, e.g. the "kvvfs" does +** not. +** ** If pVfs is NULL, sqlite3_vfs_find(0) is used. ** ** If zFile is NULL, pVfs is NULL (and sqlite3_vfs_find(0) returns @@ -967,10 +1314,7 @@ int sqlite3_wasm_db_serialize( sqlite3 *pDb, unsigned char **pOut, ** ** Whether or not directory components of zFilename are created ** automatically or not is unspecified: that detail is left to the -** VFS. The "opfs" VFS, for example, create them. -** -** Not all VFSes support this functionality, e.g. the "kvvfs" does -** not. +** VFS. The "opfs" VFS, for example, creates them. ** ** If an error happens while populating or truncating the file, the ** target file will be deleted (if needed) if this function created @@ -1002,11 +1346,18 @@ int sqlite3_wasm_vfs_create_file( sqlite3_vfs *pVfs, ** it may have a buffer limit related to sqlite3's pager size, we ** conservatively write in 512-byte blocks (smallest page ** size). */; - + //fprintf(stderr, "pVfs=%p, zFilename=%s, nData=%d\n", pVfs, zFilename, nData); if( !pVfs ) pVfs = sqlite3_vfs_find(0); if( !pVfs || !zFilename || nData<0 ) return SQLITE_MISUSE; pVfs->xAccess(pVfs, zFilename, SQLITE_ACCESS_EXISTS, &fileExisted); rc = sqlite3OsOpenMalloc(pVfs, zFilename, &pFile, openFlags, &flagsOut); +#if 0 +# define RC fprintf(stderr,"create_file(%s,%s) @%d rc=%d\n", \ + pVfs->zName, zFilename, __LINE__, rc); +#else +# define RC +#endif + RC; if(rc) return rc; pIo = pFile->pMethods; if( pIo->xLock ) { @@ -1017,26 +1368,37 @@ int sqlite3_wasm_vfs_create_file( sqlite3_vfs *pVfs, ** xFileSize(), xTruncate(), and the like, and release the lock ** only if it was unlocked when the op was started. */ rc = pIo->xLock(pFile, SQLITE_LOCK_EXCLUSIVE); + RC; doUnlock = 0==rc; } - if( 0==rc) rc = pIo->xTruncate(pFile, nData); + if( 0==rc ){ + rc = pIo->xTruncate(pFile, nData); + RC; + } if( 0==rc && 0!=pData && nData>0 ){ while( 0==rc && nData>0 ){ const int n = nData>=blockSize ? blockSize : nData; rc = pIo->xWrite(pFile, pPos, n, (sqlite3_int64)(pPos - pData)); + RC; nData -= n; pPos += n; } if( 0==rc && nData>0 ){ assert( nDataxWrite(pFile, pPos, nData, (sqlite3_int64)(pPos - pData)); + rc = pIo->xWrite(pFile, pPos, nData, + (sqlite3_int64)(pPos - pData)); + RC; } } - if( pIo->xUnlock && doUnlock!=0 ) pIo->xUnlock(pFile, SQLITE_LOCK_NONE); + if( pIo->xUnlock && doUnlock!=0 ){ + pIo->xUnlock(pFile, SQLITE_LOCK_NONE); + } pIo->xClose(pFile); if( rc!=0 && 0==fileExisted ){ pVfs->xDelete(pVfs, zFilename, 1); } + RC; +#undef RC return rc; } @@ -1074,6 +1436,129 @@ sqlite3_kvvfs_methods * sqlite3_wasm_kvvfs_methods(void){ return &sqlite3KvvfsMethods; } +/* +** This function is NOT part of the sqlite3 public API. It is strictly +** for use by the sqlite project's own JS/WASM bindings. +** +** This is a proxy for the variadic sqlite3_vtab_config() which passes +** its argument on, or not, to sqlite3_vtab_config(), depending on the +** value of its 2nd argument. Returns the result of +** sqlite3_vtab_config(), or SQLITE_MISUSE if the 2nd arg is not a +** valid value. +*/ +SQLITE_WASM_KEEP +int sqlite3_wasm_vtab_config(sqlite3 *pDb, int op, int arg){ + switch(op){ + case SQLITE_VTAB_DIRECTONLY: + case SQLITE_VTAB_INNOCUOUS: + return sqlite3_vtab_config(pDb, op); + case SQLITE_VTAB_CONSTRAINT_SUPPORT: + return sqlite3_vtab_config(pDb, op, arg); + default: + return SQLITE_MISUSE; + } +} + +/* +** This function is NOT part of the sqlite3 public API. It is strictly +** for use by the sqlite project's own JS/WASM bindings. +** +** Wrapper for the variants of sqlite3_db_config() which take +** (int,int*) variadic args. +*/ +SQLITE_WASM_KEEP +int sqlite3_wasm_db_config_ip(sqlite3 *pDb, int op, int arg1, int* pArg2){ + switch(op){ + case SQLITE_DBCONFIG_ENABLE_FKEY: + case SQLITE_DBCONFIG_ENABLE_TRIGGER: + case SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER: + case SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION: + case SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE: + case SQLITE_DBCONFIG_ENABLE_QPSG: + case SQLITE_DBCONFIG_TRIGGER_EQP: + case SQLITE_DBCONFIG_RESET_DATABASE: + case SQLITE_DBCONFIG_DEFENSIVE: + case SQLITE_DBCONFIG_WRITABLE_SCHEMA: + case SQLITE_DBCONFIG_LEGACY_ALTER_TABLE: + case SQLITE_DBCONFIG_DQS_DML: + case SQLITE_DBCONFIG_DQS_DDL: + case SQLITE_DBCONFIG_ENABLE_VIEW: + case SQLITE_DBCONFIG_LEGACY_FILE_FORMAT: + case SQLITE_DBCONFIG_TRUSTED_SCHEMA: + return sqlite3_db_config(pDb, op, arg1, pArg2); + default: return SQLITE_MISUSE; + } +} + +/* +** This function is NOT part of the sqlite3 public API. It is strictly +** for use by the sqlite project's own JS/WASM bindings. +** +** Wrapper for the variants of sqlite3_db_config() which take +** (void*,int,int) variadic args. +*/ +SQLITE_WASM_KEEP +int sqlite3_wasm_db_config_pii(sqlite3 *pDb, int op, void * pArg1, int arg2, int arg3){ + switch(op){ + case SQLITE_DBCONFIG_LOOKASIDE: + return sqlite3_db_config(pDb, op, pArg1, arg2, arg3); + default: return SQLITE_MISUSE; + } +} + +/* +** This function is NOT part of the sqlite3 public API. It is strictly +** for use by the sqlite project's own JS/WASM bindings. +** +** Wrapper for the variants of sqlite3_db_config() which take +** (const char *) variadic args. +*/ +SQLITE_WASM_KEEP +int sqlite3_wasm_db_config_s(sqlite3 *pDb, int op, const char *zArg){ + switch(op){ + case SQLITE_DBCONFIG_MAINDBNAME: + return sqlite3_db_config(pDb, op, zArg); + default: return SQLITE_MISUSE; + } +} + + +/* +** This function is NOT part of the sqlite3 public API. It is strictly +** for use by the sqlite project's own JS/WASM bindings. +** +** Binding for combinations of sqlite3_config() arguments which take +** a single integer argument. +*/ +SQLITE_WASM_KEEP +int sqlite3_wasm_config_i(int op, int arg){ + return sqlite3_config(op, arg); +} + +/* +** This function is NOT part of the sqlite3 public API. It is strictly +** for use by the sqlite project's own JS/WASM bindings. +** +** Binding for combinations of sqlite3_config() arguments which take +** two int arguments. +*/ +SQLITE_WASM_KEEP +int sqlite3_wasm_config_ii(int op, int arg1, int arg2){ + return sqlite3_config(op, arg1, arg2); +} + +/* +** This function is NOT part of the sqlite3 public API. It is strictly +** for use by the sqlite project's own JS/WASM bindings. +** +** Binding for combinations of sqlite3_config() arguments which take +** a single i64 argument. +*/ +SQLITE_WASM_KEEP +int sqlite3_wasm_config_j(int op, sqlite3_int64 arg){ + return sqlite3_config(op, arg); +} + #if defined(__EMSCRIPTEN__) && defined(SQLITE_ENABLE_WASMFS) #include @@ -1166,10 +1651,10 @@ void sqlite3_wasm_test_stack_overflow(int recurse){ if(recurse) sqlite3_wasm_test_stack_overflow(recurse); } -/* For testing the 'string-free' whwasmutil.xWrap() conversion. */ +/* For testing the 'string:dealloc' whwasmutil.xWrap() conversion. */ SQLITE_WASM_KEEP char * sqlite3_wasm_test_str_hello(int fail){ - char * s = fail ? 0 : (char *)malloc(6); + char * s = fail ? 0 : (char *)sqlite3_malloc(6); if(s){ memcpy(s, "hello", 5); s[5] = 0; diff --git a/ext/wasm/batch-runner.js b/ext/wasm/batch-runner.js index 11c43217ff..ff287a66e7 100644 --- a/ext/wasm/batch-runner.js +++ b/ext/wasm/batch-runner.js @@ -227,20 +227,20 @@ const pSqlEnd = pSqlBegin + sqlByteLen; t = performance.now(); wasm.heap8().set(sql, pSql); - wasm.setMemValue(pSql + sqlByteLen, 0); + wasm.poke(pSql + sqlByteLen, 0); metrics.strcpy = performance.now() - t; let breaker = 0; - while(pSql && wasm.getMemValue(pSql,'i8')){ - wasm.setPtrValue(ppStmt, 0); - wasm.setPtrValue(pzTail, 0); + while(pSql && wasm.peek(pSql,'i8')){ + wasm.pokePtr(ppStmt, 0); + wasm.pokePtr(pzTail, 0); t = performance.now(); let rc = capi.sqlite3_prepare_v3( db.handle, pSql, sqlByteLen, 0, ppStmt, pzTail ); metrics.prepTotal += performance.now() - t; checkSqliteRc(db.handle, rc); - pStmt = wasm.getPtrValue(ppStmt); - pSql = wasm.getPtrValue(pzTail); + pStmt = wasm.peekPtr(ppStmt); + pSql = wasm.peekPtr(pzTail); sqlByteLen = pSqlEnd - pSql; if(!pStmt) continue/*empty statement*/; ++metrics.stmtCount; @@ -495,7 +495,7 @@ const oFlags = capi.SQLITE_OPEN_CREATE | capi.SQLITE_OPEN_READWRITE; const ppDb = wasm.scopedAllocPtr(); const rc = capi.sqlite3_open_v2(d.filename, ppDb, oFlags, null); - pDb = wasm.getPtrValue(ppDb) + pDb = wasm.peekPtr(ppDb) if(rc) toss("sqlite3_open_v2() failed with code",rc); capi.sqlite3_exec(pDb, "PRAGMA cache_size="+cacheSize, 0, 0, 0); this.logHtml(dbId,"cache_size =",cacheSize); diff --git a/ext/wasm/c-pp.c b/ext/wasm/c-pp.c new file mode 100644 index 0000000000..881c009acb --- /dev/null +++ b/ext/wasm/c-pp.c @@ -0,0 +1,1525 @@ +/* +** 2022-11-12: +** +** In place of a legal notice, here is a blessing: +** +** * May you do good and not evil. +** * May you find forgiveness for yourself and forgive others. +** * May you share freely, never taking more than you give. +** +************************************************************************ +** +** The C-minus Preprocessor: a truly minimal C-like preprocessor. +** Why? Because C preprocessors _can_ process non-C code but generally make +** quite a mess of it. The purpose of this application is an extremely +** minimal preprocessor with only the most basic functionality of a C +** preprocessor, namely: +** +** - Limited `#if`, where its one argument is a macro name which +** resolves to true if it's defined, false if it's not. Likewise, +** `#ifnot` is the inverse. Includes `#else` and `#elif` and +** `#elifnot`. Such chains are terminated with `#endif`. +** +** - `#define` accepts one or more arguments, the names of +** macros. Each one is implicitly true. +** +** - `#undef` undefine one or more macros. +** +** - `#error` treats the rest of the line as a fatal error message. +** +** - `#include` treats its argument as a filename token (NOT quoted, +** though support for quoting may be added later). Some effort is +** made to prevent recursive inclusion, but that support is both +** somewhat fragile and possibly completely unnecessary. +** +** - `#pragma` is in place for adding "meta-commands", but it does not +** yet have any concrete list of documented commands. +** +* - `#stderr` outputs its file name, line number, and the remaininder +** of that line to stderr. +** +** - `#//` acts as a single-line comment, noting that there must be as +** space after the `//` part because `//` is (despite appearances) +** parsed like a keyword. +** +** Note that "#" above is symbolic. The keyword delimiter is +** configurable and defaults to "##". Define CMPP_DEFAULT_DELIM to a +** string when compiling to define the default at build-time. +** +** This preprocessor does no expansion of content except within the +** bounds of its `#keywords`. +** +** Design note: this code makes use of sqlite3. Though not _strictly_ +** needed in order to implement it, this tool was specifically created +** for potential use with the sqlite3 project's own JavaScript code, +** so there's no reason not to make use of it to do some of the heavy +** lifting. It does not require any cutting-edge sqlite3 features and +** should be usable with any version which supports `WITHOUT ROWID`. +** +** Author(s): +** +** - Stephan Beal +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include "sqlite3.h" + +#if defined(_WIN32) || defined(WIN32) +# include +# include +# ifndef access +# define access(f,m) _access((f),(m)) +# endif +#else +# include +#endif + +#ifndef CMPP_DEFAULT_DELIM +#define CMPP_DEFAULT_DELIM "##" +#endif + +#if 1 +# define CMPP_NORETURN __attribute__((noreturn)) +#else +# define CMPP_NORETURN +#endif + +/* Fatally exits the app with the given printf-style message. */ +static CMPP_NORETURN void fatalv(char const *zFmt, va_list); +static CMPP_NORETURN void fatal(char const *zFmt, ...); + +/** Proxy for free(), for symmetry with cmpp_realloc(). */ +static void cmpp_free(void *p); +/** A realloc() proxy which dies fatally on allocation error. */ +static void * cmpp_realloc(void * p, unsigned n); +#if 0 +/** A malloc() proxy which dies fatally on allocation error. */ +static void * cmpp_malloc(unsigned n); +#endif + +/* +** If p is stdin or stderr then this is a no-op, else it is a +** proxy for fclose(). This is a no-op if p is NULL. +*/ +static void FILE_close(FILE *p); +/* +** Works like fopen() but accepts the special name "-" to mean either +** stdin (if zMode indicates a real-only mode) or stdout. Fails +** fatally on error. +*/ +static FILE * FILE_open(char const *zName, const char * zMode); +/* +** Reads the entire contents of the given file, allocating it in a +** buffer which gets assigned to `*pOut`. `*nOut` gets assigned the +** length of the output buffer. Fails fatally on error. +*/ +static void FILE_slurp(FILE *pFile, unsigned char **pOut, + unsigned * nOut); + +/* +** Intended to be passed an sqlite3 result code. If it's non-0 +** then it emits a fatal error message which contains both the +** given string and the sqlite3_errmsg() from the application's +** database instance. +*/ +static void db_affirm_rc(int rc, const char * zMsg); + +/* +** Proxy for sqlite3_str_finish() which fails fatally if that +** routine returns NULL. +*/ +static char * db_str_finish(sqlite3_str *s, int * n); +/* +** Proxy for sqlite3_str_new() which fails fatally if that +** routine returns NULL. +*/ +static sqlite3_str * db_str_new(void); + +/* Proxy for sqlite3_finalize(). */ +static void db_finalize(sqlite3_stmt *pStmt); +/* +** Proxy for sqlite3_step() which fails fatally if the result +** is anything other than SQLITE_ROW or SQLITE_DONE. +*/ +static int db_step(sqlite3_stmt *pStmt); +/* +** Proxy for sqlite3_bind_int() which fails fatally on error. +*/ +static void db_bind_int(sqlite3_stmt *pStmt, int col, int val); +#if 0 +/* +** Proxy for sqlite3_bind_null() which fails fatally on error. +*/ +static void db_bind_null(sqlite3_stmt *pStmt, int col); +#endif +/* +** Proxy for sqlite3_bind_text() which fails fatally on error. +*/ +static void db_bind_text(sqlite3_stmt *pStmt, int col, const char * zStr); +/* +** Proxy for sqlite3_bind_text() which fails fatally on error. +*/ +static void db_bind_textn(sqlite3_stmt *pStmt, int col, const char * zStr, int len); +#if 0 +/* +** Proxy for sqlite3_bind_text() which fails fatally on error. It uses +** sqlite3_str_vappendf() so supports all of its formatting options. +*/ +static void db_bind_textv(sqlite3_stmt *pStmt, int col, const char * zFmt, ...); +#endif +/* +** Proxy for sqlite3_free(), to be passed any memory which is allocated +** by sqlite3_malloc(). +*/ +static void db_free(void *m); +/* +** Adds the given `#define` macro name to the list of macros, ignoring +** any duplicates. Fails fatally on error. +*/ +static void db_define_add(const char * zKey); +/* +** Returns true if the given key is already in the `#define` list, +** else false. Fails fatally on db error. +*/ +static int db_define_has(const char * zName); +/* +** Removes the given `#define` macro name from the list of +** macros. Fails fatally on error. +*/ +static void db_define_rm(const char * zKey); +/* +** Adds the given filename to the list of being-`#include`d files, +** using the given source file name and line number of error reporting +** purposes. If recursion is later detected. +*/ +static void db_including_add(const char * zKey, const char * zSrc, int srcLine); +/* +** Adds the given dir to the list of includes. They are checked in the +** order they are added. +*/ +static void db_include_dir_add(const char * zKey); +/* +** Returns a resolved path of PREFIX+'/'+zKey, where PREFIX is one of +** the `#include` dirs (db_include_dir_add()). If no file match is +** found, NULL is returned. Memory must eventually be passed to +** db_free() to free it. +*/ +static char * db_include_search(const char * zKey); +/* +** Removes the given key from the `#include` list. +*/ +static void db_include_rm(const char * zKey); +/* +** A proxy for sqlite3_prepare() which fails fatally on error. +*/ +static void db_prepare(sqlite3_stmt **pStmt, const char * zSql, ...); + +/* +** Opens the given file and processes its contents as c-pp, sending +** all output to the global c-pp output channel. Fails fatally on +** error. +*/ +static void cmpp_process_file(const char * zName); + +/* +** Returns the number newline characters between the given starting +** point and inclusive ending point. Results are undefined if zFrom is +** greater than zTo. +*/ +static unsigned count_lines(unsigned char const * zFrom, + unsigned char const *zTo); + +/* +** Wrapper around a FILE handle. +*/ +struct FileWrapper { + /* File's name. */ + char const *zName; + /* FILE handle. */ + FILE * pFile; + /* Where FileWrapper_slurp() stores the file's contents. */ + unsigned char * zContent; + /* Size of this->zContent, as set by FileWrapper_slurp(). */ + unsigned nContent; +}; +typedef struct FileWrapper FileWrapper; +#define FileWrapper_empty_m {0,0,0,0} +static const FileWrapper FileWrapper_empty = FileWrapper_empty_m; + +/* Proxy for FILE_close(). */ +static void FileWrapper_close(FileWrapper * p); +/* Proxy for FILE_open(). */ +static void FileWrapper_open(FileWrapper * p, const char * zName, const char *zMode); +/* Proxy for FILE_slurp(). */ +static void FileWrapper_slurp(FileWrapper * p); + +/* +** Outputs a printf()-formatted message to stderr. +*/ +static void g_stderr(char const *zFmt, ...); +/* +** Outputs a printf()-formatted message to stderr. +*/ +static void g_stderrv(char const *zFmt, va_list); +#define g_debug(lvl,pfexpr) \ + if(lvl<=g.doDebug) g_stderr("%s @ %s:%d: ",g.zArgv0,__FILE__,__LINE__); \ + if(lvl<=g.doDebug) g_stderr pfexpr + +void fatalv(char const *zFmt, va_list va){ + if(zFmt && *zFmt){ + vfprintf(stderr, zFmt, va); + } + fputc('\n', stderr); + exit(1); +} + +void fatal(char const *zFmt, ...){ + va_list va; + va_start(va, zFmt); + fatalv(zFmt, va); + va_end(va); +} + +void cmpp_free(void *p){ + free(p); +} + +void * cmpp_realloc(void * p, unsigned n){ + void * const rc = realloc(p, n); + if(!rc) fatal("realloc(P,%u) failed", n); + return rc; +} + +#if 0 +void * cmpp_malloc(unsigned n){ + void * const rc = malloc(n); + if(!rc) fatal("malloc(%u) failed", n); + return rc; +} +#endif + +FILE * FILE_open(char const *zName, const char * zMode){ + FILE * p; + if('-'==zName[0] && 0==zName[1]){ + p = strstr(zMode,"w") ? stdout : stdin; + }else{ + p = fopen(zName, zMode); + if(!p) fatal("Cannot open file [%s] with mode [%s]", zName, zMode); + } + return p; +} + +void FILE_close(FILE *p){ + if(p && p!=stdout && p!=stderr){ + fclose(p); + } +} + +void FILE_slurp(FILE *pFile, unsigned char **pOut, + unsigned * nOut){ + unsigned char zBuf[1024 * 8]; + unsigned char * pDest = 0; + unsigned nAlloc = 0; + unsigned nOff = 0; + /* Note that this needs to be able to work on non-seekable streams, + ** thus we read in chunks instead of doing a single alloc and + ** filling it in one go. */ + while( !feof(pFile) ){ + size_t const n = fread(zBuf, 1, sizeof(zBuf), pFile); + if(n>0){ + if(nAlloc < nOff + n + 1){ + nAlloc = nOff + n + 1; + pDest = cmpp_realloc(pDest, nAlloc); + } + memcpy(pDest + nOff, zBuf, n); + nOff += n; + } + } + if(pDest) pDest[nOff] = 0; + *pOut = pDest; + *nOut = nOff; +} + +void FileWrapper_close(FileWrapper * p){ + if(p->pFile) FILE_close(p->pFile); + if(p->zContent) cmpp_free(p->zContent); + *p = FileWrapper_empty; +} + +void FileWrapper_open(FileWrapper * p, const char * zName, + const char * zMode){ + FileWrapper_close(p); + p->pFile = FILE_open(zName, zMode); + p->zName = zName; +} + +void FileWrapper_slurp(FileWrapper * p){ + assert(!p->zContent); + assert(p->pFile); + FILE_slurp(p->pFile, &p->zContent, &p->nContent); +} + +unsigned count_lines(unsigned char const * zFrom, unsigned char const *zTo){ + unsigned ln = 0; + unsigned char const *zPos = zFrom; + assert(zFrom && zTo); + assert(zFrom <= zTo); + for(; zPos < zTo; ++zPos){ + switch(*zPos){ + case (unsigned)'\n': ++ln; break; + default: break; + } + } + return ln; +} + +enum CmppParseState { +TS_Start = 1, +TS_If, +TS_IfPassed, +TS_Else, +TS_Error +}; +typedef enum CmppParseState CmppParseState; +enum CmppTokenType { +TT_Invalid = 0, +TT_Comment, +TT_Define, +TT_Elif, +TT_ElifNot, +TT_Else, +TT_EndIf, +TT_Error, +TT_If, +TT_IfNot, +TT_Include, +TT_Line, +TT_Pragma, +TT_Stderr, +TT_Undef +}; +typedef enum CmppTokenType CmppTokenType; + +struct CmppToken { + CmppTokenType ttype; + /* Line number of this token in the source file. */ + unsigned lineNo; + /* Start of the token. */ + unsigned char const * zBegin; + /* One-past-the-end byte of the token. */ + unsigned char const * zEnd; +}; +typedef struct CmppToken CmppToken; +#define CmppToken_empty_m {TT_Invalid,0,0,0} +static const CmppToken CmppToken_empty = CmppToken_empty_m; + +/* +** CmppLevel represents one "level" of tokenization, starting at the +** top of the main input, incrementing once for each level of `#if`, +** and decrementing for each `#endif`. +*/ +typedef struct CmppLevel CmppLevel; +struct CmppLevel { + unsigned short flags; + /* + ** Used for controlling which parts of an if/elif/...endif chain + ** should get output. + */ + unsigned short skipLevel; + /* The token which started this level (an 'if' or 'ifnot'). */ + CmppToken token; + CmppParseState pstate; +}; +#define CmppLevel_empty_m {0U,0U,CmppToken_empty_m,TS_Start} +static const CmppLevel CmppLevel_empty = CmppLevel_empty_m; +enum CmppLevel_Flags { +/* Max depth of nested `#if` constructs in a single tokenizer. */ +CmppLevel_Max = 10, +/* Max number of keyword arguments. */ +CmppArgs_Max = 10, +/* Flag indicating that output for a CmpLevel should be elided. */ +CmppLevel_F_ELIDE = 0x01, +/* +** Mask of CmppLevel::flags which are inherited when CmppLevel_push() +** is used. +*/ +CmppLevel_F_INHERIT_MASK = 0x01 +}; + +typedef struct CmppTokenizer CmppTokenizer; +typedef struct CmppKeyword CmppKeyword; +typedef void (*cmpp_keyword_f)(CmppKeyword const * pKw, CmppTokenizer * t); +struct CmppKeyword { + const char *zName; + unsigned nName; + int bTokenize; + CmppTokenType ttype; + cmpp_keyword_f xCall; +}; + +static CmppKeyword const * CmppKeyword_search(const char *zName); +static void cmpp_process_keyword(CmppTokenizer * const t); + +/* +** Tokenizer for c-pp input files. +*/ +struct CmppTokenizer { + const char * zName; /* Input (file) name for error reporting */ + unsigned const char * zBegin; /* start of input */ + unsigned const char * zEnd; /* one-after-the-end of input */ + unsigned const char * zAnchor; /* start of input or end point of + previous token */ + unsigned const char * zPos; /* current position */ + unsigned int lineNo; /* line # of current pos */ + CmppParseState pstate; + CmppToken token; /* current token result */ + struct { + unsigned ndx; + CmppLevel stack[CmppLevel_Max]; + } level; + /* Args for use in cmpp_keyword_f() impls. */ + struct { + CmppKeyword const * pKw; + int argc; + const unsigned char * argv[CmppArgs_Max]; + unsigned char lineBuf[1024]; + } args; +}; +#define CT_level(t) (t)->level.stack[(t)->level.ndx] +#define CT_pstate(t) CT_level(t).pstate +#define CT_skipLevel(t) CT_level(t).skipLevel +#define CLvl_skip(lvl) ((lvl)->skipLevel || ((lvl)->flags & CmppLevel_F_ELIDE)) +#define CT_skip(t) CLvl_skip(&CT_level(t)) +#define CmppTokenizer_empty_m { \ + 0,0,0,0,0,1U/*lineNo*/, \ + TS_Start, \ + CmppToken_empty_m, \ + {/*level*/0U,{CmppLevel_empty_m}}, \ + {/*args*/0,0,{0},{0}} \ + } +static const CmppTokenizer CmppTokenizer_empty = CmppTokenizer_empty_m; + +static void cmpp_t_out(CmppTokenizer * t, void const *z, unsigned int n); +/*static void cmpp_t_outf(CmppTokenizer * t, char const *zFmt, ...);*/ + +/* +** Pushes a new level into the given tokenizer. Fails fatally if +** it's too deep. +*/ +static void CmppLevel_push(CmppTokenizer * const t); +/* +** Pops a level from the tokenizer. Fails fatally if the top +** level is popped. +*/ +static void CmppLevel_pop(CmppTokenizer * const t); +/* +** Returns the current level object. +*/ +static CmppLevel * CmppLevel_get(CmppTokenizer * const t); + +/* +** Global app state singleton. */ +static struct Global { + /* main()'s argv[0]. */ + const char * zArgv0; + /* + ** Bytes of the keyword delimiter/prefix. Owned + ** elsewhere. + */ + const char * zDelim; + /* Byte length of this->zDelim. */ + unsigned short nDelim; + /* If true, enables certain debugging output. */ + int doDebug; + /* App's db instance. */ + sqlite3 * db; + /* Output channel. */ + FileWrapper out; + struct { + sqlite3_stmt * defIns; + sqlite3_stmt * defDel; + sqlite3_stmt * defHas; + sqlite3_stmt * inclIns; + sqlite3_stmt * inclDel; + sqlite3_stmt * inclHas; + sqlite3_stmt * inclPathAdd; + sqlite3_stmt * inclSearch; + } stmt; +} g = { +"?", +CMPP_DEFAULT_DELIM/*zDelim*/, +(unsigned short) sizeof(CMPP_DEFAULT_DELIM)-1/*nDelim*/, +0/*doDebug*/, +0/*db*/, +FileWrapper_empty_m/*out*/, +{/*stmt*/ + 0/*defIns*/, 0/*defDel*/, 0/*defHas*/, + 0/*inclIns*/, 0/*inclDel*/, 0/*inclHas*/, + 0/*inclPathAdd*/ +} +}; + + +#if 0 +/* +** Outputs a printf()-formatted message to c-pp's global output +** channel. +*/ +static void g_outf(char const *zFmt, ...); +void g_outf(char const *zFmt, ...){ + va_list va; + va_start(va, zFmt); + vfprintf(g.out.pFile, zFmt, va); + va_end(va); +} +#endif + +#if 0 +/* Outputs n bytes from z to c-pp's global output channel. */ +static void g_out(void const *z, unsigned int n); +void g_out(void const *z, unsigned int n){ + if(1!=fwrite(z, n, 1, g.out.pFile)){ + int const err = errno; + fatal("fwrite() output failed with errno #%d", err); + } +} +#endif + +void g_stderrv(char const *zFmt, va_list va){ + vfprintf(stderr, zFmt, va); +} + +void g_stderr(char const *zFmt, ...){ + va_list va; + va_start(va, zFmt); + g_stderrv(zFmt, va); + va_end(va); +} + +#if 0 +void cmpp_t_outf(CmppTokenizer * t, char const *zFmt, ...){ + if(!CT_skip(t)){ + va_list va; + va_start(va, zFmt); + vfprintf(g.out.pFile, zFmt, va); + va_end(va); + } +} +#endif + +void cmpp_t_out(CmppTokenizer * t, void const *z, unsigned int n){ + if(!CT_skip(t)){ + if(1!=fwrite(z, n, 1, g.out.pFile)){ + int const err = errno; + fatal("fwrite() output failed with errno #%d", err); + } + } +} + +void CmppLevel_push(CmppTokenizer * const t){ + CmppLevel * pPrev; + CmppLevel * p; + if(t->level.ndx+1 == (unsigned)CmppLevel_Max){ + fatal("%sif nesting level is too deep. Max=%d\n", + g.zDelim, CmppLevel_Max); + } + pPrev = &CT_level(t); + p = &t->level.stack[++t->level.ndx]; + *p = CmppLevel_empty; + p->token = t->token; + p->flags = (CmppLevel_F_INHERIT_MASK & pPrev->flags); + if(CLvl_skip(pPrev)) p->flags |= CmppLevel_F_ELIDE; +} + +void CmppLevel_pop(CmppTokenizer * const t){ + if(!t->level.ndx){ + fatal("Internal error: CmppLevel_pop() at the top of the stack"); + } + t->level.stack[t->level.ndx--] = CmppLevel_empty; +} + +CmppLevel * CmppLevel_get(CmppTokenizer * const t){ + return &t->level.stack[t->level.ndx]; +} + + +void db_affirm_rc(int rc, const char * zMsg){ + if(rc){ + fatal("Db error #%d %s: %s", rc, zMsg, sqlite3_errmsg(g.db)); + } +} + +void db_finalize(sqlite3_stmt *pStmt){ + sqlite3_finalize(pStmt); +} + +int db_step(sqlite3_stmt *pStmt){ + int const rc = sqlite3_step(pStmt); + if(SQLITE_ROW!=rc && SQLITE_DONE!=rc){ + db_affirm_rc(rc, "from db_step()"); + } + return rc; +} + +static sqlite3_str * db_str_new(void){ + sqlite3_str * rc = sqlite3_str_new(g.db); + if(!rc) fatal("Alloc failed for sqlite3_str_new()"); + return rc; +} + +static char * db_str_finish(sqlite3_str *s, int * n){ + int const rc = sqlite3_str_errcode(s); + if(rc) fatal("Error #%d from sqlite3_str_errcode()", rc); + if(n) *n = sqlite3_str_length(s); + char * z = sqlite3_str_finish(s); + if(!z) fatal("Alloc failed for sqlite3_str_new()"); + return z; +} + +void db_prepare(sqlite3_stmt **pStmt, const char * zSql, ...){ + int rc; + sqlite3_str * str = db_str_new(); + char * z = 0; + int n = 0; + va_list va; + if(!str) fatal("sqlite3_str_new() failed"); + va_start(va, zSql); + sqlite3_str_vappendf(str, zSql, va); + va_end(va); + rc = sqlite3_str_errcode(str); + if(rc) fatal("sqlite3_str_errcode() = %d", rc); + z = db_str_finish(str, &n); + rc = sqlite3_prepare_v2(g.db, z, n, pStmt, 0); + if(rc) fatal("Error #%d (%s) preparing: %s", + rc, sqlite3_errmsg(g.db), z); + sqlite3_free(z); +} + +void db_bind_int(sqlite3_stmt *pStmt, int col, int val){ + int const rc = sqlite3_bind_int(pStmt, col, val); + db_affirm_rc(rc,"from db_bind_int()"); +} + +#if 0 +void db_bind_null(sqlite3_stmt *pStmt, int col){ + int const rc = sqlite3_bind_null(pStmt, col); + db_affirm_rc(rc,"from db_bind_null()"); +} +#endif + +void db_bind_textn(sqlite3_stmt *pStmt, int col, + const char * zStr, int n){ + int const rc = zStr + ? sqlite3_bind_text(pStmt, col, zStr, n, SQLITE_TRANSIENT) + : sqlite3_bind_null(pStmt, col); + db_affirm_rc(rc,"from db_bind_textn()"); +} + +void db_bind_text(sqlite3_stmt *pStmt, int col, + const char * zStr){ + db_bind_textn(pStmt, col, zStr, -1); +} + +#if 0 +void db_bind_textv(sqlite3_stmt *pStmt, int col, + const char * zFmt, ...){ + int rc; + sqlite3_str * str = db_str_new(); + int n = 0; + char * z; + va_list va; + va_start(va,zFmt); + sqlite3_str_vappendf(str, zFmt, va); + va_end(va); + z = db_str_finish(str, &n); + rc = sqlite3_bind_text(pStmt, col, z, n, sqlite3_free); + db_affirm_rc(rc,"from db_bind_textv()"); +} +#endif + +void db_free(void *m){ + sqlite3_free(m); +} + +void db_define_add(const char * zKey){ + int rc; + if(!g.stmt.defIns){ + db_prepare(&g.stmt.defIns, + "INSERT OR REPLACE INTO def(k) VALUES(?)"); + } + db_bind_text(g.stmt.defIns, 1, zKey); + rc = db_step(g.stmt.defIns); + if(SQLITE_DONE != rc){ + db_affirm_rc(rc, "Stepping INSERT on def"); + } + g_debug(2,("define: %s\n",zKey)); + sqlite3_reset(g.stmt.defIns); +} + +int db_define_has(const char * zName){ + int rc; + if(!g.stmt.defHas){ + db_prepare(&g.stmt.defHas, "SELECT 1 FROM def WHERE k=?"); + } + db_bind_text(g.stmt.defHas, 1, zName); + rc = db_step(g.stmt.defHas); + if(SQLITE_ROW == rc){ + rc = 1; + }else{ + assert(SQLITE_DONE==rc); + rc = 0; + } + g_debug(1,("define has [%s] = %d\n",zName, rc)); + sqlite3_clear_bindings(g.stmt.defHas); + sqlite3_reset(g.stmt.defHas); + return rc; +} + + +void db_define_rm(const char * zKey){ + int rc; + int n = 0; + const char *zPos = zKey; + if(!g.stmt.defDel){ + db_prepare(&g.stmt.defDel, "DELETE FROM def WHERE k=?"); + } + for( ; *zPos && '='!=*zPos; ++n, ++zPos) {} + db_bind_text(g.stmt.defDel, 1, zKey); + rc = db_step(g.stmt.defDel); + if(SQLITE_DONE != rc){ + db_affirm_rc(rc, "Stepping DELETE on def"); + } + g_debug(2,("undefine: %.*s\n",n, zKey)); + sqlite3_clear_bindings(g.stmt.defDel); + sqlite3_reset(g.stmt.defDel); +} + +void db_including_add(const char * zKey, const char * zSrc, int srcLine){ + int rc; + if(!g.stmt.inclIns){ + db_prepare(&g.stmt.inclIns, + "INSERT OR FAIL INTO incl(file,srcFile,srcLine) VALUES(?,?,?)"); + } + db_bind_text(g.stmt.inclIns, 1, zKey); + db_bind_text(g.stmt.inclIns, 2, zSrc); + db_bind_int(g.stmt.inclIns, 3, srcLine); + rc = db_step(g.stmt.inclIns); + if(SQLITE_DONE != rc){ + db_affirm_rc(rc, "Stepping INSERT on incl"); + } + g_debug(2,("inclpath add [%s] from [%s]:%d\n", zKey, zSrc, srcLine)); + sqlite3_clear_bindings(g.stmt.inclIns); + sqlite3_reset(g.stmt.inclIns); +} + +void db_include_rm(const char * zKey){ + int rc; + if(!g.stmt.inclDel){ + db_prepare(&g.stmt.inclDel, "DELETE FROM incl WHERE file=?"); + } + db_bind_text(g.stmt.inclDel, 1, zKey); + rc = db_step(g.stmt.inclDel); + if(SQLITE_DONE != rc){ + db_affirm_rc(rc, "Stepping DELETE on incl"); + } + g_debug(2,("inclpath rm [%s]\n", zKey)); + sqlite3_clear_bindings(g.stmt.inclDel); + sqlite3_reset(g.stmt.inclDel); +} + +char * db_include_search(const char * zKey){ + char * zName = 0; + if(!g.stmt.inclSearch){ + db_prepare(&g.stmt.inclSearch, + "SELECT ?1 fn WHERE fileExists(fn) " + "UNION ALL SELECT * FROM (" + "SELECT replace(dir||'/'||?1, '//','/') AS fn " + "FROM inclpath WHERE fileExists(fn) ORDER BY seq" + ")"); + } + db_bind_text(g.stmt.inclSearch, 1, zKey); + if(SQLITE_ROW==db_step(g.stmt.inclSearch)){ + const unsigned char * z = sqlite3_column_text(g.stmt.inclSearch, 0); + zName = z ? sqlite3_mprintf("%s", z) : 0; + if(!zName) fatal("Alloc failed"); + } + sqlite3_clear_bindings(g.stmt.inclSearch); + sqlite3_reset(g.stmt.inclSearch); + return zName; +} + +static int db_including_has(const char * zName){ + int rc; + if(!g.stmt.inclHas){ + db_prepare(&g.stmt.inclHas, "SELECT 1 FROM incl WHERE file=?"); + } + db_bind_text(g.stmt.inclHas, 1, zName); + rc = db_step(g.stmt.inclHas); + if(SQLITE_ROW == rc){ + rc = 1; + }else{ + assert(SQLITE_DONE==rc); + rc = 0; + } + g_debug(2,("inclpath has [%s] = %d\n",zName, rc)); + sqlite3_clear_bindings(g.stmt.inclHas); + sqlite3_reset(g.stmt.inclHas); + return rc; +} + +#if 0 +/* +** Fails fatally if the `#include` list contains the given key. +*/ +static void db_including_check(const char * zKey); +void db_including_check(const char * zName){ + if(db_including_has(zName)){ + fatal("Recursive include detected: %s\n", zName); + } +} +#endif + +void db_include_dir_add(const char * zDir){ + static int seq = 0; + int rc; + if(!g.stmt.inclPathAdd){ + db_prepare(&g.stmt.inclPathAdd, + "INSERT OR FAIL INTO inclpath(seq,dir) VALUES(?,?)"); + } + db_bind_int(g.stmt.inclPathAdd, 1, ++seq); + db_bind_text(g.stmt.inclPathAdd, 2, zDir); + rc = db_step(g.stmt.inclPathAdd); + if(SQLITE_DONE != rc){ + db_affirm_rc(rc, "Stepping INSERT on inclpath"); + } + g_debug(2,("inclpath add #%d: %s\n",seq, zDir)); + sqlite3_clear_bindings(g.stmt.inclPathAdd); + sqlite3_reset(g.stmt.inclPathAdd); +} + +static void cmpp_atexit(void){ +#define FINI(M) if(g.stmt.M) sqlite3_finalize(g.stmt.M) + FINI(defIns); FINI(defDel); FINI(defHas); + FINI(inclIns); FINI(inclDel); FINI(inclHas); + FINI(inclPathAdd); FINI(inclSearch); +#undef FINI + FileWrapper_close(&g.out); + if(g.db) sqlite3_close(g.db); +} + +/* +** sqlite3 UDF which returns true if its argument refers to an +** accessible file, else false. +*/ +static void udf_file_exists( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + const char *zName; + (void)(argc); /* Unused parameter */ + zName = (const char*)sqlite3_value_text(argv[0]); + if( zName==0 ) return; + sqlite3_result_int(context, 0==access(zName, 0)); +} + +/* Initialize g.db, failing fatally on error. */ +static void cmpp_initdb(void){ + int rc; + char * zErr = 0; + const char * zSchema = + "CREATE TABLE def(" + "k TEXT PRIMARY KEY NOT NULL" + /*"v INTEGER DEFAULT 1"*/ + ") WITHOUT ROWID;" + /* ^^^ defines */ + "CREATE TABLE incl(" + "file TEXT PRIMARY KEY NOT NULL," + "srcFile TEXT DEFAULT NULL," + "srcLine INTEGER DEFAULT 0" + ") WITHOUT ROWID;" + /* ^^^ files currently being included */ + "CREATE TABLE inclpath(" + "seq INTEGER UNIQUE, " + "dir TEXT PRIMARY KEY NOT NULL ON CONFLICT IGNORE" + ")" + /* ^^^ include path */ + ; + assert(0==g.db); + if(g.db) return; + rc = sqlite3_open_v2(":memory:", &g.db, SQLITE_OPEN_READWRITE, 0); + if(rc) fatal("Error opening :memory: db."); + rc = sqlite3_exec(g.db, zSchema, 0, 0, &zErr); + if(rc) fatal("Error initializing database: %s", zErr); + rc = sqlite3_create_function(g.db, "fileExists", 1, + SQLITE_UTF8|SQLITE_DIRECTONLY, 0, + udf_file_exists, 0, 0); + db_affirm_rc(rc, "UDF registration failed."); +} + +/* +** For position zPos, which must be in the half-open range +** [zBegin,zEnd), returns g.nDelim if it is at the start of a line and +** starts with g.zDelim, else returns 0. +*/ +static unsigned short cmpp_is_delim(unsigned char const *zBegin, + unsigned char const *zEnd, + unsigned char const *zPos){ + assert(zEnd>zBegin); + assert(zPos=zBegin); + if(zPos>zBegin && + ('\n'!=*(zPos - 1) + || ((unsigned)(zEnd - zPos) <= g.nDelim))){ + return 0; + }else if(0==memcmp(zPos, g.zDelim, g.nDelim)){ + return g.nDelim; + }else{ + return 0; + } +} + +/* +** Scans t to the next keyword line, emitting all input before that +** which is _not_ a keyword line unless it's elided due to being +** inside a block which elides its content. Returns 0 if no keyword +** line was found, in which case the end of the input has been +** reached, else returns a truthy value and sets up t's state for use +** with cmpp_process_keyword(), which should then be called. +*/ +static int cmpp_next_keyword_line(CmppTokenizer * const t){ + unsigned char const * zStart; + unsigned char const * z; + CmppToken * const tok = &t->token; + unsigned short isDelim = 0; + + assert(t->zBegin); + assert(t->zEnd > t->zBegin); + if(!t->zPos) t->zPos = t->zBegin; + t->zAnchor = t->zPos; + zStart = z = t->zPos; + *tok = CmppToken_empty; + while(zzEnd + && 0==(isDelim = cmpp_is_delim(t->zBegin, t->zEnd, z))){ + ++z; + } + if(z>zStart){ + /* We passed up content */ + cmpp_t_out(t, zStart, (unsigned)(z - zStart)); + } + assert(isDelim==0 || isDelim==g.nDelim); + tok->lineNo = t->lineNo += count_lines(zStart, z); + if(isDelim){ + /* Handle backslash-escaped newlines */ + int isEsc = 0, atEol = 0; + tok->zBegin = z+isDelim; + for( ++z ; zzEnd && 0==atEol; ++z ){ + switch((int)*z){ + case (int)'\\': + isEsc = 0==isEsc; break; + case (int)'\n': + atEol = 0==isEsc; + isEsc = 0; + ++t->lineNo; + break; + default: + break; + } + } + tok->zEnd = atEol ? z-1 : z; + /* Strip leading spaces */ + while(tok->zBegin < tok->zEnd && isspace((char)(*tok->zBegin))){ + ++tok->zBegin; + } + tok->ttype = TT_Line; + g_debug(2,("Keyword @ line %u: [[[%.*s]]]\n", + tok->lineNo, + (int)(tok->zEnd-tok->zBegin), tok->zBegin)); + } + t->zPos = z; + if(isDelim){ + /* Split t->token into arguments for the line's keyword */ + int i, argc = 0, prevChar = 0; + const unsigned tokLen = (unsigned)(tok->zEnd - tok->zBegin); + unsigned char * zKwd; + unsigned char * zEsc; + unsigned char * zz; + + assert(TT_Line==tok->ttype); + if((unsigned)sizeof(t->args.lineBuf) < tokLen + 1){ + fatal("Keyword line is unreasonably long: %.*s", + tokLen, tok->zBegin); + }else if(!tokLen){ + fatal("Line #%u has no keyword after delimiter", tok->lineNo); + } + g_debug(2,("token @ line %u len=%u [[[%.*s]]]\n", + tok->lineNo, tokLen, tokLen, tok->zBegin)); + zKwd = &t->args.lineBuf[0]; + memcpy(zKwd, tok->zBegin, tokLen); + memset(zKwd + tokLen, 0, sizeof(t->args.lineBuf) - tokLen); + for( zEsc = 0, zz = zKwd; *zz; ++zz ){ + /* Convert backslash-escaped newlines to whitespace */ + switch((int)*zz){ + case (int)'\\': + if(zEsc) zEsc = 0; + else zEsc = zz; + break; + case (int)'\n': + assert(zEsc && "Should not have an unescaped newline?"); + if(zEsc==zz-1){ + *zEsc = (unsigned char)' '; + /* FIXME?: memmove() lnBuf content one byte to the left here + ** to collapse backslash and newline into a single + ** byte. Also consider collapsing all leading space on the + ** next line. */ + } + zEsc = 0; + *zz = (unsigned char)' '; + break; + default: + zEsc = 0; + break; + } + } + t->args.argv[argc++] = zKwd; + for( zz = zKwd; *zz; ++zz ){ + if(isspace(*zz)){ + *zz = 0; + break; + } + } + t->args.pKw = CmppKeyword_search((char const *)zKwd); + if(!t->args.pKw){ + fatal("Unknown keyword '%s' at line %u\n", (char const *)zKwd, + tok->lineNo); + } + for( ++zz ; *zz && isspace(*zz); ++zz ){} + if(t->args.pKw->bTokenize){ + for( ; *zz; prevChar = *zz, ++zz ){ + /* Split string into word-shaped tokens. + ** TODO ?= quoted strings, for the sake of the + ** #error keyword. */ + if(isspace(*zz)){ + assert(zz!=zKwd && "Leading space was stripped earlier."); + *zz = 0; + }else{ + if(argc == (int)CmppArgs_Max){ + fatal("Too many arguments @ line %u: %.*s", + tok->lineNo, tokLen, tok->zBegin); + }else if(zz>zKwd && !prevChar){ + t->args.argv[argc++] = zz; + } + } + } + }else{ + /* Treat rest of line as one token */ + if(*zz) t->args.argv[argc++] = zz; + } + tok->ttype = t->args.pKw->ttype; + if(g.doDebug>1){ + for(i = 0; i < argc; ++i){ + g_debug(0,("line %u arg #%d=%s\n", + tok->lineNo, i, + (char const *)t->args.argv[i])); + } + } + t->args.argc = argc; + }else{ + t->args.pKw = 0; + t->args.argc = 0; + } + return isDelim; +} + +static void cmpp_kwd__err_prefix(CmppKeyword const * pKw, CmppTokenizer *t, + char const *zPrefix){ + g_stderr("%s%s%s @ %s line %u: ", + zPrefix ? zPrefix : "", + zPrefix ? ": " : "", + pKw->zName, t->zName, t->token.lineNo); +} + +/* Internal error reporting helper for cmpp_keyword_f() impls. */ +static CMPP_NORETURN void cmpp_kwd__misuse(CmppKeyword const * pKw, + CmppTokenizer *t, + char const *zFmt, ...){ + va_list va; + cmpp_kwd__err_prefix(pKw, t, "Fatal error"); + va_start(va, zFmt); + fatalv(zFmt, va); + va_end(va); +} + +/* No-op cmpp_keyword_f() impl. */ +static void cmpp_kwd_noop(CmppKeyword const * pKw, CmppTokenizer *t){ + if(t || pKw){/*unused*/} +} + +/* #error impl. */ +static void cmpp_kwd_error(CmppKeyword const * pKw, CmppTokenizer *t){ + if(CT_skip(t)) return; + else{ + assert(t->args.argc < 3); + const char *zBegin = t->args.argc>1 + ? (const char *)t->args.argv[1] : 0; + cmpp_kwd__err_prefix(pKw, t, NULL); + fatal("%s", zBegin ? zBegin : "(no additional info)"); + } +} + +/* Impl. for #define, #undef */ +static void cmpp_kwd_define(CmppKeyword const * pKw, CmppTokenizer *t){ + if(CT_skip(t)) return; + if(t->args.argc<2){ + cmpp_kwd__misuse(pKw, t, "Expecting one or more arguments"); + }else{ + int i = 1; + void (*func)(const char *) = TT_Define==pKw->ttype + ? db_define_add : db_define_rm; + for( ; i < t->args.argc; ++i){ + func( (char const *)t->args.argv[i] ); + } + } +} + +/* Impl. for #if, #ifnot, #elif, #elifnot. */ +static void cmpp_kwd_if(CmppKeyword const * pKw, CmppTokenizer *t){ + int buul; + CmppParseState tmpState = TS_Start; + if(t->args.argc!=2){ + cmpp_kwd__misuse(pKw, t, "Expecting exactly 1 argument"); + } + /*g_debug(0,("%s %s level %u pstate=%d\n", pKw->zName, + (char const *)t->args.argv[1], + t->level.ndx, (int)CT_pstate(t)));*/ + switch(pKw->ttype){ + case TT_Elif: + case TT_ElifNot: + switch(CT_pstate(t)){ + case TS_If: break; + case TS_IfPassed: CT_level(t).flags |= CmppLevel_F_ELIDE; return; + default: goto misuse; + } + break; + case TT_If: + case TT_IfNot: + CmppLevel_push(t); + break; + default: + cmpp_kwd__misuse(pKw, t, "Unpexected keyword token type"); + break; + } + buul = db_define_has((char const *)t->args.argv[1]); + if(TT_IfNot==pKw->ttype || TT_ElifNot==pKw->ttype) buul = !buul; + if(buul){ + CT_pstate(t) = tmpState = TS_IfPassed; + CT_skipLevel(t) = 0; + }else{ + CT_pstate(t) = TS_If /* also for TT_IfNot, TT_Elif, TT_ElifNot */; + CT_skipLevel(t) = 1; + } + if(TT_If==pKw->ttype || TT_IfNot==pKw->ttype){ + unsigned const lvlIf = t->level.ndx; + CmppToken const lvlToken = CT_level(t).token; + while(cmpp_next_keyword_line(t)){ + cmpp_process_keyword(t); + if(lvlIf > t->level.ndx){ + assert(TT_EndIf == t->token.ttype); + break; + } + if(TS_IfPassed==tmpState){ + tmpState = TS_Start; + t->level.stack[lvlIf].flags |= CmppLevel_F_ELIDE; + } + } + if(lvlIf <= t->level.ndx){ + cmpp_kwd__err_prefix(pKw, t, NULL); + fatal("Input ended inside an unterminated %sif " + "opened at [%s] line %u", + g.zDelim, t->zName, lvlToken.lineNo); + } + } + return; + misuse: + cmpp_kwd__misuse(pKw, t, "'%s' used out of context", + pKw->zName); +} + +/* Impl. for #else. */ +static void cmpp_kwd_else(CmppKeyword const * pKw, CmppTokenizer *t){ + if(t->args.argc>1){ + cmpp_kwd__misuse(pKw, t, "Expecting no arguments"); + } + switch(CT_pstate(t)){ + case TS_IfPassed: CT_skipLevel(t) = 1; break; + case TS_If: CT_skipLevel(t) = 0; break; + default: + cmpp_kwd__misuse(pKw, t, "'%s' with no matching 'if'", + pKw->zName); + } + /*g_debug(0,("else flags=0x%02x skipLevel=%u\n", + CT_level(t).flags, CT_level(t).skipLevel));*/ + CT_pstate(t) = TS_Else; +} + +/* Impl. for #endif. */ +static void cmpp_kwd_endif(CmppKeyword const * pKw, CmppTokenizer *t){ + /* Maintenance reminder: we ignore all arguments after the endif + ** to allow for constructs like: + ** + ** #endif // foo + ** + ** in a manner which does not require a specific comment style */ + switch(CT_pstate(t)){ + case TS_Else: + case TS_If: + case TS_IfPassed: + break; + default: + cmpp_kwd__misuse(pKw, t, "'%s' with no matching 'if'", + pKw->zName); + } + CmppLevel_pop(t); +} + +/* Impl. for #include. */ +static void cmpp_kwd_include(CmppKeyword const * pKw, CmppTokenizer *t){ + char const * zFile; + char * zResolved; + if(CT_skip(t)) return; + else if(t->args.argc!=2){ + cmpp_kwd__misuse(pKw, t, "Expecting exactly 1 filename argument"); + } + zFile = (const char *)t->args.argv[1]; + if(db_including_has(zFile)){ + /* Note that different spellings of the same filename + ** will elude this check, but that seems okay, as different + ** spellings means that we're not re-running the exact same + ** invocation. We might want some other form of multi-include + ** protection, rather than this, however. There may well be + ** sensible uses for recursion. */ + cmpp_kwd__err_prefix(pKw, t, NULL); + fatal("Recursive include of file: %s", zFile); + } + zResolved = db_include_search(zFile); + if(zResolved){ + db_including_add(zFile, t->zName, t->token.lineNo); + cmpp_process_file(zResolved); + db_include_rm(zFile); + db_free(zResolved); + }else{ + cmpp_kwd__err_prefix(pKw, t, NULL); + fatal("file not found: %s", zFile); + } +} + +/* Impl. for #pragma. */ +static void cmpp_kwd_pragma(CmppKeyword const * pKw, CmppTokenizer *t){ + const char * zArg; + if(CT_skip(t)) return; + else if(t->args.argc!=2){ + cmpp_kwd__misuse(pKw, t, "Expecting one argument"); + } + zArg = (const char *)t->args.argv[1]; +#define M(X) 0==strcmp(zArg,X) + if(M("defines")){ + sqlite3_stmt * q = 0; + db_prepare(&q, "SELECT k FROM def ORDER BY k"); + g_stderr("cmpp defines:\n"); + while(SQLITE_ROW==db_step(q)){ + int const n = sqlite3_column_bytes(q, 0); + const char * z = (const char *)sqlite3_column_text(q, 0); + g_stderr("\t%.*s\n", n, z); + } + db_finalize(q); + }else{ + cmpp_kwd__misuse(pKw, t, "Unknown pragma"); + } +#undef M +} + +/* #stder impl. */ +static void cmpp_kwd_stderr(CmppKeyword const * pKw, CmppTokenizer *t){ + if(CT_skip(t)) return; + else{ + const char *zBegin = t->args.argc>1 + ? (const char *)t->args.argv[1] : 0; + if(zBegin){ + g_stderr("%s:%u: %s\n", t->zName, t->token.lineNo, zBegin); + }else{ + g_stderr("%s:%u: (no %.*s%s argument)\n", + t->zName, t->token.lineNo, + g.nDelim, g.zDelim, pKw->zName); + } + } +} + +#if 0 +/* Impl. for dummy placeholder. */ +static void cmpp_kwd_todo(CmppKeyword const * pKw, CmppTokenizer *t){ + if(t){/*unused*/} + g_debug(0,("TODO: keyword handler for %s\n", pKw->zName)); +} +#endif + +CmppKeyword aKeywords[] = { +/* Keep these sorted by zName */ + {"//", 2, 0, TT_Comment, cmpp_kwd_noop}, + {"define", 6, 1, TT_Define, cmpp_kwd_define}, + {"elif", 4, 1, TT_Elif, cmpp_kwd_if}, + {"elifnot", 7, 1, TT_ElifNot, cmpp_kwd_if}, + {"else", 4, 1, TT_Else, cmpp_kwd_else}, + {"endif", 5, 0, TT_EndIf, cmpp_kwd_endif}, + {"error", 4, 0, TT_Error, cmpp_kwd_error}, + {"if", 2, 1, TT_If, cmpp_kwd_if}, + {"ifnot", 5, 1, TT_IfNot, cmpp_kwd_if}, + {"include", 7, 0, TT_Include, cmpp_kwd_include}, + {"pragma", 6, 1, TT_Pragma, cmpp_kwd_pragma}, + {"stderr", 6, 0, TT_Stderr, cmpp_kwd_stderr}, + {"undef", 5, 1, TT_Undef, cmpp_kwd_define}, + {0,0,TT_Invalid, 0} +}; + +static int cmp_CmppKeyword(const void *p1, const void *p2){ + char const * zName = (const char *)p1; + CmppKeyword const * kw = (CmppKeyword const *)p2; + return strcmp(zName, kw->zName); +} + +CmppKeyword const * CmppKeyword_search(const char *zName){ + return (CmppKeyword const *)bsearch(zName, &aKeywords[0], + sizeof(aKeywords)/sizeof(aKeywords[0]) - 1, + sizeof(aKeywords[0]), + cmp_CmppKeyword); +} + +void cmpp_process_keyword(CmppTokenizer * const t){ + assert(t->args.pKw); + assert(t->args.argc); + t->args.pKw->xCall(t->args.pKw, t); + t->args.pKw = 0; + t->args.argc = 0; +} + +void cmpp_process_file(const char * zName){ + FileWrapper fw = FileWrapper_empty; + CmppTokenizer ct = CmppTokenizer_empty; + + FileWrapper_open(&fw, zName, "r"); + FileWrapper_slurp(&fw); + g_debug(1,("Read %u byte(s) from [%s]\n", fw.nContent, fw.zName)); + ct.zName = zName; + ct.zBegin = fw.zContent; + ct.zEnd = fw.zContent + fw.nContent; + while(cmpp_next_keyword_line(&ct)){ + cmpp_process_keyword(&ct); + } + FileWrapper_close(&fw); + if(0!=ct.level.ndx){ + CmppLevel * const lv = CmppLevel_get(&ct); + fatal("Input ended inside an unterminated nested construct" + "opened at [%s] line %u", zName, lv->token.lineNo); + } +} + +static void usage(int isErr){ + FILE * const fOut = isErr ? stderr : stdout; + fprintf(fOut, + "Usage: %s [flags] [infile]\n" + "Flags:\n", + g.zArgv0); +#define arg(F,D) fprintf(fOut," %s\n %s\n",F, D) + arg("-f|--file FILE","Read input from FILE (default=- (stdin)).\n" + " Alternately, the first non-flag argument is assumed to " + "be the input file."); + arg("-o|--outfile FILE","Send output to FILE (default=- (stdout))"); + arg("-DXYZ","Define XYZ to true"); + arg("-UXYZ","Undefine XYZ (equivalent to false)"); + arg("-IXYZ","Add dir XYZ to include path"); + arg("-d|--delimiter VALUE", "Set keyword delimiter to VALUE " + "(default=" CMPP_DEFAULT_DELIM ")"); +#undef arg + fputs("",fOut); +} + +int main(int argc, char const * const * argv){ + int rc = 0; + int i; + int inclCount = 0; + const char * zInfile = 0; +#define M(X) (0==strcmp(X,zArg)) +#define ISFLAG(X) else if(M(X)) +#define ISFLAG2(X,Y) else if(M(X) || M(Y)) +#define ARGVAL \ + if(i+1>=argc) fatal("Missing value for flag '%s'", zArg); \ + zArg = argv[++i] + g.zArgv0 = argv[0]; + atexit(cmpp_atexit); + cmpp_initdb(); + for(i = 1; i < argc; ++i){ + char const * zArg = argv[i]; + while('-'==*zArg) ++zArg; + if(M("?") || M("help")) { + usage(0); + goto end; + }else if('D'==*zArg){ + ++zArg; + if(!*zArg) fatal("Missing key for -D"); + db_define_add(zArg); + }else if('U'==*zArg){ + ++zArg; + if(!*zArg) fatal("Missing key for -U"); + db_define_rm(zArg); + }else if('I'==*zArg){ + ++zArg; + if(!*zArg) fatal("Missing directory for -I"); + db_include_dir_add(zArg); + ++inclCount; + } + ISFLAG2("o","outfile"){ + ARGVAL; + if(g.out.zName) fatal("Cannot use -o more than once."); + g.out.zName = zArg; + } + ISFLAG2("f","file"){ + ARGVAL; + do_infile: + if(zInfile) fatal("Cannot use -i more than once."); + zInfile = zArg; + } + ISFLAG2("d","delimiter"){ + ARGVAL; + g.zDelim = zArg; + g.nDelim = (unsigned short)strlen(zArg); + if(!g.nDelim) fatal("Keyword delimiter may not be empty."); + } + ISFLAG("debug"){ + ++g.doDebug; + }else if(!zInfile){ + goto do_infile; + }else{ + fatal("Unhandled flag: %s", argv[i]); + } + } + if(!zInfile) zInfile = "-"; + if(!g.out.zName) g.out.zName = "-"; + if(!inclCount) db_include_dir_add("."); + FileWrapper_open(&g.out, g.out.zName, "w"); + cmpp_process_file(zInfile); + FileWrapper_close(&g.out); + end: + return rc ? EXIT_FAILURE : EXIT_SUCCESS; +} + +#undef CT_level +#undef CT_pstate +#undef CT_skipLevel +#undef CT_skip +#undef CLvl_skip diff --git a/ext/wasm/common/emscripten.css b/ext/wasm/common/emscripten.css index 7e3dc811d0..d8f82c73b2 100644 --- a/ext/wasm/common/emscripten.css +++ b/ext/wasm/common/emscripten.css @@ -1,4 +1,4 @@ -/* emcscript-related styling, used during the module load/intialization processes... */ +/* emscripten-related styling, used during the module load/intialization processes... */ .emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; } div.emscripten { text-align: center; } div.emscripten_border { border: 1px solid black; } diff --git a/ext/wasm/common/testing.css b/ext/wasm/common/testing.css index 9438b330c9..fb44f1d612 100644 --- a/ext/wasm/common/testing.css +++ b/ext/wasm/common/testing.css @@ -61,3 +61,9 @@ span.labeled-input { flex-direction: column-reverse; } label[for] { cursor: pointer } + +h1 { + border-radius: 0.25em; + padding: 0.15em 0.25em; +} +h1:first-of-type {margin: 0 0 0.5em 0;} diff --git a/ext/wasm/common/whwasmutil.js b/ext/wasm/common/whwasmutil.js index 7e5e7981f7..87cc100f33 100644 --- a/ext/wasm/common/whwasmutil.js +++ b/ext/wasm/common/whwasmutil.js @@ -316,13 +316,11 @@ self.WhWasmUtilInstaller = function(target){ Throws if passed an invalid n. - Pedantic side note: the name "heap" is a bit of a misnomer. In an - Emscripten environment, the memory managed via the stack - allocation API is in the same Memory object as the heap (which - makes sense because otherwise arbitrary pointer X would be - ambiguous: is it in the heap or the stack?). + Pedantic side note: the name "heap" is a bit of a misnomer. In a + WASM environment, the stack and heap memory are all accessed via + the same view(s) of the memory. */ - target.heapForSize = function(n,unsigned = false){ + target.heapForSize = function(n,unsigned = true){ let ctor; const c = (cache.memory && cache.heapSize === cache.memory.buffer.byteLength) ? cache : heapWrappers(); @@ -496,6 +494,70 @@ self.WhWasmUtilInstaller = function(target){ e: { f: func } })).exports['f']; }/*jsFuncToWasm()*/; + + /** + Documented as target.installFunction() except for the 3rd + argument: if truthy, the newly-created function pointer + is stashed in the current scoped-alloc scope and will be + cleaned up at the matching scopedAllocPop(), else it + is not stashed there. + */ + const __installFunction = function f(func, sig, scoped){ + if(scoped && !cache.scopedAlloc.length){ + toss("No scopedAllocPush() scope is active."); + } + if('string'===typeof func){ + const x = sig; + sig = func; + func = x; + } + if('string'!==typeof sig || !(func instanceof Function)){ + toss("Invalid arguments: expecting (function,signature) "+ + "or (signature,function)."); + } + const ft = target.functionTable(); + const oldLen = ft.length; + let ptr; + while(cache.freeFuncIndexes.length){ + ptr = cache.freeFuncIndexes.pop(); + if(ft.get(ptr)){ /* Table was modified via a different API */ + ptr = null; + continue; + }else{ + break; + } + } + if(!ptr){ + ptr = oldLen; + ft.grow(1); + } + try{ + /*this will only work if func is a WASM-exported function*/ + ft.set(ptr, func); + if(scoped){ + cache.scopedAlloc[cache.scopedAlloc.length-1].push(ptr); + } + return ptr; + }catch(e){ + if(!(e instanceof TypeError)){ + if(ptr===oldLen) cache.freeFuncIndexes.push(oldLen); + throw e; + } + } + // It's not a WASM-exported function, so compile one... + try { + const fptr = target.jsFuncToWasm(func, sig); + ft.set(ptr, fptr); + if(scoped){ + cache.scopedAlloc[cache.scopedAlloc.length-1].push(ptr); + } + }catch(e){ + if(ptr===oldLen) cache.freeFuncIndexes.push(oldLen); + throw e; + } + return ptr; + }; + /** Expects a JS function and signature, exactly as for @@ -528,50 +590,19 @@ self.WhWasmUtilInstaller = function(target){ https://github.com/emscripten-core/emscripten/issues/17323 */ - target.installFunction = function f(func, sig){ - if(2!==arguments.length){ - toss("installFunction() requires exactly 2 arguments"); - } - if('string'===typeof func){ - const x = sig; - sig = func; - func = x; - } - const ft = target.functionTable(); - const oldLen = ft.length; - let ptr; - while(cache.freeFuncIndexes.length){ - ptr = cache.freeFuncIndexes.pop(); - if(ft.get(ptr)){ /* Table was modified via a different API */ - ptr = null; - continue; - }else{ - break; - } - } - if(!ptr){ - ptr = oldLen; - ft.grow(1); - } - try{ - /*this will only work if func is a WASM-exported function*/ - ft.set(ptr, func); - return ptr; - }catch(e){ - if(!(e instanceof TypeError)){ - if(ptr===oldLen) cache.freeFuncIndexes.push(oldLen); - throw e; - } - } - // It's not a WASM-exported function, so compile one... - try { - ft.set(ptr, target.jsFuncToWasm(func, sig)); - }catch(e){ - if(ptr===oldLen) cache.freeFuncIndexes.push(oldLen); - throw e; - } - return ptr; - }; + target.installFunction = (func, sig)=>__installFunction(func, sig, false); + + /** + EXPERIMENTAL! DO NOT USE IN CLIENT CODE! + + Works exactly like installFunction() but requires that a + scopedAllocPush() is active and uninstalls the given function + when that alloc scope is popped via scopedAllocPop(). + This is used for implementing JS/WASM function bindings which + should only persist for the life of a call into a single + C-side function. + */ + target.scopedInstallFunction = (func, sig)=>__installFunction(func, sig, true); /** Requires a pointer value previously returned from @@ -599,19 +630,24 @@ self.WhWasmUtilInstaller = function(target){ type triggers an exception if this.bigIntEnabled is falsy). Throws if given an invalid type. + If the first argument is an array, it is treated as an array of + addresses and the result is an array of the values from each of + those address, using the same 2nd argument for determining the + value type to fetch. + As a special case, if type ends with a `*`, it is considered to be a pointer type and is treated as the WASM numeric type appropriate for the pointer size (`i32`). - While likely not obvious, this routine and its setMemValue() + While likely not obvious, this routine and its poke() counterpart are how pointer-to-value _output_ parameters in WASM-compiled C code can be interacted with: ``` const ptr = alloc(4); - setMemValue(ptr, 0, 'i32'); // clear the ptr's value + poke(ptr, 0, 'i32'); // clear the ptr's value aCFuncWithOutputPtrToInt32Arg( ptr ); // e.g. void foo(int *x); - const result = getMemValue(ptr, 'i32'); // fetch ptr's value + const result = peek(ptr, 'i32'); // fetch ptr's value dealloc(ptr); ``` @@ -623,15 +659,15 @@ self.WhWasmUtilInstaller = function(target){ const scope = scopedAllocPush(); try{ const ptr = scopedAlloc(4); - setMemValue(ptr, 0, 'i32'); + poke(ptr, 0, 'i32'); aCFuncWithOutputPtrArg( ptr ); - result = getMemValue(ptr, 'i32'); + result = peek(ptr, 'i32'); }finally{ scopedAllocPop(scope); } ``` - As a rule setMemValue() must be called to set (typically zero + As a rule poke() must be called to set (typically zero out) the pointer's value, else it will contain an essentially random value. @@ -639,70 +675,143 @@ self.WhWasmUtilInstaller = function(target){ painful impact on performance. Rather than doing so, use heapForSize() to fetch the heap object and read directly from it. - See: setMemValue() + See: poke() */ - target.getMemValue = function(ptr, type='i8'){ + target.peek = function f(ptr, type='i8'){ if(type.endsWith('*')) type = ptrIR; const c = (cache.memory && cache.heapSize === cache.memory.buffer.byteLength) ? cache : heapWrappers(); - switch(type){ - case 'i1': - case 'i8': return c.HEAP8[ptr>>0]; - case 'i16': return c.HEAP16[ptr>>1]; - case 'i32': return c.HEAP32[ptr>>2]; - case 'i64': - if(target.bigIntEnabled) return BigInt(c.HEAP64[ptr>>3]); - break; - case 'float': case 'f32': return c.HEAP32F[ptr>>2]; - case 'double': case 'f64': return Number(c.HEAP64F[ptr>>3]); - default: break; - } - toss('Invalid type for getMemValue():',type); + const list = Array.isArray(ptr) ? [] : undefined; + let rc; + do{ + if(list) ptr = arguments[0].shift(); + switch(type){ + case 'i1': + case 'i8': rc = c.HEAP8[ptr>>0]; break; + case 'i16': rc = c.HEAP16[ptr>>1]; break; + case 'i32': rc = c.HEAP32[ptr>>2]; break; + case 'float': case 'f32': rc = c.HEAP32F[ptr>>2]; break; + case 'double': case 'f64': rc = Number(c.HEAP64F[ptr>>3]); break; + case 'i64': + if(target.bigIntEnabled){ + rc = BigInt(c.HEAP64[ptr>>3]); + break; + } + /* fallthru */ + default: + toss('Invalid type for peek():',type); + } + if(list) list.push(rc); + }while(list && arguments[0].length); + return list || rc; }; /** - The counterpart of getMemValue(), this sets a numeric value at + The counterpart of peek(), this sets a numeric value at the given WASM heap address, using the type to define how many bytes are written. Throws if given an invalid type. See - getMemValue() for details about the type argument. If the 3rd + peek() for details about the type argument. If the 3rd argument ends with `*` then it is treated as a pointer type and this function behaves as if the 3rd argument were `i32`. - This function returns itself. + If the first argument is an array, it is treated like a list + of pointers and the given value is written to each one. + + Returns `this`. (Prior to 2022-12-09 it returns this function.) ACHTUNG: calling this often, e.g. in a loop, can have a noticably painful impact on performance. Rather than doing so, use - heapForSize() to fetch the heap object and assign directly to it. + heapForSize() to fetch the heap object and assign directly to it + or use the heap's set() method. */ - target.setMemValue = function f(ptr, value, type='i8'){ + target.poke = function(ptr, value, type='i8'){ if (type.endsWith('*')) type = ptrIR; const c = (cache.memory && cache.heapSize === cache.memory.buffer.byteLength) ? cache : heapWrappers(); - switch (type) { - case 'i1': - case 'i8': c.HEAP8[ptr>>0] = value; return f; - case 'i16': c.HEAP16[ptr>>1] = value; return f; - case 'i32': c.HEAP32[ptr>>2] = value; return f; - case 'i64': - if(c.HEAP64){ - c.HEAP64[ptr>>3] = BigInt(value); - return f; - } - break; - case 'float': case 'f32': c.HEAP32F[ptr>>2] = value; return f; - case 'double': case 'f64': c.HEAP64F[ptr>>3] = value; return f; + for(const p of (Array.isArray(ptr) ? ptr : [ptr])){ + switch (type) { + case 'i1': + case 'i8': c.HEAP8[p>>0] = value; continue; + case 'i16': c.HEAP16[p>>1] = value; continue; + case 'i32': c.HEAP32[p>>2] = value; continue; + case 'float': case 'f32': c.HEAP32F[p>>2] = value; continue; + case 'double': case 'f64': c.HEAP64F[p>>3] = value; continue; + case 'i64': + if(c.HEAP64){ + c.HEAP64[p>>3] = BigInt(value); + continue; + } + /* fallthru */ + default: + toss('Invalid type for poke(): ' + type); + } } - toss('Invalid type for setMemValue(): ' + type); + return this; }; + /** + Convenience form of peek() intended for fetching + pointer-to-pointer values. If passed a single non-array argument + it returns the value of that one pointer address. If passed + multiple arguments, or a single array of arguments, it returns an + array of their values. + */ + target.peekPtr = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), ptrIR ); - /** Convenience form of getMemValue() intended for fetching - pointer-to-pointer values. */ - target.getPtrValue = (ptr)=>target.getMemValue(ptr, ptrIR); + /** + A variant of poke() intended for setting pointer-to-pointer + values. Its differences from poke() are that (1) it defaults to a + value of 0 and (2) it always writes to the pointer-sized heap + view. + */ + target.pokePtr = (ptr, value=0)=>target.poke(ptr, value, ptrIR); - /** Convenience form of setMemValue() intended for setting - pointer-to-pointer values. */ - target.setPtrValue = (ptr, value)=>target.setMemValue(ptr, value, ptrIR); + /** + Convenience form of peek() intended for fetching i8 values. If + passed a single non-array argument it returns the value of that + one pointer address. If passed multiple arguments, or a single + array of arguments, it returns an array of their values. + */ + target.peek8 = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), 'i8' ); + /** + Convience form of poke() intended for setting individual bytes. + Its difference from poke() is that it always writes to the + i8-sized heap view. + */ + target.poke8 = (ptr, value)=>target.poke(ptr, value, 'i8'); + /** i16 variant of peek8(). */ + target.peek16 = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), 'i16' ); + /** i16 variant of poke8(). */ + target.poke16 = (ptr, value)=>target.poke(ptr, value, 'i16'); + /** i32 variant of peek8(). */ + target.peek32 = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), 'i32' ); + /** i32 variant of poke8(). */ + target.poke32 = (ptr, value)=>target.poke(ptr, value, 'i32'); + /** i64 variant of peek8(). Will throw if this build is not + configured for BigInt support. */ + target.peek64 = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), 'i64' ); + /** i64 variant of poke8(). Will throw if this build is not + configured for BigInt support. Note that this returns + a BigInt-type value, not a Number-type value. */ + target.poke64 = (ptr, value)=>target.poke(ptr, value, 'i64'); + /** f32 variant of peek8(). */ + target.peek32f = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), 'f32' ); + /** f32 variant of poke8(). */ + target.poke32f = (ptr, value)=>target.poke(ptr, value, 'f32'); + /** f64 variant of peek8(). */ + target.peek64f = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), 'f64' ); + /** f64 variant of poke8(). */ + target.poke64f = (ptr, value)=>target.poke(ptr, value, 'f64'); + + + /** Deprecated alias for getMemValue() */ + target.getMemValue = target.peek; + /** Deprecated alias for peekPtr() */ + target.getPtrValue = target.peekPtr; + /** Deprecated alias for poke() */ + target.setMemValue = target.poke; + /** Deprecated alias for pokePtr() */ + target.setPtrValue = target.pokePtr; /** Returns true if the given value appears to be legal for use as @@ -725,11 +834,12 @@ self.WhWasmUtilInstaller = function(target){ Expects ptr to be a pointer into the WASM heap memory which refers to a NUL-terminated C-style string encoded as UTF-8. Returns the length, in bytes, of the string, as for `strlen(3)`. - As a special case, if !ptr then it it returns `null`. Throws if - ptr is out of range for target.heap8u(). + As a special case, if !ptr or if it's not a pointer then it + returns `null`. Throws if ptr is out of range for + target.heap8u(). */ target.cstrlen = function(ptr){ - if(!ptr) return null; + if(!ptr || !target.isPtr(ptr)) return null; const h = heapWrappers().HEAP8U; let pos = ptr; for( ; h[pos] !== 0; ++pos ){} @@ -753,9 +863,9 @@ self.WhWasmUtilInstaller = function(target){ refers to a NUL-terminated C-style string encoded as UTF-8. This function counts its byte length using cstrlen() then returns a JS-format string representing its contents. As a special case, if - ptr is falsy, `null` is returned. + ptr is falsy or not a pointer, `null` is returned. */ - target.cstringToJs = function(ptr){ + target.cstrToJs = function(ptr){ const n = target.cstrlen(ptr); return n ? __utf8Decode(heapWrappers().HEAP8U, ptr, ptr+n) : (null===n ? n : ""); }; @@ -942,10 +1052,19 @@ self.WhWasmUtilInstaller = function(target){ const __allocCStr = function(jstr, returnWithLength, allocator, funcName){ __affirmAlloc(target, funcName); if('string'!==typeof jstr) return null; - const n = target.jstrlen(jstr), - ptr = allocator(n+1); - target.jstrcpy(jstr, target.heap8u(), ptr, n+1, true); - return returnWithLength ? [ptr, n] : ptr; + if(0){/* older impl, possibly more widely compatible? */ + const n = target.jstrlen(jstr), + ptr = allocator(n+1); + target.jstrcpy(jstr, target.heap8u(), ptr, n+1, true); + return returnWithLength ? [ptr, n] : ptr; + }else{/* newer, (probably) faster and (certainly) simpler impl */ + const u = cache.utf8Encoder.encode(jstr), + ptr = allocator(u.length+1), + heap = heapWrappers().HEAP8U; + heap.set(u, ptr); + heap[ptr + u.length] = 0; + return returnWithLength ? [ptr, u.length] : ptr; + } }; /** @@ -1035,7 +1154,13 @@ self.WhWasmUtilInstaller = function(target){ if(n<0) toss("Invalid state object for scopedAllocPop()."); if(0===arguments.length) state = cache.scopedAlloc[n]; cache.scopedAlloc.splice(n,1); - for(let p; (p = state.pop()); ) target.dealloc(p); + for(let p; (p = state.pop()); ){ + if(target.functionEntry(p)){ + //console.warn("scopedAllocPop() uninstalling transient function",p); + target.uninstallFunction(p); + } + else target.dealloc(p); + } }; /** @@ -1081,40 +1206,67 @@ self.WhWasmUtilInstaller = function(target){ // impl for allocMainArgv() and scopedAllocMainArgv(). const __allocMainArgv = function(isScoped, list){ - if(!list.length) toss("Cannot allocate empty array."); const pList = target[ isScoped ? 'scopedAlloc' : 'alloc' - ](list.length * target.ptrSizeof); + ]((list.length + 1) * target.ptrSizeof); let i = 0; list.forEach((e)=>{ - target.setPtrValue(pList + (target.ptrSizeof * i++), + target.pokePtr(pList + (target.ptrSizeof * i++), target[ isScoped ? 'scopedAllocCString' : 'allocCString' ](""+e)); }); + target.pokePtr(pList + (target.ptrSizeof * i), 0); return pList; }; /** Creates an array, using scopedAlloc(), suitable for passing to a C-level main() routine. The input is a collection with a length - property and a forEach() method. A block of memory list.length - entries long is allocated and each pointer-sized block of that - memory is populated with a scopedAllocCString() conversion of the - (""+value) of each element. Returns a pointer to the start of the - list, suitable for passing as the 2nd argument to a C-style - main() function. + property and a forEach() method. A block of memory + (list.length+1) entries long is allocated and each pointer-sized + block of that memory is populated with a scopedAllocCString() + conversion of the (""+value) of each element, with the exception + that the final entry is a NULL pointer. Returns a pointer to the + start of the list, suitable for passing as the 2nd argument to a + C-style main() function. - Throws if list.length is falsy or scopedAllocPush() is not active. + Throws if scopedAllocPush() is not active. + + Design note: the returned array is allocated with an extra NULL + pointer entry to accommodate certain APIs, but client code which + does not need that functionality should treat the returned array + as list.length entries long. */ target.scopedAllocMainArgv = (list)=>__allocMainArgv(true, list); /** Identical to scopedAllocMainArgv() but uses alloc() instead of - scopedAllocMainArgv + scopedAlloc(). */ target.allocMainArgv = (list)=>__allocMainArgv(false, list); + /** + Expects to be given a C-style string array and its length. It + returns a JS array of strings and/or nulls: any entry in the + pArgv array which is NULL results in a null entry in the result + array. If argc is 0 then an empty array is returned. + + Results are undefined if any entry in the first argc entries of + pArgv are neither 0 (NULL) nor legal UTF-format C strings. + + To be clear, the expected C-style arguments to be passed to this + function are `(int, char **)` (optionally const-qualified). + */ + target.cArgvToJs = (argc, pArgv)=>{ + const list = []; + for(let i = 0; i < argc; ++i){ + const arg = target.peekPtr(pArgv + (target.ptrSizeof * i)); + list.push( arg ? target.cstrToJs(arg) : null ); + } + return list; + }; + /** Wraps function call func() in a scopedAllocPush() and scopedAllocPop() block, such that all calls to scopedAlloc() and @@ -1133,7 +1285,7 @@ self.WhWasmUtilInstaller = function(target){ __affirmAlloc(target, method); const pIr = safePtrSize ? 'i64' : ptrIR; let m = target[method](howMany * (safePtrSize ? 8 : ptrSizeof)); - target.setMemValue(m, 0, pIr) + target.poke(m, 0, pIr) if(1===howMany){ return m; } @@ -1141,7 +1293,7 @@ self.WhWasmUtilInstaller = function(target){ for(let i = 1; i < howMany; ++i){ m += (safePtrSize ? 8 : ptrSizeof); a[i] = m; - target.setMemValue(m, 0, pIr); + target.poke(m, 0, pIr); } return a; }; @@ -1172,7 +1324,7 @@ self.WhWasmUtilInstaller = function(target){ When one of the returned pointers will refer to a 64-bit value, e.g. a double or int64, an that value must be written or fetched, - e.g. using setMemValue() or getMemValue(), it is important that + e.g. using poke() or peek(), it is important that the pointer in question be aligned to an 8-byte boundary or else it will not be fetched or written properly and will corrupt or read neighboring memory. It is only safe to pass false when the @@ -1228,32 +1380,39 @@ self.WhWasmUtilInstaller = function(target){ State for use with xWrap() */ cache.xWrap = Object.create(null); - const xcv = cache.xWrap.convert = Object.create(null); + cache.xWrap.convert = Object.create(null); /** Map of type names to argument conversion functions. */ - cache.xWrap.convert.arg = Object.create(null); + cache.xWrap.convert.arg = new Map; /** Map of type names to return result conversion functions. */ - cache.xWrap.convert.result = Object.create(null); + cache.xWrap.convert.result = new Map; + const xArg = cache.xWrap.convert.arg, xResult = cache.xWrap.convert.result; if(target.bigIntEnabled){ - xcv.arg.i64 = (i)=>BigInt(i); + xArg.set('i64', (i)=>BigInt(i)); } - xcv.arg.i32 = (i)=>(i | 0); - xcv.arg.i16 = (i)=>((i | 0) & 0xFFFF); - xcv.arg.i8 = (i)=>((i | 0) & 0xFF); - xcv.arg.f32 = xcv.arg.float = (i)=>Number(i).valueOf(); - xcv.arg.f64 = xcv.arg.double = xcv.arg.f32; - xcv.arg.int = xcv.arg.i32; - xcv.result['*'] = xcv.result['pointer'] = xcv.arg['**'] = xcv.arg[ptrIR]; - xcv.result['number'] = (v)=>Number(v); + xArg.set('i32', (i)=>(i | 0)); + xArg.set('i16', (i)=>((i | 0) & 0xFFFF)); + xArg.set('i8', (i)=>((i | 0) & 0xFF)); + xArg.set('f32', (i)=>Number(i).valueOf()); + xArg.set('float', xArg.get('f32')); + xArg.set('f64', xArg.get('f32')); + xArg.set('double', xArg.get('f64')); + xArg.set('int', xArg.get('i32')); + xResult.set('*', xArg.get(ptrIR)); + xResult.set('pointer', xArg.get(ptrIR)); + xArg.set('**', xArg.get(ptrIR)); + xResult.set('number', (v)=>Number(v)); - { /* Copy certain xcv.arg[...] handlers to xcv.result[...] and + { /* Copy certain xArg[...] handlers to xResult[...] and add pointer-style variants of them. */ const copyToResult = ['i8', 'i16', 'i32', 'int', 'f32', 'float', 'f64', 'double']; if(target.bigIntEnabled) copyToResult.push('i64'); + const adaptPtr = xArg.get(ptrIR); for(const t of copyToResult){ - xcv.arg[t+'*'] = xcv.result[t+'*'] = xcv.arg[ptrIR]; - xcv.result[t] = xcv.arg[t] || toss("Missing arg converter:",t); + xArg.set(t+'*', adaptPtr); + xResult.set(t+'*', adaptPtr); + xResult.set(t, (xArg.get(t) || toss("Missing arg converter:",t))); } } @@ -1267,29 +1426,34 @@ self.WhWasmUtilInstaller = function(target){ - If v is a string, scopeAlloc() a new C-string from it and return that temp string's pointer. - - Else return the value from the arg adaptor defined for ptrIR. + - Else return the value from the arg adapter defined for ptrIR. TODO? Permit an Int8Array/Uint8Array and convert it to a string? Would that be too much magic concentrated in one place, ready to backfire? */ - xcv.arg.string = xcv.arg.utf8 = xcv.arg['pointer'] = xcv.arg['*'] - = function(v){ - if('string'===typeof v) return target.scopedAllocCString(v); - return v ? xcv.arg[ptrIR](v) : null; - }; - xcv.result.string = xcv.result.utf8 = (i)=>target.cstringToJs(i); - xcv.result['string:free'] = xcv.result['utf8:free'] = (i)=>{ - try { return i ? target.cstringToJs(i) : null } + xArg.set('string', function(v){ + if('string'===typeof v) return target.scopedAllocCString(v); + return v ? xArg.get(ptrIR)(v) : null; + }); + xArg.set('utf8', xArg.get('string')); + xArg.set('pointer', xArg.get('string')); + xArg.set('*', xArg.get('string')); + + xResult.set('string', (i)=>target.cstrToJs(i)); + xResult.set('utf8', xResult.get('string')); + xResult.set('string:dealloc', (i)=>{ + try { return i ? target.cstrToJs(i) : null } finally{ target.dealloc(i) } - }; - xcv.result.json = (i)=>JSON.parse(target.cstringToJs(i)); - xcv.result['json:free'] = (i)=>{ - try{ return i ? JSON.parse(target.cstringToJs(i)) : null } + }); + xResult.set('utf8:dealloc', xResult.get('string:dealloc')); + xResult.set('json', (i)=>JSON.parse(target.cstrToJs(i))); + xResult.set('json:dealloc', (i)=>{ + try{ return i ? JSON.parse(target.cstrToJs(i)) : null } finally{ target.dealloc(i) } - } - xcv.result['void'] = (v)=>undefined; - xcv.result['null'] = (v)=>v; + }); + xResult.set('void', (v)=>undefined); + xResult.set('null', (v)=>v); if(0){ /*** @@ -1302,19 +1466,228 @@ self.WhWasmUtilInstaller = function(target){ the value will always be treated like -1 (which has a useful case in the sqlite3 bindings). */ - xcv.arg['func-ptr'] = function(v){ - if(!(v instanceof Function)) return xcv.arg[ptrIR]; + xArg.set('func-ptr', function(v){ + if(!(v instanceof Function)) return xArg.get(ptrIR); const f = target.jsFuncToWasm(v, WHAT_SIGNATURE); - }; + }); } + /** + Internal-use-only base class for FuncPtrAdapter and potentially + additional stateful argument adapter classes. + + Note that its main interface (convertArg()) is strictly + internal, not to be exposed to client code, as it may still + need re-shaping. Only the constructors of concrete subclasses + should be exposed to clients, and those in such a way that + does not hinder internal redesign of the convertArg() + interface. + */ + const AbstractArgAdapter = class { + constructor(opt){ + this.name = opt.name; + } + /** + Gets called via xWrap() to "convert" v to whatever type + this specific class supports. + + argIndex is the argv index of _this_ argument in the + being-xWrap()'d call. argv is the current argument list + undergoing xWrap() argument conversion. argv entries to the + left of argIndex will have already undergone transformation and + those to the right will not have (they will have the values the + client-level code passed in, awaiting conversion). The RHS + indexes must never be relied upon for anything because their + types are indeterminate, whereas the LHS values will be + WASM-compatible values by the time this is called. + */ + convertArg(v,argIndex,argv){ + toss("AbstractArgAdapter must be subclassed."); + } + }; + + /** + An attempt at adding function pointer conversion support to + xWrap(). This type is recognized by xWrap() as a proxy for + converting a JS function to a C-side function, either + permanently, for the duration of a single call into the C layer, + or semi-contextual, where it may keep track of a single binding + for a given context and uninstall the binding if it's replaced. + + The constructor requires an options object with these properties: + + - name (optional): string describing the function binding. This + is solely for debugging and error-reporting purposes. If not + provided, an empty string is assumed. + + - signature: a function signature string compatible with + jsFuncToWasm(). + + - bindScope (string): one of ('transient', 'context', + 'singleton'). Bind scopes are: + + - 'transient': it will convert JS functions to WASM only for + the duration of the xWrap()'d function call, using + scopedInstallFunction(). Before that call returns, the + WASM-side binding will be uninstalled. + + - 'singleton': holds one function-pointer binding for this + instance. If it's called with a different function pointer, + it uninstalls the previous one after converting the new + value. This is only useful for use with "global" functions + which do not rely on any state other than this function + pointer. If the being-converted function pointer is intended + to be mapped to some sort of state object (e.g. an sqlite3*) + then "context" (see below) is the proper mode. + + - 'context': similar to singleton mode but for a given + "context", where the context is a key provided by the user + and possibly dependent on a small amount of call-time + context. This mode is the default if bindScope is _not_ set + but a property named contextKey (described below) is. + + - contextKey (function): is only used if bindScope is 'context' + or if bindScope is not set and this function is, in which case + 'context' is assumed. This function gets passed + (argIndex,argv), where argIndex is the index of _this_ function + pointer in its _wrapping_ function's arguments and argv is the + _current_ still-being-xWrap()-processed args array. All + arguments to the left of argIndex will have been processed by + xWrap() by the time this is called. argv[argIndex] will be the + value the user passed in to the xWrap()'d function for the + argument this FuncPtrAdapter is mapped to. Arguments to the + right of argv[argIndex] will not yet have been converted before + this is called. The function must return a key which uniquely + identifies this function mapping context for _this_ + FuncPtrAdapter instance (other instances are not considered), + taking into account that C functions often take some sort of + state object as one or more of their arguments. As an example, + if the xWrap()'d function takes `(int,T*,functionPtr,X*)` and + this FuncPtrAdapter is the argv[2]nd arg, contextKey(2,argv) + might return 'T@'+argv[1], or even just argv[1]. Note, + however, that the (X*) argument will not yet have been + processed by the time this is called and should not be used as + part of that key because its pre-conversion data type might be + unpredictable. Similarly, care must be taken with C-string-type + arguments: those to the left in argv will, when this is called, + be WASM pointers, whereas those to the right might (and likely + do) have another data type. When using C-strings in keys, never + use their pointers in the key because most C-strings in this + constellation are transient. + + Yes, that ^^^ is a bit awkward, but it's what we have. + + The constructor only saves the above state for later, and does + not actually bind any functions. Its convertArg() method is + called via xWrap() to perform any bindings. + + Shortcomings: function pointers which include C-string arguments + may still need a level of hand-written wrappers around them, + depending on how they're used, in order to provide the client + with JS strings. + */ + xArg.FuncPtrAdapter = class FuncPtrAdapter extends AbstractArgAdapter { + constructor(opt) { + super(opt); + this.signature = opt.signature; + if(!opt.bindScope && (opt.contextKey instanceof Function)){ + opt.bindScope = 'context'; + }else if(FuncPtrAdapter.bindScopes.indexOf(opt.bindScope)<0){ + toss("Invalid options.bindScope ("+opt.bindMod+") for FuncPtrAdapter. "+ + "Expecting one of: ("+FuncPtrAdapter.bindScopes.join(', ')+')'); + } + this.bindScope = opt.bindScope; + if(opt.contextKey) this.contextKey = opt.contextKey /*else inherit one*/; + this.isTransient = 'transient'===this.bindScope; + this.isContext = 'context'===this.bindScope; + if( ('singleton'===this.bindScope) ) this.singleton = []; + else this.singleton = undefined; + //console.warn("FuncPtrAdapter()",opt,this); + } + + static bindScopes = [ + 'transient', 'context', 'singleton' + ]; + + /* Dummy impl. Overwritten per-instance as needed. */ + contextKey(argIndex,argv){ + return this; + } + + /* Returns this objects mapping for the given context key, in the + form of an an array, creating the mapping if needed. The key + may be anything suitable for use in a Map. */ + contextMap(key){ + const cm = (this.__cmap || (this.__cmap = new Map)); + let rc = cm.get(key); + if(undefined===rc) cm.set(key, (rc = [])); + return rc; + } + + /** + Gets called via xWrap() to "convert" v to a WASM-bound function + pointer. If v is one of (a pointer, null, undefined) then + (v||0) is returned and any earlier function installed by this + mapping _might_, depending on how it's bound, be uninstalled. + If v is not one of those types, it must be a Function, for + which it creates (if needed) a WASM function binding and + returns the WASM pointer to that binding. If this instance is + not in 'transient' mode, it will remember the binding for at + least the next call, to avoid recreating the function binding + unnecessarily. + + If it's passed a pointer(ish) value for v, it does _not_ + perform any function binding, so this object's bindMode is + irrelevant for such cases. + + See the parent class's convertArg() docs for details on what + exactly the 2nd and 3rd arguments are. + */ + convertArg(v,argIndex,argv){ + //console.warn("FuncPtrAdapter.convertArg()",this.signature,this.transient,v); + let pair = this.singleton; + if(!pair && this.isContext){ + pair = this.contextMap(this.contextKey(argIndex, argv)); + } + if(pair && pair[0]===v) return pair[1]; + if(v instanceof Function){ + const fp = __installFunction(v, this.signature, this.isTransient); + if(pair){ + /* Replace existing stashed mapping */ + if(pair[1]){ + try{target.uninstallFunction(pair[1])} + catch(e){/*ignored*/} + } + pair[0] = v; + pair[1] = fp; + } + return fp; + }else if(target.isPtr(v) || null===v || undefined===v){ + if(pair && pair[1] && pair[1]!==v){ + /* uninstall stashed mapping and replace stashed mapping with v. */ + //console.warn("FuncPtrAdapter is uninstalling function", this.contextKey(argIndex,argv),v); + try{target.uninstallFunction(pair[1])} + catch(e){/*ignored*/} + pair[0] = pair[1] = (v || 0); + } + return v || 0; + }else{ + throw new TypeError("Invalid FuncPtrAdapter argument type. "+ + "Expecting a function pointer or a "+ + (this.name ? this.name+' ' : '')+ + "function matching signature "+ + this.signature+"."); + } + }/*convertArg()*/ + }/*FuncPtrAdapter*/; + const __xArgAdapterCheck = - (t)=>xcv.arg[t] || toss("Argument adapter not found:",t); + (t)=>xArg.get(t) || toss("Argument adapter not found:",t); const __xResultAdapterCheck = - (t)=>xcv.result[t] || toss("Result adapter not found:",t); - - cache.xWrap.convertArg = (t,v)=>__xArgAdapterCheck(t)(v); + (t)=>xResult.get(t) || toss("Result adapter not found:",t); + + cache.xWrap.convertArg = (t,...args)=>__xArgAdapterCheck(t)(...args); cache.xWrap.convertResult = (t,v)=>(null===t ? v : (t ? __xResultAdapterCheck(t)(v) : undefined)); @@ -1383,7 +1756,7 @@ self.WhWasmUtilInstaller = function(target){ true. - `f32` (`float`), `f64` (`double`) (args and results): pass - their argument to Number(). i.e. the adaptor does not currently + their argument to Number(). i.e. the adapter does not currently distinguish between the two types of floating-point numbers. - `number` (results): converts the result to a JS Number using @@ -1411,7 +1784,7 @@ self.WhWasmUtilInstaller = function(target){ const C-string, encoded as UTF-8, copies it to a JS string, and returns that JS string. - - `string:free` or `utf8:free) (results): treats the result value + - `string:dealloc` or `utf8:dealloc) (results): treats the result value as a non-const UTF-8 C-string, ownership of which has just been transfered to the caller. It copies the C-string to a JS string, frees the C-string, and returns the JS string. If such @@ -1422,8 +1795,8 @@ self.WhWasmUtilInstaller = function(target){ required. For example: ```js - target.xWrap.resultAdaptor('string:my_free',(i)=>{ - try { return i ? target.cstringToJs(i) : null } + target.xWrap.resultAdapter('string:my_free',(i)=>{ + try { return i ? target.cstrToJs(i) : null } finally{ target.exports.my_free(i) } }; ``` @@ -1432,9 +1805,9 @@ self.WhWasmUtilInstaller = function(target){ returns the result of passing the converted-to-JS string to JSON.parse(). Returns `null` if the C-string is a NULL pointer. - - `json:free` (results): works exactly like `string:free` but + - `json:dealloc` (results): works exactly like `string:dealloc` but returns the same thing as the `json` adapter. Note the - warning in `string:free` regarding maching allocators and + warning in `string:dealloc` regarding maching allocators and deallocators. The type names for results and arguments are validated when @@ -1442,7 +1815,7 @@ self.WhWasmUtilInstaller = function(target){ exception. Clients may map their own result and argument adapters using - xWrap.resultAdapter() and xWrap.argAdaptor(), noting that not all + xWrap.resultAdapter() and xWrap.argAdapter(), noting that not all type conversions are valid for both arguments _and_ result types as they often have different memory ownership requirements. @@ -1451,8 +1824,8 @@ self.WhWasmUtilInstaller = function(target){ - Figure out how/whether we can (semi-)transparently handle pointer-type _output_ arguments. Those currently require explicit handling by allocating pointers, assigning them before - the call using setMemValue(), and fetching them with - getMemValue() after the call. We may be able to automate some + the call using poke(), and fetching them with + peek() after the call. We may be able to automate some or all of that. - Figure out whether it makes sense to extend the arg adapter @@ -1478,37 +1851,56 @@ self.WhWasmUtilInstaller = function(target){ } /*Verify the arg type conversions are valid...*/; if(undefined!==resultType && null!==resultType) __xResultAdapterCheck(resultType); - argTypes.forEach(__xArgAdapterCheck); + for(const t of argTypes){ + if(t instanceof AbstractArgAdapter) xArg.set(t, (...args)=>t.convertArg(...args)); + else __xArgAdapterCheck(t); + } + const cxw = cache.xWrap; if(0===xf.length){ // No args to convert, so we can create a simpler wrapper... return (...args)=>(args.length ? __argcMismatch(fname, xf.length) - : cache.xWrap.convertResult(resultType, xf.call(null))); + : cxw.convertResult(resultType, xf.call(null))); } return function(...args){ if(args.length!==xf.length) __argcMismatch(fname, xf.length); const scope = target.scopedAllocPush(); try{ - const rc = xf.apply(null,args.map((v,i)=>cache.xWrap.convertArg(argTypes[i], v))); - return cache.xWrap.convertResult(resultType, rc); + /* + Maintenance reminder re. arguments passed to convertArgs(): + The public interface of argument adapters is that they take + ONE argument and return a (possibly) converted result for + it. The passing-on of arguments after the first is an + internal impl. detail for the sake of AbstractArgAdapter, and + not to be relied on or documented for other cases. The fact + that this is how AbstractArgAdapter.convertArgs() gets its 2nd+ + arguments, and how FuncPtrAdapter.contextKey() gets its + args, is also an implementation detail and subject to + change. i.e. the public interface of 1 argument is stable. + The fact that any arguments may be passed in after that one, + and what those arguments are, is _not_ part of the public + interface and is _not_ stable. + */ + for(const i in args) args[i] = cxw.convertArg(argTypes[i], args[i], i, args); + return cxw.convertResult(resultType, xf.apply(null,args)); }finally{ target.scopedAllocPop(scope); } }; }/*xWrap()*/; - /** Internal impl for xWrap.resultAdapter() and argAdaptor(). */ + /** Internal impl for xWrap.resultAdapter() and argAdapter(). */ const __xAdapter = function(func, argc, typeName, adapter, modeName, xcvPart){ if('string'===typeof typeName){ - if(1===argc) return xcvPart[typeName]; + if(1===argc) return xcvPart.get(typeName); else if(2===argc){ if(!adapter){ - delete xcvPart[typeName]; + delete xcvPart.get(typeName); return func; }else if(!(adapter instanceof Function)){ toss(modeName,"requires a function argument."); } - xcvPart[typeName] = adapter; + xcvPart.set(typeName, adapter); return func; } } @@ -1545,7 +1937,7 @@ self.WhWasmUtilInstaller = function(target){ */ target.xWrap.resultAdapter = function f(typeName, adapter){ return __xAdapter(f, arguments.length, typeName, adapter, - 'resultAdaptor()', xcv.result); + 'resultAdapter()', xResult); }; /** @@ -1575,9 +1967,11 @@ self.WhWasmUtilInstaller = function(target){ */ target.xWrap.argAdapter = function f(typeName, adapter){ return __xAdapter(f, arguments.length, typeName, adapter, - 'argAdaptor()', xcv.arg); + 'argAdapter()', xArg); }; + target.xWrap.FuncPtrAdapter = xArg.FuncPtrAdapter; + /** Functions like xCall() but performs argument and result type conversions as for xWrap(). The first argument is the name of the @@ -1601,6 +1995,33 @@ self.WhWasmUtilInstaller = function(target){ return target.xWrap(fname, resultType, argTypes||[]).apply(null, args||[]); }; + /** + This function is ONLY exposed in the public API to facilitate + testing. It should not be used in application-level code, only + in test code. + + Expects to be given (typeName, value) and returns a conversion + of that value as has been registered using argAdapter(). + It throws if no adapter is found. + + ACHTUNG: the adapter may require that a scopedAllocPush() is + active and it may allocate memory within that scope. + */ + target.xWrap.testConvertArg = cache.xWrap.convertArg; + /** + This function is ONLY exposed in the public API to facilitate + testing. It should not be used in application-level code, only + in test code. + + Expects to be given (typeName, value) and returns a conversion + of that value as has been registered using resultAdapter(). + It throws if no adapter is found. + + ACHTUNG: the adapter may allocate memory which the caller may need + to know how to free. + */ + target.xWrap.testConvertResult = cache.xWrap.convertResult; + return target; }; diff --git a/ext/wasm/dist.make b/ext/wasm/dist.make index 5aee8af779..8b1a2d4f1c 100644 --- a/ext/wasm/dist.make +++ b/ext/wasm/dist.make @@ -6,43 +6,47 @@ # 'make dist' rules for creating a distribution archive of the WASM/JS # pieces, noting that we only build a dist of the built files, not the # numerous pieces required to build them. +# +# Use 'make snapshot' to create "snapshot" releases. They use a +# distinctly different zip file and top directory name to distinguish +# them from release builds. ####################################################################### MAKEFILE.dist := $(lastword $(MAKEFILE_LIST)) ######################################################################## -# Chicken/egg situation: we need $(bin.version-info) to get the version -# info for the archive name, but that binary may not yet be built, and -# won't be built until we expand the dependencies. We have to use a -# temporary name for the archive. -dist-name = sqlite-wasm-TEMP -#ifeq (0,1) -# $(info WARNING *******************************************************************) -# $(info ** Be sure to create the desired build configuration before creating the) -# $(info ** distribution archive. Use one of the following targets to do so:) -# $(info **) -# $(info ** o2: builds with -O2, resulting in the fastest builds) -# $(info ** oz: builds with -Oz, resulting in the smallest builds) -# $(info /WARNING *******************************************************************) -#endif +# Chicken/egg situation: we need $(bin.version-info) to get the +# version info for the archive name, but that binary may not yet be +# built, and won't be built until we expand the dependencies. Thus we +# have to use a temporary name for the archive until we can get +# that binary built. +ifeq (,$(filter snapshot,$(MAKECMDGOALS))) +dist-name-prefix := sqlite-wasm +else +dist-name-prefix := sqlite-wasm-snapshot-$(shell /usr/bin/date +%Y%m%d) +endif +dist-name := $(dist-name-prefix)-TEMP ######################################################################## -# dist.build must be the name of a target which triggers the -# build of the files to be packed into the dist archive. The -# intention is that it be one of (o0, o1, o2, o3, os, oz), each of -# which uses like-named -Ox optimization level flags. The o2 target -# provides the best overall runtime speeds. The oz target provides -# slightly slower speeds (roughly 10%) with significantly smaller WASM -# file sizes. Note that -O2 (the o2 target) results in faster binaries -# than both -O3 and -Os (the o3 and os targets) in all tests run to -# date. -dist.build ?= oz +# dist.build must be the name of a target which triggers the build of +# the files to be packed into the dist archive. The intention is that +# it be one of (o0, o1, o2, o3, os, oz), each of which uses like-named +# -Ox optimization level flags. The o2 target provides the best +# overall runtime speeds. The oz target provides slightly slower +# speeds (roughly 10%) with significantly smaller WASM file +# sizes. Note that -O2 (the o2 target) results in faster binaries than +# both -O3 and -Os (the o3 and os targets) in all tests run to +# date. Our general policy is that we want the smallest binaries for +# dist zip files, so use the oz build unless there is a compelling +# reason not to. +dist.build ?= qoz dist-dir.top := $(dist-name) dist-dir.jswasm := $(dist-dir.top)/$(notdir $(dir.dout)) dist-dir.common := $(dist-dir.top)/common dist.top.extras := \ demo-123.html demo-123-worker.html demo-123.js \ - tester1.html tester1-worker.html tester1.js \ + tester1.html tester1-worker.html tester1-esm.html \ + tester1.js tester1.mjs \ demo-jsstorage.html demo-jsstorage.js \ demo-worker1.html demo-worker1.js \ demo-worker1-promiser.html demo-worker1-promiser.js @@ -51,7 +55,7 @@ dist.common.extras := \ $(wildcard $(dir.common)/*.css) \ $(dir.common)/SqliteTestUtil.js -.PHONY: dist +.PHONY: dist snapshot ######################################################################## # dist: create the end-user deliverable archive. # @@ -59,7 +63,7 @@ dist.common.extras := \ # $(dist.build) will depend on clean, having any deps on # $(dist-archive) which themselves may be cleaned up by the clean # target will lead to grief in parallel builds (-j #). Thus -# $(dist-target)'s deps must be trimmed to non-generated files or +# dist's deps must be trimmed to non-generated files or # files which are _not_ cleaned up by the clean target. # # Note that we require $(bin.version-info) in order to figure out the @@ -78,10 +82,12 @@ dist: \ @cp -p $(dist.jswasm.extras) $(dist-dir.jswasm) @$(bin.stripccomments) -k -k < $(sqlite3.js) \ > $(dist-dir.jswasm)/$(notdir $(sqlite3.js)) + @$(bin.stripccomments) -k -k < $(sqlite3.mjs) \ + > $(dist-dir.jswasm)/$(notdir $(sqlite3.mjs)) @cp -p $(dist.common.extras) $(dist-dir.common) @set -e; \ vnum=$$($(bin.version-info) --download-version); \ - vdir=sqlite-wasm-$$vnum; \ + vdir=$(dist-name-prefix)-$$vnum; \ arczip=$$vdir.zip; \ echo "Making $$arczip ..."; \ rm -fr $$arczip $$vdir; \ @@ -91,7 +97,17 @@ dist: \ ls -la $$arczip; \ set +e; \ unzip -lv $$arczip || echo "Missing unzip app? Not fatal." - +ifeq (,$(wasm.docs.found)) +snapshot: dist + @echo "To upload the snapshot build to the wasm docs server:"; \ + echo "1) move $(dist-name-prefix)*.zip to the top of a wasm docs checkout."; \ + echo "2) run 'make uv-sync'" +else +snapshot: dist + @echo "Moving snapshot to [$(wasm.docs.found)]..."; \ + mv $(dist-name-prefix)*.zip $(wasm.docs.found)/. + @echo "Run 'make uv-sync' from $(wasm.docs.found) to upload it." +endif # We need a separate `clean` rule to account for weirdness in # a sub-make, where we get a copy of the $(dist-name) dir # copied into the new $(dist-name) dir. diff --git a/ext/wasm/fiddle.make b/ext/wasm/fiddle.make index 43e6941f55..1ef6436c95 100644 --- a/ext/wasm/fiddle.make +++ b/ext/wasm/fiddle.make @@ -39,6 +39,7 @@ fiddle.emcc-flags = \ $(sqlite3.js.flags.--post-js) \ $(emcc.exportedRuntimeMethods) \ -sEXPORTED_FUNCTIONS=@$(abspath $(EXPORTED_FUNCTIONS.fiddle)) \ + -sEXPORTED_RUNTIME_METHODS=FS,wasmMemory \ $(SQLITE_OPT) $(SHELL_OPT) \ -DSQLITE_SHELL_FIDDLE # -D_POSIX_C_SOURCE is needed for strdup() with emcc @@ -58,12 +59,12 @@ fiddle.SOAP.js := $(dir.fiddle)/$(notdir $(SOAP.js)) $(fiddle.SOAP.js): $(SOAP.js) cp $< $@ -$(eval $(call call-make-pre-js,fiddle-module)) +$(eval $(call call-make-pre-js,fiddle-module,vanilla)) $(fiddle-module.js): $(MAKEFILE) $(MAKEFILE.fiddle) \ $(EXPORTED_FUNCTIONS.fiddle) \ - $(fiddle.cses) $(pre-post-fiddle-module.deps) $(fiddle.SOAP.js) + $(fiddle.cses) $(pre-post-fiddle-module.deps.vanilla) $(fiddle.SOAP.js) $(emcc.bin) -o $@ $(fiddle.emcc-flags) \ - $(pre-post-common.flags) $(pre-post-fiddle-module.flags) \ + $(pre-post-fiddle-module.flags.vanilla) \ $(fiddle.cses) $(maybe-wasm-strip) $(fiddle-module.wasm) gzip < $@ > $@.gz diff --git a/ext/wasm/index-dist.html b/ext/wasm/index-dist.html index 6b038c82a9..1c23bd82c7 100644 --- a/ext/wasm/index-dist.html +++ b/ext/wasm/index-dist.html @@ -19,6 +19,10 @@ header { font-size: 130%; font-weight: bold; + background: #044a64; + color: white; + padding: 0.5em; + border-radius: 0.25em; } .hidden, .initially-hidden { position: absolute !important; @@ -56,6 +60,16 @@ utility code.
  • tester1-worker: same thing but running in a Worker.
  • +
  • tester1-esm: same as + tester1 but loads sqlite3 in the main thread via + an ES6 module. +
  • +
  • tester1-worker?esm: + same as tester1-esm but loads a Worker Module which + then loads the sqlite3 API via an ES6 module. Note that + not all browsers permit loading modules in Worker + threads. +
  • Higher-level apps and demos... diff --git a/ext/wasm/index.html b/ext/wasm/index.html index 044cd1360d..767cc5e74a 100644 --- a/ext/wasm/index.html +++ b/ext/wasm/index.html @@ -8,6 +8,14 @@ sqlite3 WASM Testing Page Index +
    sqlite3 WASM test pages

    Below is the list of test pages for the sqlite3 WASM @@ -30,11 +38,7 @@ Chrome or Chromium (v102 at least, possibly newer). OPFS support in the other major browsers is pending. Development and testing is currently done against a dev-channel release - of Chrome (v107 as of 2022-09-26). -
  • -
  • Whether or not WASMFS/OPFS support is enabled on any given - page may depend on build-time options which are off by - default. + of Chrome (v110 as of 2022-12-02).
  • @@ -46,7 +50,17 @@ regression tests for the various APIs and surrounding utility code.
  • tester1-worker: same thing - but running in a Worker.
  • + but running in a Worker. +
  • tester1-esm: same as + tester1 but loads sqlite3 in the main thread via + an ES6 module. +
  • +
  • tester1-worker?esm: + same as tester1-esm but loads a Worker Module which + then loads the sqlite3 API via an ES6 module. Note that + not all browsers permit loading modules in Worker + threads. +
  • High-level apps and demos... @@ -72,11 +86,9 @@
  • speedtest1 ports (sqlite3's primary benchmarking tool)...
  • @@ -86,10 +98,6 @@ a high-level overview of the symbols exposed by the JS module.
  • batch-runner: runs batches of SQL exported from speedtest1.
  • -
  • test-opfs-vfs (same with verbose output and sanity-checking tests) is an @@ -98,8 +106,23 @@ synchronous sqlite3_vfs interface and the async OPFS impl.
  • +
  • OPFS concurrency + tests using multiple workers. +
  • + diff --git a/ext/wasm/jaccwabyt/jaccwabyt.js b/ext/wasm/jaccwabyt/jaccwabyt.js index dee7258c51..d4ec719fb5 100644 --- a/ext/wasm/jaccwabyt/jaccwabyt.js +++ b/ext/wasm/jaccwabyt/jaccwabyt.js @@ -10,9 +10,12 @@ *********************************************************************** - The Jaccwabyt API is documented in detail in an external file. + The Jaccwabyt API is documented in detail in an external file, + _possibly_ called jaccwabyt.md in the same directory as this file. - Project home: https://fossil.wanderinghorse.net/r/jaccwabyt + Project homes: + - https://fossil.wanderinghorse.net/r/jaccwabyt + - https://sqlite.org/src/dir/ext/wasm/jaccwabyt */ 'use strict'; @@ -61,7 +64,6 @@ self.Jaccwabyt = function StructBinderFactory(config){ BigInt = self['BigInt'], BigInt64Array = self['BigInt64Array'], /* Undocumented (on purpose) config options: */ - functionTable = config.functionTable/*EXPERIMENTAL, undocumented*/, ptrSizeof = config.ptrSizeof || 4, ptrIR = config.ptrIR || 'i32' ; @@ -121,6 +123,7 @@ self.Jaccwabyt = function StructBinderFactory(config){ at SIG s[0]. Throws for an unknown SIG. */ const sigIR = function(s){ switch(sigLetter(s)){ + case 'c': case 'C': return 'i8'; case 'i': return 'i32'; case 'p': case 'P': case 's': return ptrIR; case 'j': return 'i64'; @@ -129,33 +132,9 @@ self.Jaccwabyt = function StructBinderFactory(config){ } toss("Unhandled signature IR:",s); }; - /** Returns the sizeof value for the given SIG. Throws for an - unknown SIG. */ - const sigSizeof = function(s){ - switch(sigLetter(s)){ - case 'i': return 4; - case 'p': case 'P': case 's': return ptrSizeof; - case 'j': return 8; - case 'f': return 4 /* C-side floats, not JS-side */; - case 'd': return 8; - } - toss("Unhandled signature sizeof:",s); - }; + const affirmBigIntArray = BigInt64Array ? ()=>true : ()=>toss('BigInt64Array is not available.'); - /** Returns the (signed) TypedArray associated with the type - described by the given SIG. Throws for an unknown SIG. */ - /********** - const sigTypedArray = function(s){ - switch(sigIR(s)) { - case 'i32': return Int32Array; - case 'i64': return affirmBigIntArray() && BigInt64Array; - case 'float': return Float32Array; - case 'double': return Float64Array; - } - toss("Unhandled signature TypedArray:",s); - }; - **************/ /** Returns the name of a DataView getter method corresponding to the given SIG. */ const sigDVGetter = function(s){ @@ -168,6 +147,8 @@ self.Jaccwabyt = function StructBinderFactory(config){ break; } case 'i': return 'getInt32'; + case 'c': return 'getInt8'; + case 'C': return 'getUint8'; case 'j': return affirmBigIntArray() && 'getBigInt64'; case 'f': return 'getFloat32'; case 'd': return 'getFloat64'; @@ -186,6 +167,8 @@ self.Jaccwabyt = function StructBinderFactory(config){ break; } case 'i': return 'setInt32'; + case 'c': return 'setInt8'; + case 'C': return 'setUint8'; case 'j': return affirmBigIntArray() && 'setBigInt64'; case 'f': return 'setFloat32'; case 'd': return 'setFloat64'; @@ -199,7 +182,7 @@ self.Jaccwabyt = function StructBinderFactory(config){ */ const sigDVSetWrapper = function(s){ switch(sigLetter(s)) { - case 'i': case 'f': case 'd': return Number; + case 'i': case 'f': case 'c': case 'C': case 'd': return Number; case 'j': return affirmBigIntArray() && BigInt; case 'p': case 'P': case 's': switch(ptrSizeof){ @@ -211,36 +194,14 @@ self.Jaccwabyt = function StructBinderFactory(config){ toss("Unhandled DataView set wrapper for signature:",s); }; + /** Returns the given struct and member name in a form suitable for + debugging and error output. */ const sPropName = (s,k)=>s+'::'+k; const __propThrowOnSet = function(structName,propName){ return ()=>toss(sPropName(structName,propName),"is read-only."); }; - /** - When C code passes a pointer of a bound struct to back into - a JS function via a function pointer struct member, it - arrives in JS as a number (pointer). - StructType.instanceForPointer(ptr) can be used to get the - instance associated with that pointer, and __ptrBacklinks - holds that mapping. WeakMap keys must be objects, so we - cannot use a weak map to map pointers to instances. We use - the StructType constructor as the WeakMap key, mapped to a - plain, prototype-less Object which maps the pointers to - struct instances. That arrangement gives us a - per-StructType type-safe way to resolve pointers. - */ - const __ptrBacklinks = new WeakMap(); - /** - Similar to __ptrBacklinks but is scoped at the StructBinder - level and holds pointer-to-object mappings for all struct - instances created by any struct from any StructFactory - which this specific StructBinder has created. The intention - of this is to help implement more transparent handling of - pointer-type property resolution. - */ - const __ptrBacklinksGlobal = Object.create(null); - /** In order to completely hide StructBinder-bound struct pointers from JS code, we store them in a scope-local @@ -261,17 +222,13 @@ self.Jaccwabyt = function StructBinderFactory(config){ const __freeStruct = function(ctor, obj, m){ if(!m) m = __instancePointerMap.get(obj); if(m) { - if(obj.ondispose instanceof Function){ - try{obj.ondispose()} - catch(e){ - /*do not rethrow: destructors must not throw*/ - console.warn("ondispose() for",ctor.structName,'@', - m,'threw. NOT propagating it.',e); - } - }else if(Array.isArray(obj.ondispose)){ - obj.ondispose.forEach(function(x){ + __instancePointerMap.delete(obj); + if(Array.isArray(obj.ondispose)){ + let x; + while((x = obj.ondispose.shift())){ try{ if(x instanceof Function) x.call(obj); + else if(x instanceof StructType) x.dispose(); else if('number' === typeof x) dealloc(x); // else ignore. Strings are permitted to annotate entries // to assist in debugging. @@ -279,12 +236,16 @@ self.Jaccwabyt = function StructBinderFactory(config){ console.warn("ondispose() for",ctor.structName,'@', m,'threw. NOT propagating it.',e); } - }); + } + }else if(obj.ondispose instanceof Function){ + try{obj.ondispose()} + catch(e){ + /*do not rethrow: destructors must not throw*/ + console.warn("ondispose() for",ctor.structName,'@', + m,'threw. NOT propagating it.',e); + } } delete obj.ondispose; - delete __ptrBacklinks.get(ctor)[m]; - delete __ptrBacklinksGlobal[m]; - __instancePointerMap.delete(obj); if(ctor.debugFlags.__flags.dealloc){ log("debug.dealloc:",(obj[xPtrPropName]?"EXTERNAL":""), ctor.structName,"instance:", @@ -300,7 +261,7 @@ self.Jaccwabyt = function StructBinderFactory(config){ iterable: false, value: v}}; /** Allocates obj's memory buffer based on the size defined in - DEF.sizeof. */ + ctor.structInfo.sizeof. */ const __allocStruct = function(ctor, obj, m){ let fill = !m; if(m) Object.defineProperty(obj, xPtrPropName, rop(m)); @@ -316,8 +277,6 @@ self.Jaccwabyt = function StructBinderFactory(config){ } if(fill) heap().fill(0, m, m + ctor.structInfo.sizeof); __instancePointerMap.set(obj, m); - __ptrBacklinks.get(ctor)[m] = obj; - __ptrBacklinksGlobal[m] = obj; }catch(e){ __freeStruct(ctor, obj, m); throw e; @@ -339,7 +298,7 @@ self.Jaccwabyt = function StructBinderFactory(config){ if tossIfNotFound is true, else returns undefined if not found. The given name may be either the name of the structInfo.members key (faster) or the key as modified by the - memberPrefix/memberSuffix settings. + memberPrefix and memberSuffix settings. */ const __lookupMember = function(structInfo, memberName, tossIfNotFound=true){ let m = structInfo.members[memberName]; @@ -361,21 +320,11 @@ self.Jaccwabyt = function StructBinderFactory(config){ framework's native format or in Emscripten format. */ const __memberSignature = function f(obj,memberName,emscriptenFormat=false){ - if(!f._) f._ = (x)=>x.replace(/[^vipPsjrd]/g,"").replace(/[pPs]/g,'i'); + if(!f._) f._ = (x)=>x.replace(/[^vipPsjrdcC]/g,"").replace(/[pPscC]/g,'i'); const m = __lookupMember(obj.structInfo, memberName, true); return emscriptenFormat ? f._(m.signature) : m.signature; }; - /** - Returns the instanceForPointer() impl for the given - StructType constructor. - */ - const __instanceBacklinkFactory = function(ctor){ - const b = Object.create(null); - __ptrBacklinks.set(ctor, b); - return (ptr)=>b[ptr]; - }; - const __ptrPropDescriptor = { configurable: false, enumerable: false, get: function(){return __instancePointerMap.get(this)}, @@ -388,7 +337,9 @@ self.Jaccwabyt = function StructBinderFactory(config){ /** Impl of X.memberKeys() for StructType and struct ctors. */ const __structMemberKeys = rop(function(){ const a = []; - Object.keys(this.structInfo.members).forEach((k)=>a.push(this.memberKey(k))); + for(const k of Object.keys(this.structInfo.members)){ + a.push(this.memberKey(k)); + } return a; }); @@ -454,15 +405,15 @@ self.Jaccwabyt = function StructBinderFactory(config){ Adds value v to obj.ondispose, creating ondispose, or converting it to an array, if needed. */ - const __addOnDispose = function(obj, v){ + const __addOnDispose = function(obj, ...v){ if(obj.ondispose){ - if(obj.ondispose instanceof Function){ + if(!Array.isArray(obj.ondispose)){ obj.ondispose = [obj.ondispose]; - }/*else assume it's an array*/ + } }else{ obj.ondispose = []; } - obj.ondispose.push(v); + obj.ondispose.push(...v); }; /** @@ -477,8 +428,9 @@ self.Jaccwabyt = function StructBinderFactory(config){ const mem = alloc(u.length+1); if(!mem) toss("Allocation error while duplicating string:",str); const h = heap(); - let i = 0; - for( ; i < u.length; ++i ) h[mem + i] = u[i]; + //let i = 0; + //for( ; i < u.length; ++i ) h[mem + i] = u[i]; + h.set(u, mem); h[mem + u.length] = 0; //log("allocCString @",mem," =",u); return mem; @@ -490,6 +442,10 @@ self.Jaccwabyt = function StructBinderFactory(config){ to free any prior memory, if appropriate. The newly-allocated string is added to obj.ondispose so will be freed when the object is disposed. + + The given name may be either the name of the structInfo.members + key (faster) or the key as modified by the memberPrefix and + memberSuffix settings. */ const __setMemberCString = function(obj, memberName, str){ const m = __lookupMember(obj.structInfo, memberName, true); @@ -544,13 +500,19 @@ self.Jaccwabyt = function StructBinderFactory(config){ return __setMemberCString(this, memberName, str); }) }); + // Function-type non-Property inherited members + Object.assign(StructType.prototype,{ + addOnDispose: function(...v){ + __addOnDispose(this,...v); + return this; + } + }); /** "Static" properties for StructType. */ Object.defineProperties(StructType, { allocCString: rop(__allocCString), - instanceForPointer: rop((ptr)=>__ptrBacklinksGlobal[ptr]), isA: rop((v)=>v instanceof StructType), hasExternalPointer: rop((v)=>(v instanceof StructType) && !!v[xPtrPropName]), memberKey: __memberKeyProp @@ -570,7 +532,7 @@ self.Jaccwabyt = function StructBinderFactory(config){ /*cache all available getters/setters/set-wrappers for direct reuse in each accessor function. */ f._ = {getters: {}, setters: {}, sw:{}}; - const a = ['i','p','P','s','f','d','v()']; + const a = ['i','c','C','p','P','s','f','d','v()']; if(bigIntEnabled) a.push('j'); a.forEach(function(v){ //const ir = sigIR(v); @@ -579,8 +541,8 @@ self.Jaccwabyt = function StructBinderFactory(config){ f._.sw[v] = sigDVSetWrapper(v) /* BigInt or Number ctor to wrap around values for conversion */; }); - const rxSig1 = /^[ipPsjfd]$/, - rxSig2 = /^[vipPsjfd]\([ipPsjfd]*\)$/; + const rxSig1 = /^[ipPsjfdcC]$/, + rxSig2 = /^[vipPsjfdcC]\([ipPsjfdcC]*\)$/; f.sigCheck = function(obj, name, key,sig){ if(Object.prototype.hasOwnProperty.call(obj, key)){ toss(obj.structName,'already has a property named',key+'.'); @@ -594,7 +556,6 @@ self.Jaccwabyt = function StructBinderFactory(config){ f.sigCheck(ctor.prototype, name, key, descr.signature); descr.key = key; descr.name = name; - const sizeOf = sigSizeof(descr.signature); const sigGlyph = sigLetter(descr.signature); const xPropName = sPropName(ctor.prototype.structName,key); const dbg = ctor.prototype.debugFlags.__flags; @@ -610,16 +571,12 @@ self.Jaccwabyt = function StructBinderFactory(config){ prop.get = function(){ if(dbg.getter){ log("debug.getter:",f._.getters[sigGlyph],"for", sigIR(sigGlyph), - xPropName,'@', this.pointer,'+',descr.offset,'sz',sizeOf); + xPropName,'@', this.pointer,'+',descr.offset,'sz',descr.sizeof); } let rc = ( - new DataView(heap().buffer, this.pointer + descr.offset, sizeOf) + new DataView(heap().buffer, this.pointer + descr.offset, descr.sizeof) )[f._.getters[sigGlyph]](0, isLittleEndian); if(dbg.getter) log("debug.getter:",xPropName,"result =",rc); - if(rc && isAutoPtrSig(descr.signature)){ - rc = StructType.instanceForPointer(rc) || rc; - if(dbg.getter) log("debug.getter:",xPropName,"resolved =",rc); - } return rc; }; if(descr.readOnly){ @@ -628,7 +585,7 @@ self.Jaccwabyt = function StructBinderFactory(config){ prop.set = function(v){ if(dbg.setter){ log("debug.setter:",f._.setters[sigGlyph],"for", sigIR(sigGlyph), - xPropName,'@', this.pointer,'+',descr.offset,'sz',sizeOf, v); + xPropName,'@', this.pointer,'+',descr.offset,'sz',descr.sizeof, v); } if(!this.pointer){ toss("Cannot set struct property on disposed instance."); @@ -644,7 +601,7 @@ self.Jaccwabyt = function StructBinderFactory(config){ toss("Invalid value for pointer-type",xPropName+'.'); } ( - new DataView(heap().buffer, this.pointer + descr.offset, sizeOf) + new DataView(heap().buffer, this.pointer + descr.offset, descr.sizeof) )[f._.setters[sigGlyph]](0, f._.sw[sigGlyph](v), isLittleEndian); }; } @@ -665,13 +622,25 @@ self.Jaccwabyt = function StructBinderFactory(config){ if(!structName) toss("Struct name is required."); let lastMember = false; Object.keys(structInfo.members).forEach((k)=>{ + // Sanity checks of sizeof/offset info... const m = structInfo.members[k]; if(!m.sizeof) toss(structName,"member",k,"is missing sizeof."); - else if(0!==(m.sizeof%4)){ - toss(structName,"member",k,"sizeof is not aligned."); - } - else if(0!==(m.offset%4)){ - toss(structName,"member",k,"offset is not aligned."); + else if(m.sizeof===1){ + (m.signature === 'c' || m.signature === 'C') || + toss("Unexpected sizeof==1 member", + sPropName(structInfo.name,k), + "with signature",m.signature); + }else{ + // sizes and offsets of size-1 members may be odd values, but + // others may not. + if(0!==(m.sizeof%4)){ + console.warn("Invalid struct member description =",m,"from",structInfo); + toss(structName,"member",k,"sizeof is not aligned. sizeof="+m.sizeof); + } + if(0!==(m.offset%4)){ + console.warn("Invalid struct member description =",m,"from",structInfo); + toss(structName,"member",k,"offset is not aligned. offset="+m.offset); + } } if(!lastMember || lastMember.offset < m.offset) lastMember = m; }); @@ -697,27 +666,9 @@ self.Jaccwabyt = function StructBinderFactory(config){ }; Object.defineProperties(StructCtor,{ debugFlags: debugFlags, - disposeAll: rop(function(){ - const map = __ptrBacklinks.get(StructCtor); - Object.keys(map).forEach(function(ptr){ - const b = map[ptr]; - if(b) __freeStruct(StructCtor, b, ptr); - }); - __ptrBacklinks.set(StructCtor, Object.create(null)); - return StructCtor; - }), - instanceForPointer: rop(__instanceBacklinkFactory(StructCtor)), isA: rop((v)=>v instanceof StructCtor), memberKey: __memberKeyProp, memberKeys: __structMemberKeys, - resolveToInstance: rop(function(v, throwIfNot=false){ - if(!(v instanceof StructCtor)){ - v = Number.isSafeInteger(v) - ? StructCtor.instanceForPointer(v) : undefined; - } - if(!v && throwIfNot) toss("Value is-not-a",StructCtor.structName); - return v; - }), methodInfoForKey: rop(function(mKey){ }), structInfo: rop(structInfo), @@ -735,7 +686,6 @@ self.Jaccwabyt = function StructBinderFactory(config){ ); return StructCtor; }; - StructBinder.instanceForPointer = StructType.instanceForPointer; StructBinder.StructType = StructType; StructBinder.config = config; StructBinder.allocCString = __allocCString; diff --git a/ext/wasm/jaccwabyt/jaccwabyt.md b/ext/wasm/jaccwabyt/jaccwabyt.md index edcba260a7..dd80ed1c68 100644 --- a/ext/wasm/jaccwabyt/jaccwabyt.md +++ b/ext/wasm/jaccwabyt/jaccwabyt.md @@ -4,7 +4,6 @@ Jaccwabyt 🐇 **Jaccwabyt**: _JavaScript ⇄ C Struct Communication via WASM Byte Arrays_ - Welcome to Jaccwabyt, a JavaScript API which creates bindings for WASM-compiled C structs, defining them in such a way that changes to their state in JS are visible in C/WASM, and vice versa, permitting @@ -27,8 +26,28 @@ are based solely on feature compatibility tables provided at **Formalities:** - Author: [Stephan Beal][sgb] -- License: Public Domain -- Project Home: +- Project Homes: + - \ + Is the primary home but... + - \ + ... most development happens here. + +The license for both this documentation and the software it documents +is the same as [sqlite3][], the project from which this spinoff +project was spawned: + +----- + +> 2022-06-30: +> +> The author disclaims copyright to this source code. In place of a +> legal notice, here is a blessing: +> +> May you do good and not evil. +> May you find forgiveness for yourself and forgive others. +> May you share freely, never taking more than you give. + +----- Table of Contents @@ -205,7 +224,6 @@ simply look like: The StructBinder factory function returns a function which can then be used to create bindings for our structs. - Step 2: Create a Struct Description ------------------------------------------------------------ @@ -281,21 +299,29 @@ supported letters are: signature entry. - **`f`** = `float` (4 bytes) - **`d`** = `double` (8 bytes) -- **`p`** = `int32` (but see below!) +- **`c`** = `int8` (1 byte) char - see notes below! +- **`C`** = `uint8` (1 byte) unsigned char - see notes below! +- **`p`** = `int32` (see notes below!) - **`P`** = Like `p` but with extra handling. Described below. -- **`s`** = like `int32` but is a _hint_ that it's a pointer to a string - so that _some_ (very limited) contexts may treat it as such, noting - such algorithms must, for lack of information to the contrary, - assume both that the encoding is UTF-8 and that the pointer's member - is NUL-terminated. If that is _not_ the case for a given string - member, do not use `s`: use `i` or `p` instead and do any string - handling yourself. +- **`s`** = like `int32` but is a _hint_ that it's a pointer to a + string so that _some_ (very limited) contexts may treat it as such, + noting that such algorithms must, for lack of information to the + contrary, assume both that the encoding is UTF-8 and that the + pointer's member is NUL-terminated. If that is _not_ the case for a + given string member, do not use `s`: use `i` or `p` instead and do + any string handling yourself. Noting that: -- All of these types are numeric. Attempting to set any struct-bound - property to a non-numeric value will trigger an exception except in - cases explicitly noted otherwise. +- **All of these types are numeric**. Attempting to set any + struct-bound property to a non-numeric value will trigger an + exception except in cases explicitly noted otherwise. +- **"Char" types**: WASM does not define an `int8` type, nor does it + distinguish between signed and unsigned. This API treats `c` as + `int8` and `C` as `uint8` for purposes of getting and setting values + when using the `DataView` class. It is _not_ recommended that client + code use these types in new WASM-capable code, but they were added + for the sake of binding some immutable legacy code to WASM. > Sidebar: Emscripten's public docs do not mention `p`, but their generated code includes `p` as an alias for `i`, presumably to mean @@ -317,12 +343,12 @@ Signatures in the form `x(...)` denote function-pointer members and form `x()`. For function-type signatures, the strings are formulated such that they can be passed to Emscripten's `addFunction()` after stripping out the `(` and `)` characters. For good measure, to match -the public Emscripten docs, `p` should also be replaced with `i`. In -JavaScript that might look like: +the public Emscripten docs, `p`, `c`, and `C`, should also be replaced +with `i`. In JavaScript that might look like: > ``` -signature.replace(/[^vipPsjfd]/g,'').replace(/[pPs]/g,'i'); +signature.replace(/[^vipPsjfdcC]/g,'').replace(/[pPscC]/g,'i'); ``` @@ -337,12 +363,6 @@ special use of unsigned numbers). A capital `P` changes the semantics of plain member pointers (but not, as of this writing, function pointer members) as follows: -- When a `P`-type member is **fetched** via `myStruct.x` and its value is - a non-0 integer, [`StructBinder.instanceForPointer()`][StructBinder] - is used to try to map that pointer to a struct instance. If a match - is found, the "get" operation returns that instance instead of the - integer. If no match is found, it behaves exactly as for `p`, returning - the integer value. - When a `P`-type member is **set** via `myStruct.x=y`, if [`(y instanceof StructType)`][StructType] then the value of `y.pointer` is stored in `myStruct.x`. If `y` is neither a number nor @@ -388,14 +408,11 @@ It is important to understand that creating a new instance allocates memory on the WASM heap. We must not simply rely on garbage collection to clean up the instances because doing so will not free up the WASM heap memory. The correct way to free up that memory is to use the -object's `dispose()` method. Alternately, there is a "nuclear option": -`MyBinder.disposeAll()` will free the memory allocated for _all_ -instances which have not been manually disposed. +object's `dispose()` method. The following usage pattern offers one way to easily ensure proper cleanup of struct instances: - > ```javascript const my = new MyStruct(); @@ -409,11 +426,6 @@ try { from the byte array. */ // Pass the struct to C code which takes a MyStruct pointer: aCFunction( my.pointer ); - // Type-safely check if a pointer returned from C is a MyStruct: - const x = MyStruct.instanceForPointer( anotherCFunction() ); - // If it is a MyStruct, x now refers to that object. Note, however, - // that this only works for instances created in JS, as the - // pointer mapping only exists in JS space. } finally { my.dispose(); } @@ -426,6 +438,16 @@ to use `try`/`finally` without a `catch`, and doing so is an ideal match for the memory management requirements of Jaccwaby-bound struct instances. +It is often useful to wrap an existing instance of a C-side struct +without taking over ownership of its memory. That can be achieved by +simply passing a pointer to the constructor. For example: + +```js +const m = new MyStruct( functionReturningASharedPtr() ); +// calling m.dispose() will _not_ free the wrapped C-side instance +// but will trigger any ondispose handler. +``` + Now that we have struct instances, there are a number of things we can do with them, as covered in the rest of this document. @@ -549,20 +571,6 @@ The Struct Binder has the following members: any of its "significant" configuration values may have undefined results. -- `instanceForPointer(pointer)` - Given a pointer value relative to `config.memory`, if that pointer - resolves to a struct of _any type_ generated via the same Struct - Binder, this returns the struct instance associated with it, or - `undefined` if no struct object is mapped to that pointer. This - differs from the struct-type-specific member of the same name in - that this one is not "type-safe": it does not know the type of the - returned object (if any) and may return a struct of any - [StructType][] for which this Struct Binder has created a - constructor. It cannot return instances created via a different - [StructBinderFactory][] because each factory can hypothetically have - a different memory heap. - - API: Struct Type ------------------------------------------------------------ @@ -582,6 +590,14 @@ only called by the [StructBinder][]-generated has the following "static" properties (^Which are accessible from individual instances via `theInstance.constructor`.): +- `addOnDispose(...value)`\ + If this object has no `ondispose` property, this function creates it + as an array and pushes the given value(s) onto it. If the object has + a function-typed `ondispose` property, this call replaces it with an + array and moves that function into the array. In all other cases, + `ondispose` is assumed to be an array and the argument(s) is/are + appended to it. Returns `this`. + - `allocCString(str)` Identical to the [StructBinder][] method of the same name. @@ -591,9 +607,6 @@ individual instances via `theInstance.constructor`.): [struct's constructor][StructCtors]. If true, the memory is owned by someone other than the object and must outlive the object. -- `instanceForPointer(pointer)` - Works identically to the [StructBinder][] method of the same name. - - `isA(value)` Returns true if its argument is a StructType instance _from the same [StructBinder][]_ as this StructType. @@ -619,14 +632,15 @@ legally be called on concrete struct instances unless noted otherwise: - If it is a function, it is called with the struct object as its `this`. That method must not throw - if it does, the exception will be ignored. - - If it is an array, it may contain functions, pointers, and/or JS - strings. If an entry is a function, it is called as described - above. If it's a number, it's assumed to be a pointer and is - passed to the `dealloc()` function configured for the parent - [StructBinder][]. If it's a JS string, it's assumed to be a - helpful description of the next entry in the list and is simply - ignored. Strings are supported primarily for use as debugging - information. + - If it is an array, it may contain functions, pointers, other + [StructType] instances, and/or JS strings. If an entry is a + function, it is called as described above. If it's a number, it's + assumed to be a pointer and is passed to the `dealloc()` function + configured for the parent [StructBinder][]. If it's a + [StructType][] instance then its `dispose()` method is called. If + it's a JS string, it's assumed to be a helpful description of the + next entry in the list and is simply ignored. Strings are + supported primarily for use as debugging information. - Some struct APIs will manipulate the `ondispose` member, creating it as an array or converting it from a function to array as needed. @@ -738,21 +752,6 @@ pointer can be taken over using something like These constructors have the following "static" members: -- `disposeAll()` - For each instance of this struct, the equivalent of its `dispose()` - method is called. This frees all WASM-allocated memory associated - with _all_ instances and clears the `instanceForPointer()` - mappings. Returns `this`. - -- `instanceForPointer(pointer)` - Given a pointer value (accessible via the `pointer` property of all - struct instances) which ostensibly refers to an instance of this - class, this returns the instance associated with it, or `undefined` - if no object _of this specific struct type_ is mapped to that - pointer. When C-side code calls back into JS code and passes a - pointer to an object, this function can be used to type-safely - "cast" that pointer back to its original object. - - `isA(value)` Returns true if its argument was created by this constructor. @@ -762,15 +761,6 @@ These constructors have the following "static" members: - `memberKeys(string)` Works exactly as documented for [StructType][]. -- `resolveToInstance(value [,throwIfNot=false])` - Works like `instanceForPointer()` but accepts either an instance - of this struct type or a pointer which resolves to one. - It returns an instance of this struct type on success. - By default it returns a falsy value if its argument is not, - or does not resolve to, an instance of this struct type, - but if passed a truthy second argument then it will throw - instead. - - `structInfo` The structure description passed to [StructBinder][] when this constructor was generated. diff --git a/ext/wasm/module-symbols.html b/ext/wasm/module-symbols.html index 427d2dc8c3..6298aeb374 100644 --- a/ext/wasm/module-symbols.html +++ b/ext/wasm/module-symbols.html @@ -92,9 +92,8 @@
  • This page runs in the main UI thread so cannot see features which are only available in a Worker thread. If this page were to function via a Worker, it would not be able to see - functionality only available in the main thread. Starting a - Worker here to fetch those symbols requires loading a second - copy of the sqlite3 WASM module and JS code. + functionality only available in the main thread. Either way, it + would be missing certain APIs.
  • @@ -193,6 +192,178 @@ fossil-doc block so that this part can work without modification in the wasm docs repo. */ +//#if target=es6-module + +//#else +//#endif diff --git a/ext/wasm/tester1.js b/ext/wasm/tester1.c-pp.js similarity index 52% rename from ext/wasm/tester1.js rename to ext/wasm/tester1.c-pp.js index 99fb5b3184..bef34a0ba5 100644 --- a/ext/wasm/tester1.js +++ b/ext/wasm/tester1.c-pp.js @@ -22,15 +22,34 @@ groups and individual tests can be assigned a predicate function which determines whether to run them or not, and this is specifically intended to be used to toggle certain tests on or off - for the main/worker threads. + for the main/worker threads or the availability (or not) of + optional features such as int64 support. - Each test group defines a state object which gets applied as each - test function's `this`. Test functions can use that to, e.g., set up - a db in an early test and close it in a later test. Each test gets - passed the sqlite3 namespace object as its only argument. + Each test group defines a single state object which gets applied as + the test functions' `this` for all tests in that group. Test + functions can use that to, e.g., set up a db in an early test and + close it in a later test. Each test gets passed the sqlite3 + namespace object as its only argument. */ +/* + This file is intended to be processed by c-pp to inject (or not) + code specific to ES6 modules which is illegal in non-module code. + + Non-ES6 module build and ES6 module for the main-thread: + + ./c-pp -f tester1.c-pp.js -o tester1.js + + ES6 worker module build: + + ./c-pp -f tester1.c-pp.js -o tester1-esm.js -Dtarget=es6-module +*/ +//#if target=es6-module +import {default as sqlite3InitModule} from './jswasm/sqlite3.mjs'; +self.sqlite3InitModule = sqlite3InitModule; +//#else 'use strict'; -(function(){ +//#endif +(function(self){ /** Set up our output channel differently depending on whether we are running in a worker thread or @@ -101,8 +120,10 @@ } const reportFinalTestStatus = function(pass){ if(isUIThread()){ - const e = document.querySelector('#color-target'); + let e = document.querySelector('#color-target'); e.classList.add(pass ? 'tests-pass' : 'tests-fail'); + e = document.querySelector('title'); + e.innerText = (pass ? 'PASS' : 'FAIL') + ': ' + e.innerText; }else{ postMessage({type:'test-result', payload:{pass}}); } @@ -229,10 +250,13 @@ log(TestUtil.separator); logClass('group-start',"Group #"+this.number+':',this.name); const indent = ' '; - if(this.predicate && !this.predicate(sqlite3)){ - logClass('warning',indent, - "SKIPPING group because predicate says to."); - return; + if(this.predicate){ + const p = this.predicate(sqlite3); + if(!p || 'string'===typeof p){ + logClass('warning',indent, + "SKIPPING group:", p ? p : "predicate says to" ); + return; + } } const assertCount = TestUtil.counter; const groupState = Object.create(null); @@ -242,24 +266,27 @@ ++i; const n = this.number+"."+i; log(indent, n+":", t.name); - if(t.predicate && !t.predicate(sqlite3)){ - logClass('warning', indent, indent, - 'SKIPPING because predicate says to'); - skipped.push( n+': '+t.name ); - }else{ - const tc = TestUtil.counter, now = performance.now(); - await t.test.call(groupState, sqlite3); - const then = performance.now(); - runtime += then - now; - logClass('faded',indent, indent, - TestUtil.counter - tc, 'assertion(s) in', - roundMs(then-now),'ms'); + if(t.predicate){ + const p = t.predicate(sqlite3); + if(!p || 'string'===typeof p){ + logClass('warning',indent, + "SKIPPING:", p ? p : "predicate says to" ); + skipped.push( n+': '+t.name ); + continue; + } } + const tc = TestUtil.counter, now = performance.now(); + await t.test.call(groupState, sqlite3); + const then = performance.now(); + runtime += then - now; + logClass('faded',indent, indent, + TestUtil.counter - tc, 'assertion(s) in', + roundMs(then-now),'ms'); } logClass('green', "Group #"+this.number+":",(TestUtil.counter - assertCount), "assertion(s) in",roundMs(runtime),"ms"); - if(skipped.length){ + if(0 && skipped.length){ logClass('warning',"SKIPPED test(s) in group",this.number+":",skipped); } } @@ -276,14 +303,12 @@ addTest: function(name, callback){ let predicate; if(1===arguments.length){ - const opt = arguments[0]; - predicate = opt.predicate; - name = opt.name; - callback = opt.test; + this.currentTestGroup.addTest(arguments[0]); + }else{ + this.currentTestGroup.addTest({ + name, predicate, test: callback + }); } - this.currentTestGroup.addTest({ - name, predicate, test: callback - }); return this; }, runTests: async function(sqlite3){ @@ -319,6 +344,55 @@ //////////////////////////////////////////////////////////////////// T.g('Basic sanity checks') + .t({ + name:'sqlite3_config()', + test:function(sqlite3){ + for(const k of [ + 'SQLITE_CONFIG_GETMALLOC', 'SQLITE_CONFIG_URI' + ]){ + T.assert(capi[k] > 0); + } + T.assert(capi.SQLITE_MISUSE===capi.sqlite3_config( + capi.SQLITE_CONFIG_URI, 1 + ), "MISUSE because the library has already been initialized."); + T.assert(capi.SQLITE_MISUSE === capi.sqlite3_config( + // not enough args + capi.SQLITE_CONFIG_GETMALLOC + )); + T.assert(capi.SQLITE_NOTFOUND === capi.sqlite3_config( + // unhandled-in-JS config option + capi.SQLITE_CONFIG_GETMALLOC, 1 + )); + if(0){ + log("We cannot _fully_ test sqlite3_config() after the library", + "has been initialized (which it necessarily has been to", + "set up various bindings) and we cannot shut it down ", + "without losing the VFS registrations."); + T.assert(0 === capi.sqlite3_config( + capi.SQLITE_CONFIG_URI, 1 + )); + } + } + })/*sqlite3_config()*/ + + //////////////////////////////////////////////////////////////////// + .t({ + name: "JS wasm-side allocator", + test: function(sqlite3){ + if(sqlite3.config.useStdAlloc){ + warn("Using system allocator. This violates the docs and", + "may cause grief with certain APIs", + "(e.g. sqlite3_deserialize())."); + T.assert(wasm.alloc.impl === wasm.exports.malloc) + .assert(wasm.dealloc === wasm.exports.free) + .assert(wasm.realloc.impl === wasm.exports.realloc); + }else{ + T.assert(wasm.alloc.impl === wasm.exports.sqlite3_malloc) + .assert(wasm.dealloc === wasm.exports.sqlite3_free) + .assert(wasm.realloc.impl === wasm.exports.sqlite3_realloc); + } + } + }) .t('Namespace object checks', function(sqlite3){ const wasmCtypes = wasm.ctype; T.assert(wasmCtypes.structs[0].name==='sqlite3_vfs'). @@ -357,12 +431,21 @@ catch(e){T.assert("test ing ." === e.message)} try{ throw new sqlite3.SQLite3Error(capi.SQLITE_SCHEMA) } - catch(e){ T.assert('SQLITE_SCHEMA' === e.message) } + catch(e){ + T.assert('SQLITE_SCHEMA' === e.message) + .assert(capi.SQLITE_SCHEMA === e.resultCode); + } try{ sqlite3.SQLite3Error.toss(capi.SQLITE_CORRUPT,{cause: true}) } catch(e){ - T.assert('SQLITE_CORRUPT'===e.message) + T.assert('SQLITE_CORRUPT' === e.message) + .assert(capi.SQLITE_CORRUPT === e.resultCode) .assert(true===e.cause); } + try{ sqlite3.SQLite3Error.toss("resultCode check") } + catch(e){ + T.assert(capi.SQLITE_ERROR === e.resultCode) + .assert('resultCode check' === e.message); + } }) //////////////////////////////////////////////////////////////////// .t('strglob/strlike', function(sqlite3){ @@ -371,12 +454,14 @@ assert(0===capi.sqlite3_strlike("%.txt", "foo.txt", 0)). assert(0!==capi.sqlite3_strlike("%.txt", "foo.xtx", 0)); }) + //////////////////////////////////////////////////////////////////// ;/*end of basic sanity checks*/ //////////////////////////////////////////////////////////////////// T.g('C/WASM Utilities') .t('sqlite3.wasm namespace', function(sqlite3){ + // TODO: break this into smaller individual test functions. const w = wasm; const chr = (x)=>x.charCodeAt(0); //log("heap getters..."); @@ -395,6 +480,66 @@ } } + // alloc(), realloc(), allocFromTypedArray() + { + let m = w.alloc(14); + let m2 = w.realloc(m, 16); + T.assert(m === m2/* because of alignment */); + T.assert(0 === w.realloc(m, 0)); + m = m2 = 0; + + // Check allocation limits and allocator's responses... + T.assert('number' === typeof sqlite3.capi.SQLITE_MAX_ALLOCATION_SIZE); + if(!sqlite3.config.useStdAlloc){ + const tooMuch = sqlite3.capi.SQLITE_MAX_ALLOCATION_SIZE + 1, + isAllocErr = (e)=>e instanceof sqlite3.WasmAllocError; + T.mustThrowMatching(()=>w.alloc(tooMuch), isAllocErr) + .assert(0 === w.alloc.impl(tooMuch)) + .mustThrowMatching(()=>w.realloc(0, tooMuch), isAllocErr) + .assert(0 === w.realloc.impl(0, tooMuch)); + } + + // Check allocFromTypedArray()... + const byteList = [11,22,33] + const u = new Uint8Array(byteList); + m = w.allocFromTypedArray(u); + for(let i = 0; i < u.length; ++i){ + T.assert(u[i] === byteList[i]) + .assert(u[i] === w.peek8(m + i)); + } + w.dealloc(m); + m = w.allocFromTypedArray(u.buffer); + for(let i = 0; i < u.length; ++i){ + T.assert(u[i] === byteList[i]) + .assert(u[i] === w.peek8(m + i)); + } + + w.dealloc(m); + T.mustThrowMatching( + ()=>w.allocFromTypedArray(1), + 'Value is not of a supported TypedArray type.' + ); + } + + { // Test peekXYZ()/pokeXYZ()... + const m = w.alloc(8); + T.assert( 17 === w.poke8(m,17).peek8(m) ) + .assert( 31987 === w.poke16(m,31987).peek16(m) ) + .assert( 345678 === w.poke32(m,345678).peek32(m) ) + .assert( + T.eqApprox( 345678.9, w.poke32f(m,345678.9).peek32f(m) ) + ).assert( + T.eqApprox( 4567890123.4, w.poke64f(m, 4567890123.4).peek64f(m) ) + ); + if(w.bigIntEnabled){ + T.assert( + BigInt(Number.MAX_SAFE_INTEGER) === + w.poke64(m, Number.MAX_SAFE_INTEGER).peek64(m) + ); + } + w.dealloc(m); + } + // isPtr32() { const ip = w.isPtr32; @@ -464,15 +609,15 @@ let cpy = w.scopedAlloc(n+10); let rc = w.cstrncpy(cpy, cStr, n+10); T.assert(n+1 === rc). - assert("hello" === w.cstringToJs(cpy)). - assert(chr('o') === w.getMemValue(cpy+n-1)). - assert(0 === w.getMemValue(cpy+n)); + assert("hello" === w.cstrToJs(cpy)). + assert(chr('o') === w.peek8(cpy+n-1)). + assert(0 === w.peek8(cpy+n)); let cStr2 = w.scopedAllocCString("HI!!!"); rc = w.cstrncpy(cpy, cStr2, 3); T.assert(3===rc). - assert("HI!lo" === w.cstringToJs(cpy)). - assert(chr('!') === w.getMemValue(cpy+2)). - assert(chr('l') === w.getMemValue(cpy+3)); + assert("HI!lo" === w.cstrToJs(cpy)). + assert(chr('!') === w.peek8(cpy+2)). + assert(chr('l') === w.peek8(cpy+3)); }finally{ w.scopedAllocPop(scope); } @@ -492,11 +637,12 @@ //log("allocCString()..."); { - const cstr = w.allocCString("hällo, world"); - const n = w.cstrlen(cstr); - T.assert(13 === n) - .assert(0===w.getMemValue(cstr+n)) - .assert(chr('d')===w.getMemValue(cstr+n-1)); + const jstr = "hällo, world!"; + const [cstr, n] = w.allocCString(jstr, true); + T.assert(14 === n) + .assert(0===w.peek8(cstr+n)) + .assert(chr('!')===w.peek8(cstr+n-1)); + w.dealloc(cstr); } //log("scopedAlloc() and friends..."); @@ -531,8 +677,8 @@ const [z1, z2, z3] = w.scopedAllocPtr(3); T.assert('number'===typeof z1).assert(z2>z1).assert(z3>z2) - .assert(0===w.getMemValue(z1,'i32'), 'allocPtr() must zero the targets') - .assert(0===w.getMemValue(z3,'i32')); + .assert(0===w.peek32(z1), 'allocPtr() must zero the targets') + .assert(0===w.peek32(z3)); }finally{ // Pop them in "incorrect" order to make sure they behave: w.scopedAllocPop(asc); @@ -552,8 +698,8 @@ T.assert(1===w.scopedAlloc.level); const [cstr, n] = w.scopedAllocCString("hello, world", true); T.assert(12 === n) - .assert(0===w.getMemValue(cstr+n)) - .assert(chr('d')===w.getMemValue(cstr+n-1)); + .assert(0===w.peek8(cstr+n)) + .assert(chr('d')===w.peek8(cstr+n-1)); }); }/*scopedAlloc()*/ @@ -577,12 +723,34 @@ T.assert(rc>0 && Number.isFinite(rc)); rc = w.xCallWrapped('sqlite3_wasm_enum_json','utf8'); T.assert('string'===typeof rc).assert(rc.length>300); + + + { // 'string:static' argAdapter() sanity checks... + let argAd = w.xWrap.argAdapter('string:static'); + let p0 = argAd('foo'), p1 = argAd('bar'); + T.assert(w.isPtr(p0) && w.isPtr(p1)) + .assert(p0 !== p1) + .assert(p0 === argAd('foo')) + .assert(p1 === argAd('bar')); + } + + // 'string:flexible' argAdapter() sanity checks... + w.scopedAllocCall(()=>{ + const argAd = w.xWrap.argAdapter('string:flexible'); + const cj = (v)=>w.cstrToJs(argAd(v)); + T.assert('Hi' === cj('Hi')) + .assert('hi' === cj(['h','i'])) + .assert('HI' === cj(new Uint8Array([72, 73]))); + }); + if(haveWasmCTests()){ - fw = w.xWrap('sqlite3_wasm_test_str_hello', 'utf8:free',['i32']); - rc = fw(0); - T.assert('hello'===rc); - rc = fw(1); - T.assert(null===rc); + if(!sqlite3.config.useStdAlloc){ + fw = w.xWrap('sqlite3_wasm_test_str_hello', 'utf8:dealloc',['i32']); + rc = fw(0); + T.assert('hello'===rc); + rc = fw(1); + T.assert(null===rc); + } if(w.bigIntEnabled){ w.xWrap.resultAdapter('thrice', (v)=>3n*BigInt(v)); @@ -592,10 +760,10 @@ T.assert(12n===rc); w.scopedAllocCall(function(){ - let pI1 = w.scopedAlloc(8), pI2 = pI1+4; - w.setMemValue(pI1, 0,'*')(pI2, 0, '*'); - let f = w.xWrap('sqlite3_wasm_test_int64_minmax',undefined,['i64*','i64*']); - let r1 = w.getMemValue(pI1, 'i64'), r2 = w.getMemValue(pI2, 'i64'); + const pI1 = w.scopedAlloc(8), pI2 = pI1+4; + w.pokePtr([pI1, pI2], 0); + const f = w.xWrap('sqlite3_wasm_test_int64_minmax',undefined,['i64*','i64*']); + const [r1, r2] = w.peek64([pI1, pI2]); T.assert(!Number.isSafeInteger(r1)).assert(!Number.isSafeInteger(r2)); }); } @@ -604,7 +772,7 @@ }/*WhWasmUtil*/) //////////////////////////////////////////////////////////////////// - .t('sqlite3.StructBinder (jaccwabyt)', function(sqlite3){ + .t('sqlite3.StructBinder (jaccwabyt🐇)', function(sqlite3){ const S = sqlite3, W = S.wasm; const MyStructDef = { sizeof: 16, @@ -635,9 +803,6 @@ assert(undefined === K.prototype.lookupMember('nope',false)). assert(k1 instanceof StructType). assert(StructType.isA(k1)). - assert(K.resolveToInstance(k1.pointer)===k1). - mustThrowMatching(()=>K.resolveToInstance(null,true), /is-not-a my_struct/). - assert(k1 === StructType.instanceForPointer(k1.pointer)). mustThrowMatching(()=>k1.$ro = 1, /read-only/); Object.keys(MyStructDef.members).forEach(function(key){ key = K.memberKey(key); @@ -647,8 +812,7 @@ " from "+k1.memoryDump()); }); T.assert('number' === typeof k1.pointer). - mustThrowMatching(()=>k1.pointer = 1, /pointer/). - assert(K.instanceForPointer(k1.pointer) === k1); + mustThrowMatching(()=>k1.pointer = 1, /pointer/); k1.$p4 = 1; k1.$pP = 2; T.assert(1 === k1.$p4).assert(2 === k1.$pP); if(MyStructDef.members.$p8){ @@ -663,22 +827,13 @@ assert('number' === typeof k1.$cstr). assert('A C-string.' === k1.memberToJsString('cstr')); k1.$pP = k2; - T.assert(k1.$pP === k2); + T.assert(k1.$pP === k2.pointer); k1.$pP = null/*null is special-cased to 0.*/; T.assert(0===k1.$pP); let ptr = k1.pointer; k1.dispose(); T.assert(undefined === k1.pointer). - assert(undefined === K.instanceForPointer(ptr)). mustThrowMatching(()=>{k1.$pP=1}, /disposed instance/); - const k3 = new K(); - ptr = k3.pointer; - T.assert(k3 === K.instanceForPointer(ptr)); - K.disposeAll(); - T.assert(ptr). - assert(undefined === k2.pointer). - assert(undefined === k3.pointer). - assert(undefined === K.instanceForPointer(ptr)); }finally{ k1.dispose(); k2.dispose(); @@ -708,10 +863,8 @@ assert(wts instanceof WTStruct). assert(wts instanceof StructType). assert(StructType.isA(wts)). - assert(wts === StructType.instanceForPointer(wts.pointer)); - T.assert(wts.pointer>0).assert(0===wts.$v4).assert(0n===wts.$v8). - assert(0===wts.$ppV).assert(0===wts.$xFunc). - assert(WTStruct.instanceForPointer(wts.pointer) === wts); + assert(wts.pointer>0).assert(0===wts.$v4).assert(0n===wts.$v8). + assert(0===wts.$ppV).assert(0===wts.$xFunc); const testFunc = W.xGet('sqlite3_wasm_test_struct'/*name gets mangled in -O3 builds!*/); let counter = 0; @@ -720,7 +873,6 @@ /*log("This from a JS function called from C, "+ "which itself was called from JS. arg =",arg);*/ ++counter; - T.assert(WTStruct.instanceForPointer(arg) === wts); if(3===counter){ tossQuietly("Testing exception propagation."); } @@ -744,7 +896,7 @@ testFunc(wts.pointer); //log("wts.pointer, wts.$ppV",wts.pointer, wts.$ppV); T.assert(1===counter).assert(20 === wts.$v4).assert(40n === wts.$v8) - .assert(autoResolvePtr ? (wts.$ppV === wts) : (wts.$ppV === wts.pointer)) + .assert(wts.$ppV === wts.pointer) .assert('string' === typeof wts.memberToJsString('cstr')) .assert(wts.memberToJsString('cstr') === wts.memberToJsString('$cstr')) .mustThrowMatching(()=>wts.memberToJsString('xFunc'), @@ -752,279 +904,42 @@ ; testFunc(wts.pointer); T.assert(2===counter).assert(40 === wts.$v4).assert(80n === wts.$v8) - .assert(autoResolvePtr ? (wts.$ppV === wts) : (wts.$ppV === wts.pointer)); + .assert(wts.$ppV === wts.pointer); /** The 3rd call to wtsFunc throw from JS, which is called from C, which is called from JS. Let's ensure that that exception propagates back here... */ T.mustThrowMatching(()=>testFunc(wts.pointer),/^Testing/); W.uninstallFunction(wts.$xFunc); wts.$xFunc = 0; - if(autoResolvePtr){ - wts.$ppV = 0; - T.assert(!wts.$ppV); - //WTStruct.debugFlags(0x03); - wts.$ppV = wts; - T.assert(wts === wts.$ppV) - //WTStruct.debugFlags(0); - } + wts.$ppV = 0; + T.assert(!wts.$ppV); + //WTStruct.debugFlags(0x03); + wts.$ppV = wts; + T.assert(wts.pointer === wts.$ppV) wts.setMemberCString('cstr', "A C-string."); T.assert(Array.isArray(wts.ondispose)). assert(wts.ondispose[0] === wts.$cstr). assert('A C-string.' === wts.memberToJsString('cstr')); const ptr = wts.pointer; wts.dispose(); - T.assert(ptr).assert(undefined === wts.pointer). - assert(undefined === WTStruct.instanceForPointer(ptr)) + T.assert(ptr).assert(undefined === wts.pointer); }finally{ wts.dispose(); } + + if(1){ // ondispose of other struct instances + const s1 = new WTStruct, s2 = new WTStruct, s3 = new WTStruct; + T.assert(s1.lookupMember instanceof Function) + .assert(s1.addOnDispose instanceof Function); + s1.addOnDispose(s2,"testing variadic args"); + T.assert(2===s1.ondispose.length); + s2.addOnDispose(s3); + s1.dispose(); + T.assert(!s2.pointer,"Expecting s2 to be ondispose'd by s1."); + T.assert(!s3.pointer,"Expecting s3 to be ondispose'd by s2."); + } }/*StructBinder*/) - //////////////////////////////////////////////////////////////////// - .t('sqlite3.StructBinder part 2', function(sqlite3){ - // https://www.sqlite.org/c3ref/vfs.html - // https://www.sqlite.org/c3ref/io_methods.html - const sqlite3_io_methods = capi.sqlite3_io_methods, - sqlite3_vfs = capi.sqlite3_vfs, - sqlite3_file = capi.sqlite3_file; - //log("struct sqlite3_file", sqlite3_file.memberKeys()); - //log("struct sqlite3_vfs", sqlite3_vfs.memberKeys()); - //log("struct sqlite3_io_methods", sqlite3_io_methods.memberKeys()); - const installMethod = function callee(tgt, name, func){ - if(1===arguments.length){ - return (n,f)=>callee(tgt,n,f); - } - if(!callee.argcProxy){ - callee.argcProxy = function(func,sig){ - return function(...args){ - if(func.length!==arguments.length){ - toss("Argument mismatch. Native signature is:",sig); - } - return func.apply(this, args); - } - }; - callee.ondisposeRemoveFunc = function(){ - if(this.__ondispose){ - const who = this; - this.__ondispose.forEach( - (v)=>{ - if('number'===typeof v){ - try{wasm.uninstallFunction(v)} - catch(e){/*ignore*/} - }else{/*wasm function wrapper property*/ - delete who[v]; - } - } - ); - delete this.__ondispose; - } - }; - }/*static init*/ - const sigN = tgt.memberSignature(name), - memKey = tgt.memberKey(name); - //log("installMethod",tgt, name, sigN); - if(!tgt.__ondispose){ - T.assert(undefined === tgt.ondispose); - tgt.ondispose = [callee.ondisposeRemoveFunc]; - tgt.__ondispose = []; - } - const fProxy = callee.argcProxy(func, sigN); - const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true)); - tgt[memKey] = pFunc; - /** - ACHTUNG: function pointer IDs are from a different pool than - allocation IDs, starting at 1 and incrementing in steps of 1, - so if we set tgt[memKey] to those values, we'd very likely - later misinterpret them as plain old pointer addresses unless - unless we use some silly heuristic like "all values <5k are - presumably function pointers," or actually perform a function - lookup on every pointer to first see if it's a function. That - would likely work just fine, but would be kludgy. - - It turns out that "all values less than X are functions" is - essentially how it works in wasm: a function pointer is - reported to the client as its index into the - __indirect_function_table. - - So... once jaccwabyt can be told how to access the - function table, it could consider all pointer values less - than that table's size to be functions. As "real" pointer - values start much, much higher than the function table size, - that would likely work reasonably well. e.g. the object - pointer address for sqlite3's default VFS is (in this local - setup) 65104, whereas the function table has fewer than 600 - entries. - */ - const wrapperKey = '$'+memKey; - tgt[wrapperKey] = fProxy; - tgt.__ondispose.push(pFunc, wrapperKey); - //log("tgt.__ondispose =",tgt.__ondispose); - return (n,f)=>callee(tgt, n, f); - }/*installMethod*/; - - const installIOMethods = function instm(iom){ - (iom instanceof capi.sqlite3_io_methods) || toss("Invalid argument type."); - if(!instm._requireFileArg){ - instm._requireFileArg = function(arg,methodName){ - arg = capi.sqlite3_file.resolveToInstance(arg); - if(!arg){ - err("sqlite3_io_methods::xClose() was passed a non-sqlite3_file."); - } - return arg; - }; - instm._methods = { - // https://sqlite.org/c3ref/io_methods.html - xClose: /*i(P)*/function(f){ - /* int (*xClose)(sqlite3_file*) */ - log("xClose(",f,")"); - if(!(f = instm._requireFileArg(f,'xClose'))) return capi.SQLITE_MISUSE; - f.dispose(/*noting that f has externally-owned memory*/); - return 0; - }, - xRead: /*i(Ppij)*/function(f,dest,n,offset){ - /* int (*xRead)(sqlite3_file*, void*, int iAmt, sqlite3_int64 iOfst) */ - log("xRead(",arguments,")"); - if(!(f = instm._requireFileArg(f))) return capi.SQLITE_MISUSE; - wasm.heap8().fill(0, dest + offset, n); - return 0; - }, - xWrite: /*i(Ppij)*/function(f,dest,n,offset){ - /* int (*xWrite)(sqlite3_file*, const void*, int iAmt, sqlite3_int64 iOfst) */ - log("xWrite(",arguments,")"); - if(!(f=instm._requireFileArg(f,'xWrite'))) return capi.SQLITE_MISUSE; - return 0; - }, - xTruncate: /*i(Pj)*/function(f){ - /* int (*xTruncate)(sqlite3_file*, sqlite3_int64 size) */ - log("xTruncate(",arguments,")"); - if(!(f=instm._requireFileArg(f,'xTruncate'))) return capi.SQLITE_MISUSE; - return 0; - }, - xSync: /*i(Pi)*/function(f){ - /* int (*xSync)(sqlite3_file*, int flags) */ - log("xSync(",arguments,")"); - if(!(f=instm._requireFileArg(f,'xSync'))) return capi.SQLITE_MISUSE; - return 0; - }, - xFileSize: /*i(Pp)*/function(f,pSz){ - /* int (*xFileSize)(sqlite3_file*, sqlite3_int64 *pSize) */ - log("xFileSize(",arguments,")"); - if(!(f=instm._requireFileArg(f,'xFileSize'))) return capi.SQLITE_MISUSE; - wasm.setMemValue(pSz, 0/*file size*/); - return 0; - }, - xLock: /*i(Pi)*/function(f){ - /* int (*xLock)(sqlite3_file*, int) */ - log("xLock(",arguments,")"); - if(!(f=instm._requireFileArg(f,'xLock'))) return capi.SQLITE_MISUSE; - return 0; - }, - xUnlock: /*i(Pi)*/function(f){ - /* int (*xUnlock)(sqlite3_file*, int) */ - log("xUnlock(",arguments,")"); - if(!(f=instm._requireFileArg(f,'xUnlock'))) return capi.SQLITE_MISUSE; - return 0; - }, - xCheckReservedLock: /*i(Pp)*/function(){ - /* int (*xCheckReservedLock)(sqlite3_file*, int *pResOut) */ - log("xCheckReservedLock(",arguments,")"); - return 0; - }, - xFileControl: /*i(Pip)*/function(){ - /* int (*xFileControl)(sqlite3_file*, int op, void *pArg) */ - log("xFileControl(",arguments,")"); - return capi.SQLITE_NOTFOUND; - }, - xSectorSize: /*i(P)*/function(){ - /* int (*xSectorSize)(sqlite3_file*) */ - log("xSectorSize(",arguments,")"); - return 0/*???*/; - }, - xDeviceCharacteristics:/*i(P)*/function(){ - /* int (*xDeviceCharacteristics)(sqlite3_file*) */ - log("xDeviceCharacteristics(",arguments,")"); - return 0; - } - }; - }/*static init*/ - iom.$iVersion = 1; - Object.keys(instm._methods).forEach( - (k)=>installMethod(iom, k, instm._methods[k]) - ); - }/*installIOMethods()*/; - - const iom = new sqlite3_io_methods, sfile = new sqlite3_file; - const err = console.error.bind(console); - try { - const IOM = sqlite3_io_methods, S3F = sqlite3_file; - //log("iom proto",iom,iom.constructor.prototype); - //log("sfile",sfile,sfile.constructor.prototype); - T.assert(0===sfile.$pMethods).assert(iom.pointer > 0); - //log("iom",iom); - sfile.$pMethods = iom.pointer; - T.assert(iom.pointer === sfile.$pMethods) - .assert(IOM.resolveToInstance(iom)) - .assert(undefined ===IOM.resolveToInstance(sfile)) - .mustThrow(()=>IOM.resolveToInstance(0,true)) - .assert(S3F.resolveToInstance(sfile.pointer)) - .assert(undefined===S3F.resolveToInstance(iom)) - .assert(iom===IOM.resolveToInstance(sfile.$pMethods)); - T.assert(0===iom.$iVersion); - installIOMethods(iom); - T.assert(1===iom.$iVersion); - //log("iom.__ondispose",iom.__ondispose); - T.assert(Array.isArray(iom.__ondispose)).assert(iom.__ondispose.length>10); - }finally{ - iom.dispose(); - T.assert(undefined === iom.__ondispose); - } - - const dVfs = new sqlite3_vfs(capi.sqlite3_vfs_find(null)); - try { - const SB = sqlite3.StructBinder; - T.assert(dVfs instanceof SB.StructType) - .assert(dVfs.pointer) - .assert('sqlite3_vfs' === dVfs.structName) - .assert(!!dVfs.structInfo) - .assert(SB.StructType.hasExternalPointer(dVfs)) - .assert(dVfs.$iVersion>0) - .assert('number'===typeof dVfs.$zName) - .assert('number'===typeof dVfs.$xSleep) - .assert(wasm.functionEntry(dVfs.$xOpen)) - .assert(dVfs.memberIsString('zName')) - .assert(dVfs.memberIsString('$zName')) - .assert(!dVfs.memberIsString('pAppData')) - .mustThrowMatching(()=>dVfs.memberToJsString('xSleep'), - /Invalid member type signature for C-string/) - .mustThrowMatching(()=>dVfs.memberSignature('nope'), /nope is not a mapped/) - .assert('string' === typeof dVfs.memberToJsString('zName')) - .assert(dVfs.memberToJsString('zName')===dVfs.memberToJsString('$zName')) - ; - //log("Default VFS: @",dVfs.pointer); - Object.keys(sqlite3_vfs.structInfo.members).forEach(function(mname){ - const mk = sqlite3_vfs.memberKey(mname), mbr = sqlite3_vfs.structInfo.members[mname], - addr = dVfs[mk], prefix = 'defaultVfs.'+mname; - if(1===mbr.signature.length){ - let sep = '?', val = undefined; - switch(mbr.signature[0]){ - // TODO: move this into an accessor, e.g. getPreferredValue(member) - case 'i': case 'j': case 'f': case 'd': sep = '='; val = dVfs[mk]; break - case 'p': case 'P': sep = '@'; val = dVfs[mk]; break; - case 's': sep = '='; - val = dVfs.memberToJsString(mname); - break; - } - //log(prefix, sep, val); - }else{ - //log(prefix," = funcptr @",addr, wasm.functionEntry(addr)); - } - }); - }finally{ - dVfs.dispose(); - T.assert(undefined===dVfs.pointer); - } - }/*StructBinder part 2*/) - //////////////////////////////////////////////////////////////////// .t('sqlite3.wasm.pstack', function(sqlite3){ const P = wasm.pstack; @@ -1092,8 +1007,8 @@ try{ const n = 520; const p = wasm.pstack.alloc(n); - T.assert(0===wasm.getMemValue(p)) - .assert(0===wasm.getMemValue(p+n-1)); + T.assert(0===wasm.peek8(p)) + .assert(0===wasm.peek8(p+n-1)); T.assert(undefined === capi.sqlite3_randomness(n - 10, p)); let j, check = 0; const heap = wasm.heap8u(); @@ -1134,18 +1049,115 @@ .t('Create db', function(sqlite3){ const dbFile = '/tester1.db'; wasm.sqlite3_wasm_vfs_unlink(0, dbFile); - const db = this.db = new sqlite3.oo1.DB(dbFile); - T.assert(Number.isInteger(db.pointer)) + const db = this.db = new sqlite3.oo1.DB(dbFile, 0 ? 'ct' : 'c'); + db.onclose = { + disposeAfter: [], + disposeBefore: [ + (db)=>{ + //console.debug("db.onclose.before dropping modules"); + //sqlite3.capi.sqlite3_drop_modules(db.pointer, 0); + } + ], + before: function(db){ + while(this.disposeBefore.length){ + const v = this.disposeBefore.shift(); + console.debug("db.onclose.before cleaning up:",v); + if(wasm.isPtr(v)) wasm.dealloc(v); + else if(v instanceof sqlite3.StructBinder.StructType){ + v.dispose(); + }else if(v instanceof Function){ + try{ v(db) } catch(e){ + console.warn("beforeDispose() callback threw:",e); + } + } + } + }, + after: function(){ + while(this.disposeAfter.length){ + const v = this.disposeAfter.shift(); + console.debug("db.onclose.after cleaning up:",v); + if(wasm.isPtr(v)) wasm.dealloc(v); + else if(v instanceof sqlite3.StructBinder.StructType){ + v.dispose(); + }else if(v instanceof Function){ + try{v()} catch(e){/*ignored*/} + } + } + } + }; + + T.assert(wasm.isPtr(db.pointer)) .mustThrowMatching(()=>db.pointer=1, /read-only/) .assert(0===sqlite3.capi.sqlite3_extended_result_codes(db.pointer,1)) .assert('main'===db.dbName(0)) - .assert('string' === typeof db.dbVfsName()); + .assert('string' === typeof db.dbVfsName()) + .assert(db.pointer === wasm.xWrap.testConvertArg('sqlite3*',db)); // Custom db error message handling via sqlite3_prepare_v2/v3() let rc = capi.sqlite3_prepare_v3(db.pointer, {/*invalid*/}, -1, 0, null, null); T.assert(capi.SQLITE_MISUSE === rc) .assert(0 === capi.sqlite3_errmsg(db.pointer).indexOf("Invalid SQL")) .assert(dbFile === db.dbFilename()) .assert(!db.dbFilename('nope')); + //Sanity check DB.checkRc()... + let ex; + try{db.checkRc(rc)} + catch(e){ex = e} + T.assert(ex instanceof sqlite3.SQLite3Error) + .assert(0===ex.message.indexOf("sqlite3 result code")) + .assert(ex.message.indexOf("Invalid SQL")>0); + T.assert(db === db.checkRc(0)) + .assert(db === sqlite3.oo1.DB.checkRc(db,0)) + .assert(null === sqlite3.oo1.DB.checkRc(null,0)); + + this.progressHandlerCount = 0; + capi.sqlite3_progress_handler(db, 5, (p)=>{ + ++this.progressHandlerCount; + return 0; + }, 0); + }) + //////////////////////////////////////////////////////////////////// + .t('sqlite3_db_config() and sqlite3_db_status()', function(sqlite3){ + let rc = capi.sqlite3_db_config(this.db, capi.SQLITE_DBCONFIG_LEGACY_ALTER_TABLE, 0, 0); + T.assert(0===rc); + rc = capi.sqlite3_db_config(this.db, capi.SQLITE_DBCONFIG_MAX+1, 0); + T.assert(capi.SQLITE_MISUSE === rc); + rc = capi.sqlite3_db_config(this.db, capi.SQLITE_DBCONFIG_MAINDBNAME, "main"); + T.assert(0 === rc); + const stack = wasm.pstack.pointer; + try { + const [pCur, pHi] = wasm.pstack.allocChunks(2,8); + rc = capi.sqlite3_db_status(this.db, capi.SQLITE_DBSTATUS_LOOKASIDE_USED, + pCur, pHi, 0); + T.assert(0===rc); + if(wasm.peek32(pCur)){ + warn("Cannot test db_config(SQLITE_DBCONFIG_LOOKASIDE)", + "while lookaside memory is in use."); + }else{ + rc = capi.sqlite3_db_config(this.db, capi.SQLITE_DBCONFIG_LOOKASIDE, + 0, 4096, 12); + T.assert(0 === rc); + } + wasm.poke32([pCur, pHi], 0); + let [vCur, vHi] = wasm.peek32(pCur, pHi); + T.assert(0===vCur).assert(0===vHi); + rc = capi.sqlite3_status(capi.SQLITE_STATUS_MEMORY_USED, + pCur, pHi, 0); + [vCur, vHi] = wasm.peek32(pCur, pHi); + //console.warn("i32 vCur,vHi",vCur,vHi); + T.assert(0 === rc).assert(vCur > 0).assert(vHi >= vCur); + if(wasm.bigIntEnabled){ + // Again in 64-bit. Recall that pCur and pHi are allocated + // large enough to account for this re-use. + wasm.poke64([pCur, pHi], 0); + rc = capi.sqlite3_status64(capi.SQLITE_STATUS_MEMORY_USED, + pCur, pHi, 0); + [vCur, vHi] = wasm.peek64([pCur, pHi]); + //console.warn("i64 vCur,vHi",vCur,vHi); + T.assert(0 === rc).assert(vCur > 0).assert(vHi >= vCur); + } + }finally{ + wasm.pstack.restore(stack); + } }) //////////////////////////////////////////////////////////////////// @@ -1154,10 +1166,15 @@ new TextEncoder('utf-8').encode("select 3 as a") ); //debug("statement =",st); + this.progressHandlerCount = 0; try { - T.assert(Number.isInteger(st.pointer)) + T.assert(wasm.isPtr(st.pointer)) .mustThrowMatching(()=>st.pointer=1, /read-only/) .assert(1===this.db.openStatementCount()) + .assert( + capi.sqlite3_stmt_status( + st, capi.SQLITE_STMTSTATUS_RUN, 0 + ) === 0) .assert(!st._mayGet) .assert('a' === st.getColumnName(0)) .assert(1===st.columnCount) @@ -1183,8 +1200,14 @@ .assert(st._mayGet) .assert(false===st.step()) .assert(!st._mayGet) - ; - T.assert(0===capi.sqlite3_strglob("*.txt", "foo.txt")). + .assert( + capi.sqlite3_stmt_status( + st, capi.SQLITE_STMTSTATUS_RUN, 0 + ) > 0); + + T.assert(this.progressHandlerCount > 0, + "Expecting progress callback."). + assert(0===capi.sqlite3_strglob("*.txt", "foo.txt")). assert(0!==capi.sqlite3_strglob("*.txt", "foo.xtx")). assert(0===capi.sqlite3_strlike("%.txt", "foo.txt", 0)). assert(0!==capi.sqlite3_strlike("%.txt", "foo.xtx", 0)); @@ -1201,12 +1224,19 @@ if(1){ const vfsList = capi.sqlite3_js_vfs_list(); T.assert(vfsList.length>1); - T.assert('string'===typeof vfsList[0]); //log("vfsList =",vfsList); - for(const v of vfsList){ - T.assert('string' === typeof v) - .assert(capi.sqlite3_vfs_find(v) > 0); - } + wasm.scopedAllocCall(()=>{ + const vfsArg = (v)=>wasm.xWrap.testConvertArg('sqlite3_vfs*',v); + for(const v of vfsList){ + T.assert('string' === typeof v); + const pVfs = capi.sqlite3_vfs_find(v); + T.assert(wasm.isPtr(pVfs)) + .assert(pVfs===vfsArg(v)); + const vfs = new capi.sqlite3_vfs(pVfs); + try { T.assert(vfsArg(vfs)===pVfs) } + finally{ vfs.dispose() } + } + }); } /** Trivia: the magic db name ":memory:" does not actually use the @@ -1239,6 +1269,7 @@ .t('Table t', function(sqlite3){ const db = this.db; let list = []; + this.progressHandlerCount = 0; let rc = db.exec({ sql:['CREATE TABLE t(a,b);', // ^^^ using TEMP TABLE breaks the db export test @@ -1252,7 +1283,9 @@ T.assert(rc === db) .assert(2 === list.length) .assert('string'===typeof list[1]) - .assert(4===db.changes()); + .assert(4===db.changes()) + .assert(this.progressHandlerCount > 0, + "Expecting progress callback.") if(wasm.bigIntEnabled){ T.assert(4n===db.changes(false,true)); } @@ -1266,9 +1299,19 @@ rowMode: 'object', resultRows: list, columnNames: colNames, + _myState: 3 /* Accessible from the callback */, callback: function(row,stmt){ ++counter; - T.assert((row.a%2 && row.a<6) || 'blob'===row.a); + T.assert( + 3 === this._myState + /* Recall that "this" is the options object. */ + ).assert( + this.columnNames[0]==='a' && this.columnNames[1]==='b' + /* options.columnNames is filled out before the first + Stmt.step(). */ + ).assert( + (row.a%2 && row.a<6) || 'blob'===row.a + ); } }); T.assert(2 === colNames.length) @@ -1321,6 +1364,105 @@ T.assert(e instanceof sqlite3.SQLite3Error) .assert(0==e.message.indexOf('Cannot prepare empty')); } + })/*setup table T*/ + + //////////////////////////////////////////////////////////////////// + .t({ + name: "sqlite3_set_authorizer()", + test:function(sqlite3){ + T.assert(capi.SQLITE_IGNORE>0) + .assert(capi.SQLITE_DENY>0); + const db = this.db; + const ssa = capi.sqlite3_set_authorizer; + const n = db.selectValue('select count(*) from t'); + T.assert(n>0); + let authCount = 0; + let rc = ssa(db, function(pV, iCode, s0, s1, s2, s3){ + ++authCount; + return capi.SQLITE_IGNORE; + }, 0); + T.assert(0===rc) + .assert( + undefined === db.selectValue('select count(*) from t') + /* Note that the count() never runs, so we get undefined + instead of 0. */ + ) + .assert(authCount>0); + authCount = 0; + db.exec("update t set a=-9999"); + T.assert(authCount>0); + /* Reminder: we don't use DELETE because, from the C API docs: + + "If the action code is [SQLITE_DELETE] and the callback + returns [SQLITE_IGNORE] then the [DELETE] operation proceeds + but the [truncate optimization] is disabled and all rows are + deleted individually." + */ + rc = ssa(db, null, 0); + authCount = 0; + T.assert(-9999 != db.selectValue('select a from t')) + .assert(0===authCount); + rc = ssa(db, function(pV, iCode, s0, s1, s2, s3){ + ++authCount; + return capi.SQLITE_DENY; + }, 0); + T.assert(0===rc); + let err; + try{ db.exec("select 1 from t") } + catch(e){ err = e } + T.assert(err instanceof sqlite3.SQLite3Error) + .assert(err.message.indexOf('not authorized'>0)) + .assert(1===authCount); + authCount = 0; + rc = ssa(db, function(...args){ + ++authCount; + return capi.SQLITE_OK; + }, 0); + T.assert(0===rc); + T.assert(n === db.selectValue('select count(*) from t')) + .assert(authCount>0); + authCount = 0; + rc = ssa(db, function(pV, iCode, s0, s1, s2, s3){ + ++authCount; + throw new Error("Testing catching of authorizer."); + }, 0); + T.assert(0===rc); + authCount = 0; + err = undefined; + try{ db.exec("select 1 from t") } + catch(e){err = e} + T.assert(err instanceof Error) + .assert(err.message.indexOf('not authorized')>0) + /* Note that the thrown message is trumped/overwritten + by the authorizer process. */ + .assert(1===authCount); + rc = ssa(db, 0, 0); + authCount = 0; + T.assert(0===rc); + T.assert(n === db.selectValue('select count(*) from t')) + .assert(0===authCount); + } + })/*sqlite3_set_authorizer()*/ + + //////////////////////////////////////////////////////////////////////// + .t("sqlite3_table_column_metadata()", function(sqlite3){ + const stack = wasm.pstack.pointer; + try{ + const [pzDT, pzColl, pNotNull, pPK, pAuto] = + wasm.pstack.allocPtr(5); + const rc = capi.sqlite3_table_column_metadata( + this.db, "main", "t", "rowid", + pzDT, pzColl, pNotNull, pPK, pAuto + ); + T.assert(0===rc) + .assert("INTEGER"===wasm.cstrToJs(wasm.peekPtr(pzDT))) + .assert("BINARY"===wasm.cstrToJs(wasm.peekPtr(pzColl))) + .assert(0===wasm.peek32(pNotNull)) + .assert(1===wasm.peek32(pPK)) + .assert(0===wasm.peek32(pAuto)) + }finally{ + wasm.pstack.restore(stack); + } }) //////////////////////////////////////////////////////////////////////// @@ -1339,73 +1481,124 @@ rc = db.selectArray('select a, b from t where b=-1'); T.assert(undefined === rc); }) + //////////////////////////////////////////////////////////////////////// + .t('selectArrays/Objects()', function(sqlite3){ + const db = this.db; + const sql = 'select a, b from t where a=? or b=? order by a'; + let rc = db.selectArrays(sql, [1, 4]); + T.assert(Array.isArray(rc)) + .assert(2===rc.length) + .assert(2===rc[0].length) + .assert(1===rc[0][0]) + .assert(2===rc[0][1]) + .assert(3===rc[1][0]) + .assert(4===rc[1][1]) + rc = db.selectArrays(sql, [99,99]); + T.assert(Array.isArray(rc)).assert(0===rc.length); + rc = db.selectObjects(sql, [1,4]); + T.assert(Array.isArray(rc)) + .assert(2===rc.length) + .assert('object' === typeof rc[1]) + .assert(1===rc[0].a) + .assert(2===rc[0].b) + .assert(3===rc[1].a) + .assert(4===rc[1].b); + }) //////////////////////////////////////////////////////////////////////// - .t('sqlite3_js_db_export()', function(){ - const db = this.db; - const xp = capi.sqlite3_js_db_export(db.pointer); - T.assert(xp instanceof Uint8Array) - .assert(xp.byteLength>0) - .assert(0 === xp.byteLength % 512); + .t({ + name: 'sqlite3_js_db_export()', + predicate: ()=>true, + test: function(sqlite3){ + const db = this.db; + const xp = capi.sqlite3_js_db_export(db.pointer); + T.assert(xp instanceof Uint8Array) + .assert(xp.byteLength>0) + .assert(0 === xp.byteLength % 512); + this.dbExport = xp; + } }/*sqlite3_js_db_export()*/) + .t({ + name: 'sqlite3_js_vfs_create_file() with db in default VFS', + predicate: ()=>true, + test: function(sqlite3){ + const db = this.db; + const pVfs = capi.sqlite3_js_db_vfs(db); + const filename = "sqlite3_js_vfs_create_file().db"; + capi.sqlite3_js_vfs_create_file(pVfs, filename, this.dbExport); + delete this.dbExport; + const db2 = new sqlite3.oo1.DB(filename,'r'); + try { + const sql = "select count(*) from t"; + const n = db.selectValue(sql); + T.assert(n>0 && db2.selectValue(sql) === n); + }finally{ + db2.close(); + wasm.sqlite3_wasm_vfs_unlink(pVfs, filename); + } + } + }/*sqlite3_js_vfs_create_file()*/) //////////////////////////////////////////////////////////////////// - .t('Scalar UDFs', function(sqlite3){ - const db = this.db; - db.createFunction("foo",(pCx,a,b)=>a+b); - T.assert(7===db.selectValue("select foo(3,4)")). - assert(5===db.selectValue("select foo(3,?)",2)). - assert(5===db.selectValue("select foo(?,?2)",[1,4])). - assert(5===db.selectValue("select foo($a,$b)",{$a:0,$b:5})); - db.createFunction("bar", { - arity: -1, - xFunc: (pCx,...args)=>{ - let rc = 0; - for(const v of args) rc += v; - return rc; - } - }).createFunction({ - name: "asis", - xFunc: (pCx,arg)=>arg - }); - T.assert(0===db.selectValue("select bar()")). - assert(1===db.selectValue("select bar(1)")). - assert(3===db.selectValue("select bar(1,2)")). - assert(-1===db.selectValue("select bar(1,2,-4)")). - assert('hi' === db.selectValue("select asis('hi')")). - assert('hi' === db.selectValue("select ?",'hi')). - assert(null === db.selectValue("select null")). - assert(null === db.selectValue("select asis(null)")). - assert(1 === db.selectValue("select ?",1)). - assert(2 === db.selectValue("select ?",[2])). - assert(3 === db.selectValue("select $a",{$a:3})). - assert(T.eqApprox(3.1,db.selectValue("select 3.0 + 0.1"))). - assert(T.eqApprox(1.3,db.selectValue("select asis(1 + 0.3)"))); + .t({ + name:'Scalar UDFs', + //predicate: ()=>false, + test: function(sqlite3){ + const db = this.db; + db.createFunction("foo",(pCx,a,b)=>a+b); + T.assert(7===db.selectValue("select foo(3,4)")). + assert(5===db.selectValue("select foo(3,?)",2)). + assert(5===db.selectValue("select foo(?,?2)",[1,4])). + assert(5===db.selectValue("select foo($a,$b)",{$a:0,$b:5})); + db.createFunction("bar", { + arity: -1, + xFunc: (pCx,...args)=>{ + let rc = 0; + for(const v of args) rc += v; + return rc; + } + }).createFunction({ + name: "asis", + xFunc: (pCx,arg)=>arg + }); + T.assert(0===db.selectValue("select bar()")). + assert(1===db.selectValue("select bar(1)")). + assert(3===db.selectValue("select bar(1,2)")). + assert(-1===db.selectValue("select bar(1,2,-4)")). + assert('hi' === db.selectValue("select asis('hi')")). + assert('hi' === db.selectValue("select ?",'hi')). + assert(null === db.selectValue("select null")). + assert(null === db.selectValue("select asis(null)")). + assert(1 === db.selectValue("select ?",1)). + assert(2 === db.selectValue("select ?",[2])). + assert(3 === db.selectValue("select $a",{$a:3})). + assert(T.eqApprox(3.1,db.selectValue("select 3.0 + 0.1"))). + assert(T.eqApprox(1.3,db.selectValue("select asis(1 + 0.3)"))); - let blobArg = new Uint8Array(2); - blobArg.set([0x68, 0x69], 0); - let blobRc = db.selectValue("select asis(?1)", blobArg); - T.assert(blobRc instanceof Uint8Array). - assert(2 === blobRc.length). - assert(0x68==blobRc[0] && 0x69==blobRc[1]); - blobRc = db.selectValue("select asis(X'6869')"); - T.assert(blobRc instanceof Uint8Array). - assert(2 === blobRc.length). - assert(0x68==blobRc[0] && 0x69==blobRc[1]); + let blobArg = new Uint8Array([0x68, 0x69]); + let blobRc = db.selectValue("select asis(?1)", blobArg); + T.assert(blobRc instanceof Uint8Array). + assert(2 === blobRc.length). + assert(0x68==blobRc[0] && 0x69==blobRc[1]); + blobRc = db.selectValue("select asis(X'6869')"); + T.assert(blobRc instanceof Uint8Array). + assert(2 === blobRc.length). + assert(0x68==blobRc[0] && 0x69==blobRc[1]); - blobArg = new Int8Array(2); - blobArg.set([0x68, 0x69]); - //debug("blobArg=",blobArg); - blobRc = db.selectValue("select asis(?1)", blobArg); - T.assert(blobRc instanceof Uint8Array). - assert(2 === blobRc.length); - //debug("blobRc=",blobRc); - T.assert(0x68==blobRc[0] && 0x69==blobRc[1]); + blobArg = new Int8Array([0x68, 0x69]); + //debug("blobArg=",blobArg); + blobRc = db.selectValue("select asis(?1)", blobArg); + T.assert(blobRc instanceof Uint8Array). + assert(2 === blobRc.length); + //debug("blobRc=",blobRc); + T.assert(0x68==blobRc[0] && 0x69==blobRc[1]); + } }) //////////////////////////////////////////////////////////////////// .t({ name: 'Aggregate UDFs', + //predicate: ()=>false, test: function(sqlite3){ const db = this.db; const sjac = capi.sqlite3_js_aggregate_context; @@ -1413,11 +1606,11 @@ name: 'summer', xStep: (pCtx, n)=>{ const ac = sjac(pCtx, 4); - wasm.setMemValue(ac, wasm.getMemValue(ac,'i32') + Number(n), 'i32'); + wasm.poke32(ac, wasm.peek32(ac) + Number(n)); }, xFinal: (pCtx)=>{ const ac = sjac(pCtx, 0); - return ac ? wasm.getMemValue(ac,'i32') : 0; + return ac ? wasm.peek32(ac) : 0; } }); let v = db.selectValue([ @@ -1436,13 +1629,13 @@ arity: -1, xStep: (pCtx, ...args)=>{ const ac = sjac(pCtx, 4); - let sum = wasm.getMemValue(ac, 'i32'); + let sum = wasm.peek32(ac); for(const v of args) sum += Number(v); - wasm.setMemValue(ac, sum, 'i32'); + wasm.poke32(ac, sum); }, xFinal: (pCtx)=>{ const ac = sjac(pCtx, 0); - capi.sqlite3_result_int( pCtx, ac ? wasm.getMemValue(ac,'i32') : 0 ); + capi.sqlite3_result_int( pCtx, ac ? wasm.peek32(ac) : 0 ); // xFinal() may either return its value directly or call // sqlite3_result_xyz() and return undefined. Both are // functionally equivalent. @@ -1476,6 +1669,7 @@ .t({ name: 'Aggregate UDFs (64-bit)', predicate: ()=>wasm.bigIntEnabled, + //predicate: ()=>false, test: function(sqlite3){ const db = this.db; const sjac = capi.sqlite3_js_aggregate_context; @@ -1483,11 +1677,11 @@ name: 'summer64', xStep: (pCtx, n)=>{ const ac = sjac(pCtx, 8); - wasm.setMemValue(ac, wasm.getMemValue(ac,'i64') + BigInt(n), 'i64'); + wasm.poke64(ac, wasm.peek64(ac) + BigInt(n)); }, xFinal: (pCtx)=>{ const ac = sjac(pCtx, 0); - return ac ? wasm.getMemValue(ac,'i64') : 0n; + return ac ? wasm.peek64(ac) : 0n; } }); let v = db.selectValue([ @@ -1502,6 +1696,7 @@ //////////////////////////////////////////////////////////////////// .t({ name: 'Window UDFs', + //predicate: ()=>false, test: function(){ /* Example window function, table, and results taken from: https://sqlite.org/windowfunctions.html#udfwinfunc */ @@ -1509,11 +1704,11 @@ const sjac = (cx,n=4)=>capi.sqlite3_js_aggregate_context(cx,n); const xValueFinal = (pCtx)=>{ const ac = sjac(pCtx, 0); - return ac ? wasm.getMemValue(ac,'i32') : 0; + return ac ? wasm.peek32(ac) : 0; }; const xStepInverse = (pCtx, n)=>{ const ac = sjac(pCtx); - wasm.setMemValue(ac, wasm.getMemValue(ac,'i32') + Number(n), 'i32'); + wasm.poke32(ac, wasm.peek32(ac) + Number(n)); }; db.createFunction({ name: 'winsumint', @@ -1601,6 +1796,7 @@ T.assert(2===db.selectValue('select a from foo.bar where a>1 order by a')); let colCount = 0, rowCount = 0; const execCallback = function(pVoid, nCols, aVals, aNames){ + //console.warn("execCallback(",arguments,")"); colCount = nCols; ++rowCount; T.assert(2===aVals.length) @@ -1624,42 +1820,33 @@ //////////////////////////////////////////////////////////////////// .t({ - name: 'C-side WASM tests (if compiled in)', - predicate: haveWasmCTests, + name: 'C-side WASM tests', + predicate: ()=>(haveWasmCTests() || "Not compiled in."), test: function(){ const w = wasm, db = this.db; const stack = w.scopedAllocPush(); let ptrInt; const origValue = 512; - const ptrValType = 'i32'; try{ ptrInt = w.scopedAlloc(4); - w.setMemValue(ptrInt,origValue, ptrValType); + w.poke32(ptrInt,origValue); const cf = w.xGet('sqlite3_wasm_test_intptr'); const oldPtrInt = ptrInt; - //log('ptrInt',ptrInt); - //log('getMemValue(ptrInt)',w.getMemValue(ptrInt)); - T.assert(origValue === w.getMemValue(ptrInt, ptrValType)); + T.assert(origValue === w.peek32(ptrInt)); const rc = cf(ptrInt); - //log('cf(ptrInt)',rc); - //log('ptrInt',ptrInt); - //log('getMemValue(ptrInt)',w.getMemValue(ptrInt,ptrValType)); T.assert(2*origValue === rc). - assert(rc === w.getMemValue(ptrInt,ptrValType)). + assert(rc === w.peek32(ptrInt)). assert(oldPtrInt === ptrInt); const pi64 = w.scopedAlloc(8)/*ptr to 64-bit integer*/; const o64 = 0x010203040506/*>32-bit integer*/; - const ptrType64 = 'i64'; if(w.bigIntEnabled){ - w.setMemValue(pi64, o64, ptrType64); + w.poke64(pi64, o64); //log("pi64 =",pi64, "o64 = 0x",o64.toString(16), o64); - const v64 = ()=>w.getMemValue(pi64,ptrType64) - //log("getMemValue(pi64)",v64()); + const v64 = ()=>w.peek64(pi64) T.assert(v64() == o64); - //T.assert(o64 === w.getMemValue(pi64, ptrType64)); + //T.assert(o64 === w.peek64(pi64)); const cf64w = w.xGet('sqlite3_wasm_test_int64ptr'); cf64w(pi64); - //log("getMemValue(pi64)",v64()); T.assert(v64() == BigInt(2 * o64)); cf64w(pi64); T.assert(v64() == BigInt(4 * o64)); @@ -1671,9 +1858,8 @@ const pMin = w.scopedAlloc(16); const pMax = pMin + 8; - const g64 = (p)=>w.getMemValue(p,ptrType64); - w.setMemValue(pMin, 0, ptrType64); - w.setMemValue(pMax, 0, ptrType64); + const g64 = (p)=>w.peek64(p); + w.poke64([pMin, pMax], 0); const minMaxI64 = [ w.xCall('sqlite3_wasm_test_int64_min'), w.xCall('sqlite3_wasm_test_int64_max') @@ -1685,7 +1871,7 @@ T.assert(g64(pMin) === minMaxI64[0], "int64 mismatch"). assert(g64(pMax) === minMaxI64[1], "int64 mismatch"); //log("pMin",g64(pMin), "pMax",g64(pMax)); - w.setMemValue(pMin, minMaxI64[0], ptrType64); + w.poke64(pMin, minMaxI64[0]); T.assert(g64(pMin) === minMaxI64[0]). assert(minMaxI64[0] === db.selectValue("select ?",g64(pMin))). assert(minMaxI64[1] === db.selectValue("select ?",g64(pMax))); @@ -1708,9 +1894,430 @@ } }/* jaccwabyt-specific tests */) + //////////////////////////////////////////////////////////////////////// + .t({ + name: 'virtual table #1: eponymous w/ manual exception handling', + predicate: ()=>!!capi.sqlite3_index_info, + test: function(sqlite3){ + warn("The vtab/module JS bindings are experimental and subject to change."); + const VT = sqlite3.vtab; + const tmplCols = Object.assign(Object.create(null),{ + A: 0, B: 1 + }); + /** + The vtab demonstrated here is a JS-ification of + ext/misc/templatevtab.c. + */ + const tmplMod = new sqlite3.capi.sqlite3_module(); + T.assert(0===tmplMod.$xUpdate); + tmplMod.setupModule({ + catchExceptions: false, + methods: { + xConnect: function(pDb, pAux, argc, argv, ppVtab, pzErr){ + try{ + const args = wasm.cArgvToJs(argc, argv); + T.assert(args.length>=3) + .assert(args[0] === 'testvtab') + .assert(args[1] === 'main') + .assert(args[2] === 'testvtab'); + //console.debug("xConnect() args =",args); + const rc = capi.sqlite3_declare_vtab( + pDb, "CREATE TABLE ignored(a,b)" + ); + if(0===rc){ + const t = VT.xVtab.create(ppVtab); + T.assert(t === VT.xVtab.get(wasm.peekPtr(ppVtab))); + } + return rc; + }catch(e){ + if(!(e instanceof sqlite3.WasmAllocError)){ + wasm.dealloc(wasm.peekPtr, pzErr); + wasm.pokePtr(pzErr, wasm.allocCString(e.message)); + } + return VT.xError('xConnect',e); + } + }, + xCreate: true /* just for testing. Will be removed afterwards. */, + xDisconnect: function(pVtab){ + try { + VT.xVtab.unget(pVtab).dispose(); + return 0; + }catch(e){ + return VT.xError('xDisconnect',e); + } + }, + xOpen: function(pVtab, ppCursor){ + try{ + const t = VT.xVtab.get(pVtab), + c = VT.xCursor.create(ppCursor); + T.assert(t instanceof capi.sqlite3_vtab) + .assert(c instanceof capi.sqlite3_vtab_cursor); + c._rowId = 0; + return 0; + }catch(e){ + return VT.xError('xOpen',e); + } + }, + xClose: function(pCursor){ + try{ + const c = VT.xCursor.unget(pCursor); + T.assert(c instanceof capi.sqlite3_vtab_cursor) + .assert(!VT.xCursor.get(pCursor)); + c.dispose(); + return 0; + }catch(e){ + return VT.xError('xClose',e); + } + }, + xNext: function(pCursor){ + try{ + const c = VT.xCursor.get(pCursor); + ++c._rowId; + return 0; + }catch(e){ + return VT.xError('xNext',e); + } + }, + xColumn: function(pCursor, pCtx, iCol){ + try{ + const c = VT.xCursor.get(pCursor); + switch(iCol){ + case tmplCols.A: + capi.sqlite3_result_int(pCtx, 1000 + c._rowId); + break; + case tmplCols.B: + capi.sqlite3_result_int(pCtx, 2000 + c._rowId); + break; + default: sqlite3.SQLite3Error.toss("Invalid column id",iCol); + } + return 0; + }catch(e){ + return VT.xError('xColumn',e); + } + }, + xRowid: function(pCursor, ppRowid64){ + try{ + const c = VT.xCursor.get(pCursor); + VT.xRowid(ppRowid64, c._rowId); + return 0; + }catch(e){ + return VT.xError('xRowid',e); + } + }, + xEof: function(pCursor){ + const c = VT.xCursor.get(pCursor), + rc = c._rowId>=10; + return rc; + }, + xFilter: function(pCursor, idxNum, idxCStr, + argc, argv/* [sqlite3_value* ...] */){ + try{ + const c = VT.xCursor.get(pCursor); + c._rowId = 0; + const list = capi.sqlite3_values_to_js(argc, argv); + T.assert(argc === list.length); + //log(argc,"xFilter value(s):",list); + return 0; + }catch(e){ + return VT.xError('xFilter',e); + } + }, + xBestIndex: function(pVtab, pIdxInfo){ + try{ + //const t = VT.xVtab.get(pVtab); + const sii = capi.sqlite3_index_info; + const pii = new sii(pIdxInfo); + pii.$estimatedRows = 10; + pii.$estimatedCost = 10.0; + //log("xBestIndex $nConstraint =",pii.$nConstraint); + if(pii.$nConstraint>0){ + // Validate nthConstraint() and nthConstraintUsage() + const max = pii.$nConstraint; + for(let i=0; i < max; ++i ){ + let v = pii.nthConstraint(i,true); + T.assert(wasm.isPtr(v)); + v = pii.nthConstraint(i); + T.assert(v instanceof sii.sqlite3_index_constraint) + .assert(v.pointer >= pii.$aConstraint); + v.dispose(); + v = pii.nthConstraintUsage(i,true); + T.assert(wasm.isPtr(v)); + v = pii.nthConstraintUsage(i); + T.assert(v instanceof sii.sqlite3_index_constraint_usage) + .assert(v.pointer >= pii.$aConstraintUsage); + v.$argvIndex = i;//just to get some values into xFilter + v.dispose(); + } + } + //log("xBestIndex $nOrderBy =",pii.$nOrderBy); + if(pii.$nOrderBy>0){ + // Validate nthOrderBy() + const max = pii.$nOrderBy; + for(let i=0; i < max; ++i ){ + let v = pii.nthOrderBy(i,true); + T.assert(wasm.isPtr(v)); + v = pii.nthOrderBy(i); + T.assert(v instanceof sii.sqlite3_index_orderby) + .assert(v.pointer >= pii.$aOrderBy); + v.dispose(); + } + } + pii.dispose(); + return 0; + }catch(e){ + return VT.xError('xBestIndex',e); + } + } + } + }); + this.db.onclose.disposeAfter.push(tmplMod); + T.assert(0===tmplMod.$xUpdate) + .assert(tmplMod.$xCreate) + .assert(tmplMod.$xCreate === tmplMod.$xConnect, + "setup() must make these equivalent and "+ + "installMethods() must avoid re-compiling identical functions"); + tmplMod.$xCreate = 0 /* make tmplMod eponymous-only */; + let rc = capi.sqlite3_create_module( + this.db, "testvtab", tmplMod, 0 + ); + this.db.checkRc(rc); + const list = this.db.selectArrays( + "SELECT a,b FROM testvtab where a<9999 and b>1 order by a, b" + /* Query is shaped so that it will ensure that some constraints + end up in xBestIndex(). */ + ); + T.assert(10===list.length) + .assert(1000===list[0][0]) + .assert(2009===list[list.length-1][1]); + } + })/*custom vtab #1*/ + + //////////////////////////////////////////////////////////////////////// + .t({ + name: 'virtual table #2: non-eponymous w/ automated exception wrapping', + predicate: ()=>!!capi.sqlite3_index_info, + test: function(sqlite3){ + warn("The vtab/module JS bindings are experimental and subject to change."); + const VT = sqlite3.vtab; + const tmplCols = Object.assign(Object.create(null),{ + A: 0, B: 1 + }); + /** + The vtab demonstrated here is a JS-ification of + ext/misc/templatevtab.c. + */ + let throwOnCreate = 1 ? 0 : capi.SQLITE_CANTOPEN + /* ^^^ just for testing exception wrapping. Note that sqlite + always translates errors from a vtable to a generic + SQLITE_ERROR unless it's from xConnect()/xCreate() and that + callback sets an error string. */; + const vtabTrace = 1 + ? ()=>{} + : (methodName,...args)=>console.debug('sqlite3_module::'+methodName+'():',...args); + const modConfig = { + /* catchExceptions changes how the methods are wrapped */ + catchExceptions: true, + name: "vtab2test", + methods:{ + xCreate: function(pDb, pAux, argc, argv, ppVtab, pzErr){ + vtabTrace("xCreate",...arguments); + if(throwOnCreate){ + sqlite3.SQLite3Error.toss( + throwOnCreate, + "Throwing a test exception." + ); + } + const args = wasm.cArgvToJs(argc, argv); + vtabTrace("xCreate","argv:",args); + T.assert(args.length>=3); + const rc = capi.sqlite3_declare_vtab( + pDb, "CREATE TABLE ignored(a,b)" + ); + if(0===rc){ + const t = VT.xVtab.create(ppVtab); + T.assert(t === VT.xVtab.get(wasm.peekPtr(ppVtab))); + vtabTrace("xCreate",...arguments," ppVtab =",t.pointer); + } + return rc; + }, + xConnect: true, + xDestroy: function(pVtab){ + vtabTrace("xDestroy/xDisconnect",pVtab); + VT.xVtab.dispose(pVtab); + }, + xDisconnect: true, + xOpen: function(pVtab, ppCursor){ + const t = VT.xVtab.get(pVtab), + c = VT.xCursor.create(ppCursor); + T.assert(t instanceof capi.sqlite3_vtab) + .assert(c instanceof capi.sqlite3_vtab_cursor); + vtabTrace("xOpen",...arguments," cursor =",c.pointer); + c._rowId = 0; + }, + xClose: function(pCursor){ + vtabTrace("xClose",...arguments); + const c = VT.xCursor.unget(pCursor); + T.assert(c instanceof capi.sqlite3_vtab_cursor) + .assert(!VT.xCursor.get(pCursor)); + c.dispose(); + }, + xNext: function(pCursor){ + vtabTrace("xNext",...arguments); + const c = VT.xCursor.get(pCursor); + ++c._rowId; + }, + xColumn: function(pCursor, pCtx, iCol){ + vtabTrace("xColumn",...arguments); + const c = VT.xCursor.get(pCursor); + switch(iCol){ + case tmplCols.A: + capi.sqlite3_result_int(pCtx, 1000 + c._rowId); + break; + case tmplCols.B: + capi.sqlite3_result_int(pCtx, 2000 + c._rowId); + break; + default: sqlite3.SQLite3Error.toss("Invalid column id",iCol); + } + }, + xRowid: function(pCursor, ppRowid64){ + vtabTrace("xRowid",...arguments); + const c = VT.xCursor.get(pCursor); + VT.xRowid(ppRowid64, c._rowId); + }, + xEof: function(pCursor){ + vtabTrace("xEof",...arguments); + return VT.xCursor.get(pCursor)._rowId>=10; + }, + xFilter: function(pCursor, idxNum, idxCStr, + argc, argv/* [sqlite3_value* ...] */){ + vtabTrace("xFilter",...arguments); + const c = VT.xCursor.get(pCursor); + c._rowId = 0; + const list = capi.sqlite3_values_to_js(argc, argv); + T.assert(argc === list.length); + }, + xBestIndex: function(pVtab, pIdxInfo){ + vtabTrace("xBestIndex",...arguments); + //const t = VT.xVtab.get(pVtab); + const pii = VT.xIndexInfo(pIdxInfo); + pii.$estimatedRows = 10; + pii.$estimatedCost = 10.0; + pii.dispose(); + } + }/*methods*/ + }; + const tmplMod = VT.setupModule(modConfig); + T.assert(1===tmplMod.$iVersion); + this.db.onclose.disposeAfter.push(tmplMod); + this.db.checkRc(capi.sqlite3_create_module( + this.db.pointer, modConfig.name, tmplMod.pointer, 0 + )); + this.db.exec([ + "create virtual table testvtab2 using ", + modConfig.name, + "(arg1 blah, arg2 bloop)" + ]); + if(0){ + /* If we DROP TABLE then xDestroy() is called. If the + vtab is instead destroyed when the db is closed, + xDisconnect() is called. */ + this.db.onclose.disposeBefore.push(function(db){ + console.debug("Explicitly dropping testvtab2 via disposeBefore handler..."); + db.exec( + /** DROP TABLE is the only way to get xDestroy() to be called. */ + "DROP TABLE testvtab2" + ); + }); + } + let list = this.db.selectArrays( + "SELECT a,b FROM testvtab2 where a<9999 and b>1 order by a, b" + /* Query is shaped so that it will ensure that some + constraints end up in xBestIndex(). */ + ); + T.assert(10===list.length) + .assert(1000===list[0][0]) + .assert(2009===list[list.length-1][1]); + + list = this.db.selectArrays( + "SELECT a,b FROM testvtab2 where a<9999 and b>1 order by b, a limit 5" + ); + T.assert(5===list.length) + .assert(1000===list[0][0]) + .assert(2004===list[list.length-1][1]); + + // Call it as a table-valued function... + list = this.db.selectArrays([ + "SELECT a,b FROM ", modConfig.name, + " where a<9999 and b>1 order by b, a limit 1" + ]); + T.assert(1===list.length) + .assert(1000===list[0][0]) + .assert(2000===list[0][1]); + } + })/*custom vtab #2*/ + //////////////////////////////////////////////////////////////////////// + .t('Custom collation', function(sqlite3){ + let collationCounter = 0; + let myCmp = function(pArg,n1,p1,n2,p2){ + //int (*)(void*,int,const void*,int,const void*) + ++collationCounter; + const rc = wasm.exports.sqlite3_strnicmp(p1,p2,(n1this.db.checkRc(rc), + /SQLITE_UTF8 is the only supported encoding./); + /* + We need to ensure that replacing that collation function does + the right thing. We don't have a handle to the underlying WASM + pointer from here, so cannot verify (without digging through + internal state) that the old one gets uninstalled, but we can + verify that a new one properly replaces it. (That said, + console.warn() output has shown that the uninstallation does + happen.) + */ + collationCounter = 0; + myCmp = function(pArg,n1,p1,n2,p2){ + --collationCounter; + return 0; + }; + rc = capi.sqlite3_create_collation_v2(this.db, "MYCOLLATION", capi.SQLITE_UTF8, + 0, myCmp, 0); + this.db.checkRc(rc); + rc = this.db.selectValue("select 'hi' = 'HI' collate mycollation"); + T.assert(rc>0).assert(-1===collationCounter); + rc = this.db.selectValue("select 'a' = 'b' collate mycollation"); + T.assert(rc>0).assert(-2===collationCounter); + rc = capi.sqlite3_create_collation_v2(this.db, "MYCOLLATION", capi.SQLITE_UTF8, + 0, null, 0); + this.db.checkRc(rc); + rc = 0; + try { + this.db.selectValue("select 'a' = 'b' collate mycollation"); + }catch(e){ + /* Why is e.resultCode not automatically an extended result + code? The DB() class enables those automatically. */ + rc = sqlite3.capi.sqlite3_extended_errcode(this.db); + } + T.assert(capi.SQLITE_ERROR_MISSING_COLLSEQ === rc); + })/*custom collation*/ + + //////////////////////////////////////////////////////////////////////// .t('Close db', function(){ - T.assert(this.db).assert(Number.isInteger(this.db.pointer)); - wasm.exports.sqlite3_wasm_db_reset(this.db.pointer); + T.assert(this.db).assert(wasm.isPtr(this.db.pointer)); + //wasm.sqlite3_wasm_db_reset(this.db); // will leak virtual tables! this.db.close(); T.assert(!this.db.pointer); }) @@ -1718,52 +2325,81 @@ //////////////////////////////////////////////////////////////////////// T.g('kvvfs') - .t('kvvfs sanity checks', function(sqlite3){ - if(isWorker()){ + .t({ + name: 'kvvfs is disabled in worker', + predicate: ()=>(isWorker() || "test is only valid in a Worker"), + test: function(sqlite3){ T.assert( !capi.sqlite3_vfs_find('kvvfs'), "Expecting kvvfs to be unregistered." ); - log("kvvfs is (correctly) unavailable in a Worker."); - return; } - const filename = 'session'; - const pVfs = capi.sqlite3_vfs_find('kvvfs'); - T.assert(pVfs); - const JDb = sqlite3.oo1.JsStorageDb; - const unlink = ()=>JDb.clearStorage(filename); - unlink(); - let db = new JDb(filename); - try { - db.exec([ - 'create table kvvfs(a);', - 'insert into kvvfs(a) values(1),(2),(3)' - ]); - T.assert(3 === db.selectValue('select count(*) from kvvfs')); - db.close(); - db = new JDb(filename); - db.exec('insert into kvvfs(a) values(4),(5),(6)'); - T.assert(6 === db.selectValue('select count(*) from kvvfs')); - }finally{ - db.close(); + }) + .t({ + name: 'kvvfs in main thread', + predicate: ()=>(isUIThread() + || "local/sessionStorage are unavailable in a Worker"), + test: function(sqlite3){ + const filename = this.kvvfsDbFile = 'session'; + const pVfs = capi.sqlite3_vfs_find('kvvfs'); + T.assert(pVfs); + const JDb = this.JDb = sqlite3.oo1.JsStorageDb; + const unlink = this.kvvfsUnlink = ()=>{JDb.clearStorage(filename)}; unlink(); + let db = new JDb(filename); + try { + db.exec([ + 'create table kvvfs(a);', + 'insert into kvvfs(a) values(1),(2),(3)' + ]); + T.assert(3 === db.selectValue('select count(*) from kvvfs')); + db.close(); + db = new JDb(filename); + db.exec('insert into kvvfs(a) values(4),(5),(6)'); + T.assert(6 === db.selectValue('select count(*) from kvvfs')); + }finally{ + db.close(); + } } }/*kvvfs sanity checks*/) + .t({ + name: 'kvvfs sqlite3_js_vfs_create_file()', + predicate: ()=>"kvvfs does not currently support this", + test: function(sqlite3){ + let db; + try { + db = new this.JDb(this.kvvfsDbFile); + const exp = capi.sqlite3_js_db_export(db); + db.close(); + this.kvvfsUnlink(); + capi.sqlite3_js_vfs_create_file("kvvfs", this.kvvfsDbFile, exp); + db = new this.JDb(filename); + T.assert(6 === db.selectValue('select count(*) from kvvfs')); + }finally{ + db.close(); + this.kvvfsUnlink(); + } + delete this.kvvfsDbFile; + delete this.kvvfsUnlink; + delete this.JDb; + } + }/*kvvfs sqlite3_js_vfs_create_file()*/) ;/* end kvvfs tests */ //////////////////////////////////////////////////////////////////////// - T.g('OPFS (Worker thread only and only in supported browsers)', - (sqlite3)=>{return !!sqlite3.opfs}) + T.g('OPFS: Origin-Private File System', + (sqlite3)=>(sqlite3.opfs + ? true : "requires Worker thread in a compatible browser")) .t({ - name: 'OPFS sanity checks', + name: 'OPFS db sanity checks', test: async function(sqlite3){ - const opfs = sqlite3.opfs; - const filename = 'sqlite3-tester1.db'; - const pVfs = capi.sqlite3_vfs_find('opfs'); + const filename = this.opfsDbFile = 'sqlite3-tester1.db'; + const pVfs = this.opfsVfs = capi.sqlite3_vfs_find('opfs'); T.assert(pVfs); - const unlink = (fn=filename)=>wasm.sqlite3_wasm_vfs_unlink(pVfs,fn); + const unlink = this.opfsUnlink = + (fn=filename)=>{wasm.sqlite3_wasm_vfs_unlink(pVfs,fn)}; unlink(); - let db = new opfs.OpfsDb(filename); + let db = new sqlite3.oo1.OpfsDb(filename); try { db.exec([ 'create table p(a);', @@ -1771,32 +2407,89 @@ ]); T.assert(3 === db.selectValue('select count(*) from p')); db.close(); - db = new opfs.OpfsDb(filename); + db = new sqlite3.oo1.OpfsDb(filename); db.exec('insert into p(a) values(4),(5),(6)'); T.assert(6 === db.selectValue('select count(*) from p')); + this.opfsDbExport = capi.sqlite3_js_db_export(db); + T.assert(this.opfsDbExport instanceof Uint8Array) + .assert(this.opfsDbExport.byteLength>0 + && 0===this.opfsDbExport.byteLength % 512); }finally{ db.close(); unlink(); } + } + }/*OPFS db sanity checks*/) + .t({ + name: 'OPFS export/import', + test: async function(sqlite3){ + let db; + try { + const exp = this.opfsDbExport; + delete this.opfsDbExport; + capi.sqlite3_js_vfs_create_file("opfs", this.opfsDbFile, exp); + const db = new sqlite3.oo1.OpfsDb(this.opfsDbFile); + T.assert(6 === db.selectValue('select count(*) from p')); + }finally{ + if(db) db.close(); + } + } + }/*OPFS export/import*/) + .t({ + name: 'OPFS utility APIs and sqlite3_js_vfs_create_file()', + test: async function(sqlite3){ + const filename = this.opfsDbFile; + const pVfs = this.opfsVfs; + const unlink = this.opfsUnlink; + T.assert(filename && pVfs && !!unlink); + delete this.opfsDbFile; + delete this.opfsVfs; + delete this.opfsUnlink; + unlink(); + // Sanity-test sqlite3_js_vfs_create_file()... + /************************************************************** + ATTENTION CLIENT-SIDE USERS: sqlite3.opfs is NOT intended + for client-side use. It is only for this project's own + internal use. Its APIs are subject to change or removal at + any time. + ***************************************************************/ + const opfs = sqlite3.opfs; + const fSize = 1379; + let sh; + try{ + T.assert(!(await opfs.entryExists(filename))); + capi.sqlite3_js_vfs_create_file( + pVfs, filename, null, fSize + ); + T.assert(await opfs.entryExists(filename)); + let fh = await opfs.rootDirectory.getFileHandle(filename); + sh = await fh.createSyncAccessHandle(); + T.assert(fSize === await sh.getSize()); + await sh.close(); + sh = undefined; + unlink(); + T.assert(!(await opfs.entryExists(filename))); - if(1){ - // Sanity-test sqlite3_wasm_vfs_create_file()... - const fSize = 1379; - let sh; - try{ - T.assert(!(await opfs.entryExists(filename))); - let rc = wasm.sqlite3_wasm_vfs_create_file( - pVfs, filename, null, fSize + const ba = new Uint8Array([1,2,3,4,5]); + capi.sqlite3_js_vfs_create_file( + "opfs", filename, ba + ); + T.assert(await opfs.entryExists(filename)); + fh = await opfs.rootDirectory.getFileHandle(filename); + sh = await fh.createSyncAccessHandle(); + T.assert(ba.byteLength === await sh.getSize()); + await sh.close(); + sh = undefined; + unlink(); + + T.mustThrowMatching(()=>{ + capi.sqlite3_js_vfs_create_file( + "no-such-vfs", filename, ba ); - T.assert(0===rc) - .assert(await opfs.entryExists(filename)); - const fh = await opfs.rootDirectory.getFileHandle(filename); - sh = await fh.createSyncAccessHandle(); - T.assert(fSize === await sh.getSize()); - }finally{ - if(sh) await sh.close(); - unlink(); - } + }, "Unknown sqlite3_vfs name: no-such-vfs"); + }finally{ + if(sh) await sh.close(); + unlink(); } // Some sanity checks of the opfs utility functions... @@ -1812,12 +2505,13 @@ .assert(!(await opfs.entryExists(testDir)), "entryExists(",testDir,") should have failed"); } - }/*OPFS sanity checks*/) + }/*OPFS util sanity checks*/) ;/* end OPFS tests */ //////////////////////////////////////////////////////////////////////// log("Loading and initializing sqlite3 WASM module..."); - if(!isUIThread()){ + if(!self.sqlite3InitModule && !isUIThread()){ + /* Vanilla worker, as opposed to an ES6 module worker */ /* If sqlite3.js is in a directory other than this script, in order to get sqlite3.js to resolve sqlite3.wasm properly, we have to @@ -1839,6 +2533,9 @@ } importScripts(sqlite3Js); } + self.sqlite3InitModule.__isUnderTest = + true /* disables certain API-internal cleanup so that we can + test internal APIs from here */; self.sqlite3InitModule({ print: log, printErr: error @@ -1861,4 +2558,5 @@ } TestUtil.runTests(sqlite3); }); -})(); +})(self); + diff --git a/ext/wasm/tests/opfs/concurrency/index.html b/ext/wasm/tests/opfs/concurrency/index.html new file mode 100644 index 0000000000..9a826c6e13 --- /dev/null +++ b/ext/wasm/tests/opfs/concurrency/index.html @@ -0,0 +1,50 @@ + + + + + + + + sqlite3 OPFS Worker concurrency tester + + + +

    +

    + OPFS concurrency tester using multiple independent Workers. + Disclaimer: concurrency in OPFS is currently a pain point! +

    +

    + URL flags: pass a number of workers using + the workers=N URL flag. Set the time between each + workload with interval=N (milliseconds). Set the + number of worker iterations with iterations=N. + Enable OPFS VFS verbosity with verbose=1-3 (output + goes to the dev console). Enable/disable "unlock ASAP" mode + (higher concurrency, lower speed) + with unlock-asap=0-1. +

    +

    Achtung: if it does not start to do anything within a couple of + seconds, check the dev console: Chrome sometimes fails to load + the wasm module due to "cannot allocate WasmMemory." Closing and + re-opening the tab usually resolves it, but sometimes restarting + the browser is required. +

    +
    + + +
    +
    + + + + diff --git a/ext/wasm/tests/opfs/concurrency/test.js b/ext/wasm/tests/opfs/concurrency/test.js new file mode 100644 index 0000000000..14cd6f514f --- /dev/null +++ b/ext/wasm/tests/opfs/concurrency/test.js @@ -0,0 +1,136 @@ +(async function(self){ + + const logCss = (function(){ + const mapToString = (v)=>{ + switch(typeof v){ + case 'number': case 'string': case 'boolean': + case 'undefined': case 'bigint': + return ''+v; + default: break; + } + if(null===v) return 'null'; + if(v instanceof Error){ + v = { + message: v.message, + stack: v.stack, + errorClass: v.name + }; + } + return JSON.stringify(v,undefined,2); + }; + const normalizeArgs = (args)=>args.map(mapToString); + const logTarget = document.querySelector('#test-output'); + const logCss = function(cssClass,...args){ + const ln = document.createElement('div'); + if(cssClass){ + for(const c of (Array.isArray(cssClass) ? cssClass : [cssClass])){ + ln.classList.add(c); + } + } + ln.append(document.createTextNode(normalizeArgs(args).join(' '))); + logTarget.append(ln); + }; + const cbReverse = document.querySelector('#cb-log-reverse'); + const cbReverseKey = 'tester1:cb-log-reverse'; + const cbReverseIt = ()=>{ + logTarget.classList[cbReverse.checked ? 'add' : 'remove']('reverse'); + localStorage.setItem(cbReverseKey, cbReverse.checked ? 1 : 0); + }; + cbReverse.addEventListener('change', cbReverseIt, true); + if(localStorage.getItem(cbReverseKey)){ + cbReverse.checked = !!(+localStorage.getItem(cbReverseKey)); + } + cbReverseIt(); + return logCss; + })(); + const stdout = (...args)=>logCss('',...args); + const stderr = (...args)=>logCss('error',...args); + + const wait = async (ms)=>{ + return new Promise((resolve)=>setTimeout(resolve,ms)); + }; + + const urlArgsJs = new URL(document.currentScript.src).searchParams; + const urlArgsHtml = new URL(self.location.href).searchParams; + const options = Object.create(null); + options.sqlite3Dir = urlArgsJs.get('sqlite3.dir'); + options.workerCount = ( + urlArgsHtml.has('workers') ? +urlArgsHtml.get('workers') : 3 + ) || 4; + options.opfsVerbose = ( + urlArgsHtml.has('verbose') ? +urlArgsHtml.get('verbose') : 1 + ) || 1; + options.interval = ( + urlArgsHtml.has('interval') ? +urlArgsHtml.get('interval') : 1000 + ) || 1000; + options.iterations = ( + urlArgsHtml.has('iterations') ? +urlArgsHtml.get('iterations') : 10 + ) || 10; + options.unlockAsap = ( + urlArgsHtml.has('unlock-asap') ? +urlArgsHtml.get('unlock-asap') : 0 + ) || 0; + options.noUnlink = !!urlArgsHtml.has('no-unlink'); + const workers = []; + workers.post = (type,...args)=>{ + for(const w of workers) w.postMessage({type, payload:args}); + }; + workers.counts = {loaded: 0, passed: 0, failed: 0}; + const checkFinished = function(){ + if(workers.counts.passed + workers.counts.failed !== workers.length){ + return; + } + if(workers.counts.failed>0){ + logCss('tests-fail',"Finished with",workers.counts.failed,"failure(s)."); + }else{ + logCss('tests-pass',"All",workers.length,"workers finished."); + } + }; + workers.onmessage = function(msg){ + msg = msg.data; + const prefix = 'Worker #'+msg.worker+':'; + switch(msg.type){ + case 'loaded': + stdout(prefix,"loaded"); + if(++workers.counts.loaded === workers.length){ + stdout("All",workers.length,"workers loaded. Telling them to run..."); + workers.post('run'); + } + break; + case 'stdout': stdout(prefix,...msg.payload); break; + case 'stderr': stderr(prefix,...msg.payload); break; + case 'error': stderr(prefix,"ERROR:",...msg.payload); break; + case 'finished': + ++workers.counts.passed; + logCss('tests-pass',prefix,...msg.payload); + checkFinished(); + break; + case 'failed': + ++workers.counts.failed; + logCss('tests-fail',prefix,"FAILED:",...msg.payload); + checkFinished(); + break; + default: logCss('error',"Unhandled message type:",msg); break; + } + }; + + stdout("Launching",options.workerCount,"workers. Options:",options); + workers.uri = ( + 'worker.js?' + + 'sqlite3.dir='+options.sqlite3Dir + + '&interval='+options.interval + + '&iterations='+options.iterations + + '&opfs-verbose='+options.opfsVerbose + + '&opfs-unlock-asap='+options.unlockAsap + ); + for(let i = 0; i < options.workerCount; ++i){ + stdout("Launching worker..."); + workers.push(new Worker( + workers.uri+'&workerId='+(i+1)+( + (i || options.noUnlink) ? '' : '&unlink-db' + ) + )); + } + // Have to delay onmessage assignment until after the loop + // to avoid that early workers get an undue head start. + workers.forEach((w)=>w.onmessage = workers.onmessage); +})(self); diff --git a/ext/wasm/tests/opfs/concurrency/worker.js b/ext/wasm/tests/opfs/concurrency/worker.js new file mode 100644 index 0000000000..5d28bedee0 --- /dev/null +++ b/ext/wasm/tests/opfs/concurrency/worker.js @@ -0,0 +1,113 @@ +importScripts( + (new URL(self.location.href).searchParams).get('sqlite3.dir') + '/sqlite3.js' +); +self.sqlite3InitModule().then(async function(sqlite3){ + const urlArgs = new URL(self.location.href).searchParams; + const options = { + workerName: urlArgs.get('workerId') || Math.round(Math.random()*10000), + unlockAsap: urlArgs.get('opfs-unlock-asap') || 0 /*EXPERIMENTAL*/ + }; + const wPost = (type,...payload)=>{ + postMessage({type, worker: options.workerName, payload}); + }; + const stdout = (...args)=>wPost('stdout',...args); + const stderr = (...args)=>wPost('stderr',...args); + if(!sqlite3.opfs){ + stderr("OPFS support not detected. Aborting."); + return; + } + + const wait = async (ms)=>{ + return new Promise((resolve)=>setTimeout(resolve,ms)); + }; + + const dbName = 'concurrency-tester.db'; + if(urlArgs.has('unlink-db')){ + await sqlite3.opfs.unlink(dbName); + stdout("Unlinked",dbName); + } + wPost('loaded'); + let db; + const interval = Object.assign(Object.create(null),{ + delay: urlArgs.has('interval') ? (+urlArgs.get('interval') || 750) : 750, + handle: undefined, + count: 0 + }); + const finish = ()=>{ + if(db){ + if(!db.pointer) return; + db.close(); + } + if(interval.error){ + wPost('failed',"Ending work after interval #"+interval.count, + "due to error:",interval.error); + }else{ + wPost('finished',"Ending work after",interval.count,"intervals."); + } + }; + const run = async function(){ + db = new sqlite3.oo1.OpfsDb({ + filename: 'file:'+dbName+'?opfs-unlock-asap='+options.unlockAsap, + flags: 'c' + }); + sqlite3.capi.sqlite3_busy_timeout(db.pointer, 5000); + db.transaction((db)=>{ + db.exec([ + "create table if not exists t1(w TEXT UNIQUE ON CONFLICT REPLACE,v);", + "create table if not exists t2(w TEXT UNIQUE ON CONFLICT REPLACE,v);" + ]); + }); + + const maxIterations = + urlArgs.has('iterations') ? (+urlArgs.get('iterations') || 10) : 10; + stdout("Starting interval-based db updates with delay of",interval.delay,"ms."); + const doWork = async ()=>{ + const tm = new Date().getTime(); + ++interval.count; + const prefix = "v(#"+interval.count+")"; + stdout("Setting",prefix,"=",tm); + try{ + db.exec({ + sql:"INSERT OR REPLACE INTO t1(w,v) VALUES(?,?)", + bind: [options.workerName, new Date().getTime()] + }); + //stdout("Set",prefix); + }catch(e){ + interval.error = e; + } + }; + if(1){/*use setInterval()*/ + setTimeout(async function timer(){ + await doWork(); + if(interval.error || maxIterations === interval.count){ + finish(); + }else{ + setTimeout(timer, interval.delay); + } + }, interval.delay); + }else{ + /*This approach provides no concurrency whatsoever: each worker + is run to completion before any others can work.*/ + let i; + for(i = 0; i < maxIterations; ++i){ + await doWork(); + if(interval.error) break; + await wait(interval.ms); + } + finish(); + } + }/*run()*/; + + self.onmessage = function({data}){ + switch(data.type){ + case 'run': run().catch((e)=>{ + if(!interval.error) interval.error = e; + finish(); + }); + break; + default: + stderr("Unhandled message type '"+data.type+"'."); + break; + } + }; +}); diff --git a/ext/wasm/wasmfs.make b/ext/wasm/wasmfs.make index 81b41870c8..020014f3e8 100644 --- a/ext/wasm/wasmfs.make +++ b/ext/wasm/wasmfs.make @@ -15,98 +15,117 @@ MAKEFILE.wasmfs := $(lastword $(MAKEFILE_LIST)) # Worker to an Emscripten quirk regarding loading nested Workers. dir.wasmfs := $(dir.wasm) sqlite3-wasmfs.js := $(dir.wasmfs)/sqlite3-wasmfs.js +sqlite3-wasmfs.mjs := $(dir.wasmfs)/sqlite3-wasmfs.mjs sqlite3-wasmfs.wasm := $(dir.wasmfs)/sqlite3-wasmfs.wasm CLEAN_FILES += $(sqlite3-wasmfs.js) $(sqlite3-wasmfs.wasm) \ - $(subst .js,.worker.js,$(sqlite3-wasmfs.js)) + $(subst .js,.worker.js,$(sqlite3-wasmfs.js)) \ + $(sqlite3-wasmfs.mjs) \ + $(subst .mjs,.worker.mjs,$(sqlite3-wasmfs.mjs)) ######################################################################## # emcc flags for .c/.o. -sqlite3-wasmfs.cflags := -sqlite3-wasmfs.cflags += -std=c99 -fPIC -sqlite3-wasmfs.cflags += -pthread -sqlite3-wasmfs.cflags += $(cflags.common) -sqlite3-wasmfs.cflags += $(SQLITE_OPT) -DSQLITE_ENABLE_WASMFS +cflags.sqlite3-wasmfs := +cflags.sqlite3-wasmfs += -std=c99 -fPIC +cflags.sqlite3-wasmfs += -pthread +cflags.sqlite3-wasmfs += $(cflags.speedtest1) +cflags.sqlite3-wasmfs += $(SQLITE_OPT) -DSQLITE_ENABLE_WASMFS ######################################################################## # emcc flags specific to building the final .js/.wasm file... -sqlite3-wasmfs.jsflags := -fPIC -sqlite3-wasmfs.jsflags += --no-entry -sqlite3-wasmfs.jsflags += --minify 0 -sqlite3-wasmfs.jsflags += -sMODULARIZE -sqlite3-wasmfs.jsflags += -sSTRICT_JS -sqlite3-wasmfs.jsflags += -sDYNAMIC_EXECUTION=0 -sqlite3-wasmfs.jsflags += -sNO_POLYFILL -sqlite3-wasmfs.jsflags += -sEXPORTED_FUNCTIONS=@$(abspath $(dir.api)/EXPORTED_FUNCTIONS.sqlite3-api) -sqlite3-wasmfs.jsflags += -sEXPORTED_RUNTIME_METHODS=FS,wasmMemory,allocateUTF8OnStack - # wasmMemory ==> for -sIMPORTED_MEMORY - # allocateUTF8OnStack ==> wasmfs internals -sqlite3-wasmfs.jsflags += -sUSE_CLOSURE_COMPILER=0 -sqlite3-wasmfs.jsflags += -sIMPORTED_MEMORY -#sqlite3-wasmfs.jsflags += -sINITIAL_MEMORY=13107200 -#sqlite3-wasmfs.jsflags += -sTOTAL_STACK=4194304 -sqlite3-wasmfs.jsflags += -sEXPORT_NAME=$(sqlite3.js.init-func) -sqlite3-wasmfs.jsflags += -sGLOBAL_BASE=4096 # HYPOTHETICALLY keep func table indexes from overlapping w/ heap addr. -#sqlite3-wasmfs.jsflags += -sFILESYSTEM=0 # only for experimentation. sqlite3 needs the FS API -# Perhaps the wasmfs build doesn't? -#sqlite3-wasmfs.jsflags += -sABORTING_MALLOC -sqlite3-wasmfs.jsflags += -sALLOW_TABLE_GROWTH -sqlite3-wasmfs.jsflags += -Wno-limited-postlink-optimizations +emcc.flags.sqlite3-wasmfs := -fPIC +emcc.flags.sqlite3-wasmfs += --no-entry +emcc.flags.sqlite3-wasmfs += --minify 0 +emcc.flags.sqlite3-wasmfs += -sMODULARIZE +emcc.flags.sqlite3-wasmfs += -sEXPORT_NAME=$(sqlite3.js.init-func) +emcc.flags.sqlite3-wasmfs += -sSTRICT_JS +emcc.flags.sqlite3-wasmfs += -sDYNAMIC_EXECUTION=0 +emcc.flags.sqlite3-wasmfs += -sNO_POLYFILL +emcc.flags.sqlite3-wasmfs += -sWASM_BIGINT=$(emcc.WASM_BIGINT) +emcc.flags.sqlite3-wasmfs += -sEXPORTED_FUNCTIONS=@$(abspath $(dir.api)/EXPORTED_FUNCTIONS.sqlite3-api) +emcc.flags.sqlite3-wasmfs += -sEXPORTED_RUNTIME_METHODS=FS,wasmMemory,allocateUTF8OnStack + # wasmMemory ==> for -sIMPORTED_MEMORY + # allocateUTF8OnStack ==> wasmfs internals +emcc.flags.sqlite3-wasmfs += -sUSE_CLOSURE_COMPILER=0 +emcc.flags.sqlite3-wasmfs += -Wno-limited-postlink-optimizations # ^^^^^ it likes to warn when we have "limited optimizations" via the -g3 flag. -sqlite3-wasmfs.jsflags += -sERROR_ON_UNDEFINED_SYMBOLS=0 -sqlite3-wasmfs.jsflags += -sLLD_REPORT_UNDEFINED -#sqlite3-wasmfs.jsflags += --import-undefined -sqlite3-wasmfs.jsflags += -sMEMORY64=0 -sqlite3-wasmfs.jsflags += -sINITIAL_MEMORY=128450560 +emcc.flags.sqlite3-wasmfs += -sALLOW_TABLE_GROWTH +emcc.flags.sqlite3-wasmfs += -sSTACK_SIZE=512KB +emcc.flags.sqlite3-wasmfs += -sGLOBAL_BASE=4096 # HYPOTHETICALLY keep func table indexes from overlapping w/ heap addr. +emcc.flags.sqlite3-wasmfs += -sMEMORY64=0 +emcc.flags.sqlite3-wasmfs += -sIMPORTED_MEMORY +emcc.flags.sqlite3-wasmfs += -sINITIAL_MEMORY=$(emcc.INITIAL_MEMORY.128) # ^^^^ 64MB is not enough for WASMFS/OPFS test runs using batch-runner.js -sqlite3-wasmfs.fsflags := -pthread -sWASMFS -sPTHREAD_POOL_SIZE=2 -sENVIRONMENT=web,worker -# -sPTHREAD_POOL_SIZE values of 2 or higher trigger that bug. -sqlite3-wasmfs.jsflags += $(sqlite3-wasmfs.fsflags) -#sqlite3-wasmfs.jsflags += -sALLOW_MEMORY_GROWTH +sqlite3-wasmfs.fsflags := -pthread -sWASMFS \ + -sPTHREAD_POOL_SIZE=2 -sENVIRONMENT=web,worker \ + -sERROR_ON_UNDEFINED_SYMBOLS=0 -sLLD_REPORT_UNDEFINED +# ^^^^^ why undefined symbols are necessary for the wasmfs build is anyone's guess. +emcc.flags.sqlite3-wasmfs += $(sqlite3-wasmfs.fsflags) +#emcc.flags.sqlite3-wasmfs += -sALLOW_MEMORY_GROWTH #^^^ using ALLOW_MEMORY_GROWTH produces a warning from emcc: # USE_PTHREADS + ALLOW_MEMORY_GROWTH may run non-wasm code slowly, # see https://github.com/WebAssembly/design/issues/1271 [-Wpthreads-mem-growth] -sqlite3-wasmfs.jsflags += -sWASM_BIGINT=$(emcc.WASM_BIGINT) -$(eval $(call call-make-pre-js,sqlite3-wasmfs)) -sqlite3-wasmfs.jsflags += $(pre-post-common.flags) $(pre-post-sqlite3-wasmfs.flags) -$(sqlite3-wasmfs.js): $(sqlite3-wasm.c) \ - $(EXPORTED_FUNCTIONS.api) $(MAKEFILE) $(MAKEFILE.wasmfs) \ - $(pre-post-sqlite3-wasmfs.deps) +# And, indeed, it runs slowly if memory is permitted to grow. +emcc.flags.sqlite3-wasmfs.vanilla := +emcc.flags.sqlite3-wasmfs.esm := -sEXPORT_ES6 -sUSE_ES6_IMPORT_META +$(eval $(call call-make-pre-js,sqlite3-wasmfs,vanilla)) +$(eval $(call call-make-pre-js,sqlite3-wasmfs,esm)) +Xemcc.flags.sqlite3-wasmfs.vanilla += \ + $(pre-post-common.flags.vanilla) \ + $(pre-post-sqlite3-wasmfs.flags.vanilla) +Xemcc.flags.sqlite3-wasmfs.esm += \ + $(pre-post-common.flags.esm) \ + $(pre-post-sqlite3-wasmfs.flags.esm) +$(sqlite3-wasmfs.js) $(sqlite3-wasmfs.mjs): $(sqlite3-wasm.c) \ + $(EXPORTED_FUNCTIONS.api) $(MAKEFILE) $(MAKEFILE.wasmfs) +$(sqlite3-wasmfs.js): $(pre-post-sqlite3-wasmfs.deps.vanilla) +$(sqlite3-wasmfs.mjs): $(pre-post-sqlite3-wasmfs.deps.esm) +# SQLITE3-WASMFS.xJS.RECIPE is the wasmfs-specific counterpart +# of SQLITE3.xJS.RECIPE from the main makefile. +define SQLITE3-WASMFS.xJS.RECIPE @echo "Building $@ ..." $(emcc.bin) -o $@ $(emcc_opt_full) $(emcc.flags) \ - $(sqlite3-wasmfs.cflags) $(sqlite3-wasmfs.jsflags) \ + $(cflags.sqlite3-wasmfs) \ + $(emcc.flags.sqlite3-wasmfs) $(emcc.flags.sqlite3-wasmfs.$(1)) \ + $(pre-post-sqlite3-wasmfs.flags.$(1)) \ $(sqlite3-wasm.c) + @$(call SQLITE3.xJS.ESM-EXPORT-DEFAULT,$(1)) chmod -x $(sqlite3-wasmfs.wasm) $(maybe-wasm-strip) $(sqlite3-wasmfs.wasm) - @ls -la $@ $(sqlite3-wasmfs.wasm) + @ls -la $(sqlite3-wasmfs.wasm) sqlite3-wasmfs*js +endef +$(sqlite3-wasmfs.js): + $(call SQLITE3-WASMFS.xJS.RECIPE,vanilla) +$(sqlite3-wasmfs.mjs): $(sqlite3-wasmfs.js) + $(call SQLITE3-WASMFS.xJS.RECIPE,esm) $(sqlite3-wasmfs.wasm): $(sqlite3-wasmfs.js) -wasmfs: $(sqlite3-wasmfs.js) -all: wasmfs +wasmfs: $(sqlite3-wasmfs.js) $(sqlite3-wasmfs.mjs) +#all: wasmfs ######################################################################## # speedtest1 for wasmfs. speedtest1-wasmfs.js := $(dir.wasmfs)/speedtest1-wasmfs.js speedtest1-wasmfs.wasm := $(subst .js,.wasm,$(speedtest1-wasmfs.js)) -speedtest1-wasmfs.eflags := $(sqlite3-wasmfs.fsflags) -speedtest1-wasmfs.eflags += $(SQLITE_OPT) -DSQLITE_ENABLE_WASMFS -speedtest1-wasmfs.eflags += -sALLOW_MEMORY_GROWTH=0 -speedtest1-wasmfs.eflags += -sINITIAL_MEMORY=$(emcc.INITIAL_MEMORY.128) -$(eval $(call call-make-pre-js,speedtest1-wasmfs)) +emcc.flags.speedtest1-wasmfs := $(sqlite3-wasmfs.fsflags) +emcc.flags.speedtest1-wasmfs += $(SQLITE_OPT) -DSQLITE_ENABLE_WASMFS +emcc.flags.speedtest1-wasmfs += -sALLOW_MEMORY_GROWTH=0 +emcc.flags.speedtest1-wasmfs += -sINITIAL_MEMORY=$(emcc.INITIAL_MEMORY.128) +#$(eval $(call call-make-pre-js,speedtest1-wasmfs,vanilla)) $(speedtest1-wasmfs.js): $(speedtest1.cses) $(sqlite3-wasmfs.js) \ $(MAKEFILE) $(MAKEFILE.wasmfs) \ - $(pre-post-speedtest1-wasmfs.deps) \ + $(pre-post-sqlite3-wasmfs.deps) \ $(EXPORTED_FUNCTIONS.speedtest1) @echo "Building $@ ..." $(emcc.bin) \ - $(speedtest1-wasmfs.eflags) $(speedtest1-common.eflags) \ - $(pre-post-speedtest1-wasmfs.flags) \ - $(speedtest1.cflags) \ - $(sqlite3-wasmfs.cflags) \ + $(emcc.speedtest1.common) $(emcc.flags.speedtest1-wasmfs) \ + $(pre-post-sqlite3-wasmfs.flags.vanilla) \ + $(cflags.sqlite3-wasmfs) \ -o $@ $(speedtest1.cses) -lm $(maybe-wasm-strip) $(speedtest1-wasmfs.wasm) ls -la $@ $(speedtest1-wasmfs.wasm) -speedtest1: $(speedtest1-wasmfs.js) +#speedtest1: $(speedtest1-wasmfs.js) +wasmfs: $(speedtest1-wasmfs.js) CLEAN_FILES += $(speedtest1-wasmfs.js) $(speedtest1-wasmfs.wasm) \ $(subst .js,.worker.js,$(speedtest1-wasmfs.js)) # end speedtest1.js diff --git a/main.mk b/main.mk index a8d32affe3..7bcf83880f 100644 --- a/main.mk +++ b/main.mk @@ -363,6 +363,7 @@ TESTSRC = \ TESTSRC += \ $(TOP)/ext/misc/amatch.c \ $(TOP)/ext/misc/appendvfs.c \ + $(TOP)/ext/misc/basexx.c \ $(TOP)/ext/misc/carray.c \ $(TOP)/ext/misc/cksumvfs.c \ $(TOP)/ext/misc/closure.c \ diff --git a/manifest b/manifest index bf43265be1..4b8afd8bd7 100644 --- a/manifest +++ b/manifest @@ -1,13 +1,13 @@ -C Merge\s3.40.0\sinto\sthe\sbegin-concurrent\sbranch. -D 2022-11-16T15:59:15.425 +C Update\sthe\sbegin-concurrent\sbranch\swith\sthe\slatest\senhancements\son\strunk. +D 2022-12-21T20:07:58.718 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 -F Makefile.in d879ac372b9cc301f1d2a48b7657cc954a2b53fa60aba5797c9e46cebe61b291 +F Makefile.in a4dfb6097ab3c899ab239f0fa521d2fb5294d5c4842d057edb55641382d81a25 F Makefile.linux-gcc f609543700659711fbd230eced1f01353117621dccae7b9fb70daa64236c5241 -F Makefile.msc e7a564ceec71f0d9666031d5638cf0d4f88b050b44e8df5d32125137cd259ac0 +F Makefile.msc e86ec21328921721dd6f777da435c62a5469cda1a654f5ecefacf87ddb3dfeb3 F README.md 8b8df9ca852aeac4864eb1e400002633ee6db84065bd01b78c33817f97d31f5e -F VERSION 8868ddfa6e1eee218286021a94b3e22d13e550c76c72d878857547ca001de24a +F VERSION 413ec94920a487ae32c9a2a8819544d690662d6f7c7ce025c0d0b8a1e74fa9db F aclocal.m4 a5c22d164aff7ed549d53a90fa56d56955281f50 F art/sqlite370.eps aa97a671332b432a54e1d74ff5e8775be34200c2 F art/sqlite370.ico af56c1d00fee7cd4753e8631ed60703ed0fc6e90 @@ -33,7 +33,7 @@ F autoconf/tea/win/nmakehlp.c b01f822eabbe1ed2b64e70882d97d48402b42d2689a1ea0034 F autoconf/tea/win/rules.vc c511f222b80064096b705dbeb97060ee1d6b6d63 F config.guess 883205ddf25b46f10c181818bf42c09da9888884af96f79e1719264345053bd6 F config.sub c2d0260f17f3e4bc0b6808fccf1b291cb5e9126c14fc5890efc77b9fd0175559 -F configure b93755fe94b3e9b2015f3c2d6c63928e8899b0a7c54e042deb843498094179da x +F configure 9cbf6a15a4b5f2b3551a466420e5f51ce04c40aaed7f90c886402bf0b66ec110 x F configure.ac 48dc6bfee293eef05910faf085760f2fd79b680aa47b50e8e6a22ca40bb026bb F contrib/sqlitecon.tcl 210a913ad63f9f991070821e599d600bd913e0ad F doc/F2FS.txt c1d4a0ae9711cfe0e1d8b019d154f1c29e0d3abfe820787ba1e9ed7691160fcd @@ -120,7 +120,7 @@ F ext/fts5/fts5_config.c 501e7d3566bc92766b0e11c0109a7c5a6146bc41144195459af5422 F ext/fts5/fts5_expr.c 40174a64829d30cc86e8266306ad24980f6911edd5ca0b8c1ce7821ea1341b88 F ext/fts5/fts5_hash.c d4fb70940359f2120ccd1de7ffe64cc3efe65de9e8995b822cd536ff64c96982 F ext/fts5/fts5_index.c a8ee270724ae1f958d0ce9897bcd60a5b760ecbeaa058fc8632805a283f1c20a -F ext/fts5/fts5_main.c 6078ae86d3b813753a4f1201054550aff21a3f660e97b30f200d2b1472874151 +F ext/fts5/fts5_main.c 3fd46be6a7aaac1d4210d4df0c7f9b1e78d1f0af566bfa2ab58d945ffa328ff7 F ext/fts5/fts5_storage.c 76c6085239eb44424004c022e9da17a5ecd5aaec859fba90ad47d3b08f4c8082 F ext/fts5/fts5_tcl.c b1445cbe69908c411df8084a10b2485500ac70a9c747cdc8cda175a3da59d8ae F ext/fts5/fts5_test_mi.c 08c11ec968148d4cb4119d96d819f8c1f329812c568bac3684f5464be177d3ee @@ -195,7 +195,7 @@ F ext/fts5/test/fts5leftjoin.test c0b4cafb9661379e576dc4405c0891d8fcc27826807405 F ext/fts5/test/fts5matchinfo.test 10c9a6f7fe61fb132299c4183c012770b10c4d5c2f2edb6df0b6607f683d737a F ext/fts5/test/fts5merge.test e92a8db28b45931e7a9c7b1bbd36101692759d00274df74d83fd29d25d53b3a6 F ext/fts5/test/fts5merge2.test 3ebad1a59d6ad3fb66eff6523a09e95dc6367cbefb3cd73196801dea0425c8e2 -F ext/fts5/test/fts5misc.test 4d7d20372242cc618688de029b87d897d09fa1640302c254abee63cf3ffa2c10 +F ext/fts5/test/fts5misc.test f0c5d5f6bc64f7cec522042f0ceb79c9195a4cde9fceb2af3718b3f10c8b7168 F ext/fts5/test/fts5multi.test a15bc91cdb717492e6e1b66fec1c356cb57386b980c7ba5af1915f97fe878581 F ext/fts5/test/fts5multiclient.test 5ff811c028d6108045ffef737f1e9f05028af2458e456c0937c1d1b8dea56d45 F ext/fts5/test/fts5near.test 211477940142d733ac04fad97cb24095513ab2507073a99c2765c3ddd2ef58bd @@ -290,6 +290,9 @@ F ext/misc/README.md d6dd0fe1d8af77040216798a6a2b0c46c73054d2f0ea544fbbcdccf6f23 F ext/misc/amatch.c e3ad5532799cee9a97647f483f67f43b38796b84b5a8c60594fe782a4338f358 F ext/misc/anycollseq.c 5ffdfde9829eeac52219136ad6aa7cd9a4edb3b15f4f2532de52f4a22525eddb F ext/misc/appendvfs.c 9642c7a194a2a25dca7ad3e36af24a0a46d7702168c4ad7e59c9f9b0e16a3824 +F ext/misc/base64.c 1ad313d38dc081abe5a87fa51e44f07ec6cfb618aa9606a77e42bd2fe706a67f x +F ext/misc/base85.c ace591246855806a70e63eb6705ce8736f4b782da2137bdbb5d39cc337346e0d +F ext/misc/basexx.c 58f72b3c159e875bfd2d30a56254e7f2098961c31dc733773a9270de7066aa4c F ext/misc/blobio.c a867c4c4617f6ec223a307ebfe0eabb45e0992f74dd47722b96f3e631c0edb2a F ext/misc/btreeinfo.c d28ce349b40054eaa9473e835837bad7a71deec33ba13e39f963d50933bfa0f9 F ext/misc/carray.c b752f46411e4e47e34dce6f0c88bc8e51bb821ba9e49bfcd882506451c928f69 @@ -317,13 +320,13 @@ F ext/misc/normalize.c bd84355c118e297522aba74de34a4fd286fc775524e0499b14473918d F ext/misc/percentile.c b9086e223d583bdaf8cb73c98a6539d501a2fc4282654adbfea576453d82e691 F ext/misc/prefixes.c 0f4f8cff5aebc00a7e3ac4021fd59cfe1a8e17c800ceaf592859ecb9cbc38196 F ext/misc/qpvtab.c 09738419e25f603a35c0ac8bd0a04daab794f48d08a9bc07a6085b9057b99009 -F ext/misc/regexp.c 5abed0ace2d9340b42b9ab1dbe64db9c276e4e8eba38a903232b6253e05ccdaf +F ext/misc/regexp.c 064838f7b31e90d312cce089cfafbb992034e75d359009c48886ca06c0a794b2 F ext/misc/remember.c add730f0f7e7436cd15ea3fd6a90fd83c3f706ab44169f7f048438b7d6baa69c F ext/misc/rot13.c 51ac5f51e9d5fd811db58a9c23c628ad5f333c173f1fc53c8491a3603d38556c F ext/misc/scrub.c 2a44b0d44c69584c0580ad2553f6290a307a49df4668941d2812135bfb96a946 F ext/misc/series.c 8d79354f2c3d46b95ee21272a07cf0bcabb58d1f2b06d9e7b8a31dca1dacb3e5 F ext/misc/sha1.c 4011aef176616872b2a0d5bccf0ecfb1f7ce3fe5c3d107f3a8e949d8e1e3f08d -F ext/misc/shathree.c 7b17615869a495659f1569ada1d8d3d21b4a24614f2746d93cc87ef7c0b6b36d +F ext/misc/shathree.c 9b7af7b7d55b27c5bbd16548b67fe6aa47a819e71bc6a80a456144f86f4e834d F ext/misc/showauth.c 732578f0fe4ce42d577e1c86dc89dd14a006ab52 F ext/misc/spellfix.c 94df9bbfa514a563c1484f684a2df3d128a2f7209a84ca3ca100c68a0163e29f F ext/misc/sqlar.c 0ace5d3c10fe736dc584bf1159a36b8e2e60fab309d310cd8a0eecd9036621b6 @@ -355,7 +358,7 @@ F ext/rbu/rbu7.test ae25f47b56f178197fc1098537a35a39176cc73d1629b03dc9d795929fc3 F ext/rbu/rbu8.test b98a6fc58ead84a0e6ddee775b9702cd981f318d5d4fd1d4df0fa0c40db7251b F ext/rbu/rbu9.test 0e4d985e25620d61920597e8ea69c871c9e8c1f5a0be2ae9fa70bb641d74378c F ext/rbu/rbuA.test b34a90cb495682c25b5fc03a9d5e7a4fc99541c29256f25e2e2a4f6542b4f5b3 -F ext/rbu/rbuB.test 52b07158824c6927b7e25554ace92a695cdebfc296ae3d308ac386984aded9bc +F ext/rbu/rbuB.test 8d1f141711be8122739853d876af4306bc756d925499577f9b917ec1f1c5ae65 F ext/rbu/rbuC.test 80f1cc2fb74f44b1128fd0ed8eedab3a76fefeb72a947860e2869ef76fc8dc6b F ext/rbu/rbu_common.tcl 60d904133ff843fe72cc0514e9dd2486707181e6e0fbab20979da28c48d21de9 F ext/rbu/rbubusy.test f38ef557358564491b8a2ee70e4cad31e40fcea57a16f27bc56ba40a59bbde50 @@ -384,11 +387,11 @@ F ext/rbu/rbuvacuum.test 55e101e90168c2b31df6c9638fe73dc7f7cc666b6142266d1563697 F ext/rbu/rbuvacuum2.test 2643b58f4d8d3573db0f93faae18805a35ab162b4c55ff6b656062ff432ed55b F ext/rbu/rbuvacuum3.test 8addd82e4b83b4c93fa47428eae4fd0dbf410f8512c186f38e348feb49ba03dc F ext/rbu/rbuvacuum4.test a78898e438a44803eb2bc897ba3323373c9f277418e2d6d76e90f2f1dbccfd10 -F ext/rbu/sqlite3rbu.c c4ba7901b2d3e0c7845f30840e3ffb35c6f999d6da0d80f121866491f982794c +F ext/rbu/sqlite3rbu.c 64d105c7c6c95272e7c12142cd021059214387a0d51e262bb0663285a2b91660 F ext/rbu/sqlite3rbu.h 02d981e2d39c151391759e1a400e29c7388730812957ac3db8dad7f6c9f9cfc8 F ext/rbu/test_rbu.c ee6ede75147bc081fe9bc3931e6b206277418d14d3fbceea6fdc6216d9b47055 F ext/recover/dbdata.c 8f1f75d636431de69d7977ec50fc41bfdd0c48c510d5ee7eae0cbd4164e1429a -F ext/recover/recover1.test 02004eb8f9ec2825ba77e24742c18e45162cb21d27e76a3a435b83a759a1131a +F ext/recover/recover1.test 2a2df2943d6696f9487e75868feae4b1511c4a511b102854ba0d2af0326d9dfb F ext/recover/recover_common.tcl a61306c1eb45c0c3fc45652c35b2d4ec19729e340bdf65a272ce4c229cefd85a F ext/recover/recoverclobber.test 3ba6c0c373c5c63d17e82eced64c05c57ccaf26c1abe1ca7141334022a79f32e F ext/recover/recovercorrupt.test 64c081ad1200ae77b447da99eb724785d6bf71715f394543dc7689642e92bf49 @@ -400,7 +403,7 @@ F ext/recover/recoverpgsz.test 3658ab8e68475b1bb87d6af88baa04551c84b73280a566a1b F ext/recover/recoverrowid.test f948bf4024a5f41b0e21b8af80c60564c5b5d78c05a8d64fc00787715ff9f45f F ext/recover/recoverslowidx.test 5205a9742dd9490ee99950dabb622307355ef1662dea6a3a21030057bfd81411 F ext/recover/recoversql.test e66d01f95302a223bcd3fd42b5ee58dc2b53d70afa90b0d00e41e4b8eab20486 -F ext/recover/sqlite3recover.c 1e4de9cd30daa67e5c1cd0cafb764ddf183087ce037ab47b7c1a7e2921c90d37 +F ext/recover/sqlite3recover.c 6d37a422c8917b793548a7b2c6e7ff8e1af15ba5453eef37bb69331828b73186 F ext/recover/sqlite3recover.h 011c799f02deb70ab685916f6f538e6bb32c4e0025e79bfd0e24ff9c74820959 F ext/recover/test_recover.c 1a34e2d04533d919a30ae4d5caeb1643f6684e9ccd7597ca27721d8af81f4ade F ext/repair/README.md 92f5e8aae749a4dae14f02eea8e1bb42d4db2b6ce5e83dbcdd6b1446997e0c15 @@ -472,7 +475,7 @@ F ext/session/sessionG.test 3828b944cd1285f4379340fd36f8b64c464fc84df6ff3ccbc955 F ext/session/sessionH.test ecabc041e04e48671a94cd7c02993bc73f75dced943280a026e89cdf64374eef F ext/session/session_common.tcl f613174665456b2d916ae8df3e5735092a1c1712f36f46840172e9a01e8cc53e F ext/session/session_speed_test.c dcf0ef58d76b70c8fbd9eab3be77cf9deb8bc1638fed8be518b62d6cbdef88b3 -F ext/session/sessionat.test 52993535f1230a42f70886643574ba7ae60ef854f8add9c8e3fcc3eb5c564bd2 +F ext/session/sessionat.test 46fd847f6ed194ebb7ebef9fe68b2e2ec88d9c2383a6846cddc5604b35f1d4ae F ext/session/sessionbig.test 890ade19e3f80f3d3a3e83821ff79c5e2af906a67ecb5450879f0015cadf101e F ext/session/sessiondiff.test ad13dd65664bae26744e1f18eb3cbd5588349b7e9118851d8f9364248d67bcec F ext/session/sessionfault.test da273f2712b6411e85e71465a1733b8501dbf6f7 @@ -486,42 +489,44 @@ F ext/session/sessionstat1.test 218d351cf9fcd6648f125a26b607b140310160184723c266 F ext/session/sessionwor.test 6fd9a2256442cebde5b2284936ae9e0d54bde692d0f5fd009ecef8511f4cf3fc F ext/session/sqlite3changebatch.c d5553b79e012ee2cb06c0a96bdf9dfe19e66354390ea0036cc46c4953142d517 F ext/session/sqlite3changebatch.h e72016998c9a22d439ddfd547b69e1ebac810c24 -F ext/session/sqlite3session.c 421854336ece362552508426d89091c1832148066f0641e8d72e22f58d9241f3 +F ext/session/sqlite3session.c dae195ab97cc28550ee40a61aed56837de656722606bba362aa699159cf90cc9 F ext/session/sqlite3session.h c26e54521584ad26dcb459171d94bbe6412ca26ed4613551c7f0c80aad129abd -F ext/session/test_session.c 226461035031632255c8098f3bcfa4ea27f0b051efd88857a2a36bcea5c294b9 +F ext/session/test_session.c 1468abeab18502a32d0d30cea5f39ada35c9807478ba87a99c4f737f9449ec13 F ext/userauth/sqlite3userauth.h 7f3ea8c4686db8e40b0a0e7a8e0b00fac13aa7a3 F ext/userauth/user-auth.txt e6641021a9210364665fe625d067617d03f27b04 F ext/userauth/userauth.c 7f00cded7dcaa5d47f54539b290a43d2e59f4b1eb5f447545fa865f002fc80cb F ext/wasm/EXPORTED_FUNCTIONS.fiddle 7fb73f7150ab79d83bb45a67d257553c905c78cd3d693101699243f36c5ae6c3 -F ext/wasm/GNUmakefile 3aa8c160705ab9d9d4d552a1f4e630925a65a27df216befe2e9a956904434c1d +F ext/wasm/GNUmakefile 06d385b51bfb206cf779cf1bb816862f77df97fff97a6df9baf05b98c027067a F ext/wasm/README-dist.txt 2d670b426fc7c613b90a7d2f2b05b433088fe65181abead970980f0a4a75ea20 F ext/wasm/README.md ef39861aa21632fdbca0bdd469f78f0096f6449a720f3f39642594af503030e9 F ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api c5eaceabb9e759aaae7d3101a4a3e542f96ab2c99d89a80ce20ec18c23115f33 F ext/wasm/api/EXPORTED_RUNTIME_METHODS.sqlite3-api 1ec3c73e7d66e95529c3c64ac3de2470b0e9e7fbf7a5b41261c367cf4f1b7287 -F ext/wasm/api/README.md 1350088aee90e959ad9a94fab1bb6bcb5e99d4d27f976db389050f54f2640c78 -F ext/wasm/api/extern-post-js.js f3dc4219a2a1f183d98452dcbd55a0c5351b759eccca75480a92473974d8b047 +F ext/wasm/api/README.md 77a2f1f2fc60a35def7455dffc8d3f2c56385d6ac5c6cecc60fa938252ea2c54 +F ext/wasm/api/extern-post-js.c-pp.js 8923f76c3d2213159e12d641dc750523ead5c848185dc4996fae5cc12397f88d w ext/wasm/api/extern-post-js.js F ext/wasm/api/extern-pre-js.js cc61c09c7a24a07dbecb4c352453c3985170cec12b4e7e7e7a4d11d43c5c8f41 F ext/wasm/api/post-js-footer.js cd0a8ec768501d9bd45d325ab0442037fb0e33d1f3b4f08902f15c34720ee4a1 -F ext/wasm/api/post-js-header.js d6ab3dfef4a06960d28a7eaa338d4e2a1a5981e9b38718168bbde8fdb2a439b8 -F ext/wasm/api/pre-js.js 287e462f969342b032c03900e668099fa1471d852df7a472de5bc349161d9c04 -F ext/wasm/api/sqlite3-api-cleanup.js ecdc69dbfccfe26146f04799fcfd4a6f5790d46e7e3b9b6e9b0491f92ed8ae34 -F ext/wasm/api/sqlite3-api-glue.js 056f44b82c126358a0175e08a892d56fadfce177b0d7a0012502a6acf67ea6d5 -F ext/wasm/api/sqlite3-api-oo1.js e9a83489bbb4838ce0aee46eaaa9350e0e25a5b926b565e4f5ae8e840e4fbaed -F ext/wasm/api/sqlite3-api-opfs.js 5c6d0903e100b2918920d8c596ab037cd9a67df19e388ebde4fe085218780af0 -F ext/wasm/api/sqlite3-api-prologue.js fd526fa017fa2578673ca18158354515c719e719a5d93f2f6d0e43f39170430e +F ext/wasm/api/post-js-header.js 47b6b281f39ad59fa6e8b658308cd98ea292c286a68407b35ff3ed9cfd281a62 +F ext/wasm/api/pre-js.c-pp.js b88499dc303c21fc3f55f2c364a0f814f587b60a95784303881169f9e91c1d5f w ext/wasm/api/pre-js.js +F ext/wasm/api/sqlite3-api-cleanup.js 680d5ccfff54459db136a49b2199d9f879c8405d9c99af1dda0cc5e7c29056f4 +F ext/wasm/api/sqlite3-api-glue.js 63daa4b9c36faa4c338a32a06eb142869b9ae4885a3e01aad473e1b45357089f +F ext/wasm/api/sqlite3-api-oo1.js c0c4ccc269cccee657ffd03f094da7e270e1367b7928926b3730d543555a12a6 +F ext/wasm/api/sqlite3-api-prologue.js 1767dfcd94bb4fa9dd4bd9ff6327117783d3656faf1058dcc1369db320d871fc F ext/wasm/api/sqlite3-api-worker1.js e94ba98e44afccfa482874cd9acb325883ade50ed1f9f9526beb9de1711f182f F ext/wasm/api/sqlite3-license-version-header.js a661182fc93fc2cf212dfd0b987f8e138a3ac98f850b1112e29b5fbdaecc87c3 -F ext/wasm/api/sqlite3-opfs-async-proxy.js 24d1c1982a012d998907105a4ff1ff6881bf462395e90c06326817701e69f093 +F ext/wasm/api/sqlite3-opfs-async-proxy.js 7795b84b66a7a8dedc791340709b310bb497c3c72a80bef364fa2a58e2ddae3f +F ext/wasm/api/sqlite3-v-helper.js 6f6c3e390a72e08b0a5b16a0d567d7af3c04d172831853a29d72a6f1dd40ff24 +F ext/wasm/api/sqlite3-vfs-opfs.c-pp.js 66daf6fb6843bea615fe193109e1542efbeca24f560ee9da63375a910bb48115 w ext/wasm/api/sqlite3-api-opfs.js F ext/wasm/api/sqlite3-wasi.h 25356084cfe0d40458a902afb465df8c21fc4152c1d0a59b563a3fba59a068f9 -F ext/wasm/api/sqlite3-wasm.c 8fc8f47680df0e9a6c0f2f03cb004148645ecc983aa216daba09cb21f7e092a2 +F ext/wasm/api/sqlite3-wasm.c 44ce4cf12318b0577d8222cf59132617ab9925ca3cf5fbb8c7b30d1e947c13b5 F ext/wasm/api/sqlite3-worker1-promiser.js 0c7a9826dbf82a5ed4e4f7bf7816e825a52aff253afbf3350431f5773faf0e4b F ext/wasm/api/sqlite3-worker1.js 1e54ea3d540161bcfb2100368a2fc0cad871a207b8336afee1c445715851ec54 F ext/wasm/batch-runner.html 4deeed44fe41496dc6898d9fb17938ea3291f40f4bfb977e29d0cef96fbbe4c8 -F ext/wasm/batch-runner.js 49609e89aaac9989d6c1ad3fae268e4878e1ad7bc5fd3e5c2f44959660780b2e +F ext/wasm/batch-runner.js 0dad6a02ad796f1003d3b7048947d275c4d6277f63767b8e685c27df8fdac93e +F ext/wasm/c-pp.c 92285f7bce67ed7b7020b40fde8ed0982c442b63dc33df9dfd4b658d4a6c0779 F ext/wasm/common/SqliteTestUtil.js d8bf97ecb0705a2299765c8fc9e11b1a5ac7f10988bbf375a6558b7ca287067b -F ext/wasm/common/emscripten.css 3d253a6fdb8983a2ac983855bfbdd4b6fa1ff267c28d69513dd6ef1f289ada3f -F ext/wasm/common/testing.css 35889709547d89a6109ff83b25c11bbc91d8dd43aab8722e428655ca98880a06 -F ext/wasm/common/whwasmutil.js 16a592d5c304a2d268ca1c28e08a5b029a2f3cbe10af78dbc3456cfc9e3559d1 +F ext/wasm/common/emscripten.css 11bd104b6c0d597c67d40cc8ecc0a60dae2b965151e3b6a37fa5708bac3acd15 +F ext/wasm/common/testing.css 0ff15602a3ab2bad8aef2c3bd120c7ee3fd1c2054ad2ace7e214187ae68d926f +F ext/wasm/common/whwasmutil.js ba1a8db1f32124e43e24b3d890102b6552b2c0b5a202185041a55887692df328 F ext/wasm/demo-123-worker.html a0b58d9caef098a626a1a1db567076fca4245e8d60ba94557ede8684350a81ed F ext/wasm/demo-123.html 8c70a412ce386bd3796534257935eb1e3ea5c581e5d5aea0490b8232e570a508 F ext/wasm/demo-123.js ebae30756585bca655b4ab2553ec9236a87c23ad24fc8652115dcedb06d28df6 @@ -531,37 +536,40 @@ F ext/wasm/demo-worker1-promiser.html 1de7c248c7c2cfd4a5783d2aa154bce62d74c6de98 F ext/wasm/demo-worker1-promiser.js b85a2bb1b918db4f09dfa24419241cb3edad7791389425c2505092e9b715017d F ext/wasm/demo-worker1.html 2c178c1890a2beb5a5fecb1453e796d067a4b8d3d2a04d65ca2eb1ab2c68ef5d F ext/wasm/demo-worker1.js a619adffc98b75b66c633b00f747b856449a134a9a0357909287d80a182d70fa -F ext/wasm/dist.make 481289899a07958439d07ee4302ff86235fa0fbb72f17ea05db2be90a94abf90 -F ext/wasm/fiddle.make e570ec1bfc7d803507a2e514fe32f673fe001b2114b85c73c3964a462ba8bcfc +F ext/wasm/dist.make 5523b02e824db5ab8176e3eedc2e709fe1204d8f4d6e52e8321cdf6830114b72 +F ext/wasm/fiddle.make d5308b5c35f691758ef20badd25f91f3780b20415760daf0d98afbe4f24921b9 F ext/wasm/fiddle/emscripten.css 3d253a6fdb8983a2ac983855bfbdd4b6fa1ff267c28d69513dd6ef1f289ada3f F ext/wasm/fiddle/fiddle-worker.js b4a0c8ab6c0983218543ca771c45f6075449f63a1dcf290ae5a681b2cba8800d F ext/wasm/fiddle/fiddle.html 550c5aafce40bd218de9bf26192749f69f9b10bc379423ecd2e162bcef885c08 F ext/wasm/fiddle/fiddle.js 974b995119ac443685d7d94d3b3c58c6a36540e9eb3fed7069d5653284071715 -F ext/wasm/index-dist.html cb0da16cba0f21cda2c25724c5869102d48eb0af04446acd3cd0ca031f80ed19 -F ext/wasm/index.html ce6a68a75532b47e3c0adb83381a06d15de8c0ac0331fb7bf31d33f8e7c77dc4 -F ext/wasm/jaccwabyt/jaccwabyt.js 95f573de1826474c9605dda620ee622fcb1673ae74f191eb324c0853aa4dcb66 -F ext/wasm/jaccwabyt/jaccwabyt.md 9aa6951b529a8b29f578ec8f0355713c39584c92cf1708f63ba0cf917cb5b68e -F ext/wasm/module-symbols.html b8eebafef8e536624bbe5f7a3da40c07a9062b843dfd3161a0bb72cbb6763dc5 +F ext/wasm/index-dist.html c806b6005145b71d64240606e9c6e0bf56878ee8829c66fe7486cebf34b0e6b1 +F ext/wasm/index.html cc8b174ff01be282b399e64b58bdf3c921d7020da5d4e22e88bbbb4a6787a209 +F ext/wasm/jaccwabyt/jaccwabyt.js 06f2ef1ad640c26c593def3d960336e9bb789819b920516480895c38ed5f58fa +F ext/wasm/jaccwabyt/jaccwabyt.md 37911f00db12cbcca73aa1ed72594430365f30aafae2fa9c886961de74e5e0eb +F ext/wasm/module-symbols.html 573317801087e67c946a157783715d5716e027fcf484918a0c3aae4e627cc93d F ext/wasm/scratchpad-wasmfs-main.html 20cf6f1a8f368e70d01e8c17200e3eaa90f1c8e1029186d836d14b83845fbe06 F ext/wasm/scratchpad-wasmfs-main.js 4c140457f4d6da9d646a49addd91edb6e9ad1643c6c48e3258b5bce24725dc18 -F ext/wasm/speedtest1-wasmfs.html bc28eb29b69a73864b8d7aae428448f8b7e1de81d8bfb9bba99541322054dbd0 -F ext/wasm/speedtest1-worker.html d75d9de9347c6f2f3278b73ccb6bceb559080644aef385fe496fa389f58bcd66 +F ext/wasm/speedtest1-wasmfs.html 7a301f4f5b6ad4f5d37fd6e7ca03a2f5d5547fd289da60a39075a93d7646d354 +F ext/wasm/speedtest1-worker.html fe6b36a63de1012bb9fb4d2fb888b6de9c589c21b0aa3ae054459b0093e077bf F ext/wasm/speedtest1-worker.js 13b57c4a41729678a1194014afec2bd5b94435dcfc8d1039dfa9a533ac819ee1 -F ext/wasm/speedtest1.html e4c4e5c1c8ec1ad13c995e346e4216a1df152fd2c5cd17e0793b865b2f3c5000 +F ext/wasm/speedtest1.html ff048b4a623aa192e83e143e48f1ce2a899846dd42c023fdedc8772b6e3f07da F ext/wasm/split-speedtest1-script.sh a3e271938d4d14ee49105eb05567c6a69ba4c1f1293583ad5af0cd3a3779e205 x F ext/wasm/sql/000-mandelbrot.sql 775337a4b80938ac8146aedf88808282f04d02d983d82675bd63d9c2d97a15f0 F ext/wasm/sql/001-sudoku.sql 35b7cb7239ba5d5f193bc05ec379bcf66891bce6f2a5b3879f2f78d0917299b5 F ext/wasm/test-opfs-vfs.html 1f2d672f3f3fce810dfd48a8d56914aba22e45c6834e262555e685bce3da8c3f -F ext/wasm/test-opfs-vfs.js 44363db07b2a20e73b0eb1808de4400ca71b703af718d0fa6d962f15e73bf2ac -F ext/wasm/tester1-worker.html 51bf39e2b87f974ae3d5bc3086e2fb36d258f3698c54f6e21ba4b3b99636fa27 -F ext/wasm/tester1.html 624ec41cd9f78a1f2b6d7df70aaa7a6394396b1f2455ecbd6de5775c1275b121 -F ext/wasm/tester1.js bff806de454de115922d78c056f11d523ec7ed9ed3839d4e21433a9f72558b88 +F ext/wasm/test-opfs-vfs.js f09266873e1a34d9bdb6d3981ec8c9e382f31f215c9fd2f9016d2394b8ae9b7b +F ext/wasm/tester1-worker.html d43f3c131d88f10d00aff3e328fed13c858d674ea2ff1ff90225506137f85aa9 +F ext/wasm/tester1.c-pp.html d34bef3d48e5cbc1c7c06882ad240fec49bf88f5f65696cc2c72c416933aa406 w ext/wasm/tester1.html +F ext/wasm/tester1.c-pp.js c45c46cdae1949d426ee12a736087ab180beacc2a20cd829f9052b957adf9ac9 w ext/wasm/tester1.js +F ext/wasm/tests/opfs/concurrency/index.html 86d8ac435074d1e7007b91105f4897f368c165e8cecb6a9aa3d81f5cf5dcbe70 +F ext/wasm/tests/opfs/concurrency/test.js a98016113eaf71e81ddbf71655aa29b0fed9a8b79a3cdd3620d1658eb1cc9a5d +F ext/wasm/tests/opfs/concurrency/worker.js 0a8c1a3e6ebb38aabbee24f122693f1fb29d599948915c76906681bb7da1d3d2 F ext/wasm/version-info.c 3b36468a90faf1bbd59c65fd0eb66522d9f941eedd364fabccd72273503ae7d5 -F ext/wasm/wasmfs.make edfd60691d10fd19ada4c061280fd7fbe4cf5f6bf6b913268e8ebedfccea6ab5 +F ext/wasm/wasmfs.make cf9a68162d92ca2bcb0b9528b244cb36d5cc2d84ccc9c2d398461927d6e75aea F install-sh 9d4de14ab9fb0facae2f48780b874848cbf2f895 x F ltmain.sh 3ff0879076df340d2e23ae905484d8c15d5fdea8 F magic.txt 5ade0bc977aa135e79e3faaea894d5671b26107cc91e70783aa7dc83f22f3ba0 -F main.mk 5f36606a98229c62011bbbfc013f46db2930145ecbdc6f10407b2167f59f1101 +F main.mk 89c69ad0f501e57f43abd71498706c1259ff38e698507fc285ace646e511d49e F mkso.sh fd21c06b063bb16a5d25deea1752c2da6ac3ed83 F mptest/config01.test 3c6adcbc50b991866855f1977ff172eb6d901271 F mptest/config02.test 4415dfe36c48785f751e16e32c20b077c28ae504 @@ -581,38 +589,38 @@ F src/auth.c f4fa91b6a90bbc8e0d0f738aa284551739c9543a367071f55574681e0f24f8cf F src/backup.c a2891172438e385fdbe97c11c9745676bec54f518d4447090af97189fd8e52d7 F src/bitvec.c 3907fcbe8a0c8c2db58d97087d15cdabbf2842adb9125df9ab9ff87d3db16775 F src/btmutex.c 6ffb0a22c19e2f9110be0964d0731d2ef1c67b5f7fabfbaeb7b9dabc4b7740ca -F src/btree.c 7fe482a41da06760bfb5f46da5043a62ad98ed22f1d5714058f053899ec5b0e6 -F src/btree.h 900067641b64d619e6e2a93bd115c952a52f41d3bee32e551e2a4ceee05fc431 -F src/btreeInt.h 650add92a0ffc8c315406f140325c5f41f0e386848dafbb1e27a72fe7cf6f179 -F src/build.c dd4b69d26a9e18467dc3d3612f472ba0c5ad4b34de45e1c3b367b5c64c993066 +F src/btree.c 295645736414c89b825ad1ca369bcc0beb26f1e58a9dffec1210cd02c2b8d48e +F src/btree.h b89e1a7cf3345ea1b411831929e45fe39e8a93e28eabd2ea1071c432366f4859 +F src/btreeInt.h 429769e739b67133db190b514d427cb3acbb8c7b475c15bd4e8c91e88b2692fa +F src/build.c 022afee249807f77fdd3cd7ca23b60c0336a6e724d0b1c1506c7298823d0b0a6 F src/callback.c 4cd7225b26a97f7de5fee5ae10464bed5a78f2adefe19534cc2095b3a8ca484a F src/complete.c a3634ab1e687055cd002e11b8f43eb75c17da23e F src/ctime.c 20507cc0b0a6c19cd882fcd0eaeda32ae6a4229fb4b024cfdf3183043d9b703d F src/date.c 94ce83b4cd848a387680a5f920c9018c16655db778c4d36525af0a0f34679ac5 F src/dbpage.c f1a87f4ebcf22284e0aaf0697862f4ccfc120dcd6db3d8dfa3b049b2580c01d8 -F src/dbstat.c 861e08690fcb0f2ee1165eff0060ea8d4f3e2ea10f80dab7d32ad70443a6ff2d +F src/dbstat.c a56a7ad1163a9888d46cd5820be2e65354fb1aa04ed6909f7c3e5831e0ee2c29 F src/delete.c 86573edae75e3d3e9a8b590d87db8e47222103029df4f3e11fa56044459b514e -F src/expr.c 847f87d9df3ede2b2b0a8db088af0b9c1923b21009f8ea1b9b7b28cb0a383170 +F src/expr.c 204af6a83c191f5ac19ec4af6ecc546f188cc2dd1c76fc5280982f710ec4b9c4 F src/fault.c 460f3e55994363812d9d60844b2a6de88826e007 F src/fkey.c 722f20779f5342a787922deded3628d8c74b5249cab04098cf17ee2f2aaff002 -F src/func.c 2c186ba4d58b61380b31f01cebeff6d30d93f9ac1bf7d09533926ca284ddac1d +F src/func.c 98feef981f41a00feb96af8165606a38ca364b361e8f0a037c375b0131a207fb F src/global.c e06ff8e0acd85aec13563c9ecb44fbbf38232ccf73594998fd880b92d619594b F src/hash.c 8d7dda241d0ebdafb6ffdeda3149a412d7df75102cecfc1021c98d6219823b19 F src/hash.h 3340ab6e1d13e725571d7cee6d3e3135f0779a7d8e76a9ce0a85971fa3953c51 -F src/hwtime.h cb1d7e3e1ed94b7aa6fde95ae2c2daccc3df826be26fc9ed7fd90d1750ae6144 +F src/hwtime.h b638809e083b601b618df877b2e89cb87c2a47a01f4def10be4c4ebb54664ac7 F src/in-operator.md 10cd8f4bcd225a32518407c2fb2484089112fd71 -F src/insert.c 90a32bc7faa755cd5292ade21d2b3c6edba8fd1d70754a364caccabfde2c3bb2 +F src/insert.c 1b11a2e33ee52db93c02fddac67e39d00161d61b69fac2675b82f2aa68c1b61c F src/json.c 7749b98c62f691697c7ee536b570c744c0583cab4a89200fdd0fc2aa8cc8cbd6 F src/legacy.c d7874bc885906868cd51e6c2156698f2754f02d9eee1bae2d687323c3ca8e5aa -F src/loadext.c 8086232d10e51e183a7f64199815bad1c579896354db69435347665f62f481e9 -F src/main.c 568be0895002517946bfb56444ec19c4ee9edd0d4091550597ec81f5f43195c3 -F src/malloc.c dfddca1e163496c0a10250cedeafaf56dff47673e0f15888fb0925340a8e3f90 +F src/loadext.c 25663175950c5c4404b9377840b7b4c6fe5c53b415caf43634c62f442c02a9a7 +F src/main.c 997fc865c9bf19cd65b2398b9d053cbcdbe865c6bd9cdab32197b7cc46d505e7 +F src/malloc.c 47b82c5daad557d9b963e3873e99c22570fb470719082c6658bf64e3012f7d23 F src/mem0.c 6a55ebe57c46ca1a7d98da93aaa07f99f1059645 F src/mem1.c c12a42539b1ba105e3707d0e628ad70e611040d8f5e38cf942cee30c867083de F src/mem2.c c8bfc9446fd0798bddd495eb5d9dbafa7d4b7287d8c22d50a83ac9daa26d8a75 F src/mem3.c 30301196cace2a085cbedee1326a49f4b26deff0af68774ca82c1f7c06fda4f6 F src/mem5.c 5a3dbd8ac8a6501152a4fc1fcae9b0900c2d7eb0589c4ec7456fdde15725a26c -F src/memdb.c c2dc88f97c410eb68a24468344b65526685e18354ddfd15906750c1eaf9dc2dd +F src/memdb.c 559c42e61eb70cd6d4bc692b042497133c6d96c09a3d514d92f3dac72268e223 F src/memjournal.c c283c6c95d940eb9dc70f1863eef3ee40382dbd35e5a1108026e7817c206e8a0 F src/msvc.h 3a15918220367a8876be3fa4f2abe423a861491e84b864fb2b7426bf022a28f8 F src/mutex.c 5e3409715552348732e97b9194abe92fdfcd934cfb681df4ba0ab87ac6c18d25 @@ -623,13 +631,13 @@ F src/mutex_w32.c caa50e1c0258ac4443f52e00fe8aaea73b6d0728bd8856bedfff822cae4185 F src/notify.c 89a97dc854c3aa62ad5f384ef50c5a4a11d70fcc69f86de3e991573421130ed6 F src/os.c 81c9c1c52eab711e27e33fd51fe5788488d3a02bc1a71439857abbee5d0d2c97 F src/os.h 1ff5ae51d339d0e30d8a9d814f4b8f8e448169304d83a7ed9db66a65732f3e63 -F src/os_common.h b2f4707a603e36811d9b1a13278bffd757857b85 -F src/os_kv.c 0e59600d25b72034c7666b8b7dcc527f039b5d9c16f24a7eca4c08c66f63c364 +F src/os_common.h 6c0eb8dd40ef3e12fe585a13e709710267a258e2c8dd1c40b1948a1d14582e06 +F src/os_kv.c 73f89ab97ecdb3216857d2acc8395103f89164eaadac87cce4e9e16445c89541 F src/os_setup.h 6011ad7af5db4e05155f385eb3a9b4470688de6f65d6166b8956e58a3d872107 -F src/os_unix.c b036cebe7344894e7c6ec26eeb9912699c9475242858555c1aa9dd99fae2d5a1 +F src/os_unix.c da76727158b8b963fc0b47ede94031593abae650f988c645f79b5173abd7ae4b F src/os_win.c 295fe45f18bd86f2477f4cd79f3377c6f883ceb941b1f46808665c73747f2345 F src/os_win.h 7b073010f1451abe501be30d12f6bc599824944a -F src/pager.c a992a87f6eaf5c850934d2b0469c6d50adafdff2a1fa425179ad4ca910a67838 +F src/pager.c f0b3fd5a9d2e9342ef6cc25a205eecf22eb211ebf63979f4d31a211c95c34600 F src/pager.h 7b2ec7bba30b21a97b68d5bdc0dbb82a75f48c4b1457180988f9d409fb789e16 F src/parse.y 17c50d262d92083badeb60b3ebe4725e19c76548f90aea898ab07d4f2940a7d8 F src/pcache.c f4268f7f73c6a3db12ce22fd25bc68dc42315d19599414ab1207d7cf32f79197 @@ -637,22 +645,22 @@ F src/pcache.h 4f87acd914cef5016fae3030343540d75f5b85a1877eed1a2a19b9f284248586 F src/pcache1.c dee95e3cd2b61e6512dc814c5ab76d5eb36f0bfc9441dbb4260fccc0d12bbddc F src/pragma.c 894c2621d35edd4beea9b331cfdb1b42032394420074d2294c8febe548eea8a1 F src/pragma.h 1f421360eed1a7721e8c521463df8519a7c8d0d5893ebd9dbfe0dba8de996f8c -F src/prepare.c 1b02be0441eda4579471fea097f678effcbb77ef0c39ab3f703c837822bcd674 -F src/printf.c e99ee9741e79ae3873458146f59644276657340385ade4e76a5f5d1c25793764 +F src/prepare.c 9ebd3a1b12bbd1951f0d6db850f32cf5d4547a6ab8bb9e958d75dfbe4e60d0a3 +F src/printf.c ff4b05e38bf928ff1b80d3dda4f977b10fe39ecbfe69c018224c7e5594fb2455 F src/random.c a3e70f8515721ff24d2c0e6afd83923e8faab5ab79ececea4c1bf9fe4049fbb2 F src/resolve.c efea4e5fbecfd6d0a9071b0be0d952620991673391b6ffaaf4c277b0bb674633 F src/rowset.c ba9515a922af32abe1f7d39406b9d35730ed65efab9443dc5702693b60854c92 -F src/select.c 363bef2cd043d0ba070ffcaabac6b52a209c2aa59eb34935b547db7eb8268139 -F src/shell.c.in 458cb3de9d548342fc645b699620b1af3de770d2ceec09ac71f86c19bd244064 -F src/sqlite.h.in 88bd91bf629f608400af52c01de8e9cfd04584b68ddafb329b095bbe80c4c62e +F src/select.c e0ec5c22fe0b0ca4fa94732d6d16af6df9f77dfde245ac462c50a3f1123d4ff4 +F src/shell.c.in 24e0c75947dd8a3426473d90dfc4887f42553c8b57dff02a6865f04c5efcf864 +F src/sqlite.h.in 6e33b6c208f01d746c2bd7fe8747dbe8469877c3b3abb601c0251e137105028a F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8 F src/sqlite3ext.h c4b9fa7a7e2bcdf850cfeb4b8a91d5ec47b7a00033bc996fd2ee96cbf2741f5f -F src/sqliteInt.h ca227b85962a4c96bb00e71a1b296f1dd5e8ef48a6a51025a4ca046d46725a2c +F src/sqliteInt.h 0bb7efbc278277c402809485e0ed489c63c6a05ee211c46739f4158d7a22c9a0 F src/sqliteLimit.h d7323ffea5208c6af2734574bae933ca8ed2ab728083caa117c9738581a31657 F src/status.c 160c445d7d28c984a0eae38c144f6419311ed3eace59b44ac6dafc20db4af749 F src/table.c 0f141b58a16de7e2fbe81c308379e7279f4c6b50eb08efeec5892794a0ba30d1 F src/tclsqlite.c 4e64ba300a5a26e0f1170e09032429faeb65e45e8f3d1a7833e8edb69fc2979e -F src/test1.c 917aafccba9b2061cc52471fefad26dfce4a490df4b04d763f28baa8e0b148ca +F src/test1.c 741d7a904a01ded100ee021023886b12ad16f704481a470e5a5403013defef2f F src/test2.c 827446e259a3b7ab949da1542953edda7b5117982576d3e6f1c24a0dd20a5cef F src/test3.c 61798bb0d38b915067a8c8e03f5a534b431181f802659a6616f9b4ff7d872644 F src/test4.c 4533b76419e7feb41b40582554663ed3cd77aaa54e135cf76b3205098cd6e664 @@ -669,7 +677,7 @@ F src/test_blob.c ae4a0620b478548afb67963095a7417cd06a4ec0a56adb453542203bfdcb31 F src/test_btree.c 8b2dc8b8848cf3a4db93f11578f075e82252a274 F src/test_config.c c7a93ef3c0c881e3c46ff14163af886a602317206d82a7dc4ebbf400e661a6ff F src/test_delete.c e2fe07646dff6300b48d49b2fee2fe192ed389e834dd635e3b3bac0ce0bf9f8f -F src/test_demovfs.c 7cc7623d1025d1e92c51da20fd25060759733b7a356a121545a3b7d2faa8a0f1 +F src/test_demovfs.c 38a459d1c78fd9afa770445b224c485e079018d6ac07332ff9bd07b54d2b8ce9 F src/test_devsym.c aff2255ea290d7718da08af30cdf18e470ff7325a5eff63e0057b1496ed66593 F src/test_fs.c ba1e1dc18fd3159fdba0b9c4256f14032159785320dfbd6776eb9973cb75d480 F src/test_func.c 24df3a346c012b1fc9e1001d346db6054deb426db0a7437e92490630e71c9b0a @@ -707,18 +715,18 @@ F src/test_window.c cdae419fdcea5bad6dcd9368c685abdad6deb59e9fc8b84b153de513d394 F src/test_wsd.c 41cadfd9d97fe8e3e4e44f61a4a8ccd6f7ca8fe9 F src/threads.c 4ae07fa022a3dc7c5beb373cf744a85d3c5c6c3c F src/tokenize.c 1305797eab3542a0896b552c6e7669c972c1468e11e92b370533c1f37a37082b -F src/treeview.c 07787f67cd297a6d09d04b8d70c06769c60c9c1d9080378f93929c16f8fd3298 -F src/trigger.c 4163ada044af89d51caba1cb713a73165347b2ec05fe84a283737c134d61fcd5 -F src/update.c aebd5b9f352e82b3fea709e97079c4723f2acd29d2b5d88058bcc205b77e7f0c +F src/treeview.c 29b1dc7e0f84ba090734febe27393d4719682af0cae1b902d5ebf0236ecebea4 +F src/trigger.c 5e68b790f022b8dafbfb0eb244786512a95c9575fc198719d2557d73e5795858 +F src/update.c 1a1073dd3519d2a073d62e5243d8bedf8f1c8e751d0a593588a97b9637cd7d3d F src/upsert.c 5303dc6c518fa7d4b280ec65170f465c7a70b7ac2b22491598f6d0b4875b3145 F src/utf.c ee39565f0843775cc2c81135751ddd93eceb91a673ea2c57f61c76f288b041a0 -F src/util.c 0be191521ff6d2805995f4910f0b6231b42843678b2efdc1abecaf39929a673f +F src/util.c 313f3154e2b85a447326f5dd15de8d31a4df6ab0c3579bd58f426ff634ec9050 F src/vacuum.c f6e47729554e0d2c576bb710c415d5bc414935be0d7a70f38d1f58ffa7a6d8c0 -F src/vdbe.c 40ccde927b8ccee8173671d5ab77f0675857f7f6aaadca902ea8a2c4aa29a221 -F src/vdbe.h 58675f47dcf3105bab182c3ad3726efd60ffd003e954386904ac9107d0d2b743 -F src/vdbeInt.h 17b7461ffcf9ee760d1341731715a419f6b8c763089a7ece25c2e8098d702b3f -F src/vdbeapi.c 1e8713d0b653acb43cd1bdf579c40e005c4844ea90f414f065946a83db3c27fb -F src/vdbeaux.c fc2a532acdc75265f4f4e236e004ef22dba4a6e1aababb31c504a4b954aa9819 +F src/vdbe.c 69bfbf1f0697c169f2854270cc4ae56f41c0a952ace76380701f9c97ef23e938 +F src/vdbe.h 73b904a6b3bb27f308c6cc287a5751ebc7f1f89456be0ed068a12b92844c6e8c +F src/vdbeInt.h 8651e4c4e04d1860d0bdcf330cb8294e3778a9d4222be30ce4c490d9220af783 +F src/vdbeapi.c df3f73a4d0a487f2068e3c84776cd6e3fba5ae80ff612659dcfda4307686420b +F src/vdbeaux.c 2eee24133b51fd721bbbd01b5a99bc76afc6d37f334d6c5bdb20bab2a077047e F src/vdbeblob.c 5e61ce31aca17db8fb60395407457a8c1c7fb471dde405e0cd675974611dcfcd F src/vdbemem.c 6cfed43758d57b6e3b99d9cdedfeccd86e45a07e427b22d8487cbdbebb6c522a F src/vdbesort.c 43756031ca7430f7aec3ef904824a7883c4ede783e51f280d99b9b65c0796e35 @@ -729,14 +737,14 @@ F src/vxworks.h d2988f4e5a61a4dfe82c6524dd3d6e4f2ce3cdb9 F src/wal.c 419c0a98dd65b9419fd07bdd4f9facc61143c33d57b0dbeffd83cf38a2e0e33e F src/wal.h 7ffe787437f20a098af347011967a6d3bb8e5c3dc645e6be59eff44d2b2c5297 F src/walker.c f890a3298418d7cba3b69b8803594fdc484ea241206a8dfa99db6dd36f8cbb3b -F src/where.c 1ef5aae7fac877057b9f360f06b26d4275888460d8fb6e92bbb9e70e07afe946 -F src/whereInt.h df0c79388c0b71b4a91f480d02791679fe0345d40410435c541c8893e95a4d3f -F src/wherecode.c 133a94f82858787217d073143617df19e4a6a7d0b771a1519f957608109ad5a5 +F src/where.c d0d8e3cb2c11e77ba0f8f9ed8eada9d84dbd377167cdcf387b8eeb824c35a3ad +F src/whereInt.h e25203e5bfee149f5f1225ae0166cfb4f1e65490c998a024249e98bb0647377c +F src/wherecode.c 76bca3379219880d2527493b71a3be49e696f75396d3481e4de5d4ceec7886b2 F src/whereexpr.c 05295b44b54eea76d1ba766f0908928d0e20e990c249344c9521454d3d09c7ae -F src/window.c 928e215840e2f2d9a2746e018c9643ef42c66c4ab6630ef0df7fa388fa145e86 +F src/window.c 14836767adb26573b50f528eb37f8b1336f2c430ab38de7cead1e5c546bb4d8c F test/8_3_names.test ebbb5cd36741350040fd28b432ceadf495be25b2 F test/affinity2.test ce1aafc86e110685b324e9a763eab4f2a73f737842ec3b687bd965867de90627 -F test/affinity3.test b5c19d504dec222c0dc66642673d23dce915d35737b68e74d9f237b80493eb53 +F test/affinity3.test f094773025eddf31135c7ad4cde722b7696f8eb07b97511f98585addf2a510a9 F test/aggerror.test a867e273ef9e3d7919f03ef4f0e8c0d2767944f2 F test/aggnested.test 7269d07ac879fce161cb26c8fabe65cba5715742fac8a1fccac570dcdaf28f00 F test/alias.test 4529fbc152f190268a15f9384a5651bbbabc9d87 @@ -814,6 +822,7 @@ F test/backup_ioerr.test 4c3c7147cee85b024ecf6e150e090c32fdbb5135 F test/backup_malloc.test 0c9abdf74c51e7bedb66d504cd684f28d4bd4027 F test/badutf.test d5360fc31f643d37a973ab0d8b4fb85799c3169f F test/badutf2.test f310fd3b24a491b6b77bccdf14923b85d6ebcce751068c180d93a6b8ff854399 +F test/basexx1.test d8a50f0744b93dca656625597bcd3499ff4b9a4ea2a82432b119b7d46e3e0c08 F test/bc_common.tcl b5e42d80305be95697e6370e015af571e5333a1c F test/bc_test1.c e0a092579552e066ed4ce7bcdaecfa69c4aacc8d F test/bestindex1.test 856a453dff8c68b4568601eed5a8b5e20b4763af9229f3947c215729ed878db0 @@ -860,7 +869,7 @@ F test/capi3c.test 54e2dc0c8fd7c34ad1590d1be6864397da2438c95a9f5aee2f8fbc60c112e F test/capi3d.test 8b778794af891b0dca3d900bd345fbc8ebd2aa2aae425a9dccdd10d5233dfbde F test/capi3e.test 3d49c01ef2a1a55f41d73cba2b23b5059ec460fe F test/carray01.test d55d57bf66b1af1c7ac55fae66ff4910884a8f5d21a90a18797ce386212a2634 -F test/cast.test 6064022ba9af31a8a2ff7bb345e5bd0e74172ffad85bdab5898a42d8227c7585 +F test/cast.test e3a7e452f37efec0df0a89e55aa2f04861ba6613deb16075101414668bf4bb24 F test/cffault.test 9d6b20606afe712374952eec4f8fd74b1a8097ef F test/changes.test 9dd8e597d84072122fc8a4fcdea837f4a54a461e6e536053ea984303e8ca937b F test/changes2.test d222c0cbf5ab0ac4d7c180594e486c1bf20b2098d33e56ce33b8e12eba6823b9 @@ -997,7 +1006,7 @@ F test/enc.test 9a7be5479da985381d740b15f432800f65e2c87029ee57a318f42cb2eb43763a F test/enc2.test 848bf05f15b011719f478dddb7b5e9aea35e39e457493cba4c4eef75d849a5ec F test/enc3.test 55ef64416d72975c66167310a51dc9fc544ba3ae4858b8d5ab22f4cb6500b087 F test/enc4.test c8f1ce3618508fd0909945beb8b8831feef2c020 -F test/eqp.test 473aea9599b4b7af46614b55198cd78167e4eccd48e60812a40db47c5c41dea9 +F test/eqp.test f3f7548d2f3df03e2f23ecaf35c7c2cc7b89848bd7c3606d94a010521b7ea4f6 F test/errmsg.test eae9f091eb39ce7e20305de45d8e5d115b68fa856fba4ea6757b6ca3705ff7f9 F test/eval.test 73969a2d43a511bf44080c44485a8c4d796b6a4f038d19e491867081155692c0 F test/exclusive.test 7ff63be7503990921838d5c9f77f6e33e68e48ed1a9d48cd28745bf650bf0747 @@ -1105,7 +1114,7 @@ F test/fts3e.test 1f6c6ac9cc8b772ca256e6b22aaeed50c9350851 F test/fts3expr.test ebae205a7a89446c32583bcd492dcb817b9f6b31819bb4dde2583bb99c77e526 F test/fts3expr2.test 18da930352e5693eaa163a3eacf96233b7290d1a F test/fts3expr3.test c4d4a7d6327418428c96e0a3a1137c251b8dfbf8 -F test/fts3expr4.test f5b2832549f01b1f7f73389fa21d4b875499bc95bf7c8b36271844888c6a0938 +F test/fts3expr4.test 6c7675bbdbffe6ffc95e9e861500b8ac3f739c4d004ffda812f138eeb1b45529 F test/fts3expr5.test a5b9a053becbdb8e973fbf4d6d3abaabeb42d511d1848bd57931f3e0a1cf983e F test/fts3f.test 8c438d5e1cab526b0021988fb1dc70cf3597b006a33ffd6c955ee89929077fe3 F test/fts3fault.test f4e1342acfe6d216a001490e8cd52afac1f9ffe4a11bbcdcb296129a45c5df45 @@ -1177,11 +1186,11 @@ F test/fuzzdata4.db b502c7d5498261715812dd8b3c2005bad08b3a26e6489414bd13926cd3e4 F test/fuzzdata5.db e35f64af17ec48926481cfaf3b3855e436bd40d1cfe2d59a9474cb4b748a52a5 F test/fuzzdata6.db 92a80e4afc172c24f662a10a612d188fb272de4a9bd19e017927c95f737de6d7 F test/fuzzdata7.db 0166b56fd7a6b9636a1d60ef0a060f86ddaecf99400a666bb6e5bbd7199ad1f2 -F test/fuzzdata8.db 653423800b7671e67caa740e977d80e1360f0d69e9992851f3ea5c4a69a2724a +F test/fuzzdata8.db d07d68dcebbc4d537eb9fe066ed858239f901f48032c8e627b52e0f39058ac7a F test/fuzzer1.test 3d4c4b7e547aba5e5511a2991e3e3d07166cfbb8 F test/fuzzer2.test a85ef814ce071293bce1ad8dffa217cbbaad4c14 F test/fuzzerfault.test f64c4aef4c9e9edf1d6dc0d3f1e65dcc81e67c996403c88d14f09b74807a42bc -F test/fuzzinvariants.c 7877178eaa10eb3ea986f81a7010efc371ccd3e13ee5b14fa290b0459002a36a +F test/fuzzinvariants.c a153253600b2b33a7d5710d40e89b2ac1373a1912517867fb995a45b2d67dcb8 F test/gcfault.test dd28c228a38976d6336a3fc42d7e5f1ad060cb8c F test/gencol1.test cc0dbb0ee116e5602e18ea7d47f2a0f76b26e09a823b7c36ef254370c2b0f3c1 F test/genesis.tcl 1e2e2e8e5cc4058549a154ff1892fe5c9de19f98 @@ -1220,7 +1229,7 @@ F test/index7.test b238344318e0b4e42126717f6554f0e7dfd0b39cecad4b736039b43e1e3b6 F test/index8.test caa097735c91dbc23d8a402f5e63a2a03c83840ba3928733ed7f9a03f8a912a3 F test/index9.test 2ac891806a4136ef3e91280477e23114e67575207dc331e6797fa0ed9379f997 F test/indexedby.test f21eca4f7a6ffe14c8500a7ad6cd53166666c99e5ccd311842a28bc94a195fe0 -F test/indexexpr1.test 3360c2a29a8844e7c4b13293567025281257f9e13a31854cfff6959cede11502 +F test/indexexpr1.test 1cd460113e3999a1477754fd0e3ac0d7bcbbbc742b427b418945f28af708c695 F test/indexexpr2.test 2c7abe3c48f8aaa5a448615ab4d13df3662185d28419c00999670834a3f0b484 F test/indexfault.test 98d78a8ff1f5335628b62f886a1cb7c7dac1ef6d48fa39c51ec871c87dce9811 F test/init.test 15c823093fdabbf7b531fe22cf037134d09587a7 @@ -1323,6 +1332,7 @@ F test/manydb.test 28385ae2087967aa05c38624cec7d96ec74feb3e F test/mem5.test c6460fba403c5703141348cd90de1c294188c68f F test/memdb.test c1f2a343ad14398d5d6debda6ea33e80d0dafcc7 F test/memdb1.test 2c4e9cc10d21c6bf4e217d72b7f6b8ba9b2605971bb2c5e6df76018e189f98f5 +F test/memdb2.test 7789975b96b0726032a22c1afc026592c7ff175bf05be11f1d640791541a77ea F test/memjournal.test 70f3a00c7f84ee2978ad14e831231caa1e7f23915a2c54b4f775a021d5740c6c F test/memjournal2.test 6b9083cfaab9a3281ec545c3da2487999e8025fb7501bbae10f713f80c56454c F test/memleak.test 10b9c6c57e19fc68c32941495e9ba1c50123f6e2 @@ -1333,7 +1343,7 @@ F test/minmax.test fe638b55d77d2375531a8f549b338eafcd9adfbd2f72df37ed77d9b26ca0a F test/minmax2.test cf9311babb6f0518d04e42fd6a42c619531c4309a9dd790a2c4e9b3bc595e0de F test/minmax3.test cc1e8b010136db0d01a6f2a29ba5a9f321034354 F test/minmax4.test 272ca395257f05937dc96441c9dde4bc9fbf116a8d4fa02baeb0d13d50e36c87 -F test/misc1.test 294c97185354030c4ce40e7141b72f7a589585f2a44b666825381eb3df98f07c +F test/misc1.test 9955e70cab5e284d77e358cfa1f1dd43f5e4bc00a421018581b0fbd62206a6a1 F test/misc2.test 71e746af479119386ac2ed7ab7d81d99970e75b49ffd3e8efffee100b4b5f350 F test/misc3.test cf3dda47d5dda3e53fc5804a100d3c82be736c9d F test/misc4.test 10cd6addb2fa9093df4751a1b92b50440175dd5468a6ec84d0386e78f087db0e @@ -1351,7 +1361,7 @@ F test/mmapfault.test d4c9eff9cd8c2dc14bc43e71e042f175b0a26fe3 F test/mmapwarm.test 2272005969cd17a910077bd5082f70bc1fefad9a875afec7fc9af483898ecaf3 F test/multiplex.test d74c034e52805f6de8cc5432cef8c9eb774bb64ec29b83a22effc8ca4dac1f08 F test/multiplex2.test 580ca5817c7edbe4cc68fa150609c9473393003a -F test/multiplex3.test d228f59eac91839a977eac19f21d053f03e4d101 +F test/multiplex3.test fac575e0b1b852025575a6a8357701d80933e98b5d2fe6d35ddaa68f92f6a1f7 F test/multiplex4.test e8ae4c4bd70606a5727743241f13b5701990abe4 F test/mutex1.test 177db2e4edb530f2ff21edc52ac79a412dbe63e4c47c3ae9504d3fb4f1ce81fa F test/mutex2.test bfeaeac2e73095b2ac32285d2756e3a65e681660 @@ -1375,7 +1385,7 @@ F test/openv2.test 0d3040974bf402e19b7df4b783e447289d7ab394 F test/optfuzz-db01.c 9f2fa80b8f84ebbf1f2e8b13421a4e0477fe300f6686fbd76cac1d2db66e0fdc F test/optfuzz-db01.txt 21f6bdeadc701cf11528276e2a55c70bfcb846ba42df327f979bd9e7b6ce7041 F test/optfuzz.c 690430a0bf0ad047d5a168bf52b05b2ee97aedaad8c14337e9eb5050faa64994 -F test/orderby1.test a4bba04b9c60a21e53486fbc173a596b29641a3b3a57a0f26a1cbef1360358e9 +F test/orderby1.test 02cfd870127a7342170b829175c5c53e9e7405744451ac1aeb2f7e2b0c18ca76 F test/orderby2.test bc11009f7cd99d96b1b11e57b199b00633eb5b04 F test/orderby3.test 8619d06a3debdcd80a27c0fdea5c40b468854b99 F test/orderby4.test 4d39bfbaaa3ae64d026ca2ff166353d2edca4ba4 @@ -1414,7 +1424,7 @@ F test/printf.test 390d0d7fcffc3c4ea3c1bb4cbb267444e32b33b048ae21895f23a291844fe F test/printf2.test 3f55c1871a5a65507416076f6eb97e738d5210aeda7595a74ee895f2224cce60 F test/progress.test ebab27f670bd0d4eb9d20d49cef96e68141d92fb F test/ptrchng.test ef1aa72d6cf35a2bbd0869a649b744e9d84977fc -F test/pushdown.test 5e72c51c5e33253ed639ccee1e01ce62d62b6eee5ca893cd82334e4ee7b1d7fc +F test/pushdown.test f270b8071c02efc218430e0d388c155e1962eaa1d3a3ab186dd38ad6d7e178a4 F test/queryonly.test 5f653159e0f552f0552d43259890c1089391dcca F test/quick.test 1681febc928d686362d50057c642f77a02c62e57 F test/quota-glob.test 32901e9eed6705d68ca3faee2a06b73b57cb3c26 @@ -1426,13 +1436,13 @@ F test/randexpr1.test eda062a97e60f9c38ae8d806b03b0ddf23d796df F test/rbu.test 168573d353cd0fd10196b87b0caa322c144ef736 F test/rdonly.test 64e2696c322e3538df0b1ed624e21f9a23ed9ff8 F test/recover.test fd5199f928757cb308661b5fdca1abc19398a798ff7f24b57c3071e9f8e0471e -F test/regexp1.test 83c631617357150f8054ca1d1fed40a552b0d0f8eb7a7f090c3be02cee9f9913 +F test/regexp1.test 8f2a8bc1569666e29a4cee6c1a666cd224eb6d50e2470d1dc1df995170f3e0f1 F test/regexp2.test 55ed41da802b0e284ac7e2fe944be3948f93ff25abbca0361a609acfed1368b5 F test/reindex.test cd9d6021729910ece82267b4f5e1b5ac2911a7566c43b43c176a6a4732e2118d F test/releasetest_data.tcl 0db8aee0c348090fd06da47020ab4ed8ec692e0723427b2f3947d4dfb806f3b0 F test/resetdb.test 8062cf10a09d8c048f8de7711e94571c38b38168db0e5877ba7561789e5eeb2b F test/resolver01.test f4022acafda7f4d40eca94dbf16bc5fc4ac30ceb -F test/returning1.test c43b8370a351f77aec6d71f4a2cde59b849369ed1933261a2c2c69e23e34ff5e +F test/returning1.test 1366e04566cfe1a082d17b1e0f195ec64473c79374b3a5d4ae00c43d885dea31 F test/returningfault.test ae4c4b5e8745813287a359d9ccdb9d5c883c2e68afb18fb0767937d5de5692a4 F test/rollback.test 06680159bc6746d0f26276e339e3ae2f951c64812468308838e0a3362d911eaa F test/rollback2.test 3f3a4e20401825017df7e7671e9f31b6de5fae5620c2b9b49917f52f8c160a8f @@ -1462,7 +1472,8 @@ F test/savepoint5.test 0735db177e0ebbaedc39812c8d065075d563c4fd F test/savepoint6.test f41279c5e137139fa5c21485773332c7adb98cd7 F test/savepoint7.test cde525ea3075283eb950cdcdefe23ead4f700daa F test/savepointfault.test f044eac64b59f09746c7020ee261734de82bf9b2 -F test/scanstatus.test 9a0ed37ab6d57b50567282788fffdf832d9b16739ecc41bff9d77a8d767cf317 +F test/scanstatus.test 7dbcfd6adc6a8df6abc59f83d6da5a27e1bce0b2f6fa55147c8176d7c44e0450 +F test/scanstatus2.test 719c87a0ac817a67899d3e859923c8bb297ac655617b2dcfb602eb25dd829d45 F test/schema.test 5dd11c96ba64744de955315d2e4f8992e447533690153b93377dffb2a5ef5431 F test/schema2.test 906408621ea881fdb496d878b1822572a34e32c5 F test/schema3.test 8ed4ae66e082cdd8b1b1f22d8549e1e7a0db4527a8e6ee8b6193053ee1e5c9ce @@ -1506,9 +1517,9 @@ F test/sharedB.test 1a84863d7a2204e0d42f2e1606577c5e92e4473fa37ea0f5bdf829e4bf8e F test/shared_err.test 32634e404a3317eeb94abc7a099c556a346fdb8fb3858dbe222a4cbb8926a939 F test/sharedlock.test 5ede3c37439067c43b0198f580fd374ebf15d304 F test/shell1.test e4b4de56f454708e0747b52915135baa2cbfec4965406d6eaf02a4a5c22a9880 -F test/shell2.test c536c2aab4852608f8a606262330797abc4d964a4c2c782a7760f54ea1f17a6a +F test/shell2.test 1190b951373fdfe719bc6ac16962bc743dfa4355db8ae546c0bb9bf559a28d4a F test/shell3.test 91febeac0412812bf6370abb8ed72700e32bf8f9878849414518f662dfd55e8a -F test/shell4.test 7dc8a515705bc093d8ffe381670e8fa7a969661e8ed177c35c847e3c6dfc35e2 +F test/shell4.test 9abd0c12a7e20a4c49e84d5be208d2124fa6c09e728f56f1f4bee0f02853935f F test/shell5.test c8b6c54f26ec537f8558273d7ed293ca3725ef42e6b12b8f151718628bd1473b F test/shell6.test 1ceb51b2678c472ba6cf1e5da96679ce8347889fe2c3bf93a0e0fa73f00b00d3 F test/shell7.test 115132f66d0463417f408562cc2cf534f6bbc6d83a6d50f0072a9eb171bae97f @@ -1531,7 +1542,7 @@ F test/snapshot_fault.test f6c5ef7cb93bf92fbb4e864ecc5c87df7d3a250064838822db5b4 F test/snapshot_up.test a0a29c4cf33475fcef07c3f8e64af795e24ab91b4cc68295863402a393cdd41c F test/soak.test 18944cf21b94a7fe0df02016a6ee1e9632bc4e8d095a0cb49d95e15d5cca2d5c F test/softheap1.test 843cd84db9891b2d01b9ab64cef3e9020f98d087 -F test/sort.test c2adc635c2564241fefec0b3a68391ef6868fd3b +F test/sort.test f86751134159abb5e5fd4381a0d7038c91013638cd1e3fa1d7850901f6df6196 F test/sort2.test cc23b7c19d684657559e8a55b02f7fcee03851d0 F test/sort3.test 1480ed7c4c157682542224e05e3b75faf4a149e5 F test/sort4.test 5c34d9623a4ae5921d956dfa2b70e77ed0fc6e5c @@ -1639,6 +1650,7 @@ F test/tkt-868145d012.test a5f941107ece6a64410ca4755c6329b7eb57a356 F test/tkt-8c63ff0ec.test 258b7fc8d7e4e1cb5362c7d65c143528b9c4cbed F test/tkt-91e2e8ba6f.test 08c4f94ae07696b05c9b822da0b4e5337a2f54c5 F test/tkt-94c04eaadb.test f738c57c7f68ab8be1c054415af7774617cb6223 +F test/tkt-99378177930f87bd.test 2f07020a82ed1c56bdad60a8a6ef508b2f8a1fb056300b7ec650cbd9975b46bf F test/tkt-9a8b09f8e6.test b2ef151d0984b2ebf237760dbeaa50724e5a0667 F test/tkt-9d68c883.test 16f7cb96781ba579bc2e19bb14b4ad609d9774b6 F test/tkt-9f2eb3abac.test cb6123ac695a08b4454c3792fbe85108f67fabf8 @@ -1786,7 +1798,7 @@ F test/tt3_vacuum.c 71b254cde1fc49d6c8c44efd54f4668f3e57d7b3a8f4601ade069f75a999 F test/types.test bf816ce73c7dfcfe26b700c19f97ef4050d194ff F test/types2.test 1aeb81976841a91eef292723649b5c4fe3bc3cac F test/types3.test 99e009491a54f4dc02c06bdbc0c5eea56ae3e25a -F test/unionall.test bfeeea6c18c09a46f7e8bed69173e31c71336152e6253fb37c7257c76509f0e4 +F test/unionall.test eb9afa030897af75fd2f0dd28354ef63c8a5897b6c76aa1f15acae61a12eabcf F test/unionall2.test 71e8fa08d5699d50dc9f9dc0c9799c2e7a6bb7931a330d369307a4df7f157fa1 F test/unionallfault.test 652bfbb630e6c43135965dc1e8f0a9a791da83aec885d626a632fe1909c56f73 F test/unionvtab.test e1704ab1b4c1bb3ffc9da4681f8e85a0b909fd80b937984fc94b27415ac8e5a4 @@ -1823,7 +1835,7 @@ F test/vacuum6.test b137b04bf3392d3f5c3b8fda0ce85a6775a70ca112f6559f74ff52dc9ce0 F test/vacuummem.test 4b30f5b95a9ff86e9d5c20741e50a898b2dc10b0962a3211571eb165357003fb F test/varint.test bbce22cda8fc4d135bcc2b589574be8410614e62 F test/veryquick.test 57ab846bacf7b90cf4e9a672721ea5c5b669b661 -F test/view.test d16e49e89ada6137d1447777ef2a74574526a3b024e6733bf53ae2960da8c17c +F test/view.test a7c463254fd7ce86e861b60ddf489d5c14f22e8052f5ab2f5ff57d3eb17ccdea F test/view2.test db32c8138b5b556f610b35dfddd38c5a58a292f07fda5281eedb0851b2672679 F test/view3.test ad8a8290ee2b55ff6ce66c9ef1ce3f1e47926273a3814e1c425293e128a95456 F test/vt02.c 33ecddc0832d4cd24e9e9fa83d868981b1e049462f4ec9080710353f6479a534 @@ -1890,7 +1902,7 @@ F test/walthread.test 14b20fcfa6ae152f5d8e12f5dc8a8a724b7ef189f5d8ef1e2ceab79f2a F test/walvfs.test e1a6ad0f3c78e98b55c3d5f0889cf366cc0d0a1cb2bccb44ac9ec67384adc4a1 F test/wapp.tcl b440cd8cf57953d3a49e7ee81e6a18f18efdaf113b69f7d8482b0710a64566ec F test/wapptest.tcl 1bea58a6a8e68a73f542ee4fca28b771b84ed803bd0c9e385087070b3d747b3c x -F test/where.test d13cd7c24e80009d2b54e2f7a8893c457afa49c64f99359c9eb3fe668ba1d9d4 +F test/where.test 3954cf22ba7b17f9606e177001d2963bcd1ecfbc6e1e7caadb14462f7eecd099 F test/where2.test 03c21a11e7b90e2845fc3c8b4002fc44cc2797fa74c86ee47d70bd7ea4f29ed6 F test/where3.test 5b4ffc0ac2ea0fe92f02b1244b7531522fe4d7bccf6fa8741d54e82c10e67753 F test/where4.test 4a371bfcc607f41d233701bdec33ac2972908ba8 @@ -1946,10 +1958,10 @@ F test/windowfault.test 15094c1529424e62f798bc679e3fe9dfab6e8ba2f7dfe8c923b6248c F test/windowpushd.test d8895d08870b7226f7693665bd292eb177e62ca06799184957b3ca7dc03067df F test/with1.test 9ad67fdeb2bbd808a5763c9060e214ea232f9b18d846ea3a59756dc38bdc3880 F test/with2.test a1df41b987198383b9b70bf5e5fda390582e46398653858dbc6ceb24253b28df -F test/with3.test 1e2e8d5e7b1d955342d0d18c250aaaa6e6bcf36ef2a818477bd01cb74f9a5d66 +F test/with3.test e7bf809bf75c1f44f98bca78bc331dbf542002c5227bf53c1261144db4e824c8 F test/with4.test 257be66c0c67fee1defbbac0f685c3465e2cad037f21ce65f23f86084f198205 F test/with5.test 6248213c41fab36290b5b73aa3f937309dfba337004d9d8434c3fabc8c7d4be8 -F test/with6.test c18592592b5a1c5802fd4e933d506f7b34ebbe8fdd585229793e960ae58d433f +F test/with6.test ae570b31bf1f6fab6210fb1caf6dfa9a6d69c0e6633beb905583bb158a5e309e F test/withM.test 693b61765f2b387b5e3e24a4536e2e82de15ff64 F test/without_rowid1.test a5210b8770dc4736bca4e74bc96588f43025ad03ad6a80f885afd36d9890e217 F test/without_rowid2.test af260339f79d13cb220288b67cd287fbcf81ad99 @@ -2000,15 +2012,15 @@ F tool/mkctimec.tcl c185cf1bdcd3d9bd3c06f77a2fd2df8a4a0d07266f992ecda75286965ba3 F tool/mkkeywordhash.c 35bfc41adacc4aa6ef6fca7fd0c63e0ec0534b78daf4d0cfdebe398216bbffc3 F tool/mkmsvcmin.tcl 6ecab9fe22c2c8de4d82d4c46797bda3d2deac8e763885f5a38d0c44a895ab33 F tool/mkopcodec.tcl 33d20791e191df43209b77d37f0ff0904620b28465cca6990cf8d60da61a07ef -F tool/mkopcodeh.tcl bcb2bd5affb545fd219ef0304c7978e2a356407ab723f45ec8569235892c1c3f +F tool/mkopcodeh.tcl 769d9e6a8b462323150dc13a8539d6064664b72974f7894befe2491cc73e05cd F tool/mkopts.tcl 680f785fdb09729fd9ac50632413da4eadbdf9071535e3f26d03795828ab07fa F tool/mkpragmatab.tcl 8dcba40365eceba24f059d82bbc8b61bb647d5a0b8076f24a1876872a1b58832 F tool/mkshellc.tcl 02d0de8349ef830c0fb20d29680320bde2466e2ec422e5bd94c4317a7a7e8cc9 F tool/mksourceid.c 36aa8020014aed0836fd13c51d6dc9219b0df1761d6b5f58ff5b616211b079b9 F tool/mkspeedsql.tcl a1a334d288f7adfe6e996f2e712becf076745c97 F tool/mksqlite3c-noext.tcl 4f7cfef5152b0c91920355cbfc1d608a4ad242cb819f1aea07f6d0274f584a7f -F tool/mksqlite3c.tcl 4fc26a9bfa0c4505b203d7ca0cf086e75ebcd4d63eb719c82f37e3fecdf23d37 -F tool/mksqlite3h.tcl 1f5e4a1dbbbc43c83cc6e74fe32c6c620502240b66c7c0f33a51378e78fc4edf +F tool/mksqlite3c.tcl eb47021591b1ad4a6862e2cb5625f1ac67ec1e0c6db5ba3953c069c635284cf5 +F tool/mksqlite3h.tcl d391cff7cad0a372ee1406faee9ccc7dad9cb80a0c95cae0f73d10dd26e06762 F tool/mksqlite3internalh.tcl eb994013e833359137eb53a55acdad0b5ae1049b F tool/mkvsix.tcl b9e0777a213c23156b6542842c238479e496ebf5 F tool/offsets.c 8ed2b344d33f06e71366a9b93ccedaa38c096cc1dbd4c3c26ad08c6115285845 @@ -2046,7 +2058,7 @@ F tool/symbols-mingw.sh 4dbcea7e74768305384c9fd2ed2b41bbf9f0414d F tool/symbols.sh 1612bd947750e21e7b47befad5f6b3825b06cce0705441f903bf35ced65ae9b9 F tool/varint.c 5d94cb5003db9dbbcbcc5df08d66f16071aee003 F tool/vdbe-compress.tcl 1dcb7632e57cf57105248029e6e162fddaf6c0fccb3bb9e6215603752c5a2d4a -F tool/vdbe_profile.tcl 246d0da094856d72d2c12efec03250d71639d19f +F tool/vdbe_profile.tcl 3ac5a4a9449f4baf77059358ea050db3e34395ccf59c5464d29b91746d5b961e F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee87c1b31a7 F tool/warnings.sh d58dc38367cc776550f90327e205d7946802d4004fb9f291fd8b81256bc1eedd F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f @@ -2071,8 +2083,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P 5ba588e7eb04205aac134061c7c33c844c8b8ac33ff2ead3f53c59ae52afdc2e 89c459e766ea7e9165d0beeb124708b955a4950d0f4792f457465d71b158d318 -R 5d1423a65fea9f821713bc24303d5e9c +P a06d57ee9e2862b2c67dc97e3702c00339341ba308d6558b7694e5b9e47231ac 371f9b88387a44a5f820279d79733d1deb7eafc72f320ec47a11679bbdbb49ef +R 86b38a0acfb02a8499e4768a9a084653 U drh -Z 99242b23136a2de7553c45a0f3fa98b0 +Z 41f88f8b005a3dfbf678e68c0eda1543 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index ba9738a147..d3e1ba33e6 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -a06d57ee9e2862b2c67dc97e3702c00339341ba308d6558b7694e5b9e47231ac \ No newline at end of file +0c198aee53d35cf0999f880c433140c3dfc6bd620c30920b08d7534c0f905e05 \ No newline at end of file diff --git a/src/btree.c b/src/btree.c index 61650ef09e..b7a61d66d7 100644 --- a/src/btree.c +++ b/src/btree.c @@ -2140,62 +2140,67 @@ static int freeSpace(MemPage *pPage, u16 iStart, u16 iSize){ ** Only the following combinations are supported. Anything different ** indicates a corrupt database files: ** -** PTF_ZERODATA -** PTF_ZERODATA | PTF_LEAF -** PTF_LEAFDATA | PTF_INTKEY -** PTF_LEAFDATA | PTF_INTKEY | PTF_LEAF +** PTF_ZERODATA (0x02, 2) +** PTF_LEAFDATA | PTF_INTKEY (0x05, 5) +** PTF_ZERODATA | PTF_LEAF (0x0a, 10) +** PTF_LEAFDATA | PTF_INTKEY | PTF_LEAF (0x0d, 13) */ static int decodeFlags(MemPage *pPage, int flagByte){ BtShared *pBt; /* A copy of pPage->pBt */ assert( pPage->hdrOffset==(pPage->pgno==1 ? 100 : 0) ); assert( sqlite3_mutex_held(pPage->pBt->mutex) ); - pPage->leaf = (u8)(flagByte>>3); assert( PTF_LEAF == 1<<3 ); - flagByte &= ~PTF_LEAF; - pPage->childPtrSize = 4-4*pPage->leaf; pBt = pPage->pBt; - if( flagByte==(PTF_LEAFDATA | PTF_INTKEY) ){ - /* EVIDENCE-OF: R-07291-35328 A value of 5 (0x05) means the page is an - ** interior table b-tree page. */ - assert( (PTF_LEAFDATA|PTF_INTKEY)==5 ); - /* EVIDENCE-OF: R-26900-09176 A value of 13 (0x0d) means the page is a - ** leaf table b-tree page. */ - assert( (PTF_LEAFDATA|PTF_INTKEY|PTF_LEAF)==13 ); - pPage->intKey = 1; - if( pPage->leaf ){ + pPage->max1bytePayload = pBt->max1bytePayload; + if( flagByte>=(PTF_ZERODATA | PTF_LEAF) ){ + pPage->childPtrSize = 0; + pPage->leaf = 1; + if( flagByte==(PTF_LEAFDATA | PTF_INTKEY | PTF_LEAF) ){ pPage->intKeyLeaf = 1; pPage->xCellSize = cellSizePtrTableLeaf; pPage->xParseCell = btreeParseCellPtr; + pPage->intKey = 1; + pPage->maxLocal = pBt->maxLeaf; + pPage->minLocal = pBt->minLeaf; + }else if( flagByte==(PTF_ZERODATA | PTF_LEAF) ){ + pPage->intKey = 0; + pPage->intKeyLeaf = 0; + pPage->xCellSize = cellSizePtr; + pPage->xParseCell = btreeParseCellPtrIndex; + pPage->maxLocal = pBt->maxLocal; + pPage->minLocal = pBt->minLocal; }else{ + pPage->intKey = 0; + pPage->intKeyLeaf = 0; + pPage->xCellSize = cellSizePtr; + pPage->xParseCell = btreeParseCellPtrIndex; + return SQLITE_CORRUPT_PAGE(pPage); + } + }else{ + pPage->childPtrSize = 4; + pPage->leaf = 0; + if( flagByte==(PTF_ZERODATA) ){ + pPage->intKey = 0; + pPage->intKeyLeaf = 0; + pPage->xCellSize = cellSizePtr; + pPage->xParseCell = btreeParseCellPtrIndex; + pPage->maxLocal = pBt->maxLocal; + pPage->minLocal = pBt->minLocal; + }else if( flagByte==(PTF_LEAFDATA | PTF_INTKEY) ){ pPage->intKeyLeaf = 0; pPage->xCellSize = cellSizePtrNoPayload; pPage->xParseCell = btreeParseCellPtrNoPayload; + pPage->intKey = 1; + pPage->maxLocal = pBt->maxLeaf; + pPage->minLocal = pBt->minLeaf; + }else{ + pPage->intKey = 0; + pPage->intKeyLeaf = 0; + pPage->xCellSize = cellSizePtr; + pPage->xParseCell = btreeParseCellPtrIndex; + return SQLITE_CORRUPT_PAGE(pPage); } - pPage->maxLocal = pBt->maxLeaf; - pPage->minLocal = pBt->minLeaf; - }else if( flagByte==PTF_ZERODATA ){ - /* EVIDENCE-OF: R-43316-37308 A value of 2 (0x02) means the page is an - ** interior index b-tree page. */ - assert( (PTF_ZERODATA)==2 ); - /* EVIDENCE-OF: R-59615-42828 A value of 10 (0x0a) means the page is a - ** leaf index b-tree page. */ - assert( (PTF_ZERODATA|PTF_LEAF)==10 ); - pPage->intKey = 0; - pPage->intKeyLeaf = 0; - pPage->xCellSize = cellSizePtr; - pPage->xParseCell = btreeParseCellPtrIndex; - pPage->maxLocal = pBt->maxLocal; - pPage->minLocal = pBt->minLocal; - }else{ - /* EVIDENCE-OF: R-47608-56469 Any other value for the b-tree page type is - ** an error. */ - pPage->intKey = 0; - pPage->intKeyLeaf = 0; - pPage->xCellSize = cellSizePtr; - pPage->xParseCell = btreeParseCellPtrIndex; - return SQLITE_CORRUPT_PAGE(pPage); } - pPage->max1bytePayload = pBt->max1bytePayload; return SQLITE_OK; } @@ -3739,7 +3744,7 @@ int sqlite3BtreeBeginTrans(Btree *p, int wrflag, int *pSchemaVersion){ BtShared *pBt = p->pBt; Pager *pPager = pBt->pPager; int rc = SQLITE_OK; - int bConcurrent = (p->db->eConcurrent && !ISAUTOVACUUM); + int bConcurrent = (p->db->eConcurrent && !ISAUTOVACUUM(pBt)); sqlite3BtreeEnter(p); btreeIntegrity(p); @@ -5954,9 +5959,25 @@ int sqlite3BtreeFirst(BtCursor *pCur, int *pRes){ ** on success. Set *pRes to 0 if the cursor actually points to something ** or set *pRes to 1 if the table is empty. */ +static SQLITE_NOINLINE int btreeLast(BtCursor *pCur, int *pRes){ + int rc = moveToRoot(pCur); + if( rc==SQLITE_OK ){ + assert( pCur->eState==CURSOR_VALID ); + *pRes = 0; + rc = moveToRightmost(pCur); + if( rc==SQLITE_OK ){ + pCur->curFlags |= BTCF_AtLast; + }else{ + pCur->curFlags &= ~BTCF_AtLast; + } + }else if( rc==SQLITE_EMPTY ){ + assert( pCur->pgnoRoot==0 || pCur->pPage->nCell==0 ); + *pRes = 1; + rc = SQLITE_OK; + } + return rc; +} int sqlite3BtreeLast(BtCursor *pCur, int *pRes){ - int rc; - assert( cursorOwnsBtShared(pCur) ); assert( sqlite3_mutex_held(pCur->pBtree->db->mutex) ); @@ -5977,23 +5998,7 @@ int sqlite3BtreeLast(BtCursor *pCur, int *pRes){ *pRes = 0; return SQLITE_OK; } - - rc = moveToRoot(pCur); - if( rc==SQLITE_OK ){ - assert( pCur->eState==CURSOR_VALID ); - *pRes = 0; - rc = moveToRightmost(pCur); - if( rc==SQLITE_OK ){ - pCur->curFlags |= BTCF_AtLast; - }else{ - pCur->curFlags &= ~BTCF_AtLast; - } - }else if( rc==SQLITE_EMPTY ){ - assert( pCur->pgnoRoot==0 || pCur->pPage->nCell==0 ); - *pRes = 1; - rc = SQLITE_OK; - } - return rc; + return btreeLast(pCur, pRes); } /* Move the cursor so that it points to an entry in a table (a.k.a INTKEY) @@ -6741,8 +6746,8 @@ static int allocateBtreePage( ** the entire-list will be searched for that page. */ if( eMode==BTALLOC_EXACT ){ - assert( ISAUTOVACUUM!=ISCONCURRENT ); - if( ISAUTOVACUUM ){ + assert( ISAUTOVACUUM(pBt)!=ISCONCURRENT ); + if( ISAUTOVACUUM(pBt) ){ if( nearby<=mxPage ){ u8 eType; assert( nearby>0 ); @@ -7509,24 +7514,20 @@ static void dropCell(MemPage *pPage, int idx, int sz, int *pRC){ ** in pTemp or the original pCell) and also record its index. ** Allocating a new entry in pPage->aCell[] implies that ** pPage->nOverflow is incremented. -** -** *pRC must be SQLITE_OK when this routine is called. */ -static void insertCell( +static int insertCell( MemPage *pPage, /* Page into which we are copying */ int i, /* New cell becomes the i-th cell of the page */ u8 *pCell, /* Content of the new cell */ int sz, /* Bytes of content in pCell */ u8 *pTemp, /* Temp storage space for pCell, if needed */ - Pgno iChild, /* If non-zero, replace first 4 bytes with this value */ - int *pRC /* Read and write return code from here */ + Pgno iChild /* If non-zero, replace first 4 bytes with this value */ ){ int idx = 0; /* Where to write new cell content in data[] */ int j; /* Loop counter */ u8 *data; /* The content of the whole page */ u8 *pIns; /* The point in pPage->aCellIdx[] where no cell inserted */ - assert( *pRC==SQLITE_OK ); assert( i>=0 && i<=pPage->nCell+pPage->nOverflow ); assert( MX_CELL(pPage->pBt)<=10921 ); assert( pPage->nCell<=MX_CELL(pPage->pBt) || CORRUPT_DB ); @@ -7562,14 +7563,13 @@ static void insertCell( BtShared *pBt = pPage->pBt; int rc = sqlite3PagerWrite(pPage->pDbPage); if( rc!=SQLITE_OK ){ - *pRC = rc; - return; + return rc; } assert( sqlite3PagerIswriteable(pPage->pDbPage) ); data = pPage->aData; assert( &data[pPage->cellOffset]==pPage->aCellIdx ); rc = allocateSpace(pPage, sz, &idx); - if( rc ){ *pRC = rc; return; } + if( rc ){ return rc; } /* The allocateSpace() routine guarantees the following properties ** if it returns successfully */ assert( idx >= 0 ); @@ -7595,12 +7595,15 @@ static void insertCell( if( (++data[pPage->hdrOffset+4])==0 ) data[pPage->hdrOffset+3]++; assert( get2byte(&data[pPage->hdrOffset+3])==pPage->nCell || CORRUPT_DB ); if( REQUIRE_PTRMAP ){ + int rc2 = SQLITE_OK; /* The cell may contain a pointer to an overflow page. If so, write ** the entry for the overflow page into the pointer map. */ - ptrmapPutOvflPtr(pPage, pPage, pCell, pRC); + ptrmapPutOvflPtr(pPage, pPage, pCell, &rc2); + if( rc2 ) return rc2; } } + return SQLITE_OK; } /* @@ -7701,14 +7704,16 @@ struct CellArray { ** computed. */ static void populateCellCache(CellArray *p, int idx, int N){ + MemPage *pRef = p->pRef; + u16 *szCell = p->szCell; assert( idx>=0 && idx+N<=p->nCell ); while( N>0 ){ assert( p->apCell[idx]!=0 ); - if( p->szCell[idx]==0 ){ - p->szCell[idx] = p->pRef->xCellSize(p->pRef, p->apCell[idx]); + if( szCell[idx]==0 ){ + szCell[idx] = pRef->xCellSize(pRef, p->apCell[idx]); }else{ assert( CORRUPT_DB || - p->szCell[idx]==p->pRef->xCellSize(p->pRef, p->apCell[idx]) ); + szCell[idx]==pRef->xCellSize(pRef, p->apCell[idx]) ); } idx++; N--; @@ -7910,8 +7915,8 @@ static int pageFreeArray( int nRet = 0; int i; int iEnd = iFirst + nCell; - u8 *pFree = 0; - int szFree = 0; + u8 *pFree = 0; /* \__ Parameters for pending call to */ + int szFree = 0; /* / freeSpace() */ for(i=iFirst; iapCell[i]; @@ -7932,6 +7937,9 @@ static int pageFreeArray( return 0; } }else{ + /* The current cell is adjacent to and before the pFree cell. + ** Combine the two regions into one to reduce the number of calls + ** to freeSpace(). */ pFree = pCell; szFree += sz; } @@ -8167,8 +8175,8 @@ static int balance_quick(MemPage *pParent, MemPage *pPage, u8 *pSpace){ /* Insert the new divider cell into pParent. */ if( rc==SQLITE_OK ){ - insertCell(pParent, pParent->nCell, pSpace, (int)(pOut-pSpace), - 0, pPage->pgno, &rc); + rc = insertCell(pParent, pParent->nCell, pSpace, (int)(pOut-pSpace), + 0, pPage->pgno); } /* Set the right-child pointer of pParent to point to the new page. */ @@ -8703,15 +8711,17 @@ static int balance_nonroot( d = r + 1 - leafData; (void)cachedCellSize(&b, d); do{ + int szR, szD; assert( d szLeft-(b.szCell[r]+(i==k-1?0:2)))){ + && (bBulk || szRight+szD+2 > szLeft-(szR+(i==k-1?0:2)))){ break; } - szRight += b.szCell[d] + 2; - szLeft -= b.szCell[r] + 2; + szRight += szD + 2; + szLeft -= szR + 2; cntNew[i-1] = r; r--; d--; @@ -8955,7 +8965,7 @@ static int balance_nonroot( rc = SQLITE_CORRUPT_BKPT; goto balance_cleanup; } - insertCell(pParent, nxDiv+i, pCell, sz, pTemp, pNew->pgno, &rc); + rc = insertCell(pParent, nxDiv+i, pCell, sz, pTemp, pNew->pgno); if( rc!=SQLITE_OK ) goto balance_cleanup; assert( sqlite3PagerIswriteable(pParent->pDbPage) ); } @@ -9072,7 +9082,7 @@ static int balance_nonroot( } #if 0 - if( ISAUTOVACUUM && rc==SQLITE_OK && apNew[0]->isInit ){ + if( ISAUTOVACUUM(pBt) && rc==SQLITE_OK && apNew[0]->isInit ){ /* The ptrmapCheckPages() contains assert() statements that verify that ** all pointer map pages are set correctly. This is helpful while ** debugging. This is usually disabled because a corrupt database may @@ -9466,7 +9476,6 @@ int sqlite3BtreeInsert( int idx; MemPage *pPage; Btree *p = pCur->pBtree; - BtShared *pBt = p->pBt; unsigned char *oldCell; unsigned char *newCell = 0; @@ -9485,7 +9494,7 @@ int sqlite3BtreeInsert( ** not to clear the cursor here. */ if( pCur->curFlags & BTCF_Multiple ){ - rc = saveAllCursors(pBt, pCur->pgnoRoot, pCur); + rc = saveAllCursors(p->pBt, pCur->pgnoRoot, pCur); if( rc ) return rc; if( loc && pCur->iPage<0 ){ /* This can only happen if the schema is corrupt such that there is more @@ -9509,8 +9518,8 @@ int sqlite3BtreeInsert( assert( cursorOwnsBtShared(pCur) ); assert( (pCur->curFlags & BTCF_WriteFlag)!=0 - && pBt->inTransaction==TRANS_WRITE - && (pBt->btsFlags & BTS_READ_ONLY)==0 ); + && p->pBt->inTransaction==TRANS_WRITE + && (p->pBt->btsFlags & BTS_READ_ONLY)==0 ); assert( hasSharedCacheTableLock(p, pCur->pgnoRoot, pCur->pKeyInfo!=0, 2) ); /* Assert that the caller has been consistent. If this cursor was opened @@ -9627,29 +9636,32 @@ int sqlite3BtreeInsert( pCur->pgnoRoot, pX->nKey, pX->nData, pPage->pgno, loc==0 ? "overwrite" : "new entry")); assert( pPage->isInit || CORRUPT_DB ); - newCell = pBt->pTmpSpace; + newCell = p->pBt->pTmpSpace; assert( newCell!=0 ); + assert( BTREE_PREFORMAT==OPFLAG_PREFORMAT ); if( flags & BTREE_PREFORMAT ){ rc = SQLITE_OK; - szNew = pBt->nPreformatSize; + szNew = p->pBt->nPreformatSize; if( szNew<4 ) szNew = 4; - if( ISAUTOVACUUM && szNew>pPage->maxLocal ){ + if( ISAUTOVACUUM(p->pBt) && szNew>pPage->maxLocal ){ CellInfo info; pPage->xParseCell(pPage, newCell, &info); if( info.nPayload!=info.nLocal ){ Pgno ovfl = get4byte(&newCell[szNew-4]); - ptrmapPut(pBt, ovfl, PTRMAP_OVERFLOW1, pPage->pgno, &rc); + ptrmapPut(p->pBt, ovfl, PTRMAP_OVERFLOW1, pPage->pgno, &rc); + if( NEVER(rc) ) goto end_insert; } } }else{ rc = fillInCell(pPage, newCell, pX, &szNew); + if( rc ) goto end_insert; } - if( rc ) goto end_insert; assert( szNew==pPage->xCellSize(pPage, newCell) ); - assert( szNew <= MX_CELL_SIZE(pBt) ); + assert( szNew <= MX_CELL_SIZE(p->pBt) ); idx = pCur->ix; if( loc==0 ){ CellInfo info; + BtShared *pBt = p->pBt; assert( idx>=0 ); if( idx>=pPage->nCell ){ return SQLITE_CORRUPT_BKPT; @@ -9696,7 +9708,7 @@ int sqlite3BtreeInsert( }else{ assert( pPage->leaf ); } - insertCell(pPage, idx, newCell, szNew, 0, 0, &rc); + rc = insertCell(pPage, idx, newCell, szNew, 0, 0); assert( pPage->nOverflow==0 || rc==SQLITE_OK ); assert( rc!=SQLITE_OK || pPage->nCell>0 || pPage->nOverflow>0 ); @@ -9769,7 +9781,6 @@ end_insert: ** SQLITE_OK is returned if successful, or an SQLite error code otherwise. */ int sqlite3BtreeTransferRow(BtCursor *pDest, BtCursor *pSrc, i64 iKey){ - int rc = SQLITE_OK; BtShared *pBt = pDest->pBt; u8 *aOut = pBt->pTmpSpace; /* Pointer to next output buffer */ const u8 *aIn; /* Pointer to next input buffer */ @@ -9792,7 +9803,9 @@ int sqlite3BtreeTransferRow(BtCursor *pDest, BtCursor *pSrc, i64 iKey){ if( nIn==nRem && nInpPage->maxLocal ){ memcpy(aOut, aIn, nIn); pBt->nPreformatSize = nIn + (aOut - pBt->pTmpSpace); + return SQLITE_OK; }else{ + int rc = SQLITE_OK; Pager *pSrcPager = pSrc->pBt->pPager; u8 *pPgnoOut = 0; Pgno ovflIn = 0; @@ -9844,7 +9857,7 @@ int sqlite3BtreeTransferRow(BtCursor *pDest, BtCursor *pSrc, i64 iKey){ MemPage *pNew = 0; rc = allocateBtreePage(pBt, &pNew, &pgnoNew, 0, 0); put4byte(pPgnoOut, pgnoNew); - if( ISAUTOVACUUM && pPageOut ){ + if( ISAUTOVACUUM(pBt) && pPageOut ){ ptrmapPut(pBt, pgnoNew, PTRMAP_OVERFLOW2, pPageOut->pgno, &rc); } releasePage(pPageOut); @@ -9860,9 +9873,8 @@ int sqlite3BtreeTransferRow(BtCursor *pDest, BtCursor *pSrc, i64 iKey){ releasePage(pPageOut); sqlite3PagerUnref(pPageIn); + return rc; } - - return rc; } /* @@ -10017,7 +10029,7 @@ int sqlite3BtreeDelete(BtCursor *pCur, u8 flags){ assert( pTmp!=0 ); rc = sqlite3PagerWrite(pLeaf->pDbPage); if( rc==SQLITE_OK ){ - insertCell(pPage, iCellIdx, pCell-4, nCell+4, pTmp, n, &rc); + rc = insertCell(pPage, iCellIdx, pCell-4, nCell+4, pTmp, n); } dropCell(pLeaf, pLeaf->nCell-1, nCell, &rc); if( rc ) return rc; @@ -11643,6 +11655,17 @@ int sqlite3BtreeExclusiveLock(Btree *p){ return rc; } +/* +** If no transaction is active and the database is not a temp-db, clear +** the in-memory pager cache. +*/ +void sqlite3BtreeClearCache(Btree *p){ + BtShared *pBt = p->pBt; + if( pBt->inTransaction==TRANS_NONE ){ + sqlite3PagerClearCache(pBt->pPager); + } +} + #if !defined(SQLITE_OMIT_SHARED_CACHE) /* ** Return true if the Btree passed as the only argument is sharable. diff --git a/src/btree.h b/src/btree.h index f736d523e7..ef62f1341c 100644 --- a/src/btree.h +++ b/src/btree.h @@ -183,7 +183,7 @@ int sqlite3BtreeNewDb(Btree *p); ** reduce network bandwidth. ** ** Note that BTREE_HINT_FLAGS with BTREE_BULKLOAD is the only hint used by -** standard SQLite. The other hints are provided for extentions that use +** standard SQLite. The other hints are provided for extensions that use ** the SQLite parser and code generator but substitute their own storage ** engine. */ @@ -370,6 +370,8 @@ void sqlite3BtreeCursorList(Btree*); int sqlite3BtreeTransferRow(BtCursor*, BtCursor*, i64); +void sqlite3BtreeClearCache(Btree*); + /* ** If we are not using shared cache, then there is no need to ** use mutexes to access the BtShared structures. So make the diff --git a/src/btreeInt.h b/src/btreeInt.h index 0782fe1c5b..de9f47e470 100644 --- a/src/btreeInt.h +++ b/src/btreeInt.h @@ -681,9 +681,9 @@ struct BtCursor { ** So, this macro is defined instead. */ #ifdef SQLITE_OMIT_AUTOVACUUM -#define ISAUTOVACUUM 0 +#define ISAUTOVACUUM(pBt) 0 #else -#define ISAUTOVACUUM (pBt->autoVacuum) +#define ISAUTOVACUUM(pBt) (pBt->autoVacuum) #endif #ifdef SQLITE_OMIT_CONCURRENT @@ -692,7 +692,7 @@ struct BtCursor { # define ISCONCURRENT (pBt->pMap!=0) #endif -#define REQUIRE_PTRMAP (ISAUTOVACUUM || ISCONCURRENT) +#define REQUIRE_PTRMAP (ISAUTOVACUUM(pBt) || ISCONCURRENT) /* ** This structure is passed around through all the sanity checking routines diff --git a/src/build.c b/src/build.c index 9e6d55c043..131e934d53 100644 --- a/src/build.c +++ b/src/build.c @@ -2006,6 +2006,13 @@ void sqlite3AddGenerated(Parse *pParse, Expr *pExpr, Token *pType){ if( pCol->colFlags & COLFLAG_PRIMKEY ){ makeColumnPartOfPrimaryKey(pParse, pCol); /* For the error message */ } + if( ALWAYS(pExpr) && pExpr->op==TK_ID ){ + /* The value of a generated column needs to be a real expression, not + ** just a reference to another column, in order for covering index + ** optimizations to work correctly. So if the value is not an expression, + ** turn it into one by adding a unary "+" operator. */ + pExpr = sqlite3PExpr(pParse, TK_UPLUS, pExpr, 0); + } sqlite3ColumnSetExpr(pParse, pTab, pCol, pExpr); pExpr = 0; goto generated_done; @@ -2142,7 +2149,8 @@ static char *createTableStmt(sqlite3 *db, Table *p){ /* SQLITE_AFF_TEXT */ " TEXT", /* SQLITE_AFF_NUMERIC */ " NUM", /* SQLITE_AFF_INTEGER */ " INT", - /* SQLITE_AFF_REAL */ " REAL" + /* SQLITE_AFF_REAL */ " REAL", + /* SQLITE_AFF_FLEXNUM */ " NUM", }; int len; const char *zType; @@ -2158,10 +2166,12 @@ static char *createTableStmt(sqlite3 *db, Table *p){ testcase( pCol->affinity==SQLITE_AFF_NUMERIC ); testcase( pCol->affinity==SQLITE_AFF_INTEGER ); testcase( pCol->affinity==SQLITE_AFF_REAL ); + testcase( pCol->affinity==SQLITE_AFF_FLEXNUM ); zType = azType[pCol->affinity - SQLITE_AFF_BLOB]; len = sqlite3Strlen30(zType); - assert( pCol->affinity==SQLITE_AFF_BLOB + assert( pCol->affinity==SQLITE_AFF_BLOB + || pCol->affinity==SQLITE_AFF_FLEXNUM || pCol->affinity==sqlite3AffinityType(zType, 0) ); memcpy(&zStmt[k], zType, len); k += len; @@ -3142,8 +3152,7 @@ static SQLITE_NOINLINE int viewGetColumnNames(Parse *pParse, Table *pTable){ && pTable->nCol==pSel->pEList->nExpr ){ assert( db->mallocFailed==0 ); - sqlite3SelectAddColumnTypeAndCollation(pParse, pTable, pSel, - SQLITE_AFF_NONE); + sqlite3SubqueryColumnTypes(pParse, pTable, pSel, SQLITE_AFF_NONE); } }else{ /* CREATE VIEW name AS... without an argument list. Construct diff --git a/src/dbstat.c b/src/dbstat.c index bb88a76f40..fdef913f88 100644 --- a/src/dbstat.c +++ b/src/dbstat.c @@ -824,16 +824,16 @@ static int statColumn( } break; case 4: /* ncell */ - sqlite3_result_int(ctx, pCsr->nCell); + sqlite3_result_int64(ctx, pCsr->nCell); break; case 5: /* payload */ - sqlite3_result_int(ctx, pCsr->nPayload); + sqlite3_result_int64(ctx, pCsr->nPayload); break; case 6: /* unused */ - sqlite3_result_int(ctx, pCsr->nUnused); + sqlite3_result_int64(ctx, pCsr->nUnused); break; case 7: /* mx_payload */ - sqlite3_result_int(ctx, pCsr->nMxPayload); + sqlite3_result_int64(ctx, pCsr->nMxPayload); break; case 8: /* pgoffset */ if( !pCsr->isAgg ){ @@ -841,7 +841,7 @@ static int statColumn( } break; case 9: /* pgsize */ - sqlite3_result_int(ctx, pCsr->szPage); + sqlite3_result_int64(ctx, pCsr->szPage); break; case 10: { /* schema */ sqlite3 *db = sqlite3_context_db_handle(ctx); diff --git a/src/expr.c b/src/expr.c index 7a4e59f28d..271d9dea80 100644 --- a/src/expr.c +++ b/src/expr.c @@ -44,48 +44,121 @@ char sqlite3TableColumnAffinity(const Table *pTab, int iCol){ */ char sqlite3ExprAffinity(const Expr *pExpr){ int op; - while( ExprHasProperty(pExpr, EP_Skip|EP_IfNullRow) ){ - assert( pExpr->op==TK_COLLATE - || pExpr->op==TK_IF_NULL_ROW - || (pExpr->op==TK_REGISTER && pExpr->op2==TK_IF_NULL_ROW) ); - pExpr = pExpr->pLeft; - assert( pExpr!=0 ); - } op = pExpr->op; - if( op==TK_REGISTER ) op = pExpr->op2; - if( op==TK_COLUMN || op==TK_AGG_COLUMN ){ - assert( ExprUseYTab(pExpr) ); - assert( pExpr->y.pTab!=0 ); - return sqlite3TableColumnAffinity(pExpr->y.pTab, pExpr->iColumn); - } - if( op==TK_SELECT ){ - assert( ExprUseXSelect(pExpr) ); - assert( pExpr->x.pSelect!=0 ); - assert( pExpr->x.pSelect->pEList!=0 ); - assert( pExpr->x.pSelect->pEList->a[0].pExpr!=0 ); - return sqlite3ExprAffinity(pExpr->x.pSelect->pEList->a[0].pExpr); - } + while( 1 /* exit-by-break */ ){ + if( op==TK_COLUMN || (op==TK_AGG_COLUMN && pExpr->y.pTab!=0) ){ + assert( ExprUseYTab(pExpr) ); + assert( pExpr->y.pTab!=0 ); + return sqlite3TableColumnAffinity(pExpr->y.pTab, pExpr->iColumn); + } + if( op==TK_SELECT ){ + assert( ExprUseXSelect(pExpr) ); + assert( pExpr->x.pSelect!=0 ); + assert( pExpr->x.pSelect->pEList!=0 ); + assert( pExpr->x.pSelect->pEList->a[0].pExpr!=0 ); + return sqlite3ExprAffinity(pExpr->x.pSelect->pEList->a[0].pExpr); + } #ifndef SQLITE_OMIT_CAST - if( op==TK_CAST ){ - assert( !ExprHasProperty(pExpr, EP_IntValue) ); - return sqlite3AffinityType(pExpr->u.zToken, 0); - } + if( op==TK_CAST ){ + assert( !ExprHasProperty(pExpr, EP_IntValue) ); + return sqlite3AffinityType(pExpr->u.zToken, 0); + } #endif - if( op==TK_SELECT_COLUMN ){ - assert( pExpr->pLeft!=0 && ExprUseXSelect(pExpr->pLeft) ); - assert( pExpr->iColumn < pExpr->iTable ); - assert( pExpr->iTable==pExpr->pLeft->x.pSelect->pEList->nExpr ); - return sqlite3ExprAffinity( - pExpr->pLeft->x.pSelect->pEList->a[pExpr->iColumn].pExpr - ); - } - if( op==TK_VECTOR ){ - assert( ExprUseXList(pExpr) ); - return sqlite3ExprAffinity(pExpr->x.pList->a[0].pExpr); + if( op==TK_SELECT_COLUMN ){ + assert( pExpr->pLeft!=0 && ExprUseXSelect(pExpr->pLeft) ); + assert( pExpr->iColumn < pExpr->iTable ); + assert( pExpr->iTable==pExpr->pLeft->x.pSelect->pEList->nExpr ); + return sqlite3ExprAffinity( + pExpr->pLeft->x.pSelect->pEList->a[pExpr->iColumn].pExpr + ); + } + if( op==TK_VECTOR ){ + assert( ExprUseXList(pExpr) ); + return sqlite3ExprAffinity(pExpr->x.pList->a[0].pExpr); + } + if( ExprHasProperty(pExpr, EP_Skip|EP_IfNullRow) ){ + assert( pExpr->op==TK_COLLATE + || pExpr->op==TK_IF_NULL_ROW + || (pExpr->op==TK_REGISTER && pExpr->op2==TK_IF_NULL_ROW) ); + pExpr = pExpr->pLeft; + op = pExpr->op; + continue; + } + if( op!=TK_REGISTER || (op = pExpr->op2)==TK_REGISTER ) break; } return pExpr->affExpr; } +/* +** Make a guess at all the possible datatypes of the result that could +** be returned by an expression. Return a bitmask indicating the answer: +** +** 0x01 Numeric +** 0x02 Text +** 0x04 Blob +** +** If the expression must return NULL, then 0x00 is returned. +*/ +int sqlite3ExprDataType(const Expr *pExpr){ + while( pExpr ){ + switch( pExpr->op ){ + case TK_COLLATE: + case TK_IF_NULL_ROW: + case TK_UPLUS: { + pExpr = pExpr->pLeft; + break; + } + case TK_NULL: { + pExpr = 0; + break; + } + case TK_STRING: { + return 0x02; + } + case TK_BLOB: { + return 0x04; + } + case TK_CONCAT: { + return 0x06; + } + case TK_VARIABLE: + case TK_AGG_FUNCTION: + case TK_FUNCTION: { + return 0x07; + } + case TK_COLUMN: + case TK_AGG_COLUMN: + case TK_SELECT: + case TK_CAST: + case TK_SELECT_COLUMN: + case TK_VECTOR: { + int aff = sqlite3ExprAffinity(pExpr); + if( aff>=SQLITE_AFF_NUMERIC ) return 0x05; + if( aff==SQLITE_AFF_TEXT ) return 0x06; + return 0x07; + } + case TK_CASE: { + int res = 0; + int ii; + ExprList *pList = pExpr->x.pList; + assert( ExprUseXList(pExpr) && pList!=0 ); + assert( pList->nExpr > 0); + for(ii=1; iinExpr; ii+=2){ + res |= sqlite3ExprDataType(pList->a[ii].pExpr); + } + if( pList->nExpr % 2 ){ + res |= sqlite3ExprDataType(pList->a[pList->nExpr-1].pExpr); + } + return res; + } + default: { + return 0x01; + } + } /* End of switch(op) */ + } /* End of while(pExpr) */ + return 0x00; +} + /* ** Set the collating sequence for expression pExpr to be the collating ** sequence named by pToken. Return a pointer to a new Expr node that @@ -173,7 +246,9 @@ CollSeq *sqlite3ExprCollSeq(Parse *pParse, const Expr *pExpr){ while( p ){ int op = p->op; if( op==TK_REGISTER ) op = p->op2; - if( op==TK_AGG_COLUMN || op==TK_COLUMN || op==TK_TRIGGER ){ + if( (op==TK_AGG_COLUMN && p->y.pTab!=0) + || op==TK_COLUMN || op==TK_TRIGGER + ){ int j; assert( ExprUseYTab(p) ); assert( p->y.pTab!=0 ); @@ -3255,6 +3330,9 @@ int sqlite3CodeSubselect(Parse *pParse, Expr *pExpr){ SelectDest dest; /* How to deal with SELECT result */ int nReg; /* Registers to allocate */ Expr *pLimit; /* New limit expression */ +#ifdef SQLITE_ENABLE_STMT_SCANSTATUS + int addrExplain; /* Address of OP_Explain instruction */ +#endif Vdbe *v = pParse->pVdbe; assert( v!=0 ); @@ -3307,8 +3385,9 @@ int sqlite3CodeSubselect(Parse *pParse, Expr *pExpr){ ** In both cases, the query is augmented with "LIMIT 1". Any ** preexisting limit is discarded in place of the new LIMIT 1. */ - ExplainQueryPlan((pParse, 1, "%sSCALAR SUBQUERY %d", + ExplainQueryPlan2(addrExplain, (pParse, 1, "%sSCALAR SUBQUERY %d", addrOnce?"":"CORRELATED ", pSel->selId)); + sqlite3VdbeScanStatusCounters(v, addrExplain, addrExplain, -1); nReg = pExpr->op==TK_SELECT ? pSel->pEList->nExpr : 1; sqlite3SelectDestInit(&dest, 0, pParse->nMem+1); pParse->nMem += nReg; @@ -3351,6 +3430,7 @@ int sqlite3CodeSubselect(Parse *pParse, Expr *pExpr){ if( addrOnce ){ sqlite3VdbeJumpHere(v, addrOnce); } + sqlite3VdbeScanStatusRange(v, addrExplain, addrExplain, -1); /* Subroutine return */ assert( ExprUseYSub(pExpr) ); @@ -4037,7 +4117,7 @@ static int exprCodeInlineFunction( } /* -** Check to see if pExpr is one of the indexed expressions on pParse->pIdxExpr. +** Check to see if pExpr is one of the indexed expressions on pParse->pIdxEpr. ** If it is, then resolve the expression by reading from the index and ** return the register into which the value has been read. If pExpr is ** not an indexed expression, then return negative. @@ -4049,7 +4129,7 @@ static SQLITE_NOINLINE int sqlite3IndexedExprLookup( ){ IndexedExpr *p; Vdbe *v; - for(p=pParse->pIdxExpr; p; p=p->pIENext){ + for(p=pParse->pIdxEpr; p; p=p->pIENext){ int iDataCur = p->iDataCur; if( iDataCur<0 ) continue; if( pParse->iSelfTab ){ @@ -4069,10 +4149,10 @@ static SQLITE_NOINLINE int sqlite3IndexedExprLookup( sqlite3VdbeAddOp3(v, OP_Column, p->iIdxCur, p->iIdxCol, target); VdbeComment((v, "%s expr-column %d", p->zIdxName, p->iIdxCol)); sqlite3VdbeGoto(v, 0); - p = pParse->pIdxExpr; - pParse->pIdxExpr = 0; + p = pParse->pIdxEpr; + pParse->pIdxEpr = 0; sqlite3ExprCode(pParse, pExpr, target); - pParse->pIdxExpr = p; + pParse->pIdxEpr = p; sqlite3VdbeJumpHere(v, addr+2); }else{ sqlite3VdbeAddOp3(v, OP_Column, p->iIdxCur, p->iIdxCol, target); @@ -4111,7 +4191,7 @@ int sqlite3ExprCodeTarget(Parse *pParse, Expr *pExpr, int target){ expr_code_doover: if( pExpr==0 ){ op = TK_NULL; - }else if( pParse->pIdxExpr!=0 + }else if( pParse->pIdxEpr!=0 && !ExprHasProperty(pExpr, EP_Leaf) && (r1 = sqlite3IndexedExprLookup(pParse, pExpr, target))>=0 ){ @@ -4128,15 +4208,16 @@ expr_code_doover: assert( pExpr->iAgg>=0 && pExpr->iAggnColumn ); pCol = &pAggInfo->aCol[pExpr->iAgg]; if( !pAggInfo->directMode ){ - assert( pCol->iMem>0 ); - return pCol->iMem; + return AggInfoColumnReg(pAggInfo, pExpr->iAgg); }else if( pAggInfo->useSortingIdx ){ Table *pTab = pCol->pTab; sqlite3VdbeAddOp3(v, OP_Column, pAggInfo->sortingIdxPTab, pCol->iSorterColumn, target); - if( pCol->iColumn<0 ){ + if( pTab==0 ){ + /* No comment added */ + }else if( pCol->iColumn<0 ){ VdbeComment((v,"%s.rowid",pTab->zName)); - }else if( ALWAYS(pTab!=0) ){ + }else{ VdbeComment((v,"%s.%s", pTab->zName, pTab->aCol[pCol->iColumn].zCnName)); if( pTab->aCol[pCol->iColumn].affinity==SQLITE_AFF_REAL ){ @@ -4144,6 +4225,11 @@ expr_code_doover: } } return target; + }else if( pExpr->y.pTab==0 ){ + /* This case happens when the argument to an aggregate function + ** is rewritten by aggregateConvertIndexedExprRefToColumn() */ + sqlite3VdbeAddOp3(v, OP_Column, pExpr->iTable, pExpr->iColumn, target); + return target; } /* Otherwise, fall thru into the TK_COLUMN case */ /* no break */ deliberate_fall_through @@ -4164,7 +4250,7 @@ expr_code_doover: assert( pExpr->y.pTab!=0 ); aff = sqlite3TableColumnAffinity(pExpr->y.pTab, pExpr->iColumn); if( aff>SQLITE_AFF_BLOB ){ - static const char zAff[] = "B\000C\000D\000E"; + static const char zAff[] = "B\000C\000D\000E\000F"; assert( SQLITE_AFF_BLOB=='A' ); assert( SQLITE_AFF_TEXT=='B' ); sqlite3VdbeAddOp4(v, OP_Affinity, iReg, 1, 0, @@ -4441,7 +4527,7 @@ expr_code_doover: assert( !ExprHasProperty(pExpr, EP_IntValue) ); sqlite3ErrorMsg(pParse, "misuse of aggregate: %#T()", pExpr); }else{ - return pInfo->aFunc[pExpr->iAgg].iMem; + return AggInfoFuncReg(pInfo, pExpr->iAgg); } break; } @@ -4730,7 +4816,7 @@ expr_code_doover: if( pAggInfo ){ assert( pExpr->iAgg>=0 && pExpr->iAggnColumn ); if( !pAggInfo->directMode ){ - inReg = pAggInfo->aCol[pExpr->iAgg].iMem; + inReg = AggInfoColumnReg(pAggInfo, pExpr->iAgg); break; } if( pExpr->pAggInfo->useSortingIdx ){ @@ -6161,10 +6247,8 @@ int sqlite3ReferencesSrcList(Parse *pParse, Expr *pExpr, SrcList *pSrcList){ ** it does, make a copy. This is done because the pExpr argument is ** subject to change. ** -** The copy is stored on pParse->pConstExpr with a register number of 0. -** This will cause the expression to be deleted automatically when the -** Parse object is destroyed, but the zero register number means that it -** will not generate any code in the preamble. +** The copy is scheduled for deletion using the sqlite3ExprDeferredDelete() +** which builds on the sqlite3ParserAddCleanup() mechanism. */ static int agginfoPersistExprCb(Walker *pWalker, Expr *pExpr){ if( ALWAYS(!ExprHasProperty(pExpr, EP_TokenOnly|EP_Reduced)) @@ -6175,7 +6259,6 @@ static int agginfoPersistExprCb(Walker *pWalker, Expr *pExpr){ Parse *pParse = pWalker->pParse; sqlite3 *db = pParse->db; if( pExpr->op!=TK_AGG_FUNCTION ){ - assert( pExpr->op==TK_AGG_COLUMN || pExpr->op==TK_IF_NULL_ROW ); assert( iAgg>=0 && iAggnColumn ); if( pAggInfo->aCol[iAgg].pCExpr==pExpr ){ pExpr = sqlite3ExprDup(db, pExpr, 0); @@ -6242,6 +6325,73 @@ static int addAggInfoFunc(sqlite3 *db, AggInfo *pInfo){ return i; } +/* +** Search the AggInfo object for an aCol[] entry that has iTable and iColumn. +** Return the index in aCol[] of the entry that describes that column. +** +** If no prior entry is found, create a new one and return -1. The +** new column will have an idex of pAggInfo->nColumn-1. +*/ +static void findOrCreateAggInfoColumn( + Parse *pParse, /* Parsing context */ + AggInfo *pAggInfo, /* The AggInfo object to search and/or modify */ + Expr *pExpr /* Expr describing the column to find or insert */ +){ + struct AggInfo_col *pCol; + int k; + + assert( pAggInfo->iFirstReg==0 ); + pCol = pAggInfo->aCol; + for(k=0; knColumn; k++, pCol++){ + if( pCol->iTable==pExpr->iTable + && pCol->iColumn==pExpr->iColumn + && pExpr->op!=TK_IF_NULL_ROW + ){ + goto fix_up_expr; + } + } + k = addAggInfoColumn(pParse->db, pAggInfo); + if( k<0 ){ + /* OOM on resize */ + assert( pParse->db->mallocFailed ); + return; + } + pCol = &pAggInfo->aCol[k]; + assert( ExprUseYTab(pExpr) ); + pCol->pTab = pExpr->y.pTab; + pCol->iTable = pExpr->iTable; + pCol->iColumn = pExpr->iColumn; + pCol->iSorterColumn = -1; + pCol->pCExpr = pExpr; + if( pAggInfo->pGroupBy && pExpr->op!=TK_IF_NULL_ROW ){ + int j, n; + ExprList *pGB = pAggInfo->pGroupBy; + struct ExprList_item *pTerm = pGB->a; + n = pGB->nExpr; + for(j=0; jpExpr; + if( pE->op==TK_COLUMN + && pE->iTable==pExpr->iTable + && pE->iColumn==pExpr->iColumn + ){ + pCol->iSorterColumn = j; + break; + } + } + } + if( pCol->iSorterColumn<0 ){ + pCol->iSorterColumn = pAggInfo->nSortingColumn++; + } +fix_up_expr: + ExprSetVVAProperty(pExpr, EP_NoReduce); + assert( pExpr->pAggInfo==0 || pExpr->pAggInfo==pAggInfo ); + pExpr->pAggInfo = pAggInfo; + if( pExpr->op==TK_COLUMN ){ + pExpr->op = TK_AGG_COLUMN; + } + pExpr->iAgg = (i16)k; +} + /* ** This is the xExprCallback for a tree walker. It is used to ** implement sqlite3ExprAnalyzeAggregates(). See sqlite3ExprAnalyzeAggregates @@ -6255,7 +6405,37 @@ static int analyzeAggregate(Walker *pWalker, Expr *pExpr){ AggInfo *pAggInfo = pNC->uNC.pAggInfo; assert( pNC->ncFlags & NC_UAggInfo ); + assert( pAggInfo->iFirstReg==0 ); switch( pExpr->op ){ + default: { + IndexedExpr *pIEpr; + Expr tmp; + assert( pParse->iSelfTab==0 ); + if( (pNC->ncFlags & NC_InAggFunc)==0 ) break; + if( pParse->pIdxEpr==0 ) break; + for(pIEpr=pParse->pIdxEpr; pIEpr; pIEpr=pIEpr->pIENext){ + int iDataCur = pIEpr->iDataCur; + if( iDataCur<0 ) continue; + if( sqlite3ExprCompare(0, pExpr, pIEpr->pExpr, iDataCur)==0 ) break; + } + if( pIEpr==0 ) break; + if( NEVER(!ExprUseYTab(pExpr)) ) break; + if( pExpr->pAggInfo!=0 ) break; /* Already resolved by outer context */ + + /* If we reach this point, it means that expression pExpr can be + ** translated into a reference to an index column as described by + ** pIEpr. + */ + memset(&tmp, 0, sizeof(tmp)); + tmp.op = TK_AGG_COLUMN; + tmp.iTable = pIEpr->iIdxCur; + tmp.iColumn = pIEpr->iIdxCol; + findOrCreateAggInfoColumn(pParse, pAggInfo, &tmp); + pAggInfo->aCol[tmp.iAgg].pCExpr = pExpr; + pExpr->pAggInfo = pAggInfo; + pExpr->iAgg = tmp.iAgg; + return WRC_Prune; + } case TK_IF_NULL_ROW: case TK_AGG_COLUMN: case TK_COLUMN: { @@ -6267,67 +6447,9 @@ static int analyzeAggregate(Walker *pWalker, Expr *pExpr){ if( ALWAYS(pSrcList!=0) ){ SrcItem *pItem = pSrcList->a; for(i=0; inSrc; i++, pItem++){ - struct AggInfo_col *pCol; assert( !ExprHasProperty(pExpr, EP_TokenOnly|EP_Reduced) ); if( pExpr->iTable==pItem->iCursor ){ - /* If we reach this point, it means that pExpr refers to a table - ** that is in the FROM clause of the aggregate query. - ** - ** Make an entry for the column in pAggInfo->aCol[] if there - ** is not an entry there already. - */ - int k; - pCol = pAggInfo->aCol; - for(k=0; knColumn; k++, pCol++){ - if( pCol->iTable==pExpr->iTable - && pCol->iColumn==pExpr->iColumn - && pExpr->op!=TK_IF_NULL_ROW - ){ - break; - } - } - if( (k>=pAggInfo->nColumn) - && (k = addAggInfoColumn(pParse->db, pAggInfo))>=0 - ){ - pCol = &pAggInfo->aCol[k]; - assert( ExprUseYTab(pExpr) ); - pCol->pTab = pExpr->y.pTab; - pCol->iTable = pExpr->iTable; - pCol->iColumn = pExpr->iColumn; - pCol->iMem = ++pParse->nMem; - pCol->iSorterColumn = -1; - pCol->pCExpr = pExpr; - if( pAggInfo->pGroupBy && pExpr->op!=TK_IF_NULL_ROW ){ - int j, n; - ExprList *pGB = pAggInfo->pGroupBy; - struct ExprList_item *pTerm = pGB->a; - n = pGB->nExpr; - for(j=0; jpExpr; - if( pE->op==TK_COLUMN - && pE->iTable==pExpr->iTable - && pE->iColumn==pExpr->iColumn - ){ - pCol->iSorterColumn = j; - break; - } - } - } - if( pCol->iSorterColumn<0 ){ - pCol->iSorterColumn = pAggInfo->nSortingColumn++; - } - } - /* There is now an entry for pExpr in pAggInfo->aCol[] (either - ** because it was there before or because we just created it). - ** Convert the pExpr to be a TK_AGG_COLUMN referring to that - ** pAggInfo->aCol[] entry. - */ - ExprSetVVAProperty(pExpr, EP_NoReduce); - pExpr->pAggInfo = pAggInfo; - if( pExpr->op==TK_COLUMN ){ - pExpr->op = TK_AGG_COLUMN; - } - pExpr->iAgg = (i16)k; + findOrCreateAggInfoColumn(pParse, pAggInfo, pExpr); break; } /* endif pExpr->iTable==pItem->iCursor */ } /* end loop over pSrcList */ @@ -6357,7 +6479,6 @@ static int analyzeAggregate(Walker *pWalker, Expr *pExpr){ assert( !ExprHasProperty(pExpr, EP_xIsSelect) ); pItem = &pAggInfo->aFunc[i]; pItem->pFExpr = pExpr; - pItem->iMem = ++pParse->nMem; assert( ExprUseUToken(pExpr) ); pItem->pFunc = sqlite3FindFunction(pParse->db, pExpr->u.zToken, diff --git a/src/func.c b/src/func.c index 4f04c29ffb..35fe175d66 100644 --- a/src/func.c +++ b/src/func.c @@ -1084,7 +1084,7 @@ void sqlite3QuoteValue(StrAccum *pStr, sqlite3_value *pValue){ } case SQLITE_BLOB: { char const *zBlob = sqlite3_value_blob(pValue); - int nBlob = sqlite3_value_bytes(pValue); + i64 nBlob = sqlite3_value_bytes(pValue); assert( zBlob==sqlite3_value_blob(pValue) ); /* No encoding change */ sqlite3StrAccumEnlarge(pStr, nBlob*2 + 4); if( pStr->accError==0 ){ @@ -2108,17 +2108,15 @@ static void logFunc( } ans = log(x)/b; }else{ - ans = log(x); switch( SQLITE_PTR_TO_INT(sqlite3_user_data(context)) ){ case 1: - /* Convert from natural logarithm to log base 10 */ - ans /= M_LN10; + ans = log10(x); break; case 2: - /* Convert from natural logarithm to log base 2 */ - ans /= M_LN2; + ans = log2(x); break; default: + ans = log(x); break; } } diff --git a/src/hwtime.h b/src/hwtime.h index 037c55a977..d27204a69c 100644 --- a/src/hwtime.h +++ b/src/hwtime.h @@ -48,9 +48,9 @@ #elif !defined(__STRICT_ANSI__) && (defined(__GNUC__) && defined(__x86_64__)) __inline__ sqlite_uint64 sqlite3Hwtime(void){ - unsigned long val; - __asm__ __volatile__ ("rdtsc" : "=A" (val)); - return val; + unsigned int lo, hi; + __asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi)); + return (sqlite_uint64)hi << 32 | lo; } #elif !defined(__STRICT_ANSI__) && (defined(__GNUC__) && defined(__ppc__)) diff --git a/src/insert.c b/src/insert.c index 1f6cdffcc3..65d11bee54 100644 --- a/src/insert.c +++ b/src/insert.c @@ -1793,6 +1793,7 @@ void sqlite3GenerateConstraintChecks( case OE_Fail: { char *zMsg = sqlite3MPrintf(db, "%s.%s", pTab->zName, pCol->zCnName); + testcase( zMsg==0 && db->mallocFailed==0 ); sqlite3VdbeAddOp3(v, OP_HaltIfNull, SQLITE_CONSTRAINT_NOTNULL, onError, iReg); sqlite3VdbeAppendP4(v, zMsg, P4_DYNAMIC); diff --git a/src/loadext.c b/src/loadext.c index 421ccca804..c14338f8b4 100644 --- a/src/loadext.c +++ b/src/loadext.c @@ -510,7 +510,7 @@ static const sqlite3_api_routines sqlite3Apis = { #endif sqlite3_db_name, /* Version 3.40.0 and later */ - sqlite3_value_type + sqlite3_value_encoding }; /* True if x is the directory separator character diff --git a/src/main.c b/src/main.c index a731a0ff3d..1b9ccd304f 100644 --- a/src/main.c +++ b/src/main.c @@ -1566,6 +1566,7 @@ const char *sqlite3ErrName(int rc){ case SQLITE_NOTICE_RECOVER_WAL: zName = "SQLITE_NOTICE_RECOVER_WAL";break; case SQLITE_NOTICE_RECOVER_ROLLBACK: zName = "SQLITE_NOTICE_RECOVER_ROLLBACK"; break; + case SQLITE_NOTICE_RBU: zName = "SQLITE_NOTICE_RBU"; break; case SQLITE_WARNING: zName = "SQLITE_WARNING"; break; case SQLITE_WARNING_AUTOINDEX: zName = "SQLITE_WARNING_AUTOINDEX"; break; case SQLITE_DONE: zName = "SQLITE_DONE"; break; @@ -2118,7 +2119,7 @@ int sqlite3_overload_function( rc = sqlite3FindFunction(db, zName, nArg, SQLITE_UTF8, 0)!=0; sqlite3_mutex_leave(db->mutex); if( rc ) return SQLITE_OK; - zCopy = sqlite3_mprintf(zName); + zCopy = sqlite3_mprintf("%s", zName); if( zCopy==0 ) return SQLITE_NOMEM; return sqlite3_create_function_v2(db, zName, nArg, SQLITE_UTF8, zCopy, sqlite3InvalidFunction, 0, 0, sqlite3_free); @@ -3950,6 +3951,9 @@ int sqlite3_file_control(sqlite3 *db, const char *zDbName, int op, void *pArg){ sqlite3BtreeSetPageSize(pBtree, 0, iNew, 0); } rc = SQLITE_OK; + }else if( op==SQLITE_FCNTL_RESET_CACHE ){ + sqlite3BtreeClearCache(pBtree); + rc = SQLITE_OK; }else{ int nSave = db->busyHandler.nBusy; rc = sqlite3OsFileControl(fd, op, pArg); diff --git a/src/malloc.c b/src/malloc.c index af83743fc0..48c4600606 100644 --- a/src/malloc.c +++ b/src/malloc.c @@ -279,7 +279,7 @@ static void mallocWithAlarm(int n, void **pp){ ** The upper bound is slightly less than 2GiB: 0x7ffffeff == 2,147,483,391 ** This provides a 256-byte safety margin for defense against 32-bit ** signed integer overflow bugs when computing memory allocation sizes. -** Parnoid applications might want to reduce the maximum allocation size +** Paranoid applications might want to reduce the maximum allocation size ** further for an even larger safety margin. 0x3fffffff or 0x0fffffff ** or even smaller would be reasonable upper bounds on the size of a memory ** allocations for most applications. @@ -793,9 +793,14 @@ char *sqlite3DbStrNDup(sqlite3 *db, const char *z, u64 n){ */ char *sqlite3DbSpanDup(sqlite3 *db, const char *zStart, const char *zEnd){ int n; +#ifdef SQLITE_DEBUG + /* Because of the way the parser works, the span is guaranteed to contain + ** at least one non-space character */ + for(n=0; sqlite3Isspace(zStart[n]); n++){ assert( &zStart[n]0) && sqlite3Isspace(zStart[n-1]) ) n--; + while( sqlite3Isspace(zStart[n-1]) ) n--; return sqlite3DbStrNDup(db, zStart, n); } diff --git a/src/memdb.c b/src/memdb.c index 31b2324b93..657cb9ca65 100644 --- a/src/memdb.c +++ b/src/memdb.c @@ -109,6 +109,7 @@ static int memdbTruncate(sqlite3_file*, sqlite3_int64 size); static int memdbSync(sqlite3_file*, int flags); static int memdbFileSize(sqlite3_file*, sqlite3_int64 *pSize); static int memdbLock(sqlite3_file*, int); +static int memdbUnlock(sqlite3_file*, int); /* static int memdbCheckReservedLock(sqlite3_file*, int *pResOut);// not used */ static int memdbFileControl(sqlite3_file*, int op, void *pArg); /* static int memdbSectorSize(sqlite3_file*); // not used */ @@ -167,7 +168,7 @@ static const sqlite3_io_methods memdb_io_methods = { memdbSync, /* xSync */ memdbFileSize, /* xFileSize */ memdbLock, /* xLock */ - memdbLock, /* xUnlock - same as xLock in this case */ + memdbUnlock, /* xUnlock */ 0, /* memdbCheckReservedLock, */ /* xCheckReservedLock */ memdbFileControl, /* xFileControl */ 0, /* memdbSectorSize,*/ /* xSectorSize */ @@ -368,41 +369,83 @@ static int memdbLock(sqlite3_file *pFile, int eLock){ MemFile *pThis = (MemFile*)pFile; MemStore *p = pThis->pStore; int rc = SQLITE_OK; - if( eLock==pThis->eLock ) return SQLITE_OK; + if( eLock<=pThis->eLock ) return SQLITE_OK; memdbEnter(p); - if( eLock>SQLITE_LOCK_SHARED ){ - if( p->mFlags & SQLITE_DESERIALIZE_READONLY ){ - rc = SQLITE_READONLY; - }else if( pThis->eLock<=SQLITE_LOCK_SHARED ){ - if( p->nWrLock ){ - rc = SQLITE_BUSY; - }else{ - p->nWrLock = 1; + + assert( p->nWrLock==0 || p->nWrLock==1 ); + assert( pThis->eLock<=SQLITE_LOCK_SHARED || p->nWrLock==1 ); + assert( pThis->eLock==SQLITE_LOCK_NONE || p->nRdLock>=1 ); + + if( eLock>SQLITE_LOCK_SHARED && (p->mFlags & SQLITE_DESERIALIZE_READONLY) ){ + rc = SQLITE_READONLY; + }else{ + switch( eLock ){ + case SQLITE_LOCK_SHARED: { + assert( pThis->eLock==SQLITE_LOCK_NONE ); + if( p->nWrLock>0 ){ + rc = SQLITE_BUSY; + }else{ + p->nRdLock++; + } + break; + }; + + case SQLITE_LOCK_RESERVED: + case SQLITE_LOCK_PENDING: { + assert( pThis->eLock>=SQLITE_LOCK_SHARED ); + if( ALWAYS(pThis->eLock==SQLITE_LOCK_SHARED) ){ + if( p->nWrLock>0 ){ + rc = SQLITE_BUSY; + }else{ + p->nWrLock = 1; + } + } + break; + } + + default: { + assert( eLock==SQLITE_LOCK_EXCLUSIVE ); + assert( pThis->eLock>=SQLITE_LOCK_SHARED ); + if( p->nRdLock>1 ){ + rc = SQLITE_BUSY; + }else if( pThis->eLock==SQLITE_LOCK_SHARED ){ + p->nWrLock = 1; + } + break; } } - }else if( eLock==SQLITE_LOCK_SHARED ){ - if( pThis->eLock > SQLITE_LOCK_SHARED ){ - assert( p->nWrLock==1 ); - p->nWrLock = 0; - }else if( p->nWrLock ){ - rc = SQLITE_BUSY; - }else{ - p->nRdLock++; - } - }else{ - assert( eLock==SQLITE_LOCK_NONE ); - if( pThis->eLock>SQLITE_LOCK_SHARED ){ - assert( p->nWrLock==1 ); - p->nWrLock = 0; - } - assert( p->nRdLock>0 ); - p->nRdLock--; } if( rc==SQLITE_OK ) pThis->eLock = eLock; memdbLeave(p); return rc; } +/* +** Unlock an memdb-file. +*/ +static int memdbUnlock(sqlite3_file *pFile, int eLock){ + MemFile *pThis = (MemFile*)pFile; + MemStore *p = pThis->pStore; + if( eLock>=pThis->eLock ) return SQLITE_OK; + memdbEnter(p); + + assert( eLock==SQLITE_LOCK_SHARED || eLock==SQLITE_LOCK_NONE ); + if( eLock==SQLITE_LOCK_SHARED ){ + if( ALWAYS(pThis->eLock>SQLITE_LOCK_SHARED) ){ + p->nWrLock--; + } + }else{ + if( pThis->eLock>SQLITE_LOCK_SHARED ){ + p->nWrLock--; + } + p->nRdLock--; + } + + pThis->eLock = eLock; + memdbLeave(p); + return SQLITE_OK; +} + #if 0 /* ** This interface is only used for crash recovery, which does not @@ -510,7 +553,7 @@ static int memdbOpen( memset(pFile, 0, sizeof(*pFile)); szName = sqlite3Strlen30(zName); - if( szName>1 && zName[0]=='/' ){ + if( szName>1 && (zName[0]=='/' || zName[0]=='\\') ){ int i; #ifndef SQLITE_MUTEX_OMIT sqlite3_mutex *pVfsMutex = sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_VFS1); @@ -857,6 +900,13 @@ end_deserialize: return rc; } +/* +** Return true if the VFS is the memvfs. +*/ +int sqlite3IsMemdb(const sqlite3_vfs *pVfs){ + return pVfs==&memdb_vfs; +} + /* ** This routine is called when the extension is loaded. ** Register the new VFS. diff --git a/src/os_common.h b/src/os_common.h index 1ed4d7a8e1..5b532af0ac 100644 --- a/src/os_common.h +++ b/src/os_common.h @@ -35,12 +35,6 @@ */ #ifdef SQLITE_PERFORMANCE_TRACE -/* -** hwtime.h contains inline assembler code for implementing -** high-performance timing routines. -*/ -#include "hwtime.h" - static sqlite_uint64 g_start; static sqlite_uint64 g_elapsed; #define TIMER_START g_start=sqlite3Hwtime() diff --git a/src/os_kv.c b/src/os_kv.c index 322588be9a..45955d18f2 100644 --- a/src/os_kv.c +++ b/src/os_kv.c @@ -52,7 +52,9 @@ struct KVVfsFile { char *aJrnl; /* Journal content */ int szPage; /* Last known page size */ sqlite3_int64 szDb; /* Database file size. -1 means unknown */ + char *aData; /* Buffer to hold page data */ }; +#define SQLITE_KVOS_SZ 133073 /* ** Methods for KVVfsFile @@ -493,6 +495,7 @@ static int kvvfsClose(sqlite3_file *pProtoFile){ SQLITE_KV_LOG(("xClose %s %s\n", pFile->zClass, pFile->isJournal ? "journal" : "db")); sqlite3_free(pFile->aJrnl); + sqlite3_free(pFile->aData); return SQLITE_OK; } @@ -541,7 +544,7 @@ static int kvvfsReadDb( unsigned int pgno; int got, n; char zKey[30]; - char aData[133073]; + char *aData = pFile->aData; assert( iOfst>=0 ); assert( iAmt>=0 ); SQLITE_KV_LOG(("xRead('%s-db',%d,%lld)\n", pFile->zClass, iAmt, iOfst)); @@ -558,7 +561,8 @@ static int kvvfsReadDb( pgno = 1; } sqlite3_snprintf(sizeof(zKey), zKey, "%u", pgno); - got = sqlite3KvvfsMethods.xRead(pFile->zClass, zKey, aData, sizeof(aData)-1); + got = sqlite3KvvfsMethods.xRead(pFile->zClass, zKey, + aData, SQLITE_KVOS_SZ-1); if( got<0 ){ n = 0; }else{ @@ -566,7 +570,7 @@ static int kvvfsReadDb( if( iOfst+iAmt<512 ){ int k = iOfst+iAmt; aData[k*2] = 0; - n = kvvfsDecode(aData, &aData[2000], sizeof(aData)-2000); + n = kvvfsDecode(aData, &aData[2000], SQLITE_KVOS_SZ-2000); if( n>=iOfst+iAmt ){ memcpy(zBuf, &aData[2000+iOfst], iAmt); n = iAmt; @@ -625,7 +629,7 @@ static int kvvfsWriteDb( KVVfsFile *pFile = (KVVfsFile*)pProtoFile; unsigned int pgno; char zKey[30]; - char aData[131073]; + char *aData = pFile->aData; SQLITE_KV_LOG(("xWrite('%s-db',%d,%lld)\n", pFile->zClass, iAmt, iOfst)); assert( iAmt>=512 && iAmt<=65536 ); assert( (iAmt & (iAmt-1))==0 ); @@ -834,6 +838,10 @@ static int kvvfsOpen( }else{ pFile->zClass = "local"; } + pFile->aData = sqlite3_malloc64(SQLITE_KVOS_SZ); + if( pFile->aData==0 ){ + return SQLITE_NOMEM; + } pFile->aJrnl = 0; pFile->nJrnl = 0; pFile->szPage = -1; diff --git a/src/os_unix.c b/src/os_unix.c index 4f0426a7c3..e044fe1ea7 100644 --- a/src/os_unix.c +++ b/src/os_unix.c @@ -686,6 +686,9 @@ static int robust_open(const char *z, int f, mode_t m){ break; } if( fd>=SQLITE_MINIMUM_FILE_DESCRIPTOR ) break; + if( (f & (O_EXCL|O_CREAT))==(O_EXCL|O_CREAT) ){ + (void)osUnlink(z); + } osClose(fd); sqlite3_log(SQLITE_WARNING, "attempt to open \"%s\" as file descriptor %d", z, fd); @@ -6726,7 +6729,7 @@ static int unixRandomness(sqlite3_vfs *NotUsed, int nBuf, char *zBuf){ ** than the argument. */ static int unixSleep(sqlite3_vfs *NotUsed, int microseconds){ -#if OS_VXWORKS +#if OS_VXWORKS || _POSIX_C_SOURCE >= 199309L struct timespec sp; sp.tv_sec = microseconds / 1000000; diff --git a/src/pager.c b/src/pager.c index 30e3929093..d64da53db6 100644 --- a/src/pager.c +++ b/src/pager.c @@ -7170,7 +7170,11 @@ int sqlite3PagerSavepoint(Pager *pPager, int op, int iSavepoint){ */ const char *sqlite3PagerFilename(const Pager *pPager, int nullIfMemDb){ static const char zFake[8] = { 0, 0, 0, 0, 0, 0, 0, 0 }; - return (nullIfMemDb && pPager->memDb) ? &zFake[4] : pPager->zFilename; + if( nullIfMemDb && (pPager->memDb || sqlite3IsMemdb(pPager->pVfs)) ){ + return &zFake[4]; + }else{ + return pPager->zFilename; + } } /* diff --git a/src/prepare.c b/src/prepare.c index 1e7a1222ba..7607387408 100644 --- a/src/prepare.c +++ b/src/prepare.c @@ -520,8 +520,8 @@ static void schemaIsValid(Parse *pParse){ sqlite3BtreeGetMeta(pBt, BTREE_SCHEMA_VERSION, (u32 *)&cookie); assert( sqlite3SchemaMutexHeld(db, iDb, 0) ); if( cookie!=db->aDb[iDb].pSchema->schema_cookie ){ + if( DbHasProperty(db, iDb, DB_SchemaLoaded) ) pParse->rc = SQLITE_SCHEMA; sqlite3ResetOneSchema(db, iDb); - pParse->rc = SQLITE_SCHEMA; } /* Close the transaction, if one was opened. */ diff --git a/src/printf.c b/src/printf.c index 3602e1fcb9..335ad06844 100644 --- a/src/printf.c +++ b/src/printf.c @@ -736,13 +736,26 @@ void sqlite3_str_vappendf( } } if( precision>1 ){ + i64 nPrior = 1; width -= precision-1; if( width>1 && !flag_leftjustify ){ sqlite3_str_appendchar(pAccum, width-1, ' '); width = 0; } - while( precision-- > 1 ){ - sqlite3_str_append(pAccum, buf, length); + sqlite3_str_append(pAccum, buf, length); + precision--; + while( precision > 1 ){ + i64 nCopyBytes; + if( nPrior > precision-1 ) nPrior = precision - 1; + nCopyBytes = length*nPrior; + if( nCopyBytes + pAccum->nChar >= pAccum->nAlloc ){ + sqlite3StrAccumEnlarge(pAccum, nCopyBytes); + } + if( pAccum->accError ) break; + sqlite3_str_append(pAccum, + &pAccum->zText[pAccum->nChar-nCopyBytes], nCopyBytes); + precision -= nPrior; + nPrior *= 2; } } bufpt = buf; @@ -970,9 +983,9 @@ void sqlite3RecordErrorOffsetOfExpr(sqlite3 *db, const Expr *pExpr){ ** Return the number of bytes of text that StrAccum is able to accept ** after the attempted enlargement. The value returned might be zero. */ -int sqlite3StrAccumEnlarge(StrAccum *p, int N){ +int sqlite3StrAccumEnlarge(StrAccum *p, i64 N){ char *zNew; - assert( p->nChar+(i64)N >= p->nAlloc ); /* Only called if really needed */ + assert( p->nChar+N >= p->nAlloc ); /* Only called if really needed */ if( p->accError ){ testcase(p->accError==SQLITE_TOOBIG); testcase(p->accError==SQLITE_NOMEM); @@ -983,8 +996,7 @@ int sqlite3StrAccumEnlarge(StrAccum *p, int N){ return p->nAlloc - p->nChar - 1; }else{ char *zOld = isMalloced(p) ? p->zText : 0; - i64 szNew = p->nChar; - szNew += (sqlite3_int64)N + 1; + i64 szNew = p->nChar + N + 1; if( szNew+p->nChar<=p->mxAlloc ){ /* Force exponential buffer size growth as long as it does not overflow, ** to avoid having to call this routine too often */ @@ -1014,7 +1026,8 @@ int sqlite3StrAccumEnlarge(StrAccum *p, int N){ return 0; } } - return N; + assert( N>=0 && N<=0x7fffffff ); + return (int)N; } /* diff --git a/src/select.c b/src/select.c index b7f58f552b..a06a472c08 100644 --- a/src/select.c +++ b/src/select.c @@ -65,6 +65,10 @@ struct SortCtx { } aDefer[4]; #endif struct RowLoadInfo *pDeferredRowLoad; /* Deferred row loading info or NULL */ +#ifdef SQLITE_ENABLE_STMT_SCANSTATUS + int addrPush; /* First instruction to push data into sorter */ + int addrPushEnd; /* Last instruction that pushes data into sorter */ +#endif }; #define SORTFLAG_UseSorter 0x01 /* Use SorterOpen instead of OpenEphemeral */ @@ -721,6 +725,10 @@ static void pushOntoSorter( */ assert( nData==1 || regData==regOrigData || regOrigData==0 ); +#ifdef SQLITE_ENABLE_STMT_SCANSTATUS + pSort->addrPush = sqlite3VdbeCurrentAddr(v); +#endif + if( nPrefixReg ){ assert( nPrefixReg==nExpr+bSeq ); regBase = regData - nPrefixReg; @@ -821,6 +829,9 @@ static void pushOntoSorter( sqlite3VdbeChangeP2(v, iSkip, pSort->labelOBLopt ? pSort->labelOBLopt : sqlite3VdbeCurrentAddr(v)); } +#ifdef SQLITE_ENABLE_STMT_SCANSTATUS + pSort->addrPushEnd = sqlite3VdbeCurrentAddr(v)-1; +#endif } /* @@ -1287,9 +1298,6 @@ static void selectInnerLoop( testcase( eDest==SRT_Fifo ); testcase( eDest==SRT_DistFifo ); sqlite3VdbeAddOp3(v, OP_MakeRecord, regResult, nResultCol, r1+nPrefixReg); - if( pDest->zAffSdst ){ - sqlite3VdbeChangeP4(v, -1, pDest->zAffSdst, nResultCol); - } #ifndef SQLITE_OMIT_CTE if( eDest==SRT_DistFifo ){ /* If the destination is DistFifo, then cursor (iParm+1) is open @@ -1647,6 +1655,16 @@ static void generateSortTail( int bSeq; /* True if sorter record includes seq. no. */ int nRefKey = 0; struct ExprList_item *aOutEx = p->pEList->a; +#ifdef SQLITE_ENABLE_STMT_SCANSTATUS + int addrExplain; /* Address of OP_Explain instruction */ +#endif + + ExplainQueryPlan2(addrExplain, (pParse, 0, + "USE TEMP B-TREE FOR %sORDER BY", pSort->nOBSat>0?"RIGHT PART OF ":"") + ); + sqlite3VdbeScanStatusRange(v, addrExplain,pSort->addrPush,pSort->addrPushEnd); + sqlite3VdbeScanStatusCounters(v, addrExplain, addrExplain, pSort->addrPush); + assert( addrBreak<0 ); if( pSort->labelBkOut ){ @@ -1759,6 +1777,7 @@ static void generateSortTail( VdbeComment((v, "%s", aOutEx[i].zEName)); } } + sqlite3VdbeScanStatusRange(v, addrExplain, addrExplain, -1); switch( eDest ){ case SRT_Table: case SRT_EphemTab: { @@ -1820,6 +1839,7 @@ static void generateSortTail( }else{ sqlite3VdbeAddOp2(v, OP_Next, iTab, addr); VdbeCoverage(v); } + sqlite3VdbeScanStatusRange(v, addrExplain, sqlite3VdbeCurrentAddr(v)-1, -1); if( pSort->regReturn ) sqlite3VdbeAddOp1(v, OP_Return, pSort->regReturn); sqlite3VdbeResolveLabel(v, addrBreak); } @@ -1850,6 +1870,7 @@ static void generateSortTail( #else /* if !defined(SQLITE_ENABLE_COLUMN_METADATA) */ # define columnType(A,B,C,D,E) columnTypeImpl(A,B) #endif +#ifndef SQLITE_OMIT_DECLTYPE static const char *columnTypeImpl( NameContext *pNC, #ifndef SQLITE_ENABLE_COLUMN_METADATA @@ -1880,7 +1901,7 @@ static const char *columnTypeImpl( Table *pTab = 0; /* Table structure column is extracted from */ Select *pS = 0; /* Select the column is extracted from */ int iCol = pExpr->iColumn; /* Index of column in pTab */ - while( pNC && !pTab ){ + while( ALWAYS(pNC) && !pTab ){ SrcList *pTabList = pNC->pSrcList; for(j=0;jnSrc && pTabList->a[j].iCursor!=pExpr->iTable;j++); if( jnSrc ){ @@ -1891,7 +1912,7 @@ static const char *columnTypeImpl( } } - if( pTab==0 ){ + if( NEVER(pTab==0) ){ /* At one time, code such as "SELECT new.x" within a trigger would ** cause this condition to run. Since then, we have restructured how ** trigger code is generated and so this condition is no longer @@ -1996,6 +2017,7 @@ static const char *columnTypeImpl( #endif return zType; } +#endif /* !defined(SQLITE_OMIT_DECLTYPE) */ /* ** Generate code that will tell the VDBE the declaration types of columns @@ -2091,7 +2113,7 @@ void sqlite3GenerateColumnNames( if( pParse->colNamesSet ) return; /* Column names are determined by the left-most term of a compound select */ while( pSelect->pPrior ) pSelect = pSelect->pPrior; - SELECTTRACE(1,pParse,pSelect,("generating column names\n")); + TREETRACE(0x80,pParse,pSelect,("generating column names\n")); pTabList = pSelect->pSrc; pEList = pSelect->pEList; assert( v!=0 ); @@ -2267,47 +2289,76 @@ int sqlite3ColumnsFromExprList( } /* -** Add type and collation information to a column list based on -** a SELECT statement. -** -** The column list presumably came from selectColumnNamesFromExprList(). -** The column list has only names, not types or collations. This -** routine goes through and adds the types and collations. +** pTab is a transient Table object that represents a subquery of some +** kind (maybe a parenthesized subquery in the FROM clause of a larger +** query, or a VIEW, or a CTE). This routine computes type information +** for that Table object based on the Select object that implements the +** subquery. For the purposes of this routine, "type infomation" means: ** -** This routine requires that all identifiers in the SELECT -** statement be resolved. +** * The datatype name, as it might appear in a CREATE TABLE statement +** * Which collating sequence to use for the column +** * The affinity of the column */ -void sqlite3SelectAddColumnTypeAndCollation( - Parse *pParse, /* Parsing contexts */ - Table *pTab, /* Add column type information to this table */ - Select *pSelect, /* SELECT used to determine types and collations */ - char aff /* Default affinity for columns */ +void sqlite3SubqueryColumnTypes( + Parse *pParse, /* Parsing contexts */ + Table *pTab, /* Add column type information to this table */ + Select *pSelect, /* SELECT used to determine types and collations */ + char aff /* Default affinity. */ ){ sqlite3 *db = pParse->db; - NameContext sNC; Column *pCol; CollSeq *pColl; - int i; + int i,j; Expr *p; struct ExprList_item *a; assert( pSelect!=0 ); assert( (pSelect->selFlags & SF_Resolved)!=0 ); assert( pTab->nCol==pSelect->pEList->nExpr || db->mallocFailed ); + assert( aff==SQLITE_AFF_NONE || aff==SQLITE_AFF_BLOB ); if( db->mallocFailed ) return; - memset(&sNC, 0, sizeof(sNC)); - sNC.pSrcList = pSelect->pSrc; + while( pSelect->pPrior ) pSelect = pSelect->pPrior; a = pSelect->pEList->a; for(i=0, pCol=pTab->aCol; inCol; i++, pCol++){ const char *zType; - i64 n, m; + i64 n; pTab->tabFlags |= (pCol->colFlags & COLFLAG_NOINSERT); p = a[i].pExpr; - zType = columnType(&sNC, p, 0, 0, 0); /* pCol->szEst = ... // Column size est for SELECT tables never used */ pCol->affinity = sqlite3ExprAffinity(p); + if( pCol->affinity<=SQLITE_AFF_NONE ){ + pCol->affinity = aff; + }else if( pCol->affinity>=SQLITE_AFF_NUMERIC && p->op==TK_CAST ){ + pCol->affinity = SQLITE_AFF_FLEXNUM; + } + if( pCol->affinity>=SQLITE_AFF_TEXT && pSelect->pNext ){ + int m = 0; + Select *pS2; + for(m=0, pS2=pSelect->pNext; pS2; pS2=pS2->pNext){ + m |= sqlite3ExprDataType(pS2->pEList->a[i].pExpr); + } + if( pCol->affinity==SQLITE_AFF_TEXT && (m&0x01)!=0 ){ + pCol->affinity = SQLITE_AFF_BLOB; + }else + if( pCol->affinity>=SQLITE_AFF_NUMERIC && (m&0x02)!=0 ){ + pCol->affinity = SQLITE_AFF_BLOB; + } + } + if( pCol->affinity==SQLITE_AFF_NUMERIC + || pCol->affinity==SQLITE_AFF_FLEXNUM + ){ + zType = "NUM"; + }else{ + zType = 0; + for(j=1; jaffinity ){ + zType = sqlite3StdType[j]; + break; + } + } + } if( zType ){ - m = sqlite3Strlen30(zType); + i64 m = sqlite3Strlen30(zType); n = sqlite3Strlen30(pCol->zCnName); pCol->zCnName = sqlite3DbReallocOrFree(db, pCol->zCnName, n+m+2); if( pCol->zCnName ){ @@ -2318,7 +2369,6 @@ void sqlite3SelectAddColumnTypeAndCollation( pCol->colFlags &= ~(COLFLAG_HASTYPE|COLFLAG_HASCOLL); } } - if( pCol->affinity<=SQLITE_AFF_NONE ) pCol->affinity = aff; pColl = sqlite3ExprCollSeq(pParse, p); if( pColl ){ assert( pTab->pIndex==0 ); @@ -2352,7 +2402,7 @@ Table *sqlite3ResultSetOfSelect(Parse *pParse, Select *pSelect, char aff){ pTab->zName = 0; pTab->nRowLogEst = 200; assert( 200==sqlite3LogEst(1048576) ); sqlite3ColumnsFromExprList(pParse, pSelect->pEList, &pTab->nCol, &pTab->aCol); - sqlite3SelectAddColumnTypeAndCollation(pParse, pTab, pSelect, aff); + sqlite3SubqueryColumnTypes(pParse, pTab, pSelect, aff); pTab->iPKey = -1; if( db->mallocFailed ){ sqlite3DeleteTable(db, pTab); @@ -2877,7 +2927,7 @@ static int multiSelect( pPrior->iLimit = p->iLimit; pPrior->iOffset = p->iOffset; pPrior->pLimit = p->pLimit; - SELECTTRACE(1, pParse, p, ("multiSelect UNION ALL left...\n")); + TREETRACE(0x200, pParse, p, ("multiSelect UNION ALL left...\n")); rc = sqlite3Select(pParse, pPrior, &dest); pPrior->pLimit = 0; if( rc ){ @@ -2895,7 +2945,7 @@ static int multiSelect( } } ExplainQueryPlan((pParse, 1, "UNION ALL")); - SELECTTRACE(1, pParse, p, ("multiSelect UNION ALL right...\n")); + TREETRACE(0x200, pParse, p, ("multiSelect UNION ALL right...\n")); rc = sqlite3Select(pParse, p, &dest); testcase( rc!=SQLITE_OK ); pDelete = p->pPrior; @@ -2948,7 +2998,7 @@ static int multiSelect( */ assert( !pPrior->pOrderBy ); sqlite3SelectDestInit(&uniondest, priorOp, unionTab); - SELECTTRACE(1, pParse, p, ("multiSelect EXCEPT/UNION left...\n")); + TREETRACE(0x200, pParse, p, ("multiSelect EXCEPT/UNION left...\n")); rc = sqlite3Select(pParse, pPrior, &uniondest); if( rc ){ goto multi_select_end; @@ -2968,7 +3018,7 @@ static int multiSelect( uniondest.eDest = op; ExplainQueryPlan((pParse, 1, "%s USING TEMP B-TREE", sqlite3SelectOpName(p->op))); - SELECTTRACE(1, pParse, p, ("multiSelect EXCEPT/UNION right...\n")); + TREETRACE(0x200, pParse, p, ("multiSelect EXCEPT/UNION right...\n")); rc = sqlite3Select(pParse, p, &uniondest); testcase( rc!=SQLITE_OK ); assert( p->pOrderBy==0 ); @@ -3029,7 +3079,7 @@ static int multiSelect( /* Code the SELECTs to our left into temporary table "tab1". */ sqlite3SelectDestInit(&intersectdest, SRT_Union, tab1); - SELECTTRACE(1, pParse, p, ("multiSelect INTERSECT left...\n")); + TREETRACE(0x400, pParse, p, ("multiSelect INTERSECT left...\n")); rc = sqlite3Select(pParse, pPrior, &intersectdest); if( rc ){ goto multi_select_end; @@ -3046,7 +3096,7 @@ static int multiSelect( intersectdest.iSDParm = tab2; ExplainQueryPlan((pParse, 1, "%s USING TEMP B-TREE", sqlite3SelectOpName(p->op))); - SELECTTRACE(1, pParse, p, ("multiSelect INTERSECT right...\n")); + TREETRACE(0x400, pParse, p, ("multiSelect INTERSECT right...\n")); rc = sqlite3Select(pParse, p, &intersectdest); testcase( rc!=SQLITE_OK ); pDelete = p->pPrior; @@ -4050,6 +4100,34 @@ static ExprList *findLeftmostExprlist(Select *pSel){ return pSel->pEList; } +/* +** Return true if any of the result-set columns in the compound query +** have incompatible affinities on one or more arms of the compound. +*/ +static int compoundHasDifferentAffinities(Select *p){ + int ii; + ExprList *pList; + assert( p!=0 ); + assert( p->pEList!=0 ); + assert( p->pPrior!=0 ); + pList = p->pEList; + for(ii=0; iinExpr; ii++){ + char aff; + Select *pSub1; + assert( pList->a[ii].pExpr!=0 ); + aff = sqlite3ExprAffinity(pList->a[ii].pExpr); + for(pSub1=p->pPrior; pSub1; pSub1=pSub1->pPrior){ + assert( pSub1->pEList!=0 ); + assert( pSub1->pEList->nExpr>ii ); + assert( pSub1->pEList->a[ii].pExpr!=0 ); + if( sqlite3ExprAffinity(pSub1->pEList->a[ii].pExpr)!=aff ){ + return 1; + } + } + } + return 0; +} + #if !defined(SQLITE_OMIT_SUBQUERY) || !defined(SQLITE_OMIT_VIEW) /* ** This routine attempts to flatten subqueries as a performance optimization. @@ -4153,7 +4231,8 @@ static ExprList *findLeftmostExprlist(Select *pSel){ ** query or there are no RIGHT or FULL JOINs in any arm ** of the subquery. (This is a duplicate of condition (27b).) ** (17h) The corresponding result set expressions in all arms of the -** compound must have the same affinity. +** compound must have the same affinity. (See restriction (9) +** on the push-down optimization.) ** ** The parent and sub-query may contain WHERE clauses. Subject to ** rules (11), (13) and (14), they may also contain ORDER BY, @@ -4372,19 +4451,7 @@ static int flattenSubquery( if( (p->selFlags & SF_Recursive) ) return 0; /* Restriction (17h) */ - for(ii=0; iipEList->nExpr; ii++){ - char aff; - assert( pSub->pEList->a[ii].pExpr!=0 ); - aff = sqlite3ExprAffinity(pSub->pEList->a[ii].pExpr); - for(pSub1=pSub->pPrior; pSub1; pSub1=pSub1->pPrior){ - assert( pSub1->pEList!=0 ); - assert( pSub1->pEList->nExpr>ii ); - assert( pSub1->pEList->a[ii].pExpr!=0 ); - if( sqlite3ExprAffinity(pSub1->pEList->a[ii].pExpr)!=aff ){ - return 0; - } - } - } + if( compoundHasDifferentAffinities(pSub) ) return 0; if( pSrc->nSrc>1 ){ if( pParse->nSelect>500 ) return 0; @@ -4395,7 +4462,7 @@ static int flattenSubquery( } /***** If we reach this point, flattening is permitted. *****/ - SELECTTRACE(1,pParse,p,("flatten %u.%p from term %d\n", + TREETRACE(0x4,pParse,p,("flatten %u.%p from term %d\n", pSub->selId, pSub, iFrom)); /* Authorize the subquery */ @@ -4474,7 +4541,7 @@ static int flattenSubquery( if( pPrior ) pPrior->pNext = pNew; pNew->pNext = p; p->pPrior = pNew; - SELECTTRACE(2,pParse,p,("compound-subquery flattener" + TREETRACE(0x4,pParse,p,("compound-subquery flattener" " creates %u as peer\n",pNew->selId)); } assert( pSubitem->pSelect==0 ); @@ -4654,8 +4721,8 @@ static int flattenSubquery( sqlite3SelectDelete(db, pSub1); #if TREETRACE_ENABLED - if( sqlite3TreeTrace & 0x100 ){ - SELECTTRACE(0x100,pParse,p,("After flattening:\n")); + if( sqlite3TreeTrace & 0x4 ){ + TREETRACE(0x4,pParse,p,("After flattening:\n")); sqlite3TreeViewSelect(0, p, 0); } #endif @@ -5029,12 +5096,14 @@ static int pushDownWindowCheck(Parse *pParse, Select *pSubq, Expr *pExpr){ ** be materialized. (This restriction is implemented in the calling ** routine.) ** -** (8) The subquery may not be a compound that uses UNION, INTERSECT, -** or EXCEPT. (We could, perhaps, relax this restriction to allow -** this case if none of the comparisons operators between left and -** right arms of the compound use a collation other than BINARY. -** But it is a lot of work to check that case for an obscure and -** minor optimization, so we omit it for now.) +** (8) If the subquery is a compound that uses UNION, INTERSECT, +** or EXCEPT, then all of the result set columns for all arms of +** the compound must use the BINARY collating sequence. +** +** (9) If the subquery is a compound, then all arms of the compound must +** have the same affinity. (This is the same as restriction (17h) +** for query flattening.) +** ** ** Return 0 if no changes are made and non-zero if one or more WHERE clause ** terms are duplicated into the subquery. @@ -5051,20 +5120,44 @@ static int pushDownWhereTerms( if( pSubq->selFlags & (SF_Recursive|SF_MultiPart) ) return 0; if( pSrc->fg.jointype & (JT_LTORJ|JT_RIGHT) ) return 0; -#ifndef SQLITE_OMIT_WINDOWFUNC if( pSubq->pPrior ){ Select *pSel; + int notUnionAll = 0; for(pSel=pSubq; pSel; pSel=pSel->pPrior){ u8 op = pSel->op; assert( op==TK_ALL || op==TK_SELECT || op==TK_UNION || op==TK_INTERSECT || op==TK_EXCEPT ); - if( op!=TK_ALL && op!=TK_SELECT ) return 0; /* restriction (8) */ + if( op!=TK_ALL && op!=TK_SELECT ){ + notUnionAll = 1; + } +#ifndef SQLITE_OMIT_WINDOWFUNC if( pSel->pWin ) return 0; /* restriction (6b) */ +#endif + } + if( compoundHasDifferentAffinities(pSubq) ){ + return 0; /* restriction (9) */ + } + if( notUnionAll ){ + /* If any of the compound arms are connected using UNION, INTERSECT, + ** or EXCEPT, then we must ensure that none of the columns use a + ** non-BINARY collating sequence. */ + for(pSel=pSubq; pSel; pSel=pSel->pPrior){ + int ii; + const ExprList *pList = pSel->pEList; + assert( pList!=0 ); + for(ii=0; iinExpr; ii++){ + CollSeq *pColl = sqlite3ExprCollSeq(pParse, pList->a[ii].pExpr); + if( !sqlite3IsBinary(pColl) ){ + return 0; /* Restriction (8) */ + } + } + } } }else{ +#ifndef SQLITE_OMIT_WINDOWFUNC if( pSubq->pWin && pSubq->pWin->pPartition==0 ) return 0; - } #endif + } #ifdef SQLITE_DEBUG /* Only the first term of a compound can have a WITH clause. But make @@ -6068,8 +6161,8 @@ static int selectExpander(Walker *pWalker, Select *p){ } } #if TREETRACE_ENABLED - if( sqlite3TreeTrace & 0x100 ){ - SELECTTRACE(0x100,pParse,p,("After result-set wildcard expansion:\n")); + if( sqlite3TreeTrace & 0x8 ){ + TREETRACE(0x8,pParse,p,("After result-set wildcard expansion:\n")); sqlite3TreeViewSelect(0, p, 0); } #endif @@ -6120,14 +6213,14 @@ static void sqlite3SelectExpand(Parse *pParse, Select *pSelect){ ** This is a Walker.xSelectCallback callback for the sqlite3SelectTypeInfo() ** interface. ** -** For each FROM-clause subquery, add Column.zType and Column.zColl -** information to the Table structure that represents the result set -** of that subquery. +** For each FROM-clause subquery, add Column.zType, Column.zColl, and +** Column.affinity information to the Table structure that represents +** the result set of that subquery. ** ** The Table structure that represents the result set was constructed -** by selectExpander() but the type and collation information was omitted -** at that point because identifiers had not yet been resolved. This -** routine is called after identifier resolution. +** by selectExpander() but the type and collation and affinity information +** was omitted at that point because identifiers had not yet been resolved. +** This routine is called after identifier resolution. */ static void selectAddSubqueryTypeInfo(Walker *pWalker, Select *p){ Parse *pParse; @@ -6147,9 +6240,7 @@ static void selectAddSubqueryTypeInfo(Walker *pWalker, Select *p){ /* A sub-query in the FROM clause of a SELECT */ Select *pSel = pFrom->pSelect; if( pSel ){ - while( pSel->pPrior ) pSel = pSel->pPrior; - sqlite3SelectAddColumnTypeAndCollation(pParse, pTab, pSel, - SQLITE_AFF_NONE); + sqlite3SubqueryColumnTypes(pParse, pTab, pSel, SQLITE_AFF_NONE); } } } @@ -6204,6 +6295,173 @@ void sqlite3SelectPrep( sqlite3SelectAddTypeInfo(pParse, p); } +#if TREETRACE_ENABLED +/* +** Display all information about an AggInfo object +*/ +static void printAggInfo(AggInfo *pAggInfo){ + int ii; + for(ii=0; iinColumn; ii++){ + struct AggInfo_col *pCol = &pAggInfo->aCol[ii]; + sqlite3DebugPrintf( + "agg-column[%d] pTab=%s iTable=%d iColumn=%d iMem=%d" + " iSorterColumn=%d %s\n", + ii, pCol->pTab ? pCol->pTab->zName : "NULL", + pCol->iTable, pCol->iColumn, pAggInfo->iFirstReg+ii, + pCol->iSorterColumn, + ii>=pAggInfo->nAccumulator ? "" : " Accumulator"); + sqlite3TreeViewExpr(0, pAggInfo->aCol[ii].pCExpr, 0); + } + for(ii=0; iinFunc; ii++){ + sqlite3DebugPrintf("agg-func[%d]: iMem=%d\n", + ii, pAggInfo->iFirstReg+pAggInfo->nColumn+ii); + sqlite3TreeViewExpr(0, pAggInfo->aFunc[ii].pFExpr, 0); + } +} +#endif /* TREETRACE_ENABLED */ + +/* +** Analyze the arguments to aggregate functions. Create new pAggInfo->aCol[] +** entries for columns that are arguments to aggregate functions but which +** are not otherwise used. +** +** The aCol[] entries in AggInfo prior to nAccumulator are columns that +** are referenced outside of aggregate functions. These might be columns +** that are part of the GROUP by clause, for example. Other database engines +** would throw an error if there is a column reference that is not in the +** GROUP BY clause and that is not part of an aggregate function argument. +** But SQLite allows this. +** +** The aCol[] entries beginning with the aCol[nAccumulator] and following +** are column references that are used exclusively as arguments to +** aggregate functions. This routine is responsible for computing +** (or recomputing) those aCol[] entries. +*/ +static void analyzeAggFuncArgs( + AggInfo *pAggInfo, + NameContext *pNC +){ + int i; + assert( pAggInfo!=0 ); + assert( pAggInfo->iFirstReg==0 ); + pNC->ncFlags |= NC_InAggFunc; + for(i=0; inFunc; i++){ + Expr *pExpr = pAggInfo->aFunc[i].pFExpr; + assert( ExprUseXList(pExpr) ); + sqlite3ExprAnalyzeAggList(pNC, pExpr->x.pList); +#ifndef SQLITE_OMIT_WINDOWFUNC + assert( !IsWindowFunc(pExpr) ); + if( ExprHasProperty(pExpr, EP_WinFunc) ){ + sqlite3ExprAnalyzeAggregates(pNC, pExpr->y.pWin->pFilter); + } +#endif + } + pNC->ncFlags &= ~NC_InAggFunc; +} + +/* +** An index on expressions is being used in the inner loop of an +** aggregate query with a GROUP BY clause. This routine attempts +** to adjust the AggInfo object to take advantage of index and to +** perhaps use the index as a covering index. +** +*/ +static void optimizeAggregateUseOfIndexedExpr( + Parse *pParse, /* Parsing context */ + Select *pSelect, /* The SELECT statement being processed */ + AggInfo *pAggInfo, /* The aggregate info */ + NameContext *pNC /* Name context used to resolve agg-func args */ +){ + assert( pAggInfo->iFirstReg==0 ); + pAggInfo->nColumn = pAggInfo->nAccumulator; + if( ALWAYS(pAggInfo->nSortingColumn>0) ){ + if( pAggInfo->nColumn==0 ){ + pAggInfo->nSortingColumn = 0; + }else{ + pAggInfo->nSortingColumn = + pAggInfo->aCol[pAggInfo->nColumn-1].iSorterColumn+1; + } + } + analyzeAggFuncArgs(pAggInfo, pNC); +#if TREETRACE_ENABLED + if( sqlite3TreeTrace & 0x20 ){ + IndexedExpr *pIEpr; + TREETRACE(0x20, pParse, pSelect, + ("AggInfo (possibly) adjusted for Indexed Exprs\n")); + sqlite3TreeViewSelect(0, pSelect, 0); + for(pIEpr=pParse->pIdxEpr; pIEpr; pIEpr=pIEpr->pIENext){ + printf("data-cursor=%d index={%d,%d}\n", + pIEpr->iDataCur, pIEpr->iIdxCur, pIEpr->iIdxCol); + sqlite3TreeViewExpr(0, pIEpr->pExpr, 0); + } + printAggInfo(pAggInfo); + } +#else + UNUSED_PARAMETER(pSelect); + UNUSED_PARAMETER(pParse); +#endif +} + +/* +** Walker callback for aggregateConvertIndexedExprRefToColumn(). +*/ +static int aggregateIdxEprRefToColCallback(Walker *pWalker, Expr *pExpr){ + AggInfo *pAggInfo; + struct AggInfo_col *pCol; + UNUSED_PARAMETER(pWalker); + if( pExpr->pAggInfo==0 ) return WRC_Continue; + if( pExpr->op==TK_AGG_COLUMN ) return WRC_Continue; + if( pExpr->op==TK_AGG_FUNCTION ) return WRC_Continue; + if( pExpr->op==TK_IF_NULL_ROW ) return WRC_Continue; + pAggInfo = pExpr->pAggInfo; + assert( pExpr->iAgg>=0 && pExpr->iAggnColumn ); + pCol = &pAggInfo->aCol[pExpr->iAgg]; + pExpr->op = TK_AGG_COLUMN; + pExpr->iTable = pCol->iTable; + pExpr->iColumn = pCol->iColumn; + return WRC_Prune; +} + +/* +** Convert every pAggInfo->aFunc[].pExpr such that any node within +** those expressions that has pAppInfo set is changed into a TK_AGG_COLUMN +** opcode. +*/ +static void aggregateConvertIndexedExprRefToColumn(AggInfo *pAggInfo){ + int i; + Walker w; + memset(&w, 0, sizeof(w)); + w.xExprCallback = aggregateIdxEprRefToColCallback; + for(i=0; inFunc; i++){ + sqlite3WalkExpr(&w, pAggInfo->aFunc[i].pFExpr); + } +} + + +/* +** Allocate a block of registers so that there is one register for each +** pAggInfo->aCol[] and pAggInfo->aFunc[] entry in pAggInfo. The first +** register in this block is stored in pAggInfo->iFirstReg. +** +** This routine may only be called once for each AggInfo object. Prior +** to calling this routine: +** +** * The aCol[] and aFunc[] arrays may be modified +** * The AggInfoColumnReg() and AggInfoFuncReg() macros may not be used +** +** After clling this routine: +** +** * The aCol[] and aFunc[] arrays are fixed +** * The AggInfoColumnReg() and AggInfoFuncReg() macros may be used +** +*/ +static void assignAggregateRegisters(Parse *pParse, AggInfo *pAggInfo){ + assert( pAggInfo!=0 ); + assert( pAggInfo->iFirstReg==0 ); + pAggInfo->iFirstReg = pParse->nMem + 1; + pParse->nMem += pAggInfo->nColumn + pAggInfo->nFunc; +} + /* ** Reset the aggregate accumulator. ** @@ -6217,24 +6475,13 @@ static void resetAccumulator(Parse *pParse, AggInfo *pAggInfo){ int i; struct AggInfo_func *pFunc; int nReg = pAggInfo->nFunc + pAggInfo->nColumn; + assert( pAggInfo->iFirstReg>0 ); assert( pParse->db->pParse==pParse ); assert( pParse->db->mallocFailed==0 || pParse->nErr!=0 ); if( nReg==0 ) return; if( pParse->nErr ) return; -#ifdef SQLITE_DEBUG - /* Verify that all AggInfo registers are within the range specified by - ** AggInfo.mnReg..AggInfo.mxReg */ - assert( nReg==pAggInfo->mxReg-pAggInfo->mnReg+1 ); - for(i=0; inColumn; i++){ - assert( pAggInfo->aCol[i].iMem>=pAggInfo->mnReg - && pAggInfo->aCol[i].iMem<=pAggInfo->mxReg ); - } - for(i=0; inFunc; i++){ - assert( pAggInfo->aFunc[i].iMem>=pAggInfo->mnReg - && pAggInfo->aFunc[i].iMem<=pAggInfo->mxReg ); - } -#endif - sqlite3VdbeAddOp3(v, OP_Null, 0, pAggInfo->mnReg, pAggInfo->mxReg); + sqlite3VdbeAddOp3(v, OP_Null, 0, pAggInfo->iFirstReg, + pAggInfo->iFirstReg+nReg-1); for(pFunc=pAggInfo->aFunc, i=0; inFunc; i++, pFunc++){ if( pFunc->iDistinct>=0 ){ Expr *pE = pFunc->pFExpr; @@ -6266,15 +6513,16 @@ static void finalizeAggFunctions(Parse *pParse, AggInfo *pAggInfo){ ExprList *pList; assert( ExprUseXList(pF->pFExpr) ); pList = pF->pFExpr->x.pList; - sqlite3VdbeAddOp2(v, OP_AggFinal, pF->iMem, pList ? pList->nExpr : 0); + sqlite3VdbeAddOp2(v, OP_AggFinal, AggInfoFuncReg(pAggInfo,i), + pList ? pList->nExpr : 0); sqlite3VdbeAppendP4(v, pF->pFunc, P4_FUNCDEF); } } /* -** Update the accumulator memory cells for an aggregate based on -** the current cursor position. +** Generate code that will update the accumulator memory cells for an +** aggregate based on the current cursor position. ** ** If regAcc is non-zero and there are no min() or max() aggregates ** in pAggInfo, then only populate the pAggInfo->nAccumulator accumulator @@ -6294,6 +6542,8 @@ static void updateAccumulator( struct AggInfo_func *pF; struct AggInfo_col *pC; + assert( pAggInfo->iFirstReg>0 ); + if( pParse->nErr ) return; pAggInfo->directMode = 1; for(i=0, pF=pAggInfo->aFunc; inFunc; i++, pF++){ int nArg; @@ -6354,7 +6604,7 @@ static void updateAccumulator( if( regHit==0 && pAggInfo->nAccumulator ) regHit = ++pParse->nMem; sqlite3VdbeAddOp4(v, OP_CollSeq, regHit, 0, 0, (char *)pColl, P4_COLLSEQ); } - sqlite3VdbeAddOp3(v, OP_AggStep, 0, regAgg, pF->iMem); + sqlite3VdbeAddOp3(v, OP_AggStep, 0, regAgg, AggInfoFuncReg(pAggInfo,i)); sqlite3VdbeAppendP4(v, pF->pFunc, P4_FUNCDEF); sqlite3VdbeChangeP5(v, (u8)nArg); sqlite3ReleaseTempRange(pParse, regAgg, nArg); @@ -6369,7 +6619,7 @@ static void updateAccumulator( addrHitTest = sqlite3VdbeAddOp1(v, OP_If, regHit); VdbeCoverage(v); } for(i=0, pC=pAggInfo->aCol; inAccumulator; i++, pC++){ - sqlite3ExprCode(pParse, pC->pCExpr, pC->iMem); + sqlite3ExprCode(pParse, pC->pCExpr, AggInfoColumnReg(pAggInfo,i)); } pAggInfo->directMode = 0; @@ -6465,26 +6715,31 @@ static void havingToWhere(Parse *pParse, Select *p){ sqlite3WalkExpr(&sWalker, p->pHaving); #if TREETRACE_ENABLED if( sWalker.eCode && (sqlite3TreeTrace & 0x100)!=0 ){ - SELECTTRACE(0x100,pParse,p,("Move HAVING terms into WHERE:\n")); + TREETRACE(0x100,pParse,p,("Move HAVING terms into WHERE:\n")); sqlite3TreeViewSelect(0, p, 0); } #endif } /* -** Check to see if the pThis entry of pTabList is a self-join of a prior view. -** If it is, then return the SrcItem for the prior view. If it is not, -** then return 0. +** Check to see if the pThis entry of pTabList is a self-join of another view. +** Search FROM-clause entries in the range of iFirst..iEnd, including iFirst +** but stopping before iEnd. +** +** If pThis is a self-join, then return the SrcItem for the first other +** instance of that view found. If pThis is not a self-join then return 0. */ static SrcItem *isSelfJoinView( SrcList *pTabList, /* Search for self-joins in this FROM clause */ - SrcItem *pThis /* Search for prior reference to this subquery */ + SrcItem *pThis, /* Search for prior reference to this subquery */ + int iFirst, int iEnd /* Range of FROM-clause entries to search. */ ){ SrcItem *pItem; assert( pThis->pSelect!=0 ); if( pThis->pSelect->selFlags & SF_PushDown ) return 0; - for(pItem = pTabList->a; pItema[iFirst++]; if( pItem->pSelect==0 ) continue; if( pItem->fg.viaCoroutine ) continue; if( pItem->zName==0 ) continue; @@ -6597,8 +6852,8 @@ static int countOfViewOptimization(Parse *pParse, Select *p){ p->selFlags &= ~SF_Aggregate; #if TREETRACE_ENABLED - if( sqlite3TreeTrace & 0x400 ){ - SELECTTRACE(0x400,pParse,p,("After count-of-view optimization:\n")); + if( sqlite3TreeTrace & 0x200 ){ + TREETRACE(0x200,pParse,p,("After count-of-view optimization:\n")); sqlite3TreeViewSelect(0, p, 0); } #endif @@ -6629,6 +6884,62 @@ static int sameSrcAlias(SrcItem *p0, SrcList *pSrc){ return 0; } +/* +** Return TRUE (non-zero) if the i-th entry in the pTabList SrcList can +** be implemented as a co-routine. The i-th entry is guaranteed to be +** a subquery. +** +** The subquery is implemented as a co-routine if all of the following are +** true: +** +** (1) The subquery will likely be implemented in the outer loop of +** the query. This will be the case if any one of the following +** conditions hold: +** (a) The subquery is the only term in the FROM clause +** (b) The subquery is the left-most term and a CROSS JOIN or similar +** requires it to be the outer loop +** (c) All of the following are true: +** (i) The subquery is the left-most subquery in the FROM clause +** (ii) There is nothing that would prevent the subquery from +** being used as the outer loop if the sqlite3WhereBegin() +** routine nominates it to that position. +** (iii) The query is not a UPDATE ... FROM +** (2) The subquery is not a CTE that should be materialized because of +** the AS MATERIALIZED keywords +** (3) The subquery is not part of a left operand for a RIGHT JOIN +** (4) The SQLITE_Coroutine optimization disable flag is not set +** (5) The subquery is not self-joined +*/ +static int fromClauseTermCanBeCoroutine( + Parse *pParse, /* Parsing context */ + SrcList *pTabList, /* FROM clause */ + int i, /* Which term of the FROM clause holds the subquery */ + int selFlags /* Flags on the SELECT statement */ +){ + SrcItem *pItem = &pTabList->a[i]; + if( pItem->fg.isCte && pItem->u2.pCteUse->eM10d==M10d_Yes ) return 0;/* (2) */ + if( pTabList->a[0].fg.jointype & JT_LTORJ ) return 0; /* (3) */ + if( OptimizationDisabled(pParse->db, SQLITE_Coroutines) ) return 0; /* (4) */ + if( isSelfJoinView(pTabList, pItem, i+1, pTabList->nSrc)!=0 ){ + return 0; /* (5) */ + } + if( i==0 ){ + if( pTabList->nSrc==1 ) return 1; /* (1a) */ + if( pTabList->a[1].fg.jointype & JT_CROSS ) return 1; /* (1b) */ + if( selFlags & SF_UpdateFrom ) return 0; /* (1c-iii) */ + return 1; + } + if( selFlags & SF_UpdateFrom ) return 0; /* (1c-iii) */ + while( 1 /*exit-by-break*/ ){ + if( pItem->fg.jointype & (JT_OUTER|JT_CROSS) ) return 0; /* (1c-ii) */ + if( i==0 ) break; + i--; + pItem--; + if( pItem->pSelect!=0 ) return 0; /* (1c-i) */ + } + return 1; +} + /* ** Generate code for the SELECT statement given in the p argument. ** @@ -6674,8 +6985,8 @@ int sqlite3Select( assert( db->mallocFailed==0 ); if( sqlite3AuthCheck(pParse, SQLITE_SELECT, 0, 0, 0) ) return 1; #if TREETRACE_ENABLED - SELECTTRACE(1,pParse,p, ("begin processing:\n", pParse->addrExplain)); - if( sqlite3TreeTrace & 0x10100 ){ + TREETRACE(0x1,pParse,p, ("begin processing:\n", pParse->addrExplain)); + if( sqlite3TreeTrace & 0x10000 ){ if( (sqlite3TreeTrace & 0x10001)==0x10000 ){ sqlite3TreeViewLine(0, "In sqlite3Select() at %s:%d", __FILE__, __LINE__); @@ -6695,8 +7006,8 @@ int sqlite3Select( /* All of these destinations are also able to ignore the ORDER BY clause */ if( p->pOrderBy ){ #if TREETRACE_ENABLED - SELECTTRACE(1,pParse,p, ("dropping superfluous ORDER BY:\n")); - if( sqlite3TreeTrace & 0x100 ){ + TREETRACE(0x800,pParse,p, ("dropping superfluous ORDER BY:\n")); + if( sqlite3TreeTrace & 0x800 ){ sqlite3TreeViewExprList(0, p->pOrderBy, 0, "ORDERBY"); } #endif @@ -6716,8 +7027,8 @@ int sqlite3Select( assert( db->mallocFailed==0 ); assert( p->pEList!=0 ); #if TREETRACE_ENABLED - if( sqlite3TreeTrace & 0x104 ){ - SELECTTRACE(0x104,pParse,p, ("after name resolution:\n")); + if( sqlite3TreeTrace & 0x10 ){ + TREETRACE(0x10,pParse,p, ("after name resolution:\n")); sqlite3TreeViewSelect(0, p, 0); } #endif @@ -6758,8 +7069,8 @@ int sqlite3Select( goto select_end; } #if TREETRACE_ENABLED - if( p->pWin && (sqlite3TreeTrace & 0x108)!=0 ){ - SELECTTRACE(0x104,pParse,p, ("after window rewrite:\n")); + if( p->pWin && (sqlite3TreeTrace & 0x40)!=0 ){ + TREETRACE(0x40,pParse,p, ("after window rewrite:\n")); sqlite3TreeViewSelect(0, p, 0); } #endif @@ -6790,7 +7101,7 @@ int sqlite3Select( && sqlite3ExprImpliesNonNullRow(p->pWhere, pItem->iCursor) && OptimizationEnabled(db, SQLITE_SimplifyJoin) ){ - SELECTTRACE(0x100,pParse,p, + TREETRACE(0x1000,pParse,p, ("LEFT-JOIN simplifies to JOIN on term %d\n",i)); pItem->fg.jointype &= ~(JT_LEFT|JT_OUTER); assert( pItem->iCursor>=0 ); @@ -6846,7 +7157,7 @@ int sqlite3Select( && (p->selFlags & SF_OrderByReqd)==0 /* Condition (3) and (4) */ && OptimizationEnabled(db, SQLITE_OmitOrderBy) ){ - SELECTTRACE(0x100,pParse,p, + TREETRACE(0x800,pParse,p, ("omit superfluous ORDER BY on %r FROM-clause subquery\n",i+1)); sqlite3ParserAddCleanup(pParse, (void(*)(sqlite3*,void*))sqlite3ExprListDelete, @@ -6901,8 +7212,8 @@ int sqlite3Select( if( p->pPrior ){ rc = multiSelect(pParse, p, pDest); #if TREETRACE_ENABLED - SELECTTRACE(0x1,pParse,p,("end compound-select processing\n")); - if( (sqlite3TreeTrace & 0x2000)!=0 && ExplainQueryPlanParent(pParse)==0 ){ + TREETRACE(0x400,pParse,p,("end compound-select processing\n")); + if( (sqlite3TreeTrace & 0x400)!=0 && ExplainQueryPlanParent(pParse)==0 ){ sqlite3TreeViewSelect(0, p, 0); } #endif @@ -6922,13 +7233,13 @@ int sqlite3Select( && propagateConstants(pParse, p) ){ #if TREETRACE_ENABLED - if( sqlite3TreeTrace & 0x100 ){ - SELECTTRACE(0x100,pParse,p,("After constant propagation:\n")); + if( sqlite3TreeTrace & 0x2000 ){ + TREETRACE(0x2000,pParse,p,("After constant propagation:\n")); sqlite3TreeViewSelect(0, p, 0); } #endif }else{ - SELECTTRACE(0x100,pParse,p,("Constant propagation not helpful\n")); + TREETRACE(0x2000,pParse,p,("Constant propagation not helpful\n")); } #ifdef SQLITE_COUNTOFVIEW_OPTIMIZATION @@ -7001,36 +7312,23 @@ int sqlite3Select( && pushDownWhereTerms(pParse, pSub, p->pWhere, pItem) ){ #if TREETRACE_ENABLED - if( sqlite3TreeTrace & 0x100 ){ - SELECTTRACE(0x100,pParse,p, + if( sqlite3TreeTrace & 0x4000 ){ + TREETRACE(0x4000,pParse,p, ("After WHERE-clause push-down into subquery %d:\n", pSub->selId)); sqlite3TreeViewSelect(0, p, 0); } #endif assert( pItem->pSelect && (pItem->pSelect->selFlags & SF_PushDown)!=0 ); }else{ - SELECTTRACE(0x100,pParse,p,("Push-down not possible\n")); + TREETRACE(0x4000,pParse,p,("Push-down not possible\n")); } zSavedAuthContext = pParse->zAuthContext; pParse->zAuthContext = pItem->zName; /* Generate code to implement the subquery - ** - ** The subquery is implemented as a co-routine if all of the following are - ** true: - ** - ** (1) the subquery is guaranteed to be the outer loop (so that - ** it does not need to be computed more than once), and - ** (2) the subquery is not a CTE that should be materialized - ** (3) the subquery is not part of a left operand for a RIGHT JOIN */ - if( i==0 - && (pTabList->nSrc==1 - || (pTabList->a[1].fg.jointype&(JT_OUTER|JT_CROSS))!=0) /* (1) */ - && (pItem->fg.isCte==0 || pItem->u2.pCteUse->eM10d!=M10d_Yes) /* (2) */ - && (pTabList->a[0].fg.jointype & JT_LTORJ)==0 /* (3) */ - ){ + if( fromClauseTermCanBeCoroutine(pParse, pTabList, i, p->selFlags) ){ /* Implement a co-routine that will return a single row of the result ** set on each invocation. */ @@ -7061,7 +7359,7 @@ int sqlite3Select( VdbeComment((v, "%!S", pItem)); } pSub->nSelectRow = pCteUse->nRowEst; - }else if( (pPrior = isSelfJoinView(pTabList, pItem))!=0 ){ + }else if( (pPrior = isSelfJoinView(pTabList, pItem, 0, i))!=0 ){ /* This view has already been materialized by a prior entry in ** this same FROM clause. Reuse it. */ if( pPrior->addrFillSub ){ @@ -7075,6 +7373,9 @@ int sqlite3Select( ** the same view can reuse the materialization. */ int topAddr; int onceAddr = 0; +#ifdef SQLITE_ENABLE_STMT_SCANSTATUS + int addrExplain; +#endif pItem->regReturn = ++pParse->nMem; topAddr = sqlite3VdbeAddOp0(v, OP_Goto); @@ -7090,15 +7391,14 @@ int sqlite3Select( VdbeNoopComment((v, "materialize %!S", pItem)); } sqlite3SelectDestInit(&dest, SRT_EphemTab, pItem->iCursor); - ExplainQueryPlan((pParse, 1, "MATERIALIZE %!S", pItem)); - dest.zAffSdst = sqlite3TableAffinityStr(db, pItem->pTab); + + ExplainQueryPlan2(addrExplain, (pParse, 1, "MATERIALIZE %!S", pItem)); sqlite3Select(pParse, pSub, &dest); - sqlite3DbFree(db, dest.zAffSdst); - dest.zAffSdst = 0; pItem->pTab->nRowLogEst = pSub->nSelectRow; if( onceAddr ) sqlite3VdbeJumpHere(v, onceAddr); sqlite3VdbeAddOp2(v, OP_Return, pItem->regReturn, topAddr+1); VdbeComment((v, "end %!S", pItem)); + sqlite3VdbeScanStatusRange(v, addrExplain, addrExplain, -1); sqlite3VdbeJumpHere(v, topAddr); sqlite3ClearTempRegCache(pParse); if( pItem->fg.isCte && pItem->fg.isCorrelated==0 ){ @@ -7124,8 +7424,8 @@ int sqlite3Select( sDistinct.isTnct = (p->selFlags & SF_Distinct)!=0; #if TREETRACE_ENABLED - if( sqlite3TreeTrace & 0x400 ){ - SELECTTRACE(0x400,pParse,p,("After all FROM-clause analysis:\n")); + if( sqlite3TreeTrace & 0x8000 ){ + TREETRACE(0x8000,pParse,p,("After all FROM-clause analysis:\n")); sqlite3TreeViewSelect(0, p, 0); } #endif @@ -7161,8 +7461,8 @@ int sqlite3Select( sDistinct.isTnct = 2; #if TREETRACE_ENABLED - if( sqlite3TreeTrace & 0x400 ){ - SELECTTRACE(0x400,pParse,p,("Transform DISTINCT into GROUP BY:\n")); + if( sqlite3TreeTrace & 0x20000 ){ + TREETRACE(0x20000,pParse,p,("Transform DISTINCT into GROUP BY:\n")); sqlite3TreeViewSelect(0, p, 0); } #endif @@ -7248,7 +7548,7 @@ int sqlite3Select( /* Begin the database scan. */ - SELECTTRACE(1,pParse,p,("WhereBegin\n")); + TREETRACE(0x2,pParse,p,("WhereBegin\n")); pWInfo = sqlite3WhereBegin(pParse, pTabList, pWhere, sSort.pOrderBy, p->pEList, p, wctrlFlags, p->nSelectRow); if( pWInfo==0 ) goto select_end; @@ -7265,7 +7565,7 @@ int sqlite3Select( sSort.pOrderBy = 0; } } - SELECTTRACE(1,pParse,p,("WhereBegin returns\n")); + TREETRACE(0x2,pParse,p,("WhereBegin returns\n")); /* If sorting index that was created by a prior OP_OpenEphemeral ** instruction ended up not being needed, then change the OP_OpenEphemeral @@ -7304,7 +7604,7 @@ int sqlite3Select( /* End the database scan loop. */ - SELECTTRACE(1,pParse,p,("WhereEnd\n")); + TREETRACE(0x2,pParse,p,("WhereEnd\n")); sqlite3WhereEnd(pWInfo); } }else{ @@ -7385,12 +7685,14 @@ int sqlite3Select( goto select_end; } pAggInfo->selId = p->selId; +#ifdef SQLITE_DEBUG + pAggInfo->pSelect = p; +#endif memset(&sNC, 0, sizeof(sNC)); sNC.pParse = pParse; sNC.pSrcList = pTabList; sNC.uNC.pAggInfo = pAggInfo; VVA_ONLY( sNC.ncFlags = NC_UAggInfo; ) - pAggInfo->mnReg = pParse->nMem+1; pAggInfo->nSortingColumn = pGroupBy ? pGroupBy->nExpr : 0; pAggInfo->pGroupBy = pGroupBy; sqlite3ExprAnalyzeAggList(&sNC, pEList); @@ -7411,45 +7713,17 @@ int sqlite3Select( }else{ minMaxFlag = WHERE_ORDERBY_NORMAL; } - for(i=0; inFunc; i++){ - Expr *pExpr = pAggInfo->aFunc[i].pFExpr; - assert( ExprUseXList(pExpr) ); - sNC.ncFlags |= NC_InAggFunc; - sqlite3ExprAnalyzeAggList(&sNC, pExpr->x.pList); -#ifndef SQLITE_OMIT_WINDOWFUNC - assert( !IsWindowFunc(pExpr) ); - if( ExprHasProperty(pExpr, EP_WinFunc) ){ - sqlite3ExprAnalyzeAggregates(&sNC, pExpr->y.pWin->pFilter); - } -#endif - sNC.ncFlags &= ~NC_InAggFunc; - } - pAggInfo->mxReg = pParse->nMem; + analyzeAggFuncArgs(pAggInfo, &sNC); if( db->mallocFailed ) goto select_end; #if TREETRACE_ENABLED - if( sqlite3TreeTrace & 0x400 ){ - int ii; - SELECTTRACE(0x400,pParse,p,("After aggregate analysis %p:\n", pAggInfo)); + if( sqlite3TreeTrace & 0x20 ){ + TREETRACE(0x20,pParse,p,("After aggregate analysis %p:\n", pAggInfo)); sqlite3TreeViewSelect(0, p, 0); if( minMaxFlag ){ sqlite3DebugPrintf("MIN/MAX Optimization (0x%02x) adds:\n", minMaxFlag); sqlite3TreeViewExprList(0, pMinMaxOrderBy, 0, "ORDERBY"); } - for(ii=0; iinColumn; ii++){ - struct AggInfo_col *pCol = &pAggInfo->aCol[ii]; - sqlite3DebugPrintf( - "agg-column[%d] pTab=%s iTable=%d iColumn=%d iMem=%d" - " iSorterColumn=%d\n", - ii, pCol->pTab ? pCol->pTab->zName : "NULL", - pCol->iTable, pCol->iColumn, pCol->iMem, - pCol->iSorterColumn); - sqlite3TreeViewExpr(0, pAggInfo->aCol[ii].pCExpr, 0); - } - for(ii=0; iinFunc; ii++){ - sqlite3DebugPrintf("agg-func[%d]: iMem=%d\n", - ii, pAggInfo->aFunc[ii].iMem); - sqlite3TreeViewExpr(0, pAggInfo->aFunc[ii].pFExpr, 0); - } + printAggInfo(pAggInfo); } #endif @@ -7518,7 +7792,7 @@ int sqlite3Select( ** in the right order to begin with. */ sqlite3VdbeAddOp2(v, OP_Gosub, regReset, addrReset); - SELECTTRACE(1,pParse,p,("WhereBegin\n")); + TREETRACE(0x2,pParse,p,("WhereBegin\n")); pWInfo = sqlite3WhereBegin(pParse, pTabList, pWhere, pGroupBy, pDistinct, p, (sDistinct.isTnct==2 ? WHERE_DISTINCTBY : WHERE_GROUPBY) | (orderByGrp ? WHERE_SORTBYGROUP : 0) | distFlag, 0 @@ -7527,8 +7801,12 @@ int sqlite3Select( sqlite3ExprListDelete(db, pDistinct); goto select_end; } + if( pParse->pIdxEpr ){ + optimizeAggregateUseOfIndexedExpr(pParse, p, pAggInfo, &sNC); + } + assignAggregateRegisters(pParse, pAggInfo); eDist = sqlite3WhereIsDistinct(pWInfo); - SELECTTRACE(1,pParse,p,("WhereBegin returns\n")); + TREETRACE(0x2,pParse,p,("WhereBegin returns\n")); if( sqlite3WhereIsOrdered(pWInfo)==pGroupBy->nExpr ){ /* The optimizer is able to deliver rows in group by order so ** we do not have to sort. The OP_OpenEphemeral table will be @@ -7577,7 +7855,7 @@ int sqlite3Select( sqlite3VdbeAddOp2(v, OP_SorterInsert, pAggInfo->sortingIdx, regRecord); sqlite3ReleaseTempReg(pParse, regRecord); sqlite3ReleaseTempRange(pParse, regBase, nCol); - SELECTTRACE(1,pParse,p,("WhereEnd\n")); + TREETRACE(0x2,pParse,p,("WhereEnd\n")); sqlite3WhereEnd(pWInfo); pAggInfo->sortingIdxPTab = sortPTab = pParse->nTab++; sortOut = sqlite3GetTempReg(pParse); @@ -7587,6 +7865,23 @@ int sqlite3Select( pAggInfo->useSortingIdx = 1; } + /* If there entries in pAgggInfo->aFunc[] that contain subexpressions + ** that are indexed (and that were previously identified and tagged + ** in optimizeAggregateUseOfIndexedExpr()) then those subexpressions + ** must now be converted into a TK_AGG_COLUMN node so that the value + ** is correctly pulled from the index rather than being recomputed. */ + if( pParse->pIdxEpr ){ + aggregateConvertIndexedExprRefToColumn(pAggInfo); +#if TREETRACE_ENABLED + if( sqlite3TreeTrace & 0x20 ){ + TREETRACE(0x20, pParse, p, + ("AggInfo function expressions converted to reference index\n")); + sqlite3TreeViewSelect(0, p, 0); + printAggInfo(pAggInfo); + } +#endif + } + /* If the index or temporary table used by the GROUP BY sort ** will naturally deliver rows in the order required by the ORDER BY ** clause, cancel the ephemeral table open coded earlier. @@ -7655,7 +7950,7 @@ int sqlite3Select( sqlite3VdbeAddOp2(v, OP_SorterNext, pAggInfo->sortingIdx,addrTopOfLoop); VdbeCoverage(v); }else{ - SELECTTRACE(1,pParse,p,("WhereEnd\n")); + TREETRACE(0x2,pParse,p,("WhereEnd\n")); sqlite3WhereEnd(pWInfo); sqlite3VdbeChangeToNoop(v, addrSortingIdx); } @@ -7765,7 +8060,8 @@ int sqlite3Select( if( pKeyInfo ){ sqlite3VdbeChangeP4(v, -1, (char *)pKeyInfo, P4_KEYINFO); } - sqlite3VdbeAddOp2(v, OP_Count, iCsr, pAggInfo->aFunc[0].iMem); + assignAggregateRegisters(pParse, pAggInfo); + sqlite3VdbeAddOp2(v, OP_Count, iCsr, AggInfoFuncReg(pAggInfo,0)); sqlite3VdbeAddOp1(v, OP_Close, iCsr); explainSimpleCount(pParse, pTab, pBest); }else{ @@ -7801,6 +8097,7 @@ int sqlite3Select( pDistinct = pAggInfo->aFunc[0].pFExpr->x.pList; distFlag = pDistinct ? (WHERE_WANT_DISTINCT|WHERE_AGG_DISTINCT) : 0; } + assignAggregateRegisters(pParse, pAggInfo); /* This case runs if the aggregate has no GROUP BY clause. The ** processing is much simpler since there is only a single row @@ -7817,13 +8114,13 @@ int sqlite3Select( assert( minMaxFlag==WHERE_ORDERBY_NORMAL || pMinMaxOrderBy!=0 ); assert( pMinMaxOrderBy==0 || pMinMaxOrderBy->nExpr==1 ); - SELECTTRACE(1,pParse,p,("WhereBegin\n")); + TREETRACE(0x2,pParse,p,("WhereBegin\n")); pWInfo = sqlite3WhereBegin(pParse, pTabList, pWhere, pMinMaxOrderBy, pDistinct, p, minMaxFlag|distFlag, 0); if( pWInfo==0 ){ goto select_end; } - SELECTTRACE(1,pParse,p,("WhereBegin returns\n")); + TREETRACE(0x2,pParse,p,("WhereBegin returns\n")); eDist = sqlite3WhereIsDistinct(pWInfo); updateAccumulator(pParse, regAcc, pAggInfo, eDist); if( eDist!=WHERE_DISTINCT_NOOP ){ @@ -7837,7 +8134,7 @@ int sqlite3Select( if( minMaxFlag ){ sqlite3WhereMinMaxOptEarlyOut(v, pWInfo); } - SELECTTRACE(1,pParse,p,("WhereEnd\n")); + TREETRACE(0x2,pParse,p,("WhereEnd\n")); sqlite3WhereEnd(pWInfo); finalizeAggFunctions(pParse, pAggInfo); } @@ -7859,8 +8156,6 @@ int sqlite3Select( ** and send them to the callback one by one. */ if( sSort.pOrderBy ){ - explainTempTable(pParse, - sSort.nOBSat>0 ? "RIGHT PART OF ORDER BY":"ORDER BY"); assert( p->pEList==pEList ); generateSortTail(pParse, p, &sSort, pEList->nExpr, pDest); } @@ -7884,7 +8179,7 @@ select_end: if( pAggInfo && !db->mallocFailed ){ for(i=0; inColumn; i++){ Expr *pExpr = pAggInfo->aCol[i].pCExpr; - assert( pExpr!=0 ); + if( pExpr==0 ) continue; assert( pExpr->pAggInfo==pAggInfo ); assert( pExpr->iAgg==i ); } @@ -7898,8 +8193,8 @@ select_end: #endif #if TREETRACE_ENABLED - SELECTTRACE(0x1,pParse,p,("end processing\n")); - if( (sqlite3TreeTrace & 0x2000)!=0 && ExplainQueryPlanParent(pParse)==0 ){ + TREETRACE(0x1,pParse,p,("end processing\n")); + if( (sqlite3TreeTrace & 0x40000)!=0 && ExplainQueryPlanParent(pParse)==0 ){ sqlite3TreeViewSelect(0, p, 0); } #endif diff --git a/src/shell.c.in b/src/shell.c.in index f164789776..d183c7842f 100644 --- a/src/shell.c.in +++ b/src/shell.c.in @@ -467,8 +467,95 @@ static char *Argv0; ** Prompt strings. Initialized in main. Settable with ** .prompt main continue */ -static char mainPrompt[20]; /* First line prompt. default: "sqlite> "*/ -static char continuePrompt[20]; /* Continuation prompt. default: " ...> " */ +#define PROMPT_LEN_MAX 20 +/* First line prompt. default: "sqlite> " */ +static char mainPrompt[PROMPT_LEN_MAX]; +/* Continuation prompt. default: " ...> " */ +static char continuePrompt[PROMPT_LEN_MAX]; + +/* +** Optionally disable dynamic continuation prompt. +** Unless disabled, the continuation prompt shows open SQL lexemes if any, +** or open parentheses level if non-zero, or continuation prompt as set. +** This facility interacts with the scanner and process_input() where the +** below 5 macros are used. +*/ +#ifdef SQLITE_OMIT_DYNAPROMPT +# define CONTINUATION_PROMPT continuePrompt +# define CONTINUE_PROMPT_RESET +# define CONTINUE_PROMPT_AWAITS(p,s) +# define CONTINUE_PROMPT_AWAITC(p,c) +# define CONTINUE_PAREN_INCR(p,n) +# define CONTINUE_PROMPT_PSTATE 0 +typedef void *t_NoDynaPrompt; +# define SCAN_TRACKER_REFTYPE t_NoDynaPrompt +#else +# define CONTINUATION_PROMPT dynamicContinuePrompt() +# define CONTINUE_PROMPT_RESET \ + do {setLexemeOpen(&dynPrompt,0,0); trackParenLevel(&dynPrompt,0);} while(0) +# define CONTINUE_PROMPT_AWAITS(p,s) \ + if(p && stdin_is_interactive) setLexemeOpen(p, s, 0) +# define CONTINUE_PROMPT_AWAITC(p,c) \ + if(p && stdin_is_interactive) setLexemeOpen(p, 0, c) +# define CONTINUE_PAREN_INCR(p,n) \ + if(p && stdin_is_interactive) (trackParenLevel(p,n)) +# define CONTINUE_PROMPT_PSTATE (&dynPrompt) +typedef struct DynaPrompt *t_DynaPromptRef; +# define SCAN_TRACKER_REFTYPE t_DynaPromptRef + +static struct DynaPrompt { + char dynamicPrompt[PROMPT_LEN_MAX]; + char acAwait[2]; + int inParenLevel; + char *zScannerAwaits; +} dynPrompt = { {0}, {0}, 0, 0 }; + +/* Record parenthesis nesting level change, or force level to 0. */ +static void trackParenLevel(struct DynaPrompt *p, int ni){ + p->inParenLevel += ni; + if( ni==0 ) p->inParenLevel = 0; + p->zScannerAwaits = 0; +} + +/* Record that a lexeme is opened, or closed with args==0. */ +static void setLexemeOpen(struct DynaPrompt *p, char *s, char c){ + if( s!=0 || c==0 ){ + p->zScannerAwaits = s; + p->acAwait[0] = 0; + }else{ + p->acAwait[0] = c; + p->zScannerAwaits = p->acAwait; + } +} + +/* Upon demand, derive the continuation prompt to display. */ +static char *dynamicContinuePrompt(void){ + if( continuePrompt[0]==0 + || (dynPrompt.zScannerAwaits==0 && dynPrompt.inParenLevel == 0) ){ + return continuePrompt; + }else{ + if( dynPrompt.zScannerAwaits ){ + size_t ncp = strlen(continuePrompt), ndp = strlen(dynPrompt.zScannerAwaits); + if( ndp > ncp-3 ) return continuePrompt; + strcpy(dynPrompt.dynamicPrompt, dynPrompt.zScannerAwaits); + while( ndp<3 ) dynPrompt.dynamicPrompt[ndp++] = ' '; + strncpy(dynPrompt.dynamicPrompt+3, continuePrompt+3, + PROMPT_LEN_MAX-4); + }else{ + if( dynPrompt.inParenLevel>9 ){ + strncpy(dynPrompt.dynamicPrompt, "(..", 4); + }else if( dynPrompt.inParenLevel<0 ){ + strncpy(dynPrompt.dynamicPrompt, ")x!", 4); + }else{ + strncpy(dynPrompt.dynamicPrompt, "(x.", 4); + dynPrompt.dynamicPrompt[2] = (char)('0'+dynPrompt.inParenLevel); + } + strncpy(dynPrompt.dynamicPrompt+3, continuePrompt+3, PROMPT_LEN_MAX-4); + } + } + return dynPrompt.dynamicPrompt; +} +#endif /* !defined(SQLITE_OMIT_DYNAPROMPT) */ /* ** Render output like fprintf(). Except, if the output is going to the @@ -729,7 +816,7 @@ static char *one_input_line(FILE *in, char *zPrior, int isContinuation){ if( in!=0 ){ zResult = local_getline(zPrior, in); }else{ - zPrompt = isContinuation ? continuePrompt : mainPrompt; + zPrompt = isContinuation ? CONTINUATION_PROMPT : mainPrompt; #if SHELL_USE_LOCAL_GETLINE printf("%s", zPrompt); fflush(stdout); @@ -1066,9 +1153,14 @@ INCLUDE ../ext/expert/sqlite3expert.c #define SQLITE_SHELL_HAVE_RECOVER 0 #endif #if SQLITE_SHELL_HAVE_RECOVER -INCLUDE ../ext/recover/dbdata.c INCLUDE ../ext/recover/sqlite3recover.h +# ifndef SQLITE_HAVE_SQLITE3R +INCLUDE ../ext/recover/dbdata.c INCLUDE ../ext/recover/sqlite3recover.c +# endif +#endif +#ifdef SQLITE_SHELL_EXTSRC +# include SHELL_STRINGIFY(SQLITE_SHELL_EXTSRC) #endif #if defined(SQLITE_ENABLE_SESSION) @@ -1875,7 +1967,7 @@ static int safeModeAuth( "zipfile", "zipfile_cds", }; - UNUSED_PARAMETER(zA2); + UNUSED_PARAMETER(zA1); UNUSED_PARAMETER(zA3); UNUSED_PARAMETER(zA4); switch( op ){ @@ -1890,7 +1982,7 @@ static int safeModeAuth( case SQLITE_FUNCTION: { int i; for(i=0; isGraph.pRow; if( pRow ){ if( pRow->zText[0]=='-' ){ @@ -2088,6 +2180,8 @@ static void eqp_render(ShellState *p){ utf8_printf(p->out, "%s\n", pRow->zText+3); p->sGraph.pRow = pRow->pNext; sqlite3_free(pRow); + }else if( nCycle>0 ){ + utf8_printf(p->out, "QUERY PLAN (cycles=%lld [100%%])\n", nCycle); }else{ utf8_printf(p->out, "QUERY PLAN\n"); } @@ -2965,6 +3059,35 @@ static int display_stats( return 0; } + +#ifdef SQLITE_ENABLE_STMT_SCANSTATUS +static int scanStatsHeight(sqlite3_stmt *p, int iEntry){ + int iPid = 0; + int ret = 1; + sqlite3_stmt_scanstatus_v2(p, iEntry, + SQLITE_SCANSTAT_SELECTID, SQLITE_SCANSTAT_COMPLEX, (void*)&iPid + ); + while( iPid!=0 ){ + int ii; + for(ii=0; 1; ii++){ + int iId; + int res; + res = sqlite3_stmt_scanstatus_v2(p, ii, + SQLITE_SCANSTAT_SELECTID, SQLITE_SCANSTAT_COMPLEX, (void*)&iId + ); + if( res ) break; + if( iId==iPid ){ + sqlite3_stmt_scanstatus_v2(p, ii, + SQLITE_SCANSTAT_PARENTID, SQLITE_SCANSTAT_COMPLEX, (void*)&iPid + ); + } + } + ret++; + } + return ret; +} +#endif + /* ** Display scan stats. */ @@ -2976,40 +3099,77 @@ static void display_scanstats( UNUSED_PARAMETER(db); UNUSED_PARAMETER(pArg); #else - int i, k, n, mx; - raw_printf(pArg->out, "-------- scanstats --------\n"); - mx = 0; - for(k=0; k<=mx; k++){ - double rEstLoop = 1.0; - for(i=n=0; 1; i++){ - sqlite3_stmt *p = pArg->pStmt; - sqlite3_int64 nLoop, nVisit; - double rEst; - int iSid; - const char *zExplain; - if( sqlite3_stmt_scanstatus(p, i, SQLITE_SCANSTAT_NLOOP, (void*)&nLoop) ){ - break; + static const int f = SQLITE_SCANSTAT_COMPLEX; + sqlite3_stmt *p = pArg->pStmt; + int ii = 0; + i64 nTotal = 0; + int nWidth = 0; + eqp_reset(pArg); + + for(ii=0; 1; ii++){ + const char *z = 0; + int n = 0; + if( sqlite3_stmt_scanstatus_v2(p,ii,SQLITE_SCANSTAT_EXPLAIN,f,(void*)&z) ){ + break; + } + n = strlen(z) + scanStatsHeight(p, ii)*3; + if( n>nWidth ) nWidth = n; + } + nWidth += 4; + + sqlite3_stmt_scanstatus_v2(p, -1, SQLITE_SCANSTAT_NCYCLE, f, (void*)&nTotal); + for(ii=0; 1; ii++){ + i64 nLoop = 0; + i64 nRow = 0; + i64 nCycle = 0; + int iId = 0; + int iPid = 0; + const char *z = 0; + const char *zName = 0; + char *zText = 0; + double rEst = 0.0; + + if( sqlite3_stmt_scanstatus_v2(p,ii,SQLITE_SCANSTAT_EXPLAIN,f,(void*)&z) ){ + break; + } + sqlite3_stmt_scanstatus_v2(p, ii, SQLITE_SCANSTAT_EST,f,(void*)&rEst); + sqlite3_stmt_scanstatus_v2(p, ii, SQLITE_SCANSTAT_NLOOP,f,(void*)&nLoop); + sqlite3_stmt_scanstatus_v2(p, ii, SQLITE_SCANSTAT_NVISIT,f,(void*)&nRow); + sqlite3_stmt_scanstatus_v2(p, ii, SQLITE_SCANSTAT_NCYCLE,f,(void*)&nCycle); + sqlite3_stmt_scanstatus_v2(p, ii, SQLITE_SCANSTAT_SELECTID,f,(void*)&iId); + sqlite3_stmt_scanstatus_v2(p, ii, SQLITE_SCANSTAT_PARENTID,f,(void*)&iPid); + sqlite3_stmt_scanstatus_v2(p, ii, SQLITE_SCANSTAT_NAME,f,(void*)&zName); + + zText = sqlite3_mprintf("%s", z); + if( nCycle>=0 || nLoop>=0 || nRow>=0 ){ + char *z = 0; + if( nCycle>=0 && nTotal>0 ){ + z = sqlite3_mprintf("%zcycles=%lld [%d%%]", z, + nCycle, ((nCycle*100)+nTotal/2) / nTotal + ); } - sqlite3_stmt_scanstatus(p, i, SQLITE_SCANSTAT_SELECTID, (void*)&iSid); - if( iSid>mx ) mx = iSid; - if( iSid!=k ) continue; - if( n==0 ){ - rEstLoop = (double)nLoop; - if( k>0 ) raw_printf(pArg->out, "-------- subquery %d -------\n", k); + if( nLoop>=0 ){ + z = sqlite3_mprintf("%z%sloops=%lld", z, z ? " " : "", nLoop); } - n++; - sqlite3_stmt_scanstatus(p, i, SQLITE_SCANSTAT_NVISIT, (void*)&nVisit); - sqlite3_stmt_scanstatus(p, i, SQLITE_SCANSTAT_EST, (void*)&rEst); - sqlite3_stmt_scanstatus(p, i, SQLITE_SCANSTAT_EXPLAIN, (void*)&zExplain); - utf8_printf(pArg->out, "Loop %2d: %s\n", n, zExplain); - rEstLoop *= rEst; - raw_printf(pArg->out, - " nLoop=%-8lld nRow=%-8lld estRow=%-8lld estRow/Loop=%-8g\n", - nLoop, nVisit, (sqlite3_int64)(rEstLoop+0.5), rEst + if( nRow>=0 ){ + z = sqlite3_mprintf("%z%srows=%lld", z, z ? " " : "", nRow); + } + + if( zName && pArg->scanstatsOn>1 ){ + double rpl = (double)nRow / (double)nLoop; + z = sqlite3_mprintf("%z rpl=%.1f est=%.1f", z, rpl, rEst); + } + + zText = sqlite3_mprintf( + "% *z (%z)", -1*(nWidth-scanStatsHeight(p, ii)*3), zText, z ); } + + eqp_append(pArg, iId, iPid, zText); + sqlite3_free(zText); } - raw_printf(pArg->out, "---------------------------\n"); + + eqp_render(pArg, nTotal); #endif } @@ -3933,10 +4093,10 @@ static int shell_exec( int iEqpId = sqlite3_column_int(pExplain, 0); int iParentId = sqlite3_column_int(pExplain, 1); if( zEQPLine==0 ) zEQPLine = ""; - if( zEQPLine[0]=='-' ) eqp_render(pArg); + if( zEQPLine[0]=='-' ) eqp_render(pArg, 0); eqp_append(pArg, iEqpId, iParentId, zEQPLine); } - eqp_render(pArg); + eqp_render(pArg, 0); } sqlite3_finalize(pExplain); sqlite3_free(zEQP); @@ -3985,7 +4145,7 @@ static int shell_exec( bind_prepared_stmt(pArg, pStmt); exec_prepared_stmt(pArg, pStmt); explain_data_delete(pArg); - eqp_render(pArg); + eqp_render(pArg, 0); /* print usage stats if stats on */ if( pArg && pArg->statsOn ){ @@ -4525,7 +4685,7 @@ static const char *(azHelp[]) = { ".restore ?DB? FILE Restore content of DB (default \"main\") from FILE", ".save ?OPTIONS? FILE Write database to FILE (an alias for .backup ...)", #endif - ".scanstats on|off Turn sqlite3_stmt_scanstatus() metrics on or off", + ".scanstats on|off|est Turn sqlite3_stmt_scanstatus() metrics on or off", ".schema ?PATTERN? Show the CREATE statements matching PATTERN", " Options:", " --indent Try to pretty-print the schema", @@ -5115,6 +5275,7 @@ static void open_db(ShellState *p, int openFlags){ } exit(1); } + #ifndef SQLITE_OMIT_LOAD_EXTENSION sqlite3_enable_load_extension(p->db, 1); #endif @@ -5137,6 +5298,34 @@ static void open_db(ShellState *p, int openFlags){ sqlite3_sqlar_init(p->db, 0, 0); } #endif +#ifdef SQLITE_SHELL_EXTFUNCS + /* Create a preprocessing mechanism for extensions to make + * their own provisions for being built into the shell. + * This is a short-span macro. See further below for usage. + */ +#define SHELL_SUB_MACRO(base, variant) base ## _ ## variant +#define SHELL_SUBMACRO(base, variant) SHELL_SUB_MACRO(base, variant) + /* Let custom-included extensions get their ..._init() called. + * The WHATEVER_INIT( db, pzErrorMsg, pApi ) macro should cause + * the extension's sqlite3_*_init( db, pzErrorMsg, pApi ) + * inititialization routine to be called. + */ + { + int irc = SHELL_SUBMACRO(SQLITE_SHELL_EXTFUNCS, INIT)(p->db); + /* Let custom-included extensions expose their functionality. + * The WHATEVER_EXPOSE( db, pzErrorMsg ) macro should cause + * the SQL functions, virtual tables, collating sequences or + * VFS's implemented by the extension to be registered. + */ + if( irc==SQLITE_OK + || irc==SQLITE_OK_LOAD_PERMANENTLY ){ + SHELL_SUBMACRO(SQLITE_SHELL_EXTFUNCS, EXPOSE)(p->db, 0); + } +#undef SHELL_SUB_MACRO +#undef SHELL_SUBMACRO + } +#endif + sqlite3_create_function(p->db, "shell_add_schema", 3, SQLITE_UTF8, 0, shellAddSchemaName, 0, 0); sqlite3_create_function(p->db, "shell_module_schema", 1, SQLITE_UTF8, 0, @@ -5157,6 +5346,7 @@ static void open_db(ShellState *p, int openFlags){ sqlite3_create_function(p->db, "edit", 2, SQLITE_UTF8, 0, editFunc, 0, 0); #endif + if( p->openMode==SHELL_OPEN_ZIPFILE ){ char *zSql = sqlite3_mprintf( "CREATE VIRTUAL TABLE zip USING zipfile(%Q);", zDbFilename); @@ -9524,12 +9714,16 @@ static int do_meta_command(char *zLine, ShellState *p){ if( c=='s' && cli_strncmp(azArg[0], "scanstats", n)==0 ){ if( nArg==2 ){ - p->scanstatsOn = (u8)booleanValue(azArg[1]); + if( cli_strcmp(azArg[1], "est")==0 ){ + p->scanstatsOn = 2; + }else{ + p->scanstatsOn = (u8)booleanValue(azArg[1]); + } #ifndef SQLITE_ENABLE_STMT_SCANSTATUS raw_printf(stderr, "Warning: .scanstats not available in this build.\n"); #endif }else{ - raw_printf(stderr, "Usage: .scanstats on|off\n"); + raw_printf(stderr, "Usage: .scanstats on|off|est\n"); rc = 1; } }else @@ -10079,12 +10273,12 @@ static int do_meta_command(char *zLine, ShellState *p){ } } if( bSchema ){ - zSql = "SELECT lower(name) FROM sqlite_schema" + zSql = "SELECT lower(name) as tname FROM sqlite_schema" " WHERE type='table' AND coalesce(rootpage,0)>1" " UNION ALL SELECT 'sqlite_schema'" " ORDER BY 1 collate nocase"; }else{ - zSql = "SELECT lower(name) FROM sqlite_schema" + zSql = "SELECT lower(name) as tname FROM sqlite_schema" " WHERE type='table' AND coalesce(rootpage,0)>1" " AND name NOT LIKE 'sqlite_%'" " ORDER BY 1 collate nocase"; @@ -10145,6 +10339,58 @@ static int do_meta_command(char *zLine, ShellState *p){ }else{ shell_exec(p, zSql, 0); } +#if !defined(SQLITE_OMIT_SCHEMA_PRAGMAS) && !defined(SQLITE_OMIT_VIRTUALTABLE) + { + int lrc; + char *zRevText = /* Query for reversible to-blob-to-text check */ + "SELECT lower(name) as tname FROM sqlite_schema\n" + "WHERE type='table' AND coalesce(rootpage,0)>1\n" + "AND name NOT LIKE 'sqlite_%%'%s\n" + "ORDER BY 1 collate nocase"; + zRevText = sqlite3_mprintf(zRevText, zLike? " AND name LIKE $tspec" : ""); + zRevText = sqlite3_mprintf( + /* lower-case query is first run, producing upper-case query. */ + "with tabcols as materialized(\n" + "select tname, cname\n" + "from (" + " select ss.tname as tname, ti.name as cname\n" + " from (%z) ss\n inner join pragma_table_info(tname) ti))\n" + "select 'SELECT total(bad_text_count) AS bad_text_count\n" + "FROM ('||group_concat(query, ' UNION ALL ')||')' as btc_query\n" + " from (select 'SELECT COUNT(*) AS bad_text_count\n" + "FROM '||tname||' WHERE '\n" + "||group_concat('CAST(CAST('||cname||' AS BLOB) AS TEXT)<>'||cname\n" + "|| ' AND typeof('||cname||')=''text'' ',\n" + "' OR ') as query, tname from tabcols group by tname)" + , zRevText); + shell_check_oom(zRevText); + if( bDebug ) utf8_printf(p->out, "%s\n", zRevText); + lrc = sqlite3_prepare_v2(p->db, zRevText, -1, &pStmt, 0); + assert(lrc==SQLITE_OK); + if( zLike ) sqlite3_bind_text(pStmt,1,zLike,-1,SQLITE_STATIC); + lrc = SQLITE_ROW==sqlite3_step(pStmt); + if( lrc ){ + const char *zGenQuery = (char*)sqlite3_column_text(pStmt,0); + sqlite3_stmt *pCheckStmt; + lrc = sqlite3_prepare_v2(p->db, zGenQuery, -1, &pCheckStmt, 0); + if( bDebug ) utf8_printf(p->out, "%s\n", zGenQuery); + if( SQLITE_OK==lrc ){ + if( SQLITE_ROW==sqlite3_step(pCheckStmt) ){ + double countIrreversible = sqlite3_column_double(pCheckStmt, 0); + if( countIrreversible>0 ){ + int sz = (int)(countIrreversible + 0.5); + utf8_printf(stderr, + "Digest includes %d invalidly encoded text field%s.\n", + sz, (sz>1)? "s": ""); + } + } + sqlite3_finalize(pCheckStmt); + } + sqlite3_finalize(pStmt); + } + sqlite3_free(zRevText); + } +#endif /* !defined(*_OMIT_SCHEMA_PRAGMAS) && !defined(*_OMIT_VIRTUALTABLE) */ sqlite3_free(zSql); }else @@ -10656,7 +10902,7 @@ static int do_meta_command(char *zLine, ShellState *p){ } }else{ output_file_close(p->traceOut); - p->traceOut = output_file_open(azArg[1], 0); + p->traceOut = output_file_open(z, 0); } } if( p->traceOut==0 ){ @@ -10872,7 +11118,8 @@ typedef enum { ** The scan is resumable for subsequent lines when prior ** return values are passed as the 2nd argument. */ -static QuickScanState quickscan(char *zLine, QuickScanState qss){ +static QuickScanState quickscan(char *zLine, QuickScanState qss, + SCAN_TRACKER_REFTYPE pst){ char cin; char cWait = (char)qss; /* intentional narrowing loss */ if( cWait==0 ){ @@ -10896,6 +11143,7 @@ static QuickScanState quickscan(char *zLine, QuickScanState qss){ if( *zLine=='*' ){ ++zLine; cWait = '*'; + CONTINUE_PROMPT_AWAITS(pst, "/*"); qss = QSS_SETV(qss, cWait); goto TermScan; } @@ -10906,7 +11154,14 @@ static QuickScanState quickscan(char *zLine, QuickScanState qss){ case '`': case '\'': case '"': cWait = cin; qss = QSS_HasDark | cWait; + CONTINUE_PROMPT_AWAITC(pst, cin); goto TermScan; + case '(': + CONTINUE_PAREN_INCR(pst, 1); + break; + case ')': + CONTINUE_PAREN_INCR(pst, -1); + break; default: break; } @@ -10922,16 +11177,19 @@ static QuickScanState quickscan(char *zLine, QuickScanState qss){ continue; ++zLine; cWait = 0; + CONTINUE_PROMPT_AWAITC(pst, 0); qss = QSS_SETV(qss, 0); goto PlainScan; case '`': case '\'': case '"': if(*zLine==cWait){ + /* Swallow doubled end-delimiter.*/ ++zLine; continue; } /* fall thru */ case ']': cWait = 0; + CONTINUE_PROMPT_AWAITC(pst, 0); qss = QSS_SETV(qss, 0); goto PlainScan; default: assert(0); @@ -10955,17 +11213,15 @@ static int line_is_command_terminator(char *zLine){ zLine += 2; /* SQL Server */ else return 0; - return quickscan(zLine, QSS_Start)==QSS_Start; + return quickscan(zLine, QSS_Start, 0)==QSS_Start; } /* -** We need a default sqlite3_complete() implementation to use in case -** the shell is compiled with SQLITE_OMIT_COMPLETE. The default assumes -** any arbitrary text is a complete SQL statement. This is not very -** user-friendly, but it does seem to work. +** The CLI needs a working sqlite3_complete() to work properly. So error +** out of the build if compiling with SQLITE_OMIT_COMPLETE. */ #ifdef SQLITE_OMIT_COMPLETE -#define sqlite3_complete(x) 1 +# error the CLI application is imcompatable with SQLITE_OMIT_COMPLETE. #endif /* @@ -11097,6 +11353,7 @@ static int process_input(ShellState *p){ } ++p->inputNesting; p->lineno = 0; + CONTINUE_PROMPT_RESET; while( errCnt==0 || !bail_on_error || (p->in==0 && stdin_is_interactive) ){ fflush(p->out); zLine = one_input_line(p->in, zLine, nSql>0); @@ -11115,7 +11372,7 @@ static int process_input(ShellState *p){ && line_is_complete(zSql, nSql) ){ memcpy(zLine,";",2); } - qss = quickscan(zLine, qss); + qss = quickscan(zLine, qss, CONTINUE_PROMPT_PSTATE); if( QSS_PLAINWHITE(qss) && nSql==0 ){ /* Just swallow single-line whitespace */ echo_group_input(p, zLine); @@ -11123,6 +11380,7 @@ static int process_input(ShellState *p){ continue; } if( zLine && (zLine[0]=='.' || zLine[0]=='#') && nSql==0 ){ + CONTINUE_PROMPT_RESET; echo_group_input(p, zLine); if( zLine[0]=='.' ){ rc = do_meta_command(zLine, p); @@ -11158,6 +11416,7 @@ static int process_input(ShellState *p){ if( nSql && QSS_SEMITERM(qss) && sqlite3_complete(zSql) ){ echo_group_input(p, zSql); errCnt += runOneSqlLine(p, zSql, p->in, startline); + CONTINUE_PROMPT_RESET; nSql = 0; if( p->outCount ){ output_reset(p); @@ -11177,6 +11436,7 @@ static int process_input(ShellState *p){ /* This may be incomplete. Let the SQL parser deal with that. */ echo_group_input(p, zSql); errCnt += runOneSqlLine(p, zSql, p->in, startline); + CONTINUE_PROMPT_RESET; } free(zSql); free(zLine); @@ -11253,9 +11513,43 @@ static char *find_home_dir(int clearFlag){ return home_dir; } +/* +** On non-Windows platforms, look for $XDG_CONFIG_HOME. +** If ${XDG_CONFIG_HOME}/sqlite3/sqliterc is found, return +** the path to it, else return 0. The result is cached for +** subsequent calls. +*/ +static const char *find_xdg_config(void){ +#if defined(_WIN32) || defined(WIN32) || defined(_WIN32_WCE) \ + || defined(__RTP__) || defined(_WRS_KERNEL) + return 0; +#else + static int alreadyTried = 0; + static char *zConfig = 0; + const char *zXdgHome; + + if( alreadyTried!=0 ){ + return zConfig; + } + alreadyTried = 1; + zXdgHome = getenv("XDG_CONFIG_HOME"); + if( zXdgHome==0 ){ + return 0; + } + zConfig = sqlite3_mprintf("%s/sqlite3/sqliterc", zXdgHome); + shell_check_oom(zConfig); + if( access(zConfig,0)!=0 ){ + sqlite3_free(zConfig); + zConfig = 0; + } + return zConfig; +#endif +} + /* ** Read input from the file given by sqliterc_override. Or if that -** parameter is NULL, take input from ~/.sqliterc +** parameter is NULL, take input from the first of find_xdg_config() +** or ~/.sqliterc which is found. ** ** Returns the number of errors. */ @@ -11269,7 +11563,10 @@ static void process_sqliterc( FILE *inSaved = p->in; int savedLineno = p->lineno; - if (sqliterc == NULL) { + if( sqliterc == NULL ){ + sqliterc = find_xdg_config(); + } + if( sqliterc == NULL ){ home_dir = find_home_dir(0); if( home_dir==0 ){ raw_printf(stderr, "-- warning: cannot find home directory;" @@ -11732,7 +12029,7 @@ int SQLITE_CDECL wmain(int argc, wchar_t **wargv){ if( pVfs ){ sqlite3_vfs_register(pVfs, 1); }else{ - utf8_printf(stderr, "no such VFS: \"%s\"\n", argv[i]); + utf8_printf(stderr, "no such VFS: \"%s\"\n", zVfs); exit(1); } } diff --git a/src/sqlite.h.in b/src/sqlite.h.in index 3812c0843b..aeacffa3bb 100644 --- a/src/sqlite.h.in +++ b/src/sqlite.h.in @@ -563,6 +563,7 @@ int sqlite3_exec( #define SQLITE_CONSTRAINT_DATATYPE (SQLITE_CONSTRAINT |(12<<8)) #define SQLITE_NOTICE_RECOVER_WAL (SQLITE_NOTICE | (1<<8)) #define SQLITE_NOTICE_RECOVER_ROLLBACK (SQLITE_NOTICE | (2<<8)) +#define SQLITE_NOTICE_RBU (SQLITE_NOTICE | (3<<8)) #define SQLITE_WARNING_AUTOINDEX (SQLITE_WARNING | (1<<8)) #define SQLITE_AUTH_USER (SQLITE_AUTH | (1<<8)) #define SQLITE_OK_LOAD_PERMANENTLY (SQLITE_OK | (1<<8)) @@ -1192,6 +1193,12 @@ struct sqlite3_io_methods { ** **
  • [[SQLITE_FCNTL_CKSM_FILE]] ** Used by the cksmvfs VFS module only. +** +**
  • [[SQLITE_FCNTL_RESET_CACHE]] +** If there is currently no transaction open on the database, and the +** database is not a temp db, then this file-control purges the contents +** of the in-memory page cache. If there is an open transaction, or if +** the db is a temp-db, it is a no-op, not an error. ** */ #define SQLITE_FCNTL_LOCKSTATE 1 @@ -1234,6 +1241,7 @@ struct sqlite3_io_methods { #define SQLITE_FCNTL_CKPT_START 39 #define SQLITE_FCNTL_EXTERNAL_READER 40 #define SQLITE_FCNTL_CKSM_FILE 41 +#define SQLITE_FCNTL_RESET_CACHE 42 /* deprecated names */ #define SQLITE_GET_LOCKPROXYFILE SQLITE_FCNTL_GET_LOCKPROXYFILE @@ -2177,7 +2185,7 @@ struct sqlite3_mem_methods { ** configuration for a database connection can only be changed when that ** connection is not currently using lookaside memory, or in other words ** when the "current value" returned by -** [sqlite3_db_status](D,[SQLITE_CONFIG_LOOKASIDE],...) is zero. +** [sqlite3_db_status](D,[SQLITE_DBSTATUS_LOOKASIDE_USED],...) is zero. ** Any attempt to change the lookaside memory configuration when lookaside ** memory is in use leaves the configuration unchanged and returns ** [SQLITE_BUSY].)^ @@ -2327,8 +2335,12 @@ struct sqlite3_mem_methods { **
  • sqlite3_db_config(db, SQLITE_DBCONFIG_RESET_DATABASE, 0, 0); ** ** Because resetting a database is destructive and irreversible, the -** process requires the use of this obscure API and multiple steps to help -** ensure that it does not happen by accident. +** process requires the use of this obscure API and multiple steps to +** help ensure that it does not happen by accident. Because this +** feature must be capable of resetting corrupt databases, and +** shutting down virtual tables may require access to that corrupt +** storage, the library must abandon any installed virtual tables +** without calling their xDestroy() methods. ** ** [[SQLITE_DBCONFIG_DEFENSIVE]]
    SQLITE_DBCONFIG_DEFENSIVE
    **
    The SQLITE_DBCONFIG_DEFENSIVE option activates or deactivates the @@ -5542,16 +5554,6 @@ SQLITE_DEPRECATED int sqlite3_memory_alarm(void(*)(void*,sqlite3_int64,int), ** then the conversion is performed. Otherwise no conversion occurs. ** The [SQLITE_INTEGER | datatype] after conversion is returned.)^ ** -** ^(The sqlite3_value_encoding(X) interface returns one of [SQLITE_UTF8], -** [SQLITE_UTF16BE], or [SQLITE_UTF16LE] according to the current encoding -** of the value X, assuming that X has type TEXT.)^ If sqlite3_value_type(X) -** returns something other than SQLITE_TEXT, then the return value from -** sqlite3_value_encoding(X) is meaningless. ^Calls to -** sqlite3_value_text(X), sqlite3_value_text16(X), sqlite3_value_text16be(X), -** sqlite3_value_text16le(X), sqlite3_value_bytes(X), or -** sqlite3_value_bytes16(X) might change the encoding of the value X and -** thus change the return from subsequent calls to sqlite3_value_encoding(X). -** ** ^Within the [xUpdate] method of a [virtual table], the ** sqlite3_value_nochange(X) interface returns true if and only if ** the column corresponding to X is unchanged by the UPDATE operation @@ -5616,6 +5618,27 @@ int sqlite3_value_type(sqlite3_value*); int sqlite3_value_numeric_type(sqlite3_value*); int sqlite3_value_nochange(sqlite3_value*); int sqlite3_value_frombind(sqlite3_value*); + +/* +** CAPI3REF: Report the internal text encoding state of an sqlite3_value object +** METHOD: sqlite3_value +** +** ^(The sqlite3_value_encoding(X) interface returns one of [SQLITE_UTF8], +** [SQLITE_UTF16BE], or [SQLITE_UTF16LE] according to the current text encoding +** of the value X, assuming that X has type TEXT.)^ If sqlite3_value_type(X) +** returns something other than SQLITE_TEXT, then the return value from +** sqlite3_value_encoding(X) is meaningless. ^Calls to +** [sqlite3_value_text(X)], [sqlite3_value_text16(X)], [sqlite3_value_text16be(X)], +** [sqlite3_value_text16le(X)], [sqlite3_value_bytes(X)], or +** [sqlite3_value_bytes16(X)] might change the encoding of the value X and +** thus change the return from subsequent calls to sqlite3_value_encoding(X). +** +** This routine is intended for used by applications that test and validate +** the SQLite implementation. This routine is inquiring about the opaque +** internal state of an [sqlite3_value] object. Ordinary applications should +** not need to know what the internal state of an sqlite3_value object is and +** hence should not need to use this interface. +*/ int sqlite3_value_encoding(sqlite3_value*); /* @@ -6996,15 +7019,6 @@ int sqlite3_cancel_auto_extension(void(*xEntryPoint)(void)); */ void sqlite3_reset_auto_extension(void); -/* -** The interface to the virtual-table mechanism is currently considered -** to be experimental. The interface might change in incompatible ways. -** If this is a problem for you, do not use the interface at this time. -** -** When the virtual-table mechanism stabilizes, we will declare the -** interface fixed, support it indefinitely, and remove this comment. -*/ - /* ** Structures used by the virtual table interface */ @@ -7246,7 +7260,7 @@ struct sqlite3_index_info { ** the [sqlite3_vtab_collation()] interface. For most real-world virtual ** tables, the collating sequence of constraints does not matter (for example ** because the constraints are numeric) and so the sqlite3_vtab_collation() -** interface is no commonly needed. +** interface is not commonly needed. */ #define SQLITE_INDEX_CONSTRAINT_EQ 2 #define SQLITE_INDEX_CONSTRAINT_GT 4 @@ -7405,16 +7419,6 @@ int sqlite3_declare_vtab(sqlite3*, const char *zSQL); */ int sqlite3_overload_function(sqlite3*, const char *zFuncName, int nArg); -/* -** The interface to the virtual-table mechanism defined above (back up -** to a comment remarkably similar to this one) is currently considered -** to be experimental. The interface might change in incompatible ways. -** If this is a problem for you, do not use the interface at this time. -** -** When the virtual-table mechanism stabilizes, we will declare the -** interface fixed, support it indefinitely, and remove this comment. -*/ - /* ** CAPI3REF: A Handle To An Open BLOB ** KEYWORDS: {BLOB handle} {BLOB handles} @@ -9618,7 +9622,7 @@ int sqlite3_vtab_nochange(sqlite3_context*); **
  • Otherwise, "BINARY" is returned. ** */ -SQLITE_EXPERIMENTAL const char *sqlite3_vtab_collation(sqlite3_index_info*,int); +const char *sqlite3_vtab_collation(sqlite3_index_info*,int); /* ** CAPI3REF: Determine if a virtual table query is DISTINCT @@ -9887,6 +9891,10 @@ int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **ppVal); ** managed by the prepared statement S and will be automatically freed when ** S is finalized. ** +** Not all values are available for all query elements. When a value is +** not available, the output variable is set to -1 if the value is numeric, +** or to NULL if it is a string (SQLITE_SCANSTAT_NAME). +** **

    ** [[SQLITE_SCANSTAT_NLOOP]]
    SQLITE_SCANSTAT_NLOOP
    **
    ^The [sqlite3_int64] variable pointed to by the V parameter will be @@ -9914,13 +9922,25 @@ int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **ppVal); ** to a zero-terminated UTF-8 string containing the [EXPLAIN QUERY PLAN] ** description for the X-th loop. ** -** [[SQLITE_SCANSTAT_SELECTID]]
    SQLITE_SCANSTAT_SELECT
    +** [[SQLITE_SCANSTAT_SELECTID]]
    SQLITE_SCANSTAT_SELECTID
    **
    ^The "int" variable pointed to by the V parameter will be set to the -** "select-id" for the X-th loop. The select-id identifies which query or -** subquery the loop is part of. The main query has a select-id of zero. -** The select-id is the same value as is output in the first column -** of an [EXPLAIN QUERY PLAN] query. +** id for the X-th query plan element. The id value is unique within the +** statement. The select-id is the same value as is output in the first +** column of an [EXPLAIN QUERY PLAN] query. **
    +** +** [[SQLITE_SCANSTAT_PARENTID]]
    SQLITE_SCANSTAT_PARENTID
    +**
    The "int" variable pointed to by the V parameter will be set to the +** the id of the parent of the current query element, if applicable, or +** to zero if the query element has no parent. This is the same value as +** returned in the second column of an [EXPLAIN QUERY PLAN] query. +** +** [[SQLITE_SCANSTAT_NCYCLE]]
    SQLITE_SCANSTAT_NCYCLE
    +**
    The sqlite3_int64 output value is set to the number of cycles, +** according to the processor time-stamp counter, that elapsed while the +** query element was being processed. This value is not available for +** all query elements - if it is unavailable the output variable is +** set to -1. */ #define SQLITE_SCANSTAT_NLOOP 0 #define SQLITE_SCANSTAT_NVISIT 1 @@ -9928,12 +9948,14 @@ int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **ppVal); #define SQLITE_SCANSTAT_NAME 3 #define SQLITE_SCANSTAT_EXPLAIN 4 #define SQLITE_SCANSTAT_SELECTID 5 +#define SQLITE_SCANSTAT_PARENTID 6 +#define SQLITE_SCANSTAT_NCYCLE 7 /* ** CAPI3REF: Prepared Statement Scan Status ** METHOD: sqlite3_stmt ** -** This interface returns information about the predicted and measured +** These interfaces return information about the predicted and measured ** performance for pStmt. Advanced applications can use this ** interface to compare the predicted and the measured performance and ** issue warnings and/or rerun [ANALYZE] if discrepancies are found. @@ -9944,19 +9966,25 @@ int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **ppVal); ** ** The "iScanStatusOp" parameter determines which status information to return. ** The "iScanStatusOp" must be one of the [scanstatus options] or the behavior -** of this interface is undefined. -** ^The requested measurement is written into a variable pointed to by -** the "pOut" parameter. -** Parameter "idx" identifies the specific loop to retrieve statistics for. -** Loops are numbered starting from zero. ^If idx is out of range - less than -** zero or greater than or equal to the total number of loops used to implement -** the statement - a non-zero value is returned and the variable that pOut -** points to is unchanged. +** of this interface is undefined. ^The requested measurement is written into +** a variable pointed to by the "pOut" parameter. ** -** ^Statistics might not be available for all loops in all statements. ^In cases -** where there exist loops with no available statistics, this function behaves -** as if the loop did not exist - it returns non-zero and leave the variable -** that pOut points to unchanged. +** The "flags" parameter must be passed a mask of flags. At present only +** one flag is defined - SQLITE_SCANSTAT_COMPLEX. If SQLITE_SCANSTAT_COMPLEX +** is specified, then status information is available for all elements +** of a query plan that are reported by "EXPLAIN QUERY PLAN" output. If +** SQLITE_SCANSTAT_COMPLEX is not specified, then only query plan elements +** that correspond to query loops (the "SCAN..." and "SEARCH..." elements of +** the EXPLAIN QUERY PLAN output) are available. Invoking API +** sqlite3_stmt_scanstatus() is equivalent to calling +** sqlite3_stmt_scanstatus_v2() with a zeroed flags parameter. +** +** Parameter "idx" identifies the specific query element to retrieve statistics +** for. Query elements are numbered starting from zero. A value of -1 may be +** to query for statistics regarding the entire query. ^If idx is out of range +** - less than -1 or greater than or equal to the total number of query +** elements used to implement the statement - a non-zero value is returned and +** the variable that pOut points to is unchanged. ** ** See also: [sqlite3_stmt_scanstatus_reset()] */ @@ -9966,6 +9994,19 @@ int sqlite3_stmt_scanstatus( int iScanStatusOp, /* Information desired. SQLITE_SCANSTAT_* */ void *pOut /* Result written here */ ); +int sqlite3_stmt_scanstatus_v2( + sqlite3_stmt *pStmt, /* Prepared statement for which info desired */ + int idx, /* Index of loop to report on */ + int iScanStatusOp, /* Information desired. SQLITE_SCANSTAT_* */ + int flags, /* Mask of flags defined below */ + void *pOut /* Result written here */ +); + +/* +** CAPI3REF: Prepared Statement Scan Status +** KEYWORDS: {scan status flags} +*/ +#define SQLITE_SCANSTAT_COMPLEX 0x0001 /* ** CAPI3REF: Zero Scan-Status Counters diff --git a/src/sqliteInt.h b/src/sqliteInt.h index b7a03d262f..fb37f5252a 100644 --- a/src/sqliteInt.h +++ b/src/sqliteInt.h @@ -1029,15 +1029,38 @@ extern u32 sqlite3TreeTrace; && (defined(SQLITE_TEST) || defined(SQLITE_ENABLE_SELECTTRACE) \ || defined(SQLITE_ENABLE_TREETRACE)) # define TREETRACE_ENABLED 1 -# define SELECTTRACE(K,P,S,X) \ +# define TREETRACE(K,P,S,X) \ if(sqlite3TreeTrace&(K)) \ sqlite3DebugPrintf("%u/%d/%p: ",(S)->selId,(P)->addrExplain,(S)),\ sqlite3DebugPrintf X #else -# define SELECTTRACE(K,P,S,X) +# define TREETRACE(K,P,S,X) # define TREETRACE_ENABLED 0 #endif +/* TREETRACE flag meanings: +** +** 0x00000001 Beginning and end of SELECT processing +** 0x00000002 WHERE clause processing +** 0x00000004 Query flattener +** 0x00000008 Result-set wildcard expansion +** 0x00000010 Query name resolution +** 0x00000020 Aggregate analysis +** 0x00000040 Window functions +** 0x00000080 Generated column names +** 0x00000100 Move HAVING terms into WHERE +** 0x00000200 Count-of-view optimization +** 0x00000400 Compound SELECT processing +** 0x00000800 Drop superfluous ORDER BY +** 0x00001000 LEFT JOIN simplifies to JOIN +** 0x00002000 Constant propagation +** 0x00004000 Push-down optimization +** 0x00008000 After all FROM-clause analysis +** 0x00010000 Beginning of DELETE/INSERT/UPDATE processing +** 0x00020000 Transform DISTINCT into GROUP BY +** 0x00040000 SELECT tree dump after all code has been generated +*/ + /* ** Macros for "wheretrace" */ @@ -1050,6 +1073,36 @@ extern u32 sqlite3WhereTrace; # define WHERETRACE(K,X) #endif +/* +** Bits for the sqlite3WhereTrace mask: +** +** (---any--) Top-level block structure +** 0x-------F High-level debug messages +** 0x----FFF- More detail +** 0xFFFF---- Low-level debug messages +** +** 0x00000001 Code generation +** 0x00000002 Solver +** 0x00000004 Solver costs +** 0x00000008 WhereLoop inserts +** +** 0x00000010 Display sqlite3_index_info xBestIndex calls +** 0x00000020 Range an equality scan metrics +** 0x00000040 IN operator decisions +** 0x00000080 WhereLoop cost adjustements +** 0x00000100 +** 0x00000200 Covering index decisions +** 0x00000400 OR optimization +** 0x00000800 Index scanner +** 0x00001000 More details associated with code generation +** 0x00002000 +** 0x00004000 Show all WHERE terms at key points +** 0x00008000 Show the full SELECT statement at key places +** +** 0x00010000 Show more detail when printing WHERE terms +** 0x00020000 Show WHERE terms returned from whereScanNext() +*/ + /* ** An instance of the following structure is used to store the busy-handler @@ -1812,6 +1865,7 @@ struct sqlite3 { #define SQLITE_FlttnUnionAll 0x00800000 /* Disable the UNION ALL flattener */ /* TH3 expects this value ^^^^^^^^^^ See flatten04.test */ #define SQLITE_IndexedExpr 0x01000000 /* Pull exprs from index when able */ +#define SQLITE_Coroutines 0x02000000 /* Co-routines for subqueries */ #define SQLITE_AllOpts 0xffffffff /* All optimizations */ /* @@ -2206,6 +2260,7 @@ struct CollSeq { #define SQLITE_AFF_NUMERIC 0x43 /* 'C' */ #define SQLITE_AFF_INTEGER 0x44 /* 'D' */ #define SQLITE_AFF_REAL 0x45 /* 'E' */ +#define SQLITE_AFF_FLEXNUM 0x46 /* 'F' */ #define sqlite3IsNumericAffinity(X) ((X)>=SQLITE_AFF_NUMERIC) @@ -2737,16 +2792,15 @@ struct AggInfo { ** from source tables rather than from accumulators */ u8 useSortingIdx; /* In direct mode, reference the sorting index rather ** than the source table */ + u16 nSortingColumn; /* Number of columns in the sorting index */ int sortingIdx; /* Cursor number of the sorting index */ int sortingIdxPTab; /* Cursor number of pseudo-table */ - int nSortingColumn; /* Number of columns in the sorting index */ - int mnReg, mxReg; /* Range of registers allocated for aCol and aFunc */ + int iFirstReg; /* First register in range for aCol[] and aFunc[] */ ExprList *pGroupBy; /* The group by clause */ struct AggInfo_col { /* For each column used in source tables */ Table *pTab; /* Source table */ Expr *pCExpr; /* The original expression */ int iTable; /* Cursor number of the source table */ - int iMem; /* Memory location that acts as accumulator */ i16 iColumn; /* Column number within the source table */ i16 iSorterColumn; /* Column number in the sorting index */ } *aCol; @@ -2757,14 +2811,27 @@ struct AggInfo { struct AggInfo_func { /* For each aggregate function */ Expr *pFExpr; /* Expression encoding the function */ FuncDef *pFunc; /* The aggregate function implementation */ - int iMem; /* Memory location that acts as accumulator */ int iDistinct; /* Ephemeral table used to enforce DISTINCT */ int iDistAddr; /* Address of OP_OpenEphemeral */ } *aFunc; int nFunc; /* Number of entries in aFunc[] */ u32 selId; /* Select to which this AggInfo belongs */ +#ifdef SQLITE_DEBUG + Select *pSelect; /* SELECT statement that this AggInfo supports */ +#endif }; +/* +** Macros to compute aCol[] and aFunc[] register numbers. +** +** These macros should not be used prior to the call to +** assignAggregateRegisters() that computes the value of pAggInfo->iFirstReg. +** The assert()s that are part of this macro verify that constraint. +*/ +#define AggInfoColumnReg(A,I) (assert((A)->iFirstReg),(A)->iFirstReg+(I)) +#define AggInfoFuncReg(A,I) \ + (assert((A)->iFirstReg),(A)->iFirstReg+(A)->nColumn+(I)) + /* ** The datatype ynVar is a signed integer, either 16-bit or 32-bit. ** Usually it is 16-bits. But if SQLITE_MAX_VARIABLE_NUMBER is greater @@ -3424,6 +3491,7 @@ struct Select { #define SF_MultiPart 0x2000000 /* Has multiple incompatible PARTITIONs */ #define SF_CopyCte 0x4000000 /* SELECT statement is a copy of a CTE */ #define SF_OrderByReqd 0x8000000 /* The ORDER BY clause may not be omitted */ +#define SF_UpdateFrom 0x10000000 /* Query originates with UPDATE FROM */ /* True if S exists and has SF_NestedFrom */ #define IsNestedFrom(S) ((S)!=0 && ((S)->selFlags&SF_NestedFrom)!=0) @@ -3532,7 +3600,7 @@ struct SelectDest { int iSDParm2; /* A second parameter for the eDest disposal method */ int iSdst; /* Base register where results are written */ int nSdst; /* Number of registers allocated */ - char *zAffSdst; /* Affinity used for SRT_Set, SRT_Table, and similar */ + char *zAffSdst; /* Affinity used for SRT_Set */ ExprList *pOrderBy; /* Key columns for SRT_Queue and SRT_DistQueue */ }; @@ -3677,7 +3745,7 @@ struct Parse { int nLabelAlloc; /* Number of slots in aLabel */ int *aLabel; /* Space to hold the labels */ ExprList *pConstExpr;/* Constant expressions */ - IndexedExpr *pIdxExpr;/* List of expressions used by active indexes */ + IndexedExpr *pIdxEpr;/* List of expressions used by active indexes */ Token constraintName;/* Name of the constraint currently being parsed */ yDbMask writeMask; /* Start a write transaction on these databases */ yDbMask cookieMask; /* Bitmask of schema verified databases */ @@ -4637,7 +4705,7 @@ const char *sqlite3ColumnColl(Column*); void sqlite3DeleteColumnNames(sqlite3*,Table*); void sqlite3GenerateColumnNames(Parse *pParse, Select *pSelect); int sqlite3ColumnsFromExprList(Parse*,ExprList*,i16*,Column**); -void sqlite3SelectAddColumnTypeAndCollation(Parse*,Table*,Select*,char); +void sqlite3SubqueryColumnTypes(Parse*,Table*,Select*,char); Table *sqlite3ResultSetOfSelect(Parse*,Select*,char); void sqlite3OpenSchemaTable(Parse *, int); Index *sqlite3PrimaryKeyIndex(Table*); @@ -5010,6 +5078,7 @@ char sqlite3CompareAffinity(const Expr *pExpr, char aff2); int sqlite3IndexAffinityOk(const Expr *pExpr, char idx_affinity); char sqlite3TableColumnAffinity(const Table*,int); char sqlite3ExprAffinity(const Expr *pExpr); +int sqlite3ExprDataType(const Expr *pExpr); int sqlite3Atoi64(const char*, i64*, int, u8); int sqlite3DecOrHexToI64(const char*, i64*); void sqlite3ErrorWithMsg(sqlite3*, int, const char*,...); @@ -5026,6 +5095,9 @@ const char *sqlite3ErrName(int); #ifndef SQLITE_OMIT_DESERIALIZE int sqlite3MemdbInit(void); +int sqlite3IsMemdb(const sqlite3_vfs*); +#else +# define sqlite3IsMemdb(X) 0 #endif const char *sqlite3ErrStr(int); @@ -5165,7 +5237,7 @@ int sqlite3ApiExit(sqlite3 *db, int); int sqlite3OpenTempDatabase(Parse *); void sqlite3StrAccumInit(StrAccum*, sqlite3*, char*, int, int); -int sqlite3StrAccumEnlarge(StrAccum*, int); +int sqlite3StrAccumEnlarge(StrAccum*, i64); char *sqlite3StrAccumFinish(StrAccum*); void sqlite3StrAccumSetError(StrAccum*, u8); void sqlite3ResultStrAccum(sqlite3_context*,StrAccum*); @@ -5523,4 +5595,10 @@ const char **sqlite3CompileOptions(int *pnOpt); int sqlite3KvvfsInit(void); #endif +#if defined(VDBE_PROFILE) \ + || defined(SQLITE_PERFORMANCE_TRACE) \ + || defined(SQLITE_ENABLE_STMT_SCANSTATUS) +sqlite3_uint64 sqlite3Hwtime(void); +#endif + #endif /* SQLITEINT_H */ diff --git a/src/test1.c b/src/test1.c index ea99bccc8b..64f1ec99b1 100644 --- a/src/test1.c +++ b/src/test1.c @@ -2188,7 +2188,7 @@ static int SQLITE_TCLAPI test_stmt_status( #ifdef SQLITE_ENABLE_STMT_SCANSTATUS /* -** Usage: sqlite3_stmt_scanstatus STMT IDX +** Usage: sqlite3_stmt_scanstatus ?-flags FLAGS? STMT IDX */ static int SQLITE_TCLAPI test_stmt_scanstatus( void * clientData, @@ -2203,36 +2203,99 @@ static int SQLITE_TCLAPI test_stmt_scanstatus( const char *zExplain; sqlite3_int64 nLoop; sqlite3_int64 nVisit; + sqlite3_int64 nCycle; double rEst; int res; + int flags = 0; + int iSelectId = 0; + int iParentId = 0; - if( objc!=3 ){ - Tcl_WrongNumArgs(interp, 1, objv, "STMT IDX"); + if( objc==5 ){ + struct Flag { + const char *zFlag; + int flag; + } aTbl[] = { + {"complex", SQLITE_SCANSTAT_COMPLEX}, + {0, 0} + }; + + Tcl_Obj **aFlag = 0; + int nFlag = 0; + int ii; + + if( Tcl_ListObjGetElements(interp, objv[2], &nFlag, &aFlag) ){ + return TCL_ERROR; + } + for(ii=0; iiflags || pExpr->affExpr || pExpr->vvaFlags ){ + if( pExpr->flags || pExpr->affExpr || pExpr->vvaFlags || pExpr->pAggInfo ){ StrAccum x; sqlite3StrAccumInit(&x, 0, zFlgs, sizeof(zFlgs), 0); sqlite3_str_appendf(&x, " fg.af=%x.%c", @@ -504,6 +504,9 @@ void sqlite3TreeViewExpr(TreeView *pView, const Expr *pExpr, u8 moreToFollow){ if( ExprHasVVAProperty(pExpr, EP_Immutable) ){ sqlite3_str_appendf(&x, " IMMUTABLE"); } + if( pExpr->pAggInfo!=0 ){ + sqlite3_str_appendf(&x, " agg-column[%d]", pExpr->iAgg); + } sqlite3StrAccumFinish(&x); }else{ zFlgs[0] = 0; diff --git a/src/trigger.c b/src/trigger.c index ca64d6145b..02d8540237 100644 --- a/src/trigger.c +++ b/src/trigger.c @@ -61,7 +61,7 @@ Trigger *sqlite3TriggerList(Parse *pParse, Table *pTab){ if( pTrig->pTabSchema==pTab->pSchema && pTrig->table && 0==sqlite3StrICmp(pTrig->table, pTab->zName) - && pTrig->pTabSchema!=pTmpSchema + && (pTrig->pTabSchema!=pTmpSchema || pTrig->bReturning) ){ pTrig->pNext = pList; pList = pTrig; diff --git a/src/update.c b/src/update.c index b88373be8a..fb065b9ec4 100644 --- a/src/update.c +++ b/src/update.c @@ -263,7 +263,8 @@ static void updateFromSelect( } } pSelect = sqlite3SelectNew(pParse, pList, - pSrc, pWhere2, pGrp, 0, pOrderBy2, SF_UFSrcCheck|SF_IncludeHidden, pLimit2 + pSrc, pWhere2, pGrp, 0, pOrderBy2, + SF_UFSrcCheck|SF_IncludeHidden|SF_UpdateFrom, pLimit2 ); if( pSelect ) pSelect->selFlags |= SF_OrderByReqd; sqlite3SelectDestInit(&dest, eDest, iEph); diff --git a/src/util.c b/src/util.c index 32e9c27786..72faafea5f 100644 --- a/src/util.c +++ b/src/util.c @@ -1713,3 +1713,12 @@ int sqlite3VListNameToNum(VList *pIn, const char *zName, int nName){ }while( i=SQLITE_AFF_NUMERIC ){ assert( affinity==SQLITE_AFF_INTEGER || affinity==SQLITE_AFF_REAL - || affinity==SQLITE_AFF_NUMERIC ); + || affinity==SQLITE_AFF_NUMERIC || affinity==SQLITE_AFF_FLEXNUM ); if( (pRec->flags & MEM_Int)==0 ){ /*OPTIMIZATION-IF-FALSE*/ if( (pRec->flags & MEM_Real)==0 ){ if( pRec->flags & MEM_Str ) applyNumericAffinity(pRec,1); - }else{ + }else if( affinity<=SQLITE_AFF_REAL ){ sqlite3VdbeIntegerAffinity(pRec); } } @@ -617,17 +621,6 @@ void sqlite3VdbeRegisterDump(Vdbe *v){ # define REGISTER_TRACE(R,M) #endif - -#ifdef VDBE_PROFILE - -/* -** hwtime.h contains inline assembler code for implementing -** high-performance timing routines. -*/ -#include "hwtime.h" - -#endif - #ifndef NDEBUG /* ** This function is only called from within an assert() expression. It @@ -717,10 +710,8 @@ int sqlite3VdbeExec( ){ Op *aOp = p->aOp; /* Copy of p->aOp */ Op *pOp = aOp; /* Current operation */ -#if defined(SQLITE_DEBUG) || defined(VDBE_PROFILE) - Op *pOrigOp; /* Value of pOp at the top of the loop */ -#endif #ifdef SQLITE_DEBUG + Op *pOrigOp; /* Value of pOp at the top of the loop */ int nExtraDelete = 0; /* Verifies FORDELETE and AUXDELETE flags */ #endif int rc = SQLITE_OK; /* Value to return */ @@ -737,8 +728,8 @@ int sqlite3VdbeExec( Mem *pIn2 = 0; /* 2nd input operand */ Mem *pIn3 = 0; /* 3rd input operand */ Mem *pOut = 0; /* Output operand */ -#ifdef VDBE_PROFILE - u64 start; /* CPU clock count at start of opcode */ +#if defined(SQLITE_ENABLE_STMT_SCANSTATUS) || defined(VDBE_PROFILE) + u64 *pnCycle = 0; #endif /*** INSERT STACK UNION HERE ***/ @@ -801,12 +792,14 @@ int sqlite3VdbeExec( assert( rc==SQLITE_OK ); assert( pOp>=aOp && pOp<&aOp[p->nOp]); -#ifdef VDBE_PROFILE - start = sqlite3NProfileCnt ? sqlite3NProfileCnt : sqlite3Hwtime(); -#endif nVmStep++; -#ifdef SQLITE_ENABLE_STMT_SCANSTATUS - if( p->anExec ) p->anExec[(int)(pOp-aOp)]++; +#if defined(SQLITE_ENABLE_STMT_SCANSTATUS) || defined(VDBE_PROFILE) + pOp->nExec++; + pnCycle = &pOp->nCycle; +# ifdef VDBE_PROFILE + if( sqlite3NProfileCnt==0 ) +# endif + *pnCycle -= sqlite3Hwtime(); #endif /* Only allow tracing if SQLITE_DEBUG is defined. @@ -868,7 +861,7 @@ int sqlite3VdbeExec( } } #endif -#if defined(SQLITE_DEBUG) || defined(VDBE_PROFILE) +#ifdef SQLITE_DEBUG pOrigOp = pOp; #endif @@ -2126,7 +2119,6 @@ case OP_Ge: { /* same as TK_GE, jump, in1, in3 */ flags1 = pIn1->flags; flags3 = pIn3->flags; if( (flags1 & flags3 & MEM_Int)!=0 ){ - assert( (pOp->p5 & SQLITE_AFF_MASK)!=SQLITE_AFF_TEXT || CORRUPT_DB ); /* Common case of comparison of two integers */ if( pIn3->u.i > pIn1->u.i ){ if( sqlite3aGTb[pOp->opcode] ){ @@ -2194,7 +2186,7 @@ case OP_Ge: { /* same as TK_GE, jump, in1, in3 */ applyNumericAffinity(pIn3,0); } } - }else if( affinity==SQLITE_AFF_TEXT ){ + }else if( affinity==SQLITE_AFF_TEXT && ((flags1 | flags3) & MEM_Str)!=0 ){ if( (flags1 & MEM_Str)==0 && (flags1&(MEM_Int|MEM_Real|MEM_IntReal))!=0 ){ testcase( pIn1->flags & MEM_Int ); testcase( pIn1->flags & MEM_Real ); @@ -2202,7 +2194,7 @@ case OP_Ge: { /* same as TK_GE, jump, in1, in3 */ sqlite3VdbeMemStringify(pIn1, encoding, 1); testcase( (flags1&MEM_Dyn) != (pIn1->flags&MEM_Dyn) ); flags1 = (pIn1->flags & ~MEM_TypeMask) | (flags1 & MEM_TypeMask); - if( pIn1==pIn3 ) flags3 = flags1 | MEM_Str; + if( NEVER(pIn1==pIn3) ) flags3 = flags1 | MEM_Str; } if( (flags3 & MEM_Str)==0 && (flags3&(MEM_Int|MEM_Real|MEM_IntReal))!=0 ){ testcase( pIn3->flags & MEM_Int ); @@ -2788,7 +2780,7 @@ case OP_Offset: { /* out3 */ ** typeof() function or the IS NULL or IS NOT NULL operators or the ** equivalent. In this case, all content loading can be omitted. */ -case OP_Column: { +case OP_Column: { /* ncycle */ u32 p2; /* column number to retrieve */ VdbeCursor *pC; /* The VDBE cursor */ BtCursor *pCrsr; /* The B-Tree cursor corresponding to pC */ @@ -4171,7 +4163,7 @@ case OP_SetCookie: { ** ** See also: OP_OpenRead, OP_ReopenIdx */ -case OP_ReopenIdx: { +case OP_ReopenIdx: { /* ncycle */ int nField; KeyInfo *pKeyInfo; u32 p2; @@ -4192,7 +4184,7 @@ case OP_ReopenIdx: { } /* If the cursor is not currently open or is open on a different ** index, then fall through into OP_OpenRead to force a reopen */ -case OP_OpenRead: +case OP_OpenRead: /* ncycle */ case OP_OpenWrite: assert( pOp->opcode==OP_OpenWrite || pOp->p5==0 || pOp->p5==OPFLAG_SEEKEQ ); @@ -4291,7 +4283,7 @@ open_cursor_set_hints: ** ** Duplicate ephemeral cursors are used for self-joins of materialized views. */ -case OP_OpenDup: { +case OP_OpenDup: { /* ncycle */ VdbeCursor *pOrig; /* The original cursor to be duplicated */ VdbeCursor *pCx; /* The new cursor */ @@ -4353,8 +4345,8 @@ case OP_OpenDup: { ** by this opcode will be used for automatically created transient ** indices in joins. */ -case OP_OpenAutoindex: -case OP_OpenEphemeral: { +case OP_OpenAutoindex: /* ncycle */ +case OP_OpenEphemeral: { /* ncycle */ VdbeCursor *pCx; KeyInfo *pKeyInfo; @@ -4512,7 +4504,7 @@ case OP_OpenPseudo: { ** Close a cursor previously opened as P1. If P1 is not ** currently open, this instruction is a no-op. */ -case OP_Close: { +case OP_Close: { /* ncycle */ assert( pOp->p1>=0 && pOp->p1nCursor ); sqlite3VdbeFreeCursor(p, p->apCsr[pOp->p1]); p->apCsr[pOp->p1] = 0; @@ -4629,10 +4621,10 @@ case OP_ColumnsUsed: { ** ** See also: Found, NotFound, SeekGt, SeekGe, SeekLt */ -case OP_SeekLT: /* jump, in3, group */ -case OP_SeekLE: /* jump, in3, group */ -case OP_SeekGE: /* jump, in3, group */ -case OP_SeekGT: { /* jump, in3, group */ +case OP_SeekLT: /* jump, in3, group, ncycle */ +case OP_SeekLE: /* jump, in3, group, ncycle */ +case OP_SeekGE: /* jump, in3, group, ncycle */ +case OP_SeekGT: { /* jump, in3, group, ncycle */ int res; /* Comparison result */ int oc; /* Opcode */ VdbeCursor *pC; /* The cursor to seek */ @@ -4898,7 +4890,7 @@ seek_not_found: ** jump to SeekOP.P2 if This.P5==0 or to This.P2 if This.P5>0. ** */ -case OP_SeekScan: { +case OP_SeekScan: { /* ncycle */ VdbeCursor *pC; int res; int nStep; @@ -5020,7 +5012,7 @@ case OP_SeekScan: { ** ** P1 must be a valid b-tree cursor. */ -case OP_SeekHit: { +case OP_SeekHit: { /* ncycle */ VdbeCursor *pC; assert( pOp->p1>=0 && pOp->p1nCursor ); pC = p->apCsr[pOp->p1]; @@ -5152,7 +5144,7 @@ case OP_IfNotOpen: { /* jump */ ** ** See also: NotFound, Found, NotExists */ -case OP_IfNoHope: { /* jump, in3 */ +case OP_IfNoHope: { /* jump, in3, ncycle */ VdbeCursor *pC; assert( pOp->p1>=0 && pOp->p1nCursor ); pC = p->apCsr[pOp->p1]; @@ -5166,9 +5158,9 @@ case OP_IfNoHope: { /* jump, in3 */ /* Fall through into OP_NotFound */ /* no break */ deliberate_fall_through } -case OP_NoConflict: /* jump, in3 */ -case OP_NotFound: /* jump, in3 */ -case OP_Found: { /* jump, in3 */ +case OP_NoConflict: /* jump, in3, ncycle */ +case OP_NotFound: /* jump, in3, ncycle */ +case OP_Found: { /* jump, in3, ncycle */ int alreadyExists; int ii; VdbeCursor *pC; @@ -5298,7 +5290,7 @@ case OP_Found: { /* jump, in3 */ ** ** See also: Found, NotFound, NoConflict, SeekRowid */ -case OP_SeekRowid: { /* jump, in3 */ +case OP_SeekRowid: { /* jump, in3, ncycle */ VdbeCursor *pC; BtCursor *pCrsr; int res; @@ -5323,7 +5315,7 @@ case OP_SeekRowid: { /* jump, in3 */ } /* Fall through into OP_NotExists */ /* no break */ deliberate_fall_through -case OP_NotExists: /* jump, in3 */ +case OP_NotExists: /* jump, in3, ncycle */ pIn3 = &aMem[pOp->p3]; assert( (pIn3->flags & MEM_Int)!=0 || pOp->opcode==OP_SeekRowid ); assert( pOp->p1>=0 && pOp->p1nCursor ); @@ -5615,6 +5607,7 @@ case OP_Insert: { x.nZero = 0; } x.pKey = 0; + assert( BTREE_PREFORMAT==OPFLAG_PREFORMAT ); rc = sqlite3BtreeInsert(pC->uc.pCursor, &x, (pOp->p5 & (OPFLAG_APPEND|OPFLAG_SAVEPOSITION|OPFLAG_PREFORMAT)), seekResult @@ -5946,7 +5939,7 @@ case OP_RowData: { ** be a separate OP_VRowid opcode for use with virtual tables, but this ** one opcode now works for both table types. */ -case OP_Rowid: { /* out2 */ +case OP_Rowid: { /* out2, ncycle */ VdbeCursor *pC; i64 v; sqlite3_vtab *pVtab; @@ -6045,8 +6038,8 @@ case OP_NullRow: { ** from the end toward the beginning. In other words, the cursor is ** configured to use Prev, not Next. */ -case OP_SeekEnd: -case OP_Last: { /* jump */ +case OP_SeekEnd: /* ncycle */ +case OP_Last: { /* jump, ncycle */ VdbeCursor *pC; BtCursor *pCrsr; int res; @@ -6151,7 +6144,7 @@ case OP_Sort: { /* jump */ ** from the beginning toward the end. In other words, the cursor is ** configured to use Next, not Prev. */ -case OP_Rewind: { /* jump */ +case OP_Rewind: { /* jump, ncycle */ VdbeCursor *pC; BtCursor *pCrsr; int res; @@ -6245,7 +6238,7 @@ case OP_SorterNext: { /* jump */ rc = sqlite3VdbeSorterNext(db, pC); goto next_tail; -case OP_Prev: /* jump */ +case OP_Prev: /* jump, ncycle */ assert( pOp->p1>=0 && pOp->p1nCursor ); assert( pOp->p5==0 || pOp->p5==SQLITE_STMTSTATUS_FULLSCAN_STEP @@ -6260,7 +6253,7 @@ case OP_Prev: /* jump */ rc = sqlite3BtreePrevious(pC->uc.pCursor, pOp->p3); goto next_tail; -case OP_Next: /* jump */ +case OP_Next: /* jump, ncycle */ assert( pOp->p1>=0 && pOp->p1nCursor ); assert( pOp->p5==0 || pOp->p5==SQLITE_STMTSTATUS_FULLSCAN_STEP @@ -6452,8 +6445,8 @@ case OP_IdxDelete: { ** ** See also: Rowid, MakeRecord. */ -case OP_DeferredSeek: -case OP_IdxRowid: { /* out2 */ +case OP_DeferredSeek: /* ncycle */ +case OP_IdxRowid: { /* out2, ncycle */ VdbeCursor *pC; /* The P1 index cursor */ VdbeCursor *pTabCur; /* The P2 table cursor (OP_DeferredSeek only) */ i64 rowid; /* Rowid that P1 current points to */ @@ -6515,8 +6508,8 @@ case OP_IdxRowid: { /* out2 */ ** seek operation now, without further delay. If the cursor seek has ** already occurred, this instruction is a no-op. */ -case OP_FinishSeek: { - VdbeCursor *pC; /* The P1 index cursor */ +case OP_FinishSeek: { /* ncycle */ + VdbeCursor *pC; /* The P1 index cursor */ assert( pOp->p1>=0 && pOp->p1nCursor ); pC = p->apCsr[pOp->p1]; @@ -6571,10 +6564,10 @@ case OP_FinishSeek: { ** If the P1 index entry is less than or equal to the key value then jump ** to P2. Otherwise fall through to the next instruction. */ -case OP_IdxLE: /* jump */ -case OP_IdxGT: /* jump */ -case OP_IdxLT: /* jump */ -case OP_IdxGE: { /* jump */ +case OP_IdxLE: /* jump, ncycle */ +case OP_IdxGT: /* jump, ncycle */ +case OP_IdxLT: /* jump, ncycle */ +case OP_IdxGE: { /* jump, ncycle */ VdbeCursor *pC; int res; UnpackedRecord r; @@ -7195,9 +7188,6 @@ case OP_Program: { /* jump */ pFrame->aOp = p->aOp; pFrame->nOp = p->nOp; pFrame->token = pProgram->token; -#ifdef SQLITE_ENABLE_STMT_SCANSTATUS - pFrame->anExec = p->anExec; -#endif #ifdef SQLITE_DEBUG pFrame->iFrameMagic = SQLITE_FRAME_MAGIC; #endif @@ -7234,9 +7224,6 @@ case OP_Program: { /* jump */ memset(pFrame->aOnce, 0, (pProgram->nOp + 7)/8); p->aOp = aOp = pProgram->aOp; p->nOp = pProgram->nOp; -#ifdef SQLITE_ENABLE_STMT_SCANSTATUS - p->anExec = 0; -#endif #ifdef SQLITE_DEBUG /* Verify that second and subsequent executions of the same trigger do not ** try to reuse register values from the first use. */ @@ -7998,7 +7985,7 @@ case OP_VDestroy: { ** P1 is a cursor number. This opcode opens a cursor to the virtual ** table and stores that cursor in P1. */ -case OP_VOpen: { +case OP_VOpen: { /* ncycle */ VdbeCursor *pCur; sqlite3_vtab_cursor *pVCur; sqlite3_vtab *pVtab; @@ -8045,7 +8032,7 @@ case OP_VOpen: { ** cursor. Register P3 is used to hold the values returned by ** sqlite3_vtab_in_first() and sqlite3_vtab_in_next(). */ -case OP_VInitIn: { /* out2 */ +case OP_VInitIn: { /* out2, ncycle */ VdbeCursor *pC; /* The cursor containing the RHS values */ ValueList *pRhs; /* New ValueList object to put in reg[P2] */ @@ -8082,7 +8069,7 @@ case OP_VInitIn: { /* out2 */ ** ** A jump is made to P2 if the result set after filtering would be empty. */ -case OP_VFilter: { /* jump */ +case OP_VFilter: { /* jump, ncycle */ int nArg; int iQuery; const sqlite3_module *pModule; @@ -8142,7 +8129,7 @@ case OP_VFilter: { /* jump */ ** bits (OPFLAG_LENGTHARG or OPFLAG_TYPEOFARG) but those bits are ** unused by OP_VColumn. */ -case OP_VColumn: { +case OP_VColumn: { /* ncycle */ sqlite3_vtab *pVtab; const sqlite3_module *pModule; Mem *pDest; @@ -8194,7 +8181,7 @@ case OP_VColumn: { ** jump to instruction P2. Or, if the virtual table has reached ** the end of its result set, then fall through to the next instruction. */ -case OP_VNext: { /* jump */ +case OP_VNext: { /* jump, ncycle */ sqlite3_vtab *pVtab; const sqlite3_module *pModule; int res; @@ -8777,12 +8764,12 @@ default: { /* This is really OP_Noop, OP_Explain */ *****************************************************************************/ } -#ifdef VDBE_PROFILE - { - u64 endTime = sqlite3NProfileCnt ? sqlite3NProfileCnt : sqlite3Hwtime(); - if( endTime>start ) pOrigOp->cycles += endTime - start; - pOrigOp->cnt++; - } +#if defined(VDBE_PROFILE) + *pnCycle += sqlite3NProfileCnt ? sqlite3NProfileCnt : sqlite3Hwtime(); + pnCycle = 0; +#elif defined(SQLITE_ENABLE_STMT_SCANSTATUS) + *pnCycle += sqlite3Hwtime(); + pnCycle = 0; #endif /* The following code adds nothing to the actual functionality @@ -8858,6 +8845,18 @@ abort_due_to_error: ** release the mutexes on btrees that were acquired at the ** top. */ vdbe_return: +#if defined(VDBE_PROFILE) + if( pnCycle ){ + *pnCycle += sqlite3NProfileCnt ? sqlite3NProfileCnt : sqlite3Hwtime(); + pnCycle = 0; + } +#elif defined(SQLITE_ENABLE_STMT_SCANSTATUS) + if( pnCycle ){ + *pnCycle += sqlite3Hwtime(); + pnCycle = 0; + } +#endif + #ifndef SQLITE_OMIT_PROGRESS_CALLBACK while( nVmStep>=nProgressLimit && db->xProgress!=0 ){ nProgressLimit += db->nProgressOps; diff --git a/src/vdbe.h b/src/vdbe.h index a244468b52..3caf1f7872 100644 --- a/src/vdbe.h +++ b/src/vdbe.h @@ -67,14 +67,14 @@ struct VdbeOp { #ifdef SQLITE_ENABLE_EXPLAIN_COMMENTS char *zComment; /* Comment to improve readability */ #endif -#ifdef VDBE_PROFILE - u32 cnt; /* Number of times this instruction was executed */ - u64 cycles; /* Total time spent executing this instruction */ -#endif #ifdef SQLITE_VDBE_COVERAGE u32 iSrcLine; /* Source-code line that generated this opcode ** with flags in the upper 8 bits */ #endif +#if defined(SQLITE_ENABLE_STMT_SCANSTATUS) || defined(VDBE_PROFILE) + u64 nExec; + u64 nCycle; +#endif }; typedef struct VdbeOp VdbeOp; @@ -205,14 +205,20 @@ void sqlite3VdbeEndCoroutine(Vdbe*,int); #endif VdbeOp *sqlite3VdbeAddOpList(Vdbe*, int nOp, VdbeOpList const *aOp,int iLineno); #ifndef SQLITE_OMIT_EXPLAIN - void sqlite3VdbeExplain(Parse*,u8,const char*,...); + int sqlite3VdbeExplain(Parse*,u8,const char*,...); void sqlite3VdbeExplainPop(Parse*); int sqlite3VdbeExplainParent(Parse*); # define ExplainQueryPlan(P) sqlite3VdbeExplain P +# ifdef SQLITE_ENABLE_STMT_SCANSTATUS +# define ExplainQueryPlan2(V,P) (V = sqlite3VdbeExplain P) +# else +# define ExplainQueryPlan2(V,P) ExplainQueryPlan(P) +# endif # define ExplainQueryPlanPop(P) sqlite3VdbeExplainPop(P) # define ExplainQueryPlanParent(P) sqlite3VdbeExplainParent(P) #else # define ExplainQueryPlan(P) +# define ExplainQueryPlan2(V,P) # define ExplainQueryPlanPop(P) # define ExplainQueryPlanParent(P) 0 # define sqlite3ExplainBreakpoint(A,B) /*no-op*/ @@ -385,8 +391,12 @@ int sqlite3VdbeBytecodeVtabInit(sqlite3*); #ifdef SQLITE_ENABLE_STMT_SCANSTATUS void sqlite3VdbeScanStatus(Vdbe*, int, int, int, LogEst, const char*); +void sqlite3VdbeScanStatusRange(Vdbe*, int, int, int); +void sqlite3VdbeScanStatusCounters(Vdbe*, int, int, int); #else -# define sqlite3VdbeScanStatus(a,b,c,d,e) +# define sqlite3VdbeScanStatus(a,b,c,d,e,f) +# define sqlite3VdbeScanStatusRange(a,b,c,d) +# define sqlite3VdbeScanStatusCounters(a,b,c,d) #endif #if defined(SQLITE_DEBUG) || defined(VDBE_PROFILE) diff --git a/src/vdbeInt.h b/src/vdbeInt.h index 0c6301c8f1..cd112e9c48 100644 --- a/src/vdbeInt.h +++ b/src/vdbeInt.h @@ -171,7 +171,6 @@ struct VdbeFrame { Vdbe *v; /* VM this frame belongs to */ VdbeFrame *pParent; /* Parent of this frame, or NULL if parent is main */ Op *aOp; /* Program instructions for parent frame */ - i64 *anExec; /* Event counters from parent frame */ Mem *aMem; /* Array of memory cells for parent frame */ VdbeCursor **apCsr; /* Array of Vdbe cursors for parent frame */ u8 *aOnce; /* Bitmask used by OP_Once */ @@ -387,10 +386,19 @@ typedef unsigned bft; /* Bit Field Type */ /* The ScanStatus object holds a single value for the ** sqlite3_stmt_scanstatus() interface. +** +** aAddrRange[]: +** This array is used by ScanStatus elements associated with EQP +** notes that make an SQLITE_SCANSTAT_NCYCLE value available. It is +** an array of up to 3 ranges of VM addresses for which the Vdbe.anCycle[] +** values should be summed to calculate the NCYCLE value. Each pair of +** integer addresses is a start and end address (both inclusive) for a range +** instructions. A start value of 0 indicates an empty range. */ typedef struct ScanStatus ScanStatus; struct ScanStatus { int addrExplain; /* OP_Explain for loop */ + int aAddrRange[6]; int addrLoop; /* Address of "loops" counter */ int addrVisit; /* Address of "rows visited" counter */ int iSelectID; /* The "Select-ID" for this loop */ @@ -483,7 +491,6 @@ struct Vdbe { SubProgram *pProgram; /* Linked list of all sub-programs used by VM */ AuxData *pAuxData; /* Linked list of auxdata allocations */ #ifdef SQLITE_ENABLE_STMT_SCANSTATUS - i64 *anExec; /* Number of times each op has been executed */ int nScan; /* Entries in aScan[] */ ScanStatus *aScan; /* Scan definitions for sqlite3_stmt_scanstatus() */ #endif diff --git a/src/vdbeapi.c b/src/vdbeapi.c index 04295342b9..414745e8ba 100644 --- a/src/vdbeapi.c +++ b/src/vdbeapi.c @@ -15,6 +15,7 @@ */ #include "sqliteInt.h" #include "vdbeInt.h" +#include "opcodes.h" #ifndef SQLITE_OMIT_DEPRECATED /* @@ -505,7 +506,10 @@ void sqlite3_result_text64( ){ assert( sqlite3_mutex_held(pCtx->pOut->db->mutex) ); assert( xDel!=SQLITE_DYNAMIC ); - if( enc==SQLITE_UTF16 ) enc = SQLITE_UTF16NATIVE; + if( enc!=SQLITE_UTF8 ){ + if( enc==SQLITE_UTF16 ) enc = SQLITE_UTF16NATIVE; + n &= ~(u64)1; + } if( n>0x7fffffff ){ (void)invokeValueDestructor(z, xDel, pCtx); }else{ @@ -520,7 +524,7 @@ void sqlite3_result_text16( void (*xDel)(void *) ){ assert( sqlite3_mutex_held(pCtx->pOut->db->mutex) ); - setResultStrOrError(pCtx, z, n, SQLITE_UTF16NATIVE, xDel); + setResultStrOrError(pCtx, z, n & ~(u64)1, SQLITE_UTF16NATIVE, xDel); } void sqlite3_result_text16be( sqlite3_context *pCtx, @@ -529,7 +533,7 @@ void sqlite3_result_text16be( void (*xDel)(void *) ){ assert( sqlite3_mutex_held(pCtx->pOut->db->mutex) ); - setResultStrOrError(pCtx, z, n, SQLITE_UTF16BE, xDel); + setResultStrOrError(pCtx, z, n & ~(u64)1, SQLITE_UTF16BE, xDel); } void sqlite3_result_text16le( sqlite3_context *pCtx, @@ -538,7 +542,7 @@ void sqlite3_result_text16le( void (*xDel)(void *) ){ assert( sqlite3_mutex_held(pCtx->pOut->db->mutex) ); - setResultStrOrError(pCtx, z, n, SQLITE_UTF16LE, xDel); + setResultStrOrError(pCtx, z, n & ~(u64)1, SQLITE_UTF16LE, xDel); } #endif /* SQLITE_OMIT_UTF16 */ void sqlite3_result_value(sqlite3_context *pCtx, sqlite3_value *pValue){ @@ -1603,7 +1607,10 @@ int sqlite3_bind_text64( unsigned char enc ){ assert( xDel!=SQLITE_DYNAMIC ); - if( enc==SQLITE_UTF16 ) enc = SQLITE_UTF16NATIVE; + if( enc!=SQLITE_UTF8 ){ + if( enc==SQLITE_UTF16 ) enc = SQLITE_UTF16NATIVE; + nData &= ~(u16)1; + } return bindText(pStmt, i, zData, nData, xDel, enc); } #ifndef SQLITE_OMIT_UTF16 @@ -1611,10 +1618,10 @@ int sqlite3_bind_text16( sqlite3_stmt *pStmt, int i, const void *zData, - int nData, + int n, void (*xDel)(void*) ){ - return bindText(pStmt, i, zData, nData, xDel, SQLITE_UTF16NATIVE); + return bindText(pStmt, i, zData, n & ~(u64)1, xDel, SQLITE_UTF16NATIVE); } #endif /* SQLITE_OMIT_UTF16 */ int sqlite3_bind_value(sqlite3_stmt *pStmt, int i, const sqlite3_value *pValue){ @@ -2105,23 +2112,60 @@ int sqlite3_preupdate_new(sqlite3 *db, int iIdx, sqlite3_value **ppValue){ /* ** Return status data for a single loop within query pStmt. */ -int sqlite3_stmt_scanstatus( +int sqlite3_stmt_scanstatus_v2( sqlite3_stmt *pStmt, /* Prepared statement being queried */ - int idx, /* Index of loop to report on */ + int iScan, /* Index of loop to report on */ int iScanStatusOp, /* Which metric to return */ + int flags, void *pOut /* OUT: Write the answer here */ ){ Vdbe *p = (Vdbe*)pStmt; ScanStatus *pScan; - if( idx<0 || idx>=p->nScan ) return 1; - pScan = &p->aScan[idx]; + int idx; + + if( iScan<0 ){ + int ii; + if( iScanStatusOp==SQLITE_SCANSTAT_NCYCLE ){ + i64 res = 0; + for(ii=0; iinOp; ii++){ + res += p->aOp[ii].nCycle; + } + *(i64*)pOut = res; + return 0; + } + return 1; + } + if( flags & SQLITE_SCANSTAT_COMPLEX ){ + idx = iScan; + pScan = &p->aScan[idx]; + }else{ + /* If the COMPLEX flag is clear, then this function must ignore any + ** ScanStatus structures with ScanStatus.addrLoop set to 0. */ + for(idx=0; idxnScan; idx++){ + pScan = &p->aScan[idx]; + if( pScan->zName ){ + iScan--; + if( iScan<0 ) break; + } + } + } + if( idx>=p->nScan ) return 1; + switch( iScanStatusOp ){ case SQLITE_SCANSTAT_NLOOP: { - *(sqlite3_int64*)pOut = p->anExec[pScan->addrLoop]; + if( pScan->addrLoop>0 ){ + *(sqlite3_int64*)pOut = p->aOp[pScan->addrLoop].nExec; + }else{ + *(sqlite3_int64*)pOut = -1; + } break; } case SQLITE_SCANSTAT_NVISIT: { - *(sqlite3_int64*)pOut = p->anExec[pScan->addrVisit]; + if( pScan->addrVisit>0 ){ + *(sqlite3_int64*)pOut = p->aOp[pScan->addrVisit].nExec; + }else{ + *(sqlite3_int64*)pOut = -1; + } break; } case SQLITE_SCANSTAT_EST: { @@ -2154,6 +2198,45 @@ int sqlite3_stmt_scanstatus( } break; } + case SQLITE_SCANSTAT_PARENTID: { + if( pScan->addrExplain ){ + *(int*)pOut = p->aOp[ pScan->addrExplain ].p2; + }else{ + *(int*)pOut = -1; + } + break; + } + case SQLITE_SCANSTAT_NCYCLE: { + i64 res = 0; + if( pScan->aAddrRange[0]==0 ){ + res = -1; + }else{ + int ii; + for(ii=0; iiaAddrRange); ii+=2){ + int iIns = pScan->aAddrRange[ii]; + int iEnd = pScan->aAddrRange[ii+1]; + if( iIns==0 ) break; + if( iIns>0 ){ + while( iIns<=iEnd ){ + res += p->aOp[iIns].nCycle; + iIns++; + } + }else{ + int iOp; + for(iOp=0; iOpnOp; iOp++){ + Op *pOp = &p->aOp[iOp]; + if( pOp->p1!=iEnd ) continue; + if( (sqlite3OpcodeProperty[pOp->opcode] & OPFLG_NCYCLE)==0 ){ + continue; + } + res += p->aOp[iOp].nCycle; + } + } + } + } + *(i64*)pOut = res; + break; + } default: { return 1; } @@ -2161,11 +2244,28 @@ int sqlite3_stmt_scanstatus( return 0; } +/* +** Return status data for a single loop within query pStmt. +*/ +int sqlite3_stmt_scanstatus( + sqlite3_stmt *pStmt, /* Prepared statement being queried */ + int iScan, /* Index of loop to report on */ + int iScanStatusOp, /* Which metric to return */ + void *pOut /* OUT: Write the answer here */ +){ + return sqlite3_stmt_scanstatus_v2(pStmt, iScan, iScanStatusOp, 0, pOut); +} + /* ** Zero all counters associated with the sqlite3_stmt_scanstatus() data. */ void sqlite3_stmt_scanstatus_reset(sqlite3_stmt *pStmt){ Vdbe *p = (Vdbe*)pStmt; - memset(p->anExec, 0, p->nOp * sizeof(i64)); + int ii; + for(ii=0; iinOp; ii++){ + Op *pOp = &p->aOp[ii]; + pOp->nExec = 0; + pOp->nCycle = 0; + } } #endif /* SQLITE_ENABLE_STMT_SCANSTATUS */ diff --git a/src/vdbeaux.c b/src/vdbeaux.c index 08c08e1327..d96bce3aea 100644 --- a/src/vdbeaux.c +++ b/src/vdbeaux.c @@ -262,16 +262,16 @@ int sqlite3VdbeAddOp3(Vdbe *p, int op, int p1, int p2, int p3){ #ifdef SQLITE_ENABLE_EXPLAIN_COMMENTS pOp->zComment = 0; #endif +#if defined(SQLITE_ENABLE_STMT_SCANSTATUS) || defined(VDBE_PROFILE) + pOp->nExec = 0; + pOp->nCycle = 0; +#endif #ifdef SQLITE_DEBUG if( p->db->flags & SQLITE_VdbeAddopTrace ){ sqlite3VdbePrintOp(0, i, &p->aOp[i]); test_addop_breakpoint(i, &p->aOp[i]); } #endif -#ifdef VDBE_PROFILE - pOp->cycles = 0; - pOp->cnt = 0; -#endif #ifdef SQLITE_VDBE_COVERAGE pOp->iSrcLine = 0; #endif @@ -439,8 +439,9 @@ void sqlite3ExplainBreakpoint(const char *z1, const char *z2){ ** If the bPush flag is true, then make this opcode the parent for ** subsequent Explains until sqlite3VdbeExplainPop() is called. */ -void sqlite3VdbeExplain(Parse *pParse, u8 bPush, const char *zFmt, ...){ -#ifndef SQLITE_DEBUG +int sqlite3VdbeExplain(Parse *pParse, u8 bPush, const char *zFmt, ...){ + int addr = 0; +#if !defined(SQLITE_DEBUG) && !defined(SQLITE_ENABLE_STMT_SCANSTATUS) /* Always include the OP_Explain opcodes if SQLITE_DEBUG is defined. ** But omit them (for performance) during production builds */ if( pParse->explain==2 ) @@ -455,13 +456,15 @@ void sqlite3VdbeExplain(Parse *pParse, u8 bPush, const char *zFmt, ...){ va_end(ap); v = pParse->pVdbe; iThis = v->nOp; - sqlite3VdbeAddOp4(v, OP_Explain, iThis, pParse->addrExplain, 0, + addr = sqlite3VdbeAddOp4(v, OP_Explain, iThis, pParse->addrExplain, 0, zMsg, P4_DYNAMIC); sqlite3ExplainBreakpoint(bPush?"PUSH":"", sqlite3VdbeGetLastOp(v)->p4.z); if( bPush){ pParse->addrExplain = iThis; } + sqlite3VdbeScanStatus(v, iThis, 0, 0, 0, 0); } + return addr; } /* @@ -1119,6 +1122,7 @@ void sqlite3VdbeScanStatus( aNew = (ScanStatus*)sqlite3DbRealloc(p->db, p->aScan, nByte); if( aNew ){ ScanStatus *pNew = &aNew[p->nScan++]; + memset(pNew, 0, sizeof(ScanStatus)); pNew->addrExplain = addrExplain; pNew->addrLoop = addrLoop; pNew->addrVisit = addrVisit; @@ -1127,6 +1131,62 @@ void sqlite3VdbeScanStatus( p->aScan = aNew; } } + +/* +** Add the range of instructions from addrStart to addrEnd (inclusive) to +** the set of those corresponding to the sqlite3_stmt_scanstatus() counters +** associated with the OP_Explain instruction at addrExplain. The +** sum of the sqlite3Hwtime() values for each of these instructions +** will be returned for SQLITE_SCANSTAT_NCYCLE requests. +*/ +void sqlite3VdbeScanStatusRange( + Vdbe *p, + int addrExplain, + int addrStart, + int addrEnd +){ + ScanStatus *pScan = 0; + int ii; + for(ii=p->nScan-1; ii>=0; ii--){ + pScan = &p->aScan[ii]; + if( pScan->addrExplain==addrExplain ) break; + pScan = 0; + } + if( pScan ){ + if( addrEnd<0 ) addrEnd = sqlite3VdbeCurrentAddr(p)-1; + for(ii=0; iiaAddrRange); ii+=2){ + if( pScan->aAddrRange[ii]==0 ){ + pScan->aAddrRange[ii] = addrStart; + pScan->aAddrRange[ii+1] = addrEnd; + break; + } + } + } +} + +/* +** Set the addresses for the SQLITE_SCANSTAT_NLOOP and SQLITE_SCANSTAT_NROW +** counters for the query element associated with the OP_Explain at +** addrExplain. +*/ +void sqlite3VdbeScanStatusCounters( + Vdbe *p, + int addrExplain, + int addrLoop, + int addrVisit +){ + ScanStatus *pScan = 0; + int ii; + for(ii=p->nScan-1; ii>=0; ii--){ + pScan = &p->aScan[ii]; + if( pScan->addrExplain==addrExplain ) break; + pScan = 0; + } + if( pScan ){ + pScan->addrLoop = addrLoop; + pScan->addrVisit = addrVisit; + } +} #endif @@ -1457,7 +1517,7 @@ void sqlite3VdbeAppendP4(Vdbe *p, void *pP4, int n){ if( p->db->mallocFailed ){ freeP4(p->db, n, pP4); }else{ - assert( pP4!=0 ); + assert( pP4!=0 || n==P4_DYNAMIC ); assert( p->nOp>0 ); pOp = &p->aOp[p->nOp-1]; assert( pOp->p4type==P4_NOTUSED ); @@ -2424,7 +2484,7 @@ static void *allocSpace( ** running it. */ void sqlite3VdbeRewind(Vdbe *p){ -#if defined(SQLITE_DEBUG) || defined(VDBE_PROFILE) +#if defined(SQLITE_DEBUG) int i; #endif assert( p!=0 ); @@ -2453,8 +2513,8 @@ void sqlite3VdbeRewind(Vdbe *p){ p->nFkConstraint = 0; #ifdef VDBE_PROFILE for(i=0; inOp; i++){ - p->aOp[i].cnt = 0; - p->aOp[i].cycles = 0; + p->aOp[i].nExec = 0; + p->aOp[i].nCycle = 0; } #endif } @@ -2563,9 +2623,6 @@ void sqlite3VdbeMakeReady( p->aVar = allocSpace(&x, 0, nVar*sizeof(Mem)); p->apArg = allocSpace(&x, 0, nArg*sizeof(Mem*)); p->apCsr = allocSpace(&x, 0, nCursor*sizeof(VdbeCursor*)); -#ifdef SQLITE_ENABLE_STMT_SCANSTATUS - p->anExec = allocSpace(&x, 0, p->nOp*sizeof(i64)); -#endif if( x.nNeeded ){ x.pSpace = p->pFree = sqlite3DbMallocRawNN(db, x.nNeeded); x.nFree = x.nNeeded; @@ -2574,9 +2631,6 @@ void sqlite3VdbeMakeReady( p->aVar = allocSpace(&x, p->aVar, nVar*sizeof(Mem)); p->apArg = allocSpace(&x, p->apArg, nArg*sizeof(Mem*)); p->apCsr = allocSpace(&x, p->apCsr, nCursor*sizeof(VdbeCursor*)); -#ifdef SQLITE_ENABLE_STMT_SCANSTATUS - p->anExec = allocSpace(&x, p->anExec, p->nOp*sizeof(i64)); -#endif } } @@ -2591,9 +2645,6 @@ void sqlite3VdbeMakeReady( p->nMem = nMem; initMemArray(p->aMem, nMem, db, MEM_Undefined); memset(p->apCsr, 0, nCursor*sizeof(VdbeCursor*)); -#ifdef SQLITE_ENABLE_STMT_SCANSTATUS - memset(p->anExec, 0, p->nOp*sizeof(i64)); -#endif } sqlite3VdbeRewind(p); } @@ -2651,9 +2702,6 @@ static void closeCursorsInFrame(Vdbe *p){ int sqlite3VdbeFrameRestore(VdbeFrame *pFrame){ Vdbe *v = pFrame->v; closeCursorsInFrame(v); -#ifdef SQLITE_ENABLE_STMT_SCANSTATUS - v->anExec = pFrame->anExec; -#endif v->aOp = pFrame->aOp; v->nOp = pFrame->nOp; v->aMem = pFrame->aMem; @@ -3505,10 +3553,12 @@ int sqlite3VdbeReset(Vdbe *p){ } for(i=0; inOp; i++){ char zHdr[100]; + i64 cnt = p->aOp[i].nExec; + i64 cycles = p->aOp[i].nCycle; sqlite3_snprintf(sizeof(zHdr), zHdr, "%6u %12llu %8llu ", - p->aOp[i].cnt, - p->aOp[i].cycles, - p->aOp[i].cnt>0 ? p->aOp[i].cycles/p->aOp[i].cnt : 0 + cnt, + cycles, + cnt>0 ? cycles/cnt : 0 ); fprintf(out, "%s", zHdr); sqlite3VdbePrintOp(out, i, &p->aOp[i]); diff --git a/src/where.c b/src/where.c index 7ba72d2766..890c3bf52c 100644 --- a/src/where.c +++ b/src/where.c @@ -67,7 +67,7 @@ int sqlite3WhereIsDistinct(WhereInfo *pWInfo){ ** block sorting is required. */ int sqlite3WhereIsOrdered(WhereInfo *pWInfo){ - return pWInfo->nOBSat; + return pWInfo->nOBSat<0 ? 0 : pWInfo->nOBSat; } /* @@ -705,7 +705,7 @@ static void translateColumnToCopy( #if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(WHERETRACE_ENABLED) static void whereTraceIndexInfoInputs(sqlite3_index_info *p){ int i; - if( !sqlite3WhereTrace ) return; + if( (sqlite3WhereTrace & 0x10)==0 ) return; for(i=0; inConstraint; i++){ sqlite3DebugPrintf( " constraint[%d]: col=%d termid=%d op=%d usabled=%d collseq=%s\n", @@ -725,7 +725,7 @@ static void whereTraceIndexInfoInputs(sqlite3_index_info *p){ } static void whereTraceIndexInfoOutputs(sqlite3_index_info *p){ int i; - if( !sqlite3WhereTrace ) return; + if( (sqlite3WhereTrace & 0x10)==0 ) return; for(i=0; inConstraint; i++){ sqlite3DebugPrintf(" usage[%d]: argvIdx=%d omit=%d\n", i, @@ -812,6 +812,57 @@ static int termCanDriveIndex( #ifndef SQLITE_OMIT_AUTOMATIC_INDEX + +#ifdef SQLITE_ENABLE_STMT_SCANSTATUS +/* +** Argument pIdx represents an automatic index that the current statement +** will create and populate. Add an OP_Explain with text of the form: +** +** CREATE AUTOMATIC INDEX ON () [WHERE ] +** +** This is only required if sqlite3_stmt_scanstatus() is enabled, to +** associate an SQLITE_SCANSTAT_NCYCLE and SQLITE_SCANSTAT_NLOOP +** values with. In order to avoid breaking legacy code and test cases, +** the OP_Explain is not added if this is an EXPLAIN QUERY PLAN command. +*/ +static void explainAutomaticIndex( + Parse *pParse, + Index *pIdx, /* Automatic index to explain */ + int bPartial, /* True if pIdx is a partial index */ + int *pAddrExplain /* OUT: Address of OP_Explain */ +){ + if( pParse->explain!=2 ){ + Table *pTab = pIdx->pTable; + const char *zSep = ""; + char *zText = 0; + int ii = 0; + sqlite3_str *pStr = sqlite3_str_new(pParse->db); + sqlite3_str_appendf(pStr,"CREATE AUTOMATIC INDEX ON %s(", pTab->zName); + assert( pIdx->nColumn>1 ); + assert( pIdx->aiColumn[pIdx->nColumn-1]==XN_ROWID ); + for(ii=0; ii<(pIdx->nColumn-1); ii++){ + const char *zName = 0; + int iCol = pIdx->aiColumn[ii]; + + zName = pTab->aCol[iCol].zCnName; + sqlite3_str_appendf(pStr, "%s%s", zSep, zName); + zSep = ", "; + } + zText = sqlite3_str_finish(pStr); + if( zText==0 ){ + sqlite3OomFault(pParse->db); + }else{ + *pAddrExplain = sqlite3VdbeExplain( + pParse, 0, "%s)%s", zText, (bPartial ? " WHERE " : "") + ); + sqlite3_free(zText); + } + } +} +#else +# define explainAutomaticIndex(a,b,c,d) +#endif + /* ** Generate code to construct the Index object for an automatic index ** and to set up the WhereLevel object pLevel so that the code generator @@ -847,6 +898,9 @@ static SQLITE_NOINLINE void constructAutomaticIndex( SrcItem *pTabItem; /* FROM clause term being indexed */ int addrCounter = 0; /* Address where integer counter is initialized */ int regBase; /* Array of registers where record is assembled */ +#ifdef SQLITE_ENABLE_STMT_SCANSTATUS + int addrExp = 0; /* Address of OP_Explain */ +#endif /* Generate code to skip over the creation and initialization of the ** transient index on 2nd and subsequent iterations of the loop. */ @@ -970,6 +1024,7 @@ static SQLITE_NOINLINE void constructAutomaticIndex( pIdx->azColl[n] = sqlite3StrBINARY; /* Create the automatic index */ + explainAutomaticIndex(pParse, pIdx, pPartial!=0, &addrExp); assert( pLevel->iIdxCur>=0 ); pLevel->iIdxCur = pParse->nTab++; sqlite3VdbeAddOp2(v, OP_OpenAutoindex, pLevel->iIdxCur, nKeyCol+1); @@ -1005,6 +1060,7 @@ static SQLITE_NOINLINE void constructAutomaticIndex( sqlite3VdbeAddOp4Int(v, OP_FilterAdd, pLevel->regFilter, 0, regBase, pLoop->u.btree.nEq); } + sqlite3VdbeScanStatusCounters(v, addrExp, addrExp, sqlite3VdbeCurrentAddr(v)); sqlite3VdbeAddOp2(v, OP_IdxInsert, pLevel->iIdxCur, regRecord); sqlite3VdbeChangeP5(v, OPFLAG_USESEEKRESULT); if( pPartial ) sqlite3VdbeResolveLabel(v, iContinue); @@ -1025,6 +1081,7 @@ static SQLITE_NOINLINE void constructAutomaticIndex( /* Jump here when skipping the initialization */ sqlite3VdbeJumpHere(v, addrInit); + sqlite3VdbeScanStatusRange(v, addrExp, addrExp, -1); end_auto_index_create: sqlite3ExprDelete(pParse->db, pPartial); @@ -1742,7 +1799,7 @@ static int whereRangeSkipScanEst( int nAdjust = (sqlite3LogEst(p->nSample) - sqlite3LogEst(nDiff)); pLoop->nOut -= nAdjust; *pbDone = 1; - WHERETRACE(0x10, ("range skip-scan regions: %u..%u adjust=%d est=%d\n", + WHERETRACE(0x20, ("range skip-scan regions: %u..%u adjust=%d est=%d\n", nLower, nUpper, nAdjust*-1, pLoop->nOut)); } @@ -1920,7 +1977,7 @@ static int whereRangeScanEst( if( nNewnOut>nOut ){ - WHERETRACE(0x10,("Range scan lowers nOut from %d to %d\n", + WHERETRACE(0x20,("Range scan lowers nOut from %d to %d\n", pLoop->nOut, nOut)); } #endif @@ -2018,7 +2075,7 @@ static int whereEqualScanEst( pBuilder->nRecValid = nEq; whereKeyStats(pParse, p, pRec, 0, a); - WHERETRACE(0x10,("equality scan regions %s(%d): %d\n", + WHERETRACE(0x20,("equality scan regions %s(%d): %d\n", p->zName, nEq-1, (int)a[1])); *pnRow = a[1]; @@ -2068,7 +2125,7 @@ static int whereInScanEst( if( rc==SQLITE_OK ){ if( nRowEst > nRow0 ) nRowEst = nRow0; *pnRow = nRowEst; - WHERETRACE(0x10,("IN row estimate: est=%d\n", nRowEst)); + WHERETRACE(0x20,("IN row estimate: est=%d\n", nRowEst)); } assert( pBuilder->nRecValid==nRecValid ); return rc; @@ -2177,7 +2234,7 @@ void sqlite3WhereLoopPrint(WhereLoop *p, WhereClause *pWC){ sqlite3DebugPrintf(" f %06x N %d", p->wsFlags, p->nLTerm); } sqlite3DebugPrintf(" cost %d,%d,%d\n", p->rSetup, p->rRun, p->nOut); - if( p->nLTerm && (sqlite3WhereTrace & 0x100)!=0 ){ + if( p->nLTerm && (sqlite3WhereTrace & 0x4000)!=0 ){ int i; for(i=0; inLTerm; i++){ sqlite3WhereTermPrint(p->aLTerm[i], i); @@ -3055,7 +3112,7 @@ static int whereLoopAddBtreeIndex( && pNew->nOut+10 > pProbe->aiRowLogEst[0] ){ #if WHERETRACE_ENABLED /* 0x01 */ - if( sqlite3WhereTrace & 0x01 ){ + if( sqlite3WhereTrace & 0x20 ){ sqlite3DebugPrintf( "STAT4 determines term has low selectivity:\n"); sqlite3WhereTermPrint(pTerm, 999); @@ -3092,9 +3149,17 @@ static int whereLoopAddBtreeIndex( ** seek only. Then, if this is a non-covering index, add the cost of ** visiting the rows in the main table. */ assert( pSrc->pTab->szTabRow>0 ); - rCostIdx = pNew->nOut + 1 + (15*pProbe->szIdxRow)/pSrc->pTab->szTabRow; + if( pProbe->idxType==SQLITE_IDXTYPE_IPK ){ + /* The pProbe->szIdxRow is low for an IPK table since the interior + ** pages are small. Thuse szIdxRow gives a good estimate of seek cost. + ** But the leaf pages are full-size, so pProbe->szIdxRow would badly + ** under-estimate the scanning cost. */ + rCostIdx = pNew->nOut + 16; + }else{ + rCostIdx = pNew->nOut + 1 + (15*pProbe->szIdxRow)/pSrc->pTab->szTabRow; + } pNew->rRun = sqlite3LogEstAdd(rLogSize, rCostIdx); - if( (pNew->wsFlags & (WHERE_IDX_ONLY|WHERE_IPK))==0 ){ + if( (pNew->wsFlags & (WHERE_IDX_ONLY|WHERE_IPK|WHERE_EXPRIDX))==0 ){ pNew->rRun = sqlite3LogEstAdd(pNew->rRun, pNew->nOut + 16); } ApplyCostMultiplier(pNew->rRun, pProbe->pTable->costMult); @@ -3247,16 +3312,39 @@ static int whereUsablePartialIndex( return 0; } +/* +** pIdx is an index containing expressions. Check it see if any of the +** expressions in the index match the pExpr expression. +*/ +static int exprIsCoveredByIndex( + const Expr *pExpr, + const Index *pIdx, + int iTabCur +){ + int i; + for(i=0; inColumn; i++){ + if( pIdx->aiColumn[i]==XN_EXPR + && sqlite3ExprCompare(0, pExpr, pIdx->aColExpr->a[i].pExpr, iTabCur)==0 + ){ + return 1; + } + } + return 0; +} + /* ** Structure passed to the whereIsCoveringIndex Walker callback. */ +typedef struct CoveringIndexCheck CoveringIndexCheck; struct CoveringIndexCheck { Index *pIdx; /* The index */ int iTabCur; /* Cursor number for the corresponding table */ + u8 bExpr; /* Uses an indexed expression */ + u8 bUnidx; /* Uses an unindexed column not within an indexed expr */ }; /* -** Information passed in is pWalk->u.pCovIdxCk. Call is pCk. +** Information passed in is pWalk->u.pCovIdxCk. Call it pCk. ** ** If the Expr node references the table with cursor pCk->iTabCur, then ** make sure that column is covered by the index pCk->pIdx. We know that @@ -3268,71 +3356,103 @@ struct CoveringIndexCheck { ** ** If this node does not disprove that the index can be a covering index, ** then just return WRC_Continue, to continue the search. +** +** If pCk->pIdx contains indexed expressions and one of those expressions +** matches pExpr, then prune the search. */ static int whereIsCoveringIndexWalkCallback(Walker *pWalk, Expr *pExpr){ - int i; /* Loop counter */ - const Index *pIdx; /* The index of interest */ - const i16 *aiColumn; /* Columns contained in the index */ - u16 nColumn; /* Number of columns in the index */ - if( pExpr->op!=TK_COLUMN && pExpr->op!=TK_AGG_COLUMN ) return WRC_Continue; - if( pExpr->iColumn<(BMS-1) ) return WRC_Continue; - if( pExpr->iTable!=pWalk->u.pCovIdxCk->iTabCur ) return WRC_Continue; - pIdx = pWalk->u.pCovIdxCk->pIdx; - aiColumn = pIdx->aiColumn; - nColumn = pIdx->nColumn; - for(i=0; iiColumn ) return WRC_Continue; + int i; /* Loop counter */ + const Index *pIdx; /* The index of interest */ + const i16 *aiColumn; /* Columns contained in the index */ + u16 nColumn; /* Number of columns in the index */ + CoveringIndexCheck *pCk; /* Info about this search */ + + pCk = pWalk->u.pCovIdxCk; + pIdx = pCk->pIdx; + if( (pExpr->op==TK_COLUMN || pExpr->op==TK_AGG_COLUMN) ){ + /* if( pExpr->iColumn<(BMS-1) && pIdx->bHasExpr==0 ) return WRC_Continue;*/ + if( pExpr->iTable!=pCk->iTabCur ) return WRC_Continue; + pIdx = pWalk->u.pCovIdxCk->pIdx; + aiColumn = pIdx->aiColumn; + nColumn = pIdx->nColumn; + for(i=0; iiColumn ) return WRC_Continue; + } + pCk->bUnidx = 1; + return WRC_Abort; + }else if( pIdx->bHasExpr + && exprIsCoveredByIndex(pExpr, pIdx, pWalk->u.pCovIdxCk->iTabCur) ){ + pCk->bExpr = 1; + return WRC_Prune; } - pWalk->eCode = 1; - return WRC_Abort; + return WRC_Continue; } /* ** pIdx is an index that covers all of the low-number columns used by -** pWInfo->pSelect (columns from 0 through 62). But there are columns -** in pWInfo->pSelect beyond 62. This routine tries to answer the question -** of whether pIdx covers *all* columns in the query. +** pWInfo->pSelect (columns from 0 through 62) or an index that has +** expressions terms. Hence, we cannot determine whether or not it is +** a covering index by using the colUsed bitmasks. We have to do a search +** to see if the index is covering. This routine does that search. ** -** Return 0 if pIdx is a covering index. Return non-zero if pIdx is -** not a covering index or if we are unable to determine if pIdx is a -** covering index. +** The return value is one of these: ** -** This routine is an optimization. It is always safe to return non-zero. -** But returning zero when non-zero should have been returned can lead to -** incorrect bytecode and assertion faults. +** 0 The index is definitely not a covering index +** +** WHERE_IDX_ONLY The index is definitely a covering index +** +** WHERE_EXPRIDX The index is likely a covering index, but it is +** difficult to determine precisely because of the +** expressions that are indexed. Score it as a +** covering index, but still keep the main table open +** just in case we need it. +** +** This routine is an optimization. It is always safe to return zero. +** But returning one of the other two values when zero should have been +** returned can lead to incorrect bytecode and assertion faults. */ static SQLITE_NOINLINE u32 whereIsCoveringIndex( WhereInfo *pWInfo, /* The WHERE clause context */ Index *pIdx, /* Index that is being tested */ int iTabCur /* Cursor for the table being indexed */ ){ - int i; + int i, rc; struct CoveringIndexCheck ck; Walker w; if( pWInfo->pSelect==0 ){ /* We don't have access to the full query, so we cannot check to see ** if pIdx is covering. Assume it is not. */ - return 1; + return 0; } - for(i=0; inColumn; i++){ - if( pIdx->aiColumn[i]>=BMS-1 ) break; - } - if( i>=pIdx->nColumn ){ - /* pIdx does not index any columns greater than 62, but we know from - ** colMask that columns greater than 62 are used, so this is not a - ** covering index */ - return 1; + if( pIdx->bHasExpr==0 ){ + for(i=0; inColumn; i++){ + if( pIdx->aiColumn[i]>=BMS-1 ) break; + } + if( i>=pIdx->nColumn ){ + /* pIdx does not index any columns greater than 62, but we know from + ** colMask that columns greater than 62 are used, so this is not a + ** covering index */ + return 0; + } } ck.pIdx = pIdx; ck.iTabCur = iTabCur; + ck.bExpr = 0; + ck.bUnidx = 0; memset(&w, 0, sizeof(w)); w.xExprCallback = whereIsCoveringIndexWalkCallback; w.xSelectCallback = sqlite3SelectWalkNoop; w.u.pCovIdxCk = &ck; - w.eCode = 0; sqlite3WalkSelect(&w, pWInfo->pSelect); - return w.eCode; + if( ck.bUnidx ){ + rc = 0; + }else if( ck.bExpr ){ + rc = WHERE_EXPRIDX; + }else{ + rc = WHERE_IDX_ONLY; + } + return rc; } /* @@ -3417,7 +3537,7 @@ static int whereLoopAddBtree( sPk.aiRowLogEst = aiRowEstPk; sPk.onError = OE_Replace; sPk.pTable = pTab; - sPk.szIdxRow = pTab->szTabRow; + sPk.szIdxRow = 3; /* TUNING: Interior rows of IPK table are very small */ sPk.idxType = SQLITE_IDXTYPE_IPK; aiRowEstPk[0] = pTab->nRowLogEst; aiRowEstPk[1] = 0; @@ -3548,14 +3668,38 @@ static int whereLoopAddBtree( }else{ Bitmask m; if( pProbe->isCovering ){ - pNew->wsFlags = WHERE_IDX_ONLY | WHERE_INDEXED; m = 0; + pNew->wsFlags = WHERE_IDX_ONLY | WHERE_INDEXED; }else{ m = pSrc->colUsed & pProbe->colNotIdxed; - if( m==TOPBIT ){ - m = whereIsCoveringIndex(pWInfo, pProbe, pSrc->iCursor); + pNew->wsFlags = WHERE_INDEXED; + if( m==TOPBIT || (pProbe->bHasExpr && !pProbe->bHasVCol && m!=0) ){ + u32 isCov = whereIsCoveringIndex(pWInfo, pProbe, pSrc->iCursor); + if( isCov==0 ){ + WHERETRACE(0x200, + ("-> %s is not a covering index" + " according to whereIsCoveringIndex()\n", pProbe->zName)); + assert( m!=0 ); + }else{ + m = 0; + pNew->wsFlags |= isCov; + if( isCov & WHERE_IDX_ONLY ){ + WHERETRACE(0x200, + ("-> %s is a covering expression index" + " according to whereIsCoveringIndex()\n", pProbe->zName)); + }else{ + assert( isCov==WHERE_EXPRIDX ); + WHERETRACE(0x200, + ("-> %s might be a covering expression index" + " according to whereIsCoveringIndex()\n", pProbe->zName)); + } + } + }else if( m==0 ){ + WHERETRACE(0x200, + ("-> %s a covering index according to bitmasks\n", + pProbe->zName, m==0 ? "is" : "is not")); + pNew->wsFlags = WHERE_IDX_ONLY | WHERE_INDEXED; } - pNew->wsFlags = (m==0) ? (WHERE_IDX_ONLY|WHERE_INDEXED) : WHERE_INDEXED; } /* Full scan via index */ @@ -3728,7 +3872,7 @@ static int whereLoopAddVirtualOne( ** that the particular combination of parameters provided is unusable. ** Make no entries in the loop table. */ - WHERETRACE(0xffff, (" ^^^^--- non-viable plan rejected!\n")); + WHERETRACE(0xffffffff, (" ^^^^--- non-viable plan rejected!\n")); return SQLITE_OK; } return rc; @@ -3839,7 +3983,7 @@ static int whereLoopAddVirtualOne( sqlite3_free(pNew->u.vtab.idxStr); pNew->u.vtab.needFree = 0; } - WHERETRACE(0xffff, (" bIn=%d prereqIn=%04llx prereqOut=%04llx\n", + WHERETRACE(0xffffffff, (" bIn=%d prereqIn=%04llx prereqOut=%04llx\n", *pbIn, (sqlite3_uint64)mPrereq, (sqlite3_uint64)(pNew->prereq & ~mPrereq))); @@ -4031,7 +4175,7 @@ static int whereLoopAddVirtual( /* First call xBestIndex() with all constraints usable. */ WHERETRACE(0x800, ("BEGIN %s.addVirtual()\n", pSrc->pTab->zName)); - WHERETRACE(0x40, (" VirtualOne: all usable\n")); + WHERETRACE(0x800, (" VirtualOne: all usable\n")); rc = whereLoopAddVirtualOne( pBuilder, mPrereq, ALLBITS, 0, p, mNoOmit, &bIn, &bRetry ); @@ -4056,7 +4200,7 @@ static int whereLoopAddVirtual( /* If the plan produced by the earlier call uses an IN(...) term, call ** xBestIndex again, this time with IN(...) terms disabled. */ if( bIn ){ - WHERETRACE(0x40, (" VirtualOne: all usable w/o IN\n")); + WHERETRACE(0x800, (" VirtualOne: all usable w/o IN\n")); rc = whereLoopAddVirtualOne( pBuilder, mPrereq, ALLBITS, WO_IN, p, mNoOmit, &bIn, 0); assert( bIn==0 ); @@ -4082,7 +4226,7 @@ static int whereLoopAddVirtual( mPrev = mNext; if( mNext==ALLBITS ) break; if( mNext==mBest || mNext==mBestNoIn ) continue; - WHERETRACE(0x40, (" VirtualOne: mPrev=%04llx mNext=%04llx\n", + WHERETRACE(0x800, (" VirtualOne: mPrev=%04llx mNext=%04llx\n", (sqlite3_uint64)mPrev, (sqlite3_uint64)mNext)); rc = whereLoopAddVirtualOne( pBuilder, mPrereq, mNext|mPrereq, 0, p, mNoOmit, &bIn, 0); @@ -4096,7 +4240,7 @@ static int whereLoopAddVirtual( ** that requires no source tables at all (i.e. one guaranteed to be ** usable), make a call here with all source tables disabled */ if( rc==SQLITE_OK && seenZero==0 ){ - WHERETRACE(0x40, (" VirtualOne: all disabled\n")); + WHERETRACE(0x800, (" VirtualOne: all disabled\n")); rc = whereLoopAddVirtualOne( pBuilder, mPrereq, mPrereq, 0, p, mNoOmit, &bIn, 0); if( bIn==0 ) seenZeroNoIN = 1; @@ -4106,7 +4250,7 @@ static int whereLoopAddVirtual( ** that requires no source tables at all and does not use an IN(...) ** operator, make a final call to obtain one here. */ if( rc==SQLITE_OK && seenZeroNoIN==0 ){ - WHERETRACE(0x40, (" VirtualOne: all disabled and w/o IN\n")); + WHERETRACE(0x800, (" VirtualOne: all disabled and w/o IN\n")); rc = whereLoopAddVirtualOne( pBuilder, mPrereq, mPrereq, WO_IN, p, mNoOmit, &bIn, 0); } @@ -4162,7 +4306,7 @@ static int whereLoopAddOr( sSubBuild = *pBuilder; sSubBuild.pOrSet = &sCur; - WHERETRACE(0x200, ("Begin processing OR-clause %p\n", pTerm)); + WHERETRACE(0x400, ("Begin processing OR-clause %p\n", pTerm)); for(pOrTerm=pOrWC->a; pOrTermeOperator & WO_AND)!=0 ){ sSubBuild.pWC = &pOrTerm->u.pAndInfo->wc; @@ -4179,9 +4323,9 @@ static int whereLoopAddOr( } sCur.n = 0; #ifdef WHERETRACE_ENABLED - WHERETRACE(0x200, ("OR-term %d of %p has %d subterms:\n", + WHERETRACE(0x400, ("OR-term %d of %p has %d subterms:\n", (int)(pOrTerm-pOrWC->a), pTerm, sSubBuild.pWC->nTerm)); - if( sqlite3WhereTrace & 0x400 ){ + if( sqlite3WhereTrace & 0x20000 ){ sqlite3WhereClausePrint(sSubBuild.pWC); } #endif @@ -4243,7 +4387,7 @@ static int whereLoopAddOr( pNew->prereq = sSum.a[i].prereq; rc = whereLoopInsert(pBuilder, pNew); } - WHERETRACE(0x200, ("End processing OR-clause %p\n", pTerm)); + WHERETRACE(0x400, ("End processing OR-clause %p\n", pTerm)); } } return rc; @@ -4591,8 +4735,8 @@ static i8 wherePathSatisfiesOrderBy( if( pOBExpr->iTable!=iCur ) continue; if( pOBExpr->iColumn!=iColumn ) continue; }else{ - Expr *pIdxExpr = pIndex->aColExpr->a[j].pExpr; - if( sqlite3ExprCompareSkip(pOBExpr, pIdxExpr, iCur) ){ + Expr *pIxExpr = pIndex->aColExpr->a[j].pExpr; + if( sqlite3ExprCompareSkip(pOBExpr, pIxExpr, iCur) ){ continue; } } @@ -4724,37 +4868,56 @@ static const char *wherePathName(WherePath *pPath, int nLoop, WhereLoop *pLast){ ** order. */ static LogEst whereSortingCost( - WhereInfo *pWInfo, - LogEst nRow, - int nOrderBy, - int nSorted + WhereInfo *pWInfo, /* Query planning context */ + LogEst nRow, /* Estimated number of rows to sort */ + int nOrderBy, /* Number of ORDER BY clause terms */ + int nSorted /* Number of initial ORDER BY terms naturally in order */ ){ - /* TUNING: Estimated cost of a full external sort, where N is + /* Estimated cost of a full external sort, where N is ** the number of rows to sort is: ** - ** cost = (3.0 * N * log(N)). + ** cost = (K * N * log(N)). ** ** Or, if the order-by clause has X terms but only the last Y ** terms are out of order, then block-sorting will reduce the ** sorting cost to: ** - ** cost = (3.0 * N * log(N)) * (Y/X) + ** cost = (K * N * log(N)) * (Y/X) ** - ** The (Y/X) term is implemented using stack variable rScale - ** below. + ** The constant K is at least 2.0 but will be larger if there are a + ** large number of columns to be sorted, as the sorting time is + ** proportional to the amount of content to be sorted. The algorithm + ** does not currently distinguish between fat columns (BLOBs and TEXTs) + ** and skinny columns (INTs). It just uses the number of columns as + ** an approximation for the row width. + ** + ** And extra factor of 2.0 or 3.0 is added to the sorting cost if the sort + ** is built using OP_IdxInsert and OP_Sort rather than with OP_SorterInsert. */ - LogEst rScale, rSortCost; - assert( nOrderBy>0 && 66==sqlite3LogEst(100) ); - rScale = sqlite3LogEst((nOrderBy-nSorted)*100/nOrderBy) - 66; - rSortCost = nRow + rScale + 16; + LogEst rSortCost, nCol; + assert( pWInfo->pSelect!=0 ); + assert( pWInfo->pSelect->pEList!=0 ); + /* TUNING: sorting cost proportional to the number of output columns: */ + nCol = sqlite3LogEst((pWInfo->pSelect->pEList->nExpr+59)/30); + rSortCost = nRow + nCol; + if( nSorted>0 ){ + /* Scale the result by (Y/X) */ + rSortCost += sqlite3LogEst((nOrderBy-nSorted)*100/nOrderBy) - 66; + } /* Multiple by log(M) where M is the number of output rows. ** Use the LIMIT for M if it is smaller. Or if this sort is for ** a DISTINCT operator, M will be the number of distinct output ** rows, so fudge it downwards a bit. */ - if( (pWInfo->wctrlFlags & WHERE_USE_LIMIT)!=0 && pWInfo->iLimitiLimit; + if( (pWInfo->wctrlFlags & WHERE_USE_LIMIT)!=0 ){ + rSortCost += 10; /* TUNING: Extra 2.0x if using LIMIT */ + if( nSorted!=0 ){ + rSortCost += 6; /* TUNING: Extra 1.5x if also using partial sort */ + } + if( pWInfo->iLimitiLimit; + } }else if( (pWInfo->wctrlFlags & WHERE_WANT_DISTINCT) ){ /* TUNING: In the sort for a DISTINCT operator, assume that the DISTINCT ** reduces the number of output rows by a factor of 2 */ @@ -4906,11 +5069,11 @@ static int wherePathSolver(WhereInfo *pWInfo, LogEst nRowEst){ pWInfo, nRowEst, nOrderBy, isOrdered ); } - /* TUNING: Add a small extra penalty (5) to sorting as an + /* TUNING: Add a small extra penalty (3) to sorting as an ** extra encouragment to the query planner to select a plan ** where the rows emerge in the correct order without any sorting ** required. */ - rCost = sqlite3LogEstAdd(rUnsorted, aSortCost[isOrdered]) + 5; + rCost = sqlite3LogEstAdd(rUnsorted, aSortCost[isOrdered]) + 3; WHERETRACE(0x002, ("---- sort cost=%-3d (%d/%d) increases cost %3d to %-3d\n", @@ -5258,7 +5421,7 @@ static int whereShortCut(WhereLoopBuilder *pBuilder){ pLoop->cId = '0'; #endif #ifdef WHERETRACE_ENABLED - if( sqlite3WhereTrace ){ + if( sqlite3WhereTrace & 0x02 ){ sqlite3DebugPrintf("whereShortCut() used to compute solution\n"); } #endif @@ -5388,7 +5551,7 @@ static SQLITE_NOINLINE Bitmask whereOmitNoopJoin( } } if( pTerm drop loop %c not used\n", pLoop->cId)); + WHERETRACE(0xffffffff, ("-> drop loop %c not used\n", pLoop->cId)); notReady &= ~pLoop->maskSelf; for(pTerm=pWInfo->sWC.a; pTermprereqAll & pLoop->maskSelf)!=0 ){ @@ -5448,7 +5611,7 @@ static SQLITE_NOINLINE void whereCheckIfBloomFilterIsUseful( testcase( pItem->fg.jointype & JT_LEFT ); pLoop->wsFlags |= WHERE_BLOOMFILTER; pLoop->wsFlags &= ~WHERE_IDX_ONLY; - WHERETRACE(0xffff, ( + WHERETRACE(0xffffffff, ( "-> use Bloom-filter on loop %c because there are ~%.1e " "lookups into %s which has only ~%.1e rows\n", pLoop->cId, (double)sqlite3LogEstToInt(nSearch), pTab->zName, @@ -5461,13 +5624,13 @@ static SQLITE_NOINLINE void whereCheckIfBloomFilterIsUseful( /* ** This is an sqlite3ParserAddCleanup() callback that is invoked to -** free the Parse->pIdxExpr list when the Parse object is destroyed. +** free the Parse->pIdxEpr list when the Parse object is destroyed. */ static void whereIndexedExprCleanup(sqlite3 *db, void *pObject){ Parse *pParse = (Parse*)pObject; - while( pParse->pIdxExpr!=0 ){ - IndexedExpr *p = pParse->pIdxExpr; - pParse->pIdxExpr = p->pIENext; + while( pParse->pIdxEpr!=0 ){ + IndexedExpr *p = pParse->pIdxEpr; + pParse->pIdxEpr = p->pIENext; sqlite3ExprDelete(db, p->pExpr); sqlite3DbFreeNN(db, p); } @@ -5479,13 +5642,13 @@ static void whereIndexedExprCleanup(sqlite3 *db, void *pObject){ ** number for the index and iDataCur is the cursor number for the corresponding ** table. ** -** This routine adds IndexedExpr entries to the Parse->pIdxExpr field for +** This routine adds IndexedExpr entries to the Parse->pIdxEpr field for ** each of the expressions in the index so that the expression code generator ** will know to replace occurrences of the indexed expression with ** references to the corresponding column of the index. */ static SQLITE_NOINLINE void whereAddIndexedExpr( - Parse *pParse, /* Add IndexedExpr entries to pParse->pIdxExpr */ + Parse *pParse, /* Add IndexedExpr entries to pParse->pIdxEpr */ Index *pIdx, /* The index-on-expression that contains the expressions */ int iIdxCur, /* Cursor number for pIdx */ SrcItem *pTabItem /* The FROM clause entry for the table */ @@ -5514,7 +5677,13 @@ static SQLITE_NOINLINE void whereAddIndexedExpr( if( sqlite3ExprIsConstant(pExpr) ) continue; p = sqlite3DbMallocRaw(pParse->db, sizeof(IndexedExpr)); if( p==0 ) break; - p->pIENext = pParse->pIdxExpr; + p->pIENext = pParse->pIdxEpr; +#ifdef WHERETRACE_ENABLED + if( sqlite3WhereTrace & 0x200 ){ + sqlite3DebugPrintf("New pParse->pIdxEpr term {%d,%d}\n", iIdxCur, i); + if( sqlite3WhereTrace & 0x5000 ) sqlite3ShowExpr(pExpr); + } +#endif p->pExpr = sqlite3ExprDup(pParse->db, pExpr, 0); p->iDataCur = pTabItem->iCursor; p->iIdxCur = iIdxCur; @@ -5523,7 +5692,7 @@ static SQLITE_NOINLINE void whereAddIndexedExpr( #ifdef SQLITE_ENABLE_EXPLAIN_COMMENTS p->zIdxName = pIdx->zName; #endif - pParse->pIdxExpr = p; + pParse->pIdxEpr = p; if( p->pIENext==0 ){ sqlite3ParserAddCleanup(pParse, whereIndexedExprCleanup, pParse); } @@ -5815,13 +5984,13 @@ WhereInfo *sqlite3WhereBegin( /* Construct the WhereLoop objects */ #if defined(WHERETRACE_ENABLED) - if( sqlite3WhereTrace & 0xffff ){ + if( sqlite3WhereTrace & 0xffffffff ){ sqlite3DebugPrintf("*** Optimizer Start *** (wctrlFlags: 0x%x",wctrlFlags); if( wctrlFlags & WHERE_USE_LIMIT ){ sqlite3DebugPrintf(", limit: %d", iAuxArg); } sqlite3DebugPrintf(")\n"); - if( sqlite3WhereTrace & 0x100 ){ + if( sqlite3WhereTrace & 0x8000 ){ Select sSelect; memset(&sSelect, 0, sizeof(sSelect)); sSelect.selFlags = SF_WhereBegin; @@ -5831,10 +6000,10 @@ WhereInfo *sqlite3WhereBegin( sSelect.pEList = pResultSet; sqlite3TreeViewSelect(0, &sSelect, 0); } - } - if( sqlite3WhereTrace & 0x100 ){ /* Display all terms of the WHERE clause */ - sqlite3DebugPrintf("---- WHERE clause at start of analysis:\n"); - sqlite3WhereClausePrint(sWLB.pWC); + if( sqlite3WhereTrace & 0x4000 ){ /* Display all WHERE clause terms */ + sqlite3DebugPrintf("---- WHERE clause at start of analysis:\n"); + sqlite3WhereClausePrint(sWLB.pWC); + } } #endif @@ -5850,7 +6019,7 @@ WhereInfo *sqlite3WhereBegin( ** loops will be built using the revised truthProb values. */ if( sWLB.bldFlags2 & SQLITE_BLDF2_2NDPASS ){ WHERETRACE_ALL_LOOPS(pWInfo, sWLB.pWC); - WHERETRACE(0xffff, + WHERETRACE(0xffffffff, ("**** Redo all loop computations due to" " TERM_HIGHTRUTH changes ****\n")); while( pWInfo->pLoops ){ @@ -5936,11 +6105,11 @@ WhereInfo *sqlite3WhereBegin( } #if defined(WHERETRACE_ENABLED) - if( sqlite3WhereTrace & 0x100 ){ /* Display all terms of the WHERE clause */ + if( sqlite3WhereTrace & 0x4000 ){ /* Display all terms of the WHERE clause */ sqlite3DebugPrintf("---- WHERE clause at end of analysis:\n"); sqlite3WhereClausePrint(sWLB.pWC); } - WHERETRACE(0xffff,("*** Optimizer Finished ***\n")); + WHERETRACE(0xffffffff,("*** Optimizer Finished ***\n")); #endif pWInfo->pParse->nQueryLoop += pWInfo->nRowOut; @@ -6474,9 +6643,16 @@ void sqlite3WhereEnd(WhereInfo *pWInfo){ last = pWInfo->iEndWhere; } if( pIdx->bHasExpr ){ - IndexedExpr *p = pParse->pIdxExpr; + IndexedExpr *p = pParse->pIdxEpr; while( p ){ if( p->iIdxCur==pLevel->iIdxCur ){ +#ifdef WHERETRACE_ENABLED + if( sqlite3WhereTrace & 0x200 ){ + sqlite3DebugPrintf("Disable pParse->pIdxEpr term {%d,%d}\n", + p->iIdxCur, p->iIdxCol); + if( sqlite3WhereTrace & 0x5000 ) sqlite3ShowExpr(p->pExpr); + } +#endif p->iDataCur = -1; p->iIdxCur = -1; } diff --git a/src/whereInt.h b/src/whereInt.h index 28ede5be66..b89a4513e3 100644 --- a/src/whereInt.h +++ b/src/whereInt.h @@ -634,5 +634,6 @@ void sqlite3WhereTabFuncArgs(Parse*, SrcItem*, WhereClause*); #define WHERE_SELFCULL 0x00800000 /* nOut reduced by extra WHERE terms */ #define WHERE_OMIT_OFFSET 0x01000000 /* Set offset counter to zero */ #define WHERE_VIEWSCAN 0x02000000 /* A full-scan of a VIEW or subquery */ +#define WHERE_EXPRIDX 0x04000000 /* Uses an index-on-expressions */ #endif /* !defined(SQLITE_WHEREINT_H) */ diff --git a/src/wherecode.c b/src/wherecode.c index e36d1c9964..4c22e5dd61 100644 --- a/src/wherecode.c +++ b/src/wherecode.c @@ -270,6 +270,8 @@ int sqlite3WhereExplainBloomFilter( zMsg = sqlite3StrAccumFinish(&str); ret = sqlite3VdbeAddOp4(v, OP_Explain, sqlite3VdbeCurrentAddr(v), pParse->addrExplain, 0, zMsg,P4_DYNAMIC); + + sqlite3VdbeScanStatus(v, sqlite3VdbeCurrentAddr(v)-1, 0, 0, 0, 0); return ret; } #endif /* SQLITE_OMIT_EXPLAIN */ @@ -292,14 +294,27 @@ void sqlite3WhereAddScanStatus( ){ const char *zObj = 0; WhereLoop *pLoop = pLvl->pWLoop; - if( (pLoop->wsFlags & WHERE_VIRTUALTABLE)==0 && pLoop->u.btree.pIndex!=0 ){ + int wsFlags = pLoop->wsFlags; + int viaCoroutine = 0; + + if( (wsFlags & WHERE_VIRTUALTABLE)==0 && pLoop->u.btree.pIndex!=0 ){ zObj = pLoop->u.btree.pIndex->zName; }else{ zObj = pSrclist->a[pLvl->iFrom].zName; + viaCoroutine = pSrclist->a[pLvl->iFrom].fg.viaCoroutine; } sqlite3VdbeScanStatus( v, addrExplain, pLvl->addrBody, pLvl->addrVisit, pLoop->nOut, zObj ); + + if( viaCoroutine==0 ){ + if( (wsFlags & (WHERE_MULTI_OR|WHERE_AUTO_INDEX))==0 ){ + sqlite3VdbeScanStatusRange(v, addrExplain, -1, pLvl->iTabCur); + } + if( wsFlags & WHERE_INDEXED ){ + sqlite3VdbeScanStatusRange(v, addrExplain, -1, pLvl->iIdxCur); + } + } } #endif @@ -359,7 +374,7 @@ static void disableTerm(WhereLevel *pLevel, WhereTerm *pTerm){ pTerm->wtFlags |= TERM_CODED; } #ifdef WHERETRACE_ENABLED - if( sqlite3WhereTrace & 0x20000 ){ + if( (sqlite3WhereTrace & 0x4001)==0x4001 ){ sqlite3DebugPrintf("DISABLE-"); sqlite3WhereTermPrint(pTerm, (int)(pTerm - (pTerm->pWC->a))); } @@ -1346,13 +1361,15 @@ Bitmask sqlite3WhereCodeOneLoopStart( pLevel->notReady = notReady & ~sqlite3WhereGetMask(&pWInfo->sMaskSet, iCur); bRev = (pWInfo->revMask>>iLevel)&1; VdbeModuleComment((v, "Begin WHERE-loop%d: %s",iLevel,pTabItem->pTab->zName)); -#if WHERETRACE_ENABLED /* 0x20800 */ - if( sqlite3WhereTrace & 0x800 ){ +#if WHERETRACE_ENABLED /* 0x4001 */ + if( sqlite3WhereTrace & 0x1 ){ sqlite3DebugPrintf("Coding level %d of %d: notReady=%llx iFrom=%d\n", iLevel, pWInfo->nLevel, (u64)notReady, pLevel->iFrom); - sqlite3WhereLoopPrint(pLoop, pWC); + if( sqlite3WhereTrace & 0x1000 ){ + sqlite3WhereLoopPrint(pLoop, pWC); + } } - if( sqlite3WhereTrace & 0x20000 ){ + if( (sqlite3WhereTrace & 0x4001)==0x4001 ){ if( iLevel==0 ){ sqlite3DebugPrintf("WHERE clause being coded:\n"); sqlite3TreeViewExpr(0, pWInfo->pWhere, 0); @@ -2276,7 +2293,7 @@ Bitmask sqlite3WhereCodeOneLoopStart( } /* Loop through table entries that match term pOrTerm. */ ExplainQueryPlan((pParse, 1, "INDEX %d", ii+1)); - WHERETRACE(0xffff, ("Subplan for OR-clause:\n")); + WHERETRACE(0xffffffff, ("Subplan for OR-clause:\n")); pSubWInfo = sqlite3WhereBegin(pParse, pOrTab, pOrExpr, 0, 0, 0, WHERE_OR_SUBCLAUSE, iCovCur); assert( pSubWInfo || pParse->nErr ); @@ -2513,12 +2530,12 @@ Bitmask sqlite3WhereCodeOneLoopStart( } #endif } -#ifdef WHERETRACE_ENABLED /* 0xffff */ +#ifdef WHERETRACE_ENABLED /* 0xffffffff */ if( sqlite3WhereTrace ){ VdbeNoopComment((v, "WhereTerm[%d] (%p) priority=%d", pWC->nTerm-j, pTerm, iLoop)); } - if( sqlite3WhereTrace & 0x800 ){ + if( sqlite3WhereTrace & 0x4000 ){ sqlite3DebugPrintf("Coding auxiliary constraint:\n"); sqlite3WhereTermPrint(pTerm, pWC->nTerm-j); } @@ -2547,8 +2564,8 @@ Bitmask sqlite3WhereCodeOneLoopStart( if( pTerm->leftCursor!=iCur ) continue; if( pTabItem->fg.jointype & (JT_LEFT|JT_LTORJ|JT_RIGHT) ) continue; pE = pTerm->pExpr; -#ifdef WHERETRACE_ENABLED /* 0x800 */ - if( sqlite3WhereTrace & 0x800 ){ +#ifdef WHERETRACE_ENABLED /* 0x4001 */ + if( (sqlite3WhereTrace & 0x4001)==0x4001 ){ sqlite3DebugPrintf("Coding transitive constraint:\n"); sqlite3WhereTermPrint(pTerm, pWC->nTerm-j); } @@ -2663,13 +2680,13 @@ Bitmask sqlite3WhereCodeOneLoopStart( } } -#if WHERETRACE_ENABLED /* 0x20800 */ - if( sqlite3WhereTrace & 0x20000 ){ +#if WHERETRACE_ENABLED /* 0x4001 */ + if( sqlite3WhereTrace & 0x4000 ){ sqlite3DebugPrintf("All WHERE-clause terms after coding level %d:\n", iLevel); sqlite3WhereClausePrint(pWC); } - if( sqlite3WhereTrace & 0x800 ){ + if( sqlite3WhereTrace & 0x1 ){ sqlite3DebugPrintf("End Coding level %d: notReady=%llx\n", iLevel, (u64)pLevel->notReady); } diff --git a/src/window.c b/src/window.c index f13ea8b027..1ed3e49214 100644 --- a/src/window.c +++ b/src/window.c @@ -1069,7 +1069,7 @@ int sqlite3WindowRewrite(Parse *pParse, Select *p){ pSub = sqlite3SelectNew( pParse, pSublist, pSrc, pWhere, pGroupBy, pHaving, pSort, 0, 0 ); - SELECTTRACE(1,pParse,pSub, + TREETRACE(0x40,pParse,pSub, ("New window-function subquery in FROM clause of (%u/%p)\n", p->selId, p)); p->pSrc = sqlite3SrcListAppend(pParse, 0, 0, 0); diff --git a/test/affinity3.test b/test/affinity3.test index 48942de72e..3695ea8479 100644 --- a/test/affinity3.test +++ b/test/affinity3.test @@ -104,20 +104,20 @@ do_execsql_test affinity3-200 { CREATE TABLE mzed AS SELECT * FROM idmap; } -#do_execsql_test affinity3-210 { - #PRAGMA automatic_index=ON; - #SELECT * FROM data JOIN idmap USING(id); -#} {1 abc a 4 xyz e} +do_execsql_test affinity3-210 { + PRAGMA automatic_index=ON; + SELECT * FROM data JOIN idmap USING(id); +} {4 xyz e} do_execsql_test affinity3-220 { SELECT * FROM data JOIN mzed USING(id); -} {1 abc a 4 xyz e} +} {4 xyz e} do_execsql_test affinity3-250 { PRAGMA automatic_index=OFF; SELECT * FROM data JOIN idmap USING(id); -} {1 abc a 4 xyz e} +} {4 xyz e} do_execsql_test affinity3-260 { SELECT * FROM data JOIN mzed USING(id); -} {1 abc a 4 xyz e} +} {4 xyz e} finish_test diff --git a/test/basexx1.test b/test/basexx1.test new file mode 100644 index 0000000000..947a5678f3 --- /dev/null +++ b/test/basexx1.test @@ -0,0 +1,155 @@ +# 2022 November 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. +# +#*********************************************************************** +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +set testprefix basexx + +if {[catch {load_static_extension db basexx} error]} { + puts "Skipping basexx tests, hit load error: $error" + finish_test; return +} + +# Empty blobs encode to empty strings. +do_execsql_test 100 { + SELECT base64(x'')||base85(x''); +} {{}} + +# Empty strings decode to empty blobs. +do_execsql_test 101 { + SELECT hex(x'01'||base64('')||base85('')||x'02'); +} {0102} + +# Basic base64 encoding +do_execsql_test 102 { + SELECT base64(x'000102030405'); + SELECT base64(x'0001020304'); + SELECT base64(x'00010203'); +} {{AAECAwQF +} {AAECAwQ= +} {AAECAw== +}} + +# Basic base64 decoding with pad chars +do_execsql_test 103 { + SELECT hex(base64('AAECAwQF')); + SELECT hex(base64('AAECAwQ=')); + SELECT hex(base64('AAECAw==')); +} {000102030405 0001020304 00010203} + +# Basic base64 decoding without pad chars and with whitespace +do_execsql_test 104 { + SELECT hex(base64(' AAECAwQF ')); + SELECT hex(base64(' AAECAwQ')); + SELECT hex(base64('AAECAw ')); +} {000102030405 0001020304 00010203} + +# Basic base85 encoding +do_execsql_test 105 { + SELECT base85(x'000102030405'); + SELECT base85(x'0001020304'); + SELECT base85(x'00010203'); +} {{##/2,#2/ +} {##/2,#* +} {##/2, +}} + +# Basic base85 decoding with and without whitespace +do_execsql_test 106 { + SELECT hex(base85('##/2,#2/')); + SELECT hex(base85('##/2,#*')); + SELECT hex(base85('##/2,')); + SELECT hex(base85(' ##/2,#2/ ')); + SELECT hex(base85(' ##/2,#*')); + SELECT hex(base85('##/2, ')); +} {000102030405 0001020304 00010203 000102030405 0001020304 00010203} + +# Round-trip some random blobs. +do_execsql_test 107 { + CREATE TEMP TABLE rb( len int, b blob ) STRICT; + INSERT INTO rb(len) VALUES (1),(2),(3),(4),(5),(150),(151),(152),(153),(1054); + UPDATE rb SET b = randomblob(len); + SELECT len, base64(base64(b))=b, base85(base85(b))=b + FROM rb ORDER BY len; +} {1 1 1 2 1 1 3 1 1 4 1 1 5 1 1 150 1 1 151 1 1 152 1 1 153 1 1 1054 1 1} + +# Same round-trip but with space or junk prepended and/or appended or not. +do_execsql_test 108 { + CREATE TEMP TABLE junk(j text, rank int); + INSERT INTO junk VALUES ('',0),(' ',1),('~',2); + SELECT len, base64(j.j||base64(b)||j.j)=b, base85(j.j||base85(b)||j.j)=b + FROM rb r, junk j WHERE j.rank=(r.len+r.len/25)%3 ORDER BY len; +} {1 1 1 2 1 1 3 1 1 4 1 1 5 1 1 150 1 1 151 1 1 152 1 1 153 1 1 1054 1 1} + +# Exercise the fail-on-too-large result feature. + +set inLimit [sqlite3_limit db SQLITE_LIMIT_LENGTH -1] +sqlite3_limit db SQLITE_LIMIT_LENGTH 1300 + +do_catchsql_test 109 { + SELECT len, base64(b) FROM rb WHERE len>200; +} {1 {blob expanded to base64 too big}} + +do_catchsql_test 110 { + SELECT len, base85(b) FROM rb WHERE len>200; +} {1 {blob expanded to base85 too big}} + +do_catchsql_test 111 { + SELECT length(base85(b))=1335 FROM rb WHERE len=1054; +} {1 {blob expanded to base85 too big}} + +sqlite3_limit db SQLITE_LIMIT_LENGTH $inLimit + +# Exercise is_base85(t) + +do_execsql_test 112 { + SELECT is_base85(' '||base85(x'123456')||char(10)), + is_base85('#$%&*+,-./0123456789:;<=>?@' + ||'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + ||'[\]^_`' + ||'abcdefghijklmnopqrstuvwxyz'), + is_base85('!'), is_base85('"'), is_base85(''''), is_base85('('), + is_base85(')'), is_base85(char(123)), is_base85('|'), is_base85(char(125)), + is_base85('~'), is_base85(char(127)); +} {1 1 0 0 0 0 0 0 0 0 0 0} + +do_execsql_test 113 { + SELECT is_base85(NULL) IS NULL; +} {1} + +do_catchsql_test 114 { + SELECT is_base85(1); +} {1 {is_base85 accepts only text or NULL}} + +do_catchsql_test 115 { + SELECT is_base85(1.1); +} {1 {is_base85 accepts only text or NULL}} + +do_catchsql_test 116 { + SELECT is_base85(x'00'); +} {1 {is_base85 accepts only text or NULL}} + +# Round-trip many bigger random blobs. + +do_execsql_test 117 { + CREATE TABLE bs(b blob, num); + INSERT INTO bs SELECT randomblob(4000 + n%3), n + FROM ( + WITH RECURSIVE seq(n) AS ( + VALUES(1) UNION ALL SELECT n+1 + FROM seq WHERE n<100 + ) SELECT n FROM seq); + SELECT num FROM bs WHERE base64(base64(b))!=b; + SELECT num FROM bs WHERE base85(base85(b))!=b; +} {} + +finish_test diff --git a/test/cast.test b/test/cast.test index cbeec47c9d..7f7be8b620 100644 --- a/test/cast.test +++ b/test/cast.test @@ -481,7 +481,78 @@ do_execsql_test cast-9.0 { CREATE VIEW v1(c0, c1) AS SELECT CAST(0.0 AS NUMERIC), COUNT(*) OVER () FROM t0; SELECT v1.c0 FROM v1, t0 WHERE v1.c0=0; -} {0} +} {0.0} +# Set the 2022-12-10 "reopen" of ticket [https://sqlite.org/src/tktview/57c47526c3] +# +do_execsql_test cast-9.1 { + CREATE TABLE dual(dummy TEXT); + INSERT INTO dual VALUES('X'); + SELECT CAST(4 AS NUMERIC); +} {4} +do_execsql_test cast-9.2 { + SELECT CAST(4.0 AS NUMERIC); +} {4.0} +do_execsql_test cast-9.3 { + SELECT CAST(4.5 AS NUMERIC); +} {4.5} +do_execsql_test cast-9.4 { + SELECT x, typeof(x) FROM (SELECT CAST(4 AS NUMERIC) AS x) JOIN dual; +} {4 integer} +do_execsql_test cast-9.5 { + SELECT x, typeof(x) FROM dual CROSS JOIN (SELECT CAST(4 AS NUMERIC) AS x); +} {4 integer} +do_execsql_test cast-9.10 { + SELECT x, typeof(x) FROM (SELECT CAST(4.0 AS NUMERIC) AS x) JOIN dual; +} {4.0 real} +do_execsql_test cast-9.11 { + SELECT x, typeof(x) FROM dual CROSS JOIN (SELECT CAST(4.0 AS NUMERIC) AS x); +} {4.0 real} +do_execsql_test cast-9.12 { + SELECT x, typeof(x) FROM (SELECT CAST(4.5 AS NUMERIC) AS x) JOIN dual; +} {4.5 real} +do_execsql_test cast-9.13 { + SELECT x, typeof(x) FROM dual CROSS JOIN (SELECT CAST(4.5 AS NUMERIC) AS x); +} {4.5 real} + +# 2022-12-15 dbsqlfuzz c9ee6f9a0a8b8fefb02cf69de2a8b67ca39525c8 +# +# Added a new SQLITE_AFF_FLEXNUM that does not try to convert int to real or +# real to int. +# +do_execsql_test cast-10.1 { + VALUES(CAST(44 AS REAL)),(55); +} {44.0 55} +do_execsql_test cast-10.2 { + SELECT CAST(44 AS REAL) AS 'm' UNION ALL SELECT 55; +} {44.0 55} +do_execsql_test cast-10.3 { + SELECT * FROM (VALUES(CAST(44 AS REAL)),(55)); +} {44.0 55} +do_execsql_test cast-10.4 { + SELECT * FROM (SELECT CAST(44 AS REAL) AS 'm' UNION ALL SELECT 55); +} {44.0 55} +do_execsql_test cast-10.5 { + SELECT * FROM dual CROSS JOIN (VALUES(CAST(44 AS REAL)),(55)); +} {X 44.0 X 55} +do_execsql_test cast-10.6 { + SELECT * FROM dual CROSS JOIN (SELECT CAST(44 AS REAL) AS 'm' + UNION ALL SELECT 55); +} {X 44.0 X 55} +do_execsql_test cast-10.7 { + DROP VIEW v1; + CREATE VIEW v1 AS SELECT CAST(44 AS REAL) AS 'm' UNION ALL SELECT 55; + SELECT name, type FROM pragma_table_info('v1'); +} {m NUM} +do_execsql_test cast-10.8 { + CREATE VIEW v2 AS VALUES(CAST(44 AS REAL)),(55); + SELECT type FROM pragma_table_info('v2'); +} {NUM} +do_execsql_test cast-10.9 { + SELECT * FROM v1; +} {44.0 55} +do_execsql_test cast-10.10 { + SELECT * FROM v2; +} {44.0 55} finish_test diff --git a/test/eqp.test b/test/eqp.test index eda95776f2..61fd617d89 100644 --- a/test/eqp.test +++ b/test/eqp.test @@ -94,7 +94,7 @@ do_eqp_test 1.7.1 { SELECT * FROM t3 JOIN (SELECT 1) } { QUERY PLAN - |--MATERIALIZE (subquery-xxxxxx) + |--CO-ROUTINE (subquery-xxxxxx) | `--SCAN CONSTANT ROW |--SCAN (subquery-xxxxxx) `--SCAN t3 @@ -103,7 +103,7 @@ do_eqp_test 1.7.2 { SELECT * FROM t3 JOIN (SELECT 1) AS v1 } { QUERY PLAN - |--MATERIALIZE v1 + |--CO-ROUTINE v1 | `--SCAN CONSTANT ROW |--SCAN v1 `--SCAN t3 @@ -112,7 +112,7 @@ do_eqp_test 1.7.3 { SELECT * FROM t3 AS xx JOIN (SELECT 1) AS yy } { QUERY PLAN - |--MATERIALIZE yy + |--CO-ROUTINE yy | `--SCAN CONSTANT ROW |--SCAN yy `--SCAN xx @@ -123,7 +123,7 @@ do_eqp_test 1.8 { SELECT * FROM t3 JOIN (SELECT 1 UNION SELECT 2) } { QUERY PLAN - |--MATERIALIZE (subquery-xxxxxx) + |--CO-ROUTINE (subquery-xxxxxx) | `--COMPOUND QUERY | |--LEFT-MOST SUBQUERY | | `--SCAN CONSTANT ROW @@ -136,7 +136,7 @@ do_eqp_test 1.9 { SELECT * FROM t3 JOIN (SELECT 1 EXCEPT SELECT a FROM t3 LIMIT 17) AS abc } { QUERY PLAN - |--MATERIALIZE abc + |--CO-ROUTINE abc | `--COMPOUND QUERY | |--LEFT-MOST SUBQUERY | | `--SCAN CONSTANT ROW @@ -149,7 +149,7 @@ do_eqp_test 1.10 { SELECT * FROM t3 JOIN (SELECT 1 INTERSECT SELECT a FROM t3 LIMIT 17) AS abc } { QUERY PLAN - |--MATERIALIZE abc + |--CO-ROUTINE abc | `--COMPOUND QUERY | |--LEFT-MOST SUBQUERY | | `--SCAN CONSTANT ROW @@ -163,7 +163,7 @@ do_eqp_test 1.11 { SELECT * FROM t3 JOIN (SELECT 1 UNION ALL SELECT a FROM t3 LIMIT 17) abc } { QUERY PLAN - |--MATERIALIZE abc + |--CO-ROUTINE abc | `--COMPOUND QUERY | |--LEFT-MOST SUBQUERY | | `--SCAN CONSTANT ROW @@ -295,7 +295,7 @@ det 3.2.2 { ORDER BY x2.y LIMIT 5 } { QUERY PLAN - |--MATERIALIZE x1 + |--CO-ROUTINE x1 | |--SCAN t1 | `--USE TEMP B-TREE FOR ORDER BY |--MATERIALIZE x2 @@ -834,7 +834,7 @@ do_eqp_test 9.1 { ORDER BY 1; } { QUERY PLAN - |--MATERIALIZE thread + |--CO-ROUTINE thread | |--SCAN x USING INDEX forumthread | |--USING ROWID SEARCH ON TABLE private FOR IN-OPERATOR | |--CORRELATED SCALAR SUBQUERY xxxxxx diff --git a/test/fts3expr4.test b/test/fts3expr4.test index 4a6bd293c4..b9227aef55 100644 --- a/test/fts3expr4.test +++ b/test/fts3expr4.test @@ -50,7 +50,16 @@ do_icu_expr_test 1.6 { "(x OR y)" } {PHRASE 3 0 ( x or y )} # is passed to the tokenizer. # do_icu_expr_test 1.7 {a:word} {PHRASE 0 0 word} -do_icu_expr_test 1.8 {d:word} {PHRASE 3 0 d:word} +# do_icu_expr_test 1.8 {d:word} {PHRASE 3 0 d:word} +do_test 1.8 { + set res [ + db one {SELECT fts3_exprtest('icu en_US', 'd:word', 'a', 'b', 'c')} + ] + expr { + $res=="PHRASE 3 0 d:word" || + $res=="AND {AND {PHRASE 3 0 d} {PHRASE 3 0 :}} {PHRASE 3 0 word}" + } +} 1 set sqlite_fts3_enable_parentheses 0 diff --git a/test/fuzzdata8.db b/test/fuzzdata8.db index aff7e27340..4e5b957e0a 100644 Binary files a/test/fuzzdata8.db and b/test/fuzzdata8.db differ diff --git a/test/fuzzinvariants.c b/test/fuzzinvariants.c index c0ed2dde58..883f8cdfc9 100644 --- a/test/fuzzinvariants.c +++ b/test/fuzzinvariants.c @@ -136,16 +136,11 @@ int fuzz_invariant( } sqlite3_finalize(pCk); - if( sqlite3_strlike("%group%by%order%by%desc%",sqlite3_sql(pStmt),0)==0 ){ - /* dbsqlfuzz crash-647c162051c9b23ce091b7bbbe5125ce5f00e922 - ** Original statement is: - ** - ** SELECT a,c,d,b,'' FROM t1 GROUP BY 1 HAVING d<>345 ORDER BY a DESC; - ** - ** The values of c, d, and b are indeterminate and change when the - ** enclosed in the test query because the DESC is dropped. - ** - ** SELECT * FROM (...) WHERE "a"==0 + if( sqlite3_strlike("%group%by%",sqlite3_sql(pStmt),0)==0 ){ + /* + ** If there is a GROUP BY clause, it might not cover every term in the + ** output. And then non-covered terms can take on a value from any + ** row in the result set. This can cause differing answers. */ goto not_a_fault; } @@ -236,7 +231,7 @@ static char *fuzz_invariant_sql(sqlite3_stmt *pStmt, int iCnt){ const char *zIn; size_t nIn; const char *zAnd = "WHERE"; - int i; + int i, j; sqlite3_str *pTest; sqlite3_stmt *pBase = 0; sqlite3 *db = sqlite3_db_handle(pStmt); @@ -281,6 +276,14 @@ static char *fuzz_invariant_sql(sqlite3_stmt *pStmt, int iCnt){ ** WHERE clause. */ continue; } + for(j=0; j1 && i+2!=iCnt ) continue; if( zColName==0 ) continue; diff --git a/test/indexexpr1.test b/test/indexexpr1.test index 042132b81d..a5d1514bcd 100644 --- a/test/indexexpr1.test +++ b/test/indexexpr1.test @@ -420,7 +420,7 @@ do_execsql_test indexexpr1-1500 { # 2018-01-03 OSSFuzz discovers another test case for the same problem # above. # -do_execsql_test indexexpr-1510 { +do_execsql_test indexexpr1-1510 { DROP TABLE IF EXISTS t1; CREATE TABLE t1(a PRIMARY KEY,b UNIQUE); REPLACE INTO t1 VALUES(2, 1); @@ -434,18 +434,18 @@ do_execsql_test indexexpr-1510 { # a numeric table column, trouble can arise since there are multiple # string that can map to the same numeric value. (Ex: 123, 0123, 000123). # -do_execsql_test indexexpr-1600 { +do_execsql_test indexexpr1-1600 { DROP TABLE IF EXISTS t1; CREATE TABLE t1 (a INTEGER, b); CREATE INDEX idx1 ON t1 (lower(a)); INSERT INTO t1 VALUES('0001234',3); PRAGMA integrity_check; } {ok} -do_execsql_test indexexpr-1610 { +do_execsql_test indexexpr1-1610 { INSERT INTO t1 VALUES('1234',0),('001234',2),('01234',1); SELECT b FROM t1 WHERE lower(a)='1234' ORDER BY +b; } {0 1 2 3} -do_execsql_test indexexpr-1620 { +do_execsql_test indexexpr1-1620 { SELECT b FROM t1 WHERE lower(a)='01234' ORDER BY +b; } {} @@ -453,7 +453,7 @@ do_execsql_test indexexpr-1620 { # ExprImpliesExpr theorem prover bug: # "(NULL IS FALSE) IS FALSE" does not imply "NULL IS NULL" # -do_execsql_test indexexpr-1700 { +do_execsql_test indexexpr1-1700 { DROP TABLE IF EXISTS t0; CREATE TABLE t0(c0); INSERT INTO t0(c0) VALUES (0); @@ -468,17 +468,17 @@ do_execsql_test indexexpr-1700 { # computing the expression. # ifcapable like_match_blobs { - do_execsql_test indexexpr-1800 { + do_execsql_test indexexpr1-1800 { DROP TABLE IF EXISTS t0; CREATE TABLE t0(c0 REAL, c1 TEXT); CREATE INDEX i0 ON t0(+c0, c0); INSERT INTO t0(c0) VALUES(0); SELECT CAST(+ t0.c0 AS BLOB) LIKE 0 FROM t0; } {0} - do_execsql_test indexexpr-1810 { + do_execsql_test indexexpr1-1810 { SELECT CAST(+ t0.c0 AS BLOB) LIKE '0.0' FROM t0; } {1} - do_execsql_test indexexpr-1820 { + do_execsql_test indexexpr1-1820 { DROP TABLE IF EXISTS t1; CREATE TABLE t1(x REAL); CREATE INDEX t1x ON t1(x, +x); @@ -491,19 +491,80 @@ ifcapable like_match_blobs { # Assertion fault during a DELETE INDEXED BY. # reset_db -do_execsql_test indexexpr-1900 { +do_execsql_test indexexpr1-1900 { CREATE TABLE t1(x TEXT PRIMARY KEY, y TEXT, z INT); INSERT INTO t1(x,y,z) VALUES('alpha','ALPHA',1),('bravo','charlie',1); CREATE INDEX i1 ON t1(+y COLLATE NOCASE); SELECT * FROM t1; } {alpha ALPHA 1 bravo charlie 1} -do_execsql_test indexexpr-1910 { +do_execsql_test indexexpr1-1910 { DELETE FROM t1 INDEXED BY i1 WHERE x IS +y COLLATE NOCASE IN (SELECT z FROM t1) RETURNING *; } {alpha ALPHA 1} -do_execsql_test indexexpr-1920 { +do_execsql_test indexexpr1-1920 { SELECT * FROM t1; } {bravo charlie 1} +# 2022-11-28 Ticket 695a1a53de +# Improved ability to recognize that an index on an expression is a +# covering index. +# +reset_db +do_execsql_test indexexpr1-2000 { + CREATE TABLE t1(a INT, b TEXT); + INSERT INTO t1(a,b) VALUES + (10, '{"one":5,"two":6}'), + (10, '{"one":50,"two":60}'), + (10, '{"three":99}'), + (11, '{"one":100,"two":200}'); + CREATE INDEX t1_one ON t1(a, b->>'one'); + CREATE INDEX t1_two ON t1(a, b->>'two'); +} +do_execsql_test indexexpr1-2010 { + EXPLAIN QUERY PLAN + SELECT sum(b->>'one') FROM t1 WHERE a=10; /* Query AA */ +} {/.* t1_one .*/} +do_execsql_test indexexpr1-2011 { + SELECT sum(b->>'one') FROM t1 WHERE a=10; /* Query AA */ +} {55} +do_execsql_test indexexpr1-2020 { + EXPLAIN QUERY PLAN + SELECT sum(b->>'two') FROM t1 WHERE a=10; /* Query BB */ +} {/.* t1_two .*/} +do_execsql_test indexexpr1-2021 { + SELECT sum(b->>'two') FROM t1 WHERE a=10; /* Query BB */ +} {66} +do_execsql_test indexexpr1-2030 { + DROP TABLE t1; + CREATE TABLE t1(a INT, b TEXT, c INT, d INT); + INSERT INTO t1(a,b,c,d) VALUES + (1, '{"x":1}', 12, 3), + (1, '{"x":2}', 4, 5), + (1, '{"x":1}', 6, 11), + (2, '{"x":1}', 22, 3), + (2, '{"x":2}', 4, 5), + (3, '{"x":1}', 6, 7); + CREATE INDEX t1x ON t1(d, a, b->>'x', c); +} +do_execsql_test indexexpr1-2030 { + SELECT a, + SUM(1) AS t1, + SUM(CASE WHEN b->>'x'=1 THEN 1 END) AS t2, + SUM(c) AS t3, + SUM(CASE WHEN b->>'x'=1 THEN c END) AS t4 + FROM t1; +} {1 6 4 54 46} +do_execsql_test indexexpr1-2030 { + explain query plan + SELECT a, + SUM(1) AS t1, + SUM(CASE WHEN b->>'x'=1 THEN 1 END) AS t2, + SUM(c) AS t3, + SUM(CASE WHEN b->>'x'=1 THEN c END) AS t4 + FROM t1; +} {/.*SCAN t1 USING INDEX t1x.*/} + + + finish_test diff --git a/test/memdb2.test b/test/memdb2.test new file mode 100644 index 0000000000..286bfc3f84 --- /dev/null +++ b/test/memdb2.test @@ -0,0 +1,77 @@ +# 2022-12-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 implements regression tests for SQLite library. The +# focus of this file is the "memdb" VFS +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +set testprefix memdb2 +do_not_use_codec + +ifcapable !deserialize { + finish_test + return +} + +db close + +#------------------------------------------------------------------------- +# Test that when using a memdb database, it is not possible to upgrade +# to an EXCLUSIVE lock if some other client is holding SHARED. +# +foreach {tn fname} { + 1 file:/test.db?vfs=memdb + 2 file:\\test.db?vfs=memdb +} { + if {$tn==2} breakpoint + sqlite3 db $fname -uri 1 + sqlite3 db2 $fname -uri 1 + + + do_execsql_test 1.$tn.1 { + CREATE TABLE t1(x, y); + INSERT INTO t1 VALUES(1, 2); + } + + do_execsql_test -db db2 1.$tn.2 { + BEGIN; + SELECT * FROM t1; + } {1 2} + + do_execsql_test 1.$tn.3 { + BEGIN; + INSERT INTO t1 VALUES(3, 4); + } + + do_catchsql_test 1.$tn.4 { + COMMIT + } {1 {database is locked}} + + do_execsql_test -db db2 1.$tn.5 { + SELECT * FROM t1; + END; + } {1 2} + + do_execsql_test 1.$tn.6 { + COMMIT + } {} + + do_execsql_test -db db2 1.$tn.7 { + SELECT * FROM t1 + } {1 2 3 4} + + db close + db2 close +} + +finish_test + diff --git a/test/misc1.test b/test/misc1.test index b1b1b083c3..44914156cb 100644 --- a/test/misc1.test +++ b/test/misc1.test @@ -605,14 +605,17 @@ do_execsql_test misc1-19.2 { SELECT * FROM t19b; } {4 5 6} -# 2015-05-20: CREATE TABLE AS should not store INT value is a TEXT +# 2015-05-20: CREATE TABLE AS should not store INT value in a TEXT # column. # +# 2022-12-14: Change: The column is not TEXT if the AS SELECT is +# a compound with different types on each arm. +# do_execsql_test misc1-19.3 { CREATE TABLE t19c(x TEXT); CREATE TABLE t19d AS SELECT * FROM t19c UNION ALL SELECT 1234; SELECT x, typeof(x) FROM t19d; -} {1234 text} +} {1234 integer} # 2014-05-16: Tests for the SQLITE_TESTCTRL_FAULT_INSTALL feature. # @@ -622,11 +625,11 @@ proc fault_callback {n} { lappend ::fault_callbacks $n return 0 } -do_test misc1-19.1 { +do_test misc1-19.11 { sqlite3_test_control_fault_install fault_callback set fault_callbacks } {0} -do_test misc1-19.2 { +do_test misc1-19.12 { sqlite3_test_control_fault_install set fault_callbacks } {0} diff --git a/test/multiplex3.test b/test/multiplex3.test index c1e741acdb..3188350163 100644 --- a/test/multiplex3.test +++ b/test/multiplex3.test @@ -82,6 +82,8 @@ do_faultsim_test 1 -prep { multiplex_restore_db sqlite3 db file:test.db?8_3_names=1 sqlite3_multiplex_control db main chunk_size [expr 256*1024] + execsql { PRAGMA journal_mode = truncate } + execsql { PRAGMA synchronous = off } } -body { execsql { UPDATE t1 SET a=randomblob(12), b=randomblob(1500) WHERE (rowid%32)=0 diff --git a/test/orderby1.test b/test/orderby1.test index 9b5df4c753..7432b5473d 100644 --- a/test/orderby1.test +++ b/test/orderby1.test @@ -43,6 +43,7 @@ do_test 1.0 { (NULL, 1, 3, 'one-c'), (NULL, 2, 1, 'two-a'), (NULL, 3, 1, 'three-a'); + ANALYZE; COMMIT; } } {} @@ -180,6 +181,7 @@ do_test 2.0 { (1, 3, 'one-c'), (20, 1, 'two-a'), (3, 1, 'three-a'); + ANALYZE; COMMIT; } } {} @@ -327,6 +329,7 @@ do_test 3.0 { (NULL, 1, 3, 'one-c'), (NULL, 2, 1, 'two-a'), (NULL, 3, 1, 'three-a'); + ANALYZE; COMMIT; } } {} diff --git a/test/pushdown.test b/test/pushdown.test index af5162495b..ca816fb213 100644 --- a/test/pushdown.test +++ b/test/pushdown.test @@ -86,6 +86,41 @@ do_test 2.2 { set L } {three} - +# 2022-11-25 dbsqlfuzz crash-3a548de406a50e896c1bf7142692d35d339d697f +# Disable the push-down optimization for compound subqueries if any +# arm of the compound has an incompatible affinity. +# +reset_db +do_execsql_test 3.1 { + CREATE TABLE t0(c0 INT); + INSERT INTO t0 VALUES(0); + CREATE TABLE t1_a(a INTEGER PRIMARY KEY, b TEXT); + INSERT INTO t1_a VALUES(1,'one'); + CREATE TABLE t1_b(c INTEGER PRIMARY KEY, d TEXT); + INSERT INTO t1_b VALUES(2,'two'); + CREATE VIEW v0 AS SELECT CAST(t0.c0 AS INTEGER) AS c0 FROM t0; + CREATE VIEW v1(a,b) AS SELECT a, b FROM t1_a UNION ALL SELECT c, 0 FROM t1_b; + SELECT v1.a, quote(v1.b), t0.c0 AS cd FROM t0 LEFT JOIN v0 ON v0.c0!=0,v1; +} { + 1 'one' 0 + 2 0 0 +} +do_execsql_test 3.2 { + SELECT a, quote(b), cd FROM ( + SELECT v1.a, v1.b, t0.c0 AS cd FROM t0 LEFT JOIN v0 ON v0.c0!=0, v1 + ) WHERE a=2 AND b='0' AND cd=0; +} {} +do_execsql_test 3.3 { + SELECT a, quote(b), cd FROM ( + SELECT v1.a, v1.b, t0.c0 AS cd FROM t0 LEFT JOIN v0 ON v0.c0!=0, v1 + ) WHERE a=1 AND b='one' AND cd=0; +} {1 'one' 0} +do_execsql_test 3.4 { + SELECT a, quote(b), cd FROM ( + SELECT v1.a, v1.b, t0.c0 AS cd FROM t0 LEFT JOIN v0 ON v0.c0!=0, v1 + ) WHERE a=2 AND b=0 AND cd=0; +} { + 2 0 0 +} finish_test diff --git a/test/regexp1.test b/test/regexp1.test index 102c1280c0..0401b13d72 100644 --- a/test/regexp1.test +++ b/test/regexp1.test @@ -303,6 +303,33 @@ do_execsql_test regexp1-6.7 {SELECT 'xabc' REGEXP '(^abc|def)';} {0} do_execsql_test regexp1-6.8 {SELECT 'def' REGEXP '(^abc|def)';} {1} do_execsql_test regexp1-6.9 {SELECT 'xdef' REGEXP '(^abc|def)';} {1} +# 2022-11-17 +# https://sqlite.org/forum/forumpost/3ffe058b04 +# +do_execsql_test regexp1-7.1 { + SELECT char(0x61,0x7ff,0x62) REGEXP char(0x7ff); +} 1 +do_execsql_test regexp1-7.2 { + SELECT char(0x61,0x800,0x62) REGEXP char(0x800); +} 1 +do_execsql_test regexp1-7.3 { + SELECT char(0x61,0xabc,0x62) REGEXP char(0xabc); +} 1 +do_execsql_test regexp1-7.4 { + SELECT char(0x61,0xfff,0x62) REGEXP char(0xfff); +} 1 +do_execsql_test regexp1-7.5 { + SELECT char(0x61,0x1000,0x62) REGEXP char(0x1000); +} 1 +do_execsql_test regexp1-7.10 { + SELECT char(0x61,0xffff,0x62) REGEXP char(0xffff); +} 1 +do_execsql_test regexp1-7.11 { + SELECT char(0x61,0x10000,0x62) REGEXP char(0x10000); +} 1 +do_execsql_test regexp1-7.12 { + SELECT char(0x61,0x10ffff,0x62) REGEXP char(0x10ffff); +} 1 finish_test diff --git a/test/returning1.test b/test/returning1.test index fb058a6438..92e10ee9f4 100644 --- a/test/returning1.test +++ b/test/returning1.test @@ -376,4 +376,28 @@ do_execsql_test 16.1 { SELECT * FROM t2; } {1 2 3 a b c} + +foreach {tn temp} { + 1 "" + 2 TEMP +} { + reset_db + do_execsql_test 17.$tn.0 " + CREATE $temp TABLE foo ( + fooid INTEGER PRIMARY KEY, + fooval INTEGER NOT NULL UNIQUE, + refcnt INTEGER NOT NULL DEFAULT 1 + ); + " + do_execsql_test 17.$tn.1 { + INSERT INTO foo (fooval) VALUES (17), (4711), (17) + ON CONFLICT DO + UPDATE SET refcnt = refcnt+1 + RETURNING fooid; + } { + 1 2 1 + } +} + + finish_test diff --git a/test/scanstatus.test b/test/scanstatus.test index 46249f665d..caad70c5b4 100644 --- a/test/scanstatus.test +++ b/test/scanstatus.test @@ -36,7 +36,9 @@ proc do_scanstatus_test {tn res} { while {1} { set r [sqlite3_stmt_scanstatus $stmt $idx] if {[llength $r]==0} break - lappend ret {*}$r + foreach v {nLoop nVisit nEst zName zExplain} { + lappend ret $v [dict get $r $v] + } incr idx } @@ -312,8 +314,8 @@ do_execsql_test 5.1.1 { SELECT count(*) FROM t1 WHERE a IN (SELECT b FROM t1 AS ii) } {2} do_scanstatus_test 5.1.2 { - nLoop 1 nVisit 10 nEst 10.0 zName t1bc - zExplain {SCAN ii USING COVERING INDEX t1bc} + nLoop 1 nVisit 10 nEst 10.0 zName t1 + zExplain {SCAN ii} nLoop 1 nVisit 2 nEst 8.0 zName sqlite_autoindex_t1_1 zExplain {SEARCH t1 USING COVERING INDEX sqlite_autoindex_t1_1 (a=?)} } @@ -341,15 +343,15 @@ do_eqp_test 5.4.1 { SELECT count(*) FROM t1, t2 WHERE y = c; } { QUERY PLAN - |--SCAN t1 USING COVERING INDEX t1bc + |--SCAN t1 `--SEARCH t2 USING COVERING INDEX t2xy (ANY(x) AND y=?) } do_execsql_test 5.4.2 { SELECT count(*) FROM t1, t2 WHERE y = c; } {200} do_scanstatus_test 5.4.3 { - nLoop 1 nVisit 10 nEst 10.0 zName t1bc - zExplain {SCAN t1 USING COVERING INDEX t1bc} + nLoop 1 nVisit 10 nEst 10.0 zName t1 + zExplain {SCAN t1} nLoop 10 nVisit 200 nEst 56.0 zName t2xy zExplain {SEARCH t2 USING COVERING INDEX t2xy (ANY(x) AND y=?)} } diff --git a/test/scanstatus2.test b/test/scanstatus2.test new file mode 100644 index 0000000000..301c19e284 --- /dev/null +++ b/test/scanstatus2.test @@ -0,0 +1,235 @@ +# 2022 December 5 +# +# 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. +# +#*********************************************************************** +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +set testprefix scanstatus2 + +ifcapable !scanstatus { + finish_test + return +} + +do_execsql_test 1.0 { + CREATE TABLE t1(a, b); + CREATE TABLE t2(x, y); + INSERT INTO t1 VALUES(1, 2); + INSERT INTO t1 VALUES(3, 4); + INSERT INTO t2 VALUES('a', 'b'); + INSERT INTO t2 VALUES('c', 'd'); + INSERT INTO t2 VALUES('e', 'f'); +} + +proc do_zexplain_test {v2 tn sql res} { + db eval $sql + set stmt [db version -last-stmt-ptr] + set idx 0 + set ret [list] + + set cmd sqlite3_stmt_scanstatus + set f [list] + if {$v2} { lappend f complex } + + while {1} { + set r [sqlite3_stmt_scanstatus -flags $f $stmt $idx] + if {[llength $r]==0} break + lappend ret [dict get $r zExplain] + incr idx + } + uplevel [list do_test $tn [list set {} $ret] [list {*}$res]] +} + +proc get_cycles {stmt} { + set r [sqlite3_stmt_scanstatus $stmt -1] + dict get $r nCycle +} + +proc foreach_scan {varname stmt body} { + upvar $varname var + + for {set ii 0} {1} {incr ii} { + set r [sqlite3_stmt_scanstatus -flags complex $stmt $ii] + if {[llength $r]==0} break + array set var $r + uplevel $body + } +} + +proc get_eqp_graph {stmt iPar nIndent} { + set res "" + foreach_scan A $stmt { + if {$A(iParentId)==$iPar} { + set txt $A(zExplain) + if {$A(nCycle)>=0} { + append txt " (nCycle=$A(nCycle))" + } + append res "[string repeat - $nIndent]$txt\n" + append res [get_eqp_graph $stmt $A(iSelectId) [expr $nIndent+2]] + } + } + set res +} + +proc get_graph {stmt} { + set nCycle [get_cycles $stmt] + set res "QUERY (nCycle=$nCycle)\n" + append res [get_eqp_graph $stmt 0 2] +} + +proc do_graph_test {tn sql res} { + db eval $sql + set stmt [db version -last-stmt-ptr] + set graph [string trim [get_graph $stmt]] + + set graph [regsub -all {nCycle=[0-9]+} $graph nCycle=nnn] + uplevel [list do_test $tn [list set {} $graph] [string trim $res]] +} + +proc puts_graph {sql} { + db eval $sql + set stmt [db version -last-stmt-ptr] + puts [string trim [get_graph $stmt]] +} + + +do_zexplain_test 0 1.1 { + SELECT (SELECT a FROM t1 WHERE b=x) FROM t2 WHERE y=2 +} { + {SCAN t2} + {SCAN t1} +} +do_zexplain_test 1 1.2 { + SELECT (SELECT a FROM t1 WHERE b=x) FROM t2 WHERE y=2 +} { + {SCAN t2} + {CORRELATED SCALAR SUBQUERY 1} + {SCAN t1} +} + +do_graph_test 1.3 { + SELECT (SELECT a FROM t1 WHERE b=x) FROM t2 WHERE y=2 +} { +QUERY (nCycle=nnn) +--SCAN t2 (nCycle=nnn) +--CORRELATED SCALAR SUBQUERY 1 (nCycle=nnn) +----SCAN t1 (nCycle=nnn) +} + +do_graph_test 1.4 { + WITH v2(x,y) AS MATERIALIZED ( + SELECT x,y FROM t2 + ) + SELECT * FROM t1, v2 ORDER BY y; +} { +QUERY (nCycle=nnn) +--MATERIALIZE v2 (nCycle=nnn) +----SCAN t2 (nCycle=nnn) +--SCAN v2 (nCycle=nnn) +--SCAN t1 (nCycle=nnn) +--USE TEMP B-TREE FOR ORDER BY (nCycle=nnn) +} + +#------------------------------------------------------------------------- +reset_db +do_execsql_test 2.0 { + CREATE VIRTUAL TABLE ft USING fts5(a); + INSERT INTO ft VALUES('abc'); + INSERT INTO ft VALUES('def'); + INSERT INTO ft VALUES('ghi'); +} + +do_graph_test 2.1 { + SELECT * FROM ft('def') +} { +QUERY (nCycle=nnn) +--SCAN ft VIRTUAL TABLE INDEX 0:M1 (nCycle=nnn) +} + +#------------------------------------------------------------------------- +reset_db +do_execsql_test 3.0 { + CREATE TABLE x1(a, b); + CREATE TABLE x2(c, d); + + WITH s(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000) + INSERT INTO x1 SELECT i, i FROM s; + INSERT INTO x2 SELECT a, b FROM x1; +} + +do_graph_test 2.1 { + SELECT * FROM x1, x2 WHERE c=+a; +} { +QUERY (nCycle=nnn) +--SCAN x1 (nCycle=nnn) +--CREATE AUTOMATIC INDEX ON x2(c, d) (nCycle=nnn) +--SEARCH x2 USING AUTOMATIC COVERING INDEX (c=?) (nCycle=nnn) +} + +#------------------------------------------------------------------------- +reset_db +do_execsql_test 4.0 { + CREATE TABLE rt1 (id INTEGER PRIMARY KEY, x1, x2); + CREATE TABLE rt2 (id, x1, x2); +} + +do_graph_test 4.1 { + SELECT * FROM rt1, rt2 WHERE rt1.id%2 AND rt2.x1=rt1.x1; +} { +QUERY (nCycle=nnn) +--SCAN rt1 (nCycle=nnn) +--CREATE AUTOMATIC INDEX ON rt2(x1, id, x2) (nCycle=nnn) +--SEARCH rt2 USING AUTOMATIC COVERING INDEX (x1=?) (nCycle=nnn) +} + +do_graph_test 4.2 { + SELECT rt2.id FROM rt1, rt2 WHERE rt1.id%2 AND rt2.x1=rt1.x1; +} { +QUERY (nCycle=nnn) +--SCAN rt1 (nCycle=nnn) +--CREATE AUTOMATIC INDEX ON rt2(x1, id) (nCycle=nnn) +--SEARCH rt2 USING AUTOMATIC COVERING INDEX (x1=?) (nCycle=nnn) +} + +do_graph_test 4.3 { + SELECT rt2.id FROM rt1, rt2 WHERE rt1.id%2 AND (rt2.x1+1)=(rt1.x1+1); +} { +QUERY (nCycle=nnn) +--SCAN rt1 (nCycle=nnn) +--SCAN rt2 (nCycle=nnn) +} + +do_graph_test 4.4 { + SELECT rt2.id FROM rt1, rt2 WHERE rt1.id%2 AND rt2.x1=(rt1.x1+1) AND rt2.id>5; +} { +QUERY (nCycle=nnn) +--SCAN rt1 (nCycle=nnn) +--CREATE AUTOMATIC INDEX ON rt2(x1, id) WHERE (nCycle=nnn) +--SEARCH rt2 USING AUTOMATIC PARTIAL COVERING INDEX (x1=?) (nCycle=nnn) +} + +do_graph_test 4.5 { + SELECT v1.cnt FROM rt1, ( + SELECT count(*) AS cnt, rt2.x1 AS x1 FROM rt2 GROUP BY x1 + ) AS v1 WHERE rt1.x1=v1.x1 +} { +QUERY (nCycle=nnn) +--CO-ROUTINE v1 +----SCAN rt2 (nCycle=nnn) +----USE TEMP B-TREE FOR GROUP BY +--SCAN rt1 (nCycle=nnn) +--CREATE AUTOMATIC INDEX ON v1(x1, cnt) (nCycle=nnn) +--SEARCH v1 USING AUTOMATIC COVERING INDEX (x1=?) (nCycle=nnn) +} + +finish_test + + diff --git a/test/shell2.test b/test/shell2.test index 2237404e5a..6b5c2bc105 100644 --- a/test/shell2.test +++ b/test/shell2.test @@ -191,4 +191,16 @@ do_test shell2-1.4.7 { SELECT 'unclosed; ^--- error here}} +# Verify that safe mode rejects certain UDFs +# Reported at https://sqlite.org/forum/forumpost/07beac8056151b2f +do_test shell2-1.4.8 { + catchcmd "-safe :memory:" { + SELECT edit('DoNotCare');} +} {1 {line 2: cannot use the edit() function in safe mode}} +do_test shell2-1.4.9 { + catchcmd "-safe :memory:" { + SELECT writefile('DoNotCare', x'');} +} {1 {line 2: cannot use the writefile() function in safe mode}} + + finish_test diff --git a/test/shell4.test b/test/shell4.test index 068072202d..193dc8b69b 100644 --- a/test/shell4.test +++ b/test/shell4.test @@ -125,6 +125,12 @@ SELECT * FROM t1;}} do_test shell4-2.5 { catchcmd ":memory:" "CREATE TABLE t1(x);\n.trace stdout\nSELECT * FROM t1;" } {0 {SELECT * FROM t1;}} +do_test shell4-2.6 { + catchcmd ":memory:" { +CREATE TABLE t1(x); +.trace --stmt stdout +SELECT * FROM t1;} +} {0 {SELECT * FROM t1;}} } do_test shell4-3.1 { diff --git a/test/sort.test b/test/sort.test index d73ecea480..e6da6c6baf 100644 --- a/test/sort.test +++ b/test/sort.test @@ -595,4 +595,36 @@ do_execsql_test 17.1 { SELECT * FROM sqlite_master ORDER BY sql; } {} +# 2022-12-03 Ticket e8b674241947eb3b +# Improve estimates for the cost of sorting relative +# to the cost of doing an index lookup, so as to get +# a better query plan. See the ticket for a deetailed +# example. +# +reset_db +do_execsql_test 18.1 { + CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c); + WITH RECURSIVE c(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM c WHERE x<50) + -- increase to 5000 for actual test data ----^^ + INSERT INTO t1(a,b,c) SELECT x, random()%5000, random()%5000 FROM c; + CREATE TABLE t2(d,e,f); + WITH RECURSIVE c(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM c WHERE x<500) + -- increase to 50000 for actual test data -----^^^ + INSERT INTO t2(d,e,f) SELECT + NULLIF(0, random()%2), random()%5000, random()%5000 + FROM c; + ANALYZE; + UPDATE sqlite_stat1 SET stat='50000' WHERE tbl='t2'; + UPDATE sqlite_stat1 SET stat='5000' WHERE tbl='t1'; + ANALYZE sqlite_schema; +} {} +do_execsql_test 18.2 { + EXPLAIN QUERY PLAN + SELECT a FROM t1 JOIN t2 + WHERE a IN (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20) + AND a=CASE WHEN d IS NOT NULL THEN e ELSE f END + ORDER BY a; +} {/.*SCAN t2.*SEARCH t1.*/} +# ^^^^^^^--^^^^^^^^^--- t2 should be the outer loop. + finish_test diff --git a/test/tkt-99378177930f87bd.test b/test/tkt-99378177930f87bd.test new file mode 100644 index 0000000000..868f36c3a5 --- /dev/null +++ b/test/tkt-99378177930f87bd.test @@ -0,0 +1,179 @@ +# 2022-11-23 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# This file implements regression tests for SQLite library. +# +# This file implements tests to verify that the enhancement +# request documented by ticket 99378177930f87bd is working. +# +# The enhancement is that if an aggregate query with a GROUP BY clause +# uses subexpressions in the arguments to aggregate functions that are +# also columns of an index, then the values are pulled from the index +# rather than being recomputed. This has the potential to make some +# indexed queries works as if the index were covering. +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl + +do_execsql_test tkt-99378-100 { + CREATE TABLE t1(a INT, b TEXT, c INT, d INT); + INSERT INTO t1(a,b,c,d) VALUES + (1, '{"x":1}', 12, 3), + (1, '{"x":2}', 4, 5), + (1, '{"x":1}', 6, 11), + (2, '{"x":1}', 22, 3), + (2, '{"x":2}', 4, 5), + (3, '{"x":1}', 6, 7); + CREATE INDEX t1x ON t1(d, a, b->>'x', c); +} {} +do_execsql_test tkt-99378-110 { + SELECT a, + SUM(1) AS t1, + SUM(CASE WHEN b->>'x'=1 THEN 1 END) AS t2, + SUM(c) AS t3, + SUM(CASE WHEN b->>'x'=1 THEN c END) AS t4 + FROM t1 + WHERE d BETWEEN 0 and 10 + GROUP BY a; +} { + 1 2 1 16 12 + 2 2 1 26 22 + 3 1 1 6 6 +} + +# The proof that the index on the expression is being used is in the +# fact that the byte code contains no "Function" opcodes. In other words, +# the ->> operator (which is implemented by a function) is never invoked. +# Instead, the b->>'x' value is pulled out of the index. +# +do_execsql_test tkt-99378-120 { + EXPLAIN + SELECT a, + SUM(1) AS t1, + SUM(CASE WHEN b->>'x'=1 THEN 1 END) AS t2, + SUM(c) AS t3, + SUM(CASE WHEN b->>'x'=1 THEN c END) AS t4 + FROM t1 + WHERE d BETWEEN 0 and 10 + GROUP BY a; +} {~/Function/} + + +do_execsql_test tkt-99378-130 { + SELECT a, + SUM(1) AS t1, + SUM(CASE WHEN b->>'x'=1 THEN 1 END) AS t2, + SUM(c) AS t3, + SUM(CASE WHEN b->>'x'=1 THEN c END) AS t4 + FROM t1 + WHERE d BETWEEN 0 and 10 + GROUP BY +a; +} { + 1 2 1 16 12 + 2 2 1 26 22 + 3 1 1 6 6 +} +do_execsql_test tkt-99378-140 { + EXPLAIN + SELECT a, + SUM(1) AS t1, + SUM(CASE WHEN b->>'x'=1 THEN 1 END) AS t2, + SUM(c) AS t3, + SUM(CASE WHEN b->>'x'=1 THEN c END) AS t4 + FROM t1 + WHERE d BETWEEN 0 and 10 + GROUP BY +a; +} {~/Function/} + +do_execsql_test tkt-99378-200 { + DROP INDEX t1x; + CREATE INDEX t1x ON t1(a, d, b->>'x', c); +} +do_execsql_test tkt-99378-210 { + SELECT a, + SUM(1) AS t1, + SUM(CASE WHEN b->>'x'=1 THEN 1 END) AS t2, + SUM(c) AS t3, + SUM(CASE WHEN b->>'x'=1 THEN c END) AS t4 + FROM t1 + WHERE d BETWEEN 0 and 10 + GROUP BY a; +} { + 1 2 1 16 12 + 2 2 1 26 22 + 3 1 1 6 6 +} +do_execsql_test tkt-99378-220 { + EXPLAIN + SELECT a, + SUM(1) AS t1, + SUM(CASE WHEN b->>'x'=1 THEN 1 END) AS t2, + SUM(c) AS t3, + SUM(CASE WHEN b->>'x'=1 THEN c END) AS t4 + FROM t1 + WHERE d BETWEEN 0 and 10 + GROUP BY a; +} {~/Function/} +do_execsql_test tkt-99378-230 { + SELECT a, + SUM(1) AS t1, + SUM(CASE WHEN b->>'x'=1 THEN 1 END) AS t2, + SUM(c) AS t3, + SUM(CASE WHEN b->>'x'=1 THEN c END) AS t4 + FROM t1 + WHERE d BETWEEN 0 and 10 + GROUP BY a; +} { + 1 2 1 16 12 + 2 2 1 26 22 + 3 1 1 6 6 +} +do_execsql_test tkt-99378-240 { + EXPLAIN + SELECT a, + SUM(1) AS t1, + SUM(CASE WHEN b->>'x'=1 THEN 1 END) AS t2, + SUM(c) AS t3, + SUM(CASE WHEN b->>'x'=1 THEN c END) AS t4 + FROM t1 + WHERE d BETWEEN 0 and 10 + GROUP BY a; +} {~/Function/} + +# 2022-12-20 dbsqlfuzz a644e70d7683a7ca59c71861a153c1dccf8850b9 +# +do_execsql_test tkt-99378-300 { + DROP TABLE IF EXISTS t1; + CREATE TABLE t1(a INT); + CREATE INDEX i1 ON t1(a,a=a); + INSERT INTO t1 VALUES(1),(2),(3),(4); + SELECT * FROM t1 NATURAL JOIN t1 + WHERE a==1 + OR ( + (SELECT avg( + (SELECT sum((SELECT 1 FROM t1 NATURAL RIGHT JOIN t1 WHERE a=a)))) AS xyz + ) + AND a==2 + ); +} {1 2} +do_execsql_test tkt-99378-310 { + DROP INDEX i1; + SELECT * FROM t1 NATURAL JOIN t1 + WHERE a==1 + OR ( + (SELECT avg( + (SELECT sum((SELECT 1 FROM t1 NATURAL RIGHT JOIN t1 WHERE a=a)))) AS xyz + ) + AND a==2 + ); +} {1 2} + +finish_test diff --git a/test/unionall.test b/test/unionall.test index 7783c04a67..32fc76543a 100644 --- a/test/unionall.test +++ b/test/unionall.test @@ -408,37 +408,39 @@ do_execsql_test 8.1 { SELECT a, b FROM t1_a UNION ALL SELECT c, c FROM t1_b UNION ALL SELECT e, f FROM t1_c; -} + SELECT t1.a, t1.b, t0.c0 AS c, v0.c0 AS d FROM t0 LEFT JOIN v0 ON v0.c0>'0',t1; +} {1 one 0 {} 4 four 0 {} 2 2 0 {} 5 5 0 {} 3 three 0 {} 6 six 0 {}} + optimization_control db all 1 do_execsql_test 8.2 { SELECT * FROM (SELECT t1.a, t1.b, t0.c0 AS c, v0.c0 AS d FROM t0 LEFT JOIN v0 ON v0.c0>'0',t1) WHERE b=2; } {2 2 0 {}} do_execsql_test 8.3 { SELECT * FROM (SELECT t1.a, t1.b, t0.c0 AS c, v0.c0 AS d FROM t0 LEFT JOIN v0 ON v0.c0>'0',t1) WHERE b=2.0; -} {} +} {2 2 0 {}} do_execsql_test 8.4 { SELECT * FROM (SELECT t1.a, t1.b, t0.c0 AS c, v0.c0 AS d FROM t0 LEFT JOIN v0 ON v0.c0>'0',t1) WHERE b='2'; -} {2 2 0 {}} +} {} optimization_control db query-flattener,push-down 0 do_execsql_test 8.5 { SELECT * FROM (SELECT t1.a, t1.b, t0.c0 AS c, v0.c0 AS d FROM t0 LEFT JOIN v0 ON v0.c0>'0',t1) WHERE b=2; } {2 2 0 {}} do_execsql_test 8.6 { SELECT * FROM (SELECT t1.a, t1.b, t0.c0 AS c, v0.c0 AS d FROM t0 LEFT JOIN v0 ON v0.c0>'0',t1) WHERE b=2.0; -} {} +} {2 2 0 {}} do_execsql_test 8.7 { SELECT * FROM (SELECT t1.a, t1.b, t0.c0 AS c, v0.c0 AS d FROM t0 LEFT JOIN v0 ON v0.c0>'0',t1) WHERE b='2'; -} {2 2 0 {}} +} {} optimization_control db all 0 do_execsql_test 8.8 { SELECT * FROM (SELECT t1.a, t1.b, t0.c0 AS c, v0.c0 AS d FROM t0 LEFT JOIN v0 ON v0.c0>'0',t1) WHERE b=2; } {2 2 0 {}} do_execsql_test 8.9 { SELECT * FROM (SELECT t1.a, t1.b, t0.c0 AS c, v0.c0 AS d FROM t0 LEFT JOIN v0 ON v0.c0>'0',t1) WHERE b=2.0; -} {} +} {2 2 0 {}} do_execsql_test 8.10 { SELECT * FROM (SELECT t1.a, t1.b, t0.c0 AS c, v0.c0 AS d FROM t0 LEFT JOIN v0 ON v0.c0>'0',t1) WHERE b='2'; -} {2 2 0 {}} +} {} finish_test diff --git a/test/view.test b/test/view.test index b0b6da2686..60eb7e5277 100644 --- a/test/view.test +++ b/test/view.test @@ -123,16 +123,16 @@ do_execsql_test view-1.10 { } {} do_execsql_test view-1.11 { PRAGMA table_info(v9a); -} {0 x INTEGER 0 {} 0} +} {0 x INT 0 {} 0} do_execsql_test view-1.12 { PRAGMA table_info(v9b); -} {0 x INTEGER 0 {} 0} +} {0 x INT 0 {} 0} do_execsql_test view-1.13 { PRAGMA table_info(v9c); -} {0 x INTEGER 0 {} 0} +} {0 x INT 0 {} 0} do_execsql_test view-1.14 { PRAGMA table_info(v9d); -} {0 x INTEGER 0 {} 0} +} {0 x INT 0 {} 0} do_test view-2.1 { execsql { @@ -775,5 +775,30 @@ do_catchsql_test view-29.1 { SELECT name FROM sqlite_schema ORDER BY name; } {0 {t1 t2}} +#------------------------------------------------------------------------- +# 2022-12-11. https://sqlite.org/src/info/679ed6a2 +# +# 2022-12-14 change: If the AS SELECT of a VIEW is a compound where +# the datatypes on each arm of the compound are different, then the +# datatype of the overall column is BLOB (ANY). +# +reset_db +do_execsql_test view-30.0 { + CREATE TABLE t0(a INT, b TEXT); + + INSERT INTO t0 VALUES(1,'one'); + + CREATE VIEW t1 AS SELECT a, b FROM t0 UNION ALL SELECT 2, 2; + CREATE VIEW t2(a,b) AS SELECT a, b FROM t0 UNION ALL SELECT 2, 2; +} + +ifcapable schema_pragmas { + do_execsql_test view-30.1 { + PRAGMA table_info = t1; + } { 0 a INT 0 {} 0 1 b BLOB 0 {} 0 } + do_execsql_test view-30.2 { + PRAGMA table_info = t2; + } { 0 a INT 0 {} 0 1 b BLOB 0 {} 0 } +} finish_test diff --git a/test/where.test b/test/where.test index e28861bc10..4f7c2f84b2 100644 --- a/test/where.test +++ b/test/where.test @@ -545,6 +545,7 @@ do_test where-6.1 { CREATE INDEX t3acb ON t3(a,c,b); INSERT INTO t3 SELECT w, 101-w, y FROM t1; SELECT count(*), sum(a), sum(b), sum(c) FROM t3; + ANALYZE; } } {100 5050 5050 348550} do_test where-6.2 { @@ -1617,4 +1618,19 @@ do_execsql_test where-28.1 { 19 5 } +# 2022-12-07 Yong Heng [https://sqlite.org/forum/forumpost/dfe8084751] +# +do_execsql_test where-29.1 { + SELECT DISTINCT 'xyz' FROM pragma_cache_size + WHERE rowid OR abs(0) + ORDER BY + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1; +} {xyz} + finish_test diff --git a/test/with3.test b/test/with3.test index 1aed92a99a..85889453a0 100644 --- a/test/with3.test +++ b/test/with3.test @@ -89,7 +89,7 @@ ifcapable analyze { SELECT * FROM cnt, y1 WHERE i=a } [string map {"\n " \n} { QUERY PLAN - |--MATERIALIZE cnt + |--CO-ROUTINE cnt | |--SETUP | | `--SCAN CONSTANT ROW | `--RECURSIVE STEP @@ -103,7 +103,7 @@ ifcapable analyze { SELECT * FROM cnt, y1 WHERE i=a } [string map {"\n " \n} { QUERY PLAN - |--MATERIALIZE cnt + |--CO-ROUTINE cnt | |--SETUP | | `--SCAN CONSTANT ROW | `--RECURSIVE STEP @@ -125,7 +125,7 @@ do_eqp_test 3.2.2 { WHERE c.id=w2.pk AND c.id=w1.pk; } { QUERY PLAN - |--MATERIALIZE c + |--CO-ROUTINE c | |--SETUP | | |--SCAN CONSTANT ROW | | `--SCALAR SUBQUERY xxxxxx diff --git a/test/with6.test b/test/with6.test index 20e518b01a..7fce34687e 100644 --- a/test/with6.test +++ b/test/with6.test @@ -87,7 +87,7 @@ do_eqp_test 131 { (SELECT x FROM c LIMIT 5) AS c3; } { QUERY PLAN - |--MATERIALIZE c1 + |--CO-ROUTINE c1 | |--CO-ROUTINE c | | `--SCAN 2 CONSTANT ROWS | `--SCAN c @@ -123,7 +123,7 @@ do_eqp_test 141 { (SELECT x FROM c LIMIT 7) AS c3; } { QUERY PLAN - |--MATERIALIZE c1 + |--CO-ROUTINE c1 | |--MATERIALIZE c | | `--SCAN 2 CONSTANT ROWS | `--SCAN c @@ -151,7 +151,7 @@ do_eqp_test 151 { (SELECT x FROM c LIMIT 7) AS c3; } { QUERY PLAN - |--MATERIALIZE c1 + |--CO-ROUTINE c1 | |--MATERIALIZE c | | `--SCAN 2 CONSTANT ROWS | `--SCAN c @@ -228,7 +228,7 @@ do_eqp_test 211 { SELECT y FROM t2 ORDER BY y; } { QUERY PLAN - |--MATERIALIZE c1 + |--CO-ROUTINE c1 | |--CO-ROUTINE c | | `--SCAN 3 CONSTANT ROWS | `--SCAN c diff --git a/tool/mkopcodeh.tcl b/tool/mkopcodeh.tcl index 8b4e345c67..6fb3b75940 100644 --- a/tool/mkopcodeh.tcl +++ b/tool/mkopcodeh.tcl @@ -86,6 +86,7 @@ while {![eof $in]} { set in3($name) 0 set out2($name) 0 set out3($name) 0 + set ncycle($name) 0 for {set i 3} {$i<[llength $line]-1} {incr i} { switch [string trim [lindex $line $i] ,] { same { @@ -107,6 +108,7 @@ while {![eof $in]} { in3 {set in3($name) 1} out2 {set out2($name) 1} out3 {set out3($name) 1} + ncycle {set ncycle($name) 1} } } if {$group($name)} { @@ -140,6 +142,7 @@ foreach name {OP_Noop OP_Explain OP_Abortable} { set in3($name) 0 set out2($name) 0 set out3($name) 0 + set ncycle($name) 0 set op($name) -1 set order($nOp) $name incr nOp @@ -285,6 +288,7 @@ for {set i 0} {$i<=$max} {incr i} { if {$in3($name)} {incr x 8} if {$out2($name)} {incr x 16} if {$out3($name)} {incr x 32} + if {$ncycle($name)} {incr x 64} } set bv($i) $x } @@ -299,6 +303,7 @@ puts "#define OPFLG_IN2 0x04 /* in2: P2 is an input */" puts "#define OPFLG_IN3 0x08 /* in3: P3 is an input */" puts "#define OPFLG_OUT2 0x10 /* out2: P2 is an output */" puts "#define OPFLG_OUT3 0x20 /* out3: P3 is an output */" +puts "#define OPFLG_NCYCLE 0x40 /* ncycle:Cycles count against P1 */" puts "#define OPFLG_INITIALIZER \173\\" for {set i 0} {$i<=$max} {incr i} { if {$i%8==0} { diff --git a/tool/mksqlite3c.tcl b/tool/mksqlite3c.tcl index 0cdf514e44..bc1aadd194 100644 --- a/tool/mksqlite3c.tcl +++ b/tool/mksqlite3c.tcl @@ -40,11 +40,14 @@ set help {Usage: tclsh mksqlite3c.tcl set addstatic 1 set linemacros 0 set useapicall 0 +set enable_recover 0 set srcdir tsrc for {set i 0} {$i<[llength $argv]} {incr i} { set x [lindex $argv $i] - if {[regexp {^-?-nostatic$} $x]} { + if {[regexp {^-?-enable-recover$} $x]} { + set enable_recover 1 + } elseif {[regexp {^-?-nostatic$} $x]} { set addstatic 0 } elseif {[regexp {^-?-linemacros(?:=([01]))?$} $x ma ulm]} { if {$ulm == ""} {set ulm 1} @@ -78,7 +81,9 @@ close $in # Open the output file and write a header comment at the beginning # of the file. # -set out [open sqlite3.c w] +set fname sqlite3.c +if {$enable_recover} { set fname sqlite3r.c } +set out [open $fname w] # Force the output to use unix line endings, even on Windows. fconfigure $out -translation lf set today [clock format [clock seconds] -format "%Y-%m-%d %H:%M:%S UTC" -gmt 1] @@ -162,6 +167,7 @@ foreach hdr { vxworks.h wal.h whereInt.h + sqlite3recover.h } { set available_hdr($hdr) 1 } @@ -325,7 +331,7 @@ proc copy_file {filename} { # used subroutines first in order to help the compiler find # inlining opportunities. # -foreach file { +set flist { sqliteInt.h os_common.h ctime.c @@ -441,7 +447,11 @@ foreach file { sqlite3session.c fts5.c stmt.c -} { +} +if {$enable_recover} { + lappend flist sqlite3recover.c dbdata.c +} +foreach file $flist { copy_file $srcdir/$file } diff --git a/tool/mksqlite3h.tcl b/tool/mksqlite3h.tcl index 9078a15753..bd579c28b0 100644 --- a/tool/mksqlite3h.tcl +++ b/tool/mksqlite3h.tcl @@ -40,9 +40,16 @@ set TOP [lindex $argv 0] # set useapicall 0 +# Include sqlite3recover.h? +# +set enable_recover 0 + if {[lsearch -regexp [lrange $argv 1 end] {^-+useapicall}] != -1} { set useapicall 1 } +if {[lsearch -regexp [lrange $argv 1 end] {^-+enable-recover}] != -1} { + set enable_recover 1 +} # Get the SQLite version number (ex: 3.6.18) from the $TOP/VERSION file. # @@ -84,6 +91,9 @@ set filelist [subst { $TOP/ext/session/sqlite3session.h $TOP/ext/fts5/fts5.h }] +if {$enable_recover} { + lappend filelist "$TOP/ext/recover/sqlite3recover.h" +} # These are the functions that accept a variable number of arguments. They # always need to use the "cdecl" calling convention even when another calling diff --git a/tool/vdbe_profile.tcl b/tool/vdbe_profile.tcl index a0dc99ec33..b7240e3567 100644 --- a/tool/vdbe_profile.tcl +++ b/tool/vdbe_profile.tcl @@ -66,6 +66,8 @@ foreach stmt $allstmt { puts "********************************************************************" puts [string trim $sql($stmt)] puts "Execution count: $cnt($stmt)" + set tcx 0 + set ttx 0 for {set i 0} {[info exists stat($stmt,$i)]} {incr i} { foreach {cx tx detail} $stat($stmt,$i) break if {$cx==0} { @@ -74,7 +76,11 @@ foreach stmt $allstmt { set ax [expr {$tx/$cx}] } puts [format {%8d %12d %12d %4d %s} $cx $tx $ax $i $detail] + incr tcx $cx + incr ttx $tx } + set tax [expr {$tcx>0?$ttx/$tcx:0}] + puts [format {%8d %12d %12d TOTAL} $tcx $ttx $tax] } puts "********************************************************************" puts "OPCODES:"