CD to `c:\Tcl\lib`. In that subfolder make a copy of the
+ "`tcl86t.lib`" file to the alternative name "`tcl86.lib`"
+ (omitting the second 't'). Leave the copy in the same directory
+ as the original.
+
CD to `c:\Tcl\bin`. Make a copy of the "`tclsh86t.exe`"
+ file into "`tclsh.exe`" (without the "86t") in the same directory.
+
Add `c:\Tcl\bin` to your %PATH%. To do this, go to Settings
+ and search for "path". Select "edit environment variables for
+ your account" and modify your default PATH accordingly.
+ You will need to close and reopen your command prompts after
+ making this change.
+
+
+ 4. Download the SQLite source tree and unpack it. CD into the
+ toplevel directory of the source tree.
+
+ 5. Set the TCLDIR environment variable to point to your TCL installation.
+ Like this:
+
+
`set TCLDIR=c:\Tcl`
+
+
+ 6. Run the "`Makefile.msc`" makefile with an appropriate target.
+ Examples:
+
+
`nmake /f makefile.msc`
+
`nmake /f makefile.msc sqlite3.c`
+
`nmake /f makefile.msc devtest`
+
`nmake /f makefile.msc releasetest`
+
+
+## 32-bit Builds
+
+Doing a 32-bit build is just like doing a 64-bit build with the
+following minor changes:
+
+ 1. Use the "x86 Native Tools Command Prompt" instead of
+ "x64 Native Tools Command Prompt". "**x86**" instead of "**x64**".
+
+ 2. Use a different installation directory for TCL.
+ The recommended directory is `c:\tcl32`. Thus you end up
+ with two TCL builds:
+
+
`c:\tcl` ← 64-bit (the default)
+
`c:\tcl32` ← 32-bit
+
+
+ 3. Ensure that `c:\tcl32\bin` comes before `c:\tcl\bin` on
+ your PATH environment variable. You can achieve this using
+ a command like:
+
Lemon was originally written by Richard Hipp sometime in the late
1980s on a Sun4 Workstation using K&R C.
-There was a companion LL(1) parser generator program named "Lime", the
-source code to which as been lost.
+There was a companion LL(1) parser generator program named "Lime".
+The Lime source code has been lost.
The lemon.c source file was originally many separate files that were
compiled together to generate the "lemon" executable. Sometime in the
diff --git a/ext/fts3/fts3_write.c b/ext/fts3/fts3_write.c
index 393f8a871..32b483b34 100644
--- a/ext/fts3/fts3_write.c
+++ b/ext/fts3/fts3_write.c
@@ -4347,6 +4347,7 @@ static int fts3IncrmergeLoad(
for(i=nHeight; i>=0 && rc==SQLITE_OK; i--){
NodeReader reader;
+ memset(&reader, 0, sizeof(reader));
pNode = &pWriter->aNodeWriter[i];
if( pNode->block.a){
diff --git a/ext/fts5/fts5.h b/ext/fts5/fts5.h
index 081e534f3..323d73a28 100644
--- a/ext/fts5/fts5.h
+++ b/ext/fts5/fts5.h
@@ -263,7 +263,7 @@ struct Fts5PhraseIter {
** See xPhraseFirstColumn above.
*/
struct Fts5ExtensionApi {
- int iVersion; /* Currently always set to 3 */
+ int iVersion; /* Currently always set to 2 */
void *(*xUserData)(Fts5Context*);
@@ -492,8 +492,8 @@ struct Fts5ExtensionApi {
** as separate queries of the FTS index are required for each synonym.
**
** When using methods (2) or (3), it is important that the tokenizer only
-** provide synonyms when tokenizing document text (method (2)) or query
-** text (method (3)), not both. Doing so will not cause any errors, but is
+** provide synonyms when tokenizing document text (method (3)) or query
+** text (method (2)), not both. Doing so will not cause any errors, but is
** inefficient.
*/
typedef struct Fts5Tokenizer Fts5Tokenizer;
@@ -541,7 +541,7 @@ struct fts5_api {
int (*xCreateTokenizer)(
fts5_api *pApi,
const char *zName,
- void *pContext,
+ void *pUserData,
fts5_tokenizer *pTokenizer,
void (*xDestroy)(void*)
);
@@ -550,7 +550,7 @@ struct fts5_api {
int (*xFindTokenizer)(
fts5_api *pApi,
const char *zName,
- void **ppContext,
+ void **ppUserData,
fts5_tokenizer *pTokenizer
);
@@ -558,7 +558,7 @@ struct fts5_api {
int (*xCreateFunction)(
fts5_api *pApi,
const char *zName,
- void *pContext,
+ void *pUserData,
fts5_extension_function xFunction,
void (*xDestroy)(void*)
);
diff --git a/ext/fts5/fts5Int.h b/ext/fts5/fts5Int.h
index 5d05da875..8bbafbaaf 100644
--- a/ext/fts5/fts5Int.h
+++ b/ext/fts5/fts5Int.h
@@ -154,6 +154,10 @@ typedef struct Fts5Config Fts5Config;
** attempt to merge together. A value of 1 sets the object to use the
** compile time default. Zero disables auto-merge altogether.
**
+** bContentlessDelete:
+** True if the contentless_delete option was present in the CREATE
+** VIRTUAL TABLE statement.
+**
** zContent:
**
** zContentRowid:
@@ -188,6 +192,7 @@ struct Fts5Config {
int nPrefix; /* Number of prefix indexes */
int *aPrefix; /* Sizes in bytes of nPrefix prefix indexes */
int eContent; /* An FTS5_CONTENT value */
+ int bContentlessDelete; /* "contentless_delete=" option (dflt==0) */
char *zContent; /* content table */
char *zContentRowid; /* "content_rowid=" option value */
int bColumnsize; /* "columnsize=" option value (dflt==1) */
@@ -209,6 +214,7 @@ struct Fts5Config {
char *zRank; /* Name of rank function */
char *zRankArgs; /* Arguments to rank function */
int bSecureDelete; /* 'secure-delete' */
+ int nDeleteMerge; /* 'deletemerge' */
/* If non-NULL, points to sqlite3_vtab.base.zErrmsg. Often NULL. */
char **pzErrmsg;
@@ -531,6 +537,9 @@ int sqlite3Fts5IndexReset(Fts5Index *p);
int sqlite3Fts5IndexLoadConfig(Fts5Index *p);
+int sqlite3Fts5IndexGetOrigin(Fts5Index *p, i64 *piOrigin);
+int sqlite3Fts5IndexContentlessDelete(Fts5Index *p, i64 iOrigin, i64 iRowid);
+
/*
** End of interface to code in fts5_index.c.
**************************************************************************/
@@ -615,6 +624,11 @@ int sqlite3Fts5HashWrite(
*/
void sqlite3Fts5HashClear(Fts5Hash*);
+/*
+** Return true if the hash is empty, false otherwise.
+*/
+int sqlite3Fts5HashIsEmpty(Fts5Hash*);
+
int sqlite3Fts5HashQuery(
Fts5Hash*, /* Hash table to query */
int nPre,
@@ -636,6 +650,7 @@ void sqlite3Fts5HashScanEntry(Fts5Hash *,
);
+
/*
** End of interface to code in fts5_hash.c.
**************************************************************************/
diff --git a/ext/fts5/fts5_config.c b/ext/fts5/fts5_config.c
index 7a4c7b817..5d0770502 100644
--- a/ext/fts5/fts5_config.c
+++ b/ext/fts5/fts5_config.c
@@ -22,6 +22,8 @@
#define FTS5_DEFAULT_CRISISMERGE 16
#define FTS5_DEFAULT_HASHSIZE (1024*1024)
+#define FTS5_DEFAULT_DELETE_AUTOMERGE 10 /* default 10% */
+
/* Maximum allowed page size */
#define FTS5_MAX_PAGE_SIZE (64*1024)
@@ -352,6 +354,16 @@ static int fts5ConfigParseSpecial(
return rc;
}
+ if( sqlite3_strnicmp("contentless_delete", zCmd, nCmd)==0 ){
+ if( (zArg[0]!='0' && zArg[0]!='1') || zArg[1]!='\0' ){
+ *pzErr = sqlite3_mprintf("malformed contentless_delete=... directive");
+ rc = SQLITE_ERROR;
+ }else{
+ pConfig->bContentlessDelete = (zArg[0]=='1');
+ }
+ return rc;
+ }
+
if( sqlite3_strnicmp("content_rowid", zCmd, nCmd)==0 ){
if( pConfig->zContentRowid ){
*pzErr = sqlite3_mprintf("multiple content_rowid=... directives");
@@ -596,6 +608,28 @@ int sqlite3Fts5ConfigParse(
sqlite3_free(zTwo);
}
+ /* We only allow contentless_delete=1 if the table is indeed contentless. */
+ if( rc==SQLITE_OK
+ && pRet->bContentlessDelete
+ && pRet->eContent!=FTS5_CONTENT_NONE
+ ){
+ *pzErr = sqlite3_mprintf(
+ "contentless_delete=1 requires a contentless table"
+ );
+ rc = SQLITE_ERROR;
+ }
+
+ /* We only allow contentless_delete=1 if columnsize=0 is not present.
+ **
+ ** This restriction may be removed at some point.
+ */
+ if( rc==SQLITE_OK && pRet->bContentlessDelete && pRet->bColumnsize==0 ){
+ *pzErr = sqlite3_mprintf(
+ "contentless_delete=1 is incompatible with columnsize=0"
+ );
+ rc = SQLITE_ERROR;
+ }
+
/* If a tokenizer= option was successfully parsed, the tokenizer has
** already been allocated. Otherwise, allocate an instance of the default
** tokenizer (unicode61) now. */
@@ -890,6 +924,18 @@ int sqlite3Fts5ConfigSetValue(
}
}
+ else if( 0==sqlite3_stricmp(zKey, "deletemerge") ){
+ int nVal = -1;
+ if( SQLITE_INTEGER==sqlite3_value_numeric_type(pVal) ){
+ nVal = sqlite3_value_int(pVal);
+ }else{
+ *pbBadkey = 1;
+ }
+ if( nVal<0 ) nVal = FTS5_DEFAULT_DELETE_AUTOMERGE;
+ if( nVal>100 ) nVal = 0;
+ pConfig->nDeleteMerge = nVal;
+ }
+
else if( 0==sqlite3_stricmp(zKey, "rank") ){
const char *zIn = (const char*)sqlite3_value_text(pVal);
char *zRank;
@@ -938,6 +984,7 @@ int sqlite3Fts5ConfigLoad(Fts5Config *pConfig, int iCookie){
pConfig->nUsermerge = FTS5_DEFAULT_USERMERGE;
pConfig->nCrisisMerge = FTS5_DEFAULT_CRISISMERGE;
pConfig->nHashSize = FTS5_DEFAULT_HASHSIZE;
+ pConfig->nDeleteMerge = FTS5_DEFAULT_DELETE_AUTOMERGE;
zSql = sqlite3Fts5Mprintf(&rc, zSelect, pConfig->zDb, pConfig->zName);
if( zSql ){
diff --git a/ext/fts5/fts5_expr.c b/ext/fts5/fts5_expr.c
index 0e018420d..f5101ba06 100644
--- a/ext/fts5/fts5_expr.c
+++ b/ext/fts5/fts5_expr.c
@@ -2477,7 +2477,7 @@ Fts5ExprNode *sqlite3Fts5ParseImplicitAnd(
return pRet;
}
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
static char *fts5ExprTermPrint(Fts5ExprTerm *pTerm){
sqlite3_int64 nByte = 0;
Fts5ExprTerm *p;
@@ -2583,6 +2583,8 @@ static char *fts5ExprPrintTcl(
if( zRet==0 ) return 0;
}
+ }else if( pExpr->eType==0 ){
+ zRet = sqlite3_mprintf("{}");
}else{
char const *zOp = 0;
int i;
@@ -2844,14 +2846,14 @@ static void fts5ExprFold(
sqlite3_result_int(pCtx, sqlite3Fts5UnicodeFold(iCode, bRemoveDiacritics));
}
}
-#endif /* ifdef SQLITE_TEST */
+#endif /* if SQLITE_TEST || SQLITE_FTS5_DEBUG */
/*
** This is called during initialization to register the fts5_expr() scalar
** UDF with the SQLite handle passed as the only argument.
*/
int sqlite3Fts5ExprInit(Fts5Global *pGlobal, sqlite3 *db){
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
struct Fts5ExprFunc {
const char *z;
void (*x)(sqlite3_context*,int,sqlite3_value**);
diff --git a/ext/fts5/fts5_hash.c b/ext/fts5/fts5_hash.c
index bc9244fc0..7e50c3660 100644
--- a/ext/fts5/fts5_hash.c
+++ b/ext/fts5/fts5_hash.c
@@ -475,7 +475,6 @@ static int fts5HashEntrySort(
pList = fts5HashEntryMerge(pList, ap[i]);
}
- pHash->nEntry = 0;
sqlite3_free(ap);
*ppSorted = pList;
return SQLITE_OK;
@@ -529,6 +528,28 @@ int sqlite3Fts5HashScanInit(
return fts5HashEntrySort(p, pTerm, nTerm, &p->pScan);
}
+#ifdef SQLITE_DEBUG
+static int fts5HashCount(Fts5Hash *pHash){
+ int nEntry = 0;
+ int ii;
+ for(ii=0; iinSlot; ii++){
+ Fts5HashEntry *p = 0;
+ for(p=pHash->aSlot[ii]; p; p=p->pHashNext){
+ nEntry++;
+ }
+ }
+ return nEntry;
+}
+#endif
+
+/*
+** Return true if the hash table is empty, false otherwise.
+*/
+int sqlite3Fts5HashIsEmpty(Fts5Hash *pHash){
+ assert( pHash->nEntry==fts5HashCount(pHash) );
+ return pHash->nEntry==0;
+}
+
void sqlite3Fts5HashScanNext(Fts5Hash *p){
assert( !sqlite3Fts5HashScanEof(p) );
p->pScan = p->pScan->pScanNext;
diff --git a/ext/fts5/fts5_index.c b/ext/fts5/fts5_index.c
index eaeeeff4f..267489a7e 100644
--- a/ext/fts5/fts5_index.c
+++ b/ext/fts5/fts5_index.c
@@ -56,6 +56,24 @@
#define FTS5_MAX_LEVEL 64
+/*
+** There are two versions of the format used for the structure record:
+**
+** 1. the legacy format, that may be read by all fts5 versions, and
+**
+** 2. the V2 format, which is used by contentless_delete=1 databases.
+**
+** Both begin with a 4-byte "configuration cookie" value. Then, a legacy
+** format structure record contains a varint - the number of levels in
+** the structure. Whereas a V2 structure record contains the constant
+** 4 bytes [0xff 0x00 0x00 0x01]. This is unambiguous as the value of a
+** varint has to be at least 16256 to begin with "0xFF". And the default
+** maximum number of levels is 64.
+**
+** See below for more on structure record formats.
+*/
+#define FTS5_STRUCTURE_V2 "\xFF\x00\x00\x01"
+
/*
** Details:
**
@@ -63,7 +81,7 @@
**
** CREATE TABLE %_data(id INTEGER PRIMARY KEY, block BLOB);
**
-** , contains the following 5 types of records. See the comments surrounding
+** , contains the following 6 types of records. See the comments surrounding
** the FTS5_*_ROWID macros below for a description of how %_data rowids are
** assigned to each fo them.
**
@@ -71,13 +89,13 @@
**
** The set of segments that make up an index - the index structure - are
** recorded in a single record within the %_data table. The record consists
-** of a single 32-bit configuration cookie value followed by a list of
-** SQLite varints. If the FTS table features more than one index (because
-** there are one or more prefix indexes), it is guaranteed that all share
-** the same cookie value.
+** of a single 32-bit configuration cookie value followed by a list of
+** SQLite varints.
**
-** Immediately following the configuration cookie, the record begins with
-** three varints:
+** If the structure record is a V2 record, the configuration cookie is
+** followed by the following 4 bytes: [0xFF 0x00 0x00 0x01].
+**
+** Next, the record continues with three varints:
**
** + number of levels,
** + total number of segments on all levels,
@@ -92,6 +110,12 @@
** + first leaf page number (often 1, always greater than 0)
** + final leaf page number
**
+** Then, for V2 structures only:
+**
+** + lower origin counter value,
+** + upper origin counter value,
+** + the number of tombstone hash pages.
+**
** 2. The Averages Record:
**
** A single record within the %_data table. The data is a list of varints.
@@ -207,6 +231,38 @@
** * A list of delta-encoded varints - the first rowid on each subsequent
** child page.
**
+** 6. Tombstone Hash Page
+**
+** These records are only ever present in contentless_delete=1 tables.
+** There are zero or more of these associated with each segment. They
+** are used to store the tombstone rowids for rows contained in the
+** associated segments.
+**
+** The set of nHashPg tombstone hash pages associated with a single
+** segment together form a single hash table containing tombstone rowids.
+** To find the page of the hash on which a key might be stored:
+**
+** iPg = (rowid % nHashPg)
+**
+** Then, within page iPg, which has nSlot slots:
+**
+** iSlot = (rowid / nHashPg) % nSlot
+**
+** Each tombstone hash page begins with an 8 byte header:
+**
+** 1-byte: Key-size (the size in bytes of each slot). Either 4 or 8.
+** 1-byte: rowid-0-tombstone flag. This flag is only valid on the
+** first tombstone hash page for each segment (iPg=0). If set,
+** the hash table contains rowid 0. If clear, it does not.
+** Rowid 0 is handled specially.
+** 2-bytes: unused.
+** 4-bytes: Big-endian integer containing number of entries on page.
+**
+** Following this are nSlot 4 or 8 byte slots (depending on the key-size
+** in the first byte of the page header). The number of slots may be
+** determined based on the size of the page record and the key-size:
+**
+** nSlot = (nByte - 8) / key-size
*/
/*
@@ -240,6 +296,7 @@
#define FTS5_SEGMENT_ROWID(segid, pgno) fts5_dri(segid, 0, 0, pgno)
#define FTS5_DLIDX_ROWID(segid, height, pgno) fts5_dri(segid, 1, height, pgno)
+#define FTS5_TOMBSTONE_ROWID(segid,ipg) fts5_dri(segid+(1<<16), 0, 0, ipg)
#ifdef SQLITE_DEBUG
int sqlite3Fts5Corrupt() { return SQLITE_CORRUPT_VTAB; }
@@ -275,6 +332,12 @@ struct Fts5Data {
/*
** One object per %_data table.
+**
+** nContentlessDelete:
+** The number of contentless delete operations since the most recent
+** call to fts5IndexFlush() or fts5IndexDiscardData(). This is tracked
+** so that extra auto-merge work can be done by fts5IndexFlush() to
+** account for the delete operations.
*/
struct Fts5Index {
Fts5Config *pConfig; /* Virtual table configuration */
@@ -289,6 +352,8 @@ struct Fts5Index {
int nPendingData; /* Current bytes of pending data */
i64 iWriteRowid; /* Rowid for current doc being written */
int bDelete; /* Current write is a delete */
+ int nContentlessDelete; /* Number of contentless delete ops */
+ int nPendingRow; /* Number of INSERT in hash table */
/* Error state. */
int rc; /* Current error code */
@@ -323,11 +388,23 @@ struct Fts5DoclistIter {
** The contents of the "structure" record for each index are represented
** using an Fts5Structure record in memory. Which uses instances of the
** other Fts5StructureXXX types as components.
+**
+** nOriginCntr:
+** This value is set to non-zero for structure records created for
+** contentlessdelete=1 tables only. In that case it represents the
+** origin value to apply to the next top-level segment created.
*/
struct Fts5StructureSegment {
int iSegid; /* Segment id */
int pgnoFirst; /* First leaf page number in segment */
int pgnoLast; /* Last leaf page number in segment */
+
+ /* contentlessdelete=1 tables only: */
+ u64 iOrigin1;
+ u64 iOrigin2;
+ int nPgTombstone; /* Number of tombstone hash table pages */
+ u64 nEntryTombstone; /* Number of tombstone entries that "count" */
+ u64 nEntry; /* Number of rows in this segment */
};
struct Fts5StructureLevel {
int nMerge; /* Number of segments in incr-merge */
@@ -337,6 +414,7 @@ struct Fts5StructureLevel {
struct Fts5Structure {
int nRef; /* Object reference count */
u64 nWriteCounter; /* Total leaves written to level 0 */
+ u64 nOriginCntr; /* Origin value for next top-level segment */
int nSegment; /* Total segments in this structure */
int nLevel; /* Number of levels in this index */
Fts5StructureLevel aLevel[1]; /* Array of nLevel level objects */
@@ -425,6 +503,13 @@ struct Fts5CResult {
**
** iTermIdx:
** Index of current term on iTermLeafPgno.
+**
+** apTombstone/nTombstone:
+** These are used for contentless_delete=1 tables only. When the cursor
+** is first allocated, the apTombstone[] array is allocated so that it
+** is large enough for all tombstones hash pages associated with the
+** segment. The pages themselves are loaded lazily from the database as
+** they are required.
*/
struct Fts5SegIter {
Fts5StructureSegment *pSeg; /* Segment to iterate through */
@@ -433,6 +518,8 @@ struct Fts5SegIter {
Fts5Data *pLeaf; /* Current leaf data */
Fts5Data *pNextLeaf; /* Leaf page (iLeafPgno+1) */
i64 iLeafOffset; /* Byte offset within current leaf */
+ Fts5Data **apTombstone; /* Array of tombstone pages */
+ int nTombstone;
/* Next method */
void (*xNext)(Fts5Index*, Fts5SegIter*, int*);
@@ -562,6 +649,60 @@ static u16 fts5GetU16(const u8 *aIn){
return ((u16)aIn[0] << 8) + aIn[1];
}
+/*
+** The only argument points to a buffer at least 8 bytes in size. This
+** function interprets the first 8 bytes of the buffer as a 64-bit big-endian
+** unsigned integer and returns the result.
+*/
+static u64 fts5GetU64(u8 *a){
+ return ((u64)a[0] << 56)
+ + ((u64)a[1] << 48)
+ + ((u64)a[2] << 40)
+ + ((u64)a[3] << 32)
+ + ((u64)a[4] << 24)
+ + ((u64)a[5] << 16)
+ + ((u64)a[6] << 8)
+ + ((u64)a[7] << 0);
+}
+
+/*
+** The only argument points to a buffer at least 4 bytes in size. This
+** function interprets the first 4 bytes of the buffer as a 32-bit big-endian
+** unsigned integer and returns the result.
+*/
+static u32 fts5GetU32(const u8 *a){
+ return ((u32)a[0] << 24)
+ + ((u32)a[1] << 16)
+ + ((u32)a[2] << 8)
+ + ((u32)a[3] << 0);
+}
+
+/*
+** Write iVal, formated as a 64-bit big-endian unsigned integer, to the
+** buffer indicated by the first argument.
+*/
+static void fts5PutU64(u8 *a, u64 iVal){
+ a[0] = ((iVal >> 56) & 0xFF);
+ a[1] = ((iVal >> 48) & 0xFF);
+ a[2] = ((iVal >> 40) & 0xFF);
+ a[3] = ((iVal >> 32) & 0xFF);
+ a[4] = ((iVal >> 24) & 0xFF);
+ a[5] = ((iVal >> 16) & 0xFF);
+ a[6] = ((iVal >> 8) & 0xFF);
+ a[7] = ((iVal >> 0) & 0xFF);
+}
+
+/*
+** Write iVal, formated as a 32-bit big-endian unsigned integer, to the
+** buffer indicated by the first argument.
+*/
+static void fts5PutU32(u8 *a, u32 iVal){
+ a[0] = ((iVal >> 24) & 0xFF);
+ a[1] = ((iVal >> 16) & 0xFF);
+ a[2] = ((iVal >> 8) & 0xFF);
+ a[3] = ((iVal >> 0) & 0xFF);
+}
+
/*
** Allocate and return a buffer at least nByte bytes in size.
**
@@ -789,10 +930,17 @@ static void fts5DataDelete(Fts5Index *p, i64 iFirst, i64 iLast){
/*
** Remove all records associated with segment iSegid.
*/
-static void fts5DataRemoveSegment(Fts5Index *p, int iSegid){
+static void fts5DataRemoveSegment(Fts5Index *p, Fts5StructureSegment *pSeg){
+ int iSegid = pSeg->iSegid;
i64 iFirst = FTS5_SEGMENT_ROWID(iSegid, 0);
i64 iLast = FTS5_SEGMENT_ROWID(iSegid+1, 0)-1;
fts5DataDelete(p, iFirst, iLast);
+
+ if( pSeg->nPgTombstone ){
+ i64 iTomb1 = FTS5_TOMBSTONE_ROWID(iSegid, 0);
+ i64 iTomb2 = FTS5_TOMBSTONE_ROWID(iSegid, pSeg->nPgTombstone-1);
+ fts5DataDelete(p, iTomb1, iTomb2);
+ }
if( p->pIdxDeleter==0 ){
Fts5Config *pConfig = p->pConfig;
fts5IndexPrepareStmt(p, &p->pIdxDeleter, sqlite3_mprintf(
@@ -903,11 +1051,19 @@ static int fts5StructureDecode(
int nSegment = 0;
sqlite3_int64 nByte; /* Bytes of space to allocate at pRet */
Fts5Structure *pRet = 0; /* Structure object to return */
+ int bStructureV2 = 0; /* True for FTS5_STRUCTURE_V2 */
+ u64 nOriginCntr = 0; /* Largest origin value seen so far */
/* Grab the cookie value */
if( piCookie ) *piCookie = sqlite3Fts5Get32(pData);
i = 4;
+ /* Check if this is a V2 structure record. Set bStructureV2 if it is. */
+ if( 0==memcmp(&pData[i], FTS5_STRUCTURE_V2, 4) ){
+ i += 4;
+ bStructureV2 = 1;
+ }
+
/* Read the total number of levels and segments from the start of the
** structure record. */
i += fts5GetVarint32(&pData[i], nLevel);
@@ -958,6 +1114,14 @@ static int fts5StructureDecode(
i += fts5GetVarint32(&pData[i], pSeg->iSegid);
i += fts5GetVarint32(&pData[i], pSeg->pgnoFirst);
i += fts5GetVarint32(&pData[i], pSeg->pgnoLast);
+ if( bStructureV2 ){
+ i += fts5GetVarint(&pData[i], &pSeg->iOrigin1);
+ i += fts5GetVarint(&pData[i], &pSeg->iOrigin2);
+ i += fts5GetVarint32(&pData[i], pSeg->nPgTombstone);
+ i += fts5GetVarint(&pData[i], &pSeg->nEntryTombstone);
+ i += fts5GetVarint(&pData[i], &pSeg->nEntry);
+ nOriginCntr = MAX(nOriginCntr, pSeg->iOrigin2);
+ }
if( pSeg->pgnoLastpgnoFirst ){
rc = FTS5_CORRUPT;
break;
@@ -968,6 +1132,9 @@ static int fts5StructureDecode(
}
}
if( nSegment!=0 && rc==SQLITE_OK ) rc = FTS5_CORRUPT;
+ if( bStructureV2 ){
+ pRet->nOriginCntr = nOriginCntr+1;
+ }
if( rc!=SQLITE_OK ){
fts5StructureRelease(pRet);
@@ -1180,6 +1347,7 @@ static void fts5StructureWrite(Fts5Index *p, Fts5Structure *pStruct){
Fts5Buffer buf; /* Buffer to serialize record into */
int iLvl; /* Used to iterate through levels */
int iCookie; /* Cookie value to store */
+ int nHdr = (pStruct->nOriginCntr>0 ? (4+4+9+9+9) : (4+9+9));
assert( pStruct->nSegment==fts5StructureCountSegments(pStruct) );
memset(&buf, 0, sizeof(Fts5Buffer));
@@ -1188,9 +1356,12 @@ static void fts5StructureWrite(Fts5Index *p, Fts5Structure *pStruct){
iCookie = p->pConfig->iCookie;
if( iCookie<0 ) iCookie = 0;
- if( 0==sqlite3Fts5BufferSize(&p->rc, &buf, 4+9+9+9) ){
+ if( 0==sqlite3Fts5BufferSize(&p->rc, &buf, nHdr) ){
sqlite3Fts5Put32(buf.p, iCookie);
buf.n = 4;
+ if( pStruct->nOriginCntr>0 ){
+ fts5BufferSafeAppendBlob(&buf, FTS5_STRUCTURE_V2, 4);
+ }
fts5BufferSafeAppendVarint(&buf, pStruct->nLevel);
fts5BufferSafeAppendVarint(&buf, pStruct->nSegment);
fts5BufferSafeAppendVarint(&buf, (i64)pStruct->nWriteCounter);
@@ -1204,9 +1375,17 @@ static void fts5StructureWrite(Fts5Index *p, Fts5Structure *pStruct){
assert( pLvl->nMerge<=pLvl->nSeg );
for(iSeg=0; iSegnSeg; iSeg++){
- fts5BufferAppendVarint(&p->rc, &buf, pLvl->aSeg[iSeg].iSegid);
- fts5BufferAppendVarint(&p->rc, &buf, pLvl->aSeg[iSeg].pgnoFirst);
- fts5BufferAppendVarint(&p->rc, &buf, pLvl->aSeg[iSeg].pgnoLast);
+ Fts5StructureSegment *pSeg = &pLvl->aSeg[iSeg];
+ fts5BufferAppendVarint(&p->rc, &buf, pSeg->iSegid);
+ fts5BufferAppendVarint(&p->rc, &buf, pSeg->pgnoFirst);
+ fts5BufferAppendVarint(&p->rc, &buf, pSeg->pgnoLast);
+ if( pStruct->nOriginCntr>0 ){
+ fts5BufferAppendVarint(&p->rc, &buf, pSeg->iOrigin1);
+ fts5BufferAppendVarint(&p->rc, &buf, pSeg->iOrigin2);
+ fts5BufferAppendVarint(&p->rc, &buf, pSeg->nPgTombstone);
+ fts5BufferAppendVarint(&p->rc, &buf, pSeg->nEntryTombstone);
+ fts5BufferAppendVarint(&p->rc, &buf, pSeg->nEntry);
+ }
}
}
@@ -1729,6 +1908,23 @@ static void fts5SegIterSetNext(Fts5Index *p, Fts5SegIter *pIter){
}
}
+/*
+** Allocate a tombstone hash page array (pIter->apTombstone) for the
+** iterator passed as the second argument. If an OOM error occurs, leave
+** an error in the Fts5Index object.
+*/
+static void fts5SegIterAllocTombstone(Fts5Index *p, Fts5SegIter *pIter){
+ const int nTomb = pIter->pSeg->nPgTombstone;
+ if( nTomb>0 ){
+ Fts5Data **apTomb = 0;
+ apTomb = (Fts5Data**)sqlite3Fts5MallocZero(&p->rc, sizeof(Fts5Data)*nTomb);
+ if( apTomb ){
+ pIter->apTombstone = apTomb;
+ pIter->nTombstone = nTomb;
+ }
+ }
+}
+
/*
** Initialize the iterator object pIter to iterate through the entries in
** segment pSeg. The iterator is left pointing to the first entry when
@@ -1770,6 +1966,7 @@ static void fts5SegIterInit(
pIter->iPgidxOff = pIter->pLeaf->szLeaf+1;
fts5SegIterLoadTerm(p, pIter, 0);
fts5SegIterLoadNPos(p, pIter);
+ fts5SegIterAllocTombstone(p, pIter);
}
}
@@ -2471,6 +2668,7 @@ static void fts5SegIterSeekInit(
}
fts5SegIterSetNext(p, pIter);
+ fts5SegIterAllocTombstone(p, pIter);
/* Either:
**
@@ -2551,6 +2749,20 @@ static void fts5SegIterHashInit(
fts5SegIterSetNext(p, pIter);
}
+/*
+** Array ap[] contains n elements. Release each of these elements using
+** fts5DataRelease(). Then free the array itself using sqlite3_free().
+*/
+static void fts5IndexFreeArray(Fts5Data **ap, int n){
+ if( ap ){
+ int ii;
+ for(ii=0; iiterm);
fts5DataRelease(pIter->pLeaf);
fts5DataRelease(pIter->pNextLeaf);
+ fts5IndexFreeArray(pIter->apTombstone, pIter->nTombstone);
fts5DlidxIterFree(pIter->pDlidx);
sqlite3_free(pIter->aRowidOffset);
memset(pIter, 0, sizeof(Fts5SegIter));
@@ -2895,6 +3108,84 @@ static void fts5MultiIterSetEof(Fts5Iter *pIter){
pIter->iSwitchRowid = pSeg->iRowid;
}
+/*
+** The argument to this macro must be an Fts5Data structure containing a
+** tombstone hash page. This macro returns the key-size of the hash-page.
+*/
+#define TOMBSTONE_KEYSIZE(pPg) (pPg->p[0]==4 ? 4 : 8)
+
+#define TOMBSTONE_NSLOT(pPg) \
+ ((pPg->nn > 16) ? ((pPg->nn-8) / TOMBSTONE_KEYSIZE(pPg)) : 1)
+
+/*
+** Query a single tombstone hash table for rowid iRowid. Return true if
+** it is found or false otherwise. The tombstone hash table is one of
+** nHashTable tables.
+*/
+static int fts5IndexTombstoneQuery(
+ Fts5Data *pHash, /* Hash table page to query */
+ int nHashTable, /* Number of pages attached to segment */
+ u64 iRowid /* Rowid to query hash for */
+){
+ const int szKey = TOMBSTONE_KEYSIZE(pHash);
+ const int nSlot = TOMBSTONE_NSLOT(pHash);
+ int iSlot = (iRowid / nHashTable) % nSlot;
+ int nCollide = nSlot;
+
+ if( iRowid==0 ){
+ return pHash->p[1];
+ }else if( szKey==4 ){
+ u32 *aSlot = (u32*)&pHash->p[8];
+ while( aSlot[iSlot] ){
+ if( fts5GetU32((u8*)&aSlot[iSlot])==iRowid ) return 1;
+ if( nCollide--==0 ) break;
+ iSlot = (iSlot+1)%nSlot;
+ }
+ }else{
+ u64 *aSlot = (u64*)&pHash->p[8];
+ while( aSlot[iSlot] ){
+ if( fts5GetU64((u8*)&aSlot[iSlot])==iRowid ) return 1;
+ if( nCollide--==0 ) break;
+ iSlot = (iSlot+1)%nSlot;
+ }
+ }
+
+ return 0;
+}
+
+/*
+** Return true if the iterator passed as the only argument points
+** to an segment entry for which there is a tombstone. Return false
+** if there is no tombstone or if the iterator is already at EOF.
+*/
+static int fts5MultiIterIsDeleted(Fts5Iter *pIter){
+ int iFirst = pIter->aFirst[1].iFirst;
+ Fts5SegIter *pSeg = &pIter->aSeg[iFirst];
+
+ if( pSeg->pLeaf && pSeg->nTombstone ){
+ /* Figure out which page the rowid might be present on. */
+ int iPg = ((u64)pSeg->iRowid) % pSeg->nTombstone;
+ assert( iPg>=0 );
+
+ /* If tombstone hash page iPg has not yet been loaded from the
+ ** database, load it now. */
+ if( pSeg->apTombstone[iPg]==0 ){
+ pSeg->apTombstone[iPg] = fts5DataRead(pIter->pIndex,
+ FTS5_TOMBSTONE_ROWID(pSeg->pSeg->iSegid, iPg)
+ );
+ if( pSeg->apTombstone[iPg]==0 ) return 0;
+ }
+
+ return fts5IndexTombstoneQuery(
+ pSeg->apTombstone[iPg],
+ pSeg->nTombstone,
+ pSeg->iRowid
+ );
+ }
+
+ return 0;
+}
+
/*
** Move the iterator to the next entry.
**
@@ -2932,7 +3223,9 @@ static void fts5MultiIterNext(
fts5AssertMultiIterSetup(p, pIter);
assert( pSeg==&pIter->aSeg[pIter->aFirst[1].iFirst] && pSeg->pLeaf );
- if( pIter->bSkipEmpty==0 || pSeg->nPos ){
+ if( (pIter->bSkipEmpty==0 || pSeg->nPos)
+ && 0==fts5MultiIterIsDeleted(pIter)
+ ){
pIter->xSetOutputs(pIter, pSeg);
return;
}
@@ -2964,7 +3257,9 @@ static void fts5MultiIterNext2(
}
fts5AssertMultiIterSetup(p, pIter);
- }while( fts5MultiIterIsEmpty(p, pIter) );
+ }while( (fts5MultiIterIsEmpty(p, pIter) || fts5MultiIterIsDeleted(pIter))
+ && (p->rc==SQLITE_OK)
+ );
}
}
@@ -3519,7 +3814,9 @@ static void fts5MultiIterNew(
fts5MultiIterSetEof(pNew);
fts5AssertMultiIterSetup(p, pNew);
- if( pNew->bSkipEmpty && fts5MultiIterIsEmpty(p, pNew) ){
+ if( (pNew->bSkipEmpty && fts5MultiIterIsEmpty(p, pNew))
+ || fts5MultiIterIsDeleted(pNew)
+ ){
fts5MultiIterNext(p, pNew, 0, 0);
}else if( pNew->base.bEof==0 ){
Fts5SegIter *pSeg = &pNew->aSeg[pNew->aFirst[1].iFirst];
@@ -3697,7 +3994,9 @@ static void fts5IndexDiscardData(Fts5Index *p){
if( p->pHash ){
sqlite3Fts5HashClear(p->pHash);
p->nPendingData = 0;
+ p->nPendingRow = 0;
}
+ p->nContentlessDelete = 0;
}
/*
@@ -4334,6 +4633,12 @@ static void fts5IndexMergeLevel(
/* Read input from all segments in the input level */
nInput = pLvl->nSeg;
+
+ /* Set the range of origins that will go into the output segment. */
+ if( pStruct->nOriginCntr>0 ){
+ pSeg->iOrigin1 = pLvl->aSeg[0].iOrigin1;
+ pSeg->iOrigin2 = pLvl->aSeg[pLvl->nSeg-1].iOrigin2;
+ }
}
bOldest = (pLvlOut->nSeg==1 && pStruct->nLevel==iLvl+2);
@@ -4393,8 +4698,11 @@ static void fts5IndexMergeLevel(
int i;
/* Remove the redundant segments from the %_data table */
+ assert( pSeg->nEntry==0 );
for(i=0; iaSeg[i].iSegid);
+ Fts5StructureSegment *pOld = &pLvl->aSeg[i];
+ pSeg->nEntry += (pOld->nEntry - pOld->nEntryTombstone);
+ fts5DataRemoveSegment(p, pOld);
}
/* Remove the redundant segments from the input level */
@@ -4420,6 +4728,43 @@ static void fts5IndexMergeLevel(
if( pnRem ) *pnRem -= writer.nLeafWritten;
}
+/*
+** If this is not a contentless_delete=1 table, or if the 'deletemerge'
+** configuration option is set to 0, then this function always returns -1.
+** Otherwise, it searches the structure object passed as the second argument
+** for a level suitable for merging due to having a large number of
+** tombstones in the tombstone hash. If one is found, its index is returned.
+** Otherwise, if there is no suitable level, -1.
+*/
+static int fts5IndexFindDeleteMerge(Fts5Index *p, Fts5Structure *pStruct){
+ Fts5Config *pConfig = p->pConfig;
+ int iRet = -1;
+ if( pConfig->bContentlessDelete && pConfig->nDeleteMerge>0 ){
+ int ii;
+ int nBest = 0;
+
+ for(ii=0; iinLevel; ii++){
+ Fts5StructureLevel *pLvl = &pStruct->aLevel[ii];
+ i64 nEntry = 0;
+ i64 nTomb = 0;
+ int iSeg;
+ for(iSeg=0; iSegnSeg; iSeg++){
+ nEntry += pLvl->aSeg[iSeg].nEntry;
+ nTomb += pLvl->aSeg[iSeg].nEntryTombstone;
+ }
+ assert_nc( nEntry>0 || pLvl->nSeg==0 );
+ if( nEntry>0 ){
+ int nPercent = (nTomb * 100) / nEntry;
+ if( nPercent>=pConfig->nDeleteMerge && nPercent>nBest ){
+ iRet = ii;
+ nBest = nPercent;
+ }
+ }
+ }
+ }
+ return iRet;
+}
+
/*
** Do up to nPg pages of automerge work on the index.
**
@@ -4439,14 +4784,15 @@ static int fts5IndexMerge(
int iBestLvl = 0; /* Level offering the most input segments */
int nBest = 0; /* Number of input segments on best level */
- /* Set iBestLvl to the level to read input segments from. */
+ /* Set iBestLvl to the level to read input segments from. Or to -1 if
+ ** there is no level suitable to merge segments from. */
assert( pStruct->nLevel>0 );
for(iLvl=0; iLvlnLevel; iLvl++){
Fts5StructureLevel *pLvl = &pStruct->aLevel[iLvl];
if( pLvl->nMerge ){
if( pLvl->nMerge>nBest ){
iBestLvl = iLvl;
- nBest = pLvl->nMerge;
+ nBest = nMin;
}
break;
}
@@ -4455,22 +4801,18 @@ static int fts5IndexMerge(
iBestLvl = iLvl;
}
}
-
- /* If nBest is still 0, then the index must be empty. */
-#ifdef SQLITE_DEBUG
- for(iLvl=0; nBest==0 && iLvlnLevel; iLvl++){
- assert( pStruct->aLevel[iLvl].nSeg==0 );
+ if( nBestaLevel[iBestLvl].nMerge==0 ){
- break;
- }
+ if( iBestLvl<0 ) break;
bRet = 1;
fts5IndexMergeLevel(p, &pStruct, iBestLvl, &nRem);
if( p->rc==SQLITE_OK && pStruct->aLevel[iBestLvl].nMerge==0 ){
fts5StructurePromote(p, iBestLvl+1, pStruct);
}
+
+ if( nMin==1 ) nMin = 2;
}
*ppStruct = pStruct;
return bRet;
@@ -4636,7 +4978,7 @@ static void fts5SecureDeleteOverflow(
pLeaf = 0;
}else if( bDetailNone ){
break;
- }else if( iNext>=pLeaf->szLeaf || iNext<4 ){
+ }else if( iNext>=pLeaf->szLeaf || pLeaf->nnszLeaf || iNext<4 ){
p->rc = FTS5_CORRUPT;
break;
}else{
@@ -4655,9 +4997,13 @@ static void fts5SecureDeleteOverflow(
int i1 = pLeaf->szLeaf;
int i2 = 0;
+ i1 += fts5GetVarint32(&aPg[i1], iFirst);
+ if( iFirstrc = FTS5_CORRUPT;
+ break;
+ }
aIdx = sqlite3Fts5MallocZero(&p->rc, (pLeaf->nn-pLeaf->szLeaf)+2);
if( aIdx==0 ) break;
- i1 += fts5GetVarint32(&aPg[i1], iFirst);
i2 = sqlite3Fts5PutVarint(aIdx, iFirst-nShift);
if( i1nn ){
memcpy(&aIdx[i2], &aPg[i1], pLeaf->nn-i1);
@@ -4840,7 +5186,9 @@ static void fts5DoSecureDelete(
iOff += sqlite3Fts5PutVarint(&aPg[iOff], nPrefix);
}
iOff += sqlite3Fts5PutVarint(&aPg[iOff], nSuffix);
- if( nPrefix2>nPrefix ){
+ if( nPrefix2>pSeg->term.n ){
+ p->rc = FTS5_CORRUPT;
+ }else if( nPrefix2>nPrefix ){
memcpy(&aPg[iOff], &pSeg->term.p[nPrefix], nPrefix2-nPrefix);
iOff += (nPrefix2-nPrefix);
}
@@ -4977,187 +5325,197 @@ static void fts5FlushOneHash(Fts5Index *p){
/* Obtain a reference to the index structure and allocate a new segment-id
** for the new level-0 segment. */
pStruct = fts5StructureRead(p);
- iSegid = fts5AllocateSegid(p, pStruct);
fts5StructureInvalidate(p);
- if( iSegid ){
- const int pgsz = p->pConfig->pgsz;
- int eDetail = p->pConfig->eDetail;
- int bSecureDelete = p->pConfig->bSecureDelete;
- Fts5StructureSegment *pSeg; /* New segment within pStruct */
- Fts5Buffer *pBuf; /* Buffer in which to assemble leaf page */
- Fts5Buffer *pPgidx; /* Buffer in which to assemble pgidx */
-
- Fts5SegWriter writer;
- fts5WriteInit(p, &writer, iSegid);
-
- pBuf = &writer.writer.buf;
- pPgidx = &writer.writer.pgidx;
-
- /* fts5WriteInit() should have initialized the buffers to (most likely)
- ** the maximum space required. */
- assert( p->rc || pBuf->nSpace>=(pgsz + FTS5_DATA_PADDING) );
- assert( p->rc || pPgidx->nSpace>=(pgsz + FTS5_DATA_PADDING) );
-
- /* Begin scanning through hash table entries. This loop runs once for each
- ** term/doclist currently stored within the hash table. */
- if( p->rc==SQLITE_OK ){
- p->rc = sqlite3Fts5HashScanInit(pHash, 0, 0);
- }
- while( p->rc==SQLITE_OK && 0==sqlite3Fts5HashScanEof(pHash) ){
- const char *zTerm; /* Buffer containing term */
- int nTerm; /* Size of zTerm in bytes */
- const u8 *pDoclist; /* Pointer to doclist for this term */
- int nDoclist; /* Size of doclist in bytes */
-
- /* Get the term and doclist for this entry. */
- sqlite3Fts5HashScanEntry(pHash, &zTerm, &pDoclist, &nDoclist);
- nTerm = (int)strlen(zTerm);
- if( bSecureDelete==0 ){
- fts5WriteAppendTerm(p, &writer, nTerm, (const u8*)zTerm);
- if( p->rc!=SQLITE_OK ) break;
- assert( writer.bFirstRowidInPage==0 );
- }
-
- if( !bSecureDelete && pgsz>=(pBuf->n + pPgidx->n + nDoclist + 1) ){
- /* The entire doclist will fit on the current leaf. */
- fts5BufferSafeAppendBlob(pBuf, pDoclist, nDoclist);
- }else{
- int bTermWritten = !bSecureDelete;
- i64 iRowid = 0;
- i64 iPrev = 0;
- int iOff = 0;
-
- /* The entire doclist will not fit on this leaf. The following
- ** loop iterates through the poslists that make up the current
- ** doclist. */
- while( p->rc==SQLITE_OK && iOffpConfig->pgsz;
+ int eDetail = p->pConfig->eDetail;
+ int bSecureDelete = p->pConfig->bSecureDelete;
+ Fts5StructureSegment *pSeg; /* New segment within pStruct */
+ Fts5Buffer *pBuf; /* Buffer in which to assemble leaf page */
+ Fts5Buffer *pPgidx; /* Buffer in which to assemble pgidx */
+
+ Fts5SegWriter writer;
+ fts5WriteInit(p, &writer, iSegid);
+
+ pBuf = &writer.writer.buf;
+ pPgidx = &writer.writer.pgidx;
+
+ /* fts5WriteInit() should have initialized the buffers to (most likely)
+ ** the maximum space required. */
+ assert( p->rc || pBuf->nSpace>=(pgsz + FTS5_DATA_PADDING) );
+ assert( p->rc || pPgidx->nSpace>=(pgsz + FTS5_DATA_PADDING) );
+
+ /* Begin scanning through hash table entries. This loop runs once for each
+ ** term/doclist currently stored within the hash table. */
+ if( p->rc==SQLITE_OK ){
+ p->rc = sqlite3Fts5HashScanInit(pHash, 0, 0);
+ }
+ while( p->rc==SQLITE_OK && 0==sqlite3Fts5HashScanEof(pHash) ){
+ const char *zTerm; /* Buffer containing term */
+ int nTerm; /* Size of zTerm in bytes */
+ const u8 *pDoclist; /* Pointer to doclist for this term */
+ int nDoclist; /* Size of doclist in bytes */
+
+ /* Get the term and doclist for this entry. */
+ sqlite3Fts5HashScanEntry(pHash, &zTerm, &pDoclist, &nDoclist);
+ nTerm = (int)strlen(zTerm);
+ if( bSecureDelete==0 ){
+ fts5WriteAppendTerm(p, &writer, nTerm, (const u8*)zTerm);
+ if( p->rc!=SQLITE_OK ) break;
+ assert( writer.bFirstRowidInPage==0 );
+ }
+
+ if( !bSecureDelete && pgsz>=(pBuf->n + pPgidx->n + nDoclist + 1) ){
+ /* The entire doclist will fit on the current leaf. */
+ fts5BufferSafeAppendBlob(pBuf, pDoclist, nDoclist);
+ }else{
+ int bTermWritten = !bSecureDelete;
+ i64 iRowid = 0;
+ i64 iPrev = 0;
+ int iOff = 0;
+
+ /* The entire doclist will not fit on this leaf. The following
+ ** loop iterates through the poslists that make up the current
+ ** doclist. */
+ while( p->rc==SQLITE_OK && iOffrc!=SQLITE_OK || pDoclist[iOff]==0x01 ){
iOff++;
- nDoclist = 0;
- }else{
continue;
}
}
- }else if( (pDoclist[iOff] & 0x01) ){
- fts5FlushSecureDelete(p, pStruct, zTerm, iRowid);
- if( p->rc!=SQLITE_OK || pDoclist[iOff]==0x01 ){
- iOff++;
- continue;
- }
}
- }
-
- if( p->rc==SQLITE_OK && bTermWritten==0 ){
- fts5WriteAppendTerm(p, &writer, nTerm, (const u8*)zTerm);
- bTermWritten = 1;
- assert( p->rc!=SQLITE_OK || writer.bFirstRowidInPage==0 );
- }
-
- if( writer.bFirstRowidInPage ){
- fts5PutU16(&pBuf->p[0], (u16)pBuf->n); /* first rowid on page */
- pBuf->n += sqlite3Fts5PutVarint(&pBuf->p[pBuf->n], iRowid);
- writer.bFirstRowidInPage = 0;
- fts5WriteDlidxAppend(p, &writer, iRowid);
- }else{
- pBuf->n += sqlite3Fts5PutVarint(&pBuf->p[pBuf->n], iRowid-iPrev);
- }
- if( p->rc!=SQLITE_OK ) break;
- assert( pBuf->n<=pBuf->nSpace );
- iPrev = iRowid;
-
- if( eDetail==FTS5_DETAIL_NONE ){
- if( iOffp[pBuf->n++] = 0;
- iOff++;
+
+ if( p->rc==SQLITE_OK && bTermWritten==0 ){
+ fts5WriteAppendTerm(p, &writer, nTerm, (const u8*)zTerm);
+ bTermWritten = 1;
+ assert( p->rc!=SQLITE_OK || writer.bFirstRowidInPage==0 );
+ }
+
+ if( writer.bFirstRowidInPage ){
+ fts5PutU16(&pBuf->p[0], (u16)pBuf->n); /* first rowid on page */
+ pBuf->n += sqlite3Fts5PutVarint(&pBuf->p[pBuf->n], iRowid);
+ writer.bFirstRowidInPage = 0;
+ fts5WriteDlidxAppend(p, &writer, iRowid);
+ }else{
+ u64 iRowidDelta = (u64)iRowid - (u64)iPrev;
+ pBuf->n += sqlite3Fts5PutVarint(&pBuf->p[pBuf->n], iRowidDelta);
+ }
+ if( p->rc!=SQLITE_OK ) break;
+ assert( pBuf->n<=pBuf->nSpace );
+ iPrev = iRowid;
+
+ if( eDetail==FTS5_DETAIL_NONE ){
if( iOffp[pBuf->n++] = 0;
iOff++;
+ if( iOffp[pBuf->n++] = 0;
+ iOff++;
+ }
+ }
+ if( (pBuf->n + pPgidx->n)>=pgsz ){
+ fts5WriteFlushLeaf(p, &writer);
}
- }
- if( (pBuf->n + pPgidx->n)>=pgsz ){
- fts5WriteFlushLeaf(p, &writer);
- }
- }else{
- int bDummy;
- int nPos;
- int nCopy = fts5GetPoslistSize(&pDoclist[iOff], &nPos, &bDummy);
- nCopy += nPos;
- if( (pBuf->n + pPgidx->n + nCopy) <= pgsz ){
- /* The entire poslist will fit on the current leaf. So copy
- ** it in one go. */
- fts5BufferSafeAppendBlob(pBuf, &pDoclist[iOff], nCopy);
}else{
- /* The entire poslist will not fit on this leaf. So it needs
- ** to be broken into sections. The only qualification being
- ** that each varint must be stored contiguously. */
- const u8 *pPoslist = &pDoclist[iOff];
- int iPos = 0;
- while( p->rc==SQLITE_OK ){
- int nSpace = pgsz - pBuf->n - pPgidx->n;
- int n = 0;
- if( (nCopy - iPos)<=nSpace ){
- n = nCopy - iPos;
- }else{
- n = fts5PoslistPrefix(&pPoslist[iPos], nSpace);
- }
- assert( n>0 );
- fts5BufferSafeAppendBlob(pBuf, &pPoslist[iPos], n);
- iPos += n;
- if( (pBuf->n + pPgidx->n)>=pgsz ){
- fts5WriteFlushLeaf(p, &writer);
+ int bDummy;
+ int nPos;
+ int nCopy = fts5GetPoslistSize(&pDoclist[iOff], &nPos, &bDummy);
+ nCopy += nPos;
+ if( (pBuf->n + pPgidx->n + nCopy) <= pgsz ){
+ /* The entire poslist will fit on the current leaf. So copy
+ ** it in one go. */
+ fts5BufferSafeAppendBlob(pBuf, &pDoclist[iOff], nCopy);
+ }else{
+ /* The entire poslist will not fit on this leaf. So it needs
+ ** to be broken into sections. The only qualification being
+ ** that each varint must be stored contiguously. */
+ const u8 *pPoslist = &pDoclist[iOff];
+ int iPos = 0;
+ while( p->rc==SQLITE_OK ){
+ int nSpace = pgsz - pBuf->n - pPgidx->n;
+ int n = 0;
+ if( (nCopy - iPos)<=nSpace ){
+ n = nCopy - iPos;
+ }else{
+ n = fts5PoslistPrefix(&pPoslist[iPos], nSpace);
+ }
+ assert( n>0 );
+ fts5BufferSafeAppendBlob(pBuf, &pPoslist[iPos], n);
+ iPos += n;
+ if( (pBuf->n + pPgidx->n)>=pgsz ){
+ fts5WriteFlushLeaf(p, &writer);
+ }
+ if( iPos>=nCopy ) break;
}
- if( iPos>=nCopy ) break;
}
+ iOff += nCopy;
}
- iOff += nCopy;
}
}
+
+ /* TODO2: Doclist terminator written here. */
+ /* pBuf->p[pBuf->n++] = '\0'; */
+ assert( pBuf->n<=pBuf->nSpace );
+ if( p->rc==SQLITE_OK ) sqlite3Fts5HashScanNext(pHash);
}
-
- /* TODO2: Doclist terminator written here. */
- /* pBuf->p[pBuf->n++] = '\0'; */
- assert( pBuf->n<=pBuf->nSpace );
- if( p->rc==SQLITE_OK ) sqlite3Fts5HashScanNext(pHash);
- }
- sqlite3Fts5HashClear(pHash);
- fts5WriteFinish(p, &writer, &pgnoLast);
-
- assert( p->rc!=SQLITE_OK || bSecureDelete || pgnoLast>0 );
- if( pgnoLast>0 ){
- /* Update the Fts5Structure. It is written back to the database by the
- ** fts5StructureRelease() call below. */
- if( pStruct->nLevel==0 ){
- fts5StructureAddLevel(&p->rc, &pStruct);
- }
- fts5StructureExtendLevel(&p->rc, pStruct, 0, 1, 0);
- if( p->rc==SQLITE_OK ){
- pSeg = &pStruct->aLevel[0].aSeg[ pStruct->aLevel[0].nSeg++ ];
- pSeg->iSegid = iSegid;
- pSeg->pgnoFirst = 1;
- pSeg->pgnoLast = pgnoLast;
- pStruct->nSegment++;
+ sqlite3Fts5HashClear(pHash);
+ fts5WriteFinish(p, &writer, &pgnoLast);
+
+ assert( p->rc!=SQLITE_OK || bSecureDelete || pgnoLast>0 );
+ if( pgnoLast>0 ){
+ /* Update the Fts5Structure. It is written back to the database by the
+ ** fts5StructureRelease() call below. */
+ if( pStruct->nLevel==0 ){
+ fts5StructureAddLevel(&p->rc, &pStruct);
+ }
+ fts5StructureExtendLevel(&p->rc, pStruct, 0, 1, 0);
+ if( p->rc==SQLITE_OK ){
+ pSeg = &pStruct->aLevel[0].aSeg[ pStruct->aLevel[0].nSeg++ ];
+ pSeg->iSegid = iSegid;
+ pSeg->pgnoFirst = 1;
+ pSeg->pgnoLast = pgnoLast;
+ if( pStruct->nOriginCntr>0 ){
+ pSeg->iOrigin1 = pStruct->nOriginCntr;
+ pSeg->iOrigin2 = pStruct->nOriginCntr;
+ pSeg->nEntry = p->nPendingRow;
+ pStruct->nOriginCntr++;
+ }
+ pStruct->nSegment++;
+ }
+ fts5StructurePromote(p, 0, pStruct);
}
- fts5StructurePromote(p, 0, pStruct);
}
}
- fts5IndexAutomerge(p, &pStruct, pgnoLast);
+ fts5IndexAutomerge(p, &pStruct, pgnoLast + p->nContentlessDelete);
fts5IndexCrisismerge(p, &pStruct);
fts5StructureWrite(p, pStruct);
fts5StructureRelease(pStruct);
+ p->nContentlessDelete = 0;
}
/*
@@ -5165,10 +5523,11 @@ static void fts5FlushOneHash(Fts5Index *p){
*/
static void fts5IndexFlush(Fts5Index *p){
/* Unless it is empty, flush the hash table to disk */
- if( p->nPendingData ){
+ if( p->nPendingData || p->nContentlessDelete ){
assert( p->pHash );
- p->nPendingData = 0;
fts5FlushOneHash(p);
+ p->nPendingData = 0;
+ p->nPendingRow = 0;
}
}
@@ -5184,17 +5543,22 @@ static Fts5Structure *fts5IndexOptimizeStruct(
/* Figure out if this structure requires optimization. A structure does
** not require optimization if either:
**
- ** + it consists of fewer than two segments, or
- ** + all segments are on the same level, or
- ** + all segments except one are currently inputs to a merge operation.
+ ** 1. it consists of fewer than two segments, or
+ ** 2. all segments are on the same level, or
+ ** 3. all segments except one are currently inputs to a merge operation.
**
- ** In the first case, return NULL. In the second, increment the ref-count
- ** on *pStruct and return a copy of the pointer to it.
+ ** In the first case, if there are no tombstone hash pages, return NULL. In
+ ** the second, increment the ref-count on *pStruct and return a copy of the
+ ** pointer to it.
*/
- if( nSeg<2 ) return 0;
+ if( nSeg==0 ) return 0;
for(i=0; inLevel; i++){
int nThis = pStruct->aLevel[i].nSeg;
- if( nThis==nSeg || (nThis==nSeg-1 && pStruct->aLevel[i].nMerge==nThis) ){
+ int nMerge = pStruct->aLevel[i].nMerge;
+ if( nThis>0 && (nThis==nSeg || (nThis==nSeg-1 && nMerge==nThis)) ){
+ if( nSeg==1 && nThis==1 && pStruct->aLevel[i].aSeg[0].nPgTombstone==0 ){
+ return 0;
+ }
fts5StructureRef(pStruct);
return pStruct;
}
@@ -5210,6 +5574,7 @@ static Fts5Structure *fts5IndexOptimizeStruct(
pNew->nLevel = MIN(pStruct->nLevel+1, FTS5_MAX_LEVEL);
pNew->nRef = 1;
pNew->nWriteCounter = pStruct->nWriteCounter;
+ pNew->nOriginCntr = pStruct->nOriginCntr;
pLvl = &pNew->aLevel[pNew->nLevel-1];
pLvl->aSeg = (Fts5StructureSegment*)sqlite3Fts5MallocZero(&p->rc, nByte);
if( pLvl->aSeg ){
@@ -5240,6 +5605,7 @@ int sqlite3Fts5IndexOptimize(Fts5Index *p){
assert( p->rc==SQLITE_OK );
fts5IndexFlush(p);
+ assert( p->nContentlessDelete==0 );
pStruct = fts5StructureRead(p);
fts5StructureInvalidate(p);
@@ -5269,7 +5635,10 @@ int sqlite3Fts5IndexOptimize(Fts5Index *p){
** INSERT command.
*/
int sqlite3Fts5IndexMerge(Fts5Index *p, int nMerge){
- Fts5Structure *pStruct = fts5StructureRead(p);
+ Fts5Structure *pStruct = 0;
+
+ fts5IndexFlush(p);
+ pStruct = fts5StructureRead(p);
if( pStruct ){
int nMin = p->pConfig->nUsermerge;
fts5StructureInvalidate(p);
@@ -5277,7 +5646,7 @@ int sqlite3Fts5IndexMerge(Fts5Index *p, int nMerge){
Fts5Structure *pNew = fts5IndexOptimizeStruct(p, pStruct);
fts5StructureRelease(pStruct);
pStruct = pNew;
- nMin = 2;
+ nMin = 1;
nMerge = nMerge*-1;
}
if( pStruct && pStruct->nLevel ){
@@ -5791,6 +6160,9 @@ int sqlite3Fts5IndexBeginWrite(Fts5Index *p, int bDelete, i64 iRowid){
p->iWriteRowid = iRowid;
p->bDelete = bDelete;
+ if( bDelete==0 ){
+ p->nPendingRow++;
+ }
return fts5IndexReturn(p);
}
@@ -5828,6 +6200,9 @@ int sqlite3Fts5IndexReinit(Fts5Index *p){
fts5StructureInvalidate(p);
fts5IndexDiscardData(p);
memset(&s, 0, sizeof(Fts5Structure));
+ if( p->pConfig->bContentlessDelete ){
+ s.nOriginCntr = 1;
+ }
fts5DataWrite(p, FTS5_AVERAGES_ROWID, (const u8*)"", 0);
fts5StructureWrite(p, &s);
return fts5IndexReturn(p);
@@ -6219,6 +6594,347 @@ int sqlite3Fts5IndexLoadConfig(Fts5Index *p){
return fts5IndexReturn(p);
}
+/*
+** Retrieve the origin value that will be used for the segment currently
+** being accumulated in the in-memory hash table when it is flushed to
+** disk. If successful, SQLITE_OK is returned and (*piOrigin) set to
+** the queried value. Or, if an error occurs, an error code is returned
+** and the final value of (*piOrigin) is undefined.
+*/
+int sqlite3Fts5IndexGetOrigin(Fts5Index *p, i64 *piOrigin){
+ Fts5Structure *pStruct;
+ pStruct = fts5StructureRead(p);
+ if( pStruct ){
+ *piOrigin = pStruct->nOriginCntr;
+ fts5StructureRelease(pStruct);
+ }
+ return fts5IndexReturn(p);
+}
+
+/*
+** Buffer pPg contains a page of a tombstone hash table - one of nPg pages
+** associated with the same segment. This function adds rowid iRowid to
+** the hash table. The caller is required to guarantee that there is at
+** least one free slot on the page.
+**
+** If parameter bForce is false and the hash table is deemed to be full
+** (more than half of the slots are occupied), then non-zero is returned
+** and iRowid not inserted. Or, if bForce is true or if the hash table page
+** is not full, iRowid is inserted and zero returned.
+*/
+static int fts5IndexTombstoneAddToPage(
+ Fts5Data *pPg,
+ int bForce,
+ int nPg,
+ u64 iRowid
+){
+ const int szKey = TOMBSTONE_KEYSIZE(pPg);
+ const int nSlot = TOMBSTONE_NSLOT(pPg);
+ const int nElem = fts5GetU32(&pPg->p[4]);
+ int iSlot = (iRowid / nPg) % nSlot;
+ int nCollide = nSlot;
+
+ if( szKey==4 && iRowid>0xFFFFFFFF ) return 2;
+ if( iRowid==0 ){
+ pPg->p[1] = 0x01;
+ return 0;
+ }
+
+ if( bForce==0 && nElem>=(nSlot/2) ){
+ return 1;
+ }
+
+ fts5PutU32(&pPg->p[4], nElem+1);
+ if( szKey==4 ){
+ u32 *aSlot = (u32*)&pPg->p[8];
+ while( aSlot[iSlot] ){
+ iSlot = (iSlot + 1) % nSlot;
+ if( nCollide--==0 ) return 0;
+ }
+ fts5PutU32((u8*)&aSlot[iSlot], (u32)iRowid);
+ }else{
+ u64 *aSlot = (u64*)&pPg->p[8];
+ while( aSlot[iSlot] ){
+ iSlot = (iSlot + 1) % nSlot;
+ if( nCollide--==0 ) return 0;
+ }
+ fts5PutU64((u8*)&aSlot[iSlot], iRowid);
+ }
+
+ return 0;
+}
+
+/*
+** This function attempts to build a new hash containing all the keys
+** currently in the tombstone hash table for segment pSeg. The new
+** hash will be stored in the nOut buffers passed in array apOut[].
+** All pages of the new hash use key-size szKey (4 or 8).
+**
+** Return 0 if the hash is successfully rebuilt into the nOut pages.
+** Or non-zero if it is not (because one page became overfull). In this
+** case the caller should retry with a larger nOut parameter.
+**
+** Parameter pData1 is page iPg1 of the hash table being rebuilt.
+*/
+static int fts5IndexTombstoneRehash(
+ Fts5Index *p,
+ Fts5StructureSegment *pSeg, /* Segment to rebuild hash of */
+ Fts5Data *pData1, /* One page of current hash - or NULL */
+ int iPg1, /* Which page of the current hash is pData1 */
+ int szKey, /* 4 or 8, the keysize */
+ int nOut, /* Number of output pages */
+ Fts5Data **apOut /* Array of output hash pages */
+){
+ int ii;
+ int res = 0;
+
+ /* Initialize the headers of all the output pages */
+ for(ii=0; iip[0] = szKey;
+ fts5PutU32(&apOut[ii]->p[4], 0);
+ }
+
+ /* Loop through the current pages of the hash table. */
+ for(ii=0; res==0 && iinPgTombstone; ii++){
+ Fts5Data *pData = 0; /* Page ii of the current hash table */
+ Fts5Data *pFree = 0; /* Free this at the end of the loop */
+
+ if( iPg1==ii ){
+ pData = pData1;
+ }else{
+ pFree = pData = fts5DataRead(p, FTS5_TOMBSTONE_ROWID(pSeg->iSegid, ii));
+ }
+
+ if( pData ){
+ int szKeyIn = TOMBSTONE_KEYSIZE(pData);
+ int nSlotIn = (pData->nn - 8) / szKeyIn;
+ int iIn;
+ for(iIn=0; iInp[8];
+ if( aSlot[iIn] ) iVal = fts5GetU32((u8*)&aSlot[iIn]);
+ }else{
+ u64 *aSlot = (u64*)&pData->p[8];
+ if( aSlot[iIn] ) iVal = fts5GetU64((u8*)&aSlot[iIn]);
+ }
+
+ /* If iVal is not 0 at this point, insert it into the new hash table */
+ if( iVal ){
+ Fts5Data *pPg = apOut[(iVal % nOut)];
+ res = fts5IndexTombstoneAddToPage(pPg, 0, nOut, iVal);
+ if( res ) break;
+ }
+ }
+
+ /* If this is page 0 of the old hash, copy the rowid-0-flag from the
+ ** old hash to the new. */
+ if( ii==0 ){
+ apOut[0]->p[1] = pData->p[1];
+ }
+ }
+ fts5DataRelease(pFree);
+ }
+
+ return res;
+}
+
+/*
+** This is called to rebuild the hash table belonging to segment pSeg.
+** If parameter pData1 is not NULL, then one page of the existing hash table
+** has already been loaded - pData1, which is page iPg1. The key-size for
+** the new hash table is szKey (4 or 8).
+**
+** If successful, the new hash table is not written to disk. Instead,
+** output parameter (*pnOut) is set to the number of pages in the new
+** hash table, and (*papOut) to point to an array of buffers containing
+** the new page data.
+**
+** If an error occurs, an error code is left in the Fts5Index object and
+** both output parameters set to 0 before returning.
+*/
+static void fts5IndexTombstoneRebuild(
+ Fts5Index *p,
+ Fts5StructureSegment *pSeg, /* Segment to rebuild hash of */
+ Fts5Data *pData1, /* One page of current hash - or NULL */
+ int iPg1, /* Which page of the current hash is pData1 */
+ int szKey, /* 4 or 8, the keysize */
+ int *pnOut, /* OUT: Number of output pages */
+ Fts5Data ***papOut /* OUT: Output hash pages */
+){
+ const int MINSLOT = 32;
+ int nSlotPerPage = MAX(MINSLOT, (p->pConfig->pgsz - 8) / szKey);
+ int nSlot = 0; /* Number of slots in each output page */
+ int nOut = 0;
+
+ /* Figure out how many output pages (nOut) and how many slots per
+ ** page (nSlot). There are three possibilities:
+ **
+ ** 1. The hash table does not yet exist. In this case the new hash
+ ** table will consist of a single page with MINSLOT slots.
+ **
+ ** 2. The hash table exists but is currently a single page. In this
+ ** case an attempt is made to grow the page to accommodate the new
+ ** entry. The page is allowed to grow up to nSlotPerPage (see above)
+ ** slots.
+ **
+ ** 3. The hash table already consists of more than one page, or of
+ ** a single page already so large that it cannot be grown. In this
+ ** case the new hash consists of (nPg*2+1) pages of nSlotPerPage
+ ** slots each, where nPg is the current number of pages in the
+ ** hash table.
+ */
+ if( pSeg->nPgTombstone==0 ){
+ /* Case 1. */
+ nOut = 1;
+ nSlot = MINSLOT;
+ }else if( pSeg->nPgTombstone==1 ){
+ /* Case 2. */
+ int nElem = (int)fts5GetU32(&pData1->p[4]);
+ assert( pData1 && iPg1==0 );
+ nOut = 1;
+ nSlot = MAX(nElem*4, MINSLOT);
+ if( nSlot>nSlotPerPage ) nOut = 0;
+ }
+ if( nOut==0 ){
+ /* Case 3. */
+ nOut = (pSeg->nPgTombstone * 2 + 1);
+ nSlot = nSlotPerPage;
+ }
+
+ /* Allocate the required array and output pages */
+ while( 1 ){
+ int res = 0;
+ int ii = 0;
+ int szPage = 0;
+ Fts5Data **apOut = 0;
+
+ /* Allocate space for the new hash table */
+ assert( nSlot>=MINSLOT );
+ apOut = (Fts5Data**)sqlite3Fts5MallocZero(&p->rc, sizeof(Fts5Data*) * nOut);
+ szPage = 8 + nSlot*szKey;
+ for(ii=0; iirc,
+ sizeof(Fts5Data)+szPage
+ );
+ if( pNew ){
+ pNew->nn = szPage;
+ pNew->p = (u8*)&pNew[1];
+ apOut[ii] = pNew;
+ }
+ }
+
+ /* Rebuild the hash table. */
+ if( p->rc==SQLITE_OK ){
+ res = fts5IndexTombstoneRehash(p, pSeg, pData1, iPg1, szKey, nOut, apOut);
+ }
+ if( res==0 ){
+ if( p->rc ){
+ fts5IndexFreeArray(apOut, nOut);
+ apOut = 0;
+ nOut = 0;
+ }
+ *pnOut = nOut;
+ *papOut = apOut;
+ break;
+ }
+
+ /* If control flows to here, it was not possible to rebuild the hash
+ ** table. Free all buffers and then try again with more pages. */
+ assert( p->rc==SQLITE_OK );
+ fts5IndexFreeArray(apOut, nOut);
+ nSlot = nSlotPerPage;
+ nOut = nOut*2 + 1;
+ }
+}
+
+
+/*
+** Add a tombstone for rowid iRowid to segment pSeg.
+*/
+static void fts5IndexTombstoneAdd(
+ Fts5Index *p,
+ Fts5StructureSegment *pSeg,
+ u64 iRowid
+){
+ Fts5Data *pPg = 0;
+ int iPg = -1;
+ int szKey = 0;
+ int nHash = 0;
+ Fts5Data **apHash = 0;
+
+ p->nContentlessDelete++;
+
+ if( pSeg->nPgTombstone>0 ){
+ iPg = iRowid % pSeg->nPgTombstone;
+ pPg = fts5DataRead(p, FTS5_TOMBSTONE_ROWID(pSeg->iSegid,iPg));
+ if( pPg==0 ){
+ assert( p->rc!=SQLITE_OK );
+ return;
+ }
+
+ if( 0==fts5IndexTombstoneAddToPage(pPg, 0, pSeg->nPgTombstone, iRowid) ){
+ fts5DataWrite(p, FTS5_TOMBSTONE_ROWID(pSeg->iSegid,iPg), pPg->p, pPg->nn);
+ fts5DataRelease(pPg);
+ return;
+ }
+ }
+
+ /* Have to rebuild the hash table. First figure out the key-size (4 or 8). */
+ szKey = pPg ? TOMBSTONE_KEYSIZE(pPg) : 4;
+ if( iRowid>0xFFFFFFFF ) szKey = 8;
+
+ /* Rebuild the hash table */
+ fts5IndexTombstoneRebuild(p, pSeg, pPg, iPg, szKey, &nHash, &apHash);
+ assert( p->rc==SQLITE_OK || (nHash==0 && apHash==0) );
+
+ /* If all has succeeded, write the new rowid into one of the new hash
+ ** table pages, then write them all out to disk. */
+ if( nHash ){
+ int ii = 0;
+ fts5IndexTombstoneAddToPage(apHash[iRowid % nHash], 1, nHash, iRowid);
+ for(ii=0; iiiSegid, ii);
+ fts5DataWrite(p, iTombstoneRowid, apHash[ii]->p, apHash[ii]->nn);
+ }
+ pSeg->nPgTombstone = nHash;
+ fts5StructureWrite(p, p->pStruct);
+ }
+
+ fts5DataRelease(pPg);
+ fts5IndexFreeArray(apHash, nHash);
+}
+
+/*
+** Add iRowid to the tombstone list of the segment or segments that contain
+** rows from origin iOrigin. Return SQLITE_OK if successful, or an SQLite
+** error code otherwise.
+*/
+int sqlite3Fts5IndexContentlessDelete(Fts5Index *p, i64 iOrigin, i64 iRowid){
+ Fts5Structure *pStruct;
+ pStruct = fts5StructureRead(p);
+ if( pStruct ){
+ int bFound = 0; /* True after pSeg->nEntryTombstone incr. */
+ int iLvl;
+ for(iLvl=pStruct->nLevel-1; iLvl>=0; iLvl--){
+ int iSeg;
+ for(iSeg=pStruct->aLevel[iLvl].nSeg-1; iSeg>=0; iSeg--){
+ Fts5StructureSegment *pSeg = &pStruct->aLevel[iLvl].aSeg[iSeg];
+ if( pSeg->iOrigin1<=(u64)iOrigin && pSeg->iOrigin2>=(u64)iOrigin ){
+ if( bFound==0 ){
+ pSeg->nEntryTombstone++;
+ bFound = 1;
+ }
+ fts5IndexTombstoneAdd(p, pSeg, iRowid);
+ }
+ }
+ }
+ fts5StructureRelease(pStruct);
+ }
+ return fts5IndexReturn(p);
+}
/*************************************************************************
**************************************************************************
@@ -6770,13 +7486,14 @@ int sqlite3Fts5IndexIntegrityCheck(Fts5Index *p, u64 cksum, int bUseCksum){
** function only.
*/
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
/*
** Decode a segment-data rowid from the %_data table. This function is
** the opposite of macro FTS5_SEGMENT_ROWID().
*/
static void fts5DecodeRowid(
i64 iRowid, /* Rowid from %_data table */
+ int *pbTombstone, /* OUT: Tombstone hash flag */
int *piSegid, /* OUT: Segment id */
int *pbDlidx, /* OUT: Dlidx flag */
int *piHeight, /* OUT: Height */
@@ -6792,13 +7509,16 @@ static void fts5DecodeRowid(
iRowid >>= FTS5_DATA_DLI_B;
*piSegid = (int)(iRowid & (((i64)1 << FTS5_DATA_ID_B) - 1));
+ iRowid >>= FTS5_DATA_ID_B;
+
+ *pbTombstone = (int)(iRowid & 0x0001);
}
-#endif /* SQLITE_TEST */
+#endif /* SQLITE_TEST || SQLITE_FTS5_DEBUG */
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
static void fts5DebugRowid(int *pRc, Fts5Buffer *pBuf, i64 iKey){
- int iSegid, iHeight, iPgno, bDlidx; /* Rowid compenents */
- fts5DecodeRowid(iKey, &iSegid, &bDlidx, &iHeight, &iPgno);
+ int iSegid, iHeight, iPgno, bDlidx, bTomb; /* Rowid compenents */
+ fts5DecodeRowid(iKey, &bTomb, &iSegid, &bDlidx, &iHeight, &iPgno);
if( iSegid==0 ){
if( iKey==FTS5_AVERAGES_ROWID ){
@@ -6808,14 +7528,16 @@ static void fts5DebugRowid(int *pRc, Fts5Buffer *pBuf, i64 iKey){
}
}
else{
- sqlite3Fts5BufferAppendPrintf(pRc, pBuf, "{%ssegid=%d h=%d pgno=%d}",
- bDlidx ? "dlidx " : "", iSegid, iHeight, iPgno
+ sqlite3Fts5BufferAppendPrintf(pRc, pBuf, "{%s%ssegid=%d h=%d pgno=%d}",
+ bDlidx ? "dlidx " : "",
+ bTomb ? "tombstone " : "",
+ iSegid, iHeight, iPgno
);
}
}
-#endif /* SQLITE_TEST */
+#endif /* SQLITE_TEST || SQLITE_FTS5_DEBUG */
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
static void fts5DebugStructure(
int *pRc, /* IN/OUT: error code */
Fts5Buffer *pBuf,
@@ -6830,16 +7552,22 @@ static void fts5DebugStructure(
);
for(iSeg=0; iSegnSeg; iSeg++){
Fts5StructureSegment *pSeg = &pLvl->aSeg[iSeg];
- sqlite3Fts5BufferAppendPrintf(pRc, pBuf, " {id=%d leaves=%d..%d}",
+ sqlite3Fts5BufferAppendPrintf(pRc, pBuf, " {id=%d leaves=%d..%d",
pSeg->iSegid, pSeg->pgnoFirst, pSeg->pgnoLast
);
+ if( pSeg->iOrigin1>0 ){
+ sqlite3Fts5BufferAppendPrintf(pRc, pBuf, " origin=%lld..%lld",
+ pSeg->iOrigin1, pSeg->iOrigin2
+ );
+ }
+ sqlite3Fts5BufferAppendPrintf(pRc, pBuf, "}");
}
sqlite3Fts5BufferAppendPrintf(pRc, pBuf, "}");
}
}
-#endif /* SQLITE_TEST */
+#endif /* SQLITE_TEST || SQLITE_FTS5_DEBUG */
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
/*
** This is part of the fts5_decode() debugging aid.
**
@@ -6864,9 +7592,9 @@ static void fts5DecodeStructure(
fts5DebugStructure(pRc, pBuf, p);
fts5StructureRelease(p);
}
-#endif /* SQLITE_TEST */
+#endif /* SQLITE_TEST || SQLITE_FTS5_DEBUG */
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
/*
** This is part of the fts5_decode() debugging aid.
**
@@ -6889,9 +7617,9 @@ static void fts5DecodeAverages(
zSpace = " ";
}
}
-#endif /* SQLITE_TEST */
+#endif /* SQLITE_TEST || SQLITE_FTS5_DEBUG */
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
/*
** Buffer (a/n) is assumed to contain a list of serialized varints. Read
** each varint and append its string representation to buffer pBuf. Return
@@ -6908,9 +7636,9 @@ static int fts5DecodePoslist(int *pRc, Fts5Buffer *pBuf, const u8 *a, int n){
}
return iOff;
}
-#endif /* SQLITE_TEST */
+#endif /* SQLITE_TEST || SQLITE_FTS5_DEBUG */
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
/*
** The start of buffer (a/n) contains the start of a doclist. The doclist
** may or may not finish within the buffer. This function appends a text
@@ -6943,9 +7671,9 @@ static int fts5DecodeDoclist(int *pRc, Fts5Buffer *pBuf, const u8 *a, int n){
return iOff;
}
-#endif /* SQLITE_TEST */
+#endif /* SQLITE_TEST || SQLITE_FTS5_DEBUG */
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
/*
** This function is part of the fts5_decode() debugging function. It is
** only ever used with detail=none tables.
@@ -6986,9 +7714,9 @@ static void fts5DecodeRowidList(
sqlite3Fts5BufferAppendPrintf(pRc, pBuf, " %lld%s", iRowid, zApp);
}
}
-#endif /* SQLITE_TEST */
+#endif /* SQLITE_TEST || SQLITE_FTS5_DEBUG */
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
/*
** The implementation of user-defined scalar function fts5_decode().
*/
@@ -6999,6 +7727,7 @@ static void fts5DecodeFunction(
){
i64 iRowid; /* Rowid for record being decoded */
int iSegid,iHeight,iPgno,bDlidx;/* Rowid components */
+ int bTomb;
const u8 *aBlob; int n; /* Record to decode */
u8 *a = 0;
Fts5Buffer s; /* Build up text to return here */
@@ -7021,7 +7750,7 @@ static void fts5DecodeFunction(
if( a==0 ) goto decode_out;
if( n>0 ) memcpy(a, aBlob, n);
- fts5DecodeRowid(iRowid, &iSegid, &bDlidx, &iHeight, &iPgno);
+ fts5DecodeRowid(iRowid, &bTomb, &iSegid, &bDlidx, &iHeight, &iPgno);
fts5DebugRowid(&rc, &s, iRowid);
if( bDlidx ){
@@ -7040,6 +7769,28 @@ static void fts5DecodeFunction(
" %d(%lld)", lvl.iLeafPgno, lvl.iRowid
);
}
+ }else if( bTomb ){
+ u32 nElem = fts5GetU32(&a[4]);
+ int szKey = (aBlob[0]==4 || aBlob[0]==8) ? aBlob[0] : 8;
+ int nSlot = (n - 8) / szKey;
+ int ii;
+ sqlite3Fts5BufferAppendPrintf(&rc, &s, " nElem=%d", (int)nElem);
+ if( aBlob[1] ){
+ sqlite3Fts5BufferAppendPrintf(&rc, &s, " 0");
+ }
+ for(ii=0; iiszLeaf ){
+ rc = FTS5_CORRUPT;
+ }else{
+ fts5DecodeRowidList(&rc, &s, &a[iOff], iTermOff-iOff);
+ }
iOff = iTermOff;
if( iOffestimatedCost = (double)100;
+ pIdxInfo->estimatedRows = 100;
+ pIdxInfo->idxNum = 0;
+ for(i=0, p=pIdxInfo->aConstraint; inConstraint; i++, p++){
+ if( p->usable==0 ) continue;
+ if( p->op==SQLITE_INDEX_CONSTRAINT_EQ && p->iColumn==11 ){
+ rc = SQLITE_OK;
+ pIdxInfo->aConstraintUsage[i].omit = 1;
+ pIdxInfo->aConstraintUsage[i].argvIndex = 1;
+ break;
+ }
+ }
+ return rc;
+}
+
+/*
+** This method is the destructor for bytecodevtab objects.
+*/
+static int fts5structDisconnectMethod(sqlite3_vtab *pVtab){
+ Fts5StructVtab *p = (Fts5StructVtab*)pVtab;
+ sqlite3_free(p);
+ return SQLITE_OK;
+}
+
+/*
+** Constructor for a new bytecodevtab_cursor object.
+*/
+static int fts5structOpenMethod(sqlite3_vtab *p, sqlite3_vtab_cursor **ppCsr){
+ int rc = SQLITE_OK;
+ Fts5StructVcsr *pNew = 0;
+
+ pNew = sqlite3Fts5MallocZero(&rc, sizeof(*pNew));
+ *ppCsr = (sqlite3_vtab_cursor*)pNew;
+
+ return SQLITE_OK;
+}
+
+/*
+** Destructor for a bytecodevtab_cursor.
+*/
+static int fts5structCloseMethod(sqlite3_vtab_cursor *cur){
+ Fts5StructVcsr *pCsr = (Fts5StructVcsr*)cur;
+ fts5StructureRelease(pCsr->pStruct);
+ sqlite3_free(pCsr);
+ return SQLITE_OK;
+}
+
+
+/*
+** Advance a bytecodevtab_cursor to its next row of output.
+*/
+static int fts5structNextMethod(sqlite3_vtab_cursor *cur){
+ Fts5StructVcsr *pCsr = (Fts5StructVcsr*)cur;
+ Fts5Structure *p = pCsr->pStruct;
+
+ assert( pCsr->pStruct );
+ pCsr->iSeg++;
+ pCsr->iRowid++;
+ while( pCsr->iLevelnLevel && pCsr->iSeg>=p->aLevel[pCsr->iLevel].nSeg ){
+ pCsr->iLevel++;
+ pCsr->iSeg = 0;
+ }
+ if( pCsr->iLevel>=p->nLevel ){
+ fts5StructureRelease(pCsr->pStruct);
+ pCsr->pStruct = 0;
+ }
+ return SQLITE_OK;
+}
+
+/*
+** Return TRUE if the cursor has been moved off of the last
+** row of output.
+*/
+static int fts5structEofMethod(sqlite3_vtab_cursor *cur){
+ Fts5StructVcsr *pCsr = (Fts5StructVcsr*)cur;
+ return pCsr->pStruct==0;
+}
+
+static int fts5structRowidMethod(
+ sqlite3_vtab_cursor *cur,
+ sqlite_int64 *piRowid
+){
+ Fts5StructVcsr *pCsr = (Fts5StructVcsr*)cur;
+ *piRowid = pCsr->iRowid;
+ return SQLITE_OK;
+}
+
+/*
+** Return values of columns for the row at which the bytecodevtab_cursor
+** is currently pointing.
+*/
+static int fts5structColumnMethod(
+ sqlite3_vtab_cursor *cur, /* The cursor */
+ sqlite3_context *ctx, /* First argument to sqlite3_result_...() */
+ int i /* Which column to return */
+){
+ Fts5StructVcsr *pCsr = (Fts5StructVcsr*)cur;
+ Fts5Structure *p = pCsr->pStruct;
+ Fts5StructureSegment *pSeg = &p->aLevel[pCsr->iLevel].aSeg[pCsr->iSeg];
+
+ switch( i ){
+ case 0: /* level */
+ sqlite3_result_int(ctx, pCsr->iLevel);
+ break;
+ case 1: /* segment */
+ sqlite3_result_int(ctx, pCsr->iSeg);
+ break;
+ case 2: /* merge */
+ sqlite3_result_int(ctx, pCsr->iSeg < p->aLevel[pCsr->iLevel].nMerge);
+ break;
+ case 3: /* segid */
+ sqlite3_result_int(ctx, pSeg->iSegid);
+ break;
+ case 4: /* leaf1 */
+ sqlite3_result_int(ctx, pSeg->pgnoFirst);
+ break;
+ case 5: /* leaf2 */
+ sqlite3_result_int(ctx, pSeg->pgnoLast);
+ break;
+ case 6: /* origin1 */
+ sqlite3_result_int64(ctx, pSeg->iOrigin1);
+ break;
+ case 7: /* origin2 */
+ sqlite3_result_int64(ctx, pSeg->iOrigin2);
+ break;
+ case 8: /* npgtombstone */
+ sqlite3_result_int(ctx, pSeg->nPgTombstone);
+ break;
+ case 9: /* nentrytombstone */
+ sqlite3_result_int64(ctx, pSeg->nEntryTombstone);
+ break;
+ case 10: /* nentry */
+ sqlite3_result_int64(ctx, pSeg->nEntry);
+ break;
+ }
+ return SQLITE_OK;
+}
+
+/*
+** Initialize a cursor.
+**
+** idxNum==0 means show all subprograms
+** idxNum==1 means show only the main bytecode and omit subprograms.
+*/
+static int fts5structFilterMethod(
+ sqlite3_vtab_cursor *pVtabCursor,
+ int idxNum, const char *idxStr,
+ int argc, sqlite3_value **argv
+){
+ Fts5StructVcsr *pCsr = (Fts5StructVcsr *)pVtabCursor;
+ int rc = SQLITE_OK;
+
+ const u8 *aBlob = 0;
+ int nBlob = 0;
+
+ assert( argc==1 );
+ fts5StructureRelease(pCsr->pStruct);
+ pCsr->pStruct = 0;
+
+ nBlob = sqlite3_value_bytes(argv[0]);
+ aBlob = (const u8*)sqlite3_value_blob(argv[0]);
+ rc = fts5StructureDecode(aBlob, nBlob, 0, &pCsr->pStruct);
+ if( rc==SQLITE_OK ){
+ pCsr->iLevel = 0;
+ pCsr->iRowid = 0;
+ pCsr->iSeg = -1;
+ rc = fts5structNextMethod(pVtabCursor);
+ }
+
+ return rc;
+}
+
+#endif /* SQLITE_TEST || SQLITE_FTS5_DEBUG */
/*
** This is called as part of registering the FTS5 module with database
@@ -7244,7 +8226,7 @@ static void fts5RowidFunction(
** SQLite error code is returned instead.
*/
int sqlite3Fts5IndexInit(sqlite3 *db){
-#ifdef SQLITE_TEST
+#if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG)
int rc = sqlite3_create_function(
db, "fts5_decode", 2, SQLITE_UTF8, 0, fts5DecodeFunction, 0, 0
);
@@ -7261,6 +8243,36 @@ int sqlite3Fts5IndexInit(sqlite3 *db){
db, "fts5_rowid", -1, SQLITE_UTF8, 0, fts5RowidFunction, 0, 0
);
}
+
+ if( rc==SQLITE_OK ){
+ static const sqlite3_module fts5structure_module = {
+ 0, /* iVersion */
+ 0, /* xCreate */
+ fts5structConnectMethod, /* xConnect */
+ fts5structBestIndexMethod, /* xBestIndex */
+ fts5structDisconnectMethod, /* xDisconnect */
+ 0, /* xDestroy */
+ fts5structOpenMethod, /* xOpen */
+ fts5structCloseMethod, /* xClose */
+ fts5structFilterMethod, /* xFilter */
+ fts5structNextMethod, /* xNext */
+ fts5structEofMethod, /* xEof */
+ fts5structColumnMethod, /* xColumn */
+ fts5structRowidMethod, /* xRowid */
+ 0, /* xUpdate */
+ 0, /* xBegin */
+ 0, /* xSync */
+ 0, /* xCommit */
+ 0, /* xRollback */
+ 0, /* xFindFunction */
+ 0, /* xRename */
+ 0, /* xSavepoint */
+ 0, /* xRelease */
+ 0, /* xRollbackTo */
+ 0 /* xShadowName */
+ };
+ rc = sqlite3_create_module(db, "fts5_structure", &fts5structure_module, 0);
+ }
return rc;
#else
return SQLITE_OK;
diff --git a/ext/fts5/fts5_main.c b/ext/fts5/fts5_main.c
index 13921ce49..c34a5a332 100644
--- a/ext/fts5/fts5_main.c
+++ b/ext/fts5/fts5_main.c
@@ -1624,7 +1624,6 @@ static int fts5UpdateMethod(
int eType0; /* value_type() of apVal[0] */
int rc = SQLITE_OK; /* Return code */
int bUpdateOrDelete = 0;
-
/* A transaction must be open when this is called. */
assert( pTab->ts.eState==1 || pTab->ts.eState==2 );
@@ -1654,7 +1653,14 @@ static int fts5UpdateMethod(
if( pConfig->eContent!=FTS5_CONTENT_NORMAL
&& 0==sqlite3_stricmp("delete", z)
){
- rc = fts5SpecialDelete(pTab, apVal);
+ if( pConfig->bContentlessDelete ){
+ fts5SetVtabError(pTab,
+ "'delete' may not be used with a contentless_delete=1 table"
+ );
+ rc = SQLITE_ERROR;
+ }else{
+ rc = fts5SpecialDelete(pTab, apVal);
+ }
}else{
rc = fts5SpecialInsert(pTab, z, apVal[2 + pConfig->nCol + 1]);
}
@@ -1671,7 +1677,7 @@ static int fts5UpdateMethod(
** Cases 3 and 4 may violate the rowid constraint.
*/
int eConflict = SQLITE_ABORT;
- if( pConfig->eContent==FTS5_CONTENT_NORMAL ){
+ if( pConfig->eContent==FTS5_CONTENT_NORMAL || pConfig->bContentlessDelete ){
eConflict = sqlite3_vtab_on_conflict(pConfig->db);
}
@@ -1679,8 +1685,12 @@ static int fts5UpdateMethod(
assert( nArg!=1 || eType0==SQLITE_INTEGER );
/* Filter out attempts to run UPDATE or DELETE on contentless tables.
- ** This is not suported. */
- if( eType0==SQLITE_INTEGER && fts5IsContentless(pTab) ){
+ ** This is not suported. Except - DELETE is supported if the CREATE
+ ** VIRTUAL TABLE statement contained "contentless_delete=1". */
+ if( eType0==SQLITE_INTEGER
+ && pConfig->eContent==FTS5_CONTENT_NONE
+ && pConfig->bContentlessDelete==0
+ ){
pTab->p.base.zErrMsg = sqlite3_mprintf(
"cannot %s contentless fts5 table: %s",
(nArg>1 ? "UPDATE" : "DELETE from"), pConfig->zName
@@ -1767,8 +1777,7 @@ static int fts5SyncMethod(sqlite3_vtab *pVtab){
Fts5FullTable *pTab = (Fts5FullTable*)pVtab;
fts5CheckTransactionState(pTab, FTS5_SYNC, 0);
pTab->p.pConfig->pzErrmsg = &pTab->p.base.zErrMsg;
- fts5TripCursors(pTab);
- rc = sqlite3Fts5StorageSync(pTab->pStorage);
+ rc = sqlite3Fts5FlushToDisk(&pTab->p);
pTab->p.pConfig->pzErrmsg = 0;
return rc;
}
@@ -2535,6 +2544,12 @@ static int fts5ColumnMethod(
sqlite3_result_value(pCtx, sqlite3_column_value(pCsr->pStmt, iCol+1));
}
pConfig->pzErrmsg = 0;
+ }else if( pConfig->bContentlessDelete && sqlite3_vtab_nochange(pCtx) ){
+ char *zErr = sqlite3_mprintf("cannot UPDATE a subset of "
+ "columns on fts5 contentless-delete table: %s", pConfig->zName
+ );
+ sqlite3_result_error(pCtx, zErr, -1);
+ sqlite3_free(zErr);
}
return rc;
}
diff --git a/ext/fts5/fts5_storage.c b/ext/fts5/fts5_storage.c
index 02b98d9e4..0a0af9d4b 100644
--- a/ext/fts5/fts5_storage.c
+++ b/ext/fts5/fts5_storage.c
@@ -77,10 +77,10 @@ static int fts5StorageGetStmt(
"INSERT INTO %Q.'%q_content' VALUES(%s)", /* INSERT_CONTENT */
"REPLACE INTO %Q.'%q_content' VALUES(%s)", /* REPLACE_CONTENT */
"DELETE FROM %Q.'%q_content' WHERE id=?", /* DELETE_CONTENT */
- "REPLACE INTO %Q.'%q_docsize' VALUES(?,?)", /* REPLACE_DOCSIZE */
+ "REPLACE INTO %Q.'%q_docsize' VALUES(?,?%s)", /* REPLACE_DOCSIZE */
"DELETE FROM %Q.'%q_docsize' WHERE id=?", /* DELETE_DOCSIZE */
- "SELECT sz FROM %Q.'%q_docsize' WHERE id=?", /* LOOKUP_DOCSIZE */
+ "SELECT sz%s FROM %Q.'%q_docsize' WHERE id=?", /* LOOKUP_DOCSIZE */
"REPLACE INTO %Q.'%q_config' VALUES(?,?)", /* REPLACE_CONFIG */
"SELECT %s FROM %s AS T", /* SCAN */
@@ -128,6 +128,19 @@ static int fts5StorageGetStmt(
break;
}
+ case FTS5_STMT_REPLACE_DOCSIZE:
+ zSql = sqlite3_mprintf(azStmt[eStmt], pC->zDb, pC->zName,
+ (pC->bContentlessDelete ? ",?" : "")
+ );
+ break;
+
+ case FTS5_STMT_LOOKUP_DOCSIZE:
+ zSql = sqlite3_mprintf(azStmt[eStmt],
+ (pC->bContentlessDelete ? ",origin" : ""),
+ pC->zDb, pC->zName
+ );
+ break;
+
default:
zSql = sqlite3_mprintf(azStmt[eStmt], pC->zDb, pC->zName);
break;
@@ -317,9 +330,11 @@ int sqlite3Fts5StorageOpen(
}
if( rc==SQLITE_OK && pConfig->bColumnsize ){
- rc = sqlite3Fts5CreateTable(
- pConfig, "docsize", "id INTEGER PRIMARY KEY, sz BLOB", 0, pzErr
- );
+ const char *zCols = "id INTEGER PRIMARY KEY, sz BLOB";
+ if( pConfig->bContentlessDelete ){
+ zCols = "id INTEGER PRIMARY KEY, sz BLOB, origin INTEGER";
+ }
+ rc = sqlite3Fts5CreateTable(pConfig, "docsize", zCols, 0, pzErr);
}
if( rc==SQLITE_OK ){
rc = sqlite3Fts5CreateTable(
@@ -396,7 +411,7 @@ static int fts5StorageDeleteFromIndex(
){
Fts5Config *pConfig = p->pConfig;
sqlite3_stmt *pSeek = 0; /* SELECT to read row iDel from %_data */
- int rc; /* Return code */
+ int rc = SQLITE_OK; /* Return code */
int rc2; /* sqlite3_reset() return code */
int iCol;
Fts5InsertCtx ctx;
@@ -412,7 +427,6 @@ static int fts5StorageDeleteFromIndex(
ctx.pStorage = p;
ctx.iCol = -1;
- rc = sqlite3Fts5IndexBeginWrite(p->pIndex, 1, iDel);
for(iCol=1; rc==SQLITE_OK && iCol<=pConfig->nCol; iCol++){
if( pConfig->abUnindexed[iCol-1]==0 ){
const char *zText;
@@ -449,6 +463,37 @@ static int fts5StorageDeleteFromIndex(
return rc;
}
+/*
+** This function is called to process a DELETE on a contentless_delete=1
+** table. It adds the tombstone required to delete the entry with rowid
+** iDel. If successful, SQLITE_OK is returned. Or, if an error occurs,
+** an SQLite error code.
+*/
+static int fts5StorageContentlessDelete(Fts5Storage *p, i64 iDel){
+ i64 iOrigin = 0;
+ sqlite3_stmt *pLookup = 0;
+ int rc = SQLITE_OK;
+
+ assert( p->pConfig->bContentlessDelete );
+ assert( p->pConfig->eContent==FTS5_CONTENT_NONE );
+
+ /* Look up the origin of the document in the %_docsize table. Store
+ ** this in stack variable iOrigin. */
+ rc = fts5StorageGetStmt(p, FTS5_STMT_LOOKUP_DOCSIZE, &pLookup, 0);
+ if( rc==SQLITE_OK ){
+ sqlite3_bind_int64(pLookup, 1, iDel);
+ if( SQLITE_ROW==sqlite3_step(pLookup) ){
+ iOrigin = sqlite3_column_int64(pLookup, 1);
+ }
+ rc = sqlite3_reset(pLookup);
+ }
+
+ if( rc==SQLITE_OK && iOrigin!=0 ){
+ rc = sqlite3Fts5IndexContentlessDelete(p->pIndex, iOrigin, iDel);
+ }
+
+ return rc;
+}
/*
** Insert a record into the %_docsize table. Specifically, do:
@@ -469,10 +514,17 @@ static int fts5StorageInsertDocsize(
rc = fts5StorageGetStmt(p, FTS5_STMT_REPLACE_DOCSIZE, &pReplace, 0);
if( rc==SQLITE_OK ){
sqlite3_bind_int64(pReplace, 1, iRowid);
- sqlite3_bind_blob(pReplace, 2, pBuf->p, pBuf->n, SQLITE_STATIC);
- sqlite3_step(pReplace);
- rc = sqlite3_reset(pReplace);
- sqlite3_bind_null(pReplace, 2);
+ if( p->pConfig->bContentlessDelete ){
+ i64 iOrigin = 0;
+ rc = sqlite3Fts5IndexGetOrigin(p->pIndex, &iOrigin);
+ sqlite3_bind_int64(pReplace, 3, iOrigin);
+ }
+ if( rc==SQLITE_OK ){
+ sqlite3_bind_blob(pReplace, 2, pBuf->p, pBuf->n, SQLITE_STATIC);
+ sqlite3_step(pReplace);
+ rc = sqlite3_reset(pReplace);
+ sqlite3_bind_null(pReplace, 2);
+ }
}
}
return rc;
@@ -536,7 +588,15 @@ int sqlite3Fts5StorageDelete(Fts5Storage *p, i64 iDel, sqlite3_value **apVal){
/* Delete the index records */
if( rc==SQLITE_OK ){
- rc = fts5StorageDeleteFromIndex(p, iDel, apVal);
+ rc = sqlite3Fts5IndexBeginWrite(p->pIndex, 1, iDel);
+ }
+
+ if( rc==SQLITE_OK ){
+ if( p->pConfig->bContentlessDelete ){
+ rc = fts5StorageContentlessDelete(p, iDel);
+ }else{
+ rc = fts5StorageDeleteFromIndex(p, iDel, apVal);
+ }
}
/* Delete the %_docsize record */
diff --git a/ext/fts5/test/fts5bigid.test b/ext/fts5/test/fts5bigid.test
new file mode 100644
index 000000000..ae20ec641
--- /dev/null
+++ b/ext/fts5/test/fts5bigid.test
@@ -0,0 +1,62 @@
+# 2023 May 28
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#*************************************************************************
+#
+
+source [file join [file dirname [info script]] fts5_common.tcl]
+set testprefix fts5bigid
+
+# If SQLITE_ENABLE_FTS5 is not defined, omit this file.
+ifcapable !fts5 {
+ finish_test
+ return
+}
+
+set nRow 20000
+
+proc do_ascdesc_test {tn query} {
+ set ::lAsc [db eval { SELECT rowid FROM x1($query) }]
+ set ::lDesc [db eval { SELECT rowid FROM x1($query) ORDER BY rowid DESC }]
+ do_test $tn.1 { lsort -integer $::lAsc } $::lAsc
+ do_test $tn.2 { lsort -integer -decr $::lDesc } $::lDesc
+ do_test $tn.3 { lsort -integer $::lDesc } $::lAsc
+}
+
+do_execsql_test 1.0 {
+ CREATE VIRTUAL TABLE x1 USING fts5(a);
+}
+
+do_test 1.1 {
+ for {set ii 0} {$ii < $nRow} {incr ii} {
+ db eval {
+ REPLACE INTO x1(rowid, a) VALUES(random(), 'movement at the station');
+ }
+ }
+} {}
+
+do_ascdesc_test 1.2 "the"
+
+do_execsql_test 1.3 {
+ DELETE FROM x1
+}
+
+do_test 1.4 {
+ for {set ii 0} {$ii < $nRow} {incr ii} {
+ db eval {
+ INSERT INTO x1(rowid, a) VALUES(
+ $ii + 0x6FFFFFFFFFFFFFFF, 'movement at the station'
+ );
+ }
+ }
+} {}
+
+do_ascdesc_test 1.5 "movement"
+
+finish_test
diff --git a/ext/fts5/test/fts5contentless.test b/ext/fts5/test/fts5contentless.test
new file mode 100644
index 000000000..f75ccb44c
--- /dev/null
+++ b/ext/fts5/test/fts5contentless.test
@@ -0,0 +1,271 @@
+# 2014 Dec 20
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+# This file contains tests for the content= and content_rowid= options.
+#
+
+source [file join [file dirname [info script]] fts5_common.tcl]
+set testprefix fts5contentless
+
+# If SQLITE_ENABLE_FTS5 is defined, omit this file.
+ifcapable !fts5 {
+ finish_test
+ return
+}
+
+# Check that it is not possible to specify "contentless_delete=1" for
+# anything other than a contentless table.
+#
+set res(0) {0 {}}
+set res(1) {1 {contentless_delete=1 requires a contentless table}}
+foreach {tn sql bError} {
+ 1 "(a, b, contentless_delete=1)" 1
+ 2 "(a, b, contentless_delete=1, content=abc)" 1
+ 3 "(a, b, contentless_delete=1, content=)" 0
+ 4 "(content=, contentless_delete=1, a)" 0
+ 5 "(content='', contentless_delete=1, hello)" 0
+} {
+ execsql { BEGIN }
+ do_catchsql_test 1.$tn "CREATE VIRTUAL TABLE t1 USING fts5 $sql" $res($bError)
+ execsql { ROLLBACK }
+}
+
+# Check that it is not possible to specify "contentless_delete=1"
+# along with columnsize=1.
+#
+set res(0) {0 {}}
+set res(1) {1 {contentless_delete=1 is incompatible with columnsize=0}}
+foreach {tn sql bError} {
+ 2 "(a, b, content='', contentless_delete=1, columnsize=0)" 1
+} {
+ execsql { BEGIN }
+ do_catchsql_test 1.$tn "CREATE VIRTUAL TABLE t1 USING fts5 $sql" $res($bError)
+ execsql { ROLLBACK }
+}
+
+# Check that if contentless_delete=1 is specified, then the "origin"
+# column is added to the %_docsize table.
+reset_db
+do_execsql_test 3.0 {
+ CREATE VIRTUAL TABLE x1 USING fts5(c, content='');
+ CREATE VIRTUAL TABLE x2 USING fts5(c, content='', contentless_delete=1);
+}
+do_execsql_test 3.1 {
+ SELECT sql FROM sqlite_schema WHERE name IN ('x1_docsize', 'x2_docsize');
+} {
+ {CREATE TABLE 'x1_docsize'(id INTEGER PRIMARY KEY, sz BLOB)}
+ {CREATE TABLE 'x2_docsize'(id INTEGER PRIMARY KEY, sz BLOB, origin INTEGER)}
+}
+
+do_execsql_test 3.2.1 {
+ SELECT hex(block) FROM x1_data WHERE id=10
+} {00000000000000}
+do_execsql_test 3.2.2 {
+ SELECT hex(block) FROM x2_data WHERE id=10
+} {00000000FF000001000000}
+
+do_execsql_test 3.3 {
+ INSERT INTO x2 VALUES('first text');
+ INSERT INTO x2 VALUES('second text');
+}
+do_execsql_test 3.4 {
+ SELECT id, origin FROM x2_docsize
+} {1 1 2 2}
+do_execsql_test 3.5 {
+ SELECT level, segment, loc1, loc2 FROM fts5_structure(
+ (SELECT block FROM x2_data WHERE id=10)
+ )
+} {
+ 0 0 1 1
+ 0 1 2 2
+}
+do_execsql_test 3.6 {
+ INSERT INTO x2(x2) VALUES('optimize');
+}
+do_execsql_test 3.7 {
+ SELECT level, segment, loc1, loc2 FROM fts5_structure(
+ (SELECT block FROM x2_data WHERE id=10)
+ )
+} {
+ 1 0 1 2
+}
+
+do_execsql_test 3.8 {
+ DELETE FROM x2 WHERE rowid=2;
+}
+
+do_execsql_test 3.9 {
+ SELECT rowid FROM x2('text')
+} {1}
+
+#--------------------------------------------------------------------------
+reset_db
+proc document {n} {
+ set vocab [list A B C D E F G H I J K L M N O P Q R S T U V W X Y Z]
+ set ret [list]
+ for {set ii 0} {$ii < $n} {incr ii} {
+ lappend ret [lindex $vocab [expr int(rand()*[llength $vocab])]]
+ }
+ set ret
+}
+
+set nRow 1000
+
+do_execsql_test 4.0 {
+ CREATE TABLE t1(x);
+ CREATE VIRTUAL TABLE ft USING fts5(x, content='', contentless_delete=1);
+ INSERT INTO ft(ft, rank) VALUES('pgsz', 100);
+}
+do_test 4.1 {
+ for {set ii 0} {$ii < $nRow} {incr ii} {
+ set doc [document 6]
+ execsql {
+ INSERT INTO t1 VALUES($doc);
+ INSERT INTO ft VALUES($doc);
+ }
+ }
+} {}
+
+foreach v {A B C D E F G H I J K L M N O P Q R S T U V W X Y Z} {
+ set L1 [execsql {SELECT rowid FROM t1 WHERE x LIKE '%'||$v||'%'}]
+ set L2 [execsql {SELECT rowid FROM ft($v)}]
+ do_test 4.2.$v { set L1 } $L2
+}
+
+do_test 4.3 {
+ for {set ii 1} {$ii < $nRow} {incr ii 2} {
+ execsql {
+ DELETE FROM ft WHERE rowid=$ii;
+ DELETE FROM t1 WHERE rowid=$ii;
+ }
+ }
+} {}
+
+foreach v {A B C D E F G H I J K L M N O P Q R S T U V W X Y Z} {
+ set L1 [execsql {SELECT rowid FROM t1 WHERE x LIKE '%'||$v||'%'}]
+ set L2 [execsql {SELECT rowid FROM ft($v)}]
+ do_test 4.4.$v { set L1 } $L2
+}
+
+do_execsql_test 4.5 {
+ INSERT INTO ft(ft) VALUES('optimize');
+} {}
+
+foreach v {A B C D E F G H I J K L M N O P Q R S T U V W X Y Z} {
+ set L1 [execsql {SELECT rowid FROM t1 WHERE x LIKE '%'||$v||'%'}]
+ set L2 [execsql {SELECT rowid FROM ft($v)}]
+ do_test 4.6.$v { set L1 } $L2
+}
+
+#execsql_pp { SELECT fts5_decode(id, block) FROM ft_data }
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 5.0 {
+ CREATE VIRTUAL TABLE ft USING fts5(x, content='', contentless_delete=1);
+ INSERT INTO ft(rowid, x) VALUES(1, 'one two three');
+ INSERT INTO ft(rowid, x) VALUES(2, 'one two four');
+ INSERT INTO ft(rowid, x) VALUES(3, 'one two five');
+ INSERT INTO ft(rowid, x) VALUES(4, 'one two seven');
+ INSERT INTO ft(rowid, x) VALUES(5, 'one two eight');
+}
+
+do_execsql_test 5.1 {
+ DELETE FROM ft WHERE rowid=2
+}
+
+do_execsql_test 5.2 {
+ SELECT rowid FROM ft
+} {1 3 4 5}
+
+do_catchsql_test 5.3 {
+ UPDATE ft SET x='four six' WHERE rowid=3
+} {0 {}}
+
+do_execsql_test 5.4 {
+ SELECT rowid FROM ft('one');
+} {1 4 5}
+
+do_execsql_test 5.5 {
+ REPLACE INTO ft(rowid, x) VALUES(3, 'four six');
+ SELECT rowid FROM ft('one');
+} {1 4 5}
+
+do_execsql_test 5.6 {
+ REPLACE INTO ft(rowid, x) VALUES(6, 'one two eleven');
+ SELECT rowid FROM ft('one');
+} {1 4 5 6}
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 6.0 {
+ CREATE VIRTUAL TABLE ft USING fts5(x, content='', contentless_delete=1);
+ INSERT INTO ft(rowid, x) VALUES(1, 'one two three');
+ INSERT INTO ft(rowid, x) VALUES(2, 'one two four');
+}
+
+do_test 6.1 {
+ db eval { SELECT rowid FROM ft('one two') } {
+ if {$rowid==1} {
+ db eval { INSERT INTO ft(rowid, x) VALUES(3, 'one two four') }
+ }
+ }
+} {}
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 7.0 {
+ CREATE VIRTUAL TABLE ft USING fts5(x, content='', contentless_delete=1);
+}
+
+set lRowid [list -450 0 1 2 42]
+
+do_test 7.1 {
+ execsql BEGIN
+ foreach r $lRowid {
+ execsql { INSERT INTO ft(rowid, x) VALUES($r, 'one one one'); }
+ }
+ execsql COMMIT
+} {}
+
+do_test 7.2 {
+ execsql BEGIN
+ foreach r $lRowid {
+ execsql { REPLACE INTO ft(rowid, x) VALUES($r, 'two two two'); }
+ }
+ execsql COMMIT
+} {}
+
+do_execsql_test 7.3 { SELECT rowid FROM ft('one'); } {}
+do_execsql_test 7.4 { SELECT rowid FROM ft('two'); } $lRowid
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 8.0 {
+ CREATE VIRTUAL TABLE ft USING fts5(x, content='', contentless_delete=1);
+ INSERT INTO ft VALUES('hello world');
+ INSERT INTO ft VALUES('one two three');
+}
+
+do_catchsql_test 8.1 {
+ INSERT INTO ft(ft, rowid, x) VALUES('delete', 1, 'hello world');
+} {1 {'delete' may not be used with a contentless_delete=1 table}}
+
+do_execsql_test 8.2 {
+ BEGIN;
+ INSERT INTO ft(rowid, x) VALUES(3, 'four four four');
+ DELETE FROM ft WHERE rowid=3;
+ COMMIT;
+ SELECT rowid FROM ft('four');
+} {}
+
+finish_test
+
diff --git a/ext/fts5/test/fts5contentless2.test b/ext/fts5/test/fts5contentless2.test
new file mode 100644
index 000000000..fbb857ab3
--- /dev/null
+++ b/ext/fts5/test/fts5contentless2.test
@@ -0,0 +1,208 @@
+# 2023 July 19
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+# This file contains tests for the content= and content_rowid= options.
+#
+
+source [file join [file dirname [info script]] fts5_common.tcl]
+set testprefix fts5contentless2
+
+# If SQLITE_ENABLE_FTS5 is defined, omit this file.
+ifcapable !fts5 {
+ finish_test
+ return
+}
+
+proc vocab {} {
+ list aaa bbb ccc ddd eee fff ggg hhh iii jjj kkk lll mmm nnn ooo ppp
+}
+
+proc document {nToken} {
+ set doc [list]
+ set vocab [vocab]
+ for {set ii 0} {$ii < $nToken} {incr ii} {
+ lappend doc [lindex $vocab [expr int(rand()*[llength $vocab])]]
+ }
+ set doc
+}
+db func document document
+
+proc contains {doc token} {
+ expr {[lsearch $doc $token]>=0}
+}
+db func contains contains
+
+proc do_compare_tables_test {tn} {
+ uplevel [list do_test $tn {
+ foreach v [vocab] {
+ set l1 [execsql { SELECT rowid FROM t1 WHERE contains(doc, $v) }]
+ set l2 [execsql { SELECT rowid FROM t2($v) }]
+ if {$l1!=$l2} { error "1: query mismatch ($l1) ($l2)" }
+
+ set w "[string range $v 0 1]*"
+ set l1 [execsql { SELECT rowid FROM t1 WHERE contains(doc, $w) }]
+ set l2 [execsql { SELECT rowid FROM t2($w) }]
+ if {$l1!=$l2} { error "2: query mismatch ($l1) ($l2)" }
+
+ set w "[string range $v 0 0]*"
+ set l1 [execsql { SELECT rowid FROM t1 WHERE contains(doc, $w) }]
+ set l2 [execsql { SELECT rowid FROM t2($w) }]
+ if {$l1!=$l2} { error "2: query mismatch ($l1) ($l2)" }
+
+ set l1 [execsql {
+ SELECT rowid FROM t1 WHERE contains(doc, $v) ORDER BY rowid DESC
+ }]
+ set l2 [execsql { SELECT rowid FROM t2($v) ORDER BY rowid DESC }]
+ if {$l1!=$l2} { error "1: query mismatch ($l1) ($l2)" }
+ }
+ set {} {}
+ } {}]
+}
+
+proc lshuffle {in} {
+ set L [list]
+ set ret [list]
+ foreach elem $in { lappend L [list [expr rand()] $elem] }
+ foreach pair [lsort -index 0 $L] { lappend ret [lindex $pair 1] }
+ set ret
+}
+
+expr srand(0)
+
+do_execsql_test 1.0 {
+ CREATE VIRTUAL TABLE t2 USING fts5(
+ doc, prefix=2, content=, contentless_delete=1
+ );
+
+ CREATE TABLE t1(doc);
+ CREATE TRIGGER tr1 AFTER DELETE ON t1 BEGIN
+ DELETE FROM t2 WHERE rowid = old.rowid;
+ END;
+}
+
+set SMALLEST64 -9223372036854775808
+set LARGEST64 9223372036854775807
+
+foreach {tn r1 r2} {
+ 1 0 50
+ 2 $SMALLEST64 $SMALLEST64+50
+ 3 $LARGEST64-50 $LARGEST64
+ 4 -50 -1
+} {
+ set r1 [expr $r1]
+ set r2 [expr $r2]
+
+ do_test 1.1.$tn {
+ execsql BEGIN
+ for {set ii $r1} {$ii <= $r2} {incr ii} {
+ execsql { INSERT INTO t1(rowid, doc) VALUES ($ii, document(8)); }
+ }
+ execsql COMMIT
+ } {}
+}
+do_test 1.2 {
+ db eval { SELECT rowid, doc FROM t1 } {
+ execsql { INSERT INTO t2(rowid, doc) VALUES($rowid, $doc) }
+ }
+} {}
+
+foreach {tn rowid} {
+ 1 $SMALLEST64
+ 2 0
+ 3 -5
+ 4 -30
+ 5 $LARGEST64
+ 6 $LARGEST64-1
+} {
+ set rowid [expr $rowid]
+ do_execsql_test 1.3.$tn.1 {
+ DELETE FROM t1 WHERE rowid=$rowid
+ }
+ do_compare_tables_test 1.3.$tn.2
+}
+
+set iTest 1
+foreach r [lshuffle [execsql {SELECT rowid FROM t1}]] {
+ if {($iTest % 50)==0} {
+ execsql { INSERT INTO t2(t2) VALUES('optimize') }
+ }
+ if {($iTest % 5)==0} {
+ execsql { INSERT INTO t2(t2, rank) VALUES('merge', 5) }
+ }
+ do_execsql_test 1.4.$iTest.1($r) {
+ DELETE FROM t1 WHERE rowid=$r
+ }
+ do_compare_tables_test 1.4.$iTest.2
+ incr iTest
+}
+
+do_execsql_test 1.5 {
+ SELECT * FROM t1
+} {}
+
+#-------------------------------------------------------------------------
+reset_db
+db func document document
+
+do_execsql_test 2.0 {
+ CREATE VIRTUAL TABLE t2 USING fts5(doc, content=, contentless_delete=1);
+ WITH s(i) AS (
+ SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000
+ )
+ INSERT INTO t2(rowid, doc) SELECT i, i || ' ' || i FROM s;
+}
+
+do_execsql_test 2.1 {
+ BEGIN;
+ DELETE FROM t2 WHERE rowid=32;
+ DELETE FROM t2 WHERE rowid=64;
+ DELETE FROM t2 WHERE rowid=96;
+ DELETE FROM t2 WHERE rowid=128;
+ DELETE FROM t2 WHERE rowid=160;
+ DELETE FROM t2 WHERE rowid=192;
+ COMMIT;
+}
+
+do_execsql_test 2.2 {
+ SELECT * FROM t2('128');
+} {}
+
+#-------------------------------------------------------------------------
+
+foreach {tn step} {
+ 1 3
+ 2 7
+ 3 15
+} {
+ set step [expr $step]
+
+ reset_db
+ db func document document
+ do_execsql_test 3.$tn.0 {
+ CREATE VIRTUAL TABLE t2 USING fts5(doc, content=, contentless_delete=1);
+ INSERT INTO t2(t2, rank) VALUES('pgsz', 100);
+ WITH s(i) AS (
+ SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000
+ )
+ INSERT INTO t2(rowid, doc) SELECT i, i || ' ' || i FROM s;
+ }
+ do_execsql_test 3.$tn.1 {
+ DELETE FROM t2 WHERE (rowid % $step)==0
+ }
+ do_execsql_test 3.$tn.2 {
+ SELECT * FROM t2( $step * 5 )
+ } {}
+}
+
+
+
+finish_test
+
diff --git a/ext/fts5/test/fts5contentless3.test b/ext/fts5/test/fts5contentless3.test
new file mode 100644
index 000000000..a44311e45
--- /dev/null
+++ b/ext/fts5/test/fts5contentless3.test
@@ -0,0 +1,196 @@
+# 2023 July 21
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+# This file contains tests for the content= and content_rowid= options.
+#
+
+source [file join [file dirname [info script]] fts5_common.tcl]
+set testprefix fts5contentless3
+
+# If SQLITE_ENABLE_FTS5 is defined, omit this file.
+ifcapable !fts5 {
+ finish_test
+ return
+}
+
+do_execsql_test 1.0 {
+ CREATE VIRTUAL TABLE ft USING fts5(x, content=, contentless_delete=1);
+ BEGIN;
+ INSERT INTO ft VALUES('one one one');
+ INSERT INTO ft VALUES('two two two');
+ INSERT INTO ft VALUES('three three three');
+ INSERT INTO ft VALUES('four four four');
+ INSERT INTO ft VALUES('five five five');
+ INSERT INTO ft VALUES('six six six');
+ INSERT INTO ft VALUES('seven seven seven');
+ INSERT INTO ft VALUES('eight eight eight');
+ INSERT INTO ft VALUES('nine nine nine');
+ COMMIT;
+
+ DELETE FROM ft WHERE rowid=3;
+}
+
+proc myhex {hex} { binary decode hex $hex }
+db func myhex myhex
+
+do_execsql_test 1.1 {
+ UPDATE ft_data SET block =
+ myhex('04000000 00000001' ||
+ '01020304 01020304 01020304 01020304' ||
+ '01020304 01020304 01020304 01020304'
+ )
+ WHERE id = (SELECT max(id) FROM ft_data);
+}
+
+do_execsql_test 1.2 {
+ DELETE FROM ft WHERE rowid=1
+}
+
+do_execsql_test 1.3 {
+ SELECT rowid FROM ft('two');
+} {2}
+
+do_execsql_test 1.3 {
+ UPDATE ft_data SET block =
+ myhex('08000000 00000001' ||
+ '0000000001020304 0000000001020304 0000000001020304 0000000001020304' ||
+ '0000000001020304 0000000001020304 0000000001020304 0000000001020304'
+ )
+ WHERE id = (SELECT max(id) FROM ft_data);
+}
+
+do_execsql_test 1.4 {
+ SELECT rowid FROM ft('two');
+} {2}
+
+do_execsql_test 1.5 {
+ DELETE FROM ft WHERE rowid=4
+}
+
+do_execsql_test 1.6 {
+ UPDATE ft_data SET block = myhex('04000000 00000000')
+ WHERE id = (SELECT max(id) FROM ft_data);
+}
+do_execsql_test 1.7 {
+ SELECT rowid FROM ft('two');
+} {2}
+
+do_execsql_test 1.8 {
+ UPDATE ft_data SET block = myhex('04000000 00000000')
+ WHERE id = (SELECT max(id) FROM ft_data);
+}
+do_execsql_test 1.9 {
+ DELETE FROM ft WHERE rowid=8
+} {}
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 2.0 {
+ CREATE VIRTUAL TABLE ft USING fts5(x, content=, contentless_delete=1);
+ INSERT INTO ft VALUES('one one one');
+ INSERT INTO ft VALUES('two two two');
+ INSERT INTO ft VALUES('three three three');
+ INSERT INTO ft VALUES('four four four');
+ INSERT INTO ft VALUES('five five five');
+ INSERT INTO ft VALUES('six six six');
+ INSERT INTO ft VALUES('seven seven seven');
+ INSERT INTO ft VALUES('eight eight eight');
+ INSERT INTO ft VALUES('nine nine nine');
+}
+
+do_execsql_test 2.1 {
+ INSERT INTO ft(ft) VALUES('optimize');
+}
+do_execsql_test 2.2 {
+ SELECT count(*) FROM ft_data
+} {3}
+do_execsql_test 2.3 {
+ DELETE FROM ft WHERE rowid=5
+}
+do_execsql_test 2.4 {
+ SELECT count(*) FROM ft_data
+} {4}
+
+# Check that an 'optimize' works (rewrites the index) if there is a single
+# segment with one or more tombstone hash pages.
+do_execsql_test 2.5 {
+ INSERT INTO ft(ft) VALUES('optimize');
+}
+do_execsql_test 2.6 {
+ SELECT count(*) FROM ft_data
+} {3}
+
+# Check that an 'optimize' is a no-op if there is a single segment
+# and no tombstone hash pages.
+do_execsql_test 2.7 {
+ INSERT INTO ft(ft) VALUES('optimize');
+ SELECT rowid FROM ft_data;
+} [db eval {SELECT rowid FROM ft_data}]
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 3.0 {
+ CREATE VIRTUAL TABLE ft USING fts5(x, content=, contentless_delete=1);
+ INSERT INTO ft(ft, rank) VALUES('pgsz', 64);
+ WITH s(i) AS (
+ SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000
+ )
+ INSERT INTO ft(rowid, x) SELECT i, i||' '||i||' '||i||' '||i FROM s;
+ INSERT INTO ft(ft) VALUES('optimize');
+}
+
+do_execsql_test 3.1 {
+ SELECT count(*) FROM ft_data
+} {200}
+
+do_execsql_test 3.2 {
+ DELETE FROM ft WHERE (rowid % 50)==0;
+ SELECT count(*) FROM ft_data;
+} {203}
+
+do_execsql_test 3.3 {
+ INSERT INTO ft(ft, rank) VALUES('merge', 500);
+ SELECT rowid FROM ft_data;
+} [db eval {SELECT rowid FROM ft_data}]
+
+do_execsql_test 3.4 {
+ INSERT INTO ft(ft, rank) VALUES('merge', -1000);
+ SELECT count(*) FROM ft_data;
+} {197}
+
+do_execsql_test 3.5 {
+ DELETE FROM ft WHERE (rowid % 50)==1;
+ SELECT count(*) FROM ft_data;
+} {200}
+
+do_execsql_test 3.6 {
+ SELECT level, segment, npgtombstone FROM fts5_structure(
+ (SELECT block FROM ft_data WHERE id=10)
+ )
+} {1 0 3}
+
+do_test 3.6 {
+ while 1 {
+ set nChange [db total_changes]
+ execsql { INSERT INTO ft(ft, rank) VALUES('merge', -5) }
+ if {([db total_changes] - $nChange)<2} break
+ }
+} {}
+
+do_execsql_test 3.7 {
+ SELECT level, segment, npgtombstone FROM fts5_structure(
+ (SELECT block FROM ft_data WHERE id=10)
+ )
+} {2 0 0}
+
+
+finish_test
+
diff --git a/ext/fts5/test/fts5contentless4.test b/ext/fts5/test/fts5contentless4.test
new file mode 100644
index 000000000..1c2666dcf
--- /dev/null
+++ b/ext/fts5/test/fts5contentless4.test
@@ -0,0 +1,248 @@
+# 2023 July 21
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+# This file contains tests for the content= and content_rowid= options.
+#
+
+source [file join [file dirname [info script]] fts5_common.tcl]
+set testprefix fts5contentless4
+
+# If SQLITE_ENABLE_FTS5 is defined, omit this file.
+ifcapable !fts5 {
+ finish_test
+ return
+}
+
+proc document {n} {
+ set vocab [list A B C D E F G H I J K L M N O P Q R S T U V W X Y Z]
+ set ret [list]
+ for {set ii 0} {$ii < $n} {incr ii} {
+ lappend ret [lindex $vocab [expr int(rand()*[llength $vocab])]]
+ }
+ set ret
+}
+db func document document
+
+do_execsql_test 1.0 {
+ CREATE VIRTUAL TABLE ft USING fts5(x, content='', contentless_delete=1);
+ INSERT INTO ft(ft, rank) VALUES('pgsz', 240);
+ WITH s(i) AS (
+ SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000
+ )
+ INSERT INTO ft SELECT document(12) FROM s;
+}
+
+do_execsql_test 1.1 {
+ INSERT INTO ft(ft) VALUES('optimize');
+}
+
+do_execsql_test 1.2 {
+ SELECT level, segment, nentry, nentrytombstone FROM fts5_structure((
+ SELECT block FROM ft_data WHERE id=10
+ ))
+} {0 0 1000 0}
+
+do_execsql_test 1.3 {
+ DELETE FROM ft WHERE rowid < 50
+}
+
+do_execsql_test 1.4 {
+ SELECT level, segment, nentry, nentrytombstone FROM fts5_structure((
+ SELECT block FROM ft_data WHERE id=10
+ ))
+} {0 0 1000 49}
+
+do_execsql_test 1.5 {
+ DELETE FROM ft WHERE rowid < 1000
+}
+
+do_execsql_test 1.6 {
+ SELECT level, segment, nentry, nentrytombstone FROM fts5_structure((
+ SELECT block FROM ft_data WHERE id=10
+ ))
+} {1 0 1 0}
+
+#--------------------------------------------------------------------------
+reset_db
+db func document document
+
+do_execsql_test 2.0 {
+ CREATE VIRTUAL TABLE ft USING fts5(x, content='', contentless_delete=1);
+}
+
+do_test 2.1 {
+ for {set ii 0} {$ii < 5000} {incr ii} {
+ execsql { INSERT INTO ft VALUES( document(12) ) }
+ }
+} {}
+
+do_execsql_test 2.2 {
+ SELECT sum(nentry) - sum(nentrytombstone) FROM fts5_structure((
+ SELECT block FROM ft_data WHERE id=10
+ ))
+} {5000}
+
+for {set ii 5000} {$ii >= 0} {incr ii -100} {
+ do_execsql_test 2.3.$ii {
+ DELETE FROM ft WHERE rowid > $ii
+ }
+ do_execsql_test 2.3.$ii.2 {
+ SELECT
+ CAST((total(nentry) - total(nentrytombstone)) AS integer)
+ FROM
+ fts5_structure( (SELECT block FROM ft_data WHERE id=10) )
+ } $ii
+}
+
+execsql_pp {
+ SELECT * FROM fts5_structure((
+ SELECT block FROM ft_data WHERE id=10
+ ))
+}
+
+do_test 2.4 {
+ for {set ii 0} {$ii < 5000} {incr ii} {
+ execsql { INSERT INTO ft VALUES( document(12) ) }
+ }
+} {}
+
+for {set ii 1} {$ii <= 5000} {incr ii 10} {
+ do_execsql_test 2.3.$ii {
+ DELETE FROM ft WHERE rowid = $ii;
+ INSERT INTO ft VALUES( document(12) );
+ INSERT INTO ft(ft, rank) VALUES('merge', -10);
+ }
+
+ do_execsql_test 2.3.$ii.2 {
+ SELECT
+ CAST((total(nentry) - total(nentrytombstone)) AS integer)
+ FROM
+ fts5_structure( (SELECT block FROM ft_data WHERE id=10) )
+ } 5000
+}
+
+#-------------------------------------------------------------------------
+reset_db
+db func document document
+do_execsql_test 3.0 {
+ CREATE VIRTUAL TABLE ft USING fts5(x, content='', contentless_delete=1);
+ WITH s(i) AS (
+ SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<100
+ )
+ INSERT INTO ft SELECT document(12) FROM s;
+}
+
+do_catchsql_test 3.1 {
+ INSERT INTO ft(ft, rank) VALUES('deletemerge', 'text');
+} {1 {SQL logic error}}
+do_catchsql_test 3.2 {
+ INSERT INTO ft(ft, rank) VALUES('deletemerge', 50);
+} {0 {}}
+do_execsql_test 3.3 {
+ SELECT * FROM ft_config WHERE k='deletemerge'
+} {deletemerge 50}
+do_catchsql_test 3.4 {
+ INSERT INTO ft(ft, rank) VALUES('deletemerge', 101);
+} {0 {}}
+do_execsql_test 3.5 {
+ SELECT * FROM ft_config WHERE k='deletemerge'
+} {deletemerge 101}
+
+do_execsql_test 3.6 {
+ DELETE FROM ft WHERE rowid<95
+}
+
+do_execsql_test 3.7 {
+ SELECT nentrytombstone, nentry FROM fts5_structure((
+ SELECT block FROM ft_data WHERE id=10
+ ))
+} {94 100}
+
+do_execsql_test 3.8 {
+ DELETE FROM ft WHERE rowid=95
+}
+
+do_execsql_test 3.9 {
+ SELECT nentrytombstone, nentry FROM fts5_structure((
+ SELECT block FROM ft_data WHERE id=10
+ ))
+} {95 100}
+
+do_execsql_test 3.10 {
+ DELETE FROM ft;
+ WITH s(i) AS (
+ SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<100
+ )
+ INSERT INTO ft SELECT document(12) FROM s;
+ INSERT INTO ft(ft, rank) VALUES('deletemerge', 50);
+}
+
+do_execsql_test 3.11 {
+ DELETE FROM ft WHERE rowid<95
+}
+
+do_execsql_test 3.12 {
+ SELECT nentrytombstone, nentry FROM fts5_structure((
+ SELECT block FROM ft_data WHERE id=10
+ ))
+} {0 6}
+
+#-------------------------------------------------------------------------
+reset_db
+db func document document
+do_execsql_test 4.0 {
+ CREATE VIRTUAL TABLE x1 USING fts5(x, content='', contentless_delete=1);
+ INSERT INTO x1(x1, rank) VALUES('usermerge', 16);
+ INSERT INTO x1(x1, rank) VALUES('deletemerge', 40);
+ INSERT INTO x1 VALUES('one');
+ INSERT INTO x1 VALUES('two');
+ INSERT INTO x1 VALUES('three');
+ INSERT INTO x1 VALUES('four');
+ INSERT INTO x1 VALUES('five');
+ INSERT INTO x1 VALUES('six');
+ INSERT INTO x1 VALUES('seven');
+ INSERT INTO x1 VALUES('eight');
+ INSERT INTO x1 VALUES('nine');
+ INSERT INTO x1 VALUES('ten');
+}
+
+do_execsql_test 4.1 {
+ SELECT level, segment FROM fts5_structure((
+ SELECT block FROM x1_data WHERE id=10
+ ))
+} {
+ 0 0 0 1 0 2 0 3 0 4 0 5 0 6 0 7 0 8 0 9
+}
+
+for {set ii 1} {$ii < 4} {incr ii} {
+ do_execsql_test 4.2.$ii {
+ DELETE FROM x1 WHERE rowid = $ii;
+ INSERT INTO x1(x1, rank) VALUES('merge', 5);
+ SELECT level, segment FROM fts5_structure((
+ SELECT block FROM x1_data WHERE id=10
+ ))
+ } {
+ 0 0 0 1 0 2 0 3 0 4 0 5 0 6 0 7 0 8 0 9
+ }
+}
+
+do_execsql_test 4.3 {
+ DELETE FROM x1 WHERE rowid = $ii;
+ INSERT INTO x1(x1, rank) VALUES('merge', 5);
+ SELECT level, segment, nentry FROM fts5_structure((
+ SELECT block FROM x1_data WHERE id=10
+ ))
+} {
+ 1 0 6
+}
+
+finish_test
+
diff --git a/ext/fts5/test/fts5contentless5.test b/ext/fts5/test/fts5contentless5.test
new file mode 100644
index 000000000..1541b0c68
--- /dev/null
+++ b/ext/fts5/test/fts5contentless5.test
@@ -0,0 +1,59 @@
+# 2023 August 7
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+# This file contains tests for the content= and content_rowid= options.
+#
+
+source [file join [file dirname [info script]] fts5_common.tcl]
+set testprefix fts5contentless5
+
+# If SQLITE_ENABLE_FTS5 is defined, omit this file.
+ifcapable !fts5 {
+ finish_test
+ return
+}
+
+do_execsql_test 1.0 {
+ CREATE VIRTUAL TABLE t1 USING fts5(a, b, c, content='', contentless_delete=1);
+ INSERT INTO t1 VALUES('A', 'B', 'C');
+ INSERT INTO t1 VALUES('D', 'E', 'F');
+ INSERT INTO t1 VALUES('G', 'H', 'I');
+}
+
+do_execsql_test 1.01 {
+ CREATE TABLE t2(x, y);
+ INSERT INTO t2 VALUES('x', 'y');
+}
+
+# explain_i "UPDATE t1 SET a='a' WHERE t1.rowid=1"
+breakpoint
+explain_i "UPDATE t1 SET a='a' FROM t2 WHERE t1.rowid=1 AND b IS NULL"
+
+#breakpoint
+#explain_i "UPDATE t1 SET a='a' WHERE b IS NULL AND rowid=?"
+
+foreach {tn up err} {
+ 1 "UPDATE t1 SET a='a', b='b', c='c' WHERE rowid=1" 0
+ 2 "UPDATE t1 SET a='a', b='b' WHERE rowid=1" 1
+ 3 "UPDATE t1 SET b='b', c='c' WHERE rowid=1" 1
+ 4 "UPDATE t1 SET a='a', c='c' WHERE rowid=1" 1
+ 5 "UPDATE t1 SET a='a', c='c' WHERE t1.rowid=1 AND b IS NULL" 1
+ 6 "UPDATE t1 SET a='a' FROM t2 WHERE t1.rowid=1" 1
+ 7 "UPDATE t1 SET a='a', b='b', c='c' FROM t2 WHERE t1.rowid=1" 0
+} {
+
+ set res(0) {0 {}}
+ set res(1) {1 {cannot UPDATE a subset of columns on fts5 contentless-delete table: t1}}
+ do_catchsql_test 1.$tn $up $res($err)
+}
+
+finish_test
+
diff --git a/ext/fts5/test/fts5corrupt5.test b/ext/fts5/test/fts5corrupt5.test
index 16682b132..efbe2e13e 100644
--- a/ext/fts5/test/fts5corrupt5.test
+++ b/ext/fts5/test/fts5corrupt5.test
@@ -15,7 +15,7 @@
#
source [file join [file dirname [info script]] fts5_common.tcl]
-set testprefix fts5corrupt3
+set testprefix fts5corrupt5
# If SQLITE_ENABLE_FTS5 is defined, omit this file.
ifcapable !fts5 {
@@ -793,6 +793,94 @@ do_catchsql_test 4.5 {
REPLACE INTO t1(rowid,a,b,rowid) VALUES(200,1,2,3);
} {1 {database disk image is malformed}}
+#-------------------------------------------------------------------------
+reset_db
+do_test 5.0 {
+ sqlite3 db {}
+ db deserialize [decode_hexdb {
+.open --hexdb
+| size 28672 pagesize 4096 filename crash-0c6d3451d11597.db
+| page 1 offset 0
+| 0: 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00 SQLite format 3.
+| 16: 10 00 01 01 00 40 20 20 00 00 00 00 00 00 00 07 .....@ ........
+| 32: 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 04 ................
+| 96: 00 00 00 00 0d 00 00 00 07 0d d2 00 0f c4 0f 6d ...............m
+| 112: 0f 02 0e ab 0e 4e 0d f6 0d d2 00 00 00 00 00 00 .....N..........
+| 3536: 00 00 22 07 06 17 11 11 01 31 74 61 62 6c 65 74 .........1tablet
+| 3552: 32 74 32 07 43 52 45 41 54 45 20 54 41 42 4c 45 2t2.CREATE TABLE
+| 3568: 20 74 32 28 78 29 56 06 06 17 1f 1f 01 7d 74 61 t2(x)V.......ta
+| 3584: 62 6c 65 74 31 5f 63 6f 6e 66 69 67 74 31 5f 63 blet1_configt1_c
+| 3600: 6f 6e 66 69 67 06 43 52 45 41 54 45 20 54 41 42 onfig.CREATE TAB
+| 3616: 4c 45 20 27 74 31 5f 63 6f 6e 66 69 67 27 28 6b LE 't1_config'(k
+| 3632: 20 50 52 49 4d 41 52 59 20 4b 45 59 2c 20 76 29 PRIMARY KEY, v)
+| 3648: 20 57 49 54 48 4f 55 54 20 52 4f 57 49 44 5b 05 WITHOUT ROWID[.
+| 3664: 07 17 21 21 01 81 01 74 61 62 6c 65 74 31 5f 64 ..!!...tablet1_d
+| 3680: 6f 63 73 69 7a 65 74 31 5f 64 6f 63 73 69 7a 65 ocsizet1_docsize
+| 3696: 05 43 52 45 41 54 45 20 54 41 42 4c 45 20 27 74 .CREATE TABLE 't
+| 3712: 31 5f 64 6f 63 73 69 7a 65 27 28 69 64 20 49 4e 1_docsize'(id IN
+| 3728: 54 45 47 45 52 20 50 52 49 4d 41 52 59 20 4b 45 TEGER PRIMARY KE
+| 3744: 59 2c 20 73 7a 20 42 4c 4f 42 29 55 04 06 17 21 Y, sz BLOB)U...!
+| 3760: 21 01 77 74 61 62 6c 65 74 31 5f 63 6f 6e 74 65 !.wtablet1_conte
+| 3776: 6e 74 74 31 5f 63 6f 6e 74 65 6e 74 04 43 52 45 ntt1_content.CRE
+| 3792: 41 54 45 20 54 41 42 4c 45 20 27 74 31 5f 63 6f ATE TABLE 't1_co
+| 3808: 6e 74 65 6e 74 27 28 69 64 20 49 4e 54 45 47 45 ntent'(id INTEGE
+| 3824: 52 20 50 52 49 4d 41 52 59 20 4b 45 59 2c 20 63 R PRIMARY KEY, c
+| 3840: 30 29 69 03 07 17 19 19 01 81 2d 74 61 62 6c 65 0)i.......-table
+| 3856: 74 31 5f 69 64 78 74 31 5f 69 64 78 03 43 52 45 t1_idxt1_idx.CRE
+| 3872: 41 54 45 20 54 41 42 4c 45 20 27 74 31 5f 69 64 ATE TABLE 't1_id
+| 3888: 78 27 28 73 65 67 69 64 2c 20 74 65 72 6d 2c 20 x'(segid, term,
+| 3904: 70 67 6e 6f 2c 20 50 52 49 4d 41 52 59 20 4b 45 pgno, PRIMARY KE
+| 3920: 59 28 73 65 67 69 64 2c 20 74 65 72 6d 29 29 20 Y(segid, term))
+| 3936: 57 49 54 48 4f 55 54 20 52 4f 57 49 44 55 02 07 WITHOUT ROWIDU..
+| 3952: 17 1b 1b 01 81 01 74 61 62 6c 65 74 31 5f 64 61 ......tablet1_da
+| 3968: 74 61 74 31 5f 64 61 74 61 02 43 52 45 41 54 45 tat1_data.CREATE
+| 3984: 20 54 41 42 4c 45 20 27 74 31 5f 64 61 74 61 27 TABLE 't1_data'
+| 4000: 28 69 64 20 49 4e 54 45 47 45 52 20 50 52 49 4d (id INTEGER PRIM
+| 4016: 41 52 b9 20 4b 45 59 2c 20 62 6c 6f 63 6b 20 42 AR. KEY, block B
+| 4032: 4c 4f 42 29 3a 01 06 17 11 11 08 63 74 61 62 6c LOB):......ctabl
+| 4048: 65 74 31 74 31 43 52 45 41 54 45 20 56 49 52 54 et1t1CREATE VIRT
+| 4064: 55 41 4c 20 54 41 42 4c 45 20 74 31 20 55 53 49 UAL TABLE t1 USI
+| 4080: 4e 47 20 66 74 73 35 28 63 6f 6e 74 65 6e 74 29 NG fts5(content)
+| page 2 offset 4096
+| 0: 0d 00 00 00 03 0f bd 00 0f e8 0f ef 0f bd 00 00 ................
+| 16: 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
+| 4016: 00 00 00 00 00 00 00 00 00 00 00 00 00 24 84 80 .............$..
+| 4032: 80 80 80 01 03 00 4e 00 00 00 1e 06 30 61 62 61 ......N.....0aba
+| 4048: 63 6b 01 02 02 04 02 66 74 02 02 02 04 04 6e 64 ck.....ft.....nd
+| 4064: 6f 6e 03 02 02 04 0a 07 05 01 03 00 10 03 03 0f on..............
+| 4080: 0a 03 00 24 00 00 00 00 01 01 01 00 01 01 01 11 ...$............
+| page 3 offset 8192
+| 0: 0a 00 00 00 01 0f 00 00 00 00 00 00 00 00 00 00 ................
+| 4080: 00 00 00 00 00 00 00 00 00 00 05 04 09 0c 01 02 ................
+| page 4 offset 12288
+| 0: 0d 00 00 00 03 0f e0 00 0f f6 0f ec 0f e0 00 00 ................
+| 4064: 0a 03 03 00 1b 61 62 61 6e 64 6f 6e 08 02 03 00 .....abandon....
+| 4080: 17 61 62 61 66 74 08 01 03 00 17 61 62 61 63 6b .abaft.....aback
+| page 5 offset 16384
+| 0: 0d 00 00 00 03 0f ee 00 0f fa 0f f4 0f ee 00 00 ................
+| 16: 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
+| 4064: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 03 ................
+| 4080: 03 00 0e 01 04 02 03 00 0e 01 04 01 03 00 0e 01 ................
+| page 6 offset 20480
+| 0: 0a 00 00 00 01 0f f4 00 0f f4 00 00 00 00 00 00 ................
+| 4080: 00 00 00 00 0b 03 1b 01 76 65 72 73 69 6f 6e 04 ........version.
+| page 7 offset 24576
+| 0: 0d 00 00 10 03 0f d6 00 0f f4 10 e1 0f d6 00 00 ................
+| 16: 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 ................
+| 4048: 00 00 00 00 00 00 09 03 02 1b 72 65 62 75 69 6c ..........rebuil
+| 4064: 64 11 02 02 2b 69 6e 74 65 67 72 69 74 79 2d 63 d...+integrity-c
+| 4080: 68 65 63 6b 0a 01 02 1d 6f 70 74 69 6d 00 00 00 heck....optim...
+| end crash-0c6d3451d11597.db
+}]} {}
+
+do_execsql_test 5.1 {
+ INSERT INTO t1(t1,rank) VALUES('secure-delete',1);
+}
+do_catchsql_test 5.4 {
+ UPDATE t1 SET content=randomblob(500);
+} {1 {database disk image is malformed}}
+
+
sqlite3_fts5_may_be_corrupt 0
finish_test
diff --git a/ext/fts5/test/fts5corrupt7.test b/ext/fts5/test/fts5corrupt7.test
index 75995a7c0..ae7f9da7d 100644
--- a/ext/fts5/test/fts5corrupt7.test
+++ b/ext/fts5/test/fts5corrupt7.test
@@ -96,4 +96,33 @@ set r 137438953475
db close
}
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 2.0 {
+ CREATE VIRTUAL TABLE t1 USING fts5(x);
+ BEGIN;
+ INSERT INTO t1 VALUES('abc');
+ INSERT INTO t1 VALUES('b d d d');
+ COMMIT;
+ INSERT INTO t1(t1, rank) VALUES('secure-delete', 1);
+}
+
+execsql_pp {
+ SELECT id, quote(block) FROM t1_data
+}
+
+do_execsql_test 2.1 {
+ SELECT quote(block) FROM t1_data WHERE id > 10;
+} {X'0000001A04306162630102020101620202020101640206030303040806'}
+
+do_execsql_test 2.2 {
+ UPDATE t1_data SET
+ block=X'0000001A04306162630102025501620202020101640206030303040806'
+ WHERE id>10
+}
+
+do_catchsql_test 2.3 {
+ DELETE FROM t1 WHERE rowid = 1
+} {1 {database disk image is malformed}}
+
finish_test
diff --git a/ext/fts5/test/fts5eb.test b/ext/fts5/test/fts5eb.test
index ce40e471a..0c775791f 100644
--- a/ext/fts5/test/fts5eb.test
+++ b/ext/fts5/test/fts5eb.test
@@ -95,6 +95,9 @@ do_execsql_test 3.3 {
SELECT rowid, bm25(e1) FROM e1 WHERE e1 MATCH '"/" OR "just"' ORDER BY rank;
} {1 -1e-06}
+do_execsql_test 3.4 "
+ SELECT fts5_expr_tcl('e AND \" \"');
+" {{AND [nearset -- {e}] [{}]}}
finish_test
diff --git a/ext/fts5/test/fts5faultF.test b/ext/fts5/test/fts5faultF.test
new file mode 100644
index 000000000..96cc2b083
--- /dev/null
+++ b/ext/fts5/test/fts5faultF.test
@@ -0,0 +1,111 @@
+# 2023 July 20
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#*************************************************************************
+#
+# This file is focused on OOM errors. Particularly those that may occur
+# when using contentless_delete=1 databases.
+#
+
+source [file join [file dirname [info script]] fts5_common.tcl]
+source $testdir/malloc_common.tcl
+set testprefix fts5faultF
+
+# If SQLITE_ENABLE_FTS5 is not defined, omit this file.
+ifcapable !fts5 {
+ finish_test
+ return
+}
+
+faultsim_save_and_close
+do_faultsim_test 1 -prep {
+ faultsim_restore_and_reopen
+} -body {
+ execsql {
+ CREATE VIRTUAL TABLE t1 USING fts5(x, y, content=, contentless_delete=1)
+ }
+} -test {
+ faultsim_test_result {0 {}} {1 {vtable constructor failed: t1}}
+}
+
+reset_db
+do_execsql_test 2.0 {
+ CREATE VIRTUAL TABLE t1 USING fts5(doc, content=, contentless_delete=1);
+ BEGIN;
+ INSERT INTO t1(rowid, doc) VALUES(1, 'a b c d');
+ INSERT INTO t1(rowid, doc) VALUES(2, 'a b c d');
+ INSERT INTO t1(rowid, doc) VALUES(3, 'a b c d');
+ INSERT INTO t1(rowid, doc) VALUES(4, 'a b c d');
+ COMMIT;
+ DELETE FROM t1 WHERE rowid IN (2, 4);
+}
+
+do_faultsim_test 2 -prep {
+ sqlite3 db test.db
+ execsql { SELECT rowid FROM t1 }
+} -body {
+ execsql {
+ SELECT rowid FROM t1('b c');
+ }
+} -test {
+ faultsim_test_result {0 {1 3}}
+}
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 3.0 {
+ CREATE VIRTUAL TABLE t1 USING fts5(doc, content=, contentless_delete=1);
+ BEGIN;
+ INSERT INTO t1(rowid, doc) VALUES(1, 'a b c d');
+ INSERT INTO t1(rowid, doc) VALUES(2, 'a b c d');
+ INSERT INTO t1(rowid, doc) VALUES(3, 'a b c d');
+ INSERT INTO t1(rowid, doc) VALUES(4, 'a b c d');
+ COMMIT;
+}
+
+faultsim_save_and_close
+do_faultsim_test 3 -prep {
+ faultsim_restore_and_reopen
+ execsql { SELECT rowid FROM t1 }
+} -body {
+ execsql {
+ INSERT INTO t1(rowid, doc) VALUES(5, 'a b c d');
+ }
+} -test {
+ faultsim_test_result {0 {}}
+}
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 4.0 {
+ CREATE VIRTUAL TABLE t1 USING fts5(doc, content=, contentless_delete=1);
+ INSERT INTO t1(t1, rank) VALUES('pgsz', 64);
+ WITH s(i) AS (
+ SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<1000
+ )
+ INSERT INTO t1(rowid, doc) SELECT i, 'a b c d' FROM s;
+}
+
+do_execsql_test 4.1 { DELETE FROM t1 WHERE rowid <= 25 }
+
+faultsim_save_and_close
+do_faultsim_test 4 -faults oom-t* -prep {
+ faultsim_restore_and_reopen
+ execsql { SELECT rowid FROM t1 }
+} -body {
+ execsql {
+ DELETE FROM t1 WHERE rowid < 100
+ }
+} -test {
+ faultsim_test_result {0 {}}
+}
+
+
+finish_test
+
diff --git a/ext/fts5/test/fts5secure6.test b/ext/fts5/test/fts5secure6.test
index e561a43f7..5ab17c4f3 100644
--- a/ext/fts5/test/fts5secure6.test
+++ b/ext/fts5/test/fts5secure6.test
@@ -50,6 +50,25 @@ do_test 1.3 {
expr $phc(1)*5 < $phc(2)
} {1}
+#-------------------------------------------------------------------------
+reset_db
+
+do_execsql_test 2.0 {
+ CREATE VIRTUAL TABLE t1 USING fts5(x);
+ INSERT INTO t1(t1, rank) VALUES('secure-delete', $sd)
+}
+
+do_execsql_test 2.1 {
+ BEGIN;
+ INSERT INTO t1(rowid, x) VALUES(-100000, 'abc def ghi');
+ INSERT INTO t1(rowid, x) VALUES(-99999, 'abc def ghi');
+ INSERT INTO t1(rowid, x) VALUES(9223372036854775800, 'abc def ghi');
+ COMMIT;
+}
+
+do_execsql_test 2.2 {
+ SELECT rowid FROM t1('def')
+} {-100000 -99999 9223372036854775800}
finish_test
diff --git a/ext/fts5/test/fts5synonym2.test b/ext/fts5/test/fts5synonym2.test
index 8bbfb0756..2c2705e34 100644
--- a/ext/fts5/test/fts5synonym2.test
+++ b/ext/fts5/test/fts5synonym2.test
@@ -122,6 +122,9 @@ foreach {tn expr} {
4.1 "NEAR(one two, 2)"
4.2 "NEAR(one two three, 2)"
4.3 "NEAR(eight nine, 1) OR NEAR(six seven, 1)"
+
+ 5.1 "one + two"
+ 5.2 "1 + two"
} {
if {[fts5_expr_ok $expr ss]==0} {
do_test 1.$tok.$tn.OMITTED { list } [list]
diff --git a/ext/jni/GNUmakefile b/ext/jni/GNUmakefile
new file mode 100644
index 000000000..1be4c0443
--- /dev/null
+++ b/ext/jni/GNUmakefile
@@ -0,0 +1,346 @@
+# Quick-and-dirty makefile to bootstrap the sqlite3-jni project. This
+# build assumes a Linux-like system.
+default: all
+
+JAVA_HOME ?= $(HOME)/jdk/current
+# e.g. /usr/lib/jvm/default-javajava-19-openjdk-amd64
+JDK_HOME ?= $(JAVA_HOME)
+# ^^^ JDK_HOME is not as widely used as JAVA_HOME
+bin.javac := $(JDK_HOME)/bin/javac
+bin.java := $(JDK_HOME)/bin/java
+bin.jar := $(JDK_HOME)/bin/jar
+ifeq (,$(wildcard $(JDK_HOME)))
+$(error set JDK_HOME to the top-most dir of your JDK installation.)
+endif
+MAKEFILE := $(lastword $(MAKEFILE_LIST))
+$(MAKEFILE):
+
+package.jar := sqlite3-jni.jar
+
+dir.top := ../..
+dir.tool := ../../tool
+dir.jni := $(patsubst %/,%,$(dir $(MAKEFILE)))
+
+dir.src := $(dir.jni)/src
+dir.src.c := $(dir.src)/c
+dir.bld := $(dir.jni)/bld
+dir.bld.c := $(dir.bld)
+dir.src.jni := $(dir.src)/org/sqlite/jni
+dir.src.jni.tester := $(dir.src.jni)/tester
+$(dir.bld.c):
+ mkdir -p $@
+
+classpath := $(dir.src)
+CLEAN_FILES := $(package.jar)
+DISTCLEAN_FILES := $(dir.jni)/*~ $(dir.src.c)/*~ $(dir.src.jni)/*~
+
+sqlite3-jni.h := $(dir.src.c)/sqlite3-jni.h
+.NOTPARALLEL: $(sqlite3-jni.h)
+SQLite3Jni.java := src/org/sqlite/jni/SQLite3Jni.java
+SQLTester.java := src/org/sqlite/jni/tester/SQLTester.java
+SQLite3Jni.class := $(SQLite3Jni.java:.java=.class)
+SQLTester.class := $(SQLTester.java:.java=.class)
+
+########################################################################
+# The future of FTS5 customization in this API is as yet unclear.
+# It would be a real doozy to bind to JNI.
+enable.fts5 ?= 1
+# If enable.tester is 0, the org/sqlite/jni/tester/* bits are elided.
+enable.tester ?= 1
+
+# bin.version-info = binary to output various sqlite3 version info
+# building the distribution zip file.
+bin.version-info := $(dir.top)/version-info
+.NOTPARALLEL: $(bin.version-info)
+$(bin.version-info): $(dir.tool)/version-info.c $(sqlite3.h) $(dir.top)/Makefile
+ $(MAKE) -C $(dir.top) version-info
+
+# Be explicit about which Java files to compile so that we can work on
+# in-progress files without requiring them to be in a compilable statae.
+JAVA_FILES.main := $(patsubst %,$(dir.src.jni)/%,\
+ BusyHandler.java \
+ Collation.java \
+ CollationNeeded.java \
+ CommitHook.java \
+ NativePointerHolder.java \
+ OutputPointer.java \
+ ProgressHandler.java \
+ ResultCode.java \
+ RollbackHook.java \
+ SQLFunction.java \
+ sqlite3_context.java \
+ sqlite3.java \
+ SQLite3Jni.java \
+ sqlite3_stmt.java \
+ sqlite3_value.java \
+ Tester1.java \
+ Tracer.java \
+ UpdateHook.java \
+ ValueHolder.java \
+)
+ifeq (1,$(enable.fts5))
+ JAVA_FILES.main += $(patsubst %,$(dir.src.jni)/%,\
+ fts5_api.java \
+ fts5_extension_function.java \
+ fts5_tokenizer.java \
+ Fts5.java \
+ Fts5Context.java \
+ Fts5ExtensionApi.java \
+ Fts5Function.java \
+ Fts5PhraseIter.java \
+ Fts5Tokenizer.java \
+ TesterFts5.java \
+ )
+endif
+JAVA_FILES.tester := $(dir.src.jni.tester)/SQLTester.java
+
+CLASS_FILES.main := $(JAVA_FILES.main:.java=.class)
+CLASS_FILES.tester := $(JAVA_FILES.tester:.java=.class)
+
+JAVA_FILES += $(JAVA_FILES.main)
+ifeq (1,$(enable.tester))
+ JAVA_FILES += $(JAVA_FILES.tester)
+endif
+
+CLASS_FILES :=
+define DOTCLASS_DEPS
+$(1).class: $(1).java $(MAKEFILE)
+all: $(1).class
+CLASS_FILES += $(1).class
+endef
+$(foreach B,$(basename $(JAVA_FILES)),$(eval $(call DOTCLASS_DEPS,$(B))))
+$(CLASS_FILES.tester): $(CLASS_FILES.main)
+javac.flags ?= -Xlint:unchecked -Xlint:deprecation
+java.flags ?=
+jnicheck ?= 1
+ifeq (1,$(jnicheck))
+ java.flags += -Xcheck:jni
+endif
+$(SQLite3Jni.class): $(JAVA_FILES)
+ $(bin.javac) $(javac.flags) -h $(dir.bld.c) -cp $(classpath) $(JAVA_FILES)
+all: $(SQLite3Jni.class)
+#.PHONY: classfiles
+
+########################################################################
+# Set up sqlite3.c and sqlite3.h...
+#
+# To build with SEE (https://sqlite.org/see), either put sqlite3-see.c
+# in the top of this build tree or pass
+# sqlite3.c=PATH_TO_sqlite3-see.c to the build. Note that only
+# encryption modules with no 3rd-party dependencies will currently
+# work here: AES256-OFB, AES128-OFB, and AES128-CCM. Not
+# coincidentally, those 3 modules are included in the sqlite3-see.c
+# bundle.
+#
+# A custom sqlite3.c must not have any spaces in its name.
+# $(sqlite3.canonical.c) must point to the sqlite3.c in
+# the sqlite3 canonical source tree, as that source file
+# is required for certain utility and test code.
+sqlite3.canonical.c := $(firstword $(wildcard $(dir.src.c)/sqlite3.c) $(dir.top)/sqlite3.c)
+sqlite3.canonical.h := $(firstword $(wildcard $(dir.src.c)/sqlite3.h) $(dir.top)/sqlite3.h)
+sqlite3.c := $(sqlite3.canonical.c)
+sqlite3.h := $(sqlite3.canonical.h)
+#ifeq (,$(shell grep sqlite3_activate_see $(sqlite3.c) 2>/dev/null))
+# SQLITE_C_IS_SEE := 0
+#else
+# SQLITE_C_IS_SEE := 1
+# $(info This is an SEE build.)
+#endif
+
+.NOTPARALLEL: $(sqlite3.h)
+$(sqlite3.h):
+ $(MAKE) -C $(dir.top) sqlite3.c
+$(sqlite3.c): $(sqlite3.h)
+
+SQLITE_OPT = \
+ -DSQLITE_ENABLE_RTREE \
+ -DSQLITE_ENABLE_EXPLAIN_COMMENTS \
+ -DSQLITE_ENABLE_STMTVTAB \
+ -DSQLITE_ENABLE_DBPAGE_VTAB \
+ -DSQLITE_ENABLE_DBSTAT_VTAB \
+ -DSQLITE_ENABLE_BYTECODE_VTAB \
+ -DSQLITE_ENABLE_OFFSET_SQL_FUNC \
+ -DSQLITE_OMIT_LOAD_EXTENSION \
+ -DSQLITE_OMIT_DEPRECATED \
+ -DSQLITE_OMIT_SHARED_CACHE \
+ -DSQLITE_THREADSAFE=0 \
+ -DSQLITE_TEMP_STORE=2 \
+ -DSQLITE_USE_URI=1 \
+ -DSQLITE_C=$(sqlite3.c)
+# -DSQLITE_DEBUG
+# -DSQLITE_DEBUG is just to work around a -Wall warning
+# for a var which gets set in all builds but only read
+# via assert().
+
+SQLITE_OPT += -g -DDEBUG -UNDEBUG
+
+ifeq (1,$(enable.fts5))
+ SQLITE_OPT += -DSQLITE_ENABLE_FTS5
+endif
+
+sqlite3-jni.c := $(dir.src.c)/sqlite3-jni.c
+sqlite3-jni.o := $(dir.bld.c)/sqlite3-jni.o
+sqlite3-jni.h := $(dir.src.c)/sqlite3-jni.h
+sqlite3-jni.dll := $(dir.bld.c)/libsqlite3-jni.so
+# All javac-generated .h files must be listed in $(sqlite3-jni.h.in):
+sqlite3-jni.h.in :=
+define ADD_JNI_H
+sqlite3-jni.h.in += $$(dir.bld.c)/org_sqlite_jni_$(1).h
+$$(dir.bld.c)/org_sqlite_jni_$(1).h: $$(dir.src.jni)/$(1).java
+endef
+$(eval $(call ADD_JNI_H,SQLite3Jni))
+ifeq (1,$(enable.fts5))
+ $(eval $(call ADD_JNI_H,Fts5ExtensionApi))
+ $(eval $(call ADD_JNI_H,fts5_api))
+ $(eval $(call ADD_JNI_H,fts5_tokenizer))
+endif
+ifeq (1,$(enable.tester))
+ sqlite3-jni.h.in += $(dir.bld.c)/org_sqlite_jni_tester_SQLTester.h
+ $(dir.bld.c)/org_sqlite_jni_tester_SQLTester.h: $(dir.src.jni.tester)/SQLTester.java
+endif
+#sqlite3-jni.dll.cfiles := $(dir.src.c)
+sqlite3-jni.dll.cflags = \
+ -fPIC \
+ -I. \
+ -I$(dir $(sqlite3.h)) \
+ -I$(dir.src.c) \
+ -I$(JDK_HOME)/include \
+ $(patsubst %,-I%,$(patsubst %.h,,$(wildcard $(JDK_HOME)/include/*))) \
+ -Wall
+# Using (-Wall -Wextra) triggers an untennable number of
+# gcc warnings from sqlite3.c for mundane things like
+# unused parameters.
+#
+# The gross $(patsubst...) above is to include the platform-specific
+# subdir which lives under $(JDK_HOME)/include and is a required
+# include path for client-level code.
+########################################################################
+ifeq (1,$(enable.tester))
+ sqlite3-jni.dll.cflags += -DS3JNI_ENABLE_SQLTester
+endif
+$(sqlite3-jni.h): $(sqlite3-jni.h.in) $(MAKEFILE)
+ cat $(sqlite3-jni.h.in) > $@
+$(sqlite3-jni.dll): $(sqlite3-jni.h) $(sqlite3.c) $(sqlite3.h)
+$(sqlite3-jni.dll): $(dir.bld.c) $(sqlite3-jni.c) $(SQLite3Jni.java) $(MAKEFILE)
+ $(CC) $(sqlite3-jni.dll.cflags) $(SQLITE_OPT) \
+ $(sqlite3-jni.c) -shared -o $@
+all: $(sqlite3-jni.dll)
+
+.PHONY: test
+test.flags ?= -v
+test: $(SQLite3Jni.class) $(sqlite3-jni.dll)
+ $(bin.java) -ea -Djava.library.path=$(dir.bld.c) \
+ $(java.flags) -cp $(classpath) \
+ org.sqlite.jni.Tester1 $(if $(test.flags),-- $(test.flags),)
+
+tester.scripts := $(sort $(wildcard $(dir.src)/tests/*.test))
+tester.flags ?= # --verbose
+.PHONY: tester tester-local tester-ext
+ifeq (1,$(enable.tester))
+tester-local: $(CLASS_FILES.tester) $(sqlite3-jni.dll)
+ $(bin.java) -ea -Djava.library.path=$(dir.bld.c) \
+ $(java.flags) -cp $(classpath) \
+ org.sqlite.jni.tester.SQLTester $(tester.flags) $(tester.scripts)
+tester: tester-local
+else
+tester:
+ @echo "SQLTester support is disabled. Build with enable.tester=1 to enable it."
+endif
+
+tester.extdir.default := src/tests/ext
+tester.extdir ?= $(tester.extdir.default)
+tester.extern-scripts := $(wildcard $(tester.extdir)/*.test)
+ifneq (,$(tester.extern-scripts))
+tester-ext:
+ $(bin.java) -ea -Djava.library.path=$(dir.bld.c) \
+ $(java.flags) -cp $(classpath) \
+ org.sqlite.jni.tester.SQLTester $(tester.flags) $(tester.extern-scripts)
+else
+tester-ext:
+ @echo "******************************************************"; \
+ echo "*** Include the out-of-tree test suite in the 'tester'"; \
+ echo "*** target by either symlinking its directory to"; \
+ echo "*** $(tester.extdir.default) or passing it to make"; \
+ echo "*** as tester.extdir=/path/to/that/dir."; \
+ echo "******************************************************";
+endif
+
+tester-ext: tester-local
+tester: tester-ext
+tests: test tester
+package.jar.in := $(abspath $(dir.src)/jar.in)
+CLEAN_FILES += $(package.jar.in)
+$(package.jar.in): $(MAKEFILE) $(CLASS_FILES.main)
+ cd $(dir.src); ls -1 org/sqlite/jni/*.java org/sqlite/jni/*.class > $@
+ @ls -la $@
+ @echo "To use this jar you will need the -Djava.library.path=DIR/WITH/libsqlite3-jni.so flag."
+ @echo "e.g. java -jar $@ -Djava.library.path=bld"
+
+$(package.jar): $(CLASS_FILES) $(MAKEFILE) $(package.jar.in)
+ rm -f $(dir.src)/c/*~ $(dir.src.jni)/*~
+ cd $(dir.src); $(bin.jar) -cfe ../$@ org.sqlite.jni.Tester1 @$(package.jar.in)
+
+jar: $(package.jar)
+
+CLEAN_FILES += $(dir.bld.c)/* \
+ $(dir.src.jni)/*.class \
+ $(dir.src.jni.tester)/*.class \
+ $(sqlite3-jni.dll) \
+ hs_err_pid*.log
+
+.PHONY: clean distclean
+clean:
+ -rm -f $(CLEAN_FILES)
+distclean: clean
+ -rm -f $(DISTCLEAN_FILES)
+ -rm -fr $(dir.bld.c)
+
+########################################################################
+# disttribution bundle rules...
+
+ifeq (,$(filter snapshot,$(MAKECMDGOALS)))
+dist-name-prefix := sqlite-jni
+else
+dist-name-prefix := sqlite-jni-snapshot-$(shell /usr/bin/date +%Y%m%d)
+endif
+dist-name := $(dist-name-prefix)-TEMP
+
+
+dist-dir.top := $(dist-name)
+dist-dir.src := $(dist-dir.top)/src
+dist.top.extras := \
+ README.md
+
+.PHONY: dist snapshot
+
+dist: \
+ $(bin.version-info) $(sqlite3.canonical.c) \
+ $(package.jar) $(MAKEFILE)
+ @echo "Making end-user deliverables..."
+ @rm -fr $(dist-dir.top)
+ @mkdir -p $(dist-dir.src)
+ @cp -p $(dist.top.extras) $(dist-dir.top)/.
+ @cp -p jar-dist.make $(dist-dir.top)/Makefile
+ @cp -p $(dir.src.c)/*.[ch] $(dist-dir.src)/.
+ @cp -p $(sqlite3.canonical.c) $(sqlite3.canonical.h) $(dist-dir.src)/.
+ @set -e; \
+ vnum=$$($(bin.version-info) --download-version); \
+ vjar=$$($(bin.version-info) --version); \
+ vdir=$(dist-name-prefix)-$$vnum; \
+ arczip=$$vdir.zip; \
+ cp -p $(package.jar) $(dist-dir.top)/sqlite3-jni-$${vjar}.jar; \
+ echo "Making $$arczip ..."; \
+ rm -fr $$arczip $$vdir; \
+ mv $(dist-dir.top) $$vdir; \
+ zip -qr $$arczip $$vdir; \
+ rm -fr $$vdir; \
+ ls -la $$arczip; \
+ set +e; \
+ unzip -lv $$arczip || echo "Missing unzip app? Not fatal."
+
+snapshot: dist
+
+.PHONY: dist-clean
+clean: dist-clean
+dist-clean:
+ rm -fr $(dist-name) $(wildcard sqlite-jni-*.zip)
diff --git a/ext/jni/README.md b/ext/jni/README.md
new file mode 100644
index 000000000..cb51a21cd
--- /dev/null
+++ b/ext/jni/README.md
@@ -0,0 +1,234 @@
+SQLite3 via JNI
+========================================================================
+
+This directory houses a Java Native Interface (JNI) binding for the
+sqlite3 API. If you are reading this from the distribution ZIP file,
+links to resources in the canonical source tree will note work. The
+canonical copy of this file can be browsed at:
+
+
+
+Technical support is available in the forum:
+
+
+
+
+> **FOREWARNING:** this subproject is very much in development and
+ subject to any number of changes. Please do not rely on any
+ information about its API until this disclaimer is removed.
+
+Project goals/requirements:
+
+- A [1-to-1(-ish) mapping of the C API](#1to1ish) to Java via JNI,
+ insofar as cross-language semantics allow for. A closely-related
+ goal is that [the C documentation](https://sqlite.org/c3ref/intro.html)
+ should be usable as-is, insofar as possible, for the JNI binding.
+
+- Support Java as far back as version 8 (2014).
+
+- Environment-independent. Should work everywhere both Java
+ and SQLite3 do.
+
+- No 3rd-party dependencies beyond the JDK. That includes no
+ build-level dependencies for specific IDEs and toolchains. We
+ welcome the addition of build files for arbitrary environments
+ insofar as they neither interfere with each other nor become
+ a maintenance burden for the sqlite developers.
+
+Non-goals:
+
+- Creation of high-level OO wrapper APIs. Clients are free to create
+ them off of the C-style API.
+
+
+Significant TODOs
+========================================================================
+
+- The initial beta release with version 3.43 has severe threading
+ limitations. Namely, two threads cannot call into the JNI-bound API
+ at once. This limitation will be remove in a subsequent release.
+
+
+Building
+========================================================================
+
+The canonical builds assumes a Linux-like environment and requires:
+
+- GNU Make
+- A JDK supporting Java 8 or higher
+- A modern C compiler. gcc and clang should both work.
+
+Put simply:
+
+```
+$ export JAVA_HOME=/path/to/jdk/root
+$ make
+$ make test
+$ make clean
+```
+
+
+One-to-One(-ish) Mapping to C
+========================================================================
+
+This JNI binding aims to provide as close to a 1-to-1 experience with
+the C API as cross-language semantics allow. Exceptions are
+necessarily made where cross-language semantics do not allow a 1-to-1,
+and judiciously made where a 1-to-1 mapping would be unduly cumbersome
+to use in Java.
+
+Golden Rule: _Never_ Throw from Callbacks
+------------------------------------------------------------------------
+
+JNI bindings which accept client-defined functions _must never throw
+exceptions_ unless _very explicitly documented_ as being
+throw-safe. Exceptions are generally reserved for higher-level
+bindings which are constructed to specifically deal with them and
+ensure that they do not leak C-level resources. Some of the JNI
+bindings are provided as Java functions which expect this rule to
+always hold.
+
+UTF-8(-ish)
+------------------------------------------------------------------------
+
+SQLite internally uses UTF-8 encoding, whereas Java natively uses
+UTF-16. Java JNI has routines for converting to and from UTF-8, _but_
+Java uses what its docs call "[modified UTF-8][modutf8]." Care must be
+taken when converting Java strings to UTF-8 to ensure that the proper
+conversion is performed. In short,
+`String.getBytes(StandardCharsets.UTF_8)` performs the proper
+conversion in Java, and there is no JNI C API for that conversion
+(JNI's `NewStringUTF()` returns MUTF-8).
+
+Known consequences and limitations of this discrepancy include:
+
+- Names of databases, tables, and collations must not contain
+ characters which differ in MUTF-8 and UTF-8, or certain APIs will
+ mis-translate them on their way between languages. APIs which
+ transfer other client-side data to Java take extra care to
+ convert the data at the cost of performance.
+
+[modutf8]: https://docs.oracle.com/javase/8/docs/api/java/io/DataInput.html#modified-utf-8
+
+
+Unwieldy Constructs are Re-mapped
+------------------------------------------------------------------------
+
+Some constructs, when modelled 1-to-1 from C to Java, are unduly
+clumsy to work with in Java because they try to shoehorn C's way of
+doing certain things into Java's wildly different ways. The following
+subsections cover those, starting with a verbose explanation and
+demonstration of where such changes are "really necessary"...
+
+### Custom Collations
+
+A prime example of where interface changes for Java are necessary for
+usability is [registration of a custom
+collation](https://sqlite.org/c3ref/create_collation.html):
+
+```
+// C:
+int sqlite3_create_collation(sqlite3 * db, const char * name, int eTextRep,
+ void *pUserData,
+ int (*xCompare)(void*,int,void const *,int,void const *));
+
+int sqlite3_create_collation_v2(sqlite3 * db, const char * name, int eTextRep,
+ void *pUserData,
+ int (*xCompare)(void*,int,void const *,int,void const *),
+ void (*xDestroy)(void*));
+```
+
+The `pUserData` object is optional client-defined state for the
+`xCompare()` and/or `xDestroy()` callback functions, both of which are
+passed that object as their first argument. That data is passed around
+"externally" in C because that's how C models the world. If we were to
+bind that part as-is to Java, the result would be awkward to use (^Yes,
+we tried this.):
+
+```
+// Java:
+int sqlite3_create_collation(sqlite3 db, String name, int eTextRep,
+ Object pUserData, xCompareType xCompare);
+
+int sqlite3_create_collation_v2(sqlite3 db, String name, int eTextRep,
+ Object pUserData,
+ xCompareType xCompare, xDestroyType xDestroy);
+```
+
+The awkwardness comes from (A) having two distinctly different objects
+for callbacks and (B) having their internal state provided separately,
+which is ill-fitting in Java. For the sake of usability, C APIs which
+follow that pattern use a slightly different Java interface:
+
+```
+int sqlite3_create_collation(sqlite3 db, String name, int eTextRep,
+ Collation collation);
+```
+
+Where the `Collation` class has an abstract `xCompare()` method and
+no-op `xDestroy()` method which can be overridden if needed, leading to
+a much more Java-esque usage:
+
+```
+int rc = sqlite3_create_collation(db, "mycollation", SQLITE_UTF8, new Collation(){
+
+ // Required comparison function:
+ @Override public int xCompare(byte[] lhs, byte[] rhs){ ... }
+
+ // Optional finalizer function:
+ @Override public void xDestroy(){ ... }
+
+ // Optional local state:
+ private String localState1 =
+ "This is local state. There are many like it, but this one is mine.";
+ private MyStateType localState2 = new MyStateType();
+ ...
+});
+```
+
+Noting that:
+
+- It is still possible to bind in call-scope-local state via closures,
+ if desired.
+
+- No capabilities of the C API are lost or unduly obscured via the
+ above API reshaping, so power users need not make any compromises.
+
+- In the specific example above, `sqlite3_create_collation_v2()`
+ becomes superfluous because the provided interface effectively
+ provides both the v1 and v2 interfaces, the difference being that
+ overriding the `xDestroy()` method effectively gives it v2
+ semantics.
+
+### User-defined SQL Functions (a.k.a. UDFs)
+
+The [`sqlite3_create_function()`](https://sqlite.org/c3ref/create_function.html)
+family of APIs make heavy use of function pointers to provide
+client-defined callbacks, necessitating interface changes in the JNI
+binding. The Java API has only one core function-registration function:
+
+```
+int sqlite3_create_function(sqlite3 db, String funcName, int nArgs,
+ int encoding, SQLFunction func);
+```
+
+> Design question: does the encoding argument serve any purpose in JS?
+
+`SQLFunction` is not used directly, but is instead instantiated via
+one of its three subclasses:
+
+- `SQLFunction.Scalar` implements simple scalar functions using but a
+ single callback.
+- `SQLFunction.Aggregate` implements aggregate functions using two
+ callbacks.
+- `SQLFunction.Window` implements window functions using four
+ callbacks.
+
+Search [`Tester1.java`](/file/ext/jni/src/org/sqlite/jni/Tester1.java) for
+`SQLFunction` for how it's used.
+
+Reminder: see the disclaimer at the top of this document regarding the
+in-flux nature of this API.
+
+[jsrc]: /file/
+[www]: https://sqlite.org
diff --git a/ext/jni/jar-dist.make b/ext/jni/jar-dist.make
new file mode 100644
index 000000000..23a26e4a8
--- /dev/null
+++ b/ext/jni/jar-dist.make
@@ -0,0 +1,55 @@
+#!/this/is/make
+#^^^^ help emacs out
+#
+# This is a POSIX-make-compatible makefile for building the sqlite3
+# JNI library from "dist" zip file. It must be edited to set the
+# proper top-level JDK directory and, depending on the platform, add a
+# platform-specific -I directory. It should build as-is with any
+# 2020s-era version of gcc or clang. It requires JDK version 8 or
+# higher.
+
+default: all
+
+JAVA_HOME = /usr/lib/jvm/java-1.8.0-openjdk-amd64
+CFLAGS = \
+ -fPIC \
+ -Isrc \
+ -I$(JAVA_HOME)/include \
+ -I$(JAVA_HOME)/include/linux \
+ -I$(JAVA_HOME)/include/apple \
+ -I$(JAVA_HOME)/include/bsd \
+ -Wall
+
+SQLITE_OPT = \
+ -DSQLITE_ENABLE_RTREE \
+ -DSQLITE_ENABLE_EXPLAIN_COMMENTS \
+ -DSQLITE_ENABLE_STMTVTAB \
+ -DSQLITE_ENABLE_DBPAGE_VTAB \
+ -DSQLITE_ENABLE_DBSTAT_VTAB \
+ -DSQLITE_ENABLE_BYTECODE_VTAB \
+ -DSQLITE_ENABLE_OFFSET_SQL_FUNC \
+ -DSQLITE_OMIT_LOAD_EXTENSION \
+ -DSQLITE_OMIT_DEPRECATED \
+ -DSQLITE_OMIT_SHARED_CACHE \
+ -DSQLITE_THREADSAFE=0 \
+ -DSQLITE_TEMP_STORE=2 \
+ -DSQLITE_USE_URI=1 \
+ -DSQLITE_ENABLE_FTS5
+
+sqlite3-jni.dll = libsqlite3-jni.so
+$(sqlite3-jni.dll):
+ @echo "************************************************************************"; \
+ echo "*** If this fails to build, be sure to edit this makefile ***"; \
+ echo "*** to configure it for your system. ***"; \
+ echo "************************************************************************"
+ $(CC) $(CFLAGS) $(SQLITE_OPT) \
+ src/sqlite3-jni.c -shared -o $@
+ @echo "Now try running it with: make test"
+
+test: $(sqlite3-jni.dll)
+ java -jar -Djava.library.path=. sqlite3-jni-*.jar
+
+clean:
+ -rm -f $(sqlite3-jni.dll)
+
+all: $(sqlite3-jni.dll)
diff --git a/ext/jni/src/c/sqlite3-jni.c b/ext/jni/src/c/sqlite3-jni.c
new file mode 100644
index 000000000..b28ea7114
--- /dev/null
+++ b/ext/jni/src/c/sqlite3-jni.c
@@ -0,0 +1,4420 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file implements the JNI bindings declared in
+** org.sqlite.jni.SQLiteJni (from which sqlite3-jni.h is generated).
+*/
+
+/**
+ If you found this comment by searching the code for
+ CallStaticObjectMethod then you're the victim of an OpenJDK bug:
+
+ https://bugs.openjdk.org/browse/JDK-8130659
+
+ It's known to happen with OpenJDK v8 but not with v19.
+
+ This code does not use JNI's CallStaticObjectMethod().
+*/
+
+/*
+** Define any SQLITE_... config defaults we want if they aren't
+** overridden by the builder. Please keep these alphabetized.
+*/
+
+/**********************************************************************/
+/* SQLITE_D... */
+#ifndef SQLITE_DEFAULT_CACHE_SIZE
+# define SQLITE_DEFAULT_CACHE_SIZE -16384
+#endif
+#if !defined(SQLITE_DEFAULT_PAGE_SIZE)
+# define SQLITE_DEFAULT_PAGE_SIZE 8192
+#endif
+#ifndef SQLITE_DQS
+# define SQLITE_DQS 0
+#endif
+
+/**********************************************************************/
+/* SQLITE_ENABLE_... */
+#ifndef SQLITE_ENABLE_BYTECODE_VTAB
+# define SQLITE_ENABLE_BYTECODE_VTAB 1
+#endif
+#ifndef SQLITE_ENABLE_DBPAGE_VTAB
+# define SQLITE_ENABLE_DBPAGE_VTAB 1
+#endif
+#ifndef SQLITE_ENABLE_DBSTAT_VTAB
+# define SQLITE_ENABLE_DBSTAT_VTAB 1
+#endif
+#ifndef SQLITE_ENABLE_EXPLAIN_COMMENTS
+# define SQLITE_ENABLE_EXPLAIN_COMMENTS 1
+#endif
+#ifndef SQLITE_ENABLE_MATH_FUNCTIONS
+# define SQLITE_ENABLE_MATH_FUNCTIONS 1
+#endif
+#ifndef SQLITE_ENABLE_OFFSET_SQL_FUNC
+# define SQLITE_ENABLE_OFFSET_SQL_FUNC 1
+#endif
+#ifndef SQLITE_ENABLE_PREUPDATE_HOOK
+# define SQLITE_ENABLE_PREUPDATE_HOOK 1 /*required by session extension*/
+#endif
+#ifndef SQLITE_ENABLE_RTREE
+# define SQLITE_ENABLE_RTREE 1
+#endif
+//#ifndef SQLITE_ENABLE_SESSION
+//# define SQLITE_ENABLE_SESSION 1
+//#endif
+#ifndef SQLITE_ENABLE_STMTVTAB
+# define SQLITE_ENABLE_STMTVTAB 1
+#endif
+//#ifndef SQLITE_ENABLE_UNKNOWN_SQL_FUNCTION
+//# define SQLITE_ENABLE_UNKNOWN_SQL_FUNCTION
+//#endif
+
+/**********************************************************************/
+/* SQLITE_M... */
+#ifndef SQLITE_MAX_ALLOCATION_SIZE
+# define SQLITE_MAX_ALLOCATION_SIZE 0x1fffffff
+#endif
+
+/**********************************************************************/
+/* SQLITE_O... */
+#ifndef SQLITE_OMIT_DEPRECATED
+# define SQLITE_OMIT_DEPRECATED 1
+#endif
+#ifndef SQLITE_OMIT_LOAD_EXTENSION
+# define SQLITE_OMIT_LOAD_EXTENSION 1
+#endif
+#ifndef SQLITE_OMIT_SHARED_CACHE
+# define SQLITE_OMIT_SHARED_CACHE 1
+#endif
+#ifdef SQLITE_OMIT_UTF16
+/* UTF16 is required for java */
+# undef SQLITE_OMIT_UTF16 1
+#endif
+
+/**********************************************************************/
+/* SQLITE_T... */
+#ifndef SQLITE_TEMP_STORE
+# define SQLITE_TEMP_STORE 2
+#endif
+#ifndef SQLITE_THREADSAFE
+# define SQLITE_THREADSAFE 0
+#endif
+
+/**********************************************************************/
+/* SQLITE_USE_... */
+#ifndef SQLITE_USE_URI
+# define SQLITE_USE_URI 1
+#endif
+
+
+/*
+** Which sqlite3.c we're using needs to be configurable to enable
+** building against a custom copy, e.g. the SEE variant. We have to
+** include sqlite3.c, as opposed to sqlite3.h, in order to get access
+** to SQLITE_MAX_... and friends. This increases the rebuild time
+** considerably but we need this in order to keep the exported values
+** of SQLITE_MAX_... and SQLITE_LIMIT_... in sync with the C build.
+*/
+#ifndef SQLITE_C
+# define SQLITE_C sqlite3.c
+#endif
+#define INC__STRINGIFY_(f) #f
+#define INC__STRINGIFY(f) INC__STRINGIFY_(f)
+#include INC__STRINGIFY(SQLITE_C)
+#undef INC__STRINGIFY_
+#undef INC__STRINGIFY
+#undef SQLITE_C
+
+#include "sqlite3-jni.h"
+#include /* only for testing/debugging */
+#include
+
+/* Only for debugging */
+#define MARKER(pfexp) \
+ do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__); \
+ printf pfexp; \
+ } while(0)
+
+/* Creates a verbose JNI function name. */
+#define JFuncName(Suffix) \
+ Java_org_sqlite_jni_SQLite3Jni_sqlite3_ ## Suffix
+
+/* Prologue for JNI functions. */
+#define JDECL(ReturnType,Suffix) \
+ JNIEXPORT ReturnType JNICALL \
+ JFuncName(Suffix)
+/**
+ Shortcuts for the first 2 parameters to all JNI bindings.
+
+ The type of the jSelf arg differs, but no docs seem to mention
+ this: for static methods it's of type jclass and for non-static
+ it's jobject. jobject actually works for all funcs, in the sense
+ that it compiles and runs so long as we don't use jSelf (which is
+ only rarely needed in this code), but to be pedantically correct we
+ need the proper type in the signature.
+
+ Not even the official docs mention this discrepancy:
+
+ https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#jni_interface_functions_and_pointers
+*/
+#define JENV_OSELF JNIEnv * const env, jobject jSelf
+#define JENV_CSELF JNIEnv * const env, jclass jKlazz
+/* Helpers to account for -Xcheck:jni warnings about not having
+ checked for exceptions. */
+#define IFTHREW if((*env)->ExceptionCheck(env))
+#define EXCEPTION_IGNORE (void)((*env)->ExceptionCheck(env))
+#define EXCEPTION_CLEAR (*env)->ExceptionClear(env)
+#define EXCEPTION_REPORT (*env)->ExceptionDescribe(env)
+#define EXCEPTION_WARN_CALLBACK_THREW(STR) \
+ MARKER(("WARNING: " STR " MUST NOT THROW.\n")); \
+ (*env)->ExceptionDescribe(env)
+#define IFTHREW_REPORT IFTHREW EXCEPTION_REPORT
+#define IFTHREW_CLEAR IFTHREW EXCEPTION_CLEAR
+
+/** To be used for cases where we're _really_ not expecting an
+ exception, e.g. looking up well-defined Java class members. */
+#define EXCEPTION_IS_FATAL(MSG) IFTHREW {\
+ EXCEPTION_REPORT; EXCEPTION_CLEAR; \
+ (*env)->FatalError(env, MSG); \
+ }
+
+/** Helpers for extracting pointers from jobjects, noting that the
+ corresponding Java interfaces have already done the type-checking.
+ */
+#define PtrGet_sqlite3(OBJ) NativePointerHolder_get(env,OBJ,S3JniClassNames.sqlite3)
+#define PtrGet_sqlite3_stmt(OBJ) NativePointerHolder_get(env,OBJ,S3JniClassNames.sqlite3_stmt)
+#define PtrGet_sqlite3_value(OBJ) NativePointerHolder_get(env,OBJ,S3JniClassNames.sqlite3_value)
+#define PtrGet_sqlite3_context(OBJ) NativePointerHolder_get(env,OBJ,S3JniClassNames.sqlite3_context)
+/* Helpers for Java value reference management. */
+static inline jobject new_global_ref(JNIEnv * const env, jobject const v){
+ return v ? (*env)->NewGlobalRef(env, v) : NULL;
+}
+static inline void delete_global_ref(JNIEnv * const env, jobject const v){
+ if(v) (*env)->DeleteGlobalRef(env, v);
+}
+static inline void delete_local_ref(JNIEnv * const env, jobject const v){
+ if(v) (*env)->DeleteLocalRef(env, v);
+}
+#define REF_G(VAR) new_global_ref(env, (VAR))
+#define REF_L(VAR) (*env)->NewLocalRef(env, VAR)
+#define UNREF_G(VAR) delete_global_ref(env,(VAR))
+#define UNREF_L(VAR) delete_local_ref(env,(VAR))
+
+/**
+ Constant string class names used as keys for S3JniGlobal_nph_cache(),
+S3Jni
+ and
+ friends.
+*/
+static const struct {
+ const char * const sqlite3;
+ const char * const sqlite3_stmt;
+ const char * const sqlite3_context;
+ const char * const sqlite3_value;
+ const char * const OutputPointer_Int32;
+ const char * const OutputPointer_Int64;
+ const char * const OutputPointer_String;
+ const char * const OutputPointer_ByteArray;
+ const char * const OutputPointer_sqlite3;
+ const char * const OutputPointer_sqlite3_stmt;
+#ifdef SQLITE_ENABLE_FTS5
+ const char * const Fts5Context;
+ const char * const Fts5ExtensionApi;
+ const char * const fts5_api;
+ const char * const fts5_tokenizer;
+ const char * const Fts5Tokenizer;
+#endif
+} S3JniClassNames = {
+ "org/sqlite/jni/sqlite3",
+ "org/sqlite/jni/sqlite3_stmt",
+ "org/sqlite/jni/sqlite3_context",
+ "org/sqlite/jni/sqlite3_value",
+ "org/sqlite/jni/OutputPointer$Int32",
+ "org/sqlite/jni/OutputPointer$Int64",
+ "org/sqlite/jni/OutputPointer$String",
+ "org/sqlite/jni/OutputPointer$ByteArray",
+ "org/sqlite/jni/OutputPointer$sqlite3",
+ "org/sqlite/jni/OutputPointer$sqlite3_stmt",
+#ifdef SQLITE_ENABLE_FTS5
+ "org/sqlite/jni/Fts5Context",
+ "org/sqlite/jni/Fts5ExtensionApi",
+ "org/sqlite/jni/fts5_api",
+ "org/sqlite/jni/fts5_tokenizer",
+ "org/sqlite/jni/Fts5Tokenizer"
+#endif
+};
+
+/** Create a trivial JNI wrapper for (int CName(void)). */
+#define WRAP_INT_VOID(JniNameSuffix,CName) \
+ JDECL(jint,JniNameSuffix)(JENV_CSELF){ \
+ return (jint)CName(); \
+ }
+
+/** Create a trivial JNI wrapper for (int CName(int)). */
+#define WRAP_INT_INT(JniNameSuffix,CName) \
+ JDECL(jint,JniNameSuffix)(JENV_CSELF, jint arg){ \
+ return (jint)CName((int)arg); \
+ }
+
+/** Create a trivial JNI wrapper for (const mutf8_string *
+ CName(void)). This is only valid for functions which are known to
+ return ASCII or text which is equivalent in UTF-8 and MUTF-8. */
+#define WRAP_MUTF8_VOID(JniNameSuffix,CName) \
+ JDECL(jstring,JniNameSuffix)(JENV_CSELF){ \
+ return (*env)->NewStringUTF( env, CName() ); \
+ }
+/** Create a trivial JNI wrapper for (int CName(sqlite3_stmt*)). */
+#define WRAP_INT_STMT(JniNameSuffix,CName) \
+ JDECL(jint,JniNameSuffix)(JENV_CSELF, jobject jpStmt){ \
+ jint const rc = (jint)CName(PtrGet_sqlite3_stmt(jpStmt)); \
+ EXCEPTION_IGNORE /* squelch -Xcheck:jni */; \
+ return rc; \
+ }
+/** Create a trivial JNI wrapper for (int CName(sqlite3_stmt*,int)). */
+#define WRAP_INT_STMT_INT(JniNameSuffix,CName) \
+ JDECL(jint,JniNameSuffix)(JENV_CSELF, jobject pStmt, jint n){ \
+ return (jint)CName(PtrGet_sqlite3_stmt(pStmt), (int)n); \
+ }
+/** Create a trivial JNI wrapper for (jstring CName(sqlite3_stmt*,int)). */
+#define WRAP_STR_STMT_INT(JniNameSuffix,CName) \
+ JDECL(jstring,JniNameSuffix)(JENV_CSELF, jobject pStmt, jint ndx){ \
+ return (*env)->NewStringUTF(env, CName(PtrGet_sqlite3_stmt(pStmt), (int)ndx)); \
+ }
+/** Create a trivial JNI wrapper for (int CName(sqlite3*)). */
+#define WRAP_INT_DB(JniNameSuffix,CName) \
+ JDECL(jint,JniNameSuffix)(JENV_CSELF, jobject pDb){ \
+ return (jint)CName(PtrGet_sqlite3(pDb)); \
+ }
+/** Create a trivial JNI wrapper for (int64 CName(sqlite3*)). */
+#define WRAP_INT64_DB(JniNameSuffix,CName) \
+ JDECL(jlong,JniNameSuffix)(JENV_CSELF, jobject pDb){ \
+ return (jlong)CName(PtrGet_sqlite3(pDb)); \
+ }
+/** Create a trivial JNI wrapper for (int CName(sqlite3_value*)). */
+#define WRAP_INT_SVALUE(JniNameSuffix,CName) \
+ JDECL(jint,JniNameSuffix)(JENV_CSELF, jobject jpSValue){ \
+ return (jint)CName(PtrGet_sqlite3_value(jpSValue)); \
+ }
+
+/* Helpers for jstring and jbyteArray. */
+#define JSTR_TOC(ARG) (*env)->GetStringUTFChars(env, ARG, NULL)
+#define JSTR_RELEASE(ARG,VAR) if(VAR) (*env)->ReleaseStringUTFChars(env, ARG, VAR)
+#define JBA_TOC(ARG) (*env)->GetByteArrayElements(env,ARG, NULL)
+#define JBA_RELEASE(ARG,VAR) if(VAR) (*env)->ReleaseByteArrayElements(env, ARG, VAR, JNI_ABORT)
+
+/* Marker for code which needs(?) to be made thread-safe. REASON is a
+ terse reminder about why that function requires a mutex.
+*/
+#define FIXME_THREADING(REASON)
+
+enum {
+ /**
+ Size of the NativePointerHolder cache. Need enough space for
+ (only) the library's NativePointerHolder types, a fixed count
+ known at build-time. If we add more than this a fatal error will
+ be triggered with a reminder to increase this. This value needs
+ to be exactly the number of entries in the S3JniClassNames
+ object. The S3JniClassNames entries are the keys for this particular
+ cache.
+ */
+ NphCache_SIZE = sizeof(S3JniClassNames) / sizeof(char const *)
+};
+
+/**
+ Cache entry for NativePointerHolder lookups.
+*/
+typedef struct S3JniNphCache S3JniNphCache;
+struct S3JniNphCache {
+ const char * zClassName /* "full/class/Name". Must be a static
+ string pointer from the S3JniClassNames
+ struct. */;
+ jclass klazz /* global ref to the concrete
+ NativePointerHolder subclass represented by
+ zClassName */;
+ jmethodID midCtor /* klazz's no-arg constructor. Used by
+ new_NativePointerHolder_object(). */;
+ jfieldID fidValue /* NativePointerHolder.nativePointer and
+ OutputPointer.X.value */;
+ jfieldID fidSetAgg /* sqlite3_context::aggregateContext. Used only
+ by the sqlite3_context binding. */;
+};
+
+/**
+ Cache for per-JNIEnv data.
+
+ Potential TODO: move the jclass entries to global space because,
+ per https://developer.android.com/training/articles/perf-jni:
+
+ > once you have a valid jclass global reference you can use it from
+ any attached thread.
+
+ Whereas we cache new refs for each thread.
+*/
+typedef struct S3JniEnvCache S3JniEnvCache;
+struct S3JniEnvCache {
+ JNIEnv *env /* env in which this cache entry was created */;
+ //! The various refs to global classes might be cacheable a single
+ // time globally. Information online seems inconsistent on that
+ // point.
+ struct {
+ jclass cObj /* global ref to java.lang.Object */;
+ jclass cLong /* global ref to java.lang.Long */;
+ jclass cString /* global ref to java.lang.String */;
+ jobject oCharsetUtf8 /* global ref to StandardCharset.UTF_8 */;
+ jmethodID ctorLong1 /* the Long(long) constructor */;
+ jmethodID ctorStringBA /* the String(byte[],Charset) constructor */;
+ jmethodID stringGetBytes /* the String.getBytes(Charset) method */;
+ } g /* refs to global Java state */;
+#ifdef SQLITE_ENABLE_FTS5
+ jobject jFtsExt /* Global ref to Java singleton for the
+ Fts5ExtensionApi instance. */;
+ struct {
+ jclass klazz;
+ jfieldID fidA;
+ jfieldID fidB;
+ } jPhraseIter;
+#endif
+ S3JniEnvCache * pPrev /* Previous entry in the linked list */;
+ S3JniEnvCache * pNext /* Next entry in the linked list */;
+ /** TODO?: S3JniNphCache *pNphHit;
+
+ and always set it to the most recent cache search result.
+
+ The intent would be to help fast-track cache lookups and would
+ speed up, e.g., the sqlite3_value-to-Java-array loop in a
+ multi-threaded app.
+ */
+ S3JniNphCache nph[NphCache_SIZE];
+};
+
+static void S3JniNphCache_clear(JNIEnv * const env, S3JniNphCache * const p){
+ UNREF_G(p->klazz);
+ memset(p, 0, sizeof(S3JniNphCache));
+}
+
+#define S3JNI_ENABLE_AUTOEXT 1
+#if S3JNI_ENABLE_AUTOEXT
+/*
+ Whether auto extensions are feasible here is currently unknown due
+ to...
+
+ 1) JNIEnv/threading issues. A db instance is mapped to a specific
+ JNIEnv object but auto extensions may be added from any thread. In
+ such contexts, which JNIEnv do we use for the JNI APIs?
+
+ 2) a chicken/egg problem involving the Java/C mapping of the db:
+ when auto extensions are run, the db has not yet been connected to
+ Java. If we do that during the auto-ext, sqlite3_open(_v2)() will not behave
+ properly because they have a different jobject and the API
+ guarantees the user that _that_ object is the one the API will bind
+ the native to.
+
+ If we change the open(_v2()) interfaces to use OutputPointer.sqlite3
+ instead of the client passing in an instance, we could work around
+ (2).
+*/
+typedef struct S3JniAutoExtension S3JniAutoExtension;
+typedef void (*S3JniAutoExtension_xEntryPoint)(sqlite3*);
+struct S3JniAutoExtension {
+ jobject jObj;
+ jmethodID midFunc;
+ S3JniAutoExtension_xEntryPoint xEntryPoint;
+ S3JniAutoExtension *pNext /* next linked-list entry */;
+ S3JniAutoExtension *pPrev /* previous linked-list entry */;
+};
+#endif
+
+/** State for various hook callbacks. */
+typedef struct S3JniHook S3JniHook;
+struct S3JniHook{
+ jobject jObj /* global ref to Java instance */;
+ jmethodID midCallback /* callback method. Signature depends on
+ jObj's type */;
+ jclass klazz /* global ref to jObj's class. Only needed
+ by hooks which have an xDestroy() method,
+ as lookup of that method is deferred
+ until the object requires cleanup. */;
+};
+
+/**
+ Per-(sqlite3*) state for various JNI bindings. This state is
+ allocated as needed, cleaned up in sqlite3_close(_v2)(), and
+ recycled when possible. It is freed during sqlite3_shutdown().
+*/
+typedef struct S3JniDb S3JniDb;
+struct S3JniDb {
+ JNIEnv *env /* The associated JNIEnv handle */;
+ sqlite3 *pDb /* The associated db handle */;
+ jobject jDb /* A global ref of the object which was passed to
+ sqlite3_open(_v2)(). We need this in order to have
+ an object to pass to sqlite3_collation_needed()'s
+ callback, or else we have to dynamically create one
+ for that purpose, which would be fine except that
+ it would be a different instance (and maybe even a
+ different class) than the one the user may expect
+ to receive. */;
+ char * zMainDbName /* Holds any string allocated on behave of
+ SQLITE_DBCONFIG_MAINDBNAME. */;
+ S3JniHook busyHandler;
+ S3JniHook collation;
+ S3JniHook collationNeeded;
+ S3JniHook commitHook;
+ S3JniHook progress;
+ S3JniHook rollbackHook;
+ S3JniHook trace;
+ S3JniHook updateHook;
+ S3JniHook authHook;
+#ifdef SQLITE_ENABLE_FTS5
+ jobject jFtsApi /* global ref to s3jni_fts5_api_from_db() */;
+#endif
+ S3JniDb * pNext /* Next entry in the available/free list */;
+ S3JniDb * pPrev /* Previous entry in the available/free list */;
+};
+
+/**
+ Global state, e.g. caches and metrics.
+*/
+static struct {
+ /**
+ According to: https://developer.ibm.com/articles/j-jni/
+
+ > A thread can get a JNIEnv by calling GetEnv() using the JNI
+ invocation interface through a JavaVM object. The JavaVM object
+ itself can be obtained by calling the JNI GetJavaVM() method
+ using a JNIEnv object and can be cached and shared across
+ threads. Caching a copy of the JavaVM object enables any thread
+ with access to the cached object to get access to its own
+ JNIEnv when necessary.
+ */
+ JavaVM * jvm;
+ struct {
+ S3JniEnvCache * aHead /* Linked list of in-use instances */;
+ S3JniEnvCache * aFree /* Linked list of free instances */;
+ } envCache;
+ struct {
+ S3JniDb * aUsed /* Linked list of in-use instances */;
+ S3JniDb * aFree /* Linked list of free instances */;
+ } perDb;
+ struct {
+ unsigned nphCacheHits;
+ unsigned nphCacheMisses;
+ unsigned envCacheHits;
+ unsigned envCacheMisses;
+ unsigned nDestroy /* xDestroy() calls across all types */;
+ struct {
+ /* Number of calls for each type of UDF callback. */
+ unsigned nFunc;
+ unsigned nStep;
+ unsigned nFinal;
+ unsigned nValue;
+ unsigned nInverse;
+ } udf;
+ } metrics;
+#if S3JNI_ENABLE_AUTOEXT
+ struct {
+ S3JniAutoExtension *pHead /* Head of the auto-extension list */;
+ S3JniDb * psOpening /* handle to the being-opened db. We
+ need this so that auto extensions
+ can have a consistent view of the
+ cross-language db connection and
+ behave property if they call further
+ db APIs. */;
+ int isRunning /* True while auto extensions are
+ running. This is used to prohibit
+ manipulation of the auto-extension
+ list while extensions are
+ running. */;
+ } autoExt;
+#endif
+} S3JniGlobal;
+
+#define OOM_CHECK(VAR) if(!(VAR)) s3jni_oom(env)
+static void s3jni_oom(JNIEnv * const env){
+ (*env)->FatalError(env, "Out of memory.") /* does not return */;
+}
+
+/**
+ sqlite3_malloc() proxy which fails fatally on OOM. This should
+ only be used for routines which manage global state and have no
+ recovery strategy for OOM. For sqlite3 API which can reasonably
+ return SQLITE_NOMEM, sqlite3_malloc() should be used instead.
+*/
+static void * s3jni_malloc(JNIEnv * const env, size_t n){
+ void * const rv = sqlite3_malloc(n);
+ if(n && !rv) s3jni_oom(env);
+ return rv;
+}
+
+/**
+ Fetches the S3JniGlobal.envCache row for the given env, allocing
+ a row if needed. When a row is allocated, its state is initialized
+ insofar as possible. Calls (*env)->FatalError() if allocation of
+ an entry fails. That's hypothetically possible but "shouldn't happen."
+*/
+FIXME_THREADING(S3JniEnvCache)
+static S3JniEnvCache * S3JniGlobal_env_cache(JNIEnv * const env){
+ struct S3JniEnvCache * row = S3JniGlobal.envCache.aHead;
+ for( ; row; row = row->pNext ){
+ if( row->env == env ){
+ ++S3JniGlobal.metrics.envCacheHits;
+ return row;
+ }
+ }
+ ++S3JniGlobal.metrics.envCacheMisses;
+ row = S3JniGlobal.envCache.aFree;
+ if( row ){
+ assert(!row->pPrev);
+ S3JniGlobal.envCache.aFree = row->pNext;
+ if( row->pNext ) row->pNext->pPrev = 0;
+ }else{
+ row = sqlite3_malloc(sizeof(S3JniEnvCache));
+ if( !row ){
+ (*env)->FatalError(env, "Maintenance required: S3JniEnvCache is full.")
+ /* Does not return, but cc doesn't know that */;
+ return NULL;
+ }
+ }
+ memset(row, 0, sizeof(*row));
+ row->pNext = S3JniGlobal.envCache.aHead;
+ if(row->pNext) row->pNext->pPrev = row;
+ S3JniGlobal.envCache.aHead = row;
+ row->env = env;
+
+ /* Grab references to various global classes and objects... */
+ row->g.cObj = REF_G((*env)->FindClass(env,"java/lang/Object"));
+ EXCEPTION_IS_FATAL("Error getting reference to Object class.");
+
+ row->g.cLong = REF_G((*env)->FindClass(env,"java/lang/Long"));
+ EXCEPTION_IS_FATAL("Error getting reference to Long class.");
+ row->g.ctorLong1 = (*env)->GetMethodID(env, row->g.cLong,
+ "", "(J)V");
+ EXCEPTION_IS_FATAL("Error getting reference to Long constructor.");
+
+ row->g.cString = REF_G((*env)->FindClass(env,"java/lang/String"));
+ EXCEPTION_IS_FATAL("Error getting reference to String class.");
+ row->g.ctorStringBA =
+ (*env)->GetMethodID(env, row->g.cString,
+ "", "([BLjava/nio/charset/Charset;)V");
+ EXCEPTION_IS_FATAL("Error getting reference to String(byte[],Charset) ctor.");
+ row->g.stringGetBytes =
+ (*env)->GetMethodID(env, row->g.cString,
+ "getBytes", "(Ljava/nio/charset/Charset;)[B");
+ EXCEPTION_IS_FATAL("Error getting reference to String.getBytes(Charset).");
+
+ { /* StandardCharsets.UTF_8 */
+ jfieldID fUtf8;
+ jclass const klazzSC =
+ (*env)->FindClass(env,"java/nio/charset/StandardCharsets");
+ EXCEPTION_IS_FATAL("Error getting reference to StndardCharsets class.");
+ fUtf8 = (*env)->GetStaticFieldID(env, klazzSC, "UTF_8",
+ "Ljava/nio/charset/Charset;");
+ EXCEPTION_IS_FATAL("Error getting StndardCharsets.UTF_8 field.");
+ row->g.oCharsetUtf8 =
+ REF_G((*env)->GetStaticObjectField(env, klazzSC, fUtf8));
+ EXCEPTION_IS_FATAL("Error getting reference to StandardCharsets.UTF_8.");
+ }
+ return row;
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own Java/JNI bindings.
+**
+** For purposes of certain hand-crafted JNI function bindings, we
+** need a way of reporting errors which is consistent with the rest of
+** the C API, as opposed to throwing JS exceptions. To that end, this
+** internal-use-only function is a thin proxy around
+** sqlite3ErrorWithMessage(). The intent is that it only be used from
+** JNI bindings such as sqlite3_prepare_v2/v3(), and definitely not
+** from client code.
+**
+** Returns err_code.
+*/
+static int s3jni_db_error(sqlite3* const db, int err_code, const char * const zMsg){
+ if( db!=0 ){
+ if( 0==zMsg ){
+ sqlite3Error(db, err_code);
+ }else{
+ const int nMsg = sqlite3Strlen30(zMsg);
+ sqlite3ErrorWithMsg(db, err_code, "%.*s", nMsg, zMsg);
+ }
+ }
+ return err_code;
+}
+
+/**
+ Creates a new jByteArray of length nP, copies p's contents into it, and
+ returns that byte array.
+ */
+static jbyteArray s3jni_new_jbyteArray(JNIEnv * const env, const unsigned char * const p, int nP){
+ jbyteArray jba = (*env)->NewByteArray(env, (jint)nP);
+ if(jba){
+ (*env)->SetByteArrayRegion(env, jba, 0, (jint)nP, (const jbyte*)p);
+ }
+ return jba;
+}
+
+/**
+ Uses the java.lang.String(byte[],Charset) constructor to create a
+ new String from UTF-8 string z. n is the number of bytes to
+ copy. If n<0 then sqlite3Strlen30() is used to calculate it.
+
+ Returns NULL if z is NULL or on OOM, else returns a new jstring
+ owned by the caller.
+
+ Sidebar: this is a painfully inefficient way to convert from
+ standard UTF-8 to a Java string, but JNI offers only algorithms for
+ working with MUTF-8, not UTF-8.
+*/
+static jstring s3jni_utf8_to_jstring(S3JniEnvCache * const jc,
+ const char * const z, int n){
+ jstring rv = NULL;
+ JNIEnv * const env = jc->env;
+ if( 0==n || (n<0 && z && !z[0]) ){
+ /* Fast-track the empty-string case via the MUTF-8 API. We could
+ hypothetically do this for any strings where n<4 and z is
+ NUL-terminated and none of z[0..3] are NUL bytes. */
+ rv = (*env)->NewStringUTF(env, "");
+ }else if( z ){
+ jbyteArray jba;
+ if( n<0 ) n = sqlite3Strlen30(z);
+ jba = s3jni_new_jbyteArray(env, (unsigned const char *)z, (jsize)n);
+ if( jba ){
+ rv = (*env)->NewObject(env, jc->g.cString, jc->g.ctorStringBA,
+ jba, jc->g.oCharsetUtf8);
+ UNREF_L(jba);
+ }
+ }
+ return rv;
+}
+
+/**
+ Converts the given java.lang.String object into a NUL-terminated
+ UTF-8 C-string by calling jstr.getBytes(StandardCharset.UTF_8).
+ Returns NULL if jstr is NULL or on allocation error. If jstr is not
+ NULL and nLen is not NULL then nLen is set to the length of the
+ returned string, not including the terminating NUL. If jstr is not
+ NULL and it returns NULL, this indicates an allocation error. In
+ that case, if nLen is not NULL then it is either set to 0 (if
+ fetching of jstr's bytes fails to allocate) or set to what would
+ have been the length of the string had C-string allocation
+ succeeded.
+
+ The returned memory is allocated from sqlite3_malloc() and
+ ownership is transferred to the caller.
+*/
+static char * s3jni_jstring_to_utf8(S3JniEnvCache * const jc,
+ jstring jstr, int *nLen){
+ JNIEnv * const env = jc->env;
+ jbyteArray jba;
+ jsize nBa;
+ char *rv;
+
+ if(!jstr) return 0;
+ jba = (*env)->CallObjectMethod(env, jstr, jc->g.stringGetBytes,
+ jc->g.oCharsetUtf8);
+ if( (*env)->ExceptionCheck(env) || !jba
+ /* order of these checks is significant for -Xlint:jni */ ) {
+ EXCEPTION_REPORT;
+ if( nLen ) *nLen = 0;
+ return 0;
+ }
+ nBa = (*env)->GetArrayLength(env, jba);
+ if( nLen ) *nLen = (int)nBa;
+ rv = sqlite3_malloc( nBa + 1 );
+ if( rv ){
+ (*env)->GetByteArrayRegion(env, jba, 0, nBa, (jbyte*)rv);
+ rv[nBa] = 0;
+ }
+ UNREF_L(jba);
+ return rv;
+}
+
+/**
+ Expects to be passed a pointer from sqlite3_column_text16() or
+ sqlite3_value_text16() and a byte-length value from
+ sqlite3_column_bytes16() or sqlite3_value_bytes16(). It creates a
+ Java String of exactly half that character length, returning NULL
+ if !p or (*env)->NewString() fails.
+*/
+static jstring s3jni_text16_to_jstring(JNIEnv * const env, const void * const p, int nP){
+ return p
+ ? (*env)->NewString(env, (const jchar *)p, (jsize)(nP/2))
+ : NULL;
+}
+
+/**
+ Requires jx to be a Throwable. Calls its toString() method and
+ returns its value converted to a UTF-8 string. The caller owns the
+ returned string and must eventually sqlite3_free() it. Returns 0
+ if there is a problem fetching the info or on OOM.
+
+ Design note: we use toString() instead of getMessage() because the
+ former includes the exception type's name:
+
+ Exception e = new RuntimeException("Hi");
+ System.out.println(e.toString()); // java.lang.RuntimeException: Hi
+ System.out.println(e.getMessage()); // Hi
+ }
+*/
+FIXME_THREADING(S3JniEnvCache)
+static char * s3jni_exception_error_msg(JNIEnv * const env, jthrowable jx ){
+ S3JniEnvCache * const jc = S3JniGlobal_env_cache(env);
+ jmethodID mid;
+ jstring msg;
+ char * zMsg;
+ jclass const klazz = (*env)->GetObjectClass(env, jx);
+ mid = (*env)->GetMethodID(env, klazz, "toString", "()Ljava/lang/String;");
+ IFTHREW{
+ EXCEPTION_REPORT;
+ EXCEPTION_CLEAR;
+ return 0;
+ }
+ msg = (*env)->CallObjectMethod(env, jx, mid);
+ IFTHREW{
+ EXCEPTION_REPORT;
+ EXCEPTION_CLEAR;
+ return 0;
+ }
+ zMsg = s3jni_jstring_to_utf8(jc, msg, 0);
+ UNREF_L(msg);
+ return zMsg;
+}
+
+/**
+ Extracts the current JNI exception, sets ps->pDb's error message to
+ its message string, and clears the exception. If errCode is non-0,
+ it is used as-is, else SQLITE_ERROR is assumed. If there's a
+ problem extracting the exception's message, it's treated as
+ non-fatal and zDfltMsg is used in its place.
+
+ This must only be called if a JNI exception is pending.
+
+ Returns errCode unless it is 0, in which case SQLITE_ERROR is
+ returned.
+*/
+static int s3jni_db_exception(JNIEnv * const env, S3JniDb * const ps,
+ int errCode, const char *zDfltMsg){
+ jthrowable const ex = (*env)->ExceptionOccurred(env);
+
+ if( 0==errCode ) errCode = SQLITE_ERROR;
+ if( ex ){
+ char * zMsg;
+ EXCEPTION_CLEAR;
+ zMsg = s3jni_exception_error_msg(env, ex);
+ s3jni_db_error(ps->pDb, errCode, zMsg ? zMsg : zDfltMsg);
+ sqlite3_free(zMsg);
+ UNREF_L(ex);
+ }
+ return errCode;
+}
+
+/**
+ Extracts the (void xDestroy()) method from the given jclass and
+ applies it to jobj. If jObj is NULL, this is a no-op. If klazz is
+ NULL then it's derived from jobj. The lack of an xDestroy() method
+ is silently ignored and any exceptions thrown by the method trigger
+ a warning to stdout or stderr and then the exception is suppressed.
+*/
+static void s3jni_call_xDestroy(JNIEnv * const env, jobject jObj, jclass klazz){
+ if(jObj){
+ jmethodID method;
+ if(!klazz){
+ klazz = (*env)->GetObjectClass(env, jObj);
+ assert(klazz);
+ }
+ method = (*env)->GetMethodID(env, klazz, "xDestroy", "()V");
+ //MARKER(("jObj=%p, klazz=%p, method=%p\n", jObj, klazz, method));
+ if(method){
+ ++S3JniGlobal.metrics.nDestroy;
+ (*env)->CallVoidMethod(env, jObj, method);
+ IFTHREW{
+ EXCEPTION_WARN_CALLBACK_THREW("xDestroy() callback");
+ EXCEPTION_CLEAR;
+ }
+ }else{
+ EXCEPTION_CLEAR;
+ }
+ }
+}
+
+/**
+ Removes any Java references from s and clears its state. If
+ doXDestroy is true and s->klazz and s->jObj are not NULL, s->jObj's
+ s is passed to s3jni_call_xDestroy() before any references are
+ cleared. It is legal to call this when the object has no Java
+ references.
+*/
+static void S3JniHook_unref(JNIEnv * const env, S3JniHook * const s, int doXDestroy){
+ if(doXDestroy && s->klazz && s->jObj){
+ s3jni_call_xDestroy(env, s->jObj, s->klazz);
+ }
+ UNREF_G(s->jObj);
+ UNREF_G(s->klazz);
+ memset(s, 0, sizeof(*s));
+}
+
+/**
+ Clears s's state and moves it to the free-list.
+*/
+FIXME_THREADING(perDb)
+static void S3JniDb_set_aside(S3JniDb * const s){
+ if(s){
+ JNIEnv * const env = s->env;
+ assert(s->pDb && "Else this object is already in the free-list.");
+ //MARKER(("state@%p for db@%p setting aside\n", s, s->pDb));
+ assert(s->pPrev != s);
+ assert(s->pNext != s);
+ assert(s->pPrev ? (s->pPrev!=s->pNext) : 1);
+ if(s->pNext) s->pNext->pPrev = s->pPrev;
+ if(s->pPrev) s->pPrev->pNext = s->pNext;
+ else if(S3JniGlobal.perDb.aUsed == s){
+ assert(!s->pPrev);
+ S3JniGlobal.perDb.aUsed = s->pNext;
+ }
+ sqlite3_free( s->zMainDbName );
+#define UNHOOK(MEMBER,XDESTROY) S3JniHook_unref(env, &s->MEMBER, XDESTROY)
+ UNHOOK(trace, 0);
+ UNHOOK(progress, 0);
+ UNHOOK(commitHook, 0);
+ UNHOOK(rollbackHook, 0);
+ UNHOOK(updateHook, 0);
+ UNHOOK(authHook, 0);
+ UNHOOK(collation, 1);
+ UNHOOK(collationNeeded, 1);
+ UNHOOK(busyHandler, 1);
+#undef UNHOOK
+ UNREF_G(s->jDb);
+#ifdef SQLITE_ENABLE_FTS5
+ UNREF_G(s->jFtsApi);
+#endif
+ memset(s, 0, sizeof(S3JniDb));
+ s->pNext = S3JniGlobal.perDb.aFree;
+ if(s->pNext) s->pNext->pPrev = s;
+ S3JniGlobal.perDb.aFree = s;
+ //MARKER(("%p->pPrev@%p, pNext@%p\n", s, s->pPrev, s->pNext));
+ //if(s->pNext) MARKER(("next: %p->pPrev@%p\n", s->pNext, s->pNext->pPrev));
+ }
+}
+
+/**
+ Requires that p has been snipped from any linked list it is
+ in. Clears all Java refs p holds and zeroes out p.
+*/
+static void S3JniEnvCache_clear(S3JniEnvCache * const p){
+ JNIEnv * const env = p->env;
+ if(env){
+ int i;
+ UNREF_G(p->g.cObj);
+ UNREF_G(p->g.cLong);
+ UNREF_G(p->g.cString);
+ UNREF_G(p->g.oCharsetUtf8);
+#ifdef SQLITE_ENABLE_FTS5
+ UNREF_G(p->jFtsExt);
+ UNREF_G(p->jPhraseIter.klazz);
+#endif
+ for( i = 0; i < NphCache_SIZE; ++i ){
+ S3JniNphCache_clear(env, &p->nph[i]);
+ }
+ memset(p, 0, sizeof(S3JniEnvCache));
+ }
+}
+
+/**
+ Cleans up all state in S3JniGlobal.perDb for th given JNIEnv.
+ Results are undefined if a Java-side db uses the API
+ from the given JNIEnv after this call.
+*/
+FIXME_THREADING(perDb)
+static void S3JniDb_free_for_env(JNIEnv *env){
+ S3JniDb * ps = S3JniGlobal.perDb.aUsed;
+ S3JniDb * pNext = 0;
+ for( ; ps; ps = pNext ){
+ pNext = ps->pNext;
+ if(ps->env == env){
+ S3JniDb * const pPrev = ps->pPrev;
+ S3JniDb_set_aside(ps);
+ assert(pPrev ? pPrev->pNext!=ps : 1);
+ pNext = pPrev;
+ }
+ }
+}
+
+/**
+ Uncache any state for the given JNIEnv, clearing all Java
+ references the cache owns. Returns true if env was cached and false
+ if it was not found in the cache.
+
+ Also passes env to S3JniDb_free_for_env() to free up
+ what would otherwise be stale references.
+*/
+static int S3JniGlobal_env_uncache(JNIEnv * const env){
+ struct S3JniEnvCache * row = S3JniGlobal.envCache.aHead;
+ for( ; row; row = row->pNext ){
+ if( row->env == env ){
+ break;
+ }
+ }
+ if( !row ) return 0;
+ if( row->pNext ) row->pNext->pPrev = row->pPrev;
+ if( row->pPrev ) row->pPrev->pNext = row->pNext;
+ if( S3JniGlobal.envCache.aHead == row ){
+ assert( !row->pPrev );
+ S3JniGlobal.envCache.aHead = row->pNext;
+ }
+ S3JniEnvCache_clear(row);
+ assert( !row->pNext );
+ assert( !row->pPrev );
+ row->pNext = S3JniGlobal.envCache.aFree;
+ if( row->pNext ) row->pNext->pPrev = row;
+ S3JniGlobal.envCache.aFree = row;
+ S3JniDb_free_for_env(env);
+ return 1;
+}
+
+static void S3JniGlobal_S3JniEnvCache_clear(void){
+ while( S3JniGlobal.envCache.aHead ){
+ S3JniGlobal_env_uncache( S3JniGlobal.envCache.aHead->env );
+ }
+}
+
+/**
+ Searches the NativePointerHolder cache for the given combination.
+ If it finds one, it returns it as-is. If it doesn't AND the cache
+ has a free slot, it populates that slot with (env, zClassName,
+ klazz) and returns it. If the cache is full with no match it
+ returns NULL.
+
+ It is up to the caller to populate the other members of the returned
+ object if needed.
+
+ zClassName must be a static string so we can use its address as a
+ cache key.
+
+ This simple cache catches >99% of searches in the current
+ (2023-07-31) tests.
+*/
+FIXME_THREADING(S3JniEnvCache)
+static S3JniNphCache * S3JniGlobal_nph_cache(JNIEnv * const env, const char *zClassName){
+ /**
+ According to:
+
+ https://developer.ibm.com/articles/j-jni/
+
+ > ... the IDs returned for a given class don't change for the
+ lifetime of the JVM process. But the call to get the field or
+ method can require significant work in the JVM, because
+ fields and methods might have been inherited from
+ superclasses, making the JVM walk up the class hierarchy to
+ find them. Because the IDs are the same for a given class,
+ you should look them up once and then reuse them. Similarly,
+ looking up class objects can be expensive, so they should be
+ cached as well.
+ */
+ struct S3JniEnvCache * const envRow = S3JniGlobal_env_cache(env);
+ S3JniNphCache * freeSlot = 0;
+ S3JniNphCache * pCache = 0;
+ int i;
+ assert(envRow);
+ for( i = 0; i < NphCache_SIZE; ++i ){
+ pCache = &envRow->nph[i];
+ if(zClassName == pCache->zClassName){
+ ++S3JniGlobal.metrics.nphCacheHits;
+#define DUMP_NPH_CACHES 0
+#if DUMP_NPH_CACHES
+ MARKER(("Cache hit #%u %s klazz@%p nativePointer field@%p, ctor@%p\n",
+ S3JniGlobal.metrics.nphCacheHits, zClassName, pCache->klazz, pCache->fidValue,
+ pCache->midCtor));
+#endif
+ assert(pCache->klazz);
+ return pCache;
+ }else if(!freeSlot && !pCache->zClassName){
+ freeSlot = pCache;
+ }
+ }
+ if(freeSlot){
+ freeSlot->zClassName = zClassName;
+ freeSlot->klazz = (*env)->FindClass(env, zClassName);
+ EXCEPTION_IS_FATAL("FindClass() unexpectedly threw");
+ freeSlot->klazz = REF_G(freeSlot->klazz);
+ ++S3JniGlobal.metrics.nphCacheMisses;
+#if DUMP_NPH_CACHES
+ static unsigned int cacheMisses = 0;
+ MARKER(("Cache miss #%u %s klazz@%p nativePointer field@%p, ctor@%p\n",
+ S3JniGlobal.metrics.nphCacheMisses, zClassName, freeSlot->klazz,
+ freeSlot->fidValue, freeSlot->midCtor));
+#endif
+#undef DUMP_NPH_CACHES
+ }else{
+ (*env)->FatalError(env, "MAINTENANCE REQUIRED: NphCache_SIZE is too low.");
+ }
+ return freeSlot;
+}
+
+/**
+ Returns the ID of the "nativePointer" field from the given
+ NativePointerHolder class.
+ */
+static jfieldID NativePointerHolder_getField(JNIEnv * const env, jclass klazz){
+ jfieldID rv = (*env)->GetFieldID(env, klazz, "nativePointer", "J");
+ EXCEPTION_IS_FATAL("Code maintenance required: missing nativePointer field.");
+ return rv;
+}
+
+/**
+ Sets a native ptr value in NativePointerHolder object ppOut.
+ zClassName must be a static string so we can use its address
+ as a cache key.
+*/
+static void NativePointerHolder_set(JNIEnv * env, jobject ppOut, const void * p,
+ const char *zClassName){
+ jfieldID setter = 0;
+ S3JniNphCache * const pCache = S3JniGlobal_nph_cache(env, zClassName);
+ if(pCache && pCache->klazz && pCache->fidValue){
+ assert(zClassName == pCache->zClassName);
+ setter = pCache->fidValue;
+ assert(setter);
+ }else{
+ jclass const klazz =
+ pCache ? pCache->klazz : (*env)->GetObjectClass(env, ppOut);
+ setter = NativePointerHolder_getField(env, klazz);
+ if(pCache){
+ assert(pCache->klazz);
+ assert(!pCache->fidValue);
+ assert(zClassName == pCache->zClassName);
+ pCache->fidValue = setter;
+ }
+ }
+ (*env)->SetLongField(env, ppOut, setter, (jlong)p);
+ EXCEPTION_IS_FATAL("Could not set NativePointerHolder.nativePointer.");
+}
+
+/**
+ Fetches a native ptr value from NativePointerHolder object ppOut.
+ zClassName must be a static string so we can use its address as a
+ cache key.
+*/
+static void * NativePointerHolder_get(JNIEnv * env, jobject pObj, const char *zClassName){
+ if( pObj ){
+ jfieldID getter = 0;
+ void * rv = 0;
+ S3JniNphCache * const pCache = S3JniGlobal_nph_cache(env, zClassName);
+ if(pCache && pCache->fidValue){
+ getter = pCache->fidValue;
+ }else{
+ jclass const klazz =
+ pCache ? pCache->klazz : (*env)->GetObjectClass(env, pObj);
+ getter = NativePointerHolder_getField(env, klazz);
+ if(pCache){
+ assert(pCache->klazz);
+ assert(zClassName == pCache->zClassName);
+ pCache->fidValue = getter;
+ }
+ }
+ rv = (void*)(*env)->GetLongField(env, pObj, getter);
+ IFTHREW_REPORT;
+ return rv;
+ }else{
+ return 0;
+ }
+}
+
+/**
+ Extracts the new S3JniDb instance from the free-list, or
+ allocates one if needed, associats it with pDb, and returns.
+ Returns NULL on OOM. pDb MUST be associated with jDb via
+ NativePointerHolder_set().
+*/
+static S3JniDb * S3JniDb_alloc(JNIEnv * const env, sqlite3 *pDb,
+ jobject jDb){
+ S3JniDb * rv;
+ if(S3JniGlobal.perDb.aFree){
+ rv = S3JniGlobal.perDb.aFree;
+ //MARKER(("state@%p for db allocating for db@%p from free-list\n", rv, pDb));
+ //MARKER(("%p->pPrev@%p, pNext@%p\n", rv, rv->pPrev, rv->pNext));
+ S3JniGlobal.perDb.aFree = rv->pNext;
+ assert(rv->pNext != rv);
+ assert(rv->pPrev != rv);
+ assert(rv->pPrev ? (rv->pPrev!=rv->pNext) : 1);
+ if(rv->pNext){
+ assert(rv->pNext->pPrev == rv);
+ assert(rv->pPrev ? (rv->pNext == rv->pPrev->pNext) : 1);
+ rv->pNext->pPrev = 0;
+ rv->pNext = 0;
+ }
+ }else{
+ rv = s3jni_malloc(env, sizeof(S3JniDb));
+ //MARKER(("state@%p for db allocating for db@%p from heap\n", rv, pDb));
+ if(rv){
+ memset(rv, 0, sizeof(S3JniDb));
+ }
+ }
+ if(rv){
+ rv->pNext = S3JniGlobal.perDb.aUsed;
+ S3JniGlobal.perDb.aUsed = rv;
+ if(rv->pNext){
+ assert(!rv->pNext->pPrev);
+ rv->pNext->pPrev = rv;
+ }
+ rv->jDb = REF_G(jDb);
+ rv->pDb = pDb;
+ rv->env = env;
+ }
+ return rv;
+}
+
+#if 0
+static void S3JniDb_dump(S3JniDb *s){
+ MARKER(("S3JniDb->env @ %p\n", s->env));
+ MARKER(("S3JniDb->pDb @ %p\n", s->pDb));
+ MARKER(("S3JniDb->trace.jObj @ %p\n", s->trace.jObj));
+ MARKER(("S3JniDb->progress.jObj @ %p\n", s->progress.jObj));
+ MARKER(("S3JniDb->commitHook.jObj @ %p\n", s->commitHook.jObj));
+ MARKER(("S3JniDb->rollbackHook.jObj @ %p\n", s->rollbackHook.jObj));
+ MARKER(("S3JniDb->busyHandler.jObj @ %p\n", s->busyHandler.jObj));
+ MARKER(("S3JniDb->env @ %p\n", s->env));
+}
+#endif
+
+/**
+ Returns the S3JniDb object for the given db. If allocIfNeeded is
+ true then a new instance will be allocated if no mapping currently
+ exists, else NULL is returned if no mapping is found.
+
+ The 3rd and 4th args should normally only be non-0 for
+ sqlite3_open(_v2)(). For most other cases, they must be 0, in which
+ case the db handle will be fished out of the jDb object and NULL is
+ returned if jDb does not have any associated S3JniDb.
+
+ If called with a NULL jDb and non-NULL pDb then allocIfNeeded MUST
+ be false and it will look for a matching db object. That usage is
+ required for functions, like sqlite3_context_db_handle(), which
+ return a (sqlite3*) but do not take one as an argument.
+*/
+FIXME_THREADING(S3JniEnvCache)
+FIXME_THREADING(perDb)
+static S3JniDb * S3JniDb_for_db(JNIEnv * const env, jobject jDb,
+ sqlite3 *pDb, int allocIfNeeded){
+ S3JniDb * s = S3JniGlobal.perDb.aUsed;
+ if(!jDb){
+ if(pDb){
+ assert(!allocIfNeeded);
+ }else{
+ return 0;
+ }
+ }
+ assert(allocIfNeeded ? !!pDb : 1);
+ if(!allocIfNeeded && !pDb){
+ pDb = PtrGet_sqlite3(jDb);
+ }
+ for( ; pDb && s; s = s->pNext){
+ if(s->pDb == pDb) return s;
+ }
+ if(allocIfNeeded){
+ s = S3JniDb_alloc(env, pDb, jDb);
+ }
+ return s;
+}
+
+#if 0
+/**
+ An alternative form which searches for the S3JniDb instance for
+ pDb with no JNIEnv-specific info. This can be (but _should_ it be?)
+ called from the context of a separate JNIEnv than the one mapped
+ to in the returned object. Returns 0 if no match is found.
+*/
+FIXME_THREADING(perDb)
+static S3JniDb * S3JniDb_for_db2(sqlite3 *pDb){
+ S3JniDb * s = S3JniGlobal.perDb.aUsed;
+ for( ; pDb && s; s = s->pNext){
+ if(s->pDb == pDb) return s;
+ }
+ return 0;
+}
+#endif
+
+#if S3JNI_ENABLE_AUTOEXT
+/**
+ Unlink ax from S3JniGlobal.autoExt and free it.
+*/
+static void S3JniAutoExtension_free(JNIEnv * const env,
+ S3JniAutoExtension * const ax){
+ if( ax ){
+ if( ax->pNext ) ax->pNext->pPrev = ax->pPrev;
+ if( ax == S3JniGlobal.autoExt.pHead ){
+ assert( !ax->pNext );
+ S3JniGlobal.autoExt.pHead = ax->pNext;
+ }else if( ax->pPrev ){
+ ax->pPrev->pNext = ax->pNext;
+ }
+ ax->pNext = ax->pPrev = 0;
+ UNREF_G(ax->jObj);
+ sqlite3_free(ax);
+ }
+}
+
+/**
+ Allocates a new auto extension and plugs it in to S3JniGlobal.autoExt.
+ Returns 0 on OOM or if there is an error collecting the required
+ state from jAutoExt (which must be an AutoExtension object).
+*/
+static S3JniAutoExtension * S3JniAutoExtension_alloc(JNIEnv *const env,
+ jobject const jAutoExt){
+ S3JniAutoExtension * const ax = sqlite3_malloc(sizeof(*ax));
+ if( ax ){
+ jclass klazz;
+ memset(ax, 0, sizeof(*ax));
+ klazz = (*env)->GetObjectClass(env, jAutoExt);
+ if(!klazz){
+ S3JniAutoExtension_free(env, ax);
+ return 0;
+ }
+ ax->midFunc = (*env)->GetMethodID(env, klazz, "xEntryPoint",
+ "(Lorg/sqlite/jni/sqlite3;)I");
+ if(!ax->midFunc){
+ MARKER(("Error getting xEntryPoint(sqlite3) from object."));
+ S3JniAutoExtension_free(env, ax);
+ return 0;
+ }
+ ax->jObj = REF_G(jAutoExt);
+ ax->pNext = S3JniGlobal.autoExt.pHead;
+ if( ax->pNext ) ax->pNext->pPrev = ax;
+ S3JniGlobal.autoExt.pHead = ax;
+ }
+ return ax;
+}
+#endif /* S3JNI_ENABLE_AUTOEXT */
+
+/**
+ Requires that jCx be a Java-side sqlite3_context wrapper for pCx.
+ This function calls sqlite3_aggregate_context() to allocate a tiny
+ sliver of memory, the address of which is set in
+ jCx->aggregateContext. The memory is only used as a key for
+ mapping client-side results of aggregate result sets across
+ calls to the UDF's callbacks.
+
+ isFinal must be 1 for xFinal() calls and 0 for all others, the
+ difference being that the xFinal() invocation will not allocate
+ new memory if it was not already, resulting in a value of 0
+ for jCx->aggregateContext.
+
+ Returns 0 on success. Returns SQLITE_NOMEM on allocation error,
+ noting that it will not allocate when isFinal is true. It returns
+ SQLITE_ERROR if there's a serious internal error in dealing with
+ the JNI state.
+*/
+static int udf_setAggregateContext(JNIEnv * env, jobject jCx,
+ sqlite3_context * pCx,
+ int isFinal){
+ jfieldID member;
+ void * pAgg;
+ int rc = 0;
+ S3JniNphCache * const pCache =
+ S3JniGlobal_nph_cache(env, S3JniClassNames.sqlite3_context);
+ if(pCache && pCache->klazz && pCache->fidSetAgg){
+ member = pCache->fidSetAgg;
+ assert(member);
+ }else{
+ jclass const klazz =
+ pCache ? pCache->klazz : (*env)->GetObjectClass(env, jCx);
+ member = (*env)->GetFieldID(env, klazz, "aggregateContext", "J");
+ if( !member ){
+ IFTHREW{ EXCEPTION_REPORT; EXCEPTION_CLEAR; }
+ return s3jni_db_error(sqlite3_context_db_handle(pCx),
+ SQLITE_ERROR,
+ "Internal error: cannot find "
+ "sqlite3_context::aggregateContext field.");
+ }
+ if(pCache){
+ assert(pCache->klazz);
+ assert(!pCache->fidSetAgg);
+ pCache->fidSetAgg = member;
+ }
+ }
+ pAgg = sqlite3_aggregate_context(pCx, isFinal ? 0 : 4);
+ if( pAgg || isFinal ){
+ (*env)->SetLongField(env, jCx, member, (jlong)pAgg);
+ }else{
+ assert(!pAgg);
+ rc = SQLITE_NOMEM;
+ }
+ return rc;
+}
+
+/**
+ Common init for OutputPointer_set_Int32() and friends. zClassName must be a
+ pointer from S3JniClassNames. jOut must be an instance of that
+ class. Fetches the jfieldID for jOut's [value] property, which must
+ be of the type represented by the JNI type signature zTypeSig, and
+ stores it in pFieldId. Fails fatally if the property is not found,
+ as that presents a serious internal misuse.
+
+ Property lookups are cached on a per-zClassName basis. Do not use
+ this routine with the same zClassName but different zTypeSig: it
+ will misbehave.
+*/
+static void setupOutputPointer(JNIEnv * const env, const char *zClassName,
+ const char * const zTypeSig,
+ jobject const jOut, jfieldID * const pFieldId){
+ jfieldID setter = 0;
+ S3JniNphCache * const pCache =
+ S3JniGlobal_nph_cache(env, zClassName);
+ if(pCache && pCache->klazz && pCache->fidValue){
+ setter = pCache->fidValue;
+ }else{
+ const jclass klazz = (*env)->GetObjectClass(env, jOut);
+ /*MARKER(("%s => %s\n", zClassName, zTypeSig));*/
+ setter = (*env)->GetFieldID(env, klazz, "value", zTypeSig);
+ EXCEPTION_IS_FATAL("setupOutputPointer() could not find OutputPointer.*.value");
+ if(pCache){
+ assert(!pCache->fidValue);
+ pCache->fidValue = setter;
+ }
+ }
+ *pFieldId = setter;
+}
+
+/* Sets the value property of the OutputPointer.Int32 jOut object
+ to v. */
+static void OutputPointer_set_Int32(JNIEnv * const env, jobject const jOut, int v){
+ jfieldID setter = 0;
+ setupOutputPointer(env, S3JniClassNames.OutputPointer_Int32, "I", jOut, &setter);
+ (*env)->SetIntField(env, jOut, setter, (jint)v);
+ EXCEPTION_IS_FATAL("Cannot set OutputPointer.Int32.value");
+}
+
+/* Sets the value property of the OutputPointer.Int64 jOut object
+ to v. */
+static void OutputPointer_set_Int64(JNIEnv * const env, jobject const jOut, jlong v){
+ jfieldID setter = 0;
+ setupOutputPointer(env, S3JniClassNames.OutputPointer_Int64, "J", jOut, &setter);
+ (*env)->SetLongField(env, jOut, setter, v);
+ EXCEPTION_IS_FATAL("Cannot set OutputPointer.Int64.value");
+}
+
+static void OutputPointer_set_sqlite3(JNIEnv * const env, jobject const jOut,
+ jobject jDb){
+ jfieldID setter = 0;
+ setupOutputPointer(env, S3JniClassNames.OutputPointer_sqlite3,
+ "Lorg/sqlite/jni/sqlite3;", jOut, &setter);
+ (*env)->SetObjectField(env, jOut, setter, jDb);
+ EXCEPTION_IS_FATAL("Cannot set OutputPointer.sqlite3.value");
+}
+
+static void OutputPointer_set_sqlite3_stmt(JNIEnv * const env, jobject const jOut,
+ jobject jStmt){
+ jfieldID setter = 0;
+ setupOutputPointer(env, S3JniClassNames.OutputPointer_sqlite3_stmt,
+ "Lorg/sqlite/jni/sqlite3_stmt;", jOut, &setter);
+ (*env)->SetObjectField(env, jOut, setter, jStmt);
+ EXCEPTION_IS_FATAL("Cannot set OutputPointer.sqlite3_stmt.value");
+}
+
+#ifdef SQLITE_ENABLE_FTS5
+#if 0
+/* Sets the value property of the OutputPointer.ByteArray jOut object
+ to v. */
+static void OutputPointer_set_ByteArray(JNIEnv * const env, jobject const jOut,
+ jbyteArray const v){
+ jfieldID setter = 0;
+ setupOutputPointer(env, S3JniClassNames.OutputPointer_ByteArray, "[B",
+ jOut, &setter);
+ (*env)->SetObjectField(env, jOut, setter, v);
+ EXCEPTION_IS_FATAL("Cannot set OutputPointer.ByteArray.value");
+}
+#endif
+/* Sets the value property of the OutputPointer.String jOut object
+ to v. */
+static void OutputPointer_set_String(JNIEnv * const env, jobject const jOut,
+ jstring const v){
+ jfieldID setter = 0;
+ setupOutputPointer(env, S3JniClassNames.OutputPointer_String,
+ "Ljava/lang/String;", jOut, &setter);
+ (*env)->SetObjectField(env, jOut, setter, v);
+ EXCEPTION_IS_FATAL("Cannot set OutputPointer.String.value");
+}
+#endif /* SQLITE_ENABLE_FTS5 */
+
+static int encodingTypeIsValid(int eTextRep){
+ switch(eTextRep){
+ case SQLITE_UTF8: case SQLITE_UTF16:
+ case SQLITE_UTF16LE: case SQLITE_UTF16BE:
+ return 1;
+ default:
+ return 0;
+ }
+}
+
+static int CollationState_xCompare(void *pArg, int nLhs, const void *lhs,
+ int nRhs, const void *rhs){
+ S3JniDb * const ps = pArg;
+ JNIEnv * env = ps->env;
+ jint rc = 0;
+ jbyteArray jbaLhs = (*env)->NewByteArray(env, (jint)nLhs);
+ jbyteArray jbaRhs = jbaLhs ? (*env)->NewByteArray(env, (jint)nRhs) : NULL;
+ //MARKER(("native xCompare nLhs=%d nRhs=%d\n", nLhs, nRhs));
+ if(!jbaRhs){
+ s3jni_db_error(ps->pDb, SQLITE_NOMEM, 0);
+ return 0;
+ //(*env)->FatalError(env, "Out of memory. Cannot allocate arrays for collation.");
+ }
+ (*env)->SetByteArrayRegion(env, jbaLhs, 0, (jint)nLhs, (const jbyte*)lhs);
+ (*env)->SetByteArrayRegion(env, jbaRhs, 0, (jint)nRhs, (const jbyte*)rhs);
+ rc = (*env)->CallIntMethod(env, ps->collation.jObj, ps->collation.midCallback,
+ jbaLhs, jbaRhs);
+ EXCEPTION_IGNORE;
+ UNREF_L(jbaLhs);
+ UNREF_L(jbaRhs);
+ return (int)rc;
+}
+
+/* Collation finalizer for use by the sqlite3 internals. */
+static void CollationState_xDestroy(void *pArg){
+ S3JniDb * const ps = pArg;
+ S3JniHook_unref( ps->env, &ps->collation, 1 );
+}
+
+/* State for sqlite3_result_java_object() and
+ sqlite3_value_java_object(). */
+typedef struct {
+ /* The JNI docs say:
+
+ https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html
+
+ > The VM is guaranteed to pass the same interface pointer to a
+ native method when it makes multiple calls to the native method
+ from the same Java thread.
+
+ Per the accompanying diagram, the "interface pointer" is the
+ pointer-to-pointer which is passed to all JNI calls
+ (`JNIEnv *env`), implying that we need to be caching that. The
+ verbiage "interface pointer" implies, however, that we should be
+ storing the dereferenced `(*env)` pointer.
+
+ This posts claims it's unsafe to cache JNIEnv at all, even when
+ it's always used in the same thread:
+
+ https://stackoverflow.com/questions/12420463
+
+ And this one seems to contradict that:
+
+ https://stackoverflow.com/questions/13964608
+
+ For later reference:
+
+ https://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/design.html#wp1242
+
+ https://developer.android.com/training/articles/perf-jni
+
+ The later has the following say about caching:
+
+ > If performance is important, it's useful to look the
+ [class/method ID] values up once and cache the results in your
+ native code. Because there is a limit of one JavaVM per
+ process, it's reasonable to store this data in a static local
+ structure. ... The class references, field IDs, and method IDs
+ are guaranteed valid until the class is unloaded. Classes are
+ only unloaded if all classes associated with a ClassLoader can
+ be garbage collected, which is rare but will not be impossible
+ in Android. Note however that the jclass is a class reference
+ and must be protected with a call to NewGlobalRef (see the next
+ section).
+ */
+ JNIEnv * env;
+ jobject jObj;
+} ResultJavaVal;
+
+/* For use with sqlite3_result/value_pointer() */
+#define RESULT_JAVA_VAL_STRING "ResultJavaVal"
+
+static ResultJavaVal * ResultJavaVal_alloc(JNIEnv * const env, jobject jObj){
+ ResultJavaVal * rv = sqlite3_malloc(sizeof(ResultJavaVal));
+ if(rv){
+ rv->env = env;
+ rv->jObj = jObj ? REF_G(jObj) : 0;
+ }
+ return rv;
+}
+
+static void ResultJavaVal_finalizer(void *v){
+ if(v){
+ ResultJavaVal * const rv = (ResultJavaVal*)v;
+ if(rv->jObj) (*(rv->env))->DeleteGlobalRef(rv->env, rv->jObj);
+ sqlite3_free(rv);
+ }
+}
+
+
+
+/**
+ Returns a new Java instance of the class named by zClassName, which
+ MUST be interface-compatible with NativePointerHolder and MUST have
+ a no-arg constructor. The NativePointerHolder_set() method is
+ passed the new Java object and pNative. Hypothetically returns NULL
+ if Java fails to allocate, but the JNI docs are not entirely clear
+ on that detail.
+
+ Always use a static string pointer from S3JniClassNames for the 2nd
+ argument so that we can use its address as a cache key.
+*/
+static jobject new_NativePointerHolder_object(JNIEnv * const env, const char *zClassName,
+ const void * pNative){
+ jobject rv = 0;
+ jclass klazz = 0;
+ jmethodID ctor = 0;
+ S3JniNphCache * const pCache =
+ S3JniGlobal_nph_cache(env, zClassName);
+ if(pCache && pCache->midCtor){
+ assert( pCache->klazz );
+ klazz = pCache->klazz;
+ ctor = pCache->midCtor;
+ }else{
+ klazz = pCache
+ ? pCache->klazz
+ : (*env)->FindClass(env, zClassName);
+ ctor = klazz ? (*env)->GetMethodID(env, klazz, "", "()V") : 0;
+ EXCEPTION_IS_FATAL("Cannot find constructor for class.");
+ if(pCache){
+ assert(zClassName == pCache->zClassName);
+ assert(pCache->klazz);
+ assert(!pCache->midCtor);
+ pCache->midCtor = ctor;
+ }
+ }
+ assert(klazz);
+ assert(ctor);
+ rv = (*env)->NewObject(env, klazz, ctor);
+ EXCEPTION_IS_FATAL("No-arg constructor threw.");
+ if(rv) NativePointerHolder_set(env, rv, pNative, zClassName);
+ return rv;
+}
+
+static inline jobject new_sqlite3_wrapper(JNIEnv * const env, sqlite3 *sv){
+ return new_NativePointerHolder_object(env, S3JniClassNames.sqlite3, sv);
+}
+static inline jobject new_sqlite3_context_wrapper(JNIEnv * const env, sqlite3_context *sv){
+ return new_NativePointerHolder_object(env, S3JniClassNames.sqlite3_context, sv);
+}
+static inline jobject new_sqlite3_stmt_wrapper(JNIEnv * const env, sqlite3_stmt *sv){
+ return new_NativePointerHolder_object(env, S3JniClassNames.sqlite3_stmt, sv);
+}
+static inline jobject new_sqlite3_value_wrapper(JNIEnv * const env, sqlite3_value *sv){
+ return new_NativePointerHolder_object(env, S3JniClassNames.sqlite3_value, sv);
+}
+
+enum UDFType {
+ UDF_SCALAR = 1,
+ UDF_AGGREGATE,
+ UDF_WINDOW,
+ UDF_UNKNOWN_TYPE/*for error propagation*/
+};
+
+typedef void (*udf_xFunc_f)(sqlite3_context*,int,sqlite3_value**);
+typedef void (*udf_xStep_f)(sqlite3_context*,int,sqlite3_value**);
+typedef void (*udf_xFinal_f)(sqlite3_context*);
+/*typedef void (*udf_xValue_f)(sqlite3_context*);*/
+/*typedef void (*udf_xInverse_f)(sqlite3_context*,int,sqlite3_value**);*/
+
+/**
+ State for binding Java-side UDFs.
+*/
+typedef struct S3JniUdf S3JniUdf;
+struct S3JniUdf {
+ JNIEnv * env; /* env registered from */;
+ jobject jObj /* SQLFunction instance */;
+ jclass klazz /* jObj's class */;
+ char * zFuncName /* Only for error reporting and debug logging */;
+ enum UDFType type;
+ /** Method IDs for the various UDF methods. */
+ jmethodID jmidxFunc;
+ jmethodID jmidxStep;
+ jmethodID jmidxFinal;
+ jmethodID jmidxValue;
+ jmethodID jmidxInverse;
+};
+
+static S3JniUdf * S3JniUdf_alloc(JNIEnv * const env, jobject jObj){
+ S3JniUdf * const s = sqlite3_malloc(sizeof(S3JniUdf));
+ if(s){
+ const char * zFSI = /* signature for xFunc, xStep, xInverse */
+ "(Lorg/sqlite/jni/sqlite3_context;[Lorg/sqlite/jni/sqlite3_value;)V";
+ const char * zFV = /* signature for xFinal, xValue */
+ "(Lorg/sqlite/jni/sqlite3_context;)V";
+ memset(s, 0, sizeof(S3JniUdf));
+ s->env = env;
+ s->jObj = REF_G(jObj);
+ s->klazz = REF_G((*env)->GetObjectClass(env, jObj));
+#define FGET(FuncName,FuncType,Field) \
+ s->Field = (*env)->GetMethodID(env, s->klazz, FuncName, FuncType); \
+ if(!s->Field) (*env)->ExceptionClear(env)
+ FGET("xFunc", zFSI, jmidxFunc);
+ FGET("xStep", zFSI, jmidxStep);
+ FGET("xFinal", zFV, jmidxFinal);
+ FGET("xValue", zFV, jmidxValue);
+ FGET("xInverse", zFSI, jmidxInverse);
+#undef FGET
+ if(s->jmidxFunc) s->type = UDF_SCALAR;
+ else if(s->jmidxStep && s->jmidxFinal){
+ s->type = s->jmidxValue ? UDF_WINDOW : UDF_AGGREGATE;
+ }else{
+ s->type = UDF_UNKNOWN_TYPE;
+ }
+ }
+ return s;
+}
+
+static void S3JniUdf_free(S3JniUdf * s){
+ JNIEnv * const env = s->env;
+ if(env){
+ //MARKER(("UDF cleanup: %s\n", s->zFuncName));
+ s3jni_call_xDestroy(env, s->jObj, s->klazz);
+ UNREF_G(s->jObj);
+ UNREF_G(s->klazz);
+ }
+ sqlite3_free(s->zFuncName);
+ sqlite3_free(s);
+}
+
+static void S3JniUdf_finalizer(void * s){
+ //MARKER(("UDF finalizer @ %p\n", s));
+ if(s) S3JniUdf_free((S3JniUdf*)s);
+}
+
+/**
+ Helper for processing args to UDF handlers
+ with signature (sqlite3_context*,int,sqlite3_value**).
+*/
+typedef struct {
+ jobject jcx;
+ jobjectArray jargv;
+} udf_jargs;
+
+/**
+ Converts the given (cx, argc, argv) into arguments for the given
+ UDF, placing the result in the final argument. Returns 0 on
+ success, SQLITE_NOMEM on allocation error.
+
+ TODO: see what we can do to optimize the
+ new_sqlite3_value_wrapper() call. e.g. find the ctor a single time
+ and call it here, rather than looking it up repeatedly.
+*/
+static int udf_args(JNIEnv *env,
+ sqlite3_context * const cx,
+ int argc, sqlite3_value**argv,
+ jobject * jCx, jobjectArray *jArgv){
+ jobjectArray ja = 0;
+ jobject jcx = new_sqlite3_context_wrapper(env, cx);
+ jint i;
+ *jCx = 0;
+ *jArgv = 0;
+ if(!jcx) goto error_oom;
+ ja = (*env)->NewObjectArray(env, argc,
+ S3JniGlobal_env_cache(env)->g.cObj,
+ NULL);
+ if(!ja) goto error_oom;
+ for(i = 0; i < argc; ++i){
+ jobject jsv = new_sqlite3_value_wrapper(env, argv[i]);
+ if(!jsv) goto error_oom;
+ (*env)->SetObjectArrayElement(env, ja, i, jsv);
+ UNREF_L(jsv)/*array has a ref*/;
+ }
+ *jCx = jcx;
+ *jArgv = ja;
+ return 0;
+error_oom:
+ sqlite3_result_error_nomem(cx);
+ UNREF_L(jcx);
+ UNREF_L(ja);
+ return SQLITE_NOMEM;
+}
+
+static int udf_report_exception(sqlite3_context * cx,
+ const char *zFuncName,
+ const char *zFuncType){
+ int rc;
+ char * z =
+ sqlite3_mprintf("Client-defined function %s.%s() threw. It should "
+ "not do that.",
+ zFuncName ? zFuncName : "", zFuncType);
+ if(z){
+ sqlite3_result_error(cx, z, -1);
+ sqlite3_free(z);
+ rc = SQLITE_ERROR;
+ }else{
+ sqlite3_result_error_nomem(cx);
+ rc = SQLITE_NOMEM;
+ }
+ return rc;
+}
+
+/**
+ Sets up the state for calling a Java-side xFunc/xStep/xInverse()
+ UDF, calls it, and returns 0 on success.
+*/
+static int udf_xFSI(sqlite3_context* pCx, int argc,
+ sqlite3_value** argv,
+ S3JniUdf * s,
+ jmethodID xMethodID,
+ const char * zFuncType){
+ JNIEnv * const env = s->env;
+ udf_jargs args = {0,0};
+ int rc = udf_args(s->env, pCx, argc, argv, &args.jcx, &args.jargv);
+ //MARKER(("%s.%s() pCx = %p\n", s->zFuncName, zFuncType, pCx));
+ if(rc) return rc;
+ //MARKER(("UDF::%s.%s()\n", s->zFuncName, zFuncType));
+ if( UDF_SCALAR != s->type ){
+ rc = udf_setAggregateContext(env, args.jcx, pCx, 0);
+ }
+ if( 0 == rc ){
+ (*env)->CallVoidMethod(env, s->jObj, xMethodID, args.jcx, args.jargv);
+ IFTHREW{
+ rc = udf_report_exception(pCx, s->zFuncName, zFuncType);
+ }
+ }
+ UNREF_L(args.jcx);
+ UNREF_L(args.jargv);
+ return rc;
+}
+
+/**
+ Sets up the state for calling a Java-side xFinal/xValue() UDF,
+ calls it, and returns 0 on success.
+*/
+static int udf_xFV(sqlite3_context* cx, S3JniUdf * s,
+ jmethodID xMethodID,
+ const char *zFuncType){
+ JNIEnv * const env = s->env;
+ jobject jcx = new_sqlite3_context_wrapper(s->env, cx);
+ int rc = 0;
+ //MARKER(("%s.%s() cx = %p\n", s->zFuncName, zFuncType, cx));
+ if(!jcx){
+ sqlite3_result_error_nomem(cx);
+ return SQLITE_NOMEM;
+ }
+ //MARKER(("UDF::%s.%s()\n", s->zFuncName, zFuncType));
+ if( UDF_SCALAR != s->type ){
+ rc = udf_setAggregateContext(env, jcx, cx, 1);
+ }
+ if( 0 == rc ){
+ (*env)->CallVoidMethod(env, s->jObj, xMethodID, jcx);
+ IFTHREW{
+ rc = udf_report_exception(cx,s->zFuncName, zFuncType);
+ }
+ }
+ UNREF_L(jcx);
+ return rc;
+}
+
+static void udf_xFunc(sqlite3_context* cx, int argc,
+ sqlite3_value** argv){
+ S3JniUdf * const s = (S3JniUdf*)sqlite3_user_data(cx);
+ ++S3JniGlobal.metrics.udf.nFunc;
+ udf_xFSI(cx, argc, argv, s, s->jmidxFunc, "xFunc");
+}
+static void udf_xStep(sqlite3_context* cx, int argc,
+ sqlite3_value** argv){
+ S3JniUdf * const s = (S3JniUdf*)sqlite3_user_data(cx);
+ ++S3JniGlobal.metrics.udf.nStep;
+ udf_xFSI(cx, argc, argv, s, s->jmidxStep, "xStep");
+}
+static void udf_xFinal(sqlite3_context* cx){
+ S3JniUdf * const s = (S3JniUdf*)sqlite3_user_data(cx);
+ ++S3JniGlobal.metrics.udf.nFinal;
+ udf_xFV(cx, s, s->jmidxFinal, "xFinal");
+}
+static void udf_xValue(sqlite3_context* cx){
+ S3JniUdf * const s = (S3JniUdf*)sqlite3_user_data(cx);
+ ++S3JniGlobal.metrics.udf.nValue;
+ udf_xFV(cx, s, s->jmidxValue, "xValue");
+}
+static void udf_xInverse(sqlite3_context* cx, int argc,
+ sqlite3_value** argv){
+ S3JniUdf * const s = (S3JniUdf*)sqlite3_user_data(cx);
+ ++S3JniGlobal.metrics.udf.nInverse;
+ udf_xFSI(cx, argc, argv, s, s->jmidxInverse, "xInverse");
+}
+
+
+////////////////////////////////////////////////////////////////////////
+// What follows is the JNI/C bindings. They are in alphabetical order
+// except for this macro-generated subset which are kept together here
+// at the front...
+////////////////////////////////////////////////////////////////////////
+WRAP_INT_STMT(1bind_1parameter_1count, sqlite3_bind_parameter_count)
+WRAP_INT_DB(1changes, sqlite3_changes)
+WRAP_INT64_DB(1changes64, sqlite3_changes64)
+WRAP_INT_STMT(1clear_1bindings, sqlite3_clear_bindings)
+WRAP_INT_STMT_INT(1column_1bytes, sqlite3_column_bytes)
+WRAP_INT_STMT_INT(1column_1bytes16, sqlite3_column_bytes16)
+WRAP_INT_STMT(1column_1count, sqlite3_column_count)
+WRAP_STR_STMT_INT(1column_1decltype, sqlite3_column_decltype)
+WRAP_STR_STMT_INT(1column_1name, sqlite3_column_name)
+WRAP_STR_STMT_INT(1column_1database_1name, sqlite3_column_database_name)
+WRAP_STR_STMT_INT(1column_1origin_1name, sqlite3_column_origin_name)
+WRAP_STR_STMT_INT(1column_1table_1name, sqlite3_column_table_name)
+WRAP_INT_STMT_INT(1column_1type, sqlite3_column_type)
+WRAP_INT_STMT(1data_1count, sqlite3_data_count)
+WRAP_INT_DB(1error_1offset, sqlite3_error_offset)
+WRAP_INT_DB(1extended_1errcode, sqlite3_extended_errcode)
+WRAP_MUTF8_VOID(1libversion, sqlite3_libversion)
+WRAP_INT_VOID(1libversion_1number, sqlite3_libversion_number)
+WRAP_INT_INT(1sleep, sqlite3_sleep)
+WRAP_MUTF8_VOID(1sourceid, sqlite3_sourceid)
+WRAP_INT_VOID(1threadsafe, sqlite3_threadsafe)
+WRAP_INT_DB(1total_1changes, sqlite3_total_changes)
+WRAP_INT64_DB(1total_1changes64, sqlite3_total_changes64)
+WRAP_INT_SVALUE(1value_1bytes, sqlite3_value_bytes)
+WRAP_INT_SVALUE(1value_1bytes16, sqlite3_value_bytes16)
+WRAP_INT_SVALUE(1value_1encoding, sqlite3_value_encoding)
+WRAP_INT_SVALUE(1value_1frombind, sqlite3_value_frombind)
+WRAP_INT_SVALUE(1value_1nochange, sqlite3_value_nochange)
+WRAP_INT_SVALUE(1value_1numeric_1type, sqlite3_value_numeric_type)
+WRAP_INT_SVALUE(1value_1subtype, sqlite3_value_subtype)
+WRAP_INT_SVALUE(1value_1type, sqlite3_value_type)
+
+#if S3JNI_ENABLE_AUTOEXT
+/* Central auto-extension handler. */
+FIXME_THREADING(autoExt)
+static int s3jni_auto_extension(sqlite3 *pDb, const char **pzErr,
+ const struct sqlite3_api_routines *ignored){
+ S3JniAutoExtension const * pAX = S3JniGlobal.autoExt.pHead;
+ int rc;
+ JNIEnv * env = 0;
+ S3JniDb * const ps = S3JniGlobal.autoExt.psOpening;
+ //MARKER(("auto-extension on open()ing ps@%p db@%p\n", ps, pDb));
+ S3JniGlobal.autoExt.psOpening = 0;
+ if( !pAX ){
+ assert( 0==S3JniGlobal.autoExt.isRunning );
+ return 0;
+ }
+ else if( S3JniGlobal.autoExt.isRunning ){
+ /* Necessary to avoid certain endless loop/stack overflow cases. */
+ *pzErr = sqlite3_mprintf("Auto-extensions must not be triggered while "
+ "auto-extensions are running.");
+ return SQLITE_MISUSE;
+ }
+ else if(!ps){
+ MARKER(("Internal error: cannot find S3JniDb for auto-extension\n"));
+ return SQLITE_ERROR;
+ }else if( (*S3JniGlobal.jvm)->GetEnv(S3JniGlobal.jvm, (void **)&env, JNI_VERSION_1_8) ){
+ assert(!"Cannot get JNIEnv");
+ *pzErr = sqlite3_mprintf("Could not get current JNIEnv.");
+ return SQLITE_ERROR;
+ }
+ assert( !ps->pDb /* it's still being opened */ );
+ ps->pDb = pDb;
+ assert( ps->jDb );
+ NativePointerHolder_set(env, ps->jDb, pDb, S3JniClassNames.sqlite3);
+ ++S3JniGlobal.autoExt.isRunning;
+ for( ; pAX; pAX = pAX->pNext ){
+ rc = (*env)->CallIntMethod(env, pAX->jObj, pAX->midFunc, ps->jDb);
+ IFTHREW {
+ jthrowable const ex = (*env)->ExceptionOccurred(env);
+ char * zMsg;
+ EXCEPTION_CLEAR;
+ zMsg = s3jni_exception_error_msg(env, ex);
+ UNREF_L(ex);
+ *pzErr = sqlite3_mprintf("auto-extension threw: %s", zMsg);
+ sqlite3_free(zMsg);
+ rc = rc ? rc : SQLITE_ERROR;
+ break;
+ }else if( rc ){
+ break;
+ }
+ }
+ --S3JniGlobal.autoExt.isRunning;
+ return rc;
+}
+
+FIXME_THREADING(autoExt)
+JDECL(jint,1auto_1extension)(JENV_OSELF, jobject jAutoExt){
+ static int once = 0;
+ S3JniAutoExtension * ax;
+
+ if( !jAutoExt ) return SQLITE_MISUSE;
+ else if( 0==once && ++once ){
+ sqlite3_auto_extension( (void(*)(void))s3jni_auto_extension );
+ }
+ ax = S3JniGlobal.autoExt.pHead;
+ for( ; ax; ax = ax->pNext ){
+ if( (*env)->IsSameObject(env, ax->jObj, jAutoExt) ){
+ return 0 /* C API treats this as a no-op. */;
+ }
+ }
+ return S3JniAutoExtension_alloc(env, jAutoExt) ? 0 : SQLITE_NOMEM;
+}
+#endif /* S3JNI_ENABLE_AUTOEXT */
+
+FIXME_THREADING(S3JniEnvCache)
+JDECL(jint,1bind_1blob)(JENV_CSELF, jobject jpStmt,
+ jint ndx, jbyteArray baData, jint nMax){
+ int rc;
+ if(!baData){
+ rc = sqlite3_bind_null(PtrGet_sqlite3_stmt(jpStmt), ndx);
+ }else{
+ jbyte * const pBuf = JBA_TOC(baData);
+ rc = sqlite3_bind_blob(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, pBuf, (int)nMax,
+ SQLITE_TRANSIENT);
+ JBA_RELEASE(baData,pBuf);
+ }
+ return (jint)rc;
+}
+
+FIXME_THREADING(S3JniEnvCache)
+JDECL(jint,1bind_1double)(JENV_CSELF, jobject jpStmt,
+ jint ndx, jdouble val){
+ return (jint)sqlite3_bind_double(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, (double)val);
+}
+
+FIXME_THREADING(S3JniEnvCache)
+JDECL(jint,1bind_1int)(JENV_CSELF, jobject jpStmt,
+ jint ndx, jint val){
+ return (jint)sqlite3_bind_int(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, (int)val);
+}
+
+FIXME_THREADING(S3JniEnvCache)
+JDECL(jint,1bind_1int64)(JENV_CSELF, jobject jpStmt,
+ jint ndx, jlong val){
+ return (jint)sqlite3_bind_int64(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, (sqlite3_int64)val);
+}
+
+FIXME_THREADING(S3JniEnvCache)
+JDECL(jint,1bind_1null)(JENV_CSELF, jobject jpStmt,
+ jint ndx){
+ return (jint)sqlite3_bind_null(PtrGet_sqlite3_stmt(jpStmt), (int)ndx);
+}
+
+FIXME_THREADING(S3JniEnvCache)
+JDECL(jint,1bind_1parameter_1index)(JENV_CSELF, jobject jpStmt, jbyteArray jName){
+ int rc = 0;
+ jbyte * const pBuf = JBA_TOC(jName);
+ if(pBuf){
+ rc = sqlite3_bind_parameter_index(PtrGet_sqlite3_stmt(jpStmt),
+ (const char *)pBuf);
+ JBA_RELEASE(jName, pBuf);
+ }
+ return rc;
+}
+
+FIXME_THREADING(S3JniEnvCache)
+JDECL(jint,1bind_1text)(JENV_CSELF, jobject jpStmt,
+ jint ndx, jbyteArray baData, jint nMax){
+ if(baData){
+ jbyte * const pBuf = JBA_TOC(baData);
+ int rc = sqlite3_bind_text(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, (const char *)pBuf,
+ (int)nMax, SQLITE_TRANSIENT);
+ JBA_RELEASE(baData, pBuf);
+ return (jint)rc;
+ }else{
+ return sqlite3_bind_null(PtrGet_sqlite3_stmt(jpStmt), (int)ndx);
+ }
+}
+
+FIXME_THREADING(S3JniEnvCache)
+JDECL(jint,1bind_1zeroblob)(JENV_CSELF, jobject jpStmt,
+ jint ndx, jint n){
+ return (jint)sqlite3_bind_zeroblob(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, (int)n);
+}
+
+FIXME_THREADING(S3JniEnvCache)
+JDECL(jint,1bind_1zeroblob64)(JENV_CSELF, jobject jpStmt,
+ jint ndx, jlong n){
+ return (jint)sqlite3_bind_zeroblob(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, (sqlite3_uint64)n);
+}
+
+static int s3jni_busy_handler(void* pState, int n){
+ S3JniDb * const ps = (S3JniDb *)pState;
+ int rc = 0;
+ if( ps->busyHandler.jObj ){
+ JNIEnv * const env = ps->env;
+ rc = (*env)->CallIntMethod(env, ps->busyHandler.jObj,
+ ps->busyHandler.midCallback, (jint)n);
+ IFTHREW{
+ EXCEPTION_WARN_CALLBACK_THREW("busy-handler callback");
+ EXCEPTION_CLEAR;
+ rc = s3jni_db_error(ps->pDb, SQLITE_ERROR, "busy-handle callback threw.");
+ }
+ }
+ return rc;
+}
+
+FIXME_THREADING(S3JniEnvCache)
+JDECL(jint,1busy_1handler)(JENV_CSELF, jobject jDb, jobject jBusy){
+ S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0);
+ int rc = 0;
+ if(!ps) return (jint)SQLITE_NOMEM;
+ if(jBusy){
+ S3JniHook * const pHook = &ps->busyHandler;
+ if(pHook->jObj && (*env)->IsSameObject(env, pHook->jObj, jBusy)){
+ /* Same object - this is a no-op. */
+ return 0;
+ }
+ S3JniHook_unref(env, pHook, 1);
+ pHook->jObj = REF_G(jBusy);
+ pHook->klazz = REF_G((*env)->GetObjectClass(env, jBusy));
+ pHook->midCallback = (*env)->GetMethodID(env, pHook->klazz, "xCallback", "(I)I");
+ IFTHREW {
+ S3JniHook_unref(env, pHook, 0);
+ rc = SQLITE_ERROR;
+ }
+ if(rc){
+ return rc;
+ }
+ }else{
+ S3JniHook_unref(env, &ps->busyHandler, 1);
+ }
+ return jBusy
+ ? sqlite3_busy_handler(ps->pDb, s3jni_busy_handler, ps)
+ : sqlite3_busy_handler(ps->pDb, 0, 0);
+}
+
+FIXME_THREADING(S3JniEnvCache)
+FIXME_THREADING(perDb)
+JDECL(jint,1busy_1timeout)(JENV_CSELF, jobject jDb, jint ms){
+ S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0);
+ if( ps ){
+ S3JniHook_unref(env, &ps->busyHandler, 1);
+ return sqlite3_busy_timeout(ps->pDb, (int)ms);
+ }
+ return SQLITE_MISUSE;
+}
+
+#if S3JNI_ENABLE_AUTOEXT
+FIXME_THREADING(autoExt)
+JDECL(jboolean,1cancel_1auto_1extension)(JENV_CSELF, jobject jAutoExt){
+ S3JniAutoExtension * ax;;
+ if( S3JniGlobal.autoExt.isRunning ) return JNI_FALSE;
+ for( ax = S3JniGlobal.autoExt.pHead; ax; ax = ax->pNext ){
+ if( (*env)->IsSameObject(env, ax->jObj, jAutoExt) ){
+ S3JniAutoExtension_free(env, ax);
+ return JNI_TRUE;
+ }
+ }
+ return JNI_FALSE;
+}
+#endif /* S3JNI_ENABLE_AUTOEXT */
+
+
+/**
+ Wrapper for sqlite3_close(_v2)().
+*/
+static jint s3jni_close_db(JNIEnv * const env, jobject jDb, int version){
+ int rc = 0;
+ S3JniDb * ps = 0;
+ assert(version == 1 || version == 2);
+ ps = S3JniDb_for_db(env, jDb, 0, 0);
+ if(ps){
+ //MARKER(("close()ing db@%p\n", ps->pDb));
+ rc = 1==version ? (jint)sqlite3_close(ps->pDb) : (jint)sqlite3_close_v2(ps->pDb);
+ S3JniDb_set_aside(ps)
+ /* MUST come after close() because of ps->trace. */;
+ NativePointerHolder_set(env, jDb, 0, S3JniClassNames.sqlite3);
+ }
+ return (jint)rc;
+}
+
+FIXME_THREADING(S3JniEnvCache)
+FIXME_THREADING(perDb)
+JDECL(jint,1close_1v2)(JENV_CSELF, jobject pDb){
+ return s3jni_close_db(env, pDb, 2);
+}
+
+FIXME_THREADING(S3JniEnvCache)
+FIXME_THREADING(perDb)
+JDECL(jint,1close)(JENV_CSELF, jobject pDb){
+ return s3jni_close_db(env, pDb, 1);
+}
+
+/**
+ Assumes z is an array of unsigned short and returns the index in
+ that array of the first element with the value 0.
+*/
+static unsigned int s3jni_utf16_strlen(void const * z){
+ unsigned int i = 0;
+ const unsigned short * p = z;
+ while( p[i] ) ++i;
+ return i;
+}
+
+/**
+ sqlite3_collation_needed16() hook impl.
+ */
+static void s3jni_collation_needed_impl16(void *pState, sqlite3 *pDb,
+ int eTextRep, const void * z16Name){
+ S3JniDb * const ps = pState;
+ JNIEnv * const env = ps->env;
+ unsigned int const nName = s3jni_utf16_strlen(z16Name);
+ jstring jName = (*env)->NewString(env, (jchar const *)z16Name, nName);
+ IFTHREW{
+ s3jni_db_error(ps->pDb, SQLITE_NOMEM, 0);
+ EXCEPTION_CLEAR;
+ }else{
+ (*env)->CallVoidMethod(env, ps->collationNeeded.jObj,
+ ps->collationNeeded.midCallback,
+ ps->jDb, (jint)eTextRep, jName);
+ IFTHREW{
+ s3jni_db_exception(env, ps, 0, "collation-needed callback threw");
+ }
+ }
+ UNREF_L(jName);
+}
+
+FIXME_THREADING(S3JniEnvCache)
+FIXME_THREADING(perDb)
+JDECL(jint,1collation_1needed)(JENV_CSELF, jobject jDb, jobject jHook){
+ S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0);
+ jclass klazz;
+ jobject pOld = 0;
+ jmethodID xCallback;
+ S3JniHook * const pHook = &ps->collationNeeded;
+ int rc;
+
+ if( !ps ) return SQLITE_MISUSE;
+ pOld = pHook->jObj;
+ if(pOld && jHook &&
+ (*env)->IsSameObject(env, pOld, jHook)){
+ return 0;
+ }
+ if( !jHook ){
+ UNREF_G(pOld);
+ memset(pHook, 0, sizeof(S3JniHook));
+ sqlite3_collation_needed(ps->pDb, 0, 0);
+ return 0;
+ }
+ klazz = (*env)->GetObjectClass(env, jHook);
+ xCallback = (*env)->GetMethodID(env, klazz, "xCollationNeeded",
+ "(Lorg/sqlite/jni/sqlite3;ILjava/lang/String;)I");
+ IFTHREW {
+ rc = s3jni_db_exception(env, ps, SQLITE_MISUSE,
+ "Cannot not find matching callback on "
+ "collation-needed hook object.");
+ }else{
+ pHook->midCallback = xCallback;
+ pHook->jObj = REF_G(jHook);
+ UNREF_G(pOld);
+ rc = sqlite3_collation_needed16(ps->pDb, ps, s3jni_collation_needed_impl16);
+ }
+ return rc;
+}
+
+FIXME_THREADING(S3JniEnvCache)
+JDECL(jbyteArray,1column_1blob)(JENV_CSELF, jobject jpStmt,
+ jint ndx){
+ sqlite3_stmt * const pStmt = PtrGet_sqlite3_stmt(jpStmt);
+ void const * const p = sqlite3_column_blob(pStmt, (int)ndx);
+ int const n = p ? sqlite3_column_bytes(pStmt, (int)ndx) : 0;
+ if( 0==p ) return NULL;
+ else{
+ jbyteArray const jba = (*env)->NewByteArray(env, n);
+ (*env)->SetByteArrayRegion(env, jba, 0, n, (const jbyte *)p);
+ return jba;
+ }
+}
+
+FIXME_THREADING(S3JniEnvCache)
+JDECL(jdouble,1column_1double)(JENV_CSELF, jobject jpStmt,
+ jint ndx){
+ return (jdouble)sqlite3_column_double(PtrGet_sqlite3_stmt(jpStmt), (int)ndx);
+}
+
+FIXME_THREADING(S3JniEnvCache)
+JDECL(jint,1column_1int)(JENV_CSELF, jobject jpStmt,
+ jint ndx){
+ return (jint)sqlite3_column_int(PtrGet_sqlite3_stmt(jpStmt), (int)ndx);
+}
+
+FIXME_THREADING(S3JniEnvCache)
+JDECL(jlong,1column_1int64)(JENV_CSELF, jobject jpStmt,
+ jint ndx){
+ return (jlong)sqlite3_column_int64(PtrGet_sqlite3_stmt(jpStmt), (int)ndx);
+}
+
+FIXME_THREADING(S3JniEnvCache)
+JDECL(jbyteArray,1column_1text)(JENV_CSELF, jobject jpStmt,
+ jint ndx){
+ sqlite3_stmt * const stmt = PtrGet_sqlite3_stmt(jpStmt);
+ const int n = sqlite3_column_bytes(stmt, (int)ndx);
+ const unsigned char * const p = sqlite3_column_text(stmt, (int)ndx);
+ return s3jni_new_jbyteArray(env, p, n);
+}
+
+FIXME_THREADING(S3JniEnvCache)
+JDECL(jstring,1column_1text16)(JENV_CSELF, jobject jpStmt,
+ jint ndx){
+ sqlite3_stmt * const stmt = PtrGet_sqlite3_stmt(jpStmt);
+ const int n = sqlite3_column_bytes16(stmt, (int)ndx);
+ const void * const p = sqlite3_column_text16(stmt, (int)ndx);
+ return s3jni_text16_to_jstring(env, p, n);
+}
+
+FIXME_THREADING(S3JniEnvCache)
+JDECL(jobject,1column_1value)(JENV_CSELF, jobject jpStmt,
+ jint ndx){
+ sqlite3_value * const sv = sqlite3_column_value(PtrGet_sqlite3_stmt(jpStmt), (int)ndx);
+ return new_sqlite3_value_wrapper(env, sv);
+}
+
+
+static int s3jni_commit_rollback_hook_impl(int isCommit, S3JniDb * const ps){
+ JNIEnv * const env = ps->env;
+ int rc = isCommit
+ ? (int)(*env)->CallIntMethod(env, ps->commitHook.jObj,
+ ps->commitHook.midCallback)
+ : (int)((*env)->CallVoidMethod(env, ps->rollbackHook.jObj,
+ ps->rollbackHook.midCallback), 0);
+ IFTHREW{
+ EXCEPTION_CLEAR;
+ rc = s3jni_db_error(ps->pDb, SQLITE_ERROR, "hook callback threw.");
+ }
+ return rc;
+}
+
+static int s3jni_commit_hook_impl(void *pP){
+ return s3jni_commit_rollback_hook_impl(1, pP);
+}
+
+static void s3jni_rollback_hook_impl(void *pP){
+ (void)s3jni_commit_rollback_hook_impl(0, pP);
+}
+
+FIXME_THREADING(perDb)
+static jobject s3jni_commit_rollback_hook(int isCommit, JNIEnv * const env,jobject jDb,
+ jobject jHook){
+ S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0);
+ jclass klazz;
+ jobject pOld = 0;
+ jmethodID xCallback;
+ S3JniHook * const pHook = isCommit ? &ps->commitHook : &ps->rollbackHook;
+ if(!ps){
+ s3jni_db_error(ps->pDb, SQLITE_NOMEM, 0);
+ return 0;
+ }
+ pOld = pHook->jObj;
+ if(pOld && jHook &&
+ (*env)->IsSameObject(env, pOld, jHook)){
+ return pOld;
+ }
+ if( !jHook ){
+ if(pOld){
+ jobject tmp = REF_L(pOld);
+ UNREF_G(pOld);
+ pOld = tmp;
+ }
+ memset(pHook, 0, sizeof(S3JniHook));
+ if( isCommit ) sqlite3_commit_hook(ps->pDb, 0, 0);
+ else sqlite3_rollback_hook(ps->pDb, 0, 0);
+ return pOld;
+ }
+ klazz = (*env)->GetObjectClass(env, jHook);
+ xCallback = (*env)->GetMethodID(env, klazz,
+ isCommit ? "xCommitHook" : "xRollbackHook",
+ isCommit ? "()I" : "()V");
+ IFTHREW {
+ EXCEPTION_REPORT;
+ EXCEPTION_CLEAR;
+ s3jni_db_error(ps->pDb, SQLITE_ERROR,
+ "Cannot not find matching callback on "
+ "hook object.");
+ }else{
+ pHook->midCallback = xCallback;
+ pHook->jObj = REF_G(jHook);
+ if( isCommit ) sqlite3_commit_hook(ps->pDb, s3jni_commit_hook_impl, ps);
+ else sqlite3_rollback_hook(ps->pDb, s3jni_rollback_hook_impl, ps);
+ if(pOld){
+ jobject tmp = REF_L(pOld);
+ UNREF_G(pOld);
+ pOld = tmp;
+ }
+ }
+ return pOld;
+}
+
+JDECL(jobject,1commit_1hook)(JENV_CSELF,jobject jDb, jobject jHook){
+ return s3jni_commit_rollback_hook(1, env, jDb, jHook);
+}
+
+
+JDECL(jstring,1compileoption_1get)(JENV_CSELF, jint n){
+ return (*env)->NewStringUTF( env, sqlite3_compileoption_get(n) );
+}
+
+JDECL(jboolean,1compileoption_1used)(JENV_CSELF, jstring name){
+ const char *zUtf8 = JSTR_TOC(name);
+ const jboolean rc =
+ 0==sqlite3_compileoption_used(zUtf8) ? JNI_FALSE : JNI_TRUE;
+ JSTR_RELEASE(name, zUtf8);
+ return rc;
+}
+
+FIXME_THREADING(perDb)
+JDECL(jobject,1context_1db_1handle)(JENV_CSELF, jobject jpCx){
+ sqlite3 * const pDb = sqlite3_context_db_handle(PtrGet_sqlite3_context(jpCx));
+ S3JniDb * const ps = pDb ? S3JniDb_for_db(env, 0, pDb, 0) : 0;
+ return ps ? ps->jDb : 0;
+}
+
+JDECL(jint,1create_1collation)(JENV_CSELF, jobject jDb,
+ jstring name, jint eTextRep,
+ jobject oCollation){
+ S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0);
+ jclass klazz;
+ int rc;
+ const char *zName;
+ S3JniHook * pHook;
+ if(!ps) return (jint)SQLITE_NOMEM;
+ pHook = &ps->collation;
+ klazz = (*env)->GetObjectClass(env, oCollation);
+ pHook->midCallback = (*env)->GetMethodID(env, klazz, "xCompare",
+ "([B[B)I");
+ IFTHREW{
+ EXCEPTION_REPORT;
+ return s3jni_db_error(ps->pDb, SQLITE_ERROR,
+ "Could not get xCompare() method for object.");
+ }
+ zName = JSTR_TOC(name);
+ rc = sqlite3_create_collation_v2(ps->pDb, zName, (int)eTextRep,
+ ps, CollationState_xCompare,
+ CollationState_xDestroy);
+ JSTR_RELEASE(name, zName);
+ if( 0==rc ){
+ pHook->jObj = REF_G(oCollation);
+ pHook->klazz = REF_G(klazz);
+ }else{
+ S3JniHook_unref(env, pHook, 1);
+ }
+ return (jint)rc;
+}
+
+static jint create_function(JNIEnv * env, jobject jDb, jstring jFuncName,
+ jint nArg, jint eTextRep, jobject jFunctor){
+ S3JniUdf * s = 0;
+ int rc;
+ sqlite3 * const pDb = PtrGet_sqlite3(jDb);
+ const char * zFuncName = 0;
+
+ if( !encodingTypeIsValid(eTextRep) ){
+ return s3jni_db_error(pDb, SQLITE_FORMAT,
+ "Invalid function encoding option.");
+ }
+ s = S3JniUdf_alloc(env, jFunctor);
+ if( !s ) return SQLITE_NOMEM;
+ else if( UDF_UNKNOWN_TYPE==s->type ){
+ rc = s3jni_db_error(pDb, SQLITE_MISUSE,
+ "Cannot unambiguously determine function type.");
+ S3JniUdf_free(s);
+ goto error_cleanup;
+ }
+ zFuncName = JSTR_TOC(jFuncName);
+ if(!zFuncName){
+ rc = SQLITE_NOMEM;
+ S3JniUdf_free(s);
+ goto error_cleanup;
+ }
+ if( UDF_WINDOW == s->type ){
+ rc = sqlite3_create_window_function(pDb, zFuncName, nArg, eTextRep, s,
+ udf_xStep, udf_xFinal, udf_xValue,
+ udf_xInverse, S3JniUdf_finalizer);
+ }else{
+ udf_xFunc_f xFunc = 0;
+ udf_xStep_f xStep = 0;
+ udf_xFinal_f xFinal = 0;
+ if( UDF_SCALAR == s->type ){
+ xFunc = udf_xFunc;
+ }else{
+ assert( UDF_AGGREGATE == s->type );
+ xStep = udf_xStep;
+ xFinal = udf_xFinal;
+ }
+ rc = sqlite3_create_function_v2(pDb, zFuncName, nArg, eTextRep, s,
+ xFunc, xStep, xFinal, S3JniUdf_finalizer);
+ }
+ if( 0==rc ){
+ s->zFuncName = sqlite3_mprintf("%s", zFuncName)
+ /* OOM here is non-fatal. Ignore it. Handling it would require
+ re-calling the appropriate create_function() func with 0
+ for all xAbc args so that s would be finalized. */;
+ }
+error_cleanup:
+ JSTR_RELEASE(jFuncName, zFuncName);
+ /* on create_function() error, s will be destroyed via create_function() */
+ return (jint)rc;
+}
+
+JDECL(jint,1create_1function)(JENV_CSELF, jobject jDb, jstring jFuncName,
+ jint nArg, jint eTextRep, jobject jFunctor){
+ return create_function(env, jDb, jFuncName, nArg, eTextRep, jFunctor);
+}
+
+/* sqlite3_db_config() for (int,const char *) */
+JDECL(int,1db_1config__Lorg_sqlite_jni_sqlite3_2ILjava_lang_String_2)(
+ JENV_CSELF, jobject jDb, jint op, jstring jStr
+){
+ S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0);
+ int rc;
+ char *zStr;
+
+ switch( (ps && jStr) ? op : 0 ){
+ case SQLITE_DBCONFIG_MAINDBNAME:
+ zStr = s3jni_jstring_to_utf8(S3JniGlobal_env_cache(env), jStr, 0);
+ if( zStr ){
+ rc = sqlite3_db_config(ps->pDb, (int)op, zStr);
+ if( rc ){
+ sqlite3_free( zStr );
+ }else{
+ sqlite3_free( ps->zMainDbName );
+ ps->zMainDbName = zStr;
+ }
+ }else{
+ rc = SQLITE_NOMEM;
+ }
+ break;
+ default:
+ rc = SQLITE_MISUSE;
+ }
+ return rc;
+}
+
+FIXME_THREADING(perDb)
+/* sqlite3_db_config() for (int,int*) */
+/* ACHTUNG: openjdk v19 creates a different mangled name for this
+ function than openjdk v8 does. */
+JDECL(jint,1db_1config__Lorg_sqlite_jni_sqlite3_2IILorg_sqlite_jni_OutputPointer_Int32_2)(
+ JENV_CSELF, jobject jDb, jint op, jint onOff, jobject jOut
+){
+ S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0);
+ int rc;
+ switch( ps ? op : 0 ){
+ case SQLITE_DBCONFIG_ENABLE_FKEY:
+ case SQLITE_DBCONFIG_ENABLE_TRIGGER:
+ case SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER:
+ case SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION:
+ case SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE:
+ case SQLITE_DBCONFIG_ENABLE_QPSG:
+ case SQLITE_DBCONFIG_TRIGGER_EQP:
+ case SQLITE_DBCONFIG_RESET_DATABASE:
+ case SQLITE_DBCONFIG_DEFENSIVE:
+ case SQLITE_DBCONFIG_WRITABLE_SCHEMA:
+ case SQLITE_DBCONFIG_LEGACY_ALTER_TABLE:
+ case SQLITE_DBCONFIG_DQS_DML:
+ case SQLITE_DBCONFIG_DQS_DDL:
+ case SQLITE_DBCONFIG_ENABLE_VIEW:
+ case SQLITE_DBCONFIG_LEGACY_FILE_FORMAT:
+ case SQLITE_DBCONFIG_TRUSTED_SCHEMA:
+ case SQLITE_DBCONFIG_STMT_SCANSTATUS:
+ case SQLITE_DBCONFIG_REVERSE_SCANORDER: {
+ int pOut = 0;
+ rc = sqlite3_db_config( ps->pDb, (int)op, onOff, &pOut );
+ if( 0==rc && jOut ){
+ OutputPointer_set_Int32(env, jOut, pOut);
+ }
+ break;
+ }
+ default:
+ rc = SQLITE_MISUSE;
+ }
+ return (jint)rc;
+}
+
+/**
+ This is a workaround for openjdk v19 (and possibly others) encoding
+ this function's name differently than JDK v8 does. If we do not
+ install both names for this function then Java will not be able to
+ find the function in both environments.
+*/
+JDECL(jint,1db_1config__Lorg_sqlite_jni_sqlite3_2IILorg_sqlite_jni_OutputPointer_00024Int32_2)(
+ JENV_CSELF, jobject jDb, jint op, jint onOff, jobject jOut
+){
+ return JFuncName(1db_1config__Lorg_sqlite_jni_sqlite3_2IILorg_sqlite_jni_OutputPointer_Int32_2)(
+ env, jKlazz, jDb, op, onOff, jOut
+ );
+}
+
+JDECL(jstring,1db_1filename)(JENV_CSELF, jobject jDb, jstring jDbName){
+ S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0);
+ S3JniEnvCache * const jc = S3JniGlobal_env_cache(env);
+ char *zDbName;
+ jstring jRv = 0;
+ int nStr = 0;
+
+ if( !ps || !jDbName ){
+ return 0;
+ }
+ zDbName = s3jni_jstring_to_utf8(jc, jDbName, &nStr);
+ if( zDbName ){
+ char const * zRv = sqlite3_db_filename(ps->pDb, zDbName);
+ sqlite3_free(zDbName);
+ if( zRv ){
+ jRv = s3jni_utf8_to_jstring(jc, zRv, -1);
+ }
+ }
+ return jRv;
+}
+
+
+JDECL(jint,1db_1status)(JENV_CSELF, jobject jDb, jint op, jobject jOutCurrent,
+ jobject jOutHigh, jboolean reset ){
+ int iCur = 0, iHigh = 0;
+ sqlite3 * const pDb = PtrGet_sqlite3(jDb);
+ int rc = sqlite3_db_status( pDb, op, &iCur, &iHigh, reset );
+ if( 0==rc ){
+ OutputPointer_set_Int32(env, jOutCurrent, iCur);
+ OutputPointer_set_Int32(env, jOutHigh, iHigh);
+ }
+ return (jint)rc;
+}
+
+
+JDECL(jint,1errcode)(JENV_CSELF, jobject jpDb){
+ sqlite3 * const pDb = PtrGet_sqlite3(jpDb);
+ return pDb ? sqlite3_errcode(pDb) : SQLITE_MISUSE;
+}
+
+JDECL(jstring,1errmsg)(JENV_CSELF, jobject jpDb){
+ sqlite3 * const pDb = PtrGet_sqlite3(jpDb);
+ S3JniEnvCache * const jc = pDb ? S3JniGlobal_env_cache(env) : 0;
+ return jc ? s3jni_utf8_to_jstring(jc, sqlite3_errmsg(pDb), -1) : 0;
+}
+
+JDECL(jstring,1errstr)(JENV_CSELF, jint rcCode){
+ return (*env)->NewStringUTF(env, sqlite3_errstr((int)rcCode))
+ /* We know these values to be plain ASCII, so pose no
+ MUTF-8 incompatibility */;
+}
+
+JDECL(jstring,1expanded_1sql)(JENV_CSELF, jobject jpStmt){
+ sqlite3_stmt * const pStmt = PtrGet_sqlite3_stmt(jpStmt);
+ jstring rv = 0;
+ if( pStmt ){
+ S3JniEnvCache * const jc = S3JniGlobal_env_cache(env);
+ char * zSql = sqlite3_expanded_sql(pStmt);
+ OOM_CHECK(zSql);
+ if( zSql ){
+ rv = s3jni_utf8_to_jstring(jc, zSql, -1);
+ sqlite3_free(zSql);
+ }
+ }
+ return rv;
+}
+
+JDECL(jboolean,1extended_1result_1codes)(JENV_CSELF, jobject jpDb,
+ jboolean onoff){
+ int const rc = sqlite3_extended_result_codes(PtrGet_sqlite3(jpDb), onoff ? 1 : 0);
+ return rc ? JNI_TRUE : JNI_FALSE;
+}
+
+JDECL(jint,1initialize)(JENV_CSELF){
+ return sqlite3_initialize();
+}
+
+JDECL(jint,1finalize)(JENV_CSELF, jobject jpStmt){
+ int rc = 0;
+ sqlite3_stmt * const pStmt = PtrGet_sqlite3_stmt(jpStmt);
+ if( pStmt ){
+ rc = sqlite3_finalize(pStmt);
+ NativePointerHolder_set(env, jpStmt, 0, S3JniClassNames.sqlite3_stmt);
+ }
+ return rc;
+}
+
+
+JDECL(jlong,1last_1insert_1rowid)(JENV_CSELF, jobject jpDb){
+ return (jlong)sqlite3_last_insert_rowid(PtrGet_sqlite3(jpDb));
+}
+
+//! Pre-open() code common to sqlite3_open(_v2)().
+static int s3jni_open_pre(JNIEnv * const env, S3JniEnvCache **jc,
+ jstring jDbName, char **zDbName,
+ S3JniDb ** ps, jobject *jDb){
+ *jc = S3JniGlobal_env_cache(env);
+ if(!*jc) return SQLITE_NOMEM;
+ *zDbName = jDbName ? s3jni_jstring_to_utf8(*jc, jDbName, 0) : 0;
+ if(jDbName && !*zDbName) return SQLITE_NOMEM;
+ *jDb = new_sqlite3_wrapper(env, 0);
+ if( !*jDb ){
+ sqlite3_free(*zDbName);
+ *zDbName = 0;
+ return SQLITE_NOMEM;
+ }
+ *ps = S3JniDb_alloc(env, 0, *jDb);
+#if S3JNI_ENABLE_AUTOEXT
+ if(*ps){
+ assert(!S3JniGlobal.autoExt.psOpening);
+ S3JniGlobal.autoExt.psOpening = *ps;
+ }
+#endif
+ //MARKER(("pre-open ps@%p\n", *ps));
+ return *ps ? 0 : SQLITE_NOMEM;
+}
+
+/**
+ Post-open() code common to both the sqlite3_open() and
+ sqlite3_open_v2() bindings. ps->jDb must be the
+ org.sqlite.jni.sqlite3 object which will hold the db's native
+ pointer. theRc must be the result code of the open() op. If
+ *ppDb is NULL then ps is set aside and its state cleared,
+ else ps is associated with *ppDb. If *ppDb is not NULL then
+ ps->jDb is stored in jOut (an OutputPointer.sqlite3 instance).
+
+ Returns theRc.
+*/
+static int s3jni_open_post(JNIEnv * const env, S3JniDb * ps,
+ sqlite3 **ppDb, jobject jOut, int theRc){
+ //MARKER(("post-open() ps@%p db@%p\n", ps, *ppDb));
+#if S3JNI_ENABLE_AUTOEXT
+ assert( S3JniGlobal.autoExt.pHead ? ps!=S3JniGlobal.autoExt.psOpening : 1 );
+ S3JniGlobal.autoExt.psOpening = 0;
+#endif
+ if(*ppDb){
+ assert(ps->jDb);
+#if S3JNI_ENABLE_AUTOEXT
+ //MARKER(("*autoExt.pHead=%p, ppDb=%p, ps->pDb=%p\n", S3JniGlobal.autoExt.pHead, *ppDb, ps->pDb));
+ // invalid when an autoext triggers another open():
+ // assert( S3JniGlobal.autoExt.pHead ? *ppDb==ps->pDb : 0==ps->pDb );
+#endif
+ ps->pDb = *ppDb;
+ NativePointerHolder_set(env, ps->jDb, *ppDb, S3JniClassNames.sqlite3);
+ }else{
+ S3JniDb_set_aside(ps);
+ ps = 0;
+ }
+ OutputPointer_set_sqlite3(env, jOut, ps ? ps->jDb : 0);
+ return theRc;
+}
+
+JDECL(jint,1open)(JENV_CSELF, jstring strName, jobject jOut){
+ sqlite3 * pOut = 0;
+ char *zName = 0;
+ jobject jDb = 0;
+ S3JniDb * ps = 0;
+ S3JniEnvCache * jc = 0;
+ S3JniDb * const prevOpening = S3JniGlobal.autoExt.psOpening;
+ int rc = s3jni_open_pre(env, &jc, strName, &zName, &ps, &jDb);
+ if( 0==rc ){
+ rc = sqlite3_open(zName, &pOut);
+ //MARKER(("env=%p, *env=%p\n", env, *env));
+ //MARKER(("open() ps@%p db@%p\n", ps, pOut));
+ rc = s3jni_open_post(env, ps, &pOut, jOut, rc);
+ assert(rc==0 ? pOut!=0 : 1);
+ sqlite3_free(zName);
+ }
+ S3JniGlobal.autoExt.psOpening = prevOpening;
+ return (jint)rc;
+}
+
+JDECL(jint,1open_1v2)(JENV_CSELF, jstring strName,
+ jobject jOut, jint flags, jstring strVfs){
+ sqlite3 * pOut = 0;
+ char *zName = 0;
+ jobject jDb = 0;
+ S3JniDb * ps = 0;
+ S3JniEnvCache * jc = 0;
+ char *zVfs = 0;
+ S3JniDb * const prevOpening = S3JniGlobal.autoExt.psOpening;
+ int rc = s3jni_open_pre(env, &jc, strName, &zName, &ps, &jDb);
+ if( 0==rc && strVfs ){
+ zVfs = s3jni_jstring_to_utf8(jc, strVfs, 0);
+ if( !zVfs ){
+ rc = SQLITE_NOMEM;
+ }
+ }
+ if( 0==rc ){
+ rc = sqlite3_open_v2(zName, &pOut, (int)flags, zVfs);
+ }
+ //MARKER(("open_v2() ps@%p db@%p\n", ps, pOut));
+ /*MARKER(("zName=%s, zVfs=%s, pOut=%p, flags=%d, nrc=%d\n",
+ zName, zVfs, pOut, (int)flags, nrc));*/
+ rc = s3jni_open_post(env, ps, &pOut, jOut, rc);
+ assert(rc==0 ? pOut!=0 : 1);
+ sqlite3_free(zName);
+ sqlite3_free(zVfs);
+ S3JniGlobal.autoExt.psOpening = prevOpening;
+ return (jint)rc;
+}
+
+/* Proxy for the sqlite3_prepare[_v2/3]() family. */
+static jint sqlite3_jni_prepare_v123(int prepVersion, JNIEnv * const env, jclass self,
+ jobject jDb, jbyteArray baSql,
+ jint nMax, jint prepFlags,
+ jobject jOutStmt, jobject outTail){
+ sqlite3_stmt * pStmt = 0;
+ jobject jStmt = 0;
+ const char * zTail = 0;
+ jbyte * const pBuf = JBA_TOC(baSql);
+ int rc = SQLITE_ERROR;
+ assert(prepVersion==1 || prepVersion==2 || prepVersion==3);
+ if( !pBuf ){
+ rc = baSql ? SQLITE_MISUSE : SQLITE_NOMEM;
+ goto end;
+ }
+ jStmt = new_sqlite3_stmt_wrapper(env, 0);
+ if( !jStmt ){
+ rc = SQLITE_NOMEM;
+ goto end;
+ }
+ switch( prepVersion ){
+ case 1: rc = sqlite3_prepare(PtrGet_sqlite3(jDb), (const char *)pBuf,
+ (int)nMax, &pStmt, &zTail);
+ break;
+ case 2: rc = sqlite3_prepare_v2(PtrGet_sqlite3(jDb), (const char *)pBuf,
+ (int)nMax, &pStmt, &zTail);
+ break;
+ case 3: rc = sqlite3_prepare_v3(PtrGet_sqlite3(jDb), (const char *)pBuf,
+ (int)nMax, (unsigned int)prepFlags,
+ &pStmt, &zTail);
+ break;
+ default:
+ assert(0 && "Invalid prepare() version");
+ }
+end:
+ JBA_RELEASE(baSql,pBuf);
+ if( 0==rc ){
+ if( 0!=outTail ){
+ /* Noting that pBuf is deallocated now but its address is all we need. */
+ assert(zTail ? ((void*)zTail>=(void*)pBuf) : 1);
+ assert(zTail ? (((int)((void*)zTail - (void*)pBuf)) >= 0) : 1);
+ OutputPointer_set_Int32(env, outTail, (int)(zTail ? (zTail - (const char *)pBuf) : 0));
+ }
+ if( pStmt ){
+ NativePointerHolder_set(env, jStmt, pStmt, S3JniClassNames.sqlite3_stmt);
+ }else{
+ /* Happens for comments and whitespace */
+ UNREF_L(jStmt);
+ jStmt = 0;
+ }
+ }else{
+ UNREF_L(jStmt);
+ jStmt = 0;
+ }
+#if 0
+ if( 0!=rc ){
+ MARKER(("prepare rc = %d\n", rc));
+ }
+#endif
+ OutputPointer_set_sqlite3_stmt(env, jOutStmt, jStmt);
+ return (jint)rc;
+}
+JDECL(jint,1prepare)(JNIEnv * const env, jclass self, jobject jDb, jbyteArray baSql,
+ jint nMax, jobject jOutStmt, jobject outTail){
+ return sqlite3_jni_prepare_v123(1, env, self, jDb, baSql, nMax, 0,
+ jOutStmt, outTail);
+}
+JDECL(jint,1prepare_1v2)(JNIEnv * const env, jclass self, jobject jDb, jbyteArray baSql,
+ jint nMax, jobject jOutStmt, jobject outTail){
+ return sqlite3_jni_prepare_v123(2, env, self, jDb, baSql, nMax, 0,
+ jOutStmt, outTail);
+}
+JDECL(jint,1prepare_1v3)(JNIEnv * const env, jclass self, jobject jDb, jbyteArray baSql,
+ jint nMax, jint prepFlags, jobject jOutStmt, jobject outTail){
+ return sqlite3_jni_prepare_v123(3, env, self, jDb, baSql, nMax,
+ prepFlags, jOutStmt, outTail);
+}
+
+
+static int s3jni_progress_handler_impl(void *pP){
+ S3JniDb * const ps = (S3JniDb *)pP;
+ JNIEnv * const env = ps->env;
+ int rc = (int)(*env)->CallIntMethod(env, ps->progress.jObj,
+ ps->progress.midCallback);
+ IFTHREW{
+ rc = s3jni_db_exception(env, ps, rc,
+ "sqlite3_progress_handler() callback threw");
+ }
+ return rc;
+}
+
+JDECL(void,1progress_1handler)(JENV_CSELF,jobject jDb, jint n, jobject jProgress){
+ S3JniDb * ps = S3JniDb_for_db(env, jDb, 0, 0);
+ jclass klazz;
+ jmethodID xCallback;
+ if( n<1 || !jProgress ){
+ if(ps){
+ UNREF_G(ps->progress.jObj);
+ memset(&ps->progress, 0, sizeof(ps->progress));
+ }
+ sqlite3_progress_handler(ps->pDb, 0, 0, 0);
+ return;
+ }
+ if(!ps){
+ s3jni_db_error(ps->pDb, SQLITE_NOMEM, 0);
+ return;
+ }
+ klazz = (*env)->GetObjectClass(env, jProgress);
+ xCallback = (*env)->GetMethodID(env, klazz, "xCallback", "()I");
+ IFTHREW {
+ EXCEPTION_CLEAR;
+ s3jni_db_error(ps->pDb, SQLITE_ERROR,
+ "Cannot not find matching xCallback() on "
+ "ProgressHandler object.");
+ }else{
+ UNREF_G(ps->progress.jObj);
+ ps->progress.midCallback = xCallback;
+ ps->progress.jObj = REF_G(jProgress);
+ sqlite3_progress_handler(ps->pDb, (int)n, s3jni_progress_handler_impl, ps);
+ }
+}
+
+
+JDECL(jint,1reset)(JENV_CSELF, jobject jpStmt){
+ int rc = 0;
+ sqlite3_stmt * const pStmt = PtrGet_sqlite3_stmt(jpStmt);
+ if( pStmt ){
+ rc = sqlite3_reset(pStmt);
+ }
+ return rc;
+}
+
+#if S3JNI_ENABLE_AUTOEXT
+JDECL(void,1reset_1auto_1extension)(JENV_CSELF){
+ if(!S3JniGlobal.autoExt.isRunning){
+ while( S3JniGlobal.autoExt.pHead ){
+ S3JniAutoExtension_free(env, S3JniGlobal.autoExt.pHead);
+ }
+ }
+}
+#endif /* S3JNI_ENABLE_AUTOEXT */
+
+/* sqlite3_result_text/blob() and friends. */
+static void result_blob_text(int asBlob, int as64,
+ int eTextRep/*only for (asBlob=0)*/,
+ JNIEnv * const env, sqlite3_context *pCx,
+ jbyteArray jBa, jlong nMax){
+ if(jBa){
+ jbyte * const pBuf = JBA_TOC(jBa);
+ jsize nBa = (*env)->GetArrayLength(env, jBa);
+ if( nMax>=0 && nBa>(jsize)nMax ){
+ nBa = (jsize)nMax;
+ /**
+ From the sqlite docs:
+
+ > If the 3rd parameter to any of the sqlite3_result_text*
+ interfaces other than sqlite3_result_text64() is negative,
+ then SQLite computes the string length itself by searching
+ the 2nd parameter for the first zero character.
+
+ Note that the text64() interfaces take an unsigned value for
+ the length, which Java does not support. This binding takes
+ the approach of passing on negative values to the C API,
+ which will, in turn fail with SQLITE_TOOBIG at some later
+ point (recall that the sqlite3_result_xyz() family do not
+ have result values).
+ */
+ }
+ if(as64){ /* 64-bit... */
+ static const jsize nLimit64 =
+ SQLITE_MAX_ALLOCATION_SIZE/*only _kinda_ arbitrary!*/
+ /* jsize is int32, not int64! */;
+ if(nBa > nLimit64){
+ sqlite3_result_error_toobig(pCx);
+ }else if(asBlob){
+ sqlite3_result_blob64(pCx, pBuf, (sqlite3_uint64)nBa,
+ SQLITE_TRANSIENT);
+ }else{ /* text64... */
+ if(encodingTypeIsValid(eTextRep)){
+ sqlite3_result_text64(pCx, (const char *)pBuf,
+ (sqlite3_uint64)nBa,
+ SQLITE_TRANSIENT, eTextRep);
+ }else{
+ sqlite3_result_error_code(pCx, SQLITE_FORMAT);
+ }
+ }
+ }else{ /* 32-bit... */
+ static const jsize nLimit = SQLITE_MAX_ALLOCATION_SIZE;
+ if(nBa > nLimit){
+ sqlite3_result_error_toobig(pCx);
+ }else if(asBlob){
+ sqlite3_result_blob(pCx, pBuf, (int)nBa,
+ SQLITE_TRANSIENT);
+ }else{
+ switch(eTextRep){
+ case SQLITE_UTF8:
+ sqlite3_result_text(pCx, (const char *)pBuf, (int)nBa,
+ SQLITE_TRANSIENT);
+ break;
+ case SQLITE_UTF16:
+ sqlite3_result_text16(pCx, (const char *)pBuf, (int)nBa,
+ SQLITE_TRANSIENT);
+ break;
+ case SQLITE_UTF16LE:
+ sqlite3_result_text16le(pCx, (const char *)pBuf, (int)nBa,
+ SQLITE_TRANSIENT);
+ break;
+ case SQLITE_UTF16BE:
+ sqlite3_result_text16be(pCx, (const char *)pBuf, (int)nBa,
+ SQLITE_TRANSIENT);
+ break;
+ }
+ }
+ JBA_RELEASE(jBa, pBuf);
+ }
+ }else{
+ sqlite3_result_null(pCx);
+ }
+}
+
+JDECL(void,1result_1blob)(JENV_CSELF, jobject jpCx, jbyteArray jBa, jint nMax){
+ return result_blob_text(1, 0, 0, env, PtrGet_sqlite3_context(jpCx), jBa, nMax);
+}
+
+JDECL(void,1result_1blob64)(JENV_CSELF, jobject jpCx, jbyteArray jBa, jlong nMax){
+ return result_blob_text(1, 1, 0, env, PtrGet_sqlite3_context(jpCx), jBa, nMax);
+}
+
+JDECL(void,1result_1double)(JENV_CSELF, jobject jpCx, jdouble v){
+ sqlite3_result_double(PtrGet_sqlite3_context(jpCx), v);
+}
+
+JDECL(void,1result_1error)(JENV_CSELF, jobject jpCx, jbyteArray baMsg,
+ int eTextRep){
+ const char * zUnspecified = "Unspecified error.";
+ jsize const baLen = (*env)->GetArrayLength(env, baMsg);
+ jbyte * const pjBuf = baMsg ? JBA_TOC(baMsg) : NULL;
+ switch(pjBuf ? eTextRep : SQLITE_UTF8){
+ case SQLITE_UTF8: {
+ const char *zMsg = pjBuf ? (const char *)pjBuf : zUnspecified;
+ sqlite3_result_error(PtrGet_sqlite3_context(jpCx), zMsg, (int)baLen);
+ break;
+ }
+ case SQLITE_UTF16: {
+ const void *zMsg = pjBuf
+ ? (const void *)pjBuf : (const void *)zUnspecified;
+ sqlite3_result_error16(PtrGet_sqlite3_context(jpCx), zMsg, (int)baLen);
+ break;
+ }
+ default:
+ sqlite3_result_error(PtrGet_sqlite3_context(jpCx),
+ "Invalid encoding argument passed "
+ "to sqlite3_result_error().", -1);
+ break;
+ }
+ JBA_RELEASE(baMsg,pjBuf);
+}
+
+JDECL(void,1result_1error_1code)(JENV_CSELF, jobject jpCx, jint v){
+ sqlite3_result_error_code(PtrGet_sqlite3_context(jpCx), v ? (int)v : SQLITE_ERROR);
+}
+
+JDECL(void,1result_1error_1nomem)(JENV_CSELF, jobject jpCx){
+ sqlite3_result_error_nomem(PtrGet_sqlite3_context(jpCx));
+}
+
+JDECL(void,1result_1error_1toobig)(JENV_CSELF, jobject jpCx){
+ sqlite3_result_error_toobig(PtrGet_sqlite3_context(jpCx));
+}
+
+JDECL(void,1result_1int)(JENV_CSELF, jobject jpCx, jint v){
+ sqlite3_result_int(PtrGet_sqlite3_context(jpCx), (int)v);
+}
+
+JDECL(void,1result_1int64)(JENV_CSELF, jobject jpCx, jlong v){
+ sqlite3_result_int64(PtrGet_sqlite3_context(jpCx), (sqlite3_int64)v);
+}
+
+JDECL(void,1result_1java_1object)(JENV_CSELF, jobject jpCx, jobject v){
+ if(v){
+ ResultJavaVal * const rjv = ResultJavaVal_alloc(env, v);
+ if(rjv){
+ sqlite3_result_pointer(PtrGet_sqlite3_context(jpCx), rjv, RESULT_JAVA_VAL_STRING,
+ ResultJavaVal_finalizer);
+ }else{
+ sqlite3_result_error_nomem(PtrGet_sqlite3_context(jpCx));
+ }
+ }else{
+ sqlite3_result_null(PtrGet_sqlite3_context(jpCx));
+ }
+}
+
+JDECL(void,1result_1null)(JENV_CSELF, jobject jpCx){
+ sqlite3_result_null(PtrGet_sqlite3_context(jpCx));
+}
+
+JDECL(void,1result_1text)(JENV_CSELF, jobject jpCx, jbyteArray jBa, jint nMax){
+ return result_blob_text(0, 0, SQLITE_UTF8, env, PtrGet_sqlite3_context(jpCx), jBa, nMax);
+}
+
+JDECL(void,1result_1text64)(JENV_CSELF, jobject jpCx, jbyteArray jBa, jlong nMax,
+ jint eTextRep){
+ return result_blob_text(0, 1, eTextRep, env, PtrGet_sqlite3_context(jpCx), jBa, nMax);
+}
+
+JDECL(void,1result_1value)(JENV_CSELF, jobject jpCx, jobject jpSVal){
+ sqlite3_result_value(PtrGet_sqlite3_context(jpCx), PtrGet_sqlite3_value(jpSVal));
+}
+
+JDECL(void,1result_1zeroblob)(JENV_CSELF, jobject jpCx, jint v){
+ sqlite3_result_zeroblob(PtrGet_sqlite3_context(jpCx), (int)v);
+}
+
+JDECL(jint,1result_1zeroblob64)(JENV_CSELF, jobject jpCx, jlong v){
+ return (jint)sqlite3_result_zeroblob64(PtrGet_sqlite3_context(jpCx), (sqlite3_int64)v);
+}
+
+JDECL(jobject,1rollback_1hook)(JENV_CSELF,jobject jDb, jobject jHook){
+ return s3jni_commit_rollback_hook(0, env, jDb, jHook);
+}
+
+/* sqlite3_set_authorizer() callback proxy. */
+static int s3jni_xAuth(void* pState, int op,const char*z0, const char*z1,
+ const char*z2,const char*z3){
+ S3JniDb * const ps = pState;
+ JNIEnv * const env = ps->env;
+ jstring const s0 = z0 ? (*env)->NewStringUTF(env, z0) : 0;
+ jstring const s1 = z1 ? (*env)->NewStringUTF(env, z1) : 0;
+ jstring const s2 = z2 ? (*env)->NewStringUTF(env, z2) : 0;
+ jstring const s3 = z3 ? (*env)->NewStringUTF(env, z3) : 0;
+ S3JniHook const * const pHook = &ps->authHook;
+ int rc;
+
+ assert( pHook->jObj );
+ rc = (*env)->CallIntMethod(env, pHook->jObj, pHook->midCallback, (jint)op,
+ s0, s1, s3, s3);
+ IFTHREW{
+ EXCEPTION_WARN_CALLBACK_THREW("sqlite3_set_authorizer() callback");
+ EXCEPTION_CLEAR;
+ }
+ UNREF_L(s0);
+ UNREF_L(s1);
+ UNREF_L(s2);
+ UNREF_L(s3);
+ return rc;
+}
+
+JDECL(jint,1set_1authorizer)(JENV_CSELF,jobject jDb, jobject jHook){
+ S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0);
+ S3JniHook * const pHook = ps ? &ps->authHook : 0;
+
+ if( !ps ) return SQLITE_MISUSE;
+ else if( !jHook ){
+ S3JniHook_unref(env, pHook, 0);
+ return (jint)sqlite3_set_authorizer( ps->pDb, 0, 0 );
+ }else{
+ int rc = 0;
+ if( pHook->jObj ){
+ if( (*env)->IsSameObject(env, pHook->jObj, jHook) ){
+ /* Same object - this is a no-op. */
+ return 0;
+ }
+ S3JniHook_unref(env, pHook, 0);
+ }
+ pHook->jObj = REF_G(jHook);
+ pHook->klazz = REF_G((*env)->GetObjectClass(env, jHook));
+ pHook->midCallback = (*env)->GetMethodID(env, pHook->klazz,
+ "xAuth",
+ "(I"
+ "Ljava/lang/String;"
+ "Ljava/lang/String;"
+ "Ljava/lang/String;"
+ "Ljava/lang/String;"
+ ")I");
+ IFTHREW {
+ S3JniHook_unref(env, pHook, 0);
+ return s3jni_db_error(ps->pDb, SQLITE_ERROR,
+ "Error setting up Java parts of authorizer hook.");
+ }
+ rc = sqlite3_set_authorizer(ps->pDb, s3jni_xAuth, ps);
+ if( rc ) S3JniHook_unref(env, pHook, 0);
+ return rc;
+ }
+}
+
+
+JDECL(void,1set_1last_1insert_1rowid)(JENV_CSELF, jobject jpDb, jlong rowId){
+ sqlite3_set_last_insert_rowid(PtrGet_sqlite3_context(jpDb),
+ (sqlite3_int64)rowId);
+}
+
+FIXME_THREADING(nphCache)
+JDECL(jint,1status)(JENV_CSELF, jint op, jobject jOutCurrent, jobject jOutHigh,
+ jboolean reset ){
+ int iCur = 0, iHigh = 0;
+ int rc = sqlite3_status( op, &iCur, &iHigh, reset );
+ if( 0==rc ){
+ OutputPointer_set_Int32(env, jOutCurrent, iCur);
+ OutputPointer_set_Int32(env, jOutHigh, iHigh);
+ }
+ return (jint)rc;
+}
+
+FIXME_THREADING(nphCache)
+JDECL(jint,1status64)(JENV_CSELF, jint op, jobject jOutCurrent, jobject jOutHigh,
+ jboolean reset ){
+ sqlite3_int64 iCur = 0, iHigh = 0;
+ int rc = sqlite3_status64( op, &iCur, &iHigh, reset );
+ if( 0==rc ){
+ OutputPointer_set_Int64(env, jOutCurrent, iCur);
+ OutputPointer_set_Int64(env, jOutHigh, iHigh);
+ }
+ return (jint)rc;
+}
+
+static int s3jni_strlike_glob(int isLike, JNIEnv *const env,
+ jbyteArray baG, jbyteArray baT, jint escLike){
+ int rc = 0;
+ jbyte * const pG = JBA_TOC(baG);
+ jbyte * const pT = pG ? JBA_TOC(baT) : 0;
+ OOM_CHECK(pT);
+
+ /* Note that we're relying on the byte arrays having been
+ NUL-terminated on the Java side. */
+ rc = isLike
+ ? sqlite3_strlike((const char *)pG, (const char *)pT,
+ (unsigned int)escLike)
+ : sqlite3_strglob((const char *)pG, (const char *)pT);
+ JBA_RELEASE(baG, pG);
+ JBA_RELEASE(baT, pT);
+ return rc;
+}
+
+JDECL(jint,1strglob)(JENV_CSELF, jbyteArray baG, jbyteArray baT){
+ return s3jni_strlike_glob(0, env, baG, baT, 0);
+}
+
+JDECL(jint,1strlike)(JENV_CSELF, jbyteArray baG, jbyteArray baT, jint escChar){
+ return s3jni_strlike_glob(1, env, baG, baT, escChar);
+}
+
+JDECL(jint,1shutdown)(JENV_CSELF){
+ S3JniGlobal_S3JniEnvCache_clear();
+ /* Do not clear S3JniGlobal.jvm: it's legal to call
+ sqlite3_initialize() again to restart the lib. */
+ return sqlite3_shutdown();
+}
+
+JDECL(jstring,1sql)(JENV_CSELF, jobject jpStmt){
+ sqlite3_stmt * const pStmt = PtrGet_sqlite3_stmt(jpStmt);
+ jstring rv = 0;
+ if( pStmt ){
+ const char * zSql = 0;
+ S3JniEnvCache * const jc = S3JniGlobal_env_cache(env);
+ zSql = sqlite3_sql(pStmt);
+ rv = s3jni_utf8_to_jstring(jc, zSql, -1);
+ OOM_CHECK(rv);
+ }
+ return rv;
+}
+
+JDECL(jint,1step)(JENV_CSELF,jobject jStmt){
+ int rc = SQLITE_MISUSE;
+ sqlite3_stmt * const pStmt = PtrGet_sqlite3_stmt(jStmt);
+ if(pStmt){
+ rc = sqlite3_step(pStmt);
+ }
+ return rc;
+}
+
+static int s3jni_trace_impl(unsigned traceflag, void *pC, void *pP, void *pX){
+ S3JniDb * const ps = (S3JniDb *)pC;
+ JNIEnv * const env = ps->env;
+ jobject jX = NULL /* the tracer's X arg */;
+ jobject jP = NULL /* the tracer's P arg */;
+ jobject jPUnref = NULL /* potentially a local ref to jP */;
+ S3JniEnvCache * const jc = S3JniGlobal_env_cache(env);
+ int rc;
+ int createStmt = 0;
+ switch(traceflag){
+ case SQLITE_TRACE_STMT:
+ jX = s3jni_utf8_to_jstring(jc, (const char *)pX, -1);
+ if(!jX) return SQLITE_NOMEM;
+ /*MARKER(("TRACE_STMT@%p SQL=%p / %s\n", pP, jX, (const char *)pX));*/
+ createStmt = 1;
+ break;
+ case SQLITE_TRACE_PROFILE:
+ jX = (*env)->NewObject(env, jc->g.cLong, jc->g.ctorLong1,
+ (jlong)*((sqlite3_int64*)pX));
+ // hmm. ^^^ (*pX) really is zero.
+ // MARKER(("profile time = %llu\n", *((sqlite3_int64*)pX)));
+ if(!jX) return SQLITE_NOMEM;
+ createStmt = 1;
+ break;
+ case SQLITE_TRACE_ROW:
+ createStmt = 1;
+ break;
+ case SQLITE_TRACE_CLOSE:
+ jP = ps->jDb;
+ break;
+ default:
+ assert(!"cannot happen - unkown trace flag");
+ return SQLITE_ERROR;
+ }
+ if( createStmt ){
+ jP = jPUnref = new_sqlite3_stmt_wrapper(env, pP);
+ if(!jP){
+ UNREF_L(jX);
+ return SQLITE_NOMEM;
+ }
+ }
+ assert(jP);
+ rc = (int)(*env)->CallIntMethod(env, ps->trace.jObj,
+ ps->trace.midCallback,
+ (jint)traceflag, jP, jX);
+ IFTHREW{
+ EXCEPTION_WARN_CALLBACK_THREW("sqlite3_trace_v2() callback");
+ EXCEPTION_CLEAR;
+ rc = SQLITE_ERROR;
+ }
+ UNREF_L(jPUnref);
+ UNREF_L(jX);
+ return rc;
+}
+
+JDECL(jint,1trace_1v2)(JENV_CSELF,jobject jDb, jint traceMask, jobject jTracer){
+ S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0);
+ jclass klazz;
+ if( !traceMask || !jTracer ){
+ if(ps){
+ UNREF_G(ps->trace.jObj);
+ memset(&ps->trace, 0, sizeof(ps->trace));
+ }
+ return (jint)sqlite3_trace_v2(ps->pDb, 0, 0, 0);
+ }
+ if(!ps) return SQLITE_NOMEM;
+ klazz = (*env)->GetObjectClass(env, jTracer);
+ ps->trace.midCallback = (*env)->GetMethodID(env, klazz, "xCallback",
+ "(ILjava/lang/Object;Ljava/lang/Object;)I");
+ IFTHREW {
+ EXCEPTION_CLEAR;
+ return s3jni_db_error(ps->pDb, SQLITE_ERROR,
+ "Cannot not find matching xCallback() on Tracer object.");
+ }
+ ps->trace.jObj = REF_G(jTracer);
+ return sqlite3_trace_v2(ps->pDb, (unsigned)traceMask, s3jni_trace_impl, ps);
+}
+
+static void s3jni_update_hook_impl(void * pState, int opId, const char *zDb,
+ const char *zTable, sqlite3_int64 nRowid){
+ S3JniDb * const ps = pState;
+ JNIEnv * const env = ps->env;
+ /* ACHTUNG: this will break if zDb or zTable contain chars which are
+ different in MUTF-8 than UTF-8. That seems like a low risk,
+ but it's possible. */
+ jstring jDbName;
+ jstring jTable;
+ jDbName = (*env)->NewStringUTF(env, zDb);
+ jTable = jDbName ? (*env)->NewStringUTF(env, zTable) : 0;
+ IFTHREW {
+ s3jni_db_error(ps->pDb, SQLITE_NOMEM, 0);
+ }else{
+ (*env)->CallVoidMethod(env, ps->updateHook.jObj,
+ ps->updateHook.midCallback,
+ (jint)opId, jDbName, jTable, (jlong)nRowid);
+ IFTHREW{
+ EXCEPTION_WARN_CALLBACK_THREW("update hook");
+ EXCEPTION_CLEAR;
+ s3jni_db_error(ps->pDb, SQLITE_ERROR, "update hook callback threw.");
+ }
+ }
+ UNREF_L(jDbName);
+ UNREF_L(jTable);
+}
+
+
+JDECL(jobject,1update_1hook)(JENV_CSELF, jobject jDb, jobject jHook){
+ S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0);
+ jclass klazz;
+ jobject pOld = 0;
+ jmethodID xCallback;
+ S3JniHook * const pHook = &ps->updateHook;
+ if(!ps){
+ s3jni_db_error(ps->pDb, SQLITE_MISUSE, 0);
+ return 0;
+ }
+ pOld = pHook->jObj;
+ if(pOld && jHook &&
+ (*env)->IsSameObject(env, pOld, jHook)){
+ return pOld;
+ }
+ if( !jHook ){
+ if(pOld){
+ jobject tmp = REF_L(pOld);
+ UNREF_G(pOld);
+ pOld = tmp;
+ }
+ memset(pHook, 0, sizeof(S3JniHook));
+ sqlite3_update_hook(ps->pDb, 0, 0);
+ return pOld;
+ }
+ klazz = (*env)->GetObjectClass(env, jHook);
+ xCallback = (*env)->GetMethodID(env, klazz, "xUpdateHook",
+ "(ILjava/lang/String;Ljava/lang/String;J)V");
+ IFTHREW {
+ EXCEPTION_CLEAR;
+ s3jni_db_error(ps->pDb, SQLITE_ERROR,
+ "Cannot not find matching callback on "
+ "update hook object.");
+ }else{
+ pHook->midCallback = xCallback;
+ pHook->jObj = REF_G(jHook);
+ sqlite3_update_hook(ps->pDb, s3jni_update_hook_impl, ps);
+ if(pOld){
+ jobject tmp = REF_L(pOld);
+ UNREF_G(pOld);
+ pOld = tmp;
+ }
+ }
+ return pOld;
+}
+
+
+JDECL(jbyteArray,1value_1blob)(JENV_CSELF, jobject jpSVal){
+ sqlite3_value * const sv = PtrGet_sqlite3_value(jpSVal);
+ int const nLen = sqlite3_value_bytes(sv);
+ const jbyte * pBytes = sqlite3_value_blob(sv);
+ jbyteArray const jba = pBytes
+ ? (*env)->NewByteArray(env, (jsize)nLen)
+ : NULL;
+ if(jba){
+ (*env)->SetByteArrayRegion(env, jba, 0, nLen, pBytes);
+ }
+ return jba;
+}
+
+
+JDECL(jdouble,1value_1double)(JENV_CSELF, jobject jpSVal){
+ return (jdouble) sqlite3_value_double(PtrGet_sqlite3_value(jpSVal));
+}
+
+
+JDECL(jobject,1value_1dup)(JENV_CSELF, jobject jpSVal){
+ sqlite3_value * const sv = sqlite3_value_dup(PtrGet_sqlite3_value(jpSVal));
+ return sv ? new_sqlite3_value_wrapper(env, sv) : 0;
+}
+
+JDECL(void,1value_1free)(JENV_CSELF, jobject jpSVal){
+ sqlite3_value_free(PtrGet_sqlite3_value(jpSVal));
+}
+
+JDECL(jint,1value_1int)(JENV_CSELF, jobject jpSVal){
+ return (jint) sqlite3_value_int(PtrGet_sqlite3_value(jpSVal));
+}
+
+JDECL(jlong,1value_1int64)(JENV_CSELF, jobject jpSVal){
+ return (jlong) sqlite3_value_int64(PtrGet_sqlite3_value(jpSVal));
+}
+
+JDECL(jobject,1value_1java_1object)(JENV_CSELF, jobject jpSVal){
+ ResultJavaVal * const rv = sqlite3_value_pointer(PtrGet_sqlite3_value(jpSVal), RESULT_JAVA_VAL_STRING);
+ return rv ? rv->jObj : NULL;
+}
+
+JDECL(jstring,1value_1text)(JENV_CSELF, jobject jpSVal){
+ sqlite3_value * const sv = PtrGet_sqlite3_value(jpSVal);
+ int const n = sqlite3_value_bytes16(sv);
+ const void * const p = sqlite3_value_text16(sv);
+ return s3jni_text16_to_jstring(env, p, n);
+}
+
+JDECL(jbyteArray,1value_1text_1utf8)(JENV_CSELF, jobject jpSVal){
+ sqlite3_value * const sv = PtrGet_sqlite3_value(jpSVal);
+ int const n = sqlite3_value_bytes(sv);
+ const unsigned char * const p = sqlite3_value_text(sv);
+ return s3jni_new_jbyteArray(env, p, n);
+}
+
+static jbyteArray value_text16(int mode, JNIEnv * const env, jobject jpSVal){
+ int const nLen = sqlite3_value_bytes16(PtrGet_sqlite3_value(jpSVal));
+ jbyteArray jba;
+ const jbyte * pBytes;
+ switch(mode){
+ case SQLITE_UTF16:
+ pBytes = sqlite3_value_text16(PtrGet_sqlite3_value(jpSVal));
+ break;
+ case SQLITE_UTF16LE:
+ pBytes = sqlite3_value_text16le(PtrGet_sqlite3_value(jpSVal));
+ break;
+ case SQLITE_UTF16BE:
+ pBytes = sqlite3_value_text16be(PtrGet_sqlite3_value(jpSVal));
+ break;
+ default:
+ assert(!"not possible");
+ return NULL;
+ }
+ jba = pBytes
+ ? (*env)->NewByteArray(env, (jsize)nLen)
+ : NULL;
+ if(jba){
+ (*env)->SetByteArrayRegion(env, jba, 0, nLen, pBytes);
+ }
+ return jba;
+}
+
+JDECL(jbyteArray,1value_1text16)(JENV_CSELF, jobject jpSVal){
+ return value_text16(SQLITE_UTF16, env, jpSVal);
+}
+
+JDECL(jbyteArray,1value_1text16le)(JENV_CSELF, jobject jpSVal){
+ return value_text16(SQLITE_UTF16LE, env, jpSVal);
+}
+
+JDECL(jbyteArray,1value_1text16be)(JENV_CSELF, jobject jpSVal){
+ return value_text16(SQLITE_UTF16BE, env, jpSVal);
+}
+
+JDECL(void,1do_1something_1for_1developer)(JENV_CSELF){
+ MARKER(("\nVarious bits of internal info:\n"));
+ puts("FTS5 is "
+#ifdef SQLITE_ENABLE_FTS5
+ "available"
+#else
+ "unavailable"
+#endif
+ "."
+ );
+ puts("sizeofs:");
+#define SO(T) printf("\tsizeof(" #T ") = %u\n", (unsigned)sizeof(T))
+ SO(void*);
+ SO(S3JniEnvCache);
+ SO(S3JniHook);
+ SO(S3JniDb);
+ SO(S3JniClassNames);
+ printf("\t(^^^ %u NativePointerHolder subclasses)\n",
+ (unsigned)(sizeof(S3JniClassNames) / sizeof(const char *)));
+ SO(S3JniGlobal);
+ SO(S3JniAutoExtension);
+ SO(S3JniUdf);
+ printf("Cache info:\n");
+ printf("\tNativePointerHolder cache: %u misses, %u hits\n",
+ S3JniGlobal.metrics.nphCacheMisses,
+ S3JniGlobal.metrics.nphCacheHits);
+ printf("\tJNIEnv cache %u misses, %u hits\n",
+ S3JniGlobal.metrics.envCacheMisses,
+ S3JniGlobal.metrics.envCacheHits);
+ puts("Java-side UDF calls:");
+#define UDF(T) printf("\t%-8s = %u\n", "x" #T, S3JniGlobal.metrics.udf.n##T)
+ UDF(Func); UDF(Step); UDF(Final); UDF(Value); UDF(Inverse);
+#undef UDF
+ printf("xDestroy calls across all callback types: %u\n",
+ S3JniGlobal.metrics.nDestroy);
+#undef SO
+}
+
+////////////////////////////////////////////////////////////////////////
+// End of the sqlite3_... API bindings. Next up, FTS5...
+////////////////////////////////////////////////////////////////////////
+#ifdef SQLITE_ENABLE_FTS5
+
+/* Creates a verbose JNI Fts5 function name. */
+#define JFuncNameFtsXA(Suffix) \
+ Java_org_sqlite_jni_Fts5ExtensionApi_ ## Suffix
+#define JFuncNameFtsApi(Suffix) \
+ Java_org_sqlite_jni_fts5_1api_ ## Suffix
+#define JFuncNameFtsTok(Suffix) \
+ Java_org_sqlite_jni_fts5_tokenizer_ ## Suffix
+
+#define JDECLFtsXA(ReturnType,Suffix) \
+ JNIEXPORT ReturnType JNICALL \
+ JFuncNameFtsXA(Suffix)
+#define JDECLFtsApi(ReturnType,Suffix) \
+ JNIEXPORT ReturnType JNICALL \
+ JFuncNameFtsApi(Suffix)
+#define JDECLFtsTok(ReturnType,Suffix) \
+ JNIEXPORT ReturnType JNICALL \
+ JFuncNameFtsTok(Suffix)
+
+#define PtrGet_fts5_api(OBJ) NativePointerHolder_get(env,OBJ,S3JniClassNames.fts5_api)
+#define PtrGet_fts5_tokenizer(OBJ) NativePointerHolder_get(env,OBJ,S3JniClassNames.fts5_tokenizer)
+#define PtrGet_Fts5Context(OBJ) NativePointerHolder_get(env,OBJ,S3JniClassNames.Fts5Context)
+#define PtrGet_Fts5Tokenizer(OBJ) NativePointerHolder_get(env,OBJ,S3JniClassNames.Fts5Tokenizer)
+#define Fts5ExtDecl Fts5ExtensionApi const * const fext = s3jni_ftsext()
+
+/**
+ State for binding Java-side FTS5 auxiliary functions.
+*/
+typedef struct {
+ JNIEnv * env; /* env registered from */;
+ jobject jObj /* functor instance */;
+ jclass klazz /* jObj's class */;
+ jobject jUserData /* 2nd arg to JNI binding of
+ xCreateFunction(), ostensibly the 3rd arg
+ to the lib-level xCreateFunction(), except
+ that we necessarily use that slot for a
+ Fts5JniAux instance. */;
+ char * zFuncName /* Only for error reporting and debug logging */;
+ jmethodID jmid /* callback member's method ID */;
+} Fts5JniAux;
+
+static void Fts5JniAux_free(Fts5JniAux * const s){
+ JNIEnv * const env = s->env;
+ if(env){
+ /*MARKER(("FTS5 aux function cleanup: %s\n", s->zFuncName));*/
+ s3jni_call_xDestroy(env, s->jObj, s->klazz);
+ UNREF_G(s->jObj);
+ UNREF_G(s->klazz);
+ UNREF_G(s->jUserData);
+ }
+ sqlite3_free(s->zFuncName);
+ sqlite3_free(s);
+}
+
+static void Fts5JniAux_xDestroy(void *p){
+ if(p) Fts5JniAux_free(p);
+}
+
+static Fts5JniAux * Fts5JniAux_alloc(JNIEnv * const env, jobject jObj){
+ Fts5JniAux * s = sqlite3_malloc(sizeof(Fts5JniAux));
+ if(s){
+ const char * zSig =
+ "(Lorg/sqlite/jni/Fts5ExtensionApi;"
+ "Lorg/sqlite/jni/Fts5Context;"
+ "Lorg/sqlite/jni/sqlite3_context;"
+ "[Lorg/sqlite/jni/sqlite3_value;)V";
+ memset(s, 0, sizeof(Fts5JniAux));
+ s->env = env;
+ s->jObj = REF_G(jObj);
+ s->klazz = REF_G((*env)->GetObjectClass(env, jObj));
+ s->jmid = (*env)->GetMethodID(env, s->klazz, "xFunction", zSig);
+ IFTHREW{
+ EXCEPTION_REPORT;
+ EXCEPTION_CLEAR;
+ Fts5JniAux_free(s);
+ s = 0;
+ }
+ }
+ return s;
+}
+
+static inline Fts5ExtensionApi const * s3jni_ftsext(void){
+ return &sFts5Api/*singleton from sqlite3.c*/;
+}
+
+static inline jobject new_Fts5Context_wrapper(JNIEnv * const env, Fts5Context *sv){
+ return new_NativePointerHolder_object(env, S3JniClassNames.Fts5Context, sv);
+}
+static inline jobject new_fts5_api_wrapper(JNIEnv * const env, fts5_api *sv){
+ return new_NativePointerHolder_object(env, S3JniClassNames.fts5_api, sv);
+}
+
+/**
+ Returns a per-JNIEnv global ref to the Fts5ExtensionApi singleton
+ instance, or NULL on OOM.
+*/
+static jobject s3jni_getFts5ExensionApi(JNIEnv * const env){
+ S3JniEnvCache * const row = S3JniGlobal_env_cache(env);
+ if( !row->jFtsExt ){
+ row->jFtsExt = new_NativePointerHolder_object(env, S3JniClassNames.Fts5ExtensionApi,
+ s3jni_ftsext());
+ if(row->jFtsExt) row->jFtsExt = REF_G(row->jFtsExt);
+ }
+ return row->jFtsExt;
+}
+
+/*
+** Return a pointer to the fts5_api instance for database connection
+** db. If an error occurs, return NULL and leave an error in the
+** database handle (accessible using sqlite3_errcode()/errmsg()).
+*/
+static fts5_api *s3jni_fts5_api_from_db(sqlite3 *db){
+ fts5_api *pRet = 0;
+ sqlite3_stmt *pStmt = 0;
+ if( SQLITE_OK==sqlite3_prepare(db, "SELECT fts5(?1)", -1, &pStmt, 0) ){
+ sqlite3_bind_pointer(pStmt, 1, (void*)&pRet, "fts5_api_ptr", NULL);
+ sqlite3_step(pStmt);
+ }
+ sqlite3_finalize(pStmt);
+ return pRet;
+}
+
+JDECLFtsApi(jobject,getInstanceForDb)(JENV_CSELF,jobject jDb){
+ S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0);
+ jobject rv = 0;
+ if(!ps) return 0;
+ else if(ps->jFtsApi){
+ rv = ps->jFtsApi;
+ }else{
+ fts5_api * const pApi = s3jni_fts5_api_from_db(ps->pDb);
+ if( pApi ){
+ rv = new_fts5_api_wrapper(env, pApi);
+ ps->jFtsApi = rv ? REF_G(rv) : 0;
+ }
+ }
+ return rv;
+}
+
+
+JDECLFtsXA(jobject,getInstance)(JENV_CSELF){
+ return s3jni_getFts5ExensionApi(env);
+}
+
+JDECLFtsXA(jint,xColumnCount)(JENV_OSELF,jobject jCtx){
+ Fts5ExtDecl;
+ return (jint)fext->xColumnCount(PtrGet_Fts5Context(jCtx));
+}
+
+JDECLFtsXA(jint,xColumnSize)(JENV_OSELF,jobject jCtx, jint iIdx, jobject jOut32){
+ Fts5ExtDecl;
+ int n1 = 0;
+ int const rc = fext->xColumnSize(PtrGet_Fts5Context(jCtx), (int)iIdx, &n1);
+ if( 0==rc ) OutputPointer_set_Int32(env, jOut32, n1);
+ return rc;
+}
+
+JDECLFtsXA(jint,xColumnText)(JENV_OSELF,jobject jCtx, jint iCol,
+ jobject jOut){
+ Fts5ExtDecl;
+ const char *pz = 0;
+ int pn = 0;
+ int rc = fext->xColumnText(PtrGet_Fts5Context(jCtx), (int)iCol,
+ &pz, &pn);
+ if( 0==rc ){
+ S3JniEnvCache * const jc = S3JniGlobal_env_cache(env);
+ jstring jstr = pz ? s3jni_utf8_to_jstring(jc, pz, pn) : 0;
+ if( pz ){
+ if( jstr ){
+ OutputPointer_set_String(env, jOut, jstr);
+ UNREF_L(jstr)/*jOut has a reference*/;
+ }else{
+ rc = SQLITE_NOMEM;
+ }
+ }
+ }
+ return (jint)rc;
+}
+
+JDECLFtsXA(jint,xColumnTotalSize)(JENV_OSELF,jobject jCtx, jint iCol, jobject jOut64){
+ Fts5ExtDecl;
+ sqlite3_int64 nOut = 0;
+ int const rc = fext->xColumnTotalSize(PtrGet_Fts5Context(jCtx), (int)iCol, &nOut);
+ if( 0==rc && jOut64 ) OutputPointer_set_Int64(env, jOut64, (jlong)nOut);
+ return (jint)rc;
+}
+
+/**
+ Proxy for fts5_extension_function instances plugged in via
+ fts5_api::xCreateFunction().
+*/
+static void s3jni_fts5_extension_function(Fts5ExtensionApi const *pApi,
+ Fts5Context *pFts,
+ sqlite3_context *pCx,
+ int argc,
+ sqlite3_value **argv){
+ Fts5JniAux * const pAux = pApi->xUserData(pFts);
+ JNIEnv *env;
+ jobject jpCx = 0;
+ jobjectArray jArgv = 0;
+ jobject jpFts = 0;
+ jobject jFXA;
+ int rc;
+ assert(pAux);
+ env = pAux->env;
+ jFXA = s3jni_getFts5ExensionApi(env);
+ if( !jFXA ) goto error_oom;
+ jpFts = new_Fts5Context_wrapper(env, pFts);
+ if(!jpFts) goto error_oom;
+ rc = udf_args(env, pCx, argc, argv, &jpCx, &jArgv);
+ if(rc) goto error_oom;
+ (*env)->CallVoidMethod(env, pAux->jObj, pAux->jmid,
+ jFXA, jpFts, jpCx, jArgv);
+ IFTHREW{
+ EXCEPTION_CLEAR;
+ udf_report_exception(pCx, pAux->zFuncName, "xFunction");
+ }
+ UNREF_L(jpFts);
+ UNREF_L(jpCx);
+ UNREF_L(jArgv);
+ return;
+error_oom:
+ assert( !jArgv );
+ assert( !jpCx );
+ UNREF_L(jpFts);
+ sqlite3_result_error_nomem(pCx);
+ return;
+}
+
+JDECLFtsApi(jint,xCreateFunction)(JENV_OSELF, jstring jName,
+ jobject jUserData, jobject jFunc){
+ fts5_api * const pApi = PtrGet_fts5_api(jSelf);
+ int rc;
+ char const * zName;
+ Fts5JniAux * pAux;
+ assert(pApi);
+ zName = JSTR_TOC(jName);
+ if(!zName) return SQLITE_NOMEM;
+ pAux = Fts5JniAux_alloc(env, jFunc);
+ if( pAux ){
+ rc = pApi->xCreateFunction(pApi, zName, pAux,
+ s3jni_fts5_extension_function,
+ Fts5JniAux_xDestroy);
+ }else{
+ rc = SQLITE_NOMEM;
+ }
+ if( 0==rc ){
+ pAux->jUserData = jUserData ? REF_G(jUserData) : 0;
+ pAux->zFuncName = sqlite3_mprintf("%s", zName)
+ /* OOM here is non-fatal. Ignore it. */;
+ }
+ JSTR_RELEASE(jName, zName);
+ return (jint)rc;
+}
+
+
+typedef struct s3jni_fts5AuxData s3jni_fts5AuxData;
+struct s3jni_fts5AuxData {
+ JNIEnv *env;
+ jobject jObj;
+};
+
+static void s3jni_fts5AuxData_xDestroy(void *x){
+ if(x){
+ s3jni_fts5AuxData * const p = x;
+ if(p->jObj){
+ JNIEnv *env = p->env;
+ s3jni_call_xDestroy(env, p->jObj, 0);
+ UNREF_G(p->jObj);
+ }
+ sqlite3_free(x);
+ }
+}
+
+JDECLFtsXA(jobject,xGetAuxdata)(JENV_OSELF,jobject jCtx, jboolean bClear){
+ Fts5ExtDecl;
+ jobject rv = 0;
+ s3jni_fts5AuxData * const pAux = fext->xGetAuxdata(PtrGet_Fts5Context(jCtx), bClear);
+ if(pAux){
+ if(bClear){
+ if( pAux->jObj ){
+ rv = REF_L(pAux->jObj);
+ UNREF_G(pAux->jObj);
+ }
+ /* Note that we do not call xDestroy() in this case. */
+ sqlite3_free(pAux);
+ }else{
+ rv = pAux->jObj;
+ }
+ }
+ return rv;
+}
+
+JDECLFtsXA(jint,xInst)(JENV_OSELF,jobject jCtx, jint iIdx, jobject jOutPhrase,
+ jobject jOutCol, jobject jOutOff){
+ Fts5ExtDecl;
+ int n1 = 0, n2 = 2, n3 = 0;
+ int const rc = fext->xInst(PtrGet_Fts5Context(jCtx), (int)iIdx, &n1, &n2, &n3);
+ if( 0==rc ){
+ OutputPointer_set_Int32(env, jOutPhrase, n1);
+ OutputPointer_set_Int32(env, jOutCol, n2);
+ OutputPointer_set_Int32(env, jOutOff, n3);
+ }
+ return rc;
+}
+
+JDECLFtsXA(jint,xInstCount)(JENV_OSELF,jobject jCtx, jobject jOut32){
+ Fts5ExtDecl;
+ int nOut = 0;
+ int const rc = fext->xInstCount(PtrGet_Fts5Context(jCtx), &nOut);
+ if( 0==rc && jOut32 ) OutputPointer_set_Int32(env, jOut32, nOut);
+ return (jint)rc;
+}
+
+JDECLFtsXA(jint,xPhraseCount)(JENV_OSELF,jobject jCtx){
+ Fts5ExtDecl;
+ return (jint)fext->xPhraseCount(PtrGet_Fts5Context(jCtx));
+}
+
+/**
+ Initializes jc->jPhraseIter if it needed it.
+*/
+static void s3jni_phraseIter_init(JNIEnv *const env, S3JniEnvCache * const jc,
+ jobject jIter){
+ if(!jc->jPhraseIter.klazz){
+ jclass klazz = (*env)->GetObjectClass(env, jIter);
+ jc->jPhraseIter.klazz = REF_G(klazz);
+ jc->jPhraseIter.fidA = (*env)->GetFieldID(env, klazz, "a", "J");
+ EXCEPTION_IS_FATAL("Cannot get Fts5PhraseIter.a field.");
+ jc->jPhraseIter.fidB = (*env)->GetFieldID(env, klazz, "a", "J");
+ EXCEPTION_IS_FATAL("Cannot get Fts5PhraseIter.b field.");
+ }
+}
+
+/* Copy the 'a' and 'b' fields from pSrc to Fts5PhraseIter object jIter. */
+static void s3jni_phraseIter_NToJ(JNIEnv *const env, S3JniEnvCache const * const jc,
+ Fts5PhraseIter const * const pSrc,
+ jobject jIter){
+ assert(jc->jPhraseIter.klazz);
+ (*env)->SetLongField(env, jIter, jc->jPhraseIter.fidA, (jlong)pSrc->a);
+ EXCEPTION_IS_FATAL("Cannot set Fts5PhraseIter.a field.");
+ (*env)->SetLongField(env, jIter, jc->jPhraseIter.fidB, (jlong)pSrc->b);
+ EXCEPTION_IS_FATAL("Cannot set Fts5PhraseIter.b field.");
+}
+
+/* Copy the 'a' and 'b' fields from Fts5PhraseIter object jIter to pDest. */
+static void s3jni_phraseIter_JToN(JNIEnv *const env, S3JniEnvCache const * const jc,
+ jobject jIter, Fts5PhraseIter * const pDest){
+ assert(jc->jPhraseIter.klazz);
+ pDest->a =
+ (const unsigned char *)(*env)->GetLongField(env, jIter, jc->jPhraseIter.fidA);
+ EXCEPTION_IS_FATAL("Cannot get Fts5PhraseIter.a field.");
+ pDest->b =
+ (const unsigned char *)(*env)->GetLongField(env, jIter, jc->jPhraseIter.fidB);
+ EXCEPTION_IS_FATAL("Cannot get Fts5PhraseIter.b field.");
+}
+
+JDECLFtsXA(jint,xPhraseFirst)(JENV_OSELF,jobject jCtx, jint iPhrase,
+ jobject jIter, jobject jOutCol,
+ jobject jOutOff){
+ Fts5ExtDecl;
+ S3JniEnvCache * const jc = S3JniGlobal_env_cache(env);
+ Fts5PhraseIter iter;
+ int rc, iCol = 0, iOff = 0;
+ s3jni_phraseIter_init(env, jc, jIter);
+ rc = fext->xPhraseFirst(PtrGet_Fts5Context(jCtx), (int)iPhrase,
+ &iter, &iCol, &iOff);
+ if( 0==rc ){
+ OutputPointer_set_Int32(env, jOutCol, iCol);
+ OutputPointer_set_Int32(env, jOutOff, iOff);
+ s3jni_phraseIter_NToJ(env, jc, &iter, jIter);
+ }
+ return rc;
+}
+
+JDECLFtsXA(jint,xPhraseFirstColumn)(JENV_OSELF,jobject jCtx, jint iPhrase,
+ jobject jIter, jobject jOutCol){
+ Fts5ExtDecl;
+ S3JniEnvCache * const jc = S3JniGlobal_env_cache(env);
+ Fts5PhraseIter iter;
+ int rc, iCol = 0;
+ s3jni_phraseIter_init(env, jc, jIter);
+ rc = fext->xPhraseFirstColumn(PtrGet_Fts5Context(jCtx), (int)iPhrase,
+ &iter, &iCol);
+ if( 0==rc ){
+ OutputPointer_set_Int32(env, jOutCol, iCol);
+ s3jni_phraseIter_NToJ(env, jc, &iter, jIter);
+ }
+ return rc;
+}
+
+JDECLFtsXA(void,xPhraseNext)(JENV_OSELF,jobject jCtx, jobject jIter,
+ jobject jOutCol, jobject jOutOff){
+ Fts5ExtDecl;
+ S3JniEnvCache * const jc = S3JniGlobal_env_cache(env);
+ Fts5PhraseIter iter;
+ int iCol = 0, iOff = 0;
+ if(!jc->jPhraseIter.klazz) return /*SQLITE_MISUSE*/;
+ s3jni_phraseIter_JToN(env, jc, jIter, &iter);
+ fext->xPhraseNext(PtrGet_Fts5Context(jCtx),
+ &iter, &iCol, &iOff);
+ OutputPointer_set_Int32(env, jOutCol, iCol);
+ OutputPointer_set_Int32(env, jOutOff, iOff);
+ s3jni_phraseIter_NToJ(env, jc, &iter, jIter);
+}
+
+JDECLFtsXA(void,xPhraseNextColumn)(JENV_OSELF,jobject jCtx, jobject jIter,
+ jobject jOutCol){
+ Fts5ExtDecl;
+ S3JniEnvCache * const jc = S3JniGlobal_env_cache(env);
+ Fts5PhraseIter iter;
+ int iCol = 0;
+ if(!jc->jPhraseIter.klazz) return /*SQLITE_MISUSE*/;
+ s3jni_phraseIter_JToN(env, jc, jIter, &iter);
+ fext->xPhraseNextColumn(PtrGet_Fts5Context(jCtx), &iter, &iCol);
+ OutputPointer_set_Int32(env, jOutCol, iCol);
+ s3jni_phraseIter_NToJ(env, jc, &iter, jIter);
+}
+
+
+JDECLFtsXA(jint,xPhraseSize)(JENV_OSELF,jobject jCtx, jint iPhrase){
+ Fts5ExtDecl;
+ return (jint)fext->xPhraseSize(PtrGet_Fts5Context(jCtx), (int)iPhrase);
+}
+
+/**
+ State for use with xQueryPhrase() and xTokenize().
+*/
+struct s3jni_xQueryPhraseState {
+ JNIEnv *env;
+ Fts5ExtensionApi const * fext;
+ S3JniEnvCache const * jc;
+ jmethodID midCallback;
+ jobject jCallback;
+ jobject jFcx;
+ /* State for xTokenize() */
+ struct {
+ const char * zPrev;
+ int nPrev;
+ jbyteArray jba;
+ } tok;
+};
+
+static int s3jni_xQueryPhrase(const Fts5ExtensionApi *xapi,
+ Fts5Context * pFcx, void *pData){
+ /* TODO: confirm that the Fts5Context passed to this function is
+ guaranteed to be the same one passed to xQueryPhrase(). If it's
+ not, we'll have to create a new wrapper object on every call. */
+ struct s3jni_xQueryPhraseState const * s = pData;
+ JNIEnv * const env = s->env;
+ int rc = (int)(*env)->CallIntMethod(env, s->jCallback, s->midCallback,
+ s->jc->jFtsExt, s->jFcx);
+ IFTHREW{
+ EXCEPTION_WARN_CALLBACK_THREW("xQueryPhrase callback");
+ EXCEPTION_CLEAR;
+ rc = SQLITE_ERROR;
+ }
+ return rc;
+}
+
+JDECLFtsXA(jint,xQueryPhrase)(JENV_OSELF,jobject jFcx, jint iPhrase,
+ jobject jCallback){
+ Fts5ExtDecl;
+ S3JniEnvCache * const jc = S3JniGlobal_env_cache(env);
+ struct s3jni_xQueryPhraseState s;
+ jclass klazz = jCallback ? (*env)->GetObjectClass(env, jCallback) : NULL;
+ if( !klazz ) return SQLITE_MISUSE;
+ s.env = env;
+ s.jc = jc;
+ s.jCallback = jCallback;
+ s.jFcx = jFcx;
+ s.fext = fext;
+ s.midCallback = (*env)->GetMethodID(env, klazz, "xCallback",
+ "(Lorg.sqlite.jni.Fts5ExtensionApi;"
+ "Lorg.sqlite.jni.Fts5Context;)I");
+ EXCEPTION_IS_FATAL("Could not extract xQueryPhraseCallback.xCallback method.");
+ return (jint)fext->xQueryPhrase(PtrGet_Fts5Context(jFcx), iPhrase, &s,
+ s3jni_xQueryPhrase);
+}
+
+
+JDECLFtsXA(jint,xRowCount)(JENV_OSELF,jobject jCtx, jobject jOut64){
+ Fts5ExtDecl;
+ sqlite3_int64 nOut = 0;
+ int const rc = fext->xRowCount(PtrGet_Fts5Context(jCtx), &nOut);
+ if( 0==rc && jOut64 ) OutputPointer_set_Int64(env, jOut64, (jlong)nOut);
+ return (jint)rc;
+}
+
+JDECLFtsXA(jlong,xRowid)(JENV_OSELF,jobject jCtx){
+ Fts5ExtDecl;
+ return (jlong)fext->xRowid(PtrGet_Fts5Context(jCtx));
+}
+
+JDECLFtsXA(int,xSetAuxdata)(JENV_OSELF,jobject jCtx, jobject jAux){
+ Fts5ExtDecl;
+ int rc;
+ s3jni_fts5AuxData * pAux;
+ pAux = sqlite3_malloc(sizeof(*pAux));
+ if(!pAux){
+ if(jAux){
+ // Emulate how xSetAuxdata() behaves when it cannot alloc
+ // its auxdata wrapper.
+ s3jni_call_xDestroy(env, jAux, 0);
+ }
+ return SQLITE_NOMEM;
+ }
+ pAux->env = env;
+ pAux->jObj = REF_G(jAux);
+ rc = fext->xSetAuxdata(PtrGet_Fts5Context(jCtx), pAux,
+ s3jni_fts5AuxData_xDestroy);
+ return rc;
+}
+
+/**
+ xToken() impl for xTokenize().
+*/
+static int s3jni_xTokenize_xToken(void *p, int tFlags, const char* z,
+ int nZ, int iStart, int iEnd){
+ int rc;
+ struct s3jni_xQueryPhraseState * const s = p;
+ JNIEnv * const env = s->env;
+ jbyteArray jba;
+ if( s->tok.zPrev == z && s->tok.nPrev == nZ ){
+ jba = s->tok.jba;
+ }else{
+ if(s->tok.jba){
+ UNREF_L(s->tok.jba);
+ }
+ s->tok.zPrev = z;
+ s->tok.nPrev = nZ;
+ s->tok.jba = (*env)->NewByteArray(env, (jint)nZ);
+ if( !s->tok.jba ) return SQLITE_NOMEM;
+ jba = s->tok.jba;
+ (*env)->SetByteArrayRegion(env, jba, 0, (jint)nZ, (const jbyte*)z);
+ }
+ rc = (int)(*env)->CallIntMethod(env, s->jCallback, s->midCallback,
+ (jint)tFlags, jba, (jint)iStart,
+ (jint)iEnd);
+ return rc;
+}
+
+/**
+ Proxy for Fts5ExtensionApi.xTokenize() and fts5_tokenizer.xTokenize()
+*/
+static jint s3jni_fts5_xTokenize(JENV_OSELF, const char *zClassName,
+ jint tokFlags, jobject jFcx,
+ jbyteArray jbaText, jobject jCallback){
+ Fts5ExtDecl;
+ S3JniEnvCache * const jc = S3JniGlobal_env_cache(env);
+ struct s3jni_xQueryPhraseState s;
+ int rc = 0;
+ jbyte * const pText = jCallback ? JBA_TOC(jbaText) : 0;
+ jsize nText = pText ? (*env)->GetArrayLength(env, jbaText) : 0;
+ jclass const klazz = jCallback ? (*env)->GetObjectClass(env, jCallback) : NULL;
+ if( !klazz ) return SQLITE_MISUSE;
+ memset(&s, 0, sizeof(s));
+ s.env = env;
+ s.jc = jc;
+ s.jCallback = jCallback;
+ s.jFcx = jFcx;
+ s.fext = fext;
+ s.midCallback = (*env)->GetMethodID(env, klazz, "xToken", "(I[BII)I");
+ IFTHREW {
+ EXCEPTION_REPORT;
+ EXCEPTION_CLEAR;
+ JBA_RELEASE(jbaText, pText);
+ return SQLITE_ERROR;
+ }
+ s.tok.jba = REF_L(jbaText);
+ s.tok.zPrev = (const char *)pText;
+ s.tok.nPrev = (int)nText;
+ if( zClassName == S3JniClassNames.Fts5ExtensionApi ){
+ rc = fext->xTokenize(PtrGet_Fts5Context(jFcx),
+ (const char *)pText, (int)nText,
+ &s, s3jni_xTokenize_xToken);
+ }else if( zClassName == S3JniClassNames.fts5_tokenizer ){
+ fts5_tokenizer * const pTok = PtrGet_fts5_tokenizer(jSelf);
+ rc = pTok->xTokenize(PtrGet_Fts5Tokenizer(jFcx), &s, tokFlags,
+ (const char *)pText, (int)nText,
+ s3jni_xTokenize_xToken);
+ }else{
+ (*env)->FatalError(env, "This cannot happen. Maintenance required.");
+ }
+ if(s.tok.jba){
+ assert( s.tok.zPrev );
+ UNREF_L(s.tok.jba);
+ }
+ JBA_RELEASE(jbaText, pText);
+ return (jint)rc;
+}
+
+JDECLFtsXA(jint,xTokenize)(JENV_OSELF,jobject jFcx, jbyteArray jbaText,
+ jobject jCallback){
+ return s3jni_fts5_xTokenize(env, jSelf, S3JniClassNames.Fts5ExtensionApi,
+ 0, jFcx, jbaText, jCallback);
+}
+
+JDECLFtsTok(jint,xTokenize)(JENV_OSELF,jobject jFcx, jint tokFlags,
+ jbyteArray jbaText, jobject jCallback){
+ return s3jni_fts5_xTokenize(env, jSelf, S3JniClassNames.Fts5Tokenizer,
+ tokFlags, jFcx, jbaText, jCallback);
+}
+
+
+JDECLFtsXA(jobject,xUserData)(JENV_OSELF,jobject jFcx){
+ Fts5ExtDecl;
+ Fts5JniAux * const pAux = fext->xUserData(PtrGet_Fts5Context(jFcx));
+ return pAux ? pAux->jUserData : 0;
+}
+
+#endif /* SQLITE_ENABLE_FTS5 */
+
+////////////////////////////////////////////////////////////////////////
+// End of the main API bindings. Start of SQLTester bits...
+////////////////////////////////////////////////////////////////////////
+
+#ifdef S3JNI_ENABLE_SQLTester
+typedef struct SQLTesterJni SQLTesterJni;
+struct SQLTesterJni {
+ sqlite3_int64 nDup;
+};
+static SQLTesterJni SQLTester = {
+ 0
+};
+
+static void SQLTester_dup_destructor(void*pToFree){
+ u64 *p = (u64*)pToFree;
+ assert( p!=0 );
+ p--;
+ assert( p[0]==0x2bbf4b7c );
+ p[0] = 0;
+ p[1] = 0;
+ sqlite3_free(p);
+}
+
+/*
+** Implementation of
+**
+** dup(TEXT)
+**
+** This SQL function simply makes a copy of its text argument. But it
+** returns the result using a custom destructor, in order to provide
+** tests for the use of Mem.xDel() in the SQLite VDBE.
+*/
+static void SQLTester_dup_func(
+ sqlite3_context *context,
+ int argc,
+ sqlite3_value **argv
+){
+ u64 *pOut;
+ char *z;
+ int n = sqlite3_value_bytes(argv[0]);
+ SQLTesterJni * const p = (SQLTesterJni *)sqlite3_user_data(context);
+
+ ++p->nDup;
+ if( n>0 && (pOut = sqlite3_malloc( (n+16)&~7 ))!=0 ){
+ pOut[0] = 0x2bbf4b7c;
+ z = (char*)&pOut[1];
+ memcpy(z, sqlite3_value_text(argv[0]), n);
+ z[n] = 0;
+ sqlite3_result_text(context, z, n, SQLTester_dup_destructor);
+ }
+ return;
+}
+
+/*
+** Return the number of calls to the dup() SQL function since the
+** SQLTester context was opened or since the last dup_count() call.
+*/
+static void SQLTester_dup_count_func(
+ sqlite3_context *context,
+ int argc,
+ sqlite3_value **argv
+){
+ SQLTesterJni * const p = (SQLTesterJni *)sqlite3_user_data(context);
+ sqlite3_result_int64(context, p->nDup);
+ p->nDup = 0;
+}
+
+/*
+** Return non-zero if string z matches glob pattern zGlob and zero if the
+** pattern does not match.
+**
+** To repeat:
+**
+** zero == no match
+** non-zero == match
+**
+** Globbing rules:
+**
+** '*' Matches any sequence of zero or more characters.
+**
+** '?' Matches exactly one character.
+**
+** [...] Matches one character from the enclosed list of
+** characters.
+**
+** [^...] Matches one character not in the enclosed list.
+**
+** '#' Matches any sequence of one or more digits with an
+** optional + or - sign in front, or a hexadecimal
+** literal of the form 0x...
+*/
+static int SQLTester_strnotglob(const char *zGlob, const char *z){
+ int c, c2;
+ int invert;
+ int seen;
+
+ while( (c = (*(zGlob++)))!=0 ){
+ if( c=='*' ){
+ while( (c=(*(zGlob++))) == '*' || c=='?' ){
+ if( c=='?' && (*(z++))==0 ) return 0;
+ }
+ if( c==0 ){
+ return 1;
+ }else if( c=='[' ){
+ while( *z && SQLTester_strnotglob(zGlob-1,z)==0 ){
+ z++;
+ }
+ return (*z)!=0;
+ }
+ while( (c2 = (*(z++)))!=0 ){
+ while( c2!=c ){
+ c2 = *(z++);
+ if( c2==0 ) return 0;
+ }
+ if( SQLTester_strnotglob(zGlob,z) ) return 1;
+ }
+ return 0;
+ }else if( c=='?' ){
+ if( (*(z++))==0 ) return 0;
+ }else if( c=='[' ){
+ int prior_c = 0;
+ seen = 0;
+ invert = 0;
+ c = *(z++);
+ if( c==0 ) return 0;
+ c2 = *(zGlob++);
+ if( c2=='^' ){
+ invert = 1;
+ c2 = *(zGlob++);
+ }
+ if( c2==']' ){
+ if( c==']' ) seen = 1;
+ c2 = *(zGlob++);
+ }
+ while( c2 && c2!=']' ){
+ if( c2=='-' && zGlob[0]!=']' && zGlob[0]!=0 && prior_c>0 ){
+ c2 = *(zGlob++);
+ if( c>=prior_c && c<=c2 ) seen = 1;
+ prior_c = 0;
+ }else{
+ if( c==c2 ){
+ seen = 1;
+ }
+ prior_c = c2;
+ }
+ c2 = *(zGlob++);
+ }
+ if( c2==0 || (seen ^ invert)==0 ) return 0;
+ }else if( c=='#' ){
+ if( z[0]=='0'
+ && (z[1]=='x' || z[1]=='X')
+ && sqlite3Isxdigit(z[2])
+ ){
+ z += 3;
+ while( sqlite3Isxdigit(z[0]) ){ z++; }
+ }else{
+ if( (z[0]=='-' || z[0]=='+') && sqlite3Isdigit(z[1]) ) z++;
+ if( !sqlite3Isdigit(z[0]) ) return 0;
+ z++;
+ while( sqlite3Isdigit(z[0]) ){ z++; }
+ }
+ }else{
+ if( c!=(*(z++)) ) return 0;
+ }
+ }
+ return *z==0;
+}
+
+JNIEXPORT jint JNICALL
+Java_org_sqlite_jni_tester_SQLTester_strglob(
+ JENV_CSELF, jbyteArray baG, jbyteArray baT
+){
+ int rc = 0;
+ jbyte * const pG = JBA_TOC(baG);
+ jbyte * const pT = pG ? JBA_TOC(baT) : 0;
+ OOM_CHECK(pT);
+
+ /* Note that we're relying on the byte arrays having been
+ NUL-terminated on the Java side. */
+ rc = !SQLTester_strnotglob((const char *)pG, (const char *)pT);
+ JBA_RELEASE(baG, pG);
+ JBA_RELEASE(baT, pT);
+ return rc;
+}
+
+
+static int SQLTester_auto_extension(sqlite3 *pDb, const char **pzErr,
+ const struct sqlite3_api_routines *ignored){
+ sqlite3_create_function(pDb, "dup", 1, SQLITE_UTF8, &SQLTester,
+ SQLTester_dup_func, 0, 0);
+ sqlite3_create_function(pDb, "dup_count", 0, SQLITE_UTF8, &SQLTester,
+ SQLTester_dup_count_func, 0, 0);
+ return 0;
+}
+
+JNIEXPORT void JNICALL
+Java_org_sqlite_jni_tester_SQLTester_installCustomExtensions(JENV_CSELF){
+ sqlite3_auto_extension( (void(*)(void))SQLTester_auto_extension );
+}
+
+#endif /* S3JNI_ENABLE_SQLTester */
+////////////////////////////////////////////////////////////////////////
+// End of SQLTester bindings. Start of lower-level bits.
+////////////////////////////////////////////////////////////////////////
+
+
+/**
+ Uncaches the current JNIEnv from the S3JniGlobal state, clearing any
+ resources owned by that cache entry and making that slot available
+ for re-use. It is important that the Java-side decl of this
+ function be declared as synchronous.
+*/
+JNIEXPORT jboolean JNICALL
+Java_org_sqlite_jni_SQLite3Jni_uncacheJniEnv(JENV_CSELF){
+ return S3JniGlobal_env_uncache(env) ? JNI_TRUE : JNI_FALSE;
+}
+
+/**
+ Called during static init of the SQLite3Jni class to sync certain
+ compile-time constants to Java-space.
+
+ This routine is part of the reason why we have to #include
+ sqlite3.c instead of sqlite3.h.
+*/
+JNIEXPORT void JNICALL
+Java_org_sqlite_jni_SQLite3Jni_init(JENV_CSELF){
+ enum JType {
+ JTYPE_INT,
+ JTYPE_BOOL
+ };
+ typedef struct {
+ const char *zName;
+ enum JType jtype;
+ int value;
+ } ConfigFlagEntry;
+ const ConfigFlagEntry aLimits[] = {
+ {"SQLITE_ENABLE_FTS5", JTYPE_BOOL,
+#ifdef SQLITE_ENABLE_FTS5
+ 1
+#else
+ 0
+#endif
+ },
+ {"SQLITE_MAX_ALLOCATION_SIZE", JTYPE_INT, SQLITE_MAX_ALLOCATION_SIZE},
+ {"SQLITE_LIMIT_LENGTH", JTYPE_INT, SQLITE_LIMIT_LENGTH},
+ {"SQLITE_MAX_LENGTH", JTYPE_INT, SQLITE_MAX_LENGTH},
+ {"SQLITE_LIMIT_SQL_LENGTH", JTYPE_INT, SQLITE_LIMIT_SQL_LENGTH},
+ {"SQLITE_MAX_SQL_LENGTH", JTYPE_INT, SQLITE_MAX_SQL_LENGTH},
+ {"SQLITE_LIMIT_COLUMN", JTYPE_INT, SQLITE_LIMIT_COLUMN},
+ {"SQLITE_MAX_COLUMN", JTYPE_INT, SQLITE_MAX_COLUMN},
+ {"SQLITE_LIMIT_EXPR_DEPTH", JTYPE_INT, SQLITE_LIMIT_EXPR_DEPTH},
+ {"SQLITE_MAX_EXPR_DEPTH", JTYPE_INT, SQLITE_MAX_EXPR_DEPTH},
+ {"SQLITE_LIMIT_COMPOUND_SELECT", JTYPE_INT, SQLITE_LIMIT_COMPOUND_SELECT},
+ {"SQLITE_MAX_COMPOUND_SELECT", JTYPE_INT, SQLITE_MAX_COMPOUND_SELECT},
+ {"SQLITE_LIMIT_VDBE_OP", JTYPE_INT, SQLITE_LIMIT_VDBE_OP},
+ {"SQLITE_MAX_VDBE_OP", JTYPE_INT, SQLITE_MAX_VDBE_OP},
+ {"SQLITE_LIMIT_FUNCTION_ARG", JTYPE_INT, SQLITE_LIMIT_FUNCTION_ARG},
+ {"SQLITE_MAX_FUNCTION_ARG", JTYPE_INT, SQLITE_MAX_FUNCTION_ARG},
+ {"SQLITE_LIMIT_ATTACHED", JTYPE_INT, SQLITE_LIMIT_ATTACHED},
+ {"SQLITE_MAX_ATTACHED", JTYPE_INT, SQLITE_MAX_ATTACHED},
+ {"SQLITE_LIMIT_LIKE_PATTERN_LENGTH", JTYPE_INT, SQLITE_LIMIT_LIKE_PATTERN_LENGTH},
+ {"SQLITE_MAX_LIKE_PATTERN_LENGTH", JTYPE_INT, SQLITE_MAX_LIKE_PATTERN_LENGTH},
+ {"SQLITE_LIMIT_VARIABLE_NUMBER", JTYPE_INT, SQLITE_LIMIT_VARIABLE_NUMBER},
+ {"SQLITE_MAX_VARIABLE_NUMBER", JTYPE_INT, SQLITE_MAX_VARIABLE_NUMBER},
+ {"SQLITE_LIMIT_TRIGGER_DEPTH", JTYPE_INT, SQLITE_LIMIT_TRIGGER_DEPTH},
+ {"SQLITE_MAX_TRIGGER_DEPTH", JTYPE_INT, SQLITE_MAX_TRIGGER_DEPTH},
+ {"SQLITE_LIMIT_WORKER_THREADS", JTYPE_INT, SQLITE_LIMIT_WORKER_THREADS},
+ {"SQLITE_MAX_WORKER_THREADS", JTYPE_INT, SQLITE_MAX_WORKER_THREADS},
+ {0,0}
+ };
+ jfieldID fieldId;
+ const ConfigFlagEntry * pConfFlag;
+
+ memset(&S3JniGlobal, 0, sizeof(S3JniGlobal));
+ if( (*env)->GetJavaVM(env, &S3JniGlobal.jvm) ){
+ (*env)->FatalError(env, "GetJavaVM() failure shouldn't be possible.");
+ return;
+ }
+#if 0
+ /* Just for sanity checking... */
+ (void)S3JniGlobal_env_cache(env);
+ if( !S3JniGlobal.envCache.aHead ){
+ (*env)->FatalError(env, "Could not allocate JNIEnv-specific cache.");
+ return;
+ }
+ assert( 1 == S3JniGlobal.metrics.envCacheMisses );
+ assert( env == S3JniGlobal.envCache.aHead->env );
+ assert( 0 != S3JniGlobal.envCache.aHead->g.cObj );
+#endif
+
+ for( pConfFlag = &aLimits[0]; pConfFlag->zName; ++pConfFlag ){
+ char const * zSig = (JTYPE_BOOL == pConfFlag->jtype) ? "Z" : "I";
+ fieldId = (*env)->GetStaticFieldID(env, jKlazz, pConfFlag->zName, zSig);
+ EXCEPTION_IS_FATAL("Missing an expected static member of the SQLite3Jni class.");
+ //MARKER(("Setting %s (field=%p) = %d\n", pConfFlag->zName, fieldId, pConfFlag->value));
+ assert(fieldId);
+ switch(pConfFlag->jtype){
+ case JTYPE_INT:
+ (*env)->SetStaticIntField(env, jKlazz, fieldId, (jint)pConfFlag->value);
+ break;
+ case JTYPE_BOOL:
+ (*env)->SetStaticBooleanField(env, jKlazz, fieldId,
+ pConfFlag->value ? JNI_TRUE : JNI_FALSE);
+ break;
+ }
+ EXCEPTION_IS_FATAL("Seting a static member of the SQLite3Jni class failed.");
+ }
+}
diff --git a/ext/jni/src/c/sqlite3-jni.h b/ext/jni/src/c/sqlite3-jni.h
new file mode 100644
index 000000000..bcb55c4f1
--- /dev/null
+++ b/ext/jni/src/c/sqlite3-jni.h
@@ -0,0 +1,1989 @@
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include
+/* Header for class org_sqlite_jni_SQLite3Jni */
+
+#ifndef _Included_org_sqlite_jni_SQLite3Jni
+#define _Included_org_sqlite_jni_SQLite3Jni
+#ifdef __cplusplus
+extern "C" {
+#endif
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_ACCESS_EXISTS
+#define org_sqlite_jni_SQLite3Jni_SQLITE_ACCESS_EXISTS 0L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_ACCESS_READWRITE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_ACCESS_READWRITE 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_ACCESS_READ
+#define org_sqlite_jni_SQLite3Jni_SQLITE_ACCESS_READ 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DENY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DENY 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IGNORE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IGNORE 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CREATE_INDEX
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CREATE_INDEX 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CREATE_TABLE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CREATE_TABLE 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CREATE_TEMP_INDEX
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CREATE_TEMP_INDEX 3L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CREATE_TEMP_TABLE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CREATE_TEMP_TABLE 4L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CREATE_TEMP_TRIGGER
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CREATE_TEMP_TRIGGER 5L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CREATE_TEMP_VIEW
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CREATE_TEMP_VIEW 6L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CREATE_TRIGGER
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CREATE_TRIGGER 7L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CREATE_VIEW
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CREATE_VIEW 8L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DELETE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DELETE 9L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DROP_INDEX
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DROP_INDEX 10L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DROP_TABLE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DROP_TABLE 11L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DROP_TEMP_INDEX
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DROP_TEMP_INDEX 12L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DROP_TEMP_TABLE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DROP_TEMP_TABLE 13L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DROP_TEMP_TRIGGER
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DROP_TEMP_TRIGGER 14L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DROP_TEMP_VIEW
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DROP_TEMP_VIEW 15L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DROP_TRIGGER
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DROP_TRIGGER 16L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DROP_VIEW
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DROP_VIEW 17L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INSERT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INSERT 18L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_PRAGMA
+#define org_sqlite_jni_SQLite3Jni_SQLITE_PRAGMA 19L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_READ
+#define org_sqlite_jni_SQLite3Jni_SQLITE_READ 20L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_SELECT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_SELECT 21L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_TRANSACTION
+#define org_sqlite_jni_SQLite3Jni_SQLITE_TRANSACTION 22L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_UPDATE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_UPDATE 23L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_ATTACH
+#define org_sqlite_jni_SQLite3Jni_SQLITE_ATTACH 24L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DETACH
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DETACH 25L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_ALTER_TABLE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_ALTER_TABLE 26L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_REINDEX
+#define org_sqlite_jni_SQLite3Jni_SQLITE_REINDEX 27L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_ANALYZE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_ANALYZE 28L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CREATE_VTABLE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CREATE_VTABLE 29L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DROP_VTABLE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DROP_VTABLE 30L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FUNCTION
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FUNCTION 31L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_SAVEPOINT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_SAVEPOINT 32L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_RECURSIVE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_RECURSIVE 33L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_STATIC
+#define org_sqlite_jni_SQLite3Jni_SQLITE_STATIC 0LL
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_TRANSIENT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_TRANSIENT -1LL
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESETSTART_INVERT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESETSTART_INVERT 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESETAPPLY_NOSAVEPOINT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESETAPPLY_NOSAVEPOINT 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESETAPPLY_INVERT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESETAPPLY_INVERT 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESETAPPLY_IGNORENOOP
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESETAPPLY_IGNORENOOP 4L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESET_DATA
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESET_DATA 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESET_NOTFOUND
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESET_NOTFOUND 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESET_CONFLICT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESET_CONFLICT 3L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESET_CONSTRAINT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESET_CONSTRAINT 4L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESET_FOREIGN_KEY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESET_FOREIGN_KEY 5L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESET_OMIT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESET_OMIT 0L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESET_REPLACE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESET_REPLACE 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESET_ABORT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CHANGESET_ABORT 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_SINGLETHREAD
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_SINGLETHREAD 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_MULTITHREAD
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_MULTITHREAD 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_SERIALIZED
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_SERIALIZED 3L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_MALLOC
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_MALLOC 4L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_GETMALLOC
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_GETMALLOC 5L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_SCRATCH
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_SCRATCH 6L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_PAGECACHE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_PAGECACHE 7L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_HEAP
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_HEAP 8L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_MEMSTATUS
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_MEMSTATUS 9L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_MUTEX
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_MUTEX 10L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_GETMUTEX
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_GETMUTEX 11L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_LOOKASIDE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_LOOKASIDE 13L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_PCACHE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_PCACHE 14L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_GETPCACHE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_GETPCACHE 15L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_LOG
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_LOG 16L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_URI
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_URI 17L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_PCACHE2
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_PCACHE2 18L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_GETPCACHE2
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_GETPCACHE2 19L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_COVERING_INDEX_SCAN
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_COVERING_INDEX_SCAN 20L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_SQLLOG
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_SQLLOG 21L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_MMAP_SIZE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_MMAP_SIZE 22L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_WIN32_HEAPSIZE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_WIN32_HEAPSIZE 23L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_PCACHE_HDRSZ
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_PCACHE_HDRSZ 24L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_PMASZ
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_PMASZ 25L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_STMTJRNL_SPILL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_STMTJRNL_SPILL 26L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_SMALL_MALLOC
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_SMALL_MALLOC 27L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_SORTERREF_SIZE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_SORTERREF_SIZE 28L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_MEMDB_MAXSIZE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONFIG_MEMDB_MAXSIZE 29L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INTEGER
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INTEGER 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FLOAT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FLOAT 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_TEXT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_TEXT 3L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_BLOB
+#define org_sqlite_jni_SQLite3Jni_SQLITE_BLOB 4L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_NULL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_NULL 5L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_MAINDBNAME
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_MAINDBNAME 1000L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_LOOKASIDE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_LOOKASIDE 1001L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_ENABLE_FKEY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_ENABLE_FKEY 1002L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_ENABLE_TRIGGER
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_ENABLE_TRIGGER 1003L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER 1004L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION 1005L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE 1006L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_ENABLE_QPSG
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_ENABLE_QPSG 1007L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_TRIGGER_EQP
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_TRIGGER_EQP 1008L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_RESET_DATABASE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_RESET_DATABASE 1009L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_DEFENSIVE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_DEFENSIVE 1010L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_WRITABLE_SCHEMA
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_WRITABLE_SCHEMA 1011L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_LEGACY_ALTER_TABLE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_LEGACY_ALTER_TABLE 1012L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_DQS_DML
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_DQS_DML 1013L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_DQS_DDL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_DQS_DDL 1014L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_ENABLE_VIEW
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_ENABLE_VIEW 1015L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_LEGACY_FILE_FORMAT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_LEGACY_FILE_FORMAT 1016L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_TRUSTED_SCHEMA
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_TRUSTED_SCHEMA 1017L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_STMT_SCANSTATUS
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_STMT_SCANSTATUS 1018L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_REVERSE_SCANORDER
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_REVERSE_SCANORDER 1019L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_MAX
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBCONFIG_MAX 1019L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_LOOKASIDE_USED
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_LOOKASIDE_USED 0L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_CACHE_USED
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_CACHE_USED 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_SCHEMA_USED
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_SCHEMA_USED 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_STMT_USED
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_STMT_USED 3L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_LOOKASIDE_HIT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_LOOKASIDE_HIT 4L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE 5L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL 6L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_CACHE_HIT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_CACHE_HIT 7L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_CACHE_MISS
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_CACHE_MISS 8L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_CACHE_WRITE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_CACHE_WRITE 9L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_DEFERRED_FKS
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_DEFERRED_FKS 10L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_CACHE_USED_SHARED
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_CACHE_USED_SHARED 11L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_CACHE_SPILL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_CACHE_SPILL 12L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_MAX
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DBSTATUS_MAX 12L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_UTF8
+#define org_sqlite_jni_SQLite3Jni_SQLITE_UTF8 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_UTF16LE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_UTF16LE 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_UTF16BE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_UTF16BE 3L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_UTF16
+#define org_sqlite_jni_SQLite3Jni_SQLITE_UTF16 4L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_UTF16_ALIGNED
+#define org_sqlite_jni_SQLite3Jni_SQLITE_UTF16_ALIGNED 8L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_LOCKSTATE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_LOCKSTATE 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_GET_LOCKPROXYFILE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_GET_LOCKPROXYFILE 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_SET_LOCKPROXYFILE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_SET_LOCKPROXYFILE 3L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_LAST_ERRNO
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_LAST_ERRNO 4L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_SIZE_HINT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_SIZE_HINT 5L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_CHUNK_SIZE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_CHUNK_SIZE 6L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_FILE_POINTER
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_FILE_POINTER 7L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_SYNC_OMITTED
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_SYNC_OMITTED 8L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_WIN32_AV_RETRY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_WIN32_AV_RETRY 9L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_PERSIST_WAL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_PERSIST_WAL 10L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_OVERWRITE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_OVERWRITE 11L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_VFSNAME
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_VFSNAME 12L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_POWERSAFE_OVERWRITE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_POWERSAFE_OVERWRITE 13L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_PRAGMA
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_PRAGMA 14L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_BUSYHANDLER
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_BUSYHANDLER 15L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_TEMPFILENAME
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_TEMPFILENAME 16L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_MMAP_SIZE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_MMAP_SIZE 18L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_TRACE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_TRACE 19L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_HAS_MOVED
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_HAS_MOVED 20L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_SYNC
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_SYNC 21L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_COMMIT_PHASETWO
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_COMMIT_PHASETWO 22L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_WIN32_SET_HANDLE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_WIN32_SET_HANDLE 23L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_WAL_BLOCK
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_WAL_BLOCK 24L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_ZIPVFS
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_ZIPVFS 25L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_RBU
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_RBU 26L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_VFS_POINTER
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_VFS_POINTER 27L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_JOURNAL_POINTER
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_JOURNAL_POINTER 28L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_WIN32_GET_HANDLE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_WIN32_GET_HANDLE 29L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_PDB
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_PDB 30L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_BEGIN_ATOMIC_WRITE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_BEGIN_ATOMIC_WRITE 31L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_COMMIT_ATOMIC_WRITE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_COMMIT_ATOMIC_WRITE 32L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE 33L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_LOCK_TIMEOUT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_LOCK_TIMEOUT 34L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_DATA_VERSION
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_DATA_VERSION 35L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_SIZE_LIMIT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_SIZE_LIMIT 36L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_CKPT_DONE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_CKPT_DONE 37L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_RESERVE_BYTES
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_RESERVE_BYTES 38L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_CKPT_START
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_CKPT_START 39L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_EXTERNAL_READER
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_EXTERNAL_READER 40L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_CKSM_FILE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_CKSM_FILE 41L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_RESET_CACHE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FCNTL_RESET_CACHE 42L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_LOCK_NONE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_LOCK_NONE 0L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_LOCK_SHARED
+#define org_sqlite_jni_SQLite3Jni_SQLITE_LOCK_SHARED 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_LOCK_RESERVED
+#define org_sqlite_jni_SQLite3Jni_SQLITE_LOCK_RESERVED 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_LOCK_PENDING
+#define org_sqlite_jni_SQLite3Jni_SQLITE_LOCK_PENDING 3L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_LOCK_EXCLUSIVE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_LOCK_EXCLUSIVE 4L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_ATOMIC
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_ATOMIC 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_ATOMIC512
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_ATOMIC512 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_ATOMIC1K
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_ATOMIC1K 4L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_ATOMIC2K
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_ATOMIC2K 8L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_ATOMIC4K
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_ATOMIC4K 16L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_ATOMIC8K
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_ATOMIC8K 32L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_ATOMIC16K
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_ATOMIC16K 64L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_ATOMIC32K
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_ATOMIC32K 128L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_ATOMIC64K
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_ATOMIC64K 256L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_SAFE_APPEND
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_SAFE_APPEND 512L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_SEQUENTIAL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_SEQUENTIAL 1024L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN 2048L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_POWERSAFE_OVERWRITE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_POWERSAFE_OVERWRITE 4096L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_IMMUTABLE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_IMMUTABLE 8192L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_BATCH_ATOMIC
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOCAP_BATCH_ATOMIC 16384L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_READONLY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_READONLY 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_READWRITE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_READWRITE 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_CREATE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_CREATE 4L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_URI
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_URI 64L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_MEMORY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_MEMORY 128L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_NOMUTEX
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_NOMUTEX 32768L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_FULLMUTEX
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_FULLMUTEX 65536L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_SHAREDCACHE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_SHAREDCACHE 131072L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_PRIVATECACHE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_PRIVATECACHE 262144L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_EXRESCODE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_EXRESCODE 33554432L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_NOFOLLOW
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_NOFOLLOW 16777216L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_MAIN_DB
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_MAIN_DB 256L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_MAIN_JOURNAL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_MAIN_JOURNAL 2048L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_TEMP_DB
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_TEMP_DB 512L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_TEMP_JOURNAL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_TEMP_JOURNAL 4096L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_TRANSIENT_DB
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_TRANSIENT_DB 1024L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_SUBJOURNAL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_SUBJOURNAL 8192L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_SUPER_JOURNAL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_SUPER_JOURNAL 16384L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_WAL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_WAL 524288L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_DELETEONCLOSE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_DELETEONCLOSE 8L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_EXCLUSIVE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OPEN_EXCLUSIVE 16L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_PREPARE_PERSISTENT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_PREPARE_PERSISTENT 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_PREPARE_NORMALIZE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_PREPARE_NORMALIZE 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_PREPARE_NO_VTAB
+#define org_sqlite_jni_SQLite3Jni_SQLITE_PREPARE_NO_VTAB 4L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OK
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OK 0L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_ERROR
+#define org_sqlite_jni_SQLite3Jni_SQLITE_ERROR 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INTERNAL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INTERNAL 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_PERM
+#define org_sqlite_jni_SQLite3Jni_SQLITE_PERM 3L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_ABORT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_ABORT 4L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_BUSY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_BUSY 5L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_LOCKED
+#define org_sqlite_jni_SQLite3Jni_SQLITE_LOCKED 6L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_NOMEM
+#define org_sqlite_jni_SQLite3Jni_SQLITE_NOMEM 7L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_READONLY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_READONLY 8L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INTERRUPT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INTERRUPT 9L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR 10L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CORRUPT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CORRUPT 11L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_NOTFOUND
+#define org_sqlite_jni_SQLite3Jni_SQLITE_NOTFOUND 12L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FULL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FULL 13L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CANTOPEN
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CANTOPEN 14L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_PROTOCOL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_PROTOCOL 15L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_EMPTY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_EMPTY 16L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_SCHEMA
+#define org_sqlite_jni_SQLite3Jni_SQLITE_SCHEMA 17L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_TOOBIG
+#define org_sqlite_jni_SQLite3Jni_SQLITE_TOOBIG 18L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT 19L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_MISMATCH
+#define org_sqlite_jni_SQLite3Jni_SQLITE_MISMATCH 20L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_MISUSE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_MISUSE 21L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_NOLFS
+#define org_sqlite_jni_SQLite3Jni_SQLITE_NOLFS 22L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_AUTH
+#define org_sqlite_jni_SQLite3Jni_SQLITE_AUTH 23L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FORMAT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FORMAT 24L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_RANGE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_RANGE 25L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_NOTADB
+#define org_sqlite_jni_SQLite3Jni_SQLITE_NOTADB 26L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_NOTICE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_NOTICE 27L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_WARNING
+#define org_sqlite_jni_SQLite3Jni_SQLITE_WARNING 28L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_ROW
+#define org_sqlite_jni_SQLite3Jni_SQLITE_ROW 100L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DONE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DONE 101L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_ERROR_MISSING_COLLSEQ
+#define org_sqlite_jni_SQLite3Jni_SQLITE_ERROR_MISSING_COLLSEQ 257L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_ERROR_RETRY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_ERROR_RETRY 513L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_ERROR_SNAPSHOT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_ERROR_SNAPSHOT 769L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_READ
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_READ 266L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_SHORT_READ
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_SHORT_READ 522L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_WRITE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_WRITE 778L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_FSYNC
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_FSYNC 1034L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_DIR_FSYNC
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_DIR_FSYNC 1290L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_TRUNCATE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_TRUNCATE 1546L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_FSTAT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_FSTAT 1802L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_UNLOCK
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_UNLOCK 2058L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_RDLOCK
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_RDLOCK 2314L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_DELETE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_DELETE 2570L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_BLOCKED
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_BLOCKED 2826L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_NOMEM
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_NOMEM 3082L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_ACCESS
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_ACCESS 3338L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_CHECKRESERVEDLOCK
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_CHECKRESERVEDLOCK 3594L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_LOCK
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_LOCK 3850L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_CLOSE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_CLOSE 4106L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_DIR_CLOSE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_DIR_CLOSE 4362L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_SHMOPEN
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_SHMOPEN 4618L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_SHMSIZE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_SHMSIZE 4874L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_SHMLOCK
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_SHMLOCK 5130L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_SHMMAP
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_SHMMAP 5386L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_SEEK
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_SEEK 5642L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_DELETE_NOENT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_DELETE_NOENT 5898L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_MMAP
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_MMAP 6154L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_GETTEMPPATH
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_GETTEMPPATH 6410L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_CONVPATH
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_CONVPATH 6666L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_VNODE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_VNODE 6922L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_AUTH
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_AUTH 7178L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_BEGIN_ATOMIC
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_BEGIN_ATOMIC 7434L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_COMMIT_ATOMIC
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_COMMIT_ATOMIC 7690L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_ROLLBACK_ATOMIC
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_ROLLBACK_ATOMIC 7946L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_DATA
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_DATA 8202L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_CORRUPTFS
+#define org_sqlite_jni_SQLite3Jni_SQLITE_IOERR_CORRUPTFS 8458L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_LOCKED_SHAREDCACHE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_LOCKED_SHAREDCACHE 262L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_LOCKED_VTAB
+#define org_sqlite_jni_SQLite3Jni_SQLITE_LOCKED_VTAB 518L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_BUSY_RECOVERY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_BUSY_RECOVERY 261L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_BUSY_SNAPSHOT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_BUSY_SNAPSHOT 517L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_BUSY_TIMEOUT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_BUSY_TIMEOUT 773L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CANTOPEN_NOTEMPDIR
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CANTOPEN_NOTEMPDIR 270L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CANTOPEN_ISDIR
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CANTOPEN_ISDIR 526L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CANTOPEN_FULLPATH
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CANTOPEN_FULLPATH 782L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CANTOPEN_CONVPATH
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CANTOPEN_CONVPATH 1038L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CANTOPEN_SYMLINK
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CANTOPEN_SYMLINK 1550L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CORRUPT_VTAB
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CORRUPT_VTAB 267L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CORRUPT_SEQUENCE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CORRUPT_SEQUENCE 523L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CORRUPT_INDEX
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CORRUPT_INDEX 779L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_READONLY_RECOVERY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_READONLY_RECOVERY 264L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_READONLY_CANTLOCK
+#define org_sqlite_jni_SQLite3Jni_SQLITE_READONLY_CANTLOCK 520L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_READONLY_ROLLBACK
+#define org_sqlite_jni_SQLite3Jni_SQLITE_READONLY_ROLLBACK 776L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_READONLY_DBMOVED
+#define org_sqlite_jni_SQLite3Jni_SQLITE_READONLY_DBMOVED 1032L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_READONLY_CANTINIT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_READONLY_CANTINIT 1288L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_READONLY_DIRECTORY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_READONLY_DIRECTORY 1544L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_ABORT_ROLLBACK
+#define org_sqlite_jni_SQLite3Jni_SQLITE_ABORT_ROLLBACK 516L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_CHECK
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_CHECK 275L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_COMMITHOOK
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_COMMITHOOK 531L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_FOREIGNKEY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_FOREIGNKEY 787L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_FUNCTION
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_FUNCTION 1043L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_NOTNULL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_NOTNULL 1299L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_PRIMARYKEY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_PRIMARYKEY 1555L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_TRIGGER
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_TRIGGER 1811L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_UNIQUE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_UNIQUE 2067L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_VTAB
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_VTAB 2323L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_ROWID
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_ROWID 2579L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_PINNED
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_PINNED 2835L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_DATATYPE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_CONSTRAINT_DATATYPE 3091L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_NOTICE_RECOVER_WAL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_NOTICE_RECOVER_WAL 283L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_NOTICE_RECOVER_ROLLBACK
+#define org_sqlite_jni_SQLite3Jni_SQLITE_NOTICE_RECOVER_ROLLBACK 539L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_WARNING_AUTOINDEX
+#define org_sqlite_jni_SQLite3Jni_SQLITE_WARNING_AUTOINDEX 284L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_AUTH_USER
+#define org_sqlite_jni_SQLite3Jni_SQLITE_AUTH_USER 279L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_OK_LOAD_PERMANENTLY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_OK_LOAD_PERMANENTLY 256L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_SERIALIZE_NOCOPY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_SERIALIZE_NOCOPY 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DESERIALIZE_FREEONCLOSE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DESERIALIZE_FREEONCLOSE 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DESERIALIZE_READONLY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DESERIALIZE_READONLY 4L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DESERIALIZE_RESIZEABLE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DESERIALIZE_RESIZEABLE 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_SESSION_CONFIG_STRMSIZE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_SESSION_CONFIG_STRMSIZE 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_SESSION_OBJCONFIG_SIZE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_SESSION_OBJCONFIG_SIZE 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_STATUS_MEMORY_USED
+#define org_sqlite_jni_SQLite3Jni_SQLITE_STATUS_MEMORY_USED 0L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_STATUS_PAGECACHE_USED
+#define org_sqlite_jni_SQLite3Jni_SQLITE_STATUS_PAGECACHE_USED 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_STATUS_PAGECACHE_OVERFLOW
+#define org_sqlite_jni_SQLite3Jni_SQLITE_STATUS_PAGECACHE_OVERFLOW 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_STATUS_MALLOC_SIZE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_STATUS_MALLOC_SIZE 5L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_STATUS_PARSER_STACK
+#define org_sqlite_jni_SQLite3Jni_SQLITE_STATUS_PARSER_STACK 6L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_STATUS_PAGECACHE_SIZE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_STATUS_PAGECACHE_SIZE 7L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_STATUS_MALLOC_COUNT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_STATUS_MALLOC_COUNT 9L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_STMTSTATUS_FULLSCAN_STEP
+#define org_sqlite_jni_SQLite3Jni_SQLITE_STMTSTATUS_FULLSCAN_STEP 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_STMTSTATUS_SORT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_STMTSTATUS_SORT 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_STMTSTATUS_AUTOINDEX
+#define org_sqlite_jni_SQLite3Jni_SQLITE_STMTSTATUS_AUTOINDEX 3L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_STMTSTATUS_VM_STEP
+#define org_sqlite_jni_SQLite3Jni_SQLITE_STMTSTATUS_VM_STEP 4L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_STMTSTATUS_REPREPARE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_STMTSTATUS_REPREPARE 5L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_STMTSTATUS_RUN
+#define org_sqlite_jni_SQLite3Jni_SQLITE_STMTSTATUS_RUN 6L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_STMTSTATUS_FILTER_MISS
+#define org_sqlite_jni_SQLite3Jni_SQLITE_STMTSTATUS_FILTER_MISS 7L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_STMTSTATUS_FILTER_HIT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_STMTSTATUS_FILTER_HIT 8L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_STMTSTATUS_MEMUSED
+#define org_sqlite_jni_SQLite3Jni_SQLITE_STMTSTATUS_MEMUSED 99L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_SYNC_NORMAL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_SYNC_NORMAL 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_SYNC_FULL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_SYNC_FULL 3L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_SYNC_DATAONLY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_SYNC_DATAONLY 16L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_TRACE_STMT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_TRACE_STMT 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_TRACE_PROFILE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_TRACE_PROFILE 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_TRACE_ROW
+#define org_sqlite_jni_SQLite3Jni_SQLITE_TRACE_ROW 4L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_TRACE_CLOSE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_TRACE_CLOSE 8L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_TXN_NONE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_TXN_NONE 0L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_TXN_READ
+#define org_sqlite_jni_SQLite3Jni_SQLITE_TXN_READ 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_TXN_WRITE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_TXN_WRITE 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DETERMINISTIC
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DETERMINISTIC 2048L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_DIRECTONLY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_DIRECTONLY 524288L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INNOCUOUS
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INNOCUOUS 2097152L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_SCAN_UNIQUE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_SCAN_UNIQUE 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_EQ
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_EQ 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_GT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_GT 4L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_LE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_LE 8L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_LT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_LT 16L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_GE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_GE 32L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_MATCH
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_MATCH 64L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_LIKE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_LIKE 65L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_GLOB
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_GLOB 66L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_REGEXP
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_REGEXP 67L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_NE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_NE 68L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_ISNOT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_ISNOT 69L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_ISNOTNULL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_ISNOTNULL 70L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_ISNULL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_ISNULL 71L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_IS
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_IS 72L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_LIMIT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_LIMIT 73L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_OFFSET
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_OFFSET 74L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_FUNCTION
+#define org_sqlite_jni_SQLite3Jni_SQLITE_INDEX_CONSTRAINT_FUNCTION 150L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_VTAB_CONSTRAINT_SUPPORT
+#define org_sqlite_jni_SQLite3Jni_SQLITE_VTAB_CONSTRAINT_SUPPORT 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_VTAB_INNOCUOUS
+#define org_sqlite_jni_SQLite3Jni_SQLITE_VTAB_INNOCUOUS 2L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_VTAB_DIRECTONLY
+#define org_sqlite_jni_SQLite3Jni_SQLITE_VTAB_DIRECTONLY 3L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_VTAB_USES_ALL_SCHEMAS
+#define org_sqlite_jni_SQLite3Jni_SQLITE_VTAB_USES_ALL_SCHEMAS 4L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_ROLLBACK
+#define org_sqlite_jni_SQLite3Jni_SQLITE_ROLLBACK 1L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_FAIL
+#define org_sqlite_jni_SQLite3Jni_SQLITE_FAIL 3L
+#undef org_sqlite_jni_SQLite3Jni_SQLITE_REPLACE
+#define org_sqlite_jni_SQLite3Jni_SQLITE_REPLACE 5L
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: init
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_init
+ (JNIEnv *, jclass);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: uncacheJniEnv
+ * Signature: ()Z
+ */
+JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_SQLite3Jni_uncacheJniEnv
+ (JNIEnv *, jclass);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_auto_extension
+ * Signature: (Lorg/sqlite/jni/AutoExtension;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1auto_1extension
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_bind_blob
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I[BI)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1blob
+ (JNIEnv *, jclass, jobject, jint, jbyteArray, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_bind_double
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;ID)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1double
+ (JNIEnv *, jclass, jobject, jint, jdouble);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_bind_int
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;II)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1int
+ (JNIEnv *, jclass, jobject, jint, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_bind_int64
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;IJ)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1int64
+ (JNIEnv *, jclass, jobject, jint, jlong);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_bind_null
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1null
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_bind_parameter_count
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1parameter_1count
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_bind_parameter_index
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;[B)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1parameter_1index
+ (JNIEnv *, jclass, jobject, jbyteArray);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_bind_text
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I[BI)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1text
+ (JNIEnv *, jclass, jobject, jint, jbyteArray, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_bind_zeroblob
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;II)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1zeroblob
+ (JNIEnv *, jclass, jobject, jint, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_bind_zeroblob64
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;IJ)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1zeroblob64
+ (JNIEnv *, jclass, jobject, jint, jlong);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_busy_handler
+ * Signature: (Lorg/sqlite/jni/sqlite3;Lorg/sqlite/jni/BusyHandler;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1busy_1handler
+ (JNIEnv *, jclass, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_busy_timeout
+ * Signature: (Lorg/sqlite/jni/sqlite3;I)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1busy_1timeout
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_cancel_auto_extension
+ * Signature: (Lorg/sqlite/jni/AutoExtension;)Z
+ */
+JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1cancel_1auto_1extension
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_changes
+ * Signature: (Lorg/sqlite/jni/sqlite3;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1changes
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_changes64
+ * Signature: (Lorg/sqlite/jni/sqlite3;)J
+ */
+JNIEXPORT jlong JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1changes64
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_clear_bindings
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1clear_1bindings
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_close
+ * Signature: (Lorg/sqlite/jni/sqlite3;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1close
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_close_v2
+ * Signature: (Lorg/sqlite/jni/sqlite3;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1close_1v2
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_column_blob
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)[B
+ */
+JNIEXPORT jbyteArray JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1blob
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_column_bytes
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1bytes
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_column_bytes16
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1bytes16
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_column_count
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1count
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_column_double
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)D
+ */
+JNIEXPORT jdouble JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1double
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_column_int
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1int
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_column_int64
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)J
+ */
+JNIEXPORT jlong JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1int64
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_column_name
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1name
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_column_database_name
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1database_1name
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_column_origin_name
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1origin_1name
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_column_table_name
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1table_1name
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_column_text16
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1text16
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_column_text
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)[B
+ */
+JNIEXPORT jbyteArray JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1text
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_column_type
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1type
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_column_value
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)Lorg/sqlite/jni/sqlite3_value;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1value
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_collation_needed
+ * Signature: (Lorg/sqlite/jni/sqlite3;Lorg/sqlite/jni/CollationNeeded;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1collation_1needed
+ (JNIEnv *, jclass, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_context_db_handle
+ * Signature: (Lorg/sqlite/jni/sqlite3_context;)Lorg/sqlite/jni/sqlite3;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1context_1db_1handle
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_commit_hook
+ * Signature: (Lorg/sqlite/jni/sqlite3;Lorg/sqlite/jni/CommitHook;)Lorg/sqlite/jni/CommitHook;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1commit_1hook
+ (JNIEnv *, jclass, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_compileoption_get
+ * Signature: (I)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1compileoption_1get
+ (JNIEnv *, jclass, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_compileoption_used
+ * Signature: (Ljava/lang/String;)Z
+ */
+JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1compileoption_1used
+ (JNIEnv *, jclass, jstring);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_create_collation
+ * Signature: (Lorg/sqlite/jni/sqlite3;Ljava/lang/String;ILorg/sqlite/jni/Collation;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1create_1collation
+ (JNIEnv *, jclass, jobject, jstring, jint, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_create_function
+ * Signature: (Lorg/sqlite/jni/sqlite3;Ljava/lang/String;IILorg/sqlite/jni/SQLFunction;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1create_1function
+ (JNIEnv *, jclass, jobject, jstring, jint, jint, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_data_count
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1data_1count
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_db_filename
+ * Signature: (Lorg/sqlite/jni/sqlite3;Ljava/lang/String;)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1db_1filename
+ (JNIEnv *, jclass, jobject, jstring);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_db_config
+ * Signature: (Lorg/sqlite/jni/sqlite3;IILorg/sqlite/jni/OutputPointer/Int32;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1db_1config__Lorg_sqlite_jni_sqlite3_2IILorg_sqlite_jni_OutputPointer_Int32_2
+ (JNIEnv *, jclass, jobject, jint, jint, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_db_config
+ * Signature: (Lorg/sqlite/jni/sqlite3;ILjava/lang/String;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1db_1config__Lorg_sqlite_jni_sqlite3_2ILjava_lang_String_2
+ (JNIEnv *, jclass, jobject, jint, jstring);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_db_status
+ * Signature: (Lorg/sqlite/jni/sqlite3;ILorg/sqlite/jni/OutputPointer/Int32;Lorg/sqlite/jni/OutputPointer/Int32;Z)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1db_1status
+ (JNIEnv *, jclass, jobject, jint, jobject, jobject, jboolean);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_errcode
+ * Signature: (Lorg/sqlite/jni/sqlite3;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1errcode
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_expanded_sql
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1expanded_1sql
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_extended_errcode
+ * Signature: (Lorg/sqlite/jni/sqlite3;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1extended_1errcode
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_extended_result_codes
+ * Signature: (Lorg/sqlite/jni/sqlite3;Z)Z
+ */
+JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1extended_1result_1codes
+ (JNIEnv *, jclass, jobject, jboolean);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_errmsg
+ * Signature: (Lorg/sqlite/jni/sqlite3;)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1errmsg
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_errstr
+ * Signature: (I)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1errstr
+ (JNIEnv *, jclass, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_error_offset
+ * Signature: (Lorg/sqlite/jni/sqlite3;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1error_1offset
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_finalize
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1finalize
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_initialize
+ * Signature: ()I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1initialize
+ (JNIEnv *, jclass);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_last_insert_rowid
+ * Signature: (Lorg/sqlite/jni/sqlite3;)J
+ */
+JNIEXPORT jlong JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1last_1insert_1rowid
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_libversion
+ * Signature: ()Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1libversion
+ (JNIEnv *, jclass);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_libversion_number
+ * Signature: ()I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1libversion_1number
+ (JNIEnv *, jclass);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_open
+ * Signature: (Ljava/lang/String;Lorg/sqlite/jni/OutputPointer/sqlite3;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1open
+ (JNIEnv *, jclass, jstring, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_open_v2
+ * Signature: (Ljava/lang/String;Lorg/sqlite/jni/OutputPointer/sqlite3;ILjava/lang/String;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1open_1v2
+ (JNIEnv *, jclass, jstring, jobject, jint, jstring);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_prepare
+ * Signature: (Lorg/sqlite/jni/sqlite3;[BILorg/sqlite/jni/OutputPointer/sqlite3_stmt;Lorg/sqlite/jni/OutputPointer/Int32;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1prepare
+ (JNIEnv *, jclass, jobject, jbyteArray, jint, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_prepare_v2
+ * Signature: (Lorg/sqlite/jni/sqlite3;[BILorg/sqlite/jni/OutputPointer/sqlite3_stmt;Lorg/sqlite/jni/OutputPointer/Int32;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1prepare_1v2
+ (JNIEnv *, jclass, jobject, jbyteArray, jint, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_prepare_v3
+ * Signature: (Lorg/sqlite/jni/sqlite3;[BIILorg/sqlite/jni/OutputPointer/sqlite3_stmt;Lorg/sqlite/jni/OutputPointer/Int32;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1prepare_1v3
+ (JNIEnv *, jclass, jobject, jbyteArray, jint, jint, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_progress_handler
+ * Signature: (Lorg/sqlite/jni/sqlite3;ILorg/sqlite/jni/ProgressHandler;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1progress_1handler
+ (JNIEnv *, jclass, jobject, jint, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_reset
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1reset
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_reset_auto_extension
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1reset_1auto_1extension
+ (JNIEnv *, jclass);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_result_double
+ * Signature: (Lorg/sqlite/jni/sqlite3_context;D)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1double
+ (JNIEnv *, jclass, jobject, jdouble);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_result_error
+ * Signature: (Lorg/sqlite/jni/sqlite3_context;[BI)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1error
+ (JNIEnv *, jclass, jobject, jbyteArray, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_result_error_toobig
+ * Signature: (Lorg/sqlite/jni/sqlite3_context;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1error_1toobig
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_result_error_nomem
+ * Signature: (Lorg/sqlite/jni/sqlite3_context;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1error_1nomem
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_result_error_code
+ * Signature: (Lorg/sqlite/jni/sqlite3_context;I)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1error_1code
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_result_null
+ * Signature: (Lorg/sqlite/jni/sqlite3_context;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1null
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_result_int
+ * Signature: (Lorg/sqlite/jni/sqlite3_context;I)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1int
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_result_int64
+ * Signature: (Lorg/sqlite/jni/sqlite3_context;J)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1int64
+ (JNIEnv *, jclass, jobject, jlong);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_result_java_object
+ * Signature: (Lorg/sqlite/jni/sqlite3_context;Ljava/lang/Object;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1java_1object
+ (JNIEnv *, jclass, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_result_value
+ * Signature: (Lorg/sqlite/jni/sqlite3_context;Lorg/sqlite/jni/sqlite3_value;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1value
+ (JNIEnv *, jclass, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_result_zeroblob
+ * Signature: (Lorg/sqlite/jni/sqlite3_context;I)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1zeroblob
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_result_zeroblob64
+ * Signature: (Lorg/sqlite/jni/sqlite3_context;J)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1zeroblob64
+ (JNIEnv *, jclass, jobject, jlong);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_result_blob
+ * Signature: (Lorg/sqlite/jni/sqlite3_context;[BI)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1blob
+ (JNIEnv *, jclass, jobject, jbyteArray, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_result_blob64
+ * Signature: (Lorg/sqlite/jni/sqlite3_context;[BJ)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1blob64
+ (JNIEnv *, jclass, jobject, jbyteArray, jlong);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_result_text
+ * Signature: (Lorg/sqlite/jni/sqlite3_context;[BI)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1text
+ (JNIEnv *, jclass, jobject, jbyteArray, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_result_text64
+ * Signature: (Lorg/sqlite/jni/sqlite3_context;[BJI)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1text64
+ (JNIEnv *, jclass, jobject, jbyteArray, jlong, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_status
+ * Signature: (ILorg/sqlite/jni/OutputPointer/Int32;Lorg/sqlite/jni/OutputPointer/Int32;Z)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1status
+ (JNIEnv *, jclass, jint, jobject, jobject, jboolean);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_status64
+ * Signature: (ILorg/sqlite/jni/OutputPointer/Int64;Lorg/sqlite/jni/OutputPointer/Int64;Z)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1status64
+ (JNIEnv *, jclass, jint, jobject, jobject, jboolean);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_rollback_hook
+ * Signature: (Lorg/sqlite/jni/sqlite3;Lorg/sqlite/jni/RollbackHook;)Lorg/sqlite/jni/RollbackHook;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1rollback_1hook
+ (JNIEnv *, jclass, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_set_authorizer
+ * Signature: (Lorg/sqlite/jni/sqlite3;Lorg/sqlite/jni/Authorizer;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1set_1authorizer
+ (JNIEnv *, jclass, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_set_last_insert_rowid
+ * Signature: (Lorg/sqlite/jni/sqlite3;J)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1set_1last_1insert_1rowid
+ (JNIEnv *, jclass, jobject, jlong);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_sleep
+ * Signature: (I)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1sleep
+ (JNIEnv *, jclass, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_sourceid
+ * Signature: ()Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1sourceid
+ (JNIEnv *, jclass);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_sql
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1sql
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_step
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1step
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_strglob
+ * Signature: ([B[B)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1strglob
+ (JNIEnv *, jclass, jbyteArray, jbyteArray);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_strlike
+ * Signature: ([B[BI)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1strlike
+ (JNIEnv *, jclass, jbyteArray, jbyteArray, jint);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_threadsafe
+ * Signature: ()I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1threadsafe
+ (JNIEnv *, jclass);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_total_changes
+ * Signature: (Lorg/sqlite/jni/sqlite3;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1total_1changes
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_total_changes64
+ * Signature: (Lorg/sqlite/jni/sqlite3;)J
+ */
+JNIEXPORT jlong JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1total_1changes64
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_trace_v2
+ * Signature: (Lorg/sqlite/jni/sqlite3;ILorg/sqlite/jni/Tracer;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1trace_1v2
+ (JNIEnv *, jclass, jobject, jint, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_update_hook
+ * Signature: (Lorg/sqlite/jni/sqlite3;Lorg/sqlite/jni/UpdateHook;)Lorg/sqlite/jni/UpdateHook;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1update_1hook
+ (JNIEnv *, jclass, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_blob
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)[B
+ */
+JNIEXPORT jbyteArray JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1blob
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_bytes
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1bytes
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_bytes16
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1bytes16
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_double
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)D
+ */
+JNIEXPORT jdouble JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1double
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_dupe
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)Lorg/sqlite/jni/sqlite3_value;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1dupe
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_encoding
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1encoding
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_free
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1free
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_int
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1int
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_int64
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)J
+ */
+JNIEXPORT jlong JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1int64
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_java_object
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)Ljava/lang/Object;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1java_1object
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_text
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1text
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_text_utf8
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)[B
+ */
+JNIEXPORT jbyteArray JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1text_1utf8
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_text16
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)[B
+ */
+JNIEXPORT jbyteArray JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1text16
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_text16le
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)[B
+ */
+JNIEXPORT jbyteArray JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1text16le
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_text16be
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)[B
+ */
+JNIEXPORT jbyteArray JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1text16be
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_type
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1type
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_numeric_type
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1numeric_1type
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_nochange
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1nochange
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_frombind
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1frombind
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_value_subtype
+ * Signature: (Lorg/sqlite/jni/sqlite3_value;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1subtype
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_shutdown
+ * Signature: ()I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1shutdown
+ (JNIEnv *, jclass);
+
+/*
+ * Class: org_sqlite_jni_SQLite3Jni
+ * Method: sqlite3_do_something_for_developer
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1do_1something_1for_1developer
+ (JNIEnv *, jclass);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include
+/* Header for class org_sqlite_jni_Fts5ExtensionApi */
+
+#ifndef _Included_org_sqlite_jni_Fts5ExtensionApi
+#define _Included_org_sqlite_jni_Fts5ExtensionApi
+#ifdef __cplusplus
+extern "C" {
+#endif
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: getInstance
+ * Signature: ()Lorg/sqlite/jni/Fts5ExtensionApi;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_getInstance
+ (JNIEnv *, jclass);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xColumnCount
+ * Signature: (Lorg/sqlite/jni/Fts5Context;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xColumnCount
+ (JNIEnv *, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xColumnSize
+ * Signature: (Lorg/sqlite/jni/Fts5Context;ILorg/sqlite/jni/OutputPointer/Int32;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xColumnSize
+ (JNIEnv *, jobject, jobject, jint, jobject);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xColumnText
+ * Signature: (Lorg/sqlite/jni/Fts5Context;ILorg/sqlite/jni/OutputPointer/String;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xColumnText
+ (JNIEnv *, jobject, jobject, jint, jobject);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xColumnTotalSize
+ * Signature: (Lorg/sqlite/jni/Fts5Context;ILorg/sqlite/jni/OutputPointer/Int64;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xColumnTotalSize
+ (JNIEnv *, jobject, jobject, jint, jobject);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xGetAuxdata
+ * Signature: (Lorg/sqlite/jni/Fts5Context;Z)Ljava/lang/Object;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xGetAuxdata
+ (JNIEnv *, jobject, jobject, jboolean);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xInst
+ * Signature: (Lorg/sqlite/jni/Fts5Context;ILorg/sqlite/jni/OutputPointer/Int32;Lorg/sqlite/jni/OutputPointer/Int32;Lorg/sqlite/jni/OutputPointer/Int32;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xInst
+ (JNIEnv *, jobject, jobject, jint, jobject, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xInstCount
+ * Signature: (Lorg/sqlite/jni/Fts5Context;Lorg/sqlite/jni/OutputPointer/Int32;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xInstCount
+ (JNIEnv *, jobject, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xPhraseCount
+ * Signature: (Lorg/sqlite/jni/Fts5Context;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xPhraseCount
+ (JNIEnv *, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xPhraseFirst
+ * Signature: (Lorg/sqlite/jni/Fts5Context;ILorg/sqlite/jni/Fts5PhraseIter;Lorg/sqlite/jni/OutputPointer/Int32;Lorg/sqlite/jni/OutputPointer/Int32;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xPhraseFirst
+ (JNIEnv *, jobject, jobject, jint, jobject, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xPhraseFirstColumn
+ * Signature: (Lorg/sqlite/jni/Fts5Context;ILorg/sqlite/jni/Fts5PhraseIter;Lorg/sqlite/jni/OutputPointer/Int32;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xPhraseFirstColumn
+ (JNIEnv *, jobject, jobject, jint, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xPhraseNext
+ * Signature: (Lorg/sqlite/jni/Fts5Context;Lorg/sqlite/jni/Fts5PhraseIter;Lorg/sqlite/jni/OutputPointer/Int32;Lorg/sqlite/jni/OutputPointer/Int32;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xPhraseNext
+ (JNIEnv *, jobject, jobject, jobject, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xPhraseNextColumn
+ * Signature: (Lorg/sqlite/jni/Fts5Context;Lorg/sqlite/jni/Fts5PhraseIter;Lorg/sqlite/jni/OutputPointer/Int32;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xPhraseNextColumn
+ (JNIEnv *, jobject, jobject, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xPhraseSize
+ * Signature: (Lorg/sqlite/jni/Fts5Context;I)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xPhraseSize
+ (JNIEnv *, jobject, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xQueryPhrase
+ * Signature: (Lorg/sqlite/jni/Fts5Context;ILorg/sqlite/jni/Fts5ExtensionApi/xQueryPhraseCallback;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xQueryPhrase
+ (JNIEnv *, jobject, jobject, jint, jobject);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xRowCount
+ * Signature: (Lorg/sqlite/jni/Fts5Context;Lorg/sqlite/jni/OutputPointer/Int64;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xRowCount
+ (JNIEnv *, jobject, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xRowid
+ * Signature: (Lorg/sqlite/jni/Fts5Context;)J
+ */
+JNIEXPORT jlong JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xRowid
+ (JNIEnv *, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xSetAuxdata
+ * Signature: (Lorg/sqlite/jni/Fts5Context;Ljava/lang/Object;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xSetAuxdata
+ (JNIEnv *, jobject, jobject, jobject);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xTokenize
+ * Signature: (Lorg/sqlite/jni/Fts5Context;[BLorg/sqlite/jni/Fts5/xTokenizeCallback;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xTokenize
+ (JNIEnv *, jobject, jobject, jbyteArray, jobject);
+
+/*
+ * Class: org_sqlite_jni_Fts5ExtensionApi
+ * Method: xUserData
+ * Signature: (Lorg/sqlite/jni/Fts5Context;)Ljava/lang/Object;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_Fts5ExtensionApi_xUserData
+ (JNIEnv *, jobject, jobject);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include
+/* Header for class org_sqlite_jni_fts5_api */
+
+#ifndef _Included_org_sqlite_jni_fts5_api
+#define _Included_org_sqlite_jni_fts5_api
+#ifdef __cplusplus
+extern "C" {
+#endif
+/*
+ * Class: org_sqlite_jni_fts5_api
+ * Method: getInstanceForDb
+ * Signature: (Lorg/sqlite/jni/sqlite3;)Lorg/sqlite/jni/fts5_api;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_fts5_1api_getInstanceForDb
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_fts5_api
+ * Method: xCreateFunction
+ * Signature: (Ljava/lang/String;Ljava/lang/Object;Lorg/sqlite/jni/fts5_extension_function;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_fts5_1api_xCreateFunction
+ (JNIEnv *, jobject, jstring, jobject, jobject);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include
+/* Header for class org_sqlite_jni_fts5_tokenizer */
+
+#ifndef _Included_org_sqlite_jni_fts5_tokenizer
+#define _Included_org_sqlite_jni_fts5_tokenizer
+#ifdef __cplusplus
+extern "C" {
+#endif
+/*
+ * Class: org_sqlite_jni_fts5_tokenizer
+ * Method: xTokenize
+ * Signature: (Lorg/sqlite/jni/Fts5Tokenizer;I[BLorg/sqlite/jni/Fts5/xTokenizeCallback;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_fts5_1tokenizer_xTokenize
+ (JNIEnv *, jobject, jobject, jint, jbyteArray, jobject);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include
+/* Header for class org_sqlite_jni_tester_SQLTester */
+
+#ifndef _Included_org_sqlite_jni_tester_SQLTester
+#define _Included_org_sqlite_jni_tester_SQLTester
+#ifdef __cplusplus
+extern "C" {
+#endif
+/*
+ * Class: org_sqlite_jni_tester_SQLTester
+ * Method: strglob
+ * Signature: ([B[B)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_tester_SQLTester_strglob
+ (JNIEnv *, jclass, jbyteArray, jbyteArray);
+
+/*
+ * Class: org_sqlite_jni_tester_SQLTester
+ * Method: installCustomExtensions
+ * Signature: ()V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_tester_SQLTester_installCustomExtensions
+ (JNIEnv *, jclass);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/ext/jni/src/org/sqlite/jni/Authorizer.java b/ext/jni/src/org/sqlite/jni/Authorizer.java
new file mode 100644
index 000000000..114c27fc6
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/Authorizer.java
@@ -0,0 +1,32 @@
+/*
+** 2023-08-05
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ A callback for use with sqlite3_set_authorizer().
+*/
+public interface Authorizer {
+ /**
+ Must functions as described for the sqlite3_set_authorizer()
+ callback, with one caveat: the string values passed here were
+ initially (at the C level) encoded in standard UTF-8. If they
+ contained any constructs which are not compatible with MUTF-8,
+ these strings will not have the expected values. For further
+ details, see the documentation for the SQLite3Jni class.
+
+ Must not throw.
+ */
+ int xAuth(int opId, @Nullable String s1, @Nullable String s2,
+ @Nullable String s3, @Nullable String s4);
+}
diff --git a/ext/jni/src/org/sqlite/jni/AutoExtension.java b/ext/jni/src/org/sqlite/jni/AutoExtension.java
new file mode 100644
index 000000000..a2ab6a0f7
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/AutoExtension.java
@@ -0,0 +1,31 @@
+/*
+** 2023-08-05
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ A callback for use with sqlite3_auto_extension().
+*/
+public interface AutoExtension {
+ /**
+ Must function as described for the sqlite3_auto_extension(),
+ with the caveat that the signature is more limited.
+
+ As an exception (as it were) to the callbacks-must-not-throw
+ rule, AutoExtensions may do so and the exception's error message
+ will be set as the db's error string.
+
+ Results are undefined if db is closed by an auto-extension.
+ */
+ int xEntryPoint(sqlite3 db);
+}
diff --git a/ext/jni/src/org/sqlite/jni/BusyHandler.java b/ext/jni/src/org/sqlite/jni/BusyHandler.java
new file mode 100644
index 000000000..8ce729c90
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/BusyHandler.java
@@ -0,0 +1,45 @@
+/*
+** 2023-07-22
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ Callback proxy for use with sqlite3_busy_handler().
+*/
+public abstract class BusyHandler {
+ /**
+ Must function as documented for the sqlite3_busy_handler()
+ callback argument, minus the (void*) argument the C-level
+ function requires.
+
+ Any exceptions thrown by this callback are suppressed in order to
+ retain the C-style API semantics of the JNI bindings.
+ */
+ public abstract int xCallback(int n);
+
+ /**
+ Optionally override to perform any cleanup when this busy
+ handler is destroyed. It is destroyed when:
+
+ - The associated db is passed to sqlite3_close() or
+ sqlite3_close_v2().
+
+ - sqlite3_busy_handler() is called to replace the handler,
+ whether it's passed a null handler or any other instance of
+ this class.
+
+ - sqlite3_busy_timeout() is called, which implicitly installs
+ a busy handler.
+ */
+ public void xDestroy(){}
+}
diff --git a/ext/jni/src/org/sqlite/jni/Collation.java b/ext/jni/src/org/sqlite/jni/Collation.java
new file mode 100644
index 000000000..a05b8ef9e
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/Collation.java
@@ -0,0 +1,28 @@
+/*
+** 2023-07-22
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+*/
+public abstract class Collation {
+ /**
+ Must compare the given byte arrays using memcmp() semantics.
+ */
+ public abstract int xCompare(byte[] lhs, byte[] rhs);
+ /**
+ Called by SQLite when the collation is destroyed. If a Collation
+ requires custom cleanup, override this method.
+ */
+ public void xDestroy() {}
+}
diff --git a/ext/jni/src/org/sqlite/jni/CollationNeeded.java b/ext/jni/src/org/sqlite/jni/CollationNeeded.java
new file mode 100644
index 000000000..85214a1d2
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/CollationNeeded.java
@@ -0,0 +1,28 @@
+/*
+** 2023-07-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 is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ Callback proxy for use with sqlite3_collation_needed().
+*/
+public interface CollationNeeded {
+ /**
+ Has the same semantics as the C-level sqlite3_create_collation()
+ callback.
+
+ If it throws, the exception message is passed on to the db and
+ the exception is suppressed.
+ */
+ int xCollationNeeded(sqlite3 db, int eTextRep, String collationName);
+}
diff --git a/ext/jni/src/org/sqlite/jni/CommitHook.java b/ext/jni/src/org/sqlite/jni/CommitHook.java
new file mode 100644
index 000000000..eaa75a004
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/CommitHook.java
@@ -0,0 +1,25 @@
+/*
+** 2023-07-22
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ Callback proxy for use with sqlite3_commit_hook().
+*/
+public interface CommitHook {
+ /**
+ Works as documented for the sqlite3_commit_hook() callback.
+ Must not throw.
+ */
+ int xCommitHook();
+}
diff --git a/ext/jni/src/org/sqlite/jni/Fts5.java b/ext/jni/src/org/sqlite/jni/Fts5.java
new file mode 100644
index 000000000..102cf575a
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/Fts5.java
@@ -0,0 +1,38 @@
+/*
+** 2023-08-05
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ INCOMPLETE AND COMPLETELY UNTESTED.
+
+ A wrapper for communicating C-level (fts5_api*) instances with
+ Java. These wrappers do not own their associated pointer, they
+ simply provide a type-safe way to communicate it between Java and C
+ via JNI.
+*/
+public final class Fts5 {
+ /* Not used */
+ private Fts5(){}
+
+ //! Callback type for use with xTokenize() variants
+ public static interface xTokenizeCallback {
+ int xToken(int tFlags, byte txt[], int iStart, int iEnd);
+ }
+
+ public static final int FTS5_TOKENIZE_QUERY = 0x0001;
+ public static final int FTS5_TOKENIZE_PREFIX = 0x0002;
+ public static final int FTS5_TOKENIZE_DOCUMENT = 0x0004;
+ public static final int FTS5_TOKENIZE_AUX = 0x0008;
+ public static final int FTS5_TOKEN_COLOCATED = 0x0001;
+}
diff --git a/ext/jni/src/org/sqlite/jni/Fts5Context.java b/ext/jni/src/org/sqlite/jni/Fts5Context.java
new file mode 100644
index 000000000..e78f67d55
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/Fts5Context.java
@@ -0,0 +1,23 @@
+/*
+** 2023-08-04
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ A wrapper for communicating C-level (Fts5Context*) instances with
+ Java. These wrappers do not own their associated pointer, they
+ simply provide a type-safe way to communicate it between Java and C
+ via JNI.
+*/
+public final class Fts5Context extends NativePointerHolder {
+}
diff --git a/ext/jni/src/org/sqlite/jni/Fts5ExtensionApi.java b/ext/jni/src/org/sqlite/jni/Fts5ExtensionApi.java
new file mode 100644
index 000000000..ac041e300
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/Fts5ExtensionApi.java
@@ -0,0 +1,86 @@
+/*
+** 2023-08-04
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+import java.nio.charset.StandardCharsets;
+
+/**
+ ALMOST COMPLETELY UNTESTED.
+
+ FAR FROM COMPLETE and the feasibility of binding this to Java
+ is still undetermined. This might be removed.
+*/
+public final class Fts5ExtensionApi extends NativePointerHolder {
+ //! Only called from JNI
+ private Fts5ExtensionApi(){}
+ private int iVersion = 2;
+
+ /* Callback type for used by xQueryPhrase(). */
+ public static interface xQueryPhraseCallback {
+ int xCallback(Fts5ExtensionApi fapi, Fts5Context cx);
+ }
+
+ /**
+ Returns the singleton instance of this class.
+ */
+ public static synchronized native Fts5ExtensionApi getInstance();
+
+ public synchronized native int xColumnCount(@NotNull Fts5Context fcx);
+ public synchronized native int xColumnSize(@NotNull Fts5Context cx, int iCol,
+ @NotNull OutputPointer.Int32 pnToken);
+ public synchronized native int xColumnText(@NotNull Fts5Context cx, int iCol,
+ @NotNull OutputPointer.String txt);
+ public synchronized native int xColumnTotalSize(@NotNull Fts5Context fcx, int iCol,
+ @NotNull OutputPointer.Int64 pnToken);
+ public synchronized native Object xGetAuxdata(@NotNull Fts5Context cx, boolean clearIt);
+ public synchronized native int xInst(@NotNull Fts5Context cx, int iIdx,
+ @NotNull OutputPointer.Int32 piPhrase,
+ @NotNull OutputPointer.Int32 piCol,
+ @NotNull OutputPointer.Int32 piOff);
+ public synchronized native int xInstCount(@NotNull Fts5Context fcx,
+ @NotNull OutputPointer.Int32 pnInst);
+ public synchronized native int xPhraseCount(@NotNull Fts5Context fcx);
+ public synchronized native int xPhraseFirst(@NotNull Fts5Context cx, int iPhrase,
+ @NotNull Fts5PhraseIter iter,
+ @NotNull OutputPointer.Int32 iCol,
+ @NotNull OutputPointer.Int32 iOff);
+ public synchronized native int xPhraseFirstColumn(@NotNull Fts5Context cx, int iPhrase,
+ @NotNull Fts5PhraseIter iter,
+ @NotNull OutputPointer.Int32 iCol);
+ public synchronized native void xPhraseNext(@NotNull Fts5Context cx,
+ @NotNull Fts5PhraseIter iter,
+ @NotNull OutputPointer.Int32 iCol,
+ @NotNull OutputPointer.Int32 iOff);
+ public synchronized native void xPhraseNextColumn(@NotNull Fts5Context cx,
+ @NotNull Fts5PhraseIter iter,
+ @NotNull OutputPointer.Int32 iCol);
+ public synchronized native int xPhraseSize(@NotNull Fts5Context fcx, int iPhrase);
+ public synchronized native int xQueryPhrase(@NotNull Fts5Context cx, int iPhrase,
+ @NotNull xQueryPhraseCallback callback);
+ public synchronized native int xRowCount(@NotNull Fts5Context fcx,
+ @NotNull OutputPointer.Int64 nRow);
+ public synchronized native long xRowid(@NotNull Fts5Context cx);
+ /* Note that the JNI binding lacks the C version's xDelete()
+ callback argument. Instead, if pAux has an xDestroy() method, it
+ is called if the FTS5 API finalizes the aux state (including if
+ allocation of storage for the auxdata fails). Any reference to
+ pAux held by the JNI layer will be relinquished regardless of
+ whether pAux has an xDestroy() method. */
+ public synchronized native int xSetAuxdata(@NotNull Fts5Context cx, @Nullable Object pAux);
+ public synchronized native int xTokenize(@NotNull Fts5Context cx, @NotNull byte pText[],
+ @NotNull Fts5.xTokenizeCallback callback);
+
+ public synchronized native Object xUserData(Fts5Context cx);
+ //^^^ returns the pointer passed as the 3rd arg to the C-level
+ // fts5_api::xCreateFunction.
+}
diff --git a/ext/jni/src/org/sqlite/jni/Fts5Function.java b/ext/jni/src/org/sqlite/jni/Fts5Function.java
new file mode 100644
index 000000000..463ec034f
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/Fts5Function.java
@@ -0,0 +1,27 @@
+/*
+** 2023-08-04
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ Fts5Function is used in conjunction with the
+ sqlite3_create_fts_function() JNI-bound API to give that native code
+ access to the callback functions needed in order to implement
+ FTS5 auxiliary functions in Java.
+*/
+public abstract class Fts5Function {
+
+ public abstract void xFunction(Fts5ExtensionApi pApi, Fts5Context pFts,
+ sqlite3_context pCtx, sqlite3_value argv[]);
+ public void xDestroy() {}
+}
diff --git a/ext/jni/src/org/sqlite/jni/Fts5PhraseIter.java b/ext/jni/src/org/sqlite/jni/Fts5PhraseIter.java
new file mode 100644
index 000000000..eb4e05fdf
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/Fts5PhraseIter.java
@@ -0,0 +1,24 @@
+/*
+** 2023-08-04
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ A wrapper for C-level Fts5PhraseIter. They are only modified and
+ inspected by native-level code.
+*/
+public final class Fts5PhraseIter extends NativePointerHolder {
+ //! Updated and used only by native code.
+ private long a;
+ private long b;
+}
diff --git a/ext/jni/src/org/sqlite/jni/Fts5Tokenizer.java b/ext/jni/src/org/sqlite/jni/Fts5Tokenizer.java
new file mode 100644
index 000000000..0d266a13d
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/Fts5Tokenizer.java
@@ -0,0 +1,30 @@
+/*
+** 2023-08-05x
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ INCOMPLETE AND COMPLETELY UNTESTED.
+
+ A wrapper for communicating C-level (Fts5Tokenizer*) instances with
+ Java. These wrappers do not own their associated pointer, they
+ simply provide a type-safe way to communicate it between Java and C
+ via JNI.
+
+ At the C level, the Fts5Tokenizer type is essentially a void
+ pointer used specifically for tokenizers.
+*/
+public final class Fts5Tokenizer extends NativePointerHolder {
+ //! Only called from JNI.
+ private Fts5Tokenizer(){}
+}
diff --git a/ext/jni/src/org/sqlite/jni/NativePointerHolder.java b/ext/jni/src/org/sqlite/jni/NativePointerHolder.java
new file mode 100644
index 000000000..afe2618a0
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/NativePointerHolder.java
@@ -0,0 +1,33 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ A helper for passing pointers between JNI C code and Java, in
+ particular for output pointers of high-level object types in the
+ sqlite3 C API, e.g. (sqlite3**) and (sqlite3_stmt**). This is
+ intended to be subclassed and the ContextType is intended to be the
+ class which is doing the subclassing. The intent of the ContextType
+ is strictly to provide some level of type safety by avoiding that
+ NativePointerHolder is not inadvertently passed to an incompatible
+ function signature.
+
+ These objects do not _own_ the pointer they refer to. They are
+ intended simply to communicate that pointer between C and Java.
+*/
+public class NativePointerHolder {
+ //! Only set from JNI, where access permissions don't matter.
+ private long nativePointer = 0;
+ public final long getNativePointer(){ return nativePointer; }
+}
diff --git a/ext/jni/src/org/sqlite/jni/OutputPointer.java b/ext/jni/src/org/sqlite/jni/OutputPointer.java
new file mode 100644
index 000000000..82a90c918
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/OutputPointer.java
@@ -0,0 +1,165 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ Helper classes for handling JNI output pointers.
+
+ We do not use a generic OutputPointer because working with those
+ from the native JNI code is unduly quirky due to a lack of
+ autoboxing at that level.
+
+ The usage is similar for all of thes types:
+
+ ```
+ OutputPointer.sqlite3 out = new OutputPointer.sqlite3();
+ assert( null==out.get() );
+ int rc = sqlite3_open(":memory:", out);
+ if( 0!=rc ) ... error;
+ assert( null!=out.get() );
+ sqlite3 db = out.take();
+ assert( null==out.get() );
+ ```
+
+ With the minor exception that the primitive types permit direct
+ access to the object's value via the `value` property, whereas the
+ JNI-level opaque types do not permit client-level code to set that
+ property.
+*/
+public final class OutputPointer {
+
+ /**
+ Output pointer for use with routines, such as sqlite3_open(),
+ which return a database handle via an output pointer. These
+ pointers can only be set by the JNI layer, not by client-level
+ code.
+ */
+ public static final class sqlite3 {
+ private org.sqlite.jni.sqlite3 value;
+ //! Initializes with a null value.
+ public sqlite3(){value = null;}
+ //! Sets the current value to null.
+ public void clear(){value = null;}
+ //! Returns the current value.
+ public final org.sqlite.jni.sqlite3 get(){return value;}
+ //! Equivalent to calling get() then clear().
+ public final org.sqlite.jni.sqlite3 take(){
+ final org.sqlite.jni.sqlite3 v = value;
+ value = null;
+ return v;
+ }
+ }
+
+ /**
+ Output pointer for use with routines, such as sqlite3_prepare(),
+ which return a statement handle via an output pointer. These
+ pointers can only be set by the JNI layer, not by client-level
+ code.
+ */
+ public static final class sqlite3_stmt {
+ private org.sqlite.jni.sqlite3_stmt value;
+ //! Initializes with a null value.
+ public sqlite3_stmt(){value = null;}
+ //! Sets the current value to null.
+ public void clear(){value = null;}
+ //! Returns the current value.
+ public final org.sqlite.jni.sqlite3_stmt get(){return value;}
+ //! Equivalent to calling get() then clear().
+ public final org.sqlite.jni.sqlite3_stmt take(){
+ final org.sqlite.jni.sqlite3_stmt v = value;
+ value = null;
+ return v;
+ }
+ }
+
+ /**
+ Output pointer for use with native routines which return integers via
+ output pointers.
+ */
+ public static final class Int32 {
+ /**
+ This is public for ease of use. Accessors are provided for
+ consistency with the higher-level types.
+ */
+ public int value;
+ //! Initializes with the value 0.
+ public Int32(){this(0);}
+ //! Initializes with the value v.
+ public Int32(int v){value = v;}
+ //! Returns the current value.
+ public final int get(){return value;}
+ //! Sets the current value to v.
+ public final void set(int v){value = v;}
+ }
+
+ /**
+ Output pointer for use with native routines which return 64-bit integers
+ via output pointers.
+ */
+ public static final class Int64 {
+ /**
+ This is public for ease of use. Accessors are provided for
+ consistency with the higher-level types.
+ */
+ public long value;
+ //! Initializes with the value 0.
+ public Int64(){this(0);}
+ //! Initializes with the value v.
+ public Int64(long v){value = v;}
+ //! Returns the current value.
+ public final long get(){return value;}
+ //! Sets the current value.
+ public final void set(long v){value = v;}
+ }
+
+ /**
+ Output pointer for use with native routines which return strings via
+ output pointers.
+ */
+ public static final class String {
+ /**
+ This is public for ease of use. Accessors are provided for
+ consistency with the higher-level types.
+ */
+ public java.lang.String value;
+ //! Initializes with a null value.
+ public String(){this(null);}
+ //! Initializes with the value v.
+ public String(java.lang.String v){value = v;}
+ //! Returns the current value.
+ public final java.lang.String get(){return value;}
+ //! Sets the current value.
+ public final void set(java.lang.String v){value = v;}
+ }
+
+ /**
+ Output pointer for use with native routines which return byte
+ arrays via output pointers.
+ */
+ public static final class ByteArray {
+ /**
+ This is public for ease of use. Accessors are provided for
+ consistency with the higher-level types.
+ */
+ public byte[] value;
+ //! Initializes with the value null.
+ public ByteArray(){this(null);}
+ //! Initializes with the value v.
+ public ByteArray(byte[] v){value = v;}
+ //! Returns the current value.
+ public final byte[] get(){return value;}
+ //! Sets the current value.
+ public final void set(byte[] v){value = v;}
+ }
+}
diff --git a/ext/jni/src/org/sqlite/jni/ProgressHandler.java b/ext/jni/src/org/sqlite/jni/ProgressHandler.java
new file mode 100644
index 000000000..c806eebca
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/ProgressHandler.java
@@ -0,0 +1,27 @@
+/*
+** 2023-07-22
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ Callback proxy for use with sqlite3_progress_handler().
+*/
+public interface ProgressHandler {
+ /**
+ Works as documented for the sqlite3_progress_handler() callback.
+
+ If it throws, the exception message is passed on to the db and
+ the exception is suppressed.
+ */
+ int xCallback();
+}
diff --git a/ext/jni/src/org/sqlite/jni/ResultCode.java b/ext/jni/src/org/sqlite/jni/ResultCode.java
new file mode 100644
index 000000000..0989bc744
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/ResultCode.java
@@ -0,0 +1,155 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ This enum contains all of the core and "extended" result codes used
+ by the sqlite3 library. It is provided not for use with the C-style
+ API (with which it won't work) but for higher-level code which may
+ find it useful to map SQLite result codes to human-readable names.
+*/
+public enum ResultCode {
+ SQLITE_OK(SQLite3Jni.SQLITE_OK),
+ SQLITE_ERROR(SQLite3Jni.SQLITE_ERROR),
+ SQLITE_INTERNAL(SQLite3Jni.SQLITE_INTERNAL),
+ SQLITE_PERM(SQLite3Jni.SQLITE_PERM),
+ SQLITE_ABORT(SQLite3Jni.SQLITE_ABORT),
+ SQLITE_BUSY(SQLite3Jni.SQLITE_BUSY),
+ SQLITE_LOCKED(SQLite3Jni.SQLITE_LOCKED),
+ SQLITE_NOMEM(SQLite3Jni.SQLITE_NOMEM),
+ SQLITE_READONLY(SQLite3Jni.SQLITE_READONLY),
+ SQLITE_INTERRUPT(SQLite3Jni.SQLITE_INTERRUPT),
+ SQLITE_IOERR(SQLite3Jni.SQLITE_IOERR),
+ SQLITE_CORRUPT(SQLite3Jni.SQLITE_CORRUPT),
+ SQLITE_NOTFOUND(SQLite3Jni.SQLITE_NOTFOUND),
+ SQLITE_FULL(SQLite3Jni.SQLITE_FULL),
+ SQLITE_CANTOPEN(SQLite3Jni.SQLITE_CANTOPEN),
+ SQLITE_PROTOCOL(SQLite3Jni.SQLITE_PROTOCOL),
+ SQLITE_EMPTY(SQLite3Jni.SQLITE_EMPTY),
+ SQLITE_SCHEMA(SQLite3Jni.SQLITE_SCHEMA),
+ SQLITE_TOOBIG(SQLite3Jni.SQLITE_TOOBIG),
+ SQLITE_CONSTRAINT(SQLite3Jni.SQLITE_CONSTRAINT),
+ SQLITE_MISMATCH(SQLite3Jni.SQLITE_MISMATCH),
+ SQLITE_MISUSE(SQLite3Jni.SQLITE_MISUSE),
+ SQLITE_NOLFS(SQLite3Jni.SQLITE_NOLFS),
+ SQLITE_AUTH(SQLite3Jni.SQLITE_AUTH),
+ SQLITE_FORMAT(SQLite3Jni.SQLITE_FORMAT),
+ SQLITE_RANGE(SQLite3Jni.SQLITE_RANGE),
+ SQLITE_NOTADB(SQLite3Jni.SQLITE_NOTADB),
+ SQLITE_NOTICE(SQLite3Jni.SQLITE_NOTICE),
+ SQLITE_WARNING(SQLite3Jni.SQLITE_WARNING),
+ SQLITE_ROW(SQLite3Jni.SQLITE_ROW),
+ SQLITE_DONE(SQLite3Jni.SQLITE_DONE),
+ SQLITE_ERROR_MISSING_COLLSEQ(SQLite3Jni.SQLITE_ERROR_MISSING_COLLSEQ),
+ SQLITE_ERROR_RETRY(SQLite3Jni.SQLITE_ERROR_RETRY),
+ SQLITE_ERROR_SNAPSHOT(SQLite3Jni.SQLITE_ERROR_SNAPSHOT),
+ SQLITE_IOERR_READ(SQLite3Jni.SQLITE_IOERR_READ),
+ SQLITE_IOERR_SHORT_READ(SQLite3Jni.SQLITE_IOERR_SHORT_READ),
+ SQLITE_IOERR_WRITE(SQLite3Jni.SQLITE_IOERR_WRITE),
+ SQLITE_IOERR_FSYNC(SQLite3Jni.SQLITE_IOERR_FSYNC),
+ SQLITE_IOERR_DIR_FSYNC(SQLite3Jni.SQLITE_IOERR_DIR_FSYNC),
+ SQLITE_IOERR_TRUNCATE(SQLite3Jni.SQLITE_IOERR_TRUNCATE),
+ SQLITE_IOERR_FSTAT(SQLite3Jni.SQLITE_IOERR_FSTAT),
+ SQLITE_IOERR_UNLOCK(SQLite3Jni.SQLITE_IOERR_UNLOCK),
+ SQLITE_IOERR_RDLOCK(SQLite3Jni.SQLITE_IOERR_RDLOCK),
+ SQLITE_IOERR_DELETE(SQLite3Jni.SQLITE_IOERR_DELETE),
+ SQLITE_IOERR_BLOCKED(SQLite3Jni.SQLITE_IOERR_BLOCKED),
+ SQLITE_IOERR_NOMEM(SQLite3Jni.SQLITE_IOERR_NOMEM),
+ SQLITE_IOERR_ACCESS(SQLite3Jni.SQLITE_IOERR_ACCESS),
+ SQLITE_IOERR_CHECKRESERVEDLOCK(SQLite3Jni.SQLITE_IOERR_CHECKRESERVEDLOCK),
+ SQLITE_IOERR_LOCK(SQLite3Jni.SQLITE_IOERR_LOCK),
+ SQLITE_IOERR_CLOSE(SQLite3Jni.SQLITE_IOERR_CLOSE),
+ SQLITE_IOERR_DIR_CLOSE(SQLite3Jni.SQLITE_IOERR_DIR_CLOSE),
+ SQLITE_IOERR_SHMOPEN(SQLite3Jni.SQLITE_IOERR_SHMOPEN),
+ SQLITE_IOERR_SHMSIZE(SQLite3Jni.SQLITE_IOERR_SHMSIZE),
+ SQLITE_IOERR_SHMLOCK(SQLite3Jni.SQLITE_IOERR_SHMLOCK),
+ SQLITE_IOERR_SHMMAP(SQLite3Jni.SQLITE_IOERR_SHMMAP),
+ SQLITE_IOERR_SEEK(SQLite3Jni.SQLITE_IOERR_SEEK),
+ SQLITE_IOERR_DELETE_NOENT(SQLite3Jni.SQLITE_IOERR_DELETE_NOENT),
+ SQLITE_IOERR_MMAP(SQLite3Jni.SQLITE_IOERR_MMAP),
+ SQLITE_IOERR_GETTEMPPATH(SQLite3Jni.SQLITE_IOERR_GETTEMPPATH),
+ SQLITE_IOERR_CONVPATH(SQLite3Jni.SQLITE_IOERR_CONVPATH),
+ SQLITE_IOERR_VNODE(SQLite3Jni.SQLITE_IOERR_VNODE),
+ SQLITE_IOERR_AUTH(SQLite3Jni.SQLITE_IOERR_AUTH),
+ SQLITE_IOERR_BEGIN_ATOMIC(SQLite3Jni.SQLITE_IOERR_BEGIN_ATOMIC),
+ SQLITE_IOERR_COMMIT_ATOMIC(SQLite3Jni.SQLITE_IOERR_COMMIT_ATOMIC),
+ SQLITE_IOERR_ROLLBACK_ATOMIC(SQLite3Jni.SQLITE_IOERR_ROLLBACK_ATOMIC),
+ SQLITE_IOERR_DATA(SQLite3Jni.SQLITE_IOERR_DATA),
+ SQLITE_IOERR_CORRUPTFS(SQLite3Jni.SQLITE_IOERR_CORRUPTFS),
+ SQLITE_LOCKED_SHAREDCACHE(SQLite3Jni.SQLITE_LOCKED_SHAREDCACHE),
+ SQLITE_LOCKED_VTAB(SQLite3Jni.SQLITE_LOCKED_VTAB),
+ SQLITE_BUSY_RECOVERY(SQLite3Jni.SQLITE_BUSY_RECOVERY),
+ SQLITE_BUSY_SNAPSHOT(SQLite3Jni.SQLITE_BUSY_SNAPSHOT),
+ SQLITE_BUSY_TIMEOUT(SQLite3Jni.SQLITE_BUSY_TIMEOUT),
+ SQLITE_CANTOPEN_NOTEMPDIR(SQLite3Jni.SQLITE_CANTOPEN_NOTEMPDIR),
+ SQLITE_CANTOPEN_ISDIR(SQLite3Jni.SQLITE_CANTOPEN_ISDIR),
+ SQLITE_CANTOPEN_FULLPATH(SQLite3Jni.SQLITE_CANTOPEN_FULLPATH),
+ SQLITE_CANTOPEN_CONVPATH(SQLite3Jni.SQLITE_CANTOPEN_CONVPATH),
+ SQLITE_CANTOPEN_SYMLINK(SQLite3Jni.SQLITE_CANTOPEN_SYMLINK),
+ SQLITE_CORRUPT_VTAB(SQLite3Jni.SQLITE_CORRUPT_VTAB),
+ SQLITE_CORRUPT_SEQUENCE(SQLite3Jni.SQLITE_CORRUPT_SEQUENCE),
+ SQLITE_CORRUPT_INDEX(SQLite3Jni.SQLITE_CORRUPT_INDEX),
+ SQLITE_READONLY_RECOVERY(SQLite3Jni.SQLITE_READONLY_RECOVERY),
+ SQLITE_READONLY_CANTLOCK(SQLite3Jni.SQLITE_READONLY_CANTLOCK),
+ SQLITE_READONLY_ROLLBACK(SQLite3Jni.SQLITE_READONLY_ROLLBACK),
+ SQLITE_READONLY_DBMOVED(SQLite3Jni.SQLITE_READONLY_DBMOVED),
+ SQLITE_READONLY_CANTINIT(SQLite3Jni.SQLITE_READONLY_CANTINIT),
+ SQLITE_READONLY_DIRECTORY(SQLite3Jni.SQLITE_READONLY_DIRECTORY),
+ SQLITE_ABORT_ROLLBACK(SQLite3Jni.SQLITE_ABORT_ROLLBACK),
+ SQLITE_CONSTRAINT_CHECK(SQLite3Jni.SQLITE_CONSTRAINT_CHECK),
+ SQLITE_CONSTRAINT_COMMITHOOK(SQLite3Jni.SQLITE_CONSTRAINT_COMMITHOOK),
+ SQLITE_CONSTRAINT_FOREIGNKEY(SQLite3Jni.SQLITE_CONSTRAINT_FOREIGNKEY),
+ SQLITE_CONSTRAINT_FUNCTION(SQLite3Jni.SQLITE_CONSTRAINT_FUNCTION),
+ SQLITE_CONSTRAINT_NOTNULL(SQLite3Jni.SQLITE_CONSTRAINT_NOTNULL),
+ SQLITE_CONSTRAINT_PRIMARYKEY(SQLite3Jni.SQLITE_CONSTRAINT_PRIMARYKEY),
+ SQLITE_CONSTRAINT_TRIGGER(SQLite3Jni.SQLITE_CONSTRAINT_TRIGGER),
+ SQLITE_CONSTRAINT_UNIQUE(SQLite3Jni.SQLITE_CONSTRAINT_UNIQUE),
+ SQLITE_CONSTRAINT_VTAB(SQLite3Jni.SQLITE_CONSTRAINT_VTAB),
+ SQLITE_CONSTRAINT_ROWID(SQLite3Jni.SQLITE_CONSTRAINT_ROWID),
+ SQLITE_CONSTRAINT_PINNED(SQLite3Jni.SQLITE_CONSTRAINT_PINNED),
+ SQLITE_CONSTRAINT_DATATYPE(SQLite3Jni.SQLITE_CONSTRAINT_DATATYPE),
+ SQLITE_NOTICE_RECOVER_WAL(SQLite3Jni.SQLITE_NOTICE_RECOVER_WAL),
+ SQLITE_NOTICE_RECOVER_ROLLBACK(SQLite3Jni.SQLITE_NOTICE_RECOVER_ROLLBACK),
+ SQLITE_WARNING_AUTOINDEX(SQLite3Jni.SQLITE_WARNING_AUTOINDEX),
+ SQLITE_AUTH_USER(SQLite3Jni.SQLITE_AUTH_USER),
+ SQLITE_OK_LOAD_PERMANENTLY(SQLite3Jni.SQLITE_OK_LOAD_PERMANENTLY);
+
+ public final int value;
+
+ ResultCode(int rc){
+ value = rc;
+ ResultCodeMap.set(rc, this);
+ }
+
+ /**
+ Returns the entry from this enum for the given result code, or
+ null if no match is found.
+ */
+ public static ResultCode getEntryForInt(int rc){
+ return ResultCodeMap.get(rc);
+ }
+
+ /**
+ Internal level of indirection required because we cannot initialize
+ static enum members in an enum before the enum constructor is
+ invoked.
+ */
+ private static final class ResultCodeMap {
+ private static final java.util.Map i2e
+ = new java.util.HashMap<>();
+ private static void set(int rc, ResultCode e){ i2e.put(rc, e); }
+ private static ResultCode get(int rc){ return i2e.get(rc); }
+ }
+
+}
diff --git a/ext/jni/src/org/sqlite/jni/RollbackHook.java b/ext/jni/src/org/sqlite/jni/RollbackHook.java
new file mode 100644
index 000000000..4ce3cb93e
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/RollbackHook.java
@@ -0,0 +1,25 @@
+/*
+** 2023-07-22
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ Callback proxy for use with sqlite3_rollback_hook().
+*/
+public interface RollbackHook {
+ /**
+ Works as documented for the sqlite3_rollback_hook() callback.
+ Must not throw.
+ */
+ void xRollbackHook();
+}
diff --git a/ext/jni/src/org/sqlite/jni/SQLFunction.java b/ext/jni/src/org/sqlite/jni/SQLFunction.java
new file mode 100644
index 000000000..21e5fe622
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/SQLFunction.java
@@ -0,0 +1,172 @@
+/*
+** 2023-07-22
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ SQLFunction is used in conjunction with the
+ sqlite3_create_function() JNI-bound API to give that native code
+ access to the callback functions needed in order to implement SQL
+ functions in Java.
+
+ This class is not used by itself, but is a marker base class. The
+ three UDF types are modelled by the inner classes Scalar,
+ Aggregate, and Window. Most simply, clients may create
+ anonymous classes from those to implement UDFs. Clients are free to
+ create their own classes for use with UDFs, so long as they conform
+ to the public interfaces defined by those three classes. The JNI
+ layer only actively relies on the SQLFunction base class.
+*/
+public abstract class SQLFunction {
+
+ /**
+ PerContextState assists aggregate and window functions in
+ managinga their accumulator state across calls to the UDF's
+ callbacks.
+
+ If a given aggregate or window function is called multiple times
+ in a single SQL statement, e.g. SELECT MYFUNC(A), MYFUNC(B)...,
+ then the clients need some way of knowing which call is which so
+ that they can map their state between their various UDF callbacks
+ and reset it via xFinal(). This class takes care of such
+ mappings.
+
+ This class works by mapping
+ sqlite3_context.getAggregateContext() to a single piece of
+ state, of a client-defined type (the T part of this class), which
+ persists across a "matching set" of the UDF's callbacks.
+
+ This class is a helper providing commonly-needed functionality -
+ it is not required for use with aggregate or window functions.
+ Client UDFs are free to perform such mappings using custom
+ approaches. The provided Aggregate and Window classes
+ use this.
+ */
+ public static final class PerContextState {
+ private final java.util.Map> map
+ = new java.util.HashMap<>();
+
+ /**
+ Should be called from a UDF's xStep(), xValue(), and xInverse()
+ methods, passing it that method's first argument and an initial
+ value for the persistent state. If there is currently no
+ mapping for cx.getAggregateContext() within the map, one is
+ created using the given initial value, else the existing one is
+ used and the 2nd argument is ignored. It returns a
+ ValueHolder which can be used to modify that state directly
+ without requiring that the client update the underlying map's
+ entry.
+
+ T must be of a type which can be legally stored as a value in
+ java.util.HashMap.
+ */
+ public ValueHolder getAggregateState(sqlite3_context cx, T initialValue){
+ ValueHolder rc = map.get(cx.getAggregateContext());
+ if(null == rc){
+ map.put(cx.getAggregateContext(), rc = new ValueHolder<>(initialValue));
+ }
+ return rc;
+ }
+
+ /**
+ Should be called from a UDF's xFinal() method and passed that
+ method's first argument. This function removes the value
+ associated with cx.getAggregateContext() from the map and
+ returns it, returning null if no other UDF method has been
+ called to set up such a mapping. The latter condition will be
+ the case if a UDF is used in a statement which has no result
+ rows.
+ */
+ public T takeAggregateState(sqlite3_context cx){
+ final ValueHolder h = map.remove(cx.getAggregateContext());
+ return null==h ? null : h.value;
+ }
+ }
+
+ //! Subclass for creating scalar functions.
+ public static abstract class Scalar extends SQLFunction {
+
+ //! As for the xFunc() argument of the C API's sqlite3_create_function()
+ public abstract void xFunc(sqlite3_context cx, sqlite3_value[] args);
+
+ /**
+ Optionally override to be notified when the UDF is finalized by
+ SQLite.
+ */
+ public void xDestroy() {}
+ }
+
+ /**
+ SQLFunction Subclass for creating aggregate functions. Its T is
+ the data type of its "accumulator" state, an instance of which is
+ intended to be be managed using the getAggregateState() and
+ takeAggregateState() methods.
+ */
+ public static abstract class Aggregate extends SQLFunction {
+
+ //! As for the xStep() argument of the C API's sqlite3_create_function()
+ public abstract void xStep(sqlite3_context cx, sqlite3_value[] args);
+
+ //! As for the xFinal() argument of the C API's sqlite3_create_function()
+ public abstract void xFinal(sqlite3_context cx);
+
+ /**
+ Optionally override to be notified when the UDF is finalized by
+ SQLite.
+ */
+ public void xDestroy() {}
+
+ //! Per-invocation state for the UDF.
+ private final PerContextState map = new PerContextState<>();
+
+ /**
+ To be called from the implementation's xStep() method, as well
+ as the xValue() and xInverse() methods of the Window
+ subclass, to fetch the current per-call UDF state. On the
+ first call to this method for any given sqlite3_context
+ argument, the context is set to the given initial value. On all other
+ calls, the 2nd argument is ignored.
+
+ @see PerContextState#takeAggregateState()
+ */
+ protected final ValueHolder getAggregateState(sqlite3_context cx, T initialValue){
+ return map.getAggregateState(cx, initialValue);
+ }
+
+ /**
+ To be called from the implementation's xFinal() method to fetch
+ the final state of the UDF and remove its mapping.
+
+ @see PerContextState#takeAggregateState()
+ */
+ protected final T takeAggregateState(sqlite3_context cx){
+ return map.takeAggregateState(cx);
+ }
+ }
+
+ /**
+ An SQLFunction subclass for creating window functions. Note that
+ Window inherits from Aggregate and each instance is
+ required to implement the inherited abstract methods from that
+ class. See Aggregate for information on managing the UDF's
+ invocation-specific state.
+ */
+ public static abstract class Window extends Aggregate {
+
+ //! As for the xInverse() argument of the C API's sqlite3_create_window_function()
+ public abstract void xInverse(sqlite3_context cx, sqlite3_value[] args);
+
+ //! As for the xValue() argument of the C API's sqlite3_create_window_function()
+ public abstract void xValue(sqlite3_context cx);
+ }
+}
diff --git a/ext/jni/src/org/sqlite/jni/SQLite3Jni.java b/ext/jni/src/org/sqlite/jni/SQLite3Jni.java
new file mode 100644
index 000000000..9b2a17650
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/SQLite3Jni.java
@@ -0,0 +1,1645 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file declares JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+import java.nio.charset.StandardCharsets;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+
+/**
+ This annotation is for flagging parameters which may legally be
+ null, noting that they may behave differently if passed null but
+ are prepared to expect null as a value.
+
+ This annotation is solely for the reader's information.
+*/
+@Documented
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.PARAMETER)
+@interface Nullable{}
+
+/**
+ This annotation is for flagging parameters which may not legally be
+ null. Note that the C-style API does _not_ throw any
+ NullPointerExceptions on its own because it has a no-throw policy
+ in order to retain its C-style semantics.
+
+ This annotation is solely for the reader's information. No policy
+ is in place to programmatically ensure that NotNull is conformed to
+ in client code.
+*/
+@Documented
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.PARAMETER)
+@interface NotNull{}
+
+/**
+ This class contains the entire sqlite3 JNI API binding. For
+ client-side use, a static import is recommended:
+
+ ```
+ import static org.sqlite.jni.SQLite3Jni.*;
+ ```
+
+ The C-side part can be found in sqlite3-jni.c.
+
+
+ Only functions which materially differ from their C counterparts
+ are documented here. The C documetation is otherwise applicable
+ here:
+
+ https://sqlite.org/c3ref/intro.html
+
+ A handful of Java-specific APIs have been added.
+
+
+ ******************************************************************
+ *** Warning regarding Java's Modified UTF-8 vs standard UTF-8: ***
+ ******************************************************************
+
+ SQLite internally uses UTF-8 encoding, whereas Java natively uses
+ UTF-16. Java JNI has routines for converting to and from UTF-8,
+ _but_ JNI uses what its docs call modified UTF-8 (see links below)
+ Care must be taken when converting Java strings to or from standard
+ UTF-8 to ensure that the proper conversion is performed. In short,
+ Java's `String.getBytes(StandardCharsets.UTF_8)` performs the proper
+ conversion in Java, and there are no JNI C APIs for that conversion
+ (JNI's `NewStringUTF()` requires its input to be in MUTF-8).
+
+ The known consequences and limitations this discrepancy places on
+ the SQLite3 JNI binding include:
+
+ - Any functions which return client-side data from a database
+ take extra care to perform proper conversion, at the cost of
+ efficiency.
+
+ - Functions which return database identifiers require those
+ identifiers to have identical representations in UTF-8 and
+ MUTF-8. They do not perform such conversions (A) because of the
+ much lower risk of an encoding discrepancy and (B) to avoid
+ significant extra code involved (see both the Java- and C-side
+ implementations of sqlite3_db_filename() for an example). Names
+ of databases, tables, columns, collations, and functions MUST NOT
+ contain characters which differ in MUTF-8 and UTF-8, or certain
+ APIs will mis-translate them on their way between languages
+ (possibly leading to a crash).
+
+ - C functions which take C-style strings without a length argument
+ require special care when taking input from Java. In particular,
+ Java strings converted to byte arrays for encoding purposes are
+ not NUL-terminated, and conversion to a Java byte array must be
+ careful to add one. Functions which take a length do not require
+ this. Search the SQLite3Jni class for "\0" for many examples.
+
+ - Similarly, C-side code which deals with strings which might not be
+ NUL-terminated (e.g. while tokenizing in FTS5-related code) cannot
+ use JNI's new-string functions to return them to Java because none
+ of those APIs take a string-length argument. Such cases must
+ return byte arrays instead of strings.
+
+ Further reading:
+
+ - https://stackoverflow.com/questions/57419723
+ - https://stackoverflow.com/questions/7921016
+ - https://itecnote.com/tecnote/java-getting-true-utf-8-characters-in-java-jni/
+ - https://docs.oracle.com/javase/8/docs/api/java/lang/Character.html#unicode
+ - https://docs.oracle.com/javase/8/docs/api/java/io/DataInput.html#modified-utf-8
+
+*/
+public final class SQLite3Jni {
+ static {
+ System.loadLibrary("sqlite3-jni");
+ }
+ //! Not used
+ private SQLite3Jni(){}
+ //! Called from static init code.
+ private static native void init();
+
+ /**
+ Each thread which uses the SQLite3 JNI APIs should call
+ uncacheJniEnv() when it is done with the library - either right
+ before it terminates or when it is finished using the SQLite API.
+ This will clean up any cached per-JNIEnv info. Calling into the
+ library again after that "should" re-initialize the cache on
+ demand, but that's untested.
+
+ This call forcibly wipes out all cached information for the
+ current JNIEnv, a side-effect of which is that behavior is
+ undefined if any database objects are (A) still active at the
+ time it is called _and_ (B) calls are subsequently made into the
+ library with such a database. Doing so will, at best, lead to a
+ crash. Azt worst, it will lead to the db possibly misbehaving
+ because some of its Java-bound state has been cleared. There is
+ no immediate harm in (A) so long as condition (B) is not met.
+ This process does _not_ actually close any databases or finalize
+ any prepared statements. For proper library behavior, and to
+ avoid C-side leaks, be sure to close them before calling this
+ function.
+
+ Calling this from the main application thread is not strictly
+ required but is "polite." Additional threads must call this
+ before ending or they will leak cache entries in the C heap,
+ which in turn may keep numerous Java-side global references
+ active.
+
+ This routine returns false without side effects if the current
+ JNIEnv is not cached, else returns true, but this information is
+ primarily for testing of the JNI bindings and is not information
+ which client-level code should use to make any informed
+ decisions.
+ */
+ public static synchronized native boolean uncacheJniEnv();
+
+ //////////////////////////////////////////////////////////////////////
+ // Maintenance reminder: please keep the sqlite3_.... functions
+ // alphabetized. The SQLITE_... values. on the other hand, are
+ // grouped by category.
+
+
+ /**
+ Functions almost as documented for the C API, with these
+ exceptions:
+
+ - The callback interface is more limited because of
+ cross-language differences. Specifically, auto-extensions do
+ not have access to the sqlite3_api object which native
+ auto-extensions do.
+
+ - If an auto-extension opens a db, thereby triggering recursion
+ in the auto-extension handler, it will fail with a message
+ explaining that recursion is not permitted.
+
+ - All of the other auto extension routines will fail without side
+ effects if invoked from within the execution of an
+ auto-extension. i.e. auto extensions can neither be added,
+ removed, nor cleared while one registered with this function is
+ running. Auto-extensions registered directly with the library
+ via C code, as opposed to indirectly via Java, do not have that
+ limitation.
+
+ See the AutoExtension class docs for more information.
+
+ Achtung: it is as yet unknown whether auto extensions registered
+ from one JNIEnv (thread) can be safely called from another.
+ */
+ public static synchronized native int sqlite3_auto_extension(@NotNull AutoExtension callback);
+
+ public static int sqlite3_bind_blob(
+ @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data
+ ){
+ return (null == data)
+ ? sqlite3_bind_null(stmt, ndx)
+ : sqlite3_bind_blob(stmt, ndx, data, data.length);
+ }
+
+ private static synchronized native int sqlite3_bind_blob(
+ @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data, int n
+ );
+
+ public static synchronized native int sqlite3_bind_double(
+ @NotNull sqlite3_stmt stmt, int ndx, double v
+ );
+
+ public static synchronized native int sqlite3_bind_int(
+ @NotNull sqlite3_stmt stmt, int ndx, int v
+ );
+
+ public static synchronized native int sqlite3_bind_int64(
+ @NotNull sqlite3_stmt stmt, int ndx, long v
+ );
+
+ public static synchronized native int sqlite3_bind_null(
+ @NotNull sqlite3_stmt stmt, int ndx
+ );
+
+ public static synchronized native int sqlite3_bind_parameter_count(
+ @NotNull sqlite3_stmt stmt
+ );
+
+
+ /** A level of indirection required to ensure that the input to the
+ C-level function of the same name is a NUL-terminated UTF-8
+ string. */
+ private static synchronized native int sqlite3_bind_parameter_index(
+ @NotNull sqlite3_stmt stmt, byte[] paramName
+ );
+
+ public static int sqlite3_bind_parameter_index(
+ @NotNull sqlite3_stmt stmt, @NotNull String paramName
+ ){
+ final byte[] utf8 = (paramName+"\0").getBytes(StandardCharsets.UTF_8);
+ return sqlite3_bind_parameter_index(stmt, utf8);
+ }
+
+ public static int sqlite3_bind_text(
+ @NotNull sqlite3_stmt stmt, int ndx, @Nullable String data
+ ){
+ if(null == data) return sqlite3_bind_null(stmt, ndx);
+ final byte[] utf8 = data.getBytes(StandardCharsets.UTF_8);
+ return sqlite3_bind_text(stmt, ndx, utf8, utf8.length);
+ }
+
+ public static int sqlite3_bind_text(
+ @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data
+ ){
+ return (null == data)
+ ? sqlite3_bind_null(stmt, ndx)
+ : sqlite3_bind_text(stmt, ndx, data, data.length);
+ }
+
+ /**
+ Works like the C-level sqlite3_bind_text() but (A) assumes
+ SQLITE_TRANSIENT for the final parameter and (B) behaves like
+ sqlite3_bind_null() if the data argument is null.
+ */
+ private static synchronized native int sqlite3_bind_text(
+ @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data, int maxBytes
+ );
+
+ public static synchronized native int sqlite3_bind_zeroblob(
+ @NotNull sqlite3_stmt stmt, int ndx, int n
+ );
+
+ public static synchronized native int sqlite3_bind_zeroblob64(
+ @NotNull sqlite3_stmt stmt, int ndx, long n
+ );
+
+ /**
+ As for the C-level function of the same name, with a BusyHandler
+ instance in place of a callback function. Pass it a null handler
+ to clear the busy handler. Calling this multiple times with the
+ same object is a no-op on the second and subsequent calls.
+ */
+ public static synchronized native int sqlite3_busy_handler(
+ @NotNull sqlite3 db, @Nullable BusyHandler handler
+ );
+
+ public static synchronized native int sqlite3_busy_timeout(
+ @NotNull sqlite3 db, int ms
+ );
+
+ /**
+ Works like the C API except that it returns false, without side
+ effects, if auto extensions are currently running. (The JNI-level
+ list of extensions cannot be manipulated while it is being traversed.)
+ */
+ public static synchronized native boolean sqlite3_cancel_auto_extension(
+ @NotNull AutoExtension ax
+ );
+
+ public static synchronized native int sqlite3_changes(
+ @NotNull sqlite3 db
+ );
+
+ public static synchronized native long sqlite3_changes64(
+ @NotNull sqlite3 db
+ );
+
+ public static synchronized native int sqlite3_clear_bindings(
+ @NotNull sqlite3_stmt stmt
+ );
+
+ public static synchronized native int sqlite3_close(
+ @NotNull sqlite3 db
+ );
+
+ public static synchronized native int sqlite3_close_v2(
+ @NotNull sqlite3 db
+ );
+
+ public static synchronized native byte[] sqlite3_column_blob(
+ @NotNull sqlite3_stmt stmt, int ndx
+ );
+
+ public static synchronized native int sqlite3_column_bytes(
+ @NotNull sqlite3_stmt stmt, int ndx
+ );
+
+ public static synchronized native int sqlite3_column_bytes16(
+ @NotNull sqlite3_stmt stmt, int ndx
+ );
+
+ public static synchronized native int sqlite3_column_count(
+ @NotNull sqlite3_stmt stmt
+ );
+
+ public static synchronized native double sqlite3_column_double(
+ @NotNull sqlite3_stmt stmt, int ndx
+ );
+
+ public static synchronized native int sqlite3_column_int(
+ @NotNull sqlite3_stmt stmt, int ndx
+ );
+
+ public static synchronized native long sqlite3_column_int64(
+ @NotNull sqlite3_stmt stmt, int ndx
+ );
+
+ public static synchronized native String sqlite3_column_name(
+ @NotNull sqlite3_stmt stmt, int ndx
+ );
+
+ public static synchronized native String sqlite3_column_database_name(
+ @NotNull sqlite3_stmt stmt, int ndx
+ );
+
+ /**
+ Column counterpart of sqlite3_value_java_object().
+ */
+ public static Object sqlite3_column_java_object(
+ @NotNull sqlite3_stmt stmt, int ndx
+ ){
+ Object rv = null;
+ sqlite3_value v = sqlite3_column_value(stmt, ndx);
+ if(null!=v){
+ v = sqlite3_value_dupe(v) /* we need a "protected" value */;
+ if(null!=v){
+ rv = sqlite3_value_java_object(v);
+ sqlite3_value_free(v);
+ }
+ }
+ return rv;
+ }
+
+ /**
+ Column counterpart of sqlite3_value_java_casted().
+ */
+ @SuppressWarnings("unchecked")
+ public static T sqlite3_column_java_casted(
+ @NotNull sqlite3_stmt stmt, int ndx, @NotNull Class type
+ ){
+ final Object o = sqlite3_column_java_object(stmt, ndx);
+ return type.isInstance(o) ? (T)o : null;
+ }
+
+ public static synchronized native String sqlite3_column_origin_name(
+ @NotNull sqlite3_stmt stmt, int ndx
+ );
+
+ public static synchronized native String sqlite3_column_table_name(
+ @NotNull sqlite3_stmt stmt, int ndx
+ );
+
+ /**
+ To extract _standard_ UTF-8, use sqlite3_column_text().
+ This API includes no functions for working with Java's Modified
+ UTF-8.
+ */
+ public static synchronized native String sqlite3_column_text16(
+ @NotNull sqlite3_stmt stmt, int ndx
+ );
+
+ /**
+ Returns the given column's contents as UTF-8-encoded (not MUTF-8) text.
+ Use sqlite3_column_text16() to fetch the text
+ */
+ public static synchronized native byte[] sqlite3_column_text(
+ @NotNull sqlite3_stmt stmt, int ndx
+ );
+
+ // The real utility of this function is questionable.
+ // /**
+ // Returns a Java value representation based on the value of
+ // sqlite_value_type(). For integer types it returns either Integer
+ // or Long, depending on whether the value will fit in an
+ // Integer. For floating-point values it always returns type Double.
+
+ // If the column was bound using sqlite3_result_java_object() then
+ // that value, as an Object, is returned.
+ // */
+ // public static Object sqlite3_column_to_java(@NotNull sqlite3_stmt stmt,
+ // int ndx){
+ // sqlite3_value v = sqlite3_column_value(stmt, ndx);
+ // Object rv = null;
+ // if(null == v) return v;
+ // v = sqlite3_value_dup(v)/*need a protected value*/;
+ // if(null == v) return v /* OOM error in C */;
+ // if(112/* 'p' */ == sqlite3_value_subtype(v)){
+ // rv = sqlite3_value_java_object(v);
+ // }else{
+ // switch(sqlite3_value_type(v)){
+ // case SQLITE_INTEGER: {
+ // final long i = sqlite3_value_int64(v);
+ // rv = (i<=0x7fffffff && i>=-0x7fffffff-1)
+ // ? new Integer((int)i) : new Long(i);
+ // break;
+ // }
+ // case SQLITE_FLOAT: rv = new Double(sqlite3_value_double(v)); break;
+ // case SQLITE_BLOB: rv = sqlite3_value_blob(v); break;
+ // case SQLITE_TEXT: rv = sqlite3_value_text(v); break;
+ // default: break;
+ // }
+ // }
+ // sqlite3_value_free(v);
+ // return rv;
+ // }
+
+ public static synchronized native int sqlite3_column_type(
+ @NotNull sqlite3_stmt stmt, int ndx
+ );
+
+ public static synchronized native sqlite3_value sqlite3_column_value(
+ @NotNull sqlite3_stmt stmt, int ndx
+ );
+
+ /**
+ This functions like C's sqlite3_collation_needed16() because
+ Java's string type is compatible with that interface.
+ */
+ public static synchronized native int sqlite3_collation_needed(
+ @NotNull sqlite3 db, @Nullable CollationNeeded callback
+ );
+
+ /**
+ Returns the db handle passed to sqlite3_open() or
+ sqlite3_open_v2(), as opposed to a new wrapper object.
+ */
+ public static synchronized native sqlite3 sqlite3_context_db_handle(
+ @NotNull sqlite3_context cx
+ );
+
+ public static synchronized native CommitHook sqlite3_commit_hook(
+ @NotNull sqlite3 db, @Nullable CommitHook hook
+ );
+
+ public static native String sqlite3_compileoption_get(
+ int n
+ );
+
+ public static native boolean sqlite3_compileoption_used(
+ @NotNull String optName
+ );
+
+ public static synchronized native int sqlite3_create_collation(
+ @NotNull sqlite3 db, @NotNull String name, int eTextRep,
+ @NotNull Collation col
+ );
+
+ /**
+ The Java counterpart to the C-native sqlite3_create_function(),
+ sqlite3_create_function_v2(), and
+ sqlite3_create_window_function(). Which one it behaves like
+ depends on which methods the final argument implements. See
+ SQLFunction's inner classes (Scalar, Aggregate, and Window)
+ for details.
+ */
+ public static synchronized native int sqlite3_create_function(
+ @NotNull sqlite3 db, @NotNull String functionName,
+ int nArg, int eTextRep, @NotNull SQLFunction func
+ );
+
+ public static synchronized native int sqlite3_data_count(
+ @NotNull sqlite3_stmt stmt
+ );
+
+ public static synchronized native String sqlite3_db_filename(
+ @NotNull sqlite3 db, @NotNull String dbName
+ );
+
+ /**
+ Overload for sqlite3_db_config() calls which take (int,int*)
+ variadic arguments. Returns SQLITE_MISUSE if op is not one of the
+ SQLITE_DBCONFIG_... options which uses this call form.
+ */
+ public static synchronized native int sqlite3_db_config(
+ @NotNull sqlite3 db, int op, int onOff, @Nullable OutputPointer.Int32 out
+ );
+
+ /**
+ Overload for sqlite3_db_config() calls which take a (const char*)
+ variadic argument. As of SQLite3 v3.43 the only such option is
+ SQLITE_DBCONFIG_MAINDBNAME. Returns SQLITE_MISUSE if op is not
+ SQLITE_DBCONFIG_MAINDBNAME, but that set of options may be
+ extended in future versions.
+ */
+ public static synchronized native int sqlite3_db_config(
+ @NotNull sqlite3 db, int op, @NotNull String val
+ );
+
+ public static synchronized native int sqlite3_db_status(
+ @NotNull sqlite3 db, int op, @NotNull OutputPointer.Int32 pCurrent,
+ @NotNull OutputPointer.Int32 pHighwater, boolean reset
+ );
+
+ public static synchronized native int sqlite3_errcode(@NotNull sqlite3 db);
+
+ public static synchronized native String sqlite3_expanded_sql(@NotNull sqlite3_stmt stmt);
+
+ public static synchronized native int sqlite3_extended_errcode(@NotNull sqlite3 db);
+
+ public static synchronized native boolean sqlite3_extended_result_codes(
+ @NotNull sqlite3 db, boolean onoff
+ );
+
+ public static synchronized native String sqlite3_errmsg(@NotNull sqlite3 db);
+
+ public static synchronized native String sqlite3_errstr(int resultCode);
+
+ /**
+ Note that the offset values assume UTF-8-encoded SQL.
+ */
+ public static synchronized native int sqlite3_error_offset(@NotNull sqlite3 db);
+
+ public static synchronized native int sqlite3_finalize(@NotNull sqlite3_stmt stmt);
+
+ public static synchronized native int sqlite3_initialize();
+
+ public static synchronized native long sqlite3_last_insert_rowid(@NotNull sqlite3 db);
+
+ public static synchronized native String sqlite3_libversion();
+
+ public static synchronized native int sqlite3_libversion_number();
+
+ /**
+ Works like its C counterpart and makes the native pointer of the
+ underling (sqlite3*) object available via
+ ppDb.getNativePointer(). That pointer is necessary for looking up
+ the JNI-side native, but clients need not pay it any
+ heed. Passing the object to sqlite3_close() or sqlite3_close_v2()
+ will clear that pointer mapping.
+
+ Recall that even if opening fails, the output pointer might be
+ non-null. Any error message about the failure will be in that
+ object and it is up to the caller to sqlite3_close() that
+ db handle.
+
+ Pedantic note: though any number of Java-level sqlite3 objects
+ may refer to/wrap a single C-level (sqlite3*), the JNI internals
+ take a reference to the object which is passed to sqlite3_open()
+ or sqlite3_open_v2() so that they have a predictible object to
+ pass to, e.g., the sqlite3_collation_needed() callback.
+ */
+ public static synchronized native int sqlite3_open(
+ @Nullable String filename, @NotNull OutputPointer.sqlite3 ppDb
+ );
+
+ public static synchronized native int sqlite3_open_v2(
+ @Nullable String filename, @NotNull OutputPointer.sqlite3 ppDb,
+ int flags, @Nullable String zVfs
+ );
+
+ /**
+ The sqlite3_prepare() family of functions require slightly
+ different signatures than their native counterparts, but
+ overloading allows us to install several convenience forms.
+
+ All of them which take their SQL in the form of a byte[] require
+ that it be in UTF-8 encoding unless explicitly noted otherwise.
+
+ The forms which take a "tail" output pointer return (via that
+ output object) the index into their SQL byte array at which the
+ end of the first SQL statement processed by the call was
+ found. That's fundamentally how the C APIs work but making use of
+ that value requires more copying of the input SQL into
+ consecutively smaller arrays in order to consume all of
+ it. (There is an example of doing that in this project's Tester1
+ class.) For that vast majority of uses, that capability is not
+ necessary, however, and overloads are provided which gloss over
+ that.
+ */
+ private static synchronized native int sqlite3_prepare(
+ @NotNull sqlite3 db, @NotNull byte[] sqlUtf8, int maxBytes,
+ @NotNull OutputPointer.sqlite3_stmt outStmt,
+ @Nullable OutputPointer.Int32 pTailOffset
+ );
+
+ public static int sqlite3_prepare(
+ @NotNull sqlite3 db, @NotNull byte[] sqlUtf8,
+ @NotNull OutputPointer.sqlite3_stmt outStmt,
+ @Nullable OutputPointer.Int32 pTailOffset
+ ){
+ return sqlite3_prepare(db, sqlUtf8, sqlUtf8.length, outStmt, pTailOffset);
+ }
+
+ public static int sqlite3_prepare(
+ @NotNull sqlite3 db, @NotNull byte[] sqlUtf8,
+ @NotNull OutputPointer.sqlite3_stmt outStmt
+ ){
+ return sqlite3_prepare(db, sqlUtf8, sqlUtf8.length, outStmt, null);
+ }
+
+ public static int sqlite3_prepare(
+ @NotNull sqlite3 db, @NotNull String sql,
+ @NotNull OutputPointer.sqlite3_stmt outStmt
+ ){
+ final byte[] utf8 = sql.getBytes(StandardCharsets.UTF_8);
+ return sqlite3_prepare(db, utf8, utf8.length, outStmt, null);
+ }
+
+ private static synchronized native int sqlite3_prepare_v2(
+ @NotNull sqlite3 db, @NotNull byte[] sqlUtf8, int maxBytes,
+ @NotNull OutputPointer.sqlite3_stmt outStmt,
+ @Nullable OutputPointer.Int32 pTailOffset
+ );
+
+ public static int sqlite3_prepare_v2(
+ @NotNull sqlite3 db, @NotNull byte[] sqlUtf8,
+ @NotNull OutputPointer.sqlite3_stmt outStmt,
+ @Nullable OutputPointer.Int32 pTailOffset
+ ){
+ return sqlite3_prepare_v2(db, sqlUtf8, sqlUtf8.length, outStmt, pTailOffset);
+ }
+
+ public static int sqlite3_prepare_v2(
+ @NotNull sqlite3 db, @NotNull byte[] sqlUtf8,
+ @NotNull OutputPointer.sqlite3_stmt outStmt
+ ){
+ return sqlite3_prepare_v2(db, sqlUtf8, sqlUtf8.length, outStmt, null);
+ }
+
+ public static int sqlite3_prepare_v2(
+ @NotNull sqlite3 db, @NotNull String sql,
+ @NotNull OutputPointer.sqlite3_stmt outStmt
+ ){
+ final byte[] utf8 = sql.getBytes(StandardCharsets.UTF_8);
+ return sqlite3_prepare_v2(db, utf8, utf8.length, outStmt, null);
+ }
+
+ private static synchronized native int sqlite3_prepare_v3(
+ @NotNull sqlite3 db, @NotNull byte[] sqlUtf8, int maxBytes,
+ int prepFlags, @NotNull OutputPointer.sqlite3_stmt outStmt,
+ @Nullable OutputPointer.Int32 pTailOffset
+ );
+
+ public static int sqlite3_prepare_v3(
+ @NotNull sqlite3 db, @NotNull byte[] sqlUtf8, int prepFlags,
+ @NotNull OutputPointer.sqlite3_stmt outStmt,
+ @Nullable OutputPointer.Int32 pTailOffset
+ ){
+ return sqlite3_prepare_v3(db, sqlUtf8, sqlUtf8.length, prepFlags, outStmt, pTailOffset);
+ }
+
+ public static int sqlite3_prepare_v3(
+ @NotNull sqlite3 db, @NotNull byte[] sqlUtf8, int prepFlags,
+ @NotNull OutputPointer.sqlite3_stmt outStmt
+ ){
+ return sqlite3_prepare_v3(db, sqlUtf8, sqlUtf8.length, prepFlags, outStmt, null);
+ }
+
+ public static int sqlite3_prepare_v3(
+ @NotNull sqlite3 db, @NotNull String sql, int prepFlags,
+ @NotNull OutputPointer.sqlite3_stmt outStmt
+ ){
+ final byte[] utf8 = sql.getBytes(StandardCharsets.UTF_8);
+ return sqlite3_prepare_v3(db, utf8, utf8.length, prepFlags, outStmt, null);
+ }
+
+ public static synchronized native void sqlite3_progress_handler(
+ @NotNull sqlite3 db, int n, @Nullable ProgressHandler h
+ );
+
+ //TODO??? void *sqlite3_preupdate_hook(...) and friends
+
+ public static synchronized native int sqlite3_reset(@NotNull sqlite3_stmt stmt);
+
+ /**
+ Works like the C API except that it has no side effects if auto
+ extensions are currently running. (The JNI-level list of
+ extensions cannot be manipulated while it is being traversed.)
+ */
+ public static synchronized native void sqlite3_reset_auto_extension();
+
+ public static synchronized native void sqlite3_result_double(
+ @NotNull sqlite3_context cx, double v
+ );
+
+ /**
+ The main sqlite3_result_error() impl of which all others are
+ proxies. eTextRep must be one of SQLITE_UTF8 or SQLITE_UTF16 and
+ msg must be encoded correspondingly. Any other eTextRep value
+ results in the C-level sqlite3_result_error() being called with
+ a complaint about the invalid argument.
+ */
+ private static synchronized native void sqlite3_result_error(
+ @NotNull sqlite3_context cx, @Nullable byte[] msg,
+ int eTextRep
+ );
+
+ public static void sqlite3_result_error(
+ @NotNull sqlite3_context cx, @NotNull byte[] utf8
+ ){
+ sqlite3_result_error(cx, utf8, SQLITE_UTF8);
+ }
+
+ public static void sqlite3_result_error(
+ @NotNull sqlite3_context cx, @NotNull String msg
+ ){
+ final byte[] utf8 = (msg+"\0").getBytes(StandardCharsets.UTF_8);
+ sqlite3_result_error(cx, utf8, SQLITE_UTF8);
+ }
+
+ public static void sqlite3_result_error16(
+ @NotNull sqlite3_context cx, @Nullable byte[] utf16
+ ){
+ sqlite3_result_error(cx, utf16, SQLITE_UTF16);
+ }
+
+ public static void sqlite3_result_error16(
+ @NotNull sqlite3_context cx, @NotNull String msg
+ ){
+ final byte[] utf8 = (msg+"\0").getBytes(StandardCharsets.UTF_16);
+ sqlite3_result_error(cx, utf8, SQLITE_UTF16);
+ }
+
+ public static void sqlite3_result_error(
+ @NotNull sqlite3_context cx, @NotNull Exception e
+ ){
+ sqlite3_result_error(cx, e.getMessage());
+ }
+
+ public static void sqlite3_result_error16(
+ @NotNull sqlite3_context cx, @NotNull Exception e
+ ){
+ sqlite3_result_error16(cx, e.getMessage());
+ }
+
+ public static synchronized native void sqlite3_result_error_toobig(
+ @NotNull sqlite3_context cx
+ );
+
+ public static synchronized native void sqlite3_result_error_nomem(
+ @NotNull sqlite3_context cx
+ );
+
+ public static synchronized native void sqlite3_result_error_code(
+ @NotNull sqlite3_context cx, int c
+ );
+
+ public static synchronized native void sqlite3_result_null(
+ @NotNull sqlite3_context cx
+ );
+
+ public static synchronized native void sqlite3_result_int(
+ @NotNull sqlite3_context cx, int v
+ );
+
+ public static synchronized native void sqlite3_result_int64(
+ @NotNull sqlite3_context cx, long v
+ );
+
+ /**
+ Binds the SQL result to the given object, or
+ sqlite3_result_null() if o is null. Use
+ sqlite3_value_java_object() or sqlite3_column_java_object() to
+ fetch it.
+
+ This is implemented in terms of sqlite3_result_pointer(), but
+ that function is not exposed to JNI because its 3rd argument must
+ be a constant string (the library does not copy it), which we
+ cannot implement cross-language here unless, in the JNI layer, we
+ allocate such strings and store them somewhere for long-term use
+ (leaking them more likely than not). Even then, passing around a
+ pointer via Java like that has little practical use.
+
+ Note that there is no sqlite3_bind_java_object() counterpart.
+ */
+ public static synchronized native void sqlite3_result_java_object(
+ @NotNull sqlite3_context cx, @NotNull Object o
+ );
+
+ public static void sqlite3_result_set(
+ @NotNull sqlite3_context cx, @NotNull Integer v
+ ){
+ sqlite3_result_int(cx, v);
+ }
+
+ public static void sqlite3_result_set(
+ @NotNull sqlite3_context cx, int v
+ ){
+ sqlite3_result_int(cx, v);
+ }
+
+ public static void sqlite3_result_set(
+ @NotNull sqlite3_context cx, @NotNull Boolean v
+ ){
+ sqlite3_result_int(cx, v ? 1 : 0);
+ }
+
+ public static void sqlite3_result_set(
+ @NotNull sqlite3_context cx, boolean v
+ ){
+ sqlite3_result_int(cx, v ? 1 : 0);
+ }
+
+ public static void sqlite3_result_set(
+ @NotNull sqlite3_context cx, @NotNull Long v
+ ){
+ sqlite3_result_int64(cx, v);
+ }
+
+ public static void sqlite3_result_set(
+ @NotNull sqlite3_context cx, long v
+ ){
+ sqlite3_result_int64(cx, v);
+ }
+
+ public static void sqlite3_result_set(
+ @NotNull sqlite3_context cx, @NotNull Double v
+ ){
+ sqlite3_result_double(cx, v);
+ }
+
+ public static void sqlite3_result_set(
+ @NotNull sqlite3_context cx, double v
+ ){
+ sqlite3_result_double(cx, v);
+ }
+
+ public static void sqlite3_result_set(
+ @NotNull sqlite3_context cx, @Nullable String v
+ ){
+ sqlite3_result_text(cx, v);
+ }
+
+ public static synchronized native void sqlite3_result_value(
+ @NotNull sqlite3_context cx, @NotNull sqlite3_value v
+ );
+
+ public static synchronized native void sqlite3_result_zeroblob(
+ @NotNull sqlite3_context cx, int n
+ );
+
+ public static synchronized native int sqlite3_result_zeroblob64(
+ @NotNull sqlite3_context cx, long n
+ );
+
+ private static synchronized native void sqlite3_result_blob(
+ @NotNull sqlite3_context cx, @Nullable byte[] blob, int maxLen
+ );
+
+ public static void sqlite3_result_blob(
+ @NotNull sqlite3_context cx, @Nullable byte[] blob
+ ){
+ sqlite3_result_blob(cx, blob, (int)(null==blob ? 0 : blob.length));
+ }
+
+ /**
+ Binds the given text using C's sqlite3_result_blob64() unless:
+
+ - blob is null ==> sqlite3_result_null()
+
+ - blob is too large ==> sqlite3_result_error_toobig()
+
+ If maxLen is larger than blob.length, it is truncated to that
+ value. If it is negative, results are undefined.
+ */
+ private static synchronized native void sqlite3_result_blob64(
+ @NotNull sqlite3_context cx, @Nullable byte[] blob, long maxLen
+ );
+
+ public static void sqlite3_result_blob64(
+ @NotNull sqlite3_context cx, @Nullable byte[] blob
+ ){
+ sqlite3_result_blob64(cx, blob, (long)(null==blob ? 0 : blob.length));
+ }
+
+ private static synchronized native void sqlite3_result_text(
+ @NotNull sqlite3_context cx, @Nullable byte[] text, int maxLen
+ );
+
+ public static void sqlite3_result_text(
+ @NotNull sqlite3_context cx, @Nullable byte[] text
+ ){
+ sqlite3_result_text(cx, text, null==text ? 0 : text.length);
+ }
+
+ public static void sqlite3_result_text(
+ @NotNull sqlite3_context cx, @Nullable String text
+ ){
+ if(null == text) sqlite3_result_null(cx);
+ else{
+ final byte[] utf8 = text.getBytes(StandardCharsets.UTF_8);
+ sqlite3_result_text(cx, utf8, utf8.length);
+ }
+ }
+
+ /**
+ Binds the given text using C's sqlite3_result_text64() unless:
+
+ - text is null ==> sqlite3_result_null()
+
+ - text is too large ==> sqlite3_result_error_toobig()
+
+ - The `encoding` argument has an invalid value ==>
+ sqlite3_result_error_code() with SQLITE_FORMAT
+
+ If maxLength (in bytes, not characters) is larger than
+ text.length, it is silently truncated to text.length. If it is
+ negative, results are undefined.
+ */
+ private static synchronized native void sqlite3_result_text64(
+ @NotNull sqlite3_context cx, @Nullable byte[] text,
+ long maxLength, int encoding
+ );
+
+ public static synchronized native int sqlite3_status(
+ int op, @NotNull OutputPointer.Int32 pCurrent,
+ @NotNull OutputPointer.Int32 pHighwater, boolean reset
+ );
+
+ public static synchronized native int sqlite3_status64(
+ int op, @NotNull OutputPointer.Int64 pCurrent,
+ @NotNull OutputPointer.Int64 pHighwater, boolean reset
+ );
+
+ /**
+ Sets the current UDF result to the given bytes, which are assumed
+ be encoded in UTF-16 using the platform's byte order.
+ */
+ public static void sqlite3_result_text16(
+ @NotNull sqlite3_context cx, @Nullable byte[] text
+ ){
+ sqlite3_result_text64(cx, text, text.length, SQLITE_UTF16);
+ }
+
+ public static void sqlite3_result_text16(
+ @NotNull sqlite3_context cx, @Nullable String text
+ ){
+ if(null == text) sqlite3_result_null(cx);
+ else{
+ final byte[] b = text.getBytes(StandardCharsets.UTF_16);
+ sqlite3_result_text64(cx, b, b.length, SQLITE_UTF16);
+ }
+ }
+
+ /**
+ Sets the current UDF result to the given bytes, which are assumed
+ be encoded in UTF-16LE.
+ */
+ public static void sqlite3_result_text16le(
+ @NotNull sqlite3_context cx, @Nullable String text
+ ){
+ if(null == text) sqlite3_result_null(cx);
+ else{
+ final byte[] b = text.getBytes(StandardCharsets.UTF_16LE);
+ sqlite3_result_text64(cx, b, b.length, SQLITE_UTF16LE);
+ }
+ }
+
+ /**
+ Sets the current UDF result to the given bytes, which are assumed
+ be encoded in UTF-16BE.
+ */
+ public static void sqlite3_result_text16be(
+ @NotNull sqlite3_context cx, @Nullable byte[] text
+ ){
+ sqlite3_result_text64(cx, text, text.length, SQLITE_UTF16BE);
+ }
+
+ public static void sqlite3_result_text16be(
+ @NotNull sqlite3_context cx, @NotNull String text
+ ){
+ final byte[] b = text.getBytes(StandardCharsets.UTF_16BE);
+ sqlite3_result_text64(cx, b, b.length, SQLITE_UTF16BE);
+ }
+
+ public static synchronized native RollbackHook sqlite3_rollback_hook(
+ @NotNull sqlite3 db, @Nullable RollbackHook hook
+ );
+
+ //! Sets or unsets (if auth is null) the current authorizer.
+ public static synchronized native int sqlite3_set_authorizer(
+ @NotNull sqlite3 db, @Nullable Authorizer auth
+ );
+
+ public static synchronized native void sqlite3_set_last_insert_rowid(
+ @NotNull sqlite3 db, long rowid
+ );
+
+ public static synchronized native int sqlite3_sleep(int ms);
+
+ public static synchronized native String sqlite3_sourceid();
+
+ public static synchronized native String sqlite3_sql(@NotNull sqlite3_stmt stmt);
+
+ public static synchronized native int sqlite3_step(@NotNull sqlite3_stmt stmt);
+
+ /**
+ Internal impl of the public sqlite3_strglob() method. Neither argument
+ may be NULL and both _MUST_ be NUL-terminated.
+ */
+ private static synchronized native int sqlite3_strglob(
+ @NotNull byte[] glob, @NotNull byte[] txt
+ );
+
+ public static int sqlite3_strglob(
+ @NotNull String glob, @NotNull String txt
+ ){
+ return sqlite3_strglob(
+ (glob+"\0").getBytes(StandardCharsets.UTF_8),
+ (txt+"\0").getBytes(StandardCharsets.UTF_8)
+ );
+ }
+
+ /**
+ Internal impl of the public sqlite3_strlike() method. Neither
+ argument may be NULL and both _MUST_ be NUL-terminated.
+ */
+ private static synchronized native int sqlite3_strlike(
+ @NotNull byte[] glob, @NotNull byte[] txt, int escChar
+ );
+
+ public static int sqlite3_strlike(
+ @NotNull String glob, @NotNull String txt, char escChar
+ ){
+ return sqlite3_strlike(
+ (glob+"\0").getBytes(StandardCharsets.UTF_8),
+ (txt+"\0").getBytes(StandardCharsets.UTF_8),
+ (int)escChar
+ );
+ }
+
+ public static synchronized native int sqlite3_threadsafe();
+
+ public static synchronized native int sqlite3_total_changes(@NotNull sqlite3 db);
+
+ public static synchronized native long sqlite3_total_changes64(@NotNull sqlite3 db);
+
+ /**
+ Works like C's sqlite3_trace_v2() except that the 3rd argument to that
+ function is elided here because the roles of that functions' 3rd and 4th
+ arguments are encapsulated in the final argument to this function.
+
+ Unlike the C API, which is documented as always returning 0, this
+ implementation returns SQLITE_NOMEM if allocation of per-db
+ mapping state fails and SQLITE_ERROR if the given callback object
+ cannot be processed propertly (i.e. an internal error).
+ */
+ public static synchronized native int sqlite3_trace_v2(
+ @NotNull sqlite3 db, int traceMask, @Nullable Tracer tracer
+ );
+
+ public static synchronized native UpdateHook sqlite3_update_hook(
+ sqlite3 db, UpdateHook hook
+ );
+
+ public static synchronized native byte[] sqlite3_value_blob(@NotNull sqlite3_value v);
+
+ public static synchronized native int sqlite3_value_bytes(@NotNull sqlite3_value v);
+
+ public static synchronized native int sqlite3_value_bytes16(@NotNull sqlite3_value v);
+
+ public static synchronized native double sqlite3_value_double(@NotNull sqlite3_value v);
+
+ public static synchronized native sqlite3_value sqlite3_value_dupe(
+ @NotNull sqlite3_value v
+ );
+
+ public static synchronized native int sqlite3_value_encoding(@NotNull sqlite3_value v);
+
+ public static synchronized native void sqlite3_value_free(@Nullable sqlite3_value v);
+
+ public static synchronized native int sqlite3_value_int(@NotNull sqlite3_value v);
+
+ public static synchronized native long sqlite3_value_int64(@NotNull sqlite3_value v);
+
+ /**
+ If the given value was set using sqlite3_result_java_value() then
+ this function returns that object, else it returns null.
+
+ It is up to the caller to inspect the object to determine its
+ type, and cast it if necessary.
+ */
+ public static synchronized native Object sqlite3_value_java_object(
+ @NotNull sqlite3_value v
+ );
+
+ /**
+ A variant of sqlite3_value_java_object() which returns the
+ fetched object cast to T if the object is an instance of the
+ given Class. It returns null in all other cases.
+ */
+ @SuppressWarnings("unchecked")
+ public static T sqlite3_value_java_casted(@NotNull sqlite3_value v,
+ @NotNull Class type){
+ final Object o = sqlite3_value_java_object(v);
+ return type.isInstance(o) ? (T)o : null;
+ }
+
+ /**
+ See sqlite3_column_text() for notes about encoding conversions.
+ See sqlite3_value_text_utf8() for how to extract text in standard
+ UTF-8.
+ */
+ public static synchronized native String sqlite3_value_text(@NotNull sqlite3_value v);
+
+ /**
+ The sqlite3_value counterpart of sqlite3_column_text_utf8().
+ */
+ public static synchronized native byte[] sqlite3_value_text_utf8(@NotNull sqlite3_value v);
+
+ public static synchronized native byte[] sqlite3_value_text16(@NotNull sqlite3_value v);
+
+ public static synchronized native byte[] sqlite3_value_text16le(@NotNull sqlite3_value v);
+
+ public static synchronized native byte[] sqlite3_value_text16be(@NotNull sqlite3_value v);
+
+ public static synchronized native int sqlite3_value_type(@NotNull sqlite3_value v);
+
+ public static synchronized native int sqlite3_value_numeric_type(@NotNull sqlite3_value v);
+
+ public static synchronized native int sqlite3_value_nochange(@NotNull sqlite3_value v);
+
+ public static synchronized native int sqlite3_value_frombind(@NotNull sqlite3_value v);
+
+ public static synchronized native int sqlite3_value_subtype(@NotNull sqlite3_value v);
+
+ /**
+ Cleans up all per-JNIEnv and per-db state managed by the library
+ then calls the C-native sqlite3_shutdown().
+ */
+ public static synchronized native int sqlite3_shutdown();
+
+ /**
+ This is NOT part of the public API. It exists solely as a place
+ to hook in arbitrary C-side code during development and testing
+ of this library.
+ */
+ public static synchronized native void sqlite3_do_something_for_developer();
+
+ //////////////////////////////////////////////////////////////////////
+ // SQLITE_... constants follow...
+
+ // version info
+ public static final int SQLITE_VERSION_NUMBER = sqlite3_libversion_number();
+ public static final String SQLITE_VERSION = sqlite3_libversion();
+ public static final String SQLITE_SOURCE_ID = sqlite3_sourceid();
+
+ //! Feature flags which are initialized at lib startup. Necessarily
+ // non-final so that lib init can fill out the proper values,
+ // but modifying them from client code has no effect.
+ public static boolean SQLITE_ENABLE_FTS5 = false;
+
+ // access
+ public static final int SQLITE_ACCESS_EXISTS = 0;
+ public static final int SQLITE_ACCESS_READWRITE = 1;
+ public static final int SQLITE_ACCESS_READ = 2;
+
+ // authorizer
+ public static final int SQLITE_DENY = 1;
+ public static final int SQLITE_IGNORE = 2;
+ public static final int SQLITE_CREATE_INDEX = 1;
+ public static final int SQLITE_CREATE_TABLE = 2;
+ public static final int SQLITE_CREATE_TEMP_INDEX = 3;
+ public static final int SQLITE_CREATE_TEMP_TABLE = 4;
+ public static final int SQLITE_CREATE_TEMP_TRIGGER = 5;
+ public static final int SQLITE_CREATE_TEMP_VIEW = 6;
+ public static final int SQLITE_CREATE_TRIGGER = 7;
+ public static final int SQLITE_CREATE_VIEW = 8;
+ public static final int SQLITE_DELETE = 9;
+ public static final int SQLITE_DROP_INDEX = 10;
+ public static final int SQLITE_DROP_TABLE = 11;
+ public static final int SQLITE_DROP_TEMP_INDEX = 12;
+ public static final int SQLITE_DROP_TEMP_TABLE = 13;
+ public static final int SQLITE_DROP_TEMP_TRIGGER = 14;
+ public static final int SQLITE_DROP_TEMP_VIEW = 15;
+ public static final int SQLITE_DROP_TRIGGER = 16;
+ public static final int SQLITE_DROP_VIEW = 17;
+ public static final int SQLITE_INSERT = 18;
+ public static final int SQLITE_PRAGMA = 19;
+ public static final int SQLITE_READ = 20;
+ public static final int SQLITE_SELECT = 21;
+ public static final int SQLITE_TRANSACTION = 22;
+ public static final int SQLITE_UPDATE = 23;
+ public static final int SQLITE_ATTACH = 24;
+ public static final int SQLITE_DETACH = 25;
+ public static final int SQLITE_ALTER_TABLE = 26;
+ public static final int SQLITE_REINDEX = 27;
+ public static final int SQLITE_ANALYZE = 28;
+ public static final int SQLITE_CREATE_VTABLE = 29;
+ public static final int SQLITE_DROP_VTABLE = 30;
+ public static final int SQLITE_FUNCTION = 31;
+ public static final int SQLITE_SAVEPOINT = 32;
+ public static final int SQLITE_RECURSIVE = 33;
+
+ // blob finalizers: these should, because they are treated as
+ // special pointer values in C, ideally have the same sizeof() as
+ // the platform's (void*), but we can't know that size from here.
+ public static final long SQLITE_STATIC = 0;
+ public static final long SQLITE_TRANSIENT = -1;
+
+ // changeset
+ public static final int SQLITE_CHANGESETSTART_INVERT = 2;
+ public static final int SQLITE_CHANGESETAPPLY_NOSAVEPOINT = 1;
+ public static final int SQLITE_CHANGESETAPPLY_INVERT = 2;
+ public static final int SQLITE_CHANGESETAPPLY_IGNORENOOP = 4;
+ public static final int SQLITE_CHANGESET_DATA = 1;
+ public static final int SQLITE_CHANGESET_NOTFOUND = 2;
+ public static final int SQLITE_CHANGESET_CONFLICT = 3;
+ public static final int SQLITE_CHANGESET_CONSTRAINT = 4;
+ public static final int SQLITE_CHANGESET_FOREIGN_KEY = 5;
+ public static final int SQLITE_CHANGESET_OMIT = 0;
+ public static final int SQLITE_CHANGESET_REPLACE = 1;
+ public static final int SQLITE_CHANGESET_ABORT = 2;
+
+ // config
+ public static final int SQLITE_CONFIG_SINGLETHREAD = 1;
+ public static final int SQLITE_CONFIG_MULTITHREAD = 2;
+ public static final int SQLITE_CONFIG_SERIALIZED = 3;
+ public static final int SQLITE_CONFIG_MALLOC = 4;
+ public static final int SQLITE_CONFIG_GETMALLOC = 5;
+ public static final int SQLITE_CONFIG_SCRATCH = 6;
+ public static final int SQLITE_CONFIG_PAGECACHE = 7;
+ public static final int SQLITE_CONFIG_HEAP = 8;
+ public static final int SQLITE_CONFIG_MEMSTATUS = 9;
+ public static final int SQLITE_CONFIG_MUTEX = 10;
+ public static final int SQLITE_CONFIG_GETMUTEX = 11;
+ public static final int SQLITE_CONFIG_LOOKASIDE = 13;
+ public static final int SQLITE_CONFIG_PCACHE = 14;
+ public static final int SQLITE_CONFIG_GETPCACHE = 15;
+ public static final int SQLITE_CONFIG_LOG = 16;
+ public static final int SQLITE_CONFIG_URI = 17;
+ public static final int SQLITE_CONFIG_PCACHE2 = 18;
+ public static final int SQLITE_CONFIG_GETPCACHE2 = 19;
+ public static final int SQLITE_CONFIG_COVERING_INDEX_SCAN = 20;
+ public static final int SQLITE_CONFIG_SQLLOG = 21;
+ public static final int SQLITE_CONFIG_MMAP_SIZE = 22;
+ public static final int SQLITE_CONFIG_WIN32_HEAPSIZE = 23;
+ public static final int SQLITE_CONFIG_PCACHE_HDRSZ = 24;
+ public static final int SQLITE_CONFIG_PMASZ = 25;
+ public static final int SQLITE_CONFIG_STMTJRNL_SPILL = 26;
+ public static final int SQLITE_CONFIG_SMALL_MALLOC = 27;
+ public static final int SQLITE_CONFIG_SORTERREF_SIZE = 28;
+ public static final int SQLITE_CONFIG_MEMDB_MAXSIZE = 29;
+
+ // data types
+ public static final int SQLITE_INTEGER = 1;
+ public static final int SQLITE_FLOAT = 2;
+ public static final int SQLITE_TEXT = 3;
+ public static final int SQLITE_BLOB = 4;
+ public static final int SQLITE_NULL = 5;
+
+ // db config
+ public static final int SQLITE_DBCONFIG_MAINDBNAME = 1000;
+ public static final int SQLITE_DBCONFIG_LOOKASIDE = 1001;
+ public static final int SQLITE_DBCONFIG_ENABLE_FKEY = 1002;
+ public static final int SQLITE_DBCONFIG_ENABLE_TRIGGER = 1003;
+ public static final int SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER = 1004;
+ public static final int SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION = 1005;
+ public static final int SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE = 1006;
+ public static final int SQLITE_DBCONFIG_ENABLE_QPSG = 1007;
+ public static final int SQLITE_DBCONFIG_TRIGGER_EQP = 1008;
+ public static final int SQLITE_DBCONFIG_RESET_DATABASE = 1009;
+ public static final int SQLITE_DBCONFIG_DEFENSIVE = 1010;
+ public static final int SQLITE_DBCONFIG_WRITABLE_SCHEMA = 1011;
+ public static final int SQLITE_DBCONFIG_LEGACY_ALTER_TABLE = 1012;
+ public static final int SQLITE_DBCONFIG_DQS_DML = 1013;
+ public static final int SQLITE_DBCONFIG_DQS_DDL = 1014;
+ public static final int SQLITE_DBCONFIG_ENABLE_VIEW = 1015;
+ public static final int SQLITE_DBCONFIG_LEGACY_FILE_FORMAT = 1016;
+ public static final int SQLITE_DBCONFIG_TRUSTED_SCHEMA = 1017;
+ public static final int SQLITE_DBCONFIG_STMT_SCANSTATUS = 1018;
+ public static final int SQLITE_DBCONFIG_REVERSE_SCANORDER = 1019;
+ public static final int SQLITE_DBCONFIG_MAX = 1019;
+
+ // db status
+ public static final int SQLITE_DBSTATUS_LOOKASIDE_USED = 0;
+ public static final int SQLITE_DBSTATUS_CACHE_USED = 1;
+ public static final int SQLITE_DBSTATUS_SCHEMA_USED = 2;
+ public static final int SQLITE_DBSTATUS_STMT_USED = 3;
+ public static final int SQLITE_DBSTATUS_LOOKASIDE_HIT = 4;
+ public static final int SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE = 5;
+ public static final int SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL = 6;
+ public static final int SQLITE_DBSTATUS_CACHE_HIT = 7;
+ public static final int SQLITE_DBSTATUS_CACHE_MISS = 8;
+ public static final int SQLITE_DBSTATUS_CACHE_WRITE = 9;
+ public static final int SQLITE_DBSTATUS_DEFERRED_FKS = 10;
+ public static final int SQLITE_DBSTATUS_CACHE_USED_SHARED = 11;
+ public static final int SQLITE_DBSTATUS_CACHE_SPILL = 12;
+ public static final int SQLITE_DBSTATUS_MAX = 12;
+
+ // encodings
+ public static final int SQLITE_UTF8 = 1;
+ public static final int SQLITE_UTF16LE = 2;
+ public static final int SQLITE_UTF16BE = 3;
+ public static final int SQLITE_UTF16 = 4;
+ public static final int SQLITE_UTF16_ALIGNED = 8;
+
+ // fcntl
+ public static final int SQLITE_FCNTL_LOCKSTATE = 1;
+ public static final int SQLITE_FCNTL_GET_LOCKPROXYFILE = 2;
+ public static final int SQLITE_FCNTL_SET_LOCKPROXYFILE = 3;
+ public static final int SQLITE_FCNTL_LAST_ERRNO = 4;
+ public static final int SQLITE_FCNTL_SIZE_HINT = 5;
+ public static final int SQLITE_FCNTL_CHUNK_SIZE = 6;
+ public static final int SQLITE_FCNTL_FILE_POINTER = 7;
+ public static final int SQLITE_FCNTL_SYNC_OMITTED = 8;
+ public static final int SQLITE_FCNTL_WIN32_AV_RETRY = 9;
+ public static final int SQLITE_FCNTL_PERSIST_WAL = 10;
+ public static final int SQLITE_FCNTL_OVERWRITE = 11;
+ public static final int SQLITE_FCNTL_VFSNAME = 12;
+ public static final int SQLITE_FCNTL_POWERSAFE_OVERWRITE = 13;
+ public static final int SQLITE_FCNTL_PRAGMA = 14;
+ public static final int SQLITE_FCNTL_BUSYHANDLER = 15;
+ public static final int SQLITE_FCNTL_TEMPFILENAME = 16;
+ public static final int SQLITE_FCNTL_MMAP_SIZE = 18;
+ public static final int SQLITE_FCNTL_TRACE = 19;
+ public static final int SQLITE_FCNTL_HAS_MOVED = 20;
+ public static final int SQLITE_FCNTL_SYNC = 21;
+ public static final int SQLITE_FCNTL_COMMIT_PHASETWO = 22;
+ public static final int SQLITE_FCNTL_WIN32_SET_HANDLE = 23;
+ public static final int SQLITE_FCNTL_WAL_BLOCK = 24;
+ public static final int SQLITE_FCNTL_ZIPVFS = 25;
+ public static final int SQLITE_FCNTL_RBU = 26;
+ public static final int SQLITE_FCNTL_VFS_POINTER = 27;
+ public static final int SQLITE_FCNTL_JOURNAL_POINTER = 28;
+ public static final int SQLITE_FCNTL_WIN32_GET_HANDLE = 29;
+ public static final int SQLITE_FCNTL_PDB = 30;
+ public static final int SQLITE_FCNTL_BEGIN_ATOMIC_WRITE = 31;
+ public static final int SQLITE_FCNTL_COMMIT_ATOMIC_WRITE = 32;
+ public static final int SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE = 33;
+ public static final int SQLITE_FCNTL_LOCK_TIMEOUT = 34;
+ public static final int SQLITE_FCNTL_DATA_VERSION = 35;
+ public static final int SQLITE_FCNTL_SIZE_LIMIT = 36;
+ public static final int SQLITE_FCNTL_CKPT_DONE = 37;
+ public static final int SQLITE_FCNTL_RESERVE_BYTES = 38;
+ public static final int SQLITE_FCNTL_CKPT_START = 39;
+ public static final int SQLITE_FCNTL_EXTERNAL_READER = 40;
+ public static final int SQLITE_FCNTL_CKSM_FILE = 41;
+ public static final int SQLITE_FCNTL_RESET_CACHE = 42;
+
+ // flock
+ public static final int SQLITE_LOCK_NONE = 0;
+ public static final int SQLITE_LOCK_SHARED = 1;
+ public static final int SQLITE_LOCK_RESERVED = 2;
+ public static final int SQLITE_LOCK_PENDING = 3;
+ public static final int SQLITE_LOCK_EXCLUSIVE = 4;
+
+ // iocap
+ public static final int SQLITE_IOCAP_ATOMIC = 1;
+ public static final int SQLITE_IOCAP_ATOMIC512 = 2;
+ public static final int SQLITE_IOCAP_ATOMIC1K = 4;
+ public static final int SQLITE_IOCAP_ATOMIC2K = 8;
+ public static final int SQLITE_IOCAP_ATOMIC4K = 16;
+ public static final int SQLITE_IOCAP_ATOMIC8K = 32;
+ public static final int SQLITE_IOCAP_ATOMIC16K = 64;
+ public static final int SQLITE_IOCAP_ATOMIC32K = 128;
+ public static final int SQLITE_IOCAP_ATOMIC64K = 256;
+ public static final int SQLITE_IOCAP_SAFE_APPEND = 512;
+ public static final int SQLITE_IOCAP_SEQUENTIAL = 1024;
+ public static final int SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN = 2048;
+ public static final int SQLITE_IOCAP_POWERSAFE_OVERWRITE = 4096;
+ public static final int SQLITE_IOCAP_IMMUTABLE = 8192;
+ public static final int SQLITE_IOCAP_BATCH_ATOMIC = 16384;
+
+ // limits. These get injected at init-time so that they stay in sync
+ // with the compile-time options. This unfortunately means they are
+ // not final, but keeping them in sync with their C values seems
+ // more important than protecting users from assigning to these
+ // (with unpredictable results).
+ public static int SQLITE_MAX_ALLOCATION_SIZE = -1;
+ public static int SQLITE_LIMIT_LENGTH = -1;
+ public static int SQLITE_MAX_LENGTH = -1;
+ public static int SQLITE_LIMIT_SQL_LENGTH = -1;
+ public static int SQLITE_MAX_SQL_LENGTH = -1;
+ public static int SQLITE_LIMIT_COLUMN = -1;
+ public static int SQLITE_MAX_COLUMN = -1;
+ public static int SQLITE_LIMIT_EXPR_DEPTH = -1;
+ public static int SQLITE_MAX_EXPR_DEPTH = -1;
+ public static int SQLITE_LIMIT_COMPOUND_SELECT = -1;
+ public static int SQLITE_MAX_COMPOUND_SELECT = -1;
+ public static int SQLITE_LIMIT_VDBE_OP = -1;
+ public static int SQLITE_MAX_VDBE_OP = -1;
+ public static int SQLITE_LIMIT_FUNCTION_ARG = -1;
+ public static int SQLITE_MAX_FUNCTION_ARG = -1;
+ public static int SQLITE_LIMIT_ATTACHED = -1;
+ public static int SQLITE_MAX_ATTACHED = -1;
+ public static int SQLITE_LIMIT_LIKE_PATTERN_LENGTH = -1;
+ public static int SQLITE_MAX_LIKE_PATTERN_LENGTH = -1;
+ public static int SQLITE_LIMIT_VARIABLE_NUMBER = -1;
+ public static int SQLITE_MAX_VARIABLE_NUMBER = -1;
+ public static int SQLITE_LIMIT_TRIGGER_DEPTH = -1;
+ public static int SQLITE_MAX_TRIGGER_DEPTH = -1;
+ public static int SQLITE_LIMIT_WORKER_THREADS = -1;
+ public static int SQLITE_MAX_WORKER_THREADS = -1;
+
+ // open flags
+ public static final int SQLITE_OPEN_READONLY = 1;
+ public static final int SQLITE_OPEN_READWRITE = 2;
+ public static final int SQLITE_OPEN_CREATE = 4;
+ public static final int SQLITE_OPEN_URI = 64;
+ public static final int SQLITE_OPEN_MEMORY = 128;
+ public static final int SQLITE_OPEN_NOMUTEX = 32768;
+ public static final int SQLITE_OPEN_FULLMUTEX = 65536;
+ public static final int SQLITE_OPEN_SHAREDCACHE = 131072;
+ public static final int SQLITE_OPEN_PRIVATECACHE = 262144;
+ public static final int SQLITE_OPEN_EXRESCODE = 33554432;
+ public static final int SQLITE_OPEN_NOFOLLOW = 16777216;
+ public static final int SQLITE_OPEN_MAIN_DB = 256;
+ public static final int SQLITE_OPEN_MAIN_JOURNAL = 2048;
+ public static final int SQLITE_OPEN_TEMP_DB = 512;
+ public static final int SQLITE_OPEN_TEMP_JOURNAL = 4096;
+ public static final int SQLITE_OPEN_TRANSIENT_DB = 1024;
+ public static final int SQLITE_OPEN_SUBJOURNAL = 8192;
+ public static final int SQLITE_OPEN_SUPER_JOURNAL = 16384;
+ public static final int SQLITE_OPEN_WAL = 524288;
+ public static final int SQLITE_OPEN_DELETEONCLOSE = 8;
+ public static final int SQLITE_OPEN_EXCLUSIVE = 16;
+
+ // prepare flags
+ public static final int SQLITE_PREPARE_PERSISTENT = 1;
+ public static final int SQLITE_PREPARE_NORMALIZE = 2;
+ public static final int SQLITE_PREPARE_NO_VTAB = 4;
+
+ // result codes
+ public static final int SQLITE_OK = 0;
+ public static final int SQLITE_ERROR = 1;
+ public static final int SQLITE_INTERNAL = 2;
+ public static final int SQLITE_PERM = 3;
+ public static final int SQLITE_ABORT = 4;
+ public static final int SQLITE_BUSY = 5;
+ public static final int SQLITE_LOCKED = 6;
+ public static final int SQLITE_NOMEM = 7;
+ public static final int SQLITE_READONLY = 8;
+ public static final int SQLITE_INTERRUPT = 9;
+ public static final int SQLITE_IOERR = 10;
+ public static final int SQLITE_CORRUPT = 11;
+ public static final int SQLITE_NOTFOUND = 12;
+ public static final int SQLITE_FULL = 13;
+ public static final int SQLITE_CANTOPEN = 14;
+ public static final int SQLITE_PROTOCOL = 15;
+ public static final int SQLITE_EMPTY = 16;
+ public static final int SQLITE_SCHEMA = 17;
+ public static final int SQLITE_TOOBIG = 18;
+ public static final int SQLITE_CONSTRAINT = 19;
+ public static final int SQLITE_MISMATCH = 20;
+ public static final int SQLITE_MISUSE = 21;
+ public static final int SQLITE_NOLFS = 22;
+ public static final int SQLITE_AUTH = 23;
+ public static final int SQLITE_FORMAT = 24;
+ public static final int SQLITE_RANGE = 25;
+ public static final int SQLITE_NOTADB = 26;
+ public static final int SQLITE_NOTICE = 27;
+ public static final int SQLITE_WARNING = 28;
+ public static final int SQLITE_ROW = 100;
+ public static final int SQLITE_DONE = 101;
+ public static final int SQLITE_ERROR_MISSING_COLLSEQ = 257;
+ public static final int SQLITE_ERROR_RETRY = 513;
+ public static final int SQLITE_ERROR_SNAPSHOT = 769;
+ public static final int SQLITE_IOERR_READ = 266;
+ public static final int SQLITE_IOERR_SHORT_READ = 522;
+ public static final int SQLITE_IOERR_WRITE = 778;
+ public static final int SQLITE_IOERR_FSYNC = 1034;
+ public static final int SQLITE_IOERR_DIR_FSYNC = 1290;
+ public static final int SQLITE_IOERR_TRUNCATE = 1546;
+ public static final int SQLITE_IOERR_FSTAT = 1802;
+ public static final int SQLITE_IOERR_UNLOCK = 2058;
+ public static final int SQLITE_IOERR_RDLOCK = 2314;
+ public static final int SQLITE_IOERR_DELETE = 2570;
+ public static final int SQLITE_IOERR_BLOCKED = 2826;
+ public static final int SQLITE_IOERR_NOMEM = 3082;
+ public static final int SQLITE_IOERR_ACCESS = 3338;
+ public static final int SQLITE_IOERR_CHECKRESERVEDLOCK = 3594;
+ public static final int SQLITE_IOERR_LOCK = 3850;
+ public static final int SQLITE_IOERR_CLOSE = 4106;
+ public static final int SQLITE_IOERR_DIR_CLOSE = 4362;
+ public static final int SQLITE_IOERR_SHMOPEN = 4618;
+ public static final int SQLITE_IOERR_SHMSIZE = 4874;
+ public static final int SQLITE_IOERR_SHMLOCK = 5130;
+ public static final int SQLITE_IOERR_SHMMAP = 5386;
+ public static final int SQLITE_IOERR_SEEK = 5642;
+ public static final int SQLITE_IOERR_DELETE_NOENT = 5898;
+ public static final int SQLITE_IOERR_MMAP = 6154;
+ public static final int SQLITE_IOERR_GETTEMPPATH = 6410;
+ public static final int SQLITE_IOERR_CONVPATH = 6666;
+ public static final int SQLITE_IOERR_VNODE = 6922;
+ public static final int SQLITE_IOERR_AUTH = 7178;
+ public static final int SQLITE_IOERR_BEGIN_ATOMIC = 7434;
+ public static final int SQLITE_IOERR_COMMIT_ATOMIC = 7690;
+ public static final int SQLITE_IOERR_ROLLBACK_ATOMIC = 7946;
+ public static final int SQLITE_IOERR_DATA = 8202;
+ public static final int SQLITE_IOERR_CORRUPTFS = 8458;
+ public static final int SQLITE_LOCKED_SHAREDCACHE = 262;
+ public static final int SQLITE_LOCKED_VTAB = 518;
+ public static final int SQLITE_BUSY_RECOVERY = 261;
+ public static final int SQLITE_BUSY_SNAPSHOT = 517;
+ public static final int SQLITE_BUSY_TIMEOUT = 773;
+ public static final int SQLITE_CANTOPEN_NOTEMPDIR = 270;
+ public static final int SQLITE_CANTOPEN_ISDIR = 526;
+ public static final int SQLITE_CANTOPEN_FULLPATH = 782;
+ public static final int SQLITE_CANTOPEN_CONVPATH = 1038;
+ public static final int SQLITE_CANTOPEN_SYMLINK = 1550;
+ public static final int SQLITE_CORRUPT_VTAB = 267;
+ public static final int SQLITE_CORRUPT_SEQUENCE = 523;
+ public static final int SQLITE_CORRUPT_INDEX = 779;
+ public static final int SQLITE_READONLY_RECOVERY = 264;
+ public static final int SQLITE_READONLY_CANTLOCK = 520;
+ public static final int SQLITE_READONLY_ROLLBACK = 776;
+ public static final int SQLITE_READONLY_DBMOVED = 1032;
+ public static final int SQLITE_READONLY_CANTINIT = 1288;
+ public static final int SQLITE_READONLY_DIRECTORY = 1544;
+ public static final int SQLITE_ABORT_ROLLBACK = 516;
+ public static final int SQLITE_CONSTRAINT_CHECK = 275;
+ public static final int SQLITE_CONSTRAINT_COMMITHOOK = 531;
+ public static final int SQLITE_CONSTRAINT_FOREIGNKEY = 787;
+ public static final int SQLITE_CONSTRAINT_FUNCTION = 1043;
+ public static final int SQLITE_CONSTRAINT_NOTNULL = 1299;
+ public static final int SQLITE_CONSTRAINT_PRIMARYKEY = 1555;
+ public static final int SQLITE_CONSTRAINT_TRIGGER = 1811;
+ public static final int SQLITE_CONSTRAINT_UNIQUE = 2067;
+ public static final int SQLITE_CONSTRAINT_VTAB = 2323;
+ public static final int SQLITE_CONSTRAINT_ROWID = 2579;
+ public static final int SQLITE_CONSTRAINT_PINNED = 2835;
+ public static final int SQLITE_CONSTRAINT_DATATYPE = 3091;
+ public static final int SQLITE_NOTICE_RECOVER_WAL = 283;
+ public static final int SQLITE_NOTICE_RECOVER_ROLLBACK = 539;
+ public static final int SQLITE_WARNING_AUTOINDEX = 284;
+ public static final int SQLITE_AUTH_USER = 279;
+ public static final int SQLITE_OK_LOAD_PERMANENTLY = 256;
+
+ // serialize
+ public static final int SQLITE_SERIALIZE_NOCOPY = 1;
+ public static final int SQLITE_DESERIALIZE_FREEONCLOSE = 1;
+ public static final int SQLITE_DESERIALIZE_READONLY = 4;
+ public static final int SQLITE_DESERIALIZE_RESIZEABLE = 2;
+
+ // session
+ public static final int SQLITE_SESSION_CONFIG_STRMSIZE = 1;
+ public static final int SQLITE_SESSION_OBJCONFIG_SIZE = 1;
+
+ // sqlite3 status
+ public static final int SQLITE_STATUS_MEMORY_USED = 0;
+ public static final int SQLITE_STATUS_PAGECACHE_USED = 1;
+ public static final int SQLITE_STATUS_PAGECACHE_OVERFLOW = 2;
+ public static final int SQLITE_STATUS_MALLOC_SIZE = 5;
+ public static final int SQLITE_STATUS_PARSER_STACK = 6;
+ public static final int SQLITE_STATUS_PAGECACHE_SIZE = 7;
+ public static final int SQLITE_STATUS_MALLOC_COUNT = 9;
+
+ // stmt status
+ public static final int SQLITE_STMTSTATUS_FULLSCAN_STEP = 1;
+ public static final int SQLITE_STMTSTATUS_SORT = 2;
+ public static final int SQLITE_STMTSTATUS_AUTOINDEX = 3;
+ public static final int SQLITE_STMTSTATUS_VM_STEP = 4;
+ public static final int SQLITE_STMTSTATUS_REPREPARE = 5;
+ public static final int SQLITE_STMTSTATUS_RUN = 6;
+ public static final int SQLITE_STMTSTATUS_FILTER_MISS = 7;
+ public static final int SQLITE_STMTSTATUS_FILTER_HIT = 8;
+ public static final int SQLITE_STMTSTATUS_MEMUSED = 99;
+
+ // sync flags
+ public static final int SQLITE_SYNC_NORMAL = 2;
+ public static final int SQLITE_SYNC_FULL = 3;
+ public static final int SQLITE_SYNC_DATAONLY = 16;
+
+ // tracing flags
+ public static final int SQLITE_TRACE_STMT = 1;
+ public static final int SQLITE_TRACE_PROFILE = 2;
+ public static final int SQLITE_TRACE_ROW = 4;
+ public static final int SQLITE_TRACE_CLOSE = 8;
+
+ // transaction state
+ public static final int SQLITE_TXN_NONE = 0;
+ public static final int SQLITE_TXN_READ = 1;
+ public static final int SQLITE_TXN_WRITE = 2;
+
+ // udf flags
+ public static final int SQLITE_DETERMINISTIC = 2048;
+ public static final int SQLITE_DIRECTONLY = 524288;
+ public static final int SQLITE_INNOCUOUS = 2097152;
+
+ // virtual tables
+ public static final int SQLITE_INDEX_SCAN_UNIQUE = 1;
+ public static final int SQLITE_INDEX_CONSTRAINT_EQ = 2;
+ public static final int SQLITE_INDEX_CONSTRAINT_GT = 4;
+ public static final int SQLITE_INDEX_CONSTRAINT_LE = 8;
+ public static final int SQLITE_INDEX_CONSTRAINT_LT = 16;
+ public static final int SQLITE_INDEX_CONSTRAINT_GE = 32;
+ public static final int SQLITE_INDEX_CONSTRAINT_MATCH = 64;
+ public static final int SQLITE_INDEX_CONSTRAINT_LIKE = 65;
+ public static final int SQLITE_INDEX_CONSTRAINT_GLOB = 66;
+ public static final int SQLITE_INDEX_CONSTRAINT_REGEXP = 67;
+ public static final int SQLITE_INDEX_CONSTRAINT_NE = 68;
+ public static final int SQLITE_INDEX_CONSTRAINT_ISNOT = 69;
+ public static final int SQLITE_INDEX_CONSTRAINT_ISNOTNULL = 70;
+ public static final int SQLITE_INDEX_CONSTRAINT_ISNULL = 71;
+ public static final int SQLITE_INDEX_CONSTRAINT_IS = 72;
+ public static final int SQLITE_INDEX_CONSTRAINT_LIMIT = 73;
+ public static final int SQLITE_INDEX_CONSTRAINT_OFFSET = 74;
+ public static final int SQLITE_INDEX_CONSTRAINT_FUNCTION = 150;
+ public static final int SQLITE_VTAB_CONSTRAINT_SUPPORT = 1;
+ public static final int SQLITE_VTAB_INNOCUOUS = 2;
+ public static final int SQLITE_VTAB_DIRECTONLY = 3;
+ public static final int SQLITE_VTAB_USES_ALL_SCHEMAS = 4;
+ public static final int SQLITE_ROLLBACK = 1;
+ public static final int SQLITE_FAIL = 3;
+ public static final int SQLITE_REPLACE = 5;
+ static {
+ // This MUST come after the SQLITE_MAX_... values or else
+ // attempting to modify them silently fails.
+ init();
+ }
+}
diff --git a/ext/jni/src/org/sqlite/jni/Tester1.java b/ext/jni/src/org/sqlite/jni/Tester1.java
new file mode 100644
index 000000000..dca49faf6
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/Tester1.java
@@ -0,0 +1,1163 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file contains a set of tests for the sqlite3 JNI bindings.
+*/
+package org.sqlite.jni;
+import static org.sqlite.jni.SQLite3Jni.*;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+public class Tester1 {
+ private static final class Metrics {
+ int dbOpen;
+ }
+
+ static final Metrics metrics = new Metrics();
+ private static final OutputPointer.sqlite3_stmt outStmt
+ = new OutputPointer.sqlite3_stmt();
+
+ public static void out(Object val){
+ System.out.print(val);
+ }
+
+ public static void outln(Object val){
+ System.out.println(val);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static void out(Object... vals){
+ int n = 0;
+ for(Object v : vals) out((n++>0 ? " " : "")+v);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static void outln(Object... vals){
+ out(vals); out("\n");
+ }
+
+ static int affirmCount = 0;
+ public static void affirm(Boolean v){
+ ++affirmCount;
+ assert( v /* prefer assert over exception if it's enabled because
+ the JNI layer sometimes has to suppress exceptions. */);
+ if( !v ) throw new RuntimeException("Assertion failed.");
+ }
+
+ private static void test1(){
+ outln("libversion_number:",
+ sqlite3_libversion_number()
+ + "\n"
+ + sqlite3_libversion()
+ + "\n"
+ + SQLITE_SOURCE_ID);
+ affirm(sqlite3_libversion_number() == SQLITE_VERSION_NUMBER);
+ //outln("threadsafe = "+sqlite3_threadsafe());
+ affirm(SQLITE_MAX_LENGTH > 0);
+ affirm(SQLITE_MAX_TRIGGER_DEPTH>0);
+ }
+
+ public static sqlite3 createNewDb(){
+ final OutputPointer.sqlite3 out = new OutputPointer.sqlite3();
+ int rc = sqlite3_open(":memory:", out);
+ ++metrics.dbOpen;
+ sqlite3 db = out.take();
+ if( 0!=rc ){
+ final String msg = db.getNativePointer()==0
+ ? sqlite3_errstr(rc)
+ : sqlite3_errmsg(db);
+ throw new RuntimeException("Opening db failed: "+msg);
+ }
+ affirm( null == out.get() );
+ affirm( 0 != db.getNativePointer() );
+ rc = sqlite3_busy_timeout(db, 2000);
+ affirm( 0 == rc );
+ return db;
+ }
+
+ public static void execSql(sqlite3 db, String[] sql){
+ execSql(db, String.join("", sql));
+ }
+
+ public static int execSql(sqlite3 db, boolean throwOnError, String sql){
+ OutputPointer.Int32 oTail = new OutputPointer.Int32();
+ final byte[] sqlUtf8 = sql.getBytes(StandardCharsets.UTF_8);
+ int pos = 0, n = 1;
+ byte[] sqlChunk = sqlUtf8;
+ int rc = 0;
+ sqlite3_stmt stmt = null;
+ while(pos < sqlChunk.length){
+ if(pos > 0){
+ sqlChunk = Arrays.copyOfRange(sqlChunk, pos,
+ sqlChunk.length);
+ }
+ if( 0==sqlChunk.length ) break;
+ rc = sqlite3_prepare_v2(db, sqlChunk, outStmt, oTail);
+ if(throwOnError) affirm(0 == rc);
+ else if( 0!=rc ) break;
+ pos = oTail.value;
+ stmt = outStmt.take();
+ if( null == stmt ){
+ // empty statement was parsed.
+ continue;
+ }
+ affirm(0 != stmt.getNativePointer());
+ while( SQLITE_ROW == (rc = sqlite3_step(stmt)) ){
+ }
+ sqlite3_finalize(stmt);
+ affirm(0 == stmt.getNativePointer());
+ if(0!=rc && SQLITE_ROW!=rc && SQLITE_DONE!=rc){
+ break;
+ }
+ }
+ sqlite3_finalize(stmt);
+ if(SQLITE_ROW==rc || SQLITE_DONE==rc) rc = 0;
+ if( 0!=rc && throwOnError){
+ throw new RuntimeException("db op failed with rc="
+ +rc+": "+sqlite3_errmsg(db));
+ }
+ return rc;
+ }
+
+ public static void execSql(sqlite3 db, String sql){
+ execSql(db, true, sql);
+ }
+
+ public static sqlite3_stmt prepare(sqlite3 db, String sql){
+ outStmt.clear();
+ int rc = sqlite3_prepare(db, sql, outStmt);
+ affirm( 0 == rc );
+ final sqlite3_stmt rv = outStmt.take();
+ affirm( null == outStmt.get() );
+ affirm( 0 != rv.getNativePointer() );
+ return rv;
+ }
+
+ private static void testCompileOption(){
+ int i = 0;
+ String optName;
+ outln("compile options:");
+ for( ; null != (optName = sqlite3_compileoption_get(i)); ++i){
+ outln("\t"+optName+"\t (used="+
+ sqlite3_compileoption_used(optName)+")");
+ }
+
+ }
+
+ private static void testOpenDb1(){
+ final OutputPointer.sqlite3 out = new OutputPointer.sqlite3();
+ int rc = sqlite3_open(":memory:", out);
+ ++metrics.dbOpen;
+ sqlite3 db = out.get();
+ affirm(0 == rc);
+ affirm(0 < db.getNativePointer());
+ sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 1, null)
+ /* This function has different mangled names in jdk8 vs jdk19,
+ and this call is here to ensure that the build fails
+ if it cannot find both names. */;
+ sqlite3_close_v2(db);
+ affirm(0 == db.getNativePointer());
+ }
+
+ private static void testOpenDb2(){
+ final OutputPointer.sqlite3 out = new OutputPointer.sqlite3();
+ int rc = sqlite3_open_v2(":memory:", out,
+ SQLITE_OPEN_READWRITE
+ | SQLITE_OPEN_CREATE, null);
+ ++metrics.dbOpen;
+ affirm(0 == rc);
+ sqlite3 db = out.get();
+ affirm(0 < db.getNativePointer());
+ sqlite3_close_v2(db);
+ affirm(0 == db.getNativePointer());
+ }
+
+ private static void testPrepare123(){
+ sqlite3 db = createNewDb();
+ int rc;
+ rc = sqlite3_prepare(db, "CREATE TABLE t1(a);", outStmt);
+ affirm(0 == rc);
+ sqlite3_stmt stmt = outStmt.get();
+ affirm(0 != stmt.getNativePointer());
+ rc = sqlite3_step(stmt);
+ affirm(SQLITE_DONE == rc);
+ sqlite3_finalize(stmt);
+ affirm(0 == stmt.getNativePointer());
+
+ { /* Demonstrate how to use the "zTail" option of
+ sqlite3_prepare() family of functions. */
+ OutputPointer.Int32 oTail = new OutputPointer.Int32();
+ final byte[] sqlUtf8 =
+ "CREATE TABLE t2(a); INSERT INTO t2(a) VALUES(1),(2),(3)"
+ .getBytes(StandardCharsets.UTF_8);
+ int pos = 0, n = 1;
+ byte[] sqlChunk = sqlUtf8;
+ while(pos < sqlChunk.length){
+ if(pos > 0){
+ sqlChunk = Arrays.copyOfRange(sqlChunk, pos, sqlChunk.length);
+ }
+ //outln("SQL chunk #"+n+" length = "+sqlChunk.length+", pos = "+pos);
+ if( 0==sqlChunk.length ) break;
+ rc = sqlite3_prepare_v2(db, sqlChunk, outStmt, oTail);
+ affirm(0 == rc);
+ stmt = outStmt.get();
+ pos = oTail.value;
+ /*outln("SQL tail pos = "+pos+". Chunk = "+
+ (new String(Arrays.copyOfRange(sqlChunk,0,pos),
+ StandardCharsets.UTF_8)));*/
+ switch(n){
+ case 1: affirm(19 == pos); break;
+ case 2: affirm(36 == pos); break;
+ default: affirm( false /* can't happen */ );
+
+ }
+ ++n;
+ affirm(0 != stmt.getNativePointer());
+ rc = sqlite3_step(stmt);
+ affirm(SQLITE_DONE == rc);
+ sqlite3_finalize(stmt);
+ affirm(0 == stmt.getNativePointer());
+ }
+ }
+
+
+ rc = sqlite3_prepare_v3(db, "INSERT INTO t2(a) VALUES(1),(2),(3)",
+ SQLITE_PREPARE_NORMALIZE, outStmt);
+ affirm(0 == rc);
+ stmt = outStmt.get();
+ affirm(0 != stmt.getNativePointer());
+ sqlite3_finalize(stmt);
+ affirm(0 == stmt.getNativePointer() );
+ sqlite3_close_v2(db);
+ }
+
+ private static void testBindFetchInt(){
+ sqlite3 db = createNewDb();
+ execSql(db, "CREATE TABLE t(a)");
+
+ sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(:a);");
+ affirm(1 == sqlite3_bind_parameter_count(stmt));
+ final int paramNdx = sqlite3_bind_parameter_index(stmt, ":a");
+ affirm(1 == paramNdx);
+ int total1 = 0;
+ long rowid = -1;
+ int changes = sqlite3_changes(db);
+ int changesT = sqlite3_total_changes(db);
+ long changes64 = sqlite3_changes64(db);
+ long changesT64 = sqlite3_total_changes64(db);
+ int rc;
+ for(int i = 99; i < 102; ++i ){
+ total1 += i;
+ rc = sqlite3_bind_int(stmt, paramNdx, i);
+ affirm(0 == rc);
+ rc = sqlite3_step(stmt);
+ sqlite3_reset(stmt);
+ affirm(SQLITE_DONE == rc);
+ long x = sqlite3_last_insert_rowid(db);
+ affirm(x > rowid);
+ rowid = x;
+ }
+ sqlite3_finalize(stmt);
+ affirm(300 == total1);
+ affirm(sqlite3_changes(db) > changes);
+ affirm(sqlite3_total_changes(db) > changesT);
+ affirm(sqlite3_changes64(db) > changes64);
+ affirm(sqlite3_total_changes64(db) > changesT64);
+ stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;");
+ int total2 = 0;
+ while( SQLITE_ROW == sqlite3_step(stmt) ){
+ total2 += sqlite3_column_int(stmt, 0);
+ sqlite3_value sv = sqlite3_column_value(stmt, 0);
+ affirm( null != sv );
+ affirm( 0 != sv.getNativePointer() );
+ affirm( SQLITE_INTEGER == sqlite3_value_type(sv) );
+ }
+ sqlite3_finalize(stmt);
+ affirm(total1 == total2);
+ sqlite3_close_v2(db);
+ affirm(0 == db.getNativePointer());
+ }
+
+ private static void testBindFetchInt64(){
+ sqlite3 db = createNewDb();
+ execSql(db, "CREATE TABLE t(a)");
+ sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);");
+ long total1 = 0;
+ for(long i = 0xffffffff; i < 0xffffffff + 3; ++i ){
+ total1 += i;
+ sqlite3_bind_int64(stmt, 1, i);
+ sqlite3_step(stmt);
+ sqlite3_reset(stmt);
+ }
+ sqlite3_finalize(stmt);
+ stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;");
+ long total2 = 0;
+ while( SQLITE_ROW == sqlite3_step(stmt) ){
+ total2 += sqlite3_column_int64(stmt, 0);
+ }
+ sqlite3_finalize(stmt);
+ affirm(total1 == total2);
+ sqlite3_close_v2(db);
+ }
+
+ private static void testBindFetchDouble(){
+ sqlite3 db = createNewDb();
+ execSql(db, "CREATE TABLE t(a)");
+ sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);");
+ double total1 = 0;
+ for(double i = 1.5; i < 5.0; i = i + 1.0 ){
+ total1 += i;
+ sqlite3_bind_double(stmt, 1, i);
+ sqlite3_step(stmt);
+ sqlite3_reset(stmt);
+ }
+ sqlite3_finalize(stmt);
+ stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;");
+ double total2 = 0;
+ int counter = 0;
+ while( SQLITE_ROW == sqlite3_step(stmt) ){
+ ++counter;
+ total2 += sqlite3_column_double(stmt, 0);
+ }
+ affirm(4 == counter);
+ sqlite3_finalize(stmt);
+ affirm(total2<=total1+0.01 && total2>=total1-0.01);
+ sqlite3_close_v2(db);
+ }
+
+ private static void testBindFetchText(){
+ sqlite3 db = createNewDb();
+ execSql(db, "CREATE TABLE t(a)");
+ sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);");
+ String[] list1 = { "hell🤩", "w😃rld", "!" };
+ int rc;
+ for( String e : list1 ){
+ rc = sqlite3_bind_text(stmt, 1, e);
+ affirm(0 == rc);
+ rc = sqlite3_step(stmt);
+ affirm(SQLITE_DONE==rc);
+ sqlite3_reset(stmt);
+ }
+ sqlite3_finalize(stmt);
+ stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;");
+ StringBuilder sbuf = new StringBuilder();
+ int n = 0;
+ while( SQLITE_ROW == sqlite3_step(stmt) ){
+ String txt = sqlite3_column_text16(stmt, 0);
+ //outln("txt = "+txt);
+ sbuf.append( txt );
+ ++n;
+ }
+ sqlite3_finalize(stmt);
+ affirm(3 == n);
+ affirm("w😃rldhell🤩!".equals(sbuf.toString()));
+ sqlite3_close_v2(db);
+ }
+
+ private static void testBindFetchBlob(){
+ sqlite3 db = createNewDb();
+ execSql(db, "CREATE TABLE t(a)");
+ sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);");
+ byte[] list1 = { 0x32, 0x33, 0x34 };
+ int rc = sqlite3_bind_blob(stmt, 1, list1);
+ affirm( 0==rc );
+ rc = sqlite3_step(stmt);
+ affirm(SQLITE_DONE == rc);
+ sqlite3_finalize(stmt);
+ stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;");
+ int n = 0;
+ int total = 0;
+ while( SQLITE_ROW == sqlite3_step(stmt) ){
+ byte[] blob = sqlite3_column_blob(stmt, 0);
+ affirm(3 == blob.length);
+ int i = 0;
+ for(byte b : blob){
+ affirm(b == list1[i++]);
+ total += b;
+ }
+ ++n;
+ }
+ sqlite3_finalize(stmt);
+ affirm(1 == n);
+ affirm(total == 0x32 + 0x33 + 0x34);
+ sqlite3_close_v2(db);
+ }
+
+ private static void testSql(){
+ sqlite3 db = createNewDb();
+ sqlite3_stmt stmt = prepare(db, "SELECT 1");
+ affirm( "SELECT 1".equals(sqlite3_sql(stmt)) );
+ sqlite3_finalize(stmt);
+ stmt = prepare(db, "SELECT ?");
+ sqlite3_bind_text(stmt, 1, "hell😃");
+ affirm( "SELECT 'hell😃'".equals(sqlite3_expanded_sql(stmt)) );
+ sqlite3_finalize(stmt);
+ }
+
+ private static void testCollation(){
+ final sqlite3 db = createNewDb();
+ execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')");
+ final ValueHolder xDestroyCalled = new ValueHolder<>(false);
+ final Collation myCollation = new Collation() {
+ private String myState =
+ "this is local state. There is much like it, but this is mine.";
+ @Override
+ // Reverse-sorts its inputs...
+ public int xCompare(byte[] lhs, byte[] rhs){
+ int len = lhs.length > rhs.length ? rhs.length : lhs.length;
+ int c = 0, i = 0;
+ for(i = 0; i < len; ++i){
+ c = lhs[i] - rhs[i];
+ if(0 != c) break;
+ }
+ if(0==c){
+ if(i < lhs.length) c = 1;
+ else if(i < rhs.length) c = -1;
+ }
+ return -c;
+ }
+ @Override
+ public void xDestroy() {
+ // Just demonstrates that xDestroy is called.
+ xDestroyCalled.value = true;
+ }
+ };
+ final CollationNeeded collLoader = new CollationNeeded(){
+ public int xCollationNeeded(sqlite3 dbArg, int eTextRep, String collationName){
+ affirm(dbArg == db/* as opposed to a temporary object*/);
+ return sqlite3_create_collation(dbArg, "reversi", eTextRep, myCollation);
+ }
+ };
+ int rc = sqlite3_collation_needed(db, collLoader);
+ affirm( 0 == rc );
+ rc = sqlite3_collation_needed(db, collLoader);
+ affirm( 0 == rc /* Installing the same object again is a no-op */);
+ sqlite3_stmt stmt = prepare(db, "SELECT a FROM t ORDER BY a COLLATE reversi");
+ int counter = 0;
+ while( SQLITE_ROW == sqlite3_step(stmt) ){
+ final String val = sqlite3_column_text16(stmt, 0);
+ ++counter;
+ //outln("REVERSI'd row#"+counter+": "+val);
+ switch(counter){
+ case 1: affirm("c".equals(val)); break;
+ case 2: affirm("b".equals(val)); break;
+ case 3: affirm("a".equals(val)); break;
+ }
+ }
+ affirm(3 == counter);
+ sqlite3_finalize(stmt);
+ stmt = prepare(db, "SELECT a FROM t ORDER BY a");
+ counter = 0;
+ while( SQLITE_ROW == sqlite3_step(stmt) ){
+ final String val = sqlite3_column_text16(stmt, 0);
+ ++counter;
+ //outln("Non-REVERSI'd row#"+counter+": "+val);
+ switch(counter){
+ case 3: affirm("c".equals(val)); break;
+ case 2: affirm("b".equals(val)); break;
+ case 1: affirm("a".equals(val)); break;
+ }
+ }
+ affirm(3 == counter);
+ sqlite3_finalize(stmt);
+ affirm(!xDestroyCalled.value);
+ rc = sqlite3_collation_needed(db, null);
+ affirm( 0 == rc );
+ sqlite3_close_v2(db);
+ affirm(xDestroyCalled.value);
+ }
+
+ private static void testToUtf8(){
+ /**
+ Java docs seem contradictory, claiming to use "modified UTF-8"
+ encoding while also claiming to export using RFC 2279:
+
+ https://docs.oracle.com/javase/8/docs/api/java/nio/charset/Charset.html
+
+ Let's ensure that we can convert to standard UTF-8 in Java code
+ (noting that the JNI native API has no way to do this).
+ */
+ final byte[] ba = "a \0 b".getBytes(StandardCharsets.UTF_8);
+ affirm( 5 == ba.length /* as opposed to 6 in modified utf-8 */);
+ }
+
+ private static void testStatus(){
+ final OutputPointer.Int64 cur64 = new OutputPointer.Int64();
+ final OutputPointer.Int64 high64 = new OutputPointer.Int64();
+ final OutputPointer.Int32 cur32 = new OutputPointer.Int32();
+ final OutputPointer.Int32 high32 = new OutputPointer.Int32();
+ final sqlite3 db = createNewDb();
+ execSql(db, "create table t(a); insert into t values(1),(2),(3)");
+
+ int rc = sqlite3_status(SQLITE_STATUS_MEMORY_USED, cur32, high32, false);
+ affirm( 0 == rc );
+ affirm( cur32.value > 0 );
+ affirm( high32.value >= cur32.value );
+
+ rc = sqlite3_status64(SQLITE_STATUS_MEMORY_USED, cur64, high64, false);
+ affirm( 0 == rc );
+ affirm( cur64.value > 0 );
+ affirm( high64.value >= cur64.value );
+
+ cur32.value = 0;
+ high32.value = 1;
+ rc = sqlite3_db_status(db, SQLITE_DBSTATUS_SCHEMA_USED, cur32, high32, false);
+ affirm( 0 == rc );
+ affirm( cur32.value > 0 );
+ affirm( high32.value == 0 /* always 0 for SCHEMA_USED */ );
+
+ sqlite3_close_v2(db);
+ }
+
+ private static void testUdf1(){
+ final sqlite3 db = createNewDb();
+ // These ValueHolders are just to confirm that the func did what we want...
+ final ValueHolder xDestroyCalled = new ValueHolder<>(false);
+ final ValueHolder xFuncAccum = new ValueHolder<>(0);
+
+ // Create an SQLFunction instance using one of its 3 subclasses:
+ // Scalar, Aggregate, or Window:
+ SQLFunction func =
+ // Each of the 3 subclasses requires a different set of
+ // functions, all of which must be implemented. Anonymous
+ // classes are a convenient way to implement these.
+ new SQLFunction.Scalar(){
+ public void xFunc(sqlite3_context cx, sqlite3_value[] args){
+ affirm(db == sqlite3_context_db_handle(cx));
+ int result = 0;
+ for( sqlite3_value v : args ) result += sqlite3_value_int(v);
+ xFuncAccum.value += result;// just for post-run testing
+ sqlite3_result_int(cx, result);
+ }
+ /* OPTIONALLY override xDestroy... */
+ public void xDestroy(){
+ xDestroyCalled.value = true;
+ }
+ };
+
+ // Register and use the function...
+ int rc = sqlite3_create_function(db, "myfunc", -1, SQLITE_UTF8, func);
+ affirm(0 == rc);
+ affirm(0 == xFuncAccum.value);
+ final sqlite3_stmt stmt = prepare(db, "SELECT myfunc(1,2,3)");
+ int n = 0;
+ while( SQLITE_ROW == sqlite3_step(stmt) ){
+ affirm( 6 == sqlite3_column_int(stmt, 0) );
+ ++n;
+ }
+ sqlite3_finalize(stmt);
+ affirm(1 == n);
+ affirm(6 == xFuncAccum.value);
+ affirm( !xDestroyCalled.value );
+ sqlite3_close_v2(db);
+ affirm( xDestroyCalled.value );
+ }
+
+ private static void testUdfJavaObject(){
+ final sqlite3 db = createNewDb();
+ final ValueHolder testResult = new ValueHolder<>(db);
+ final SQLFunction func = new SQLFunction.Scalar(){
+ public void xFunc(sqlite3_context cx, sqlite3_value args[]){
+ sqlite3_result_java_object(cx, testResult.value);
+ }
+ };
+ int rc = sqlite3_create_function(db, "myfunc", -1, SQLITE_UTF8, func);
+ affirm(0 == rc);
+ final sqlite3_stmt stmt = prepare(db, "select myfunc()");
+ affirm( 0 != stmt.getNativePointer() );
+ affirm( testResult.value == db );
+ int n = 0;
+ if( SQLITE_ROW == sqlite3_step(stmt) ){
+ final sqlite3_value v = sqlite3_column_value(stmt, 0);
+ affirm( testResult.value == sqlite3_value_java_object(v) );
+ affirm( testResult.value == sqlite3_value_java_casted(v, sqlite3.class) );
+ affirm( testResult.value ==
+ sqlite3_value_java_casted(v, testResult.value.getClass()) );
+ affirm( testResult.value == sqlite3_value_java_casted(v, Object.class) );
+ affirm( null == sqlite3_value_java_casted(v, String.class) );
+ ++n;
+ }
+ sqlite3_finalize(stmt);
+ affirm( 1 == n );
+ sqlite3_close_v2(db);
+ }
+
+ private static void testUdfAggregate(){
+ final sqlite3 db = createNewDb();
+ final ValueHolder xFinalNull =
+ // To confirm that xFinal() is called with no aggregate state
+ // when the corresponding result set is empty.
+ new ValueHolder<>(false);
+ SQLFunction func = new SQLFunction.Aggregate(){
+ @Override
+ public void xStep(sqlite3_context cx, sqlite3_value[] args){
+ this.getAggregateState(cx, 0).value += sqlite3_value_int(args[0]);
+ }
+ @Override
+ public void xFinal(sqlite3_context cx){
+ final Integer v = this.takeAggregateState(cx);
+ if(null == v){
+ xFinalNull.value = true;
+ sqlite3_result_null(cx);
+ }else{
+ sqlite3_result_int(cx, v);
+ }
+ }
+ };
+ execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES(1),(2),(3)");
+ int rc = sqlite3_create_function(db, "myfunc", 1, SQLITE_UTF8, func);
+ affirm(0 == rc);
+ sqlite3_stmt stmt = prepare(db, "select myfunc(a), myfunc(a+10) from t");
+ int n = 0;
+ if( SQLITE_ROW == sqlite3_step(stmt) ){
+ final int v = sqlite3_column_int(stmt, 0);
+ affirm( 6 == v );
+ ++n;
+ }
+ affirm(!xFinalNull.value);
+ sqlite3_reset(stmt);
+ // Ensure that the accumulator is reset...
+ n = 0;
+ if( SQLITE_ROW == sqlite3_step(stmt) ){
+ final int v = sqlite3_column_int(stmt, 0);
+ affirm( 6 == v );
+ ++n;
+ }
+ sqlite3_finalize(stmt);
+ affirm( 1==n );
+
+ stmt = prepare(db, "select myfunc(a), myfunc(a+a) from t order by a");
+ n = 0;
+ while( SQLITE_ROW == sqlite3_step(stmt) ){
+ final int c0 = sqlite3_column_int(stmt, 0);
+ final int c1 = sqlite3_column_int(stmt, 1);
+ ++n;
+ affirm( 6 == c0 );
+ affirm( 12 == c1 );
+ }
+ affirm( 1 == n );
+ affirm(!xFinalNull.value);
+ sqlite3_finalize(stmt);
+
+ execSql(db, "SELECT myfunc(1) WHERE 0");
+ affirm(xFinalNull.value);
+ sqlite3_close_v2(db);
+ }
+
+ private static void testUdfWindow(){
+ final sqlite3 db = createNewDb();
+ /* Example window function, table, and results taken from:
+ https://sqlite.org/windowfunctions.html#udfwinfunc */
+ final SQLFunction func = new SQLFunction.Window(){
+
+ private void xStepInverse(sqlite3_context cx, int v){
+ this.getAggregateState(cx,0).value += v;
+ }
+ @Override public void xStep(sqlite3_context cx, sqlite3_value[] args){
+ this.xStepInverse(cx, sqlite3_value_int(args[0]));
+ }
+ @Override public void xInverse(sqlite3_context cx, sqlite3_value[] args){
+ this.xStepInverse(cx, -sqlite3_value_int(args[0]));
+ }
+
+ private void xFinalValue(sqlite3_context cx, Integer v){
+ if(null == v) sqlite3_result_null(cx);
+ else sqlite3_result_int(cx, v);
+ }
+ @Override public void xFinal(sqlite3_context cx){
+ xFinalValue(cx, this.takeAggregateState(cx));
+ }
+ @Override public void xValue(sqlite3_context cx){
+ xFinalValue(cx, this.getAggregateState(cx,null).value);
+ }
+ };
+ int rc = sqlite3_create_function(db, "winsumint", 1, SQLITE_UTF8, func);
+ affirm( 0 == rc );
+ execSql(db, new String[] {
+ "CREATE TEMP TABLE twin(x, y); INSERT INTO twin VALUES",
+ "('a', 4),('b', 5),('c', 3),('d', 8),('e', 1)"
+ });
+ final sqlite3_stmt stmt = prepare(db,
+ "SELECT x, winsumint(y) OVER ("+
+ "ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING"+
+ ") AS sum_y "+
+ "FROM twin ORDER BY x;");
+ affirm( 0 == rc );
+ int n = 0;
+ while( SQLITE_ROW == sqlite3_step(stmt) ){
+ final String s = sqlite3_column_text16(stmt, 0);
+ final int i = sqlite3_column_int(stmt, 1);
+ switch(++n){
+ case 1: affirm( "a".equals(s) && 9==i ); break;
+ case 2: affirm( "b".equals(s) && 12==i ); break;
+ case 3: affirm( "c".equals(s) && 16==i ); break;
+ case 4: affirm( "d".equals(s) && 12==i ); break;
+ case 5: affirm( "e".equals(s) && 9==i ); break;
+ default: affirm( false /* cannot happen */ );
+ }
+ }
+ sqlite3_finalize(stmt);
+ affirm( 5 == n );
+ sqlite3_close_v2(db);
+ }
+
+ private static void listBoundMethods(){
+ if(false){
+ final java.lang.reflect.Field[] declaredFields =
+ SQLite3Jni.class.getDeclaredFields();
+ outln("Bound constants:\n");
+ for(java.lang.reflect.Field field : declaredFields) {
+ if(java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
+ outln("\t"+field.getName());
+ }
+ }
+ }
+ final java.lang.reflect.Method[] declaredMethods =
+ SQLite3Jni.class.getDeclaredMethods();
+ final java.util.List funcList = new java.util.ArrayList<>();
+ for(java.lang.reflect.Method m : declaredMethods){
+ if((m.getModifiers() & java.lang.reflect.Modifier.STATIC) != 0){
+ final String name = m.getName();
+ if(name.startsWith("sqlite3_")){
+ funcList.add(name);
+ }
+ }
+ }
+ int count = 0;
+ java.util.Collections.sort(funcList);
+ for(String n : funcList){
+ ++count;
+ outln("\t"+n+"()");
+ }
+ outln(count+" functions named sqlite3_*.");
+ }
+
+ private static void testTrace(){
+ final sqlite3 db = createNewDb();
+ final ValueHolder counter = new ValueHolder<>(0);
+ /* Ensure that characters outside of the UTF BMP survive the trip
+ from Java to sqlite3 and back to Java. (At no small efficiency
+ penalty.) */
+ final String nonBmpChar = "😃";
+ sqlite3_trace_v2(
+ db, SQLITE_TRACE_STMT | SQLITE_TRACE_PROFILE
+ | SQLITE_TRACE_ROW | SQLITE_TRACE_CLOSE,
+ new Tracer(){
+ public int xCallback(int traceFlag, Object pNative, Object x){
+ ++counter.value;
+ //outln("TRACE "+traceFlag+" pNative = "+pNative.getClass().getName());
+ switch(traceFlag){
+ case SQLITE_TRACE_STMT:
+ affirm(pNative instanceof sqlite3_stmt);
+ //outln("TRACE_STMT sql = "+x);
+ affirm(x instanceof String);
+ affirm( ((String)x).indexOf(nonBmpChar) > 0 );
+ break;
+ case SQLITE_TRACE_PROFILE:
+ affirm(pNative instanceof sqlite3_stmt);
+ affirm(x instanceof Long);
+ //outln("TRACE_PROFILE time = "+x);
+ break;
+ case SQLITE_TRACE_ROW:
+ affirm(pNative instanceof sqlite3_stmt);
+ affirm(null == x);
+ //outln("TRACE_ROW = "+sqlite3_column_text16((sqlite3_stmt)pNative, 0));
+ break;
+ case SQLITE_TRACE_CLOSE:
+ affirm(pNative instanceof sqlite3);
+ affirm(null == x);
+ break;
+ default:
+ affirm(false /*cannot happen*/);
+ break;
+ }
+ return 0;
+ }
+ });
+ execSql(db, "SELECT coalesce(null,null,'"+nonBmpChar+"'); "+
+ "SELECT 'w"+nonBmpChar+"orld'");
+ affirm( 6 == counter.value );
+ sqlite3_close_v2(db);
+ affirm( 7 == counter.value );
+ }
+
+ private static void testBusy(){
+ final String dbName = "_busy-handler.db";
+ final OutputPointer.sqlite3 outDb = new OutputPointer.sqlite3();
+
+ int rc = sqlite3_open(dbName, outDb);
+ ++metrics.dbOpen;
+ affirm( 0 == rc );
+ final sqlite3 db1 = outDb.get();
+ execSql(db1, "CREATE TABLE IF NOT EXISTS t(a)");
+ rc = sqlite3_open(dbName, outDb);
+ ++metrics.dbOpen;
+ affirm( 0 == rc );
+ affirm( outDb.get() != db1 );
+ final sqlite3 db2 = outDb.get();
+ rc = sqlite3_db_config(db1, SQLITE_DBCONFIG_MAINDBNAME, "foo");
+ affirm( sqlite3_db_filename(db1, "foo").endsWith(dbName) );
+
+ final ValueHolder xDestroyed = new ValueHolder<>(false);
+ final ValueHolder xBusyCalled = new ValueHolder<>(0);
+ BusyHandler handler = new BusyHandler(){
+ @Override public int xCallback(int n){
+ //outln("busy handler #"+n);
+ return n > 2 ? 0 : ++xBusyCalled.value;
+ }
+ @Override public void xDestroy(){
+ xDestroyed.value = true;
+ }
+ };
+ rc = sqlite3_busy_handler(db2, handler);
+ affirm(0 == rc);
+
+ // Force a locked condition...
+ execSql(db1, "BEGIN EXCLUSIVE");
+ affirm(!xDestroyed.value);
+ rc = sqlite3_prepare_v2(db2, "SELECT * from t", outStmt);
+ affirm( SQLITE_BUSY == rc);
+ assert( null == outStmt.get() );
+ affirm( 3 == xBusyCalled.value );
+ sqlite3_close_v2(db1);
+ affirm(!xDestroyed.value);
+ sqlite3_close_v2(db2);
+ affirm(xDestroyed.value);
+ try{
+ final java.io.File f = new java.io.File(dbName);
+ f.delete();
+ }catch(Exception e){
+ /* ignore */
+ }
+ }
+
+ private static void testProgress(){
+ final sqlite3 db = createNewDb();
+ final ValueHolder counter = new ValueHolder<>(0);
+ sqlite3_progress_handler(db, 1, new ProgressHandler(){
+ public int xCallback(){
+ ++counter.value;
+ return 0;
+ }
+ });
+ execSql(db, "SELECT 1; SELECT 2;");
+ affirm( counter.value > 0 );
+ int nOld = counter.value;
+ sqlite3_progress_handler(db, 0, null);
+ execSql(db, "SELECT 1; SELECT 2;");
+ affirm( nOld == counter.value );
+ sqlite3_close_v2(db);
+ }
+
+ private static void testCommitHook(){
+ final sqlite3 db = createNewDb();
+ final ValueHolder counter = new ValueHolder<>(0);
+ final ValueHolder hookResult = new ValueHolder<>(0);
+ final CommitHook theHook = new CommitHook(){
+ public int xCommitHook(){
+ ++counter.value;
+ return hookResult.value;
+ }
+ };
+ CommitHook oldHook = sqlite3_commit_hook(db, theHook);
+ affirm( null == oldHook );
+ execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')");
+ affirm( 2 == counter.value );
+ execSql(db, "BEGIN; SELECT 1; SELECT 2; COMMIT;");
+ affirm( 2 == counter.value /* NOT invoked if no changes are made */ );
+ execSql(db, "BEGIN; update t set a='d' where a='c'; COMMIT;");
+ affirm( 3 == counter.value );
+ oldHook = sqlite3_commit_hook(db, theHook);
+ affirm( theHook == oldHook );
+ execSql(db, "BEGIN; update t set a='e' where a='d'; COMMIT;");
+ affirm( 4 == counter.value );
+ oldHook = sqlite3_commit_hook(db, null);
+ affirm( theHook == oldHook );
+ execSql(db, "BEGIN; update t set a='f' where a='e'; COMMIT;");
+ affirm( 4 == counter.value );
+ oldHook = sqlite3_commit_hook(db, null);
+ affirm( null == oldHook );
+ execSql(db, "BEGIN; update t set a='g' where a='f'; COMMIT;");
+ affirm( 4 == counter.value );
+
+ final CommitHook newHook = new CommitHook(){
+ public int xCommitHook(){return 0;}
+ };
+ oldHook = sqlite3_commit_hook(db, newHook);
+ affirm( null == oldHook );
+ execSql(db, "BEGIN; update t set a='h' where a='g'; COMMIT;");
+ affirm( 4 == counter.value );
+ oldHook = sqlite3_commit_hook(db, theHook);
+ affirm( newHook == oldHook );
+ execSql(db, "BEGIN; update t set a='i' where a='h'; COMMIT;");
+ affirm( 5 == counter.value );
+ hookResult.value = SQLITE_ERROR;
+ int rc = execSql(db, false, "BEGIN; update t set a='j' where a='i'; COMMIT;");
+ affirm( SQLITE_CONSTRAINT == rc );
+ affirm( 6 == counter.value );
+ sqlite3_close_v2(db);
+ }
+
+ private static void testUpdateHook(){
+ final sqlite3 db = createNewDb();
+ final ValueHolder counter = new ValueHolder<>(0);
+ final ValueHolder expectedOp = new ValueHolder<>(0);
+ final UpdateHook theHook = new UpdateHook(){
+ @SuppressWarnings("unchecked")
+ public void xUpdateHook(int opId, String dbName, String tableName, long rowId){
+ ++counter.value;
+ if( 0!=expectedOp.value ){
+ affirm( expectedOp.value == opId );
+ }
+ }
+ };
+ UpdateHook oldHook = sqlite3_update_hook(db, theHook);
+ affirm( null == oldHook );
+ expectedOp.value = SQLITE_INSERT;
+ execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')");
+ affirm( 3 == counter.value );
+ expectedOp.value = SQLITE_UPDATE;
+ execSql(db, "update t set a='d' where a='c';");
+ affirm( 4 == counter.value );
+ oldHook = sqlite3_update_hook(db, theHook);
+ affirm( theHook == oldHook );
+ expectedOp.value = SQLITE_DELETE;
+ execSql(db, "DELETE FROM t where a='d'");
+ affirm( 5 == counter.value );
+ oldHook = sqlite3_update_hook(db, null);
+ affirm( theHook == oldHook );
+ execSql(db, "update t set a='e' where a='b';");
+ affirm( 5 == counter.value );
+ oldHook = sqlite3_update_hook(db, null);
+ affirm( null == oldHook );
+
+ final UpdateHook newHook = new UpdateHook(){
+ public void xUpdateHook(int opId, String dbName, String tableName, long rowId){
+ }
+ };
+ oldHook = sqlite3_update_hook(db, newHook);
+ affirm( null == oldHook );
+ execSql(db, "update t set a='h' where a='a'");
+ affirm( 5 == counter.value );
+ oldHook = sqlite3_update_hook(db, theHook);
+ affirm( newHook == oldHook );
+ expectedOp.value = SQLITE_UPDATE;
+ execSql(db, "update t set a='i' where a='h'");
+ affirm( 6 == counter.value );
+ sqlite3_close_v2(db);
+ }
+
+ private static void testRollbackHook(){
+ final sqlite3 db = createNewDb();
+ final ValueHolder counter = new ValueHolder<>(0);
+ final RollbackHook theHook = new RollbackHook(){
+ public void xRollbackHook(){
+ ++counter.value;
+ }
+ };
+ RollbackHook oldHook = sqlite3_rollback_hook(db, theHook);
+ affirm( null == oldHook );
+ execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')");
+ affirm( 0 == counter.value );
+ execSql(db, false, "BEGIN; SELECT 1; SELECT 2; ROLLBACK;");
+ affirm( 1 == counter.value /* contra to commit hook, is invoked if no changes are made */ );
+
+ final RollbackHook newHook = new RollbackHook(){
+ public void xRollbackHook(){return;}
+ };
+ oldHook = sqlite3_rollback_hook(db, newHook);
+ affirm( theHook == oldHook );
+ execSql(db, false, "BEGIN; SELECT 1; ROLLBACK;");
+ affirm( 1 == counter.value );
+ oldHook = sqlite3_rollback_hook(db, theHook);
+ affirm( newHook == oldHook );
+ execSql(db, false, "BEGIN; SELECT 1; ROLLBACK;");
+ affirm( 2 == counter.value );
+ int rc = execSql(db, false, "BEGIN; SELECT 1; ROLLBACK;");
+ affirm( 0 == rc );
+ affirm( 3 == counter.value );
+ sqlite3_close_v2(db);
+ }
+
+ /**
+ If FTS5 is available, runs FTS5 tests, else returns with no side
+ effects. If it is available but loading of the FTS5 bits fails,
+ it throws.
+ */
+ @SuppressWarnings("unchecked")
+ private static void testFts5() throws Exception {
+ if( !SQLITE_ENABLE_FTS5 ){
+ outln("SQLITE_ENABLE_FTS5 is not set. Skipping FTS5 tests.");
+ return;
+ }
+ Exception err = null;
+ try {
+ Class t = Class.forName("org.sqlite.jni.TesterFts5");
+ java.lang.reflect.Constructor ctor = t.getConstructor();
+ ctor.setAccessible(true);
+ ctor.newInstance() /* will run all tests */;
+ }catch(ClassNotFoundException e){
+ outln("FTS5 classes not loaded.");
+ err = e;
+ }catch(NoSuchMethodException e){
+ outln("FTS5 tester ctor not found.");
+ err = e;
+ }catch(Exception e){
+ outln("Instantiation of FTS5 tester threw.");
+ err = e;
+ }
+ if( null != err ){
+ outln("Exception: "+err);
+ err.printStackTrace();
+ throw err;
+ }
+ }
+
+ private static void testAuthorizer(){
+ final sqlite3 db = createNewDb();
+ final ValueHolder counter = new ValueHolder<>(0);
+ final ValueHolder authRc = new ValueHolder<>(0);
+ final Authorizer auth = new Authorizer(){
+ public int xAuth(int op, String s0, String s1, String s2, String s3){
+ ++counter.value;
+ //outln("xAuth(): "+s0+" "+s1+" "+s2+" "+s3);
+ return authRc.value;
+ }
+ };
+ execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')");
+ sqlite3_set_authorizer(db, auth);
+ execSql(db, "UPDATE t SET a=1");
+ affirm( 1 == counter.value );
+ authRc.value = SQLITE_DENY;
+ int rc = execSql(db, false, "UPDATE t SET a=2");
+ affirm( SQLITE_AUTH==rc );
+ // TODO: expand these tests considerably
+ sqlite3_close(db);
+ }
+
+ private static void testAutoExtension(){
+ final ValueHolder val = new ValueHolder<>(0);
+ final ValueHolder toss = new ValueHolder<>(null);
+ final AutoExtension ax = new AutoExtension(){
+ public synchronized int xEntryPoint(sqlite3 db){
+ ++val.value;
+ if( null!=toss.value ){
+ throw new RuntimeException(toss.value);
+ }
+ return 0;
+ }
+ };
+ int rc = sqlite3_auto_extension( ax );
+ affirm( 0==rc );
+ sqlite3_close(createNewDb());
+ affirm( 1==val.value );
+ sqlite3_close(createNewDb());
+ affirm( 2==val.value );
+ sqlite3_reset_auto_extension();
+ sqlite3_close(createNewDb());
+ affirm( 2==val.value );
+ rc = sqlite3_auto_extension( ax );
+ affirm( 0==rc );
+ // Must not add a new entry
+ rc = sqlite3_auto_extension( ax );
+ affirm( 0==rc );
+ sqlite3_close( createNewDb() );
+ affirm( 3==val.value );
+ affirm( sqlite3_cancel_auto_extension(ax) );
+ affirm( !sqlite3_cancel_auto_extension(ax) );
+ sqlite3_close(createNewDb());
+ affirm( 3==val.value );
+ rc = sqlite3_auto_extension( ax );
+ affirm( 0==rc );
+ Exception err = null;
+ toss.value = "Throwing from AutoExtension.";
+ try{
+ createNewDb();
+ }catch(Exception e){
+ err = e;
+ }
+ affirm( err!=null );
+ affirm( err.getMessage().indexOf(toss.value)>0 );
+ affirm( sqlite3_cancel_auto_extension(ax) );
+ }
+
+ private static void testSleep(){
+ out("Sleeping briefly... ");
+ sqlite3_sleep(600);
+ outln("Woke up.");
+ }
+
+ public static void main(String[] args) throws Exception {
+ final long timeStart = System.nanoTime();
+ test1();
+ if(false) testCompileOption();
+ final java.util.List liArgs =
+ java.util.Arrays.asList(args);
+ testOpenDb1();
+ testOpenDb2();
+ testPrepare123();
+ testBindFetchInt();
+ testBindFetchInt64();
+ testBindFetchDouble();
+ testBindFetchText();
+ testBindFetchBlob();
+ testSql();
+ testCollation();
+ testToUtf8();
+ testStatus();
+ testUdf1();
+ testUdfJavaObject();
+ testUdfAggregate();
+ testUdfWindow();
+ testTrace();
+ testBusy();
+ testProgress();
+ testCommitHook();
+ testRollbackHook();
+ testUpdateHook();
+ testAuthorizer();
+ testFts5();
+ testAutoExtension();
+ //testSleep();
+ if(liArgs.indexOf("-v")>0){
+ sqlite3_do_something_for_developer();
+ //listBoundMethods();
+ }
+ final long timeEnd = System.nanoTime();
+ affirm( SQLite3Jni.uncacheJniEnv() );
+ affirm( !SQLite3Jni.uncacheJniEnv() );
+ outln("Tests done. Metrics:");
+ outln("\tAssertions checked: "+affirmCount);
+ outln("\tDatabases opened: "+metrics.dbOpen);
+
+ int nMethods = 0;
+ int nNatives = 0;
+ final java.lang.reflect.Method[] declaredMethods =
+ SQLite3Jni.class.getDeclaredMethods();
+ for(java.lang.reflect.Method m : declaredMethods){
+ int mod = m.getModifiers();
+ if( 0!=(mod & java.lang.reflect.Modifier.STATIC) ){
+ final String name = m.getName();
+ if(name.startsWith("sqlite3_")){
+ ++nMethods;
+ if( 0!=(mod & java.lang.reflect.Modifier.NATIVE) ){
+ ++nNatives;
+ }
+ }
+ }
+ }
+ outln("\tSQLite3Jni sqlite3_*() methods: "+
+ nNatives+" native methods and "+
+ (nMethods - nNatives)+" Java impls");
+ outln("\tTotal time = "
+ +((timeEnd - timeStart)/1000000.0)+"ms");
+ }
+}
diff --git a/ext/jni/src/org/sqlite/jni/TesterFts5.java b/ext/jni/src/org/sqlite/jni/TesterFts5.java
new file mode 100644
index 000000000..6439768e2
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/TesterFts5.java
@@ -0,0 +1,87 @@
+/*
+** 2023-08-04
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file contains a set of tests for the sqlite3 JNI bindings.
+*/
+package org.sqlite.jni;
+import static org.sqlite.jni.SQLite3Jni.*;
+import static org.sqlite.jni.Tester1.*;
+
+public class TesterFts5 {
+
+ private static void test1(){
+ Fts5ExtensionApi fea = Fts5ExtensionApi.getInstance();
+ affirm( null != fea );
+ affirm( fea.getNativePointer() != 0 );
+ affirm( fea == Fts5ExtensionApi.getInstance() )/*singleton*/;
+
+ sqlite3 db = createNewDb();
+ fts5_api fApi = fts5_api.getInstanceForDb(db);
+ affirm( fApi != null );
+ affirm( fApi == fts5_api.getInstanceForDb(db) /* singleton per db */ );
+
+ execSql(db, new String[] {
+ "CREATE VIRTUAL TABLE ft USING fts5(a, b);",
+ "INSERT INTO ft(rowid, a, b) VALUES(1, 'X Y', 'Y Z');",
+ "INSERT INTO ft(rowid, a, b) VALUES(2, 'A Z', 'Y Y');"
+ });
+
+ final String pUserData = "This is pUserData";
+ ValueHolder xDestroyCalled = new ValueHolder<>(false);
+ ValueHolder xFuncCount = new ValueHolder<>(0);
+ final fts5_extension_function func = new fts5_extension_function(){
+ public void xFunction(Fts5ExtensionApi ext, Fts5Context fCx,
+ sqlite3_context pCx, sqlite3_value argv[]){
+ int nCols = ext.xColumnCount(fCx);
+ affirm( 2 == nCols );
+ affirm( nCols == argv.length );
+ affirm( ext.xUserData(fCx) == pUserData );
+ if(true){
+ OutputPointer.String op = new OutputPointer.String();
+ for(int i = 0; i < nCols; ++i ){
+ int rc = ext.xColumnText(fCx, i, op);
+ affirm( 0 == rc );
+ final String val = op.value;
+ affirm( val.equals(sqlite3_value_text(argv[i])) );
+ //outln("xFunction col "+i+": "+val);
+ }
+ }
+ ++xFuncCount.value;
+ }
+ public void xDestroy(){
+ xDestroyCalled.value = true;
+ }
+ };
+
+ int rc = fApi.xCreateFunction("myaux", pUserData, func);
+ affirm( 0==rc );
+
+ affirm( 0==xFuncCount.value );
+ execSql(db, "select myaux(ft,a,b) from ft;");
+ affirm( 2==xFuncCount.value );
+ affirm( !xDestroyCalled.value );
+ sqlite3_close_v2(db);
+ affirm( xDestroyCalled.value );
+ }
+
+ public TesterFts5(){
+ int oldAffirmCount = Tester1.affirmCount;
+ Tester1.affirmCount = 0;
+ final long timeStart = System.nanoTime();
+ test1();
+ final long timeEnd = System.nanoTime();
+ outln("FTS5 Tests done. Metrics:");
+ outln("\tAssertions checked: "+Tester1.affirmCount);
+ outln("\tTotal time = "
+ +((timeEnd - timeStart)/1000000.0)+"ms");
+ Tester1.affirmCount = oldAffirmCount;
+ }
+}
diff --git a/ext/jni/src/org/sqlite/jni/Tracer.java b/ext/jni/src/org/sqlite/jni/Tracer.java
new file mode 100644
index 000000000..fa62edbfa
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/Tracer.java
@@ -0,0 +1,62 @@
+/*
+** 2023-07-22
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ Callback proxy for use with sqlite3_trace_v2().
+*/
+public interface Tracer {
+ /**
+ Achtung: this interface is subject to change because the current
+ approach to mapping the passed-in natives back to Java is
+ uncomfortably quirky.
+
+ Called by sqlite3 for various tracing operations, as per
+ sqlite3_trace_v2(). Note that this interface elides the 2nd
+ argument to the native trace callback, as that role is better
+ filled by instance-local state.
+
+ The 2nd argument to this function, if non-0, will be a native
+ pointer to either an sqlite3 or sqlite3_stmt object, depending on
+ the first argument (see below). Client code can pass it to the
+ sqlite3 resp. sqlite3_stmt constructor to create a wrapping
+ object, if necessary. This API does not do so by default because
+ tracing can be called frequently, creating such a wrapper for
+ each call is comparatively expensive, and the objects are
+ probably only seldom useful.
+
+ The final argument to this function is the "X" argument
+ documented for sqlite3_trace() and sqlite3_trace_v2(). Its type
+ depends on value of the first argument:
+
+ - SQLITE_TRACE_STMT: pNative is a sqlite3_stmt. pX is a string
+ containing the prepared SQL, with one caveat: JNI only provides
+ us with the ability to convert that string to MUTF-8, as
+ opposed to standard UTF-8, and is cannot be ruled out that that
+ difference may be significant for certain inputs. The
+ alternative would be that we first convert it to UTF-16 before
+ passing it on, but there's no readily-available way to do that
+ without calling back into the db to peform the conversion
+ (which would lead to further tracing).
+
+ - SQLITE_TRACE_PROFILE: pNative is a sqlite3_stmt. pX is a Long
+ holding an approximate number of nanoseconds the statement took
+ to run.
+
+ - SQLITE_TRACE_ROW: pNative is a sqlite3_stmt. pX is null.
+
+ - SQLITE_TRACE_CLOSE: pNative is a sqlite3. pX is null.
+ */
+ int xCallback(int traceFlag, Object pNative, Object pX);
+}
diff --git a/ext/jni/src/org/sqlite/jni/UpdateHook.java b/ext/jni/src/org/sqlite/jni/UpdateHook.java
new file mode 100644
index 000000000..171e2bdb4
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/UpdateHook.java
@@ -0,0 +1,25 @@
+/*
+** 2023-07-22
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ Callback proxy for use with sqlite3_update_hook().
+*/
+public interface UpdateHook {
+ /**
+ Works as documented for the sqlite3_update_hook() callback.
+ Must not throw.
+ */
+ void xUpdateHook(int opId, String dbName, String tableName, long rowId);
+}
diff --git a/ext/jni/src/org/sqlite/jni/ValueHolder.java b/ext/jni/src/org/sqlite/jni/ValueHolder.java
new file mode 100644
index 000000000..7f6a463ba
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/ValueHolder.java
@@ -0,0 +1,25 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ A helper class which simply holds a single value. Its current use
+ is for communicating values out of anonymous classes, as doing so
+ requires a "final" reference.
+*/
+public class ValueHolder {
+ public T value;
+ public ValueHolder(){}
+ public ValueHolder(T v){value = v;}
+}
diff --git a/ext/jni/src/org/sqlite/jni/fts5_api.java b/ext/jni/src/org/sqlite/jni/fts5_api.java
new file mode 100644
index 000000000..43b3d62de
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/fts5_api.java
@@ -0,0 +1,69 @@
+/*
+** 2023-08-05
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ INCOMPLETE AND COMPLETELY UNTESTED.
+
+ A wrapper for communicating C-level (fts5_api*) instances with
+ Java. These wrappers do not own their associated pointer, they
+ simply provide a type-safe way to communicate it between Java and C
+ via JNI.
+*/
+public final class fts5_api extends NativePointerHolder {
+ /* Only invoked from JNI */
+ private fts5_api(){}
+ public final int iVersion = 2;
+
+ /**
+ Returns the fts5_api instance associated with the given db, or
+ null if something goes horribly wrong.
+ */
+ public static synchronized native fts5_api getInstanceForDb(@NotNull sqlite3 db);
+
+ // int (*xCreateTokenizer)(
+ // fts5_api *pApi,
+ // const char *zName,
+ // void *pContext,
+ // fts5_tokenizer *pTokenizer,
+ // void (*xDestroy)(void*)
+ // );
+
+ // /* Find an existing tokenizer */
+ // int (*xFindTokenizer)(
+ // fts5_api *pApi,
+ // const char *zName,
+ // void **ppContext,
+ // fts5_tokenizer *pTokenizer
+ // );
+
+ // /* Create a new auxiliary function */
+ // int (*xCreateFunction)(
+ // fts5_api *pApi,
+ // const char *zName,
+ // void *pContext,
+ // fts5_extension_function xFunction,
+ // void (*xDestroy)(void*)
+ // );
+
+ public synchronized native int xCreateFunction(@NotNull String name,
+ @Nullable Object userData,
+ @NotNull fts5_extension_function xFunction);
+
+ public int xCreateFunction(@NotNull String name,
+ @NotNull fts5_extension_function xFunction){
+ return xCreateFunction(name, null, xFunction);
+ }
+
+}
diff --git a/ext/jni/src/org/sqlite/jni/fts5_extension_function.java b/ext/jni/src/org/sqlite/jni/fts5_extension_function.java
new file mode 100644
index 000000000..0e273119f
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/fts5_extension_function.java
@@ -0,0 +1,37 @@
+/*
+** 2023-08-05
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ JNI-level wrapper for C's fts5_extension_function type.
+
+*/
+public abstract class fts5_extension_function {
+ // typedef void (*fts5_extension_function)(
+ // const Fts5ExtensionApi *pApi, /* API offered by current FTS version */
+ // Fts5Context *pFts, /* First arg to pass to pApi functions */
+ // sqlite3_context *pCtx, /* Context for returning result/error */
+ // int nVal, /* Number of values in apVal[] array */
+ // sqlite3_value **apVal /* Array of trailing arguments */
+ // );
+
+ /**
+ The callback implementation, corresponding to the xFunction
+ argument of C's fts5_api::xCreateFunction().
+ */
+ public abstract void xFunction(Fts5ExtensionApi ext, Fts5Context fCx,
+ sqlite3_context pCx, sqlite3_value argv[]);
+ //! Optionally override
+ public void xDestroy(){}
+}
diff --git a/ext/jni/src/org/sqlite/jni/fts5_tokenizer.java b/ext/jni/src/org/sqlite/jni/fts5_tokenizer.java
new file mode 100644
index 000000000..097a0cc05
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/fts5_tokenizer.java
@@ -0,0 +1,49 @@
+/*
+** 2023-08-05
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ INCOMPLETE AND COMPLETELY UNTESTED.
+
+ A wrapper for communicating C-level (fts5_tokenizer*) instances with
+ Java. These wrappers do not own their associated pointer, they
+ simply provide a type-safe way to communicate it between Java and C
+ via JNI.
+*/
+public final class fts5_tokenizer extends NativePointerHolder {
+ /* Only invoked by JNI */
+ private fts5_tokenizer(){}
+
+ // int (*xCreate)(void*, const char **azArg, int nArg, Fts5Tokenizer **ppOut);
+ // void (*xDelete)(Fts5Tokenizer*);
+
+ public native int xTokenize(@NotNull Fts5Tokenizer t, int tokFlags,
+ @NotNull byte pText[],
+ @NotNull Fts5.xTokenizeCallback callback);
+
+
+ // int (*xTokenize)(Fts5Tokenizer*,
+ // void *pCtx,
+ // int flags, /* Mask of FTS5_TOKENIZE_* flags */
+ // const char *pText, int nText,
+ // int (*xToken)(
+ // void *pCtx, /* Copy of 2nd argument to xTokenize() */
+ // int tflags, /* Mask of FTS5_TOKEN_* flags */
+ // const char *pToken, /* Pointer to buffer containing token */
+ // int nToken, /* Size of token in bytes */
+ // int iStart, /* Byte offset of token within input text */
+ // int iEnd /* Byte offset of end of token within input text */
+ // )
+ // );
+}
diff --git a/ext/jni/src/org/sqlite/jni/sqlite3.java b/ext/jni/src/org/sqlite/jni/sqlite3.java
new file mode 100644
index 000000000..cfc6c08d4
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/sqlite3.java
@@ -0,0 +1,37 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ A wrapper for communicating C-level (sqlite3*) instances with
+ Java. These wrappers do not own their associated pointer, they
+ simply provide a type-safe way to communicate it between Java
+ and C via JNI.
+*/
+public final class sqlite3 extends NativePointerHolder {
+ // Only invoked from JNI
+ private sqlite3(){}
+
+ public String toString(){
+ long ptr = getNativePointer();
+ if( 0==ptr ){
+ return sqlite3.class.getSimpleName()+"@null";
+ }
+ String fn = SQLite3Jni.sqlite3_db_filename(this, "main");
+ return sqlite3.class.getSimpleName()
+ +"@"+String.format("0x%08x",ptr)
+ +"["+((null == fn) ? "" : fn)+"]"
+ ;
+ }
+}
diff --git a/ext/jni/src/org/sqlite/jni/sqlite3_context.java b/ext/jni/src/org/sqlite/jni/sqlite3_context.java
new file mode 100644
index 000000000..a61ff21c7
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/sqlite3_context.java
@@ -0,0 +1,66 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ sqlite3_context instances are used in conjunction with user-defined
+ SQL functions (a.k.a. UDFs).
+*/
+public final class sqlite3_context extends NativePointerHolder {
+ /**
+ For use only by the JNI layer. It's permitted to set this even
+ though it's private.
+ */
+ private long aggregateContext = 0;
+
+ /**
+ getAggregateContext() corresponds to C's
+ sqlite3_aggregate_context(), with a slightly different interface
+ to account for cross-language differences. It serves the same
+ purposes in a slightly different way: it provides a key which is
+ stable across invocations of "matching sets" of a UDF's callbacks,
+ such that all calls into those callbacks can determine which "set"
+ of those calls they belong to.
+
+ If this object is being used in the context of an aggregate or
+ window UDF, this function returns a non-0 value which is distinct
+ for each set of UDF callbacks from a single invocation of the
+ UDF, otherwise it returns 0. The returned value is only only
+ valid within the context of execution of a single SQL statement,
+ and may be re-used by future invocations of the UDF in different
+ SQL statements.
+
+ Consider this SQL, where MYFUNC is a user-defined aggregate function:
+
+ SELECT MYFUNC(A), MYFUNC(B) FROM T;
+
+ The xStep() and xFinal() methods of the callback need to be able
+ to differentiate between those two invocations in order to
+ perform their work properly. The value returned by
+ getAggregateContext() will be distinct for each of those
+ invocations of MYFUNC() and is intended to be used as a lookup
+ key for mapping callback invocations to whatever client-defined
+ state is needed by the UDF.
+
+ There is one case where this will return 0 in the context of an
+ aggregate or window function: if the result set has no rows,
+ the UDF's xFinal() will be called without any other x...() members
+ having been called. In that one case, no aggregate context key will
+ have been generated. xFinal() implementations need to be prepared to
+ accept that condition as legal.
+ */
+ public long getAggregateContext(){
+ return aggregateContext;
+ }
+}
diff --git a/ext/jni/src/org/sqlite/jni/sqlite3_stmt.java b/ext/jni/src/org/sqlite/jni/sqlite3_stmt.java
new file mode 100644
index 000000000..d67230137
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/sqlite3_stmt.java
@@ -0,0 +1,25 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+/**
+ A wrapper for communicating C-level (sqlite3_stmt*) instances with
+ Java. These wrappers do not own their associated pointer, they
+ simply provide a type-safe way to communicate it between Java and C
+ via JNI.
+*/
+public final class sqlite3_stmt extends NativePointerHolder {
+ // Only invoked from JNI.
+ private sqlite3_stmt(){}
+}
diff --git a/ext/jni/src/org/sqlite/jni/sqlite3_value.java b/ext/jni/src/org/sqlite/jni/sqlite3_value.java
new file mode 100644
index 000000000..2cfb32ff1
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/sqlite3_value.java
@@ -0,0 +1,19 @@
+/*
+** 2023-07-21
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni;
+
+public final class sqlite3_value extends NativePointerHolder {
+ //! Invoked only from JNI.
+ private sqlite3_value(){}
+}
diff --git a/ext/jni/src/org/sqlite/jni/tester/SQLTester.java b/ext/jni/src/org/sqlite/jni/tester/SQLTester.java
new file mode 100644
index 000000000..ffdb867d9
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/tester/SQLTester.java
@@ -0,0 +1,1421 @@
+/*
+** 2023-08-08
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file contains the main application entry pointer for the
+** SQLTester framework.
+*/
+package org.sqlite.jni.tester;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.*;
+import org.sqlite.jni.*;
+import static org.sqlite.jni.SQLite3Jni.*;
+import org.sqlite.jni.sqlite3;
+
+
+/**
+ Modes for how to escape (or not) column values and names from
+ SQLTester.execSql() to the result buffer output.
+*/
+enum ResultBufferMode {
+ //! Do not append to result buffer
+ NONE,
+ //! Append output escaped.
+ ESCAPED,
+ //! Append output as-is
+ ASIS
+};
+
+/**
+ Modes to specify how to emit multi-row output from
+ SQLTester.execSql() to the result buffer.
+*/
+enum ResultRowMode {
+ //! Keep all result rows on one line, space-separated.
+ ONELINE,
+ //! Add a newline between each result row.
+ NEWLINE
+};
+
+/**
+ Base exception type for test-related failures.
+*/
+class SQLTesterException extends RuntimeException {
+ private boolean bFatal = false;
+
+ SQLTesterException(String msg){
+ super(msg);
+ }
+
+ protected SQLTesterException(String msg, boolean fatal){
+ super(msg);
+ bFatal = fatal;
+ }
+
+ /**
+ Indicates whether the framework should consider this exception
+ type as immediately fatal to the test run or not.
+ */
+ final boolean isFatal(){ return bFatal; }
+}
+
+class DbException extends SQLTesterException {
+ DbException(sqlite3 db, int rc, boolean closeDb){
+ super("DB error #"+rc+": "+sqlite3_errmsg(db),true);
+ if( closeDb ) sqlite3_close_v2(db);
+ }
+ DbException(sqlite3 db, int rc){
+ this(db, rc, false);
+ }
+}
+
+/**
+ Generic test-failed exception.
+ */
+class TestScriptFailed extends SQLTesterException {
+ public TestScriptFailed(TestScript ts, String msg){
+ super(ts.getOutputPrefix()+": "+msg, true);
+ }
+}
+
+/**
+ Thrown when an unknown test command is encountered in a script.
+*/
+class UnknownCommand extends SQLTesterException {
+ public UnknownCommand(TestScript ts, String cmd){
+ super(ts.getOutputPrefix()+": unknown command: "+cmd, false);
+ }
+}
+
+/**
+ Thrown when an "incompatible directive" is found in a script. This
+ can be the presence of a C-preprocessor construct, specific
+ metadata tags within a test script's header, or specific test
+ constructs which are incompatible with this particular
+ implementation.
+*/
+class IncompatibleDirective extends SQLTesterException {
+ public IncompatibleDirective(TestScript ts, String line){
+ super(ts.getOutputPrefix()+": incompatible directive: "+line, false);
+ }
+}
+
+/**
+ Console output utility class.
+*/
+class Outer {
+ private int verbosity = 0;
+
+ static void out(Object val){
+ System.out.print(val);
+ }
+
+ Outer out(Object... vals){
+ for(Object v : vals) out(v);
+ return this;
+ }
+
+ Outer outln(Object... vals){
+ out(vals).out("\n");
+ return this;
+ }
+
+ Outer verbose(Object... vals){
+ if(verbosity>0){
+ out("VERBOSE",(verbosity>1 ? "+: " : ": ")).outln(vals);
+ }
+ return this;
+ }
+
+ void setVerbosity(int level){
+ verbosity = level;
+ }
+
+ int getVerbosity(){
+ return verbosity;
+ }
+
+ public boolean isVerbose(){return verbosity > 0;}
+
+}
+
+/**
+ This class provides an application which aims to implement the
+ rudimentary SQL-driven test tool described in the accompanying
+ test-script-interpreter.md.
+
+ This is a work in progress.
+
+
+ An instance of this application provides a core set of services
+ which TestScript instances use for processing testing logic.
+ TestScripts, in turn, delegate the concrete test work to Command
+ objects, which the TestScript parses on their behalf.
+*/
+public class SQLTester {
+ //! List of input script files.
+ private final java.util.List listInFiles = new ArrayList<>();
+ //! Console output utility.
+ private final Outer outer = new Outer();
+ //! Test input buffer.
+ private final StringBuilder inputBuffer = new StringBuilder();
+ //! Test result buffer.
+ private final StringBuilder resultBuffer = new StringBuilder();
+ //! Buffer for REQUIRED_PROPERTIES pragmas.
+ private final StringBuilder dbInitSql = new StringBuilder();
+ //! Output representation of SQL NULL.
+ private String nullView = "nil";
+ //! Total tests run.
+ private int nTotalTest = 0;
+ //! Total test script files run.
+ private int nTestFile = 0;
+ //! Number of scripts which were aborted.
+ private int nAbortedScript = 0;
+ //! Per-script test counter.
+ private int nTest = 0;
+ //! True to enable column name output from execSql()
+ private boolean emitColNames;
+ //! True to keep going regardless of how a test fails.
+ private boolean keepGoing = false;
+ //! The list of available db handles.
+ private final sqlite3[] aDb = new sqlite3[7];
+ //! Index into aDb of the current db.
+ private int iCurrentDb = 0;
+ //! Name of the default db, re-created for each script.
+ private final String initialDbName = "test.db";
+
+
+ public SQLTester(){
+ reset();
+ }
+
+ void setVerbosity(int level){
+ this.outer.setVerbosity( level );
+ }
+ int getVerbosity(){
+ return this.outer.getVerbosity();
+ }
+ boolean isVerbose(){
+ return this.outer.isVerbose();
+ }
+
+ void outputColumnNames(boolean b){ emitColNames = b; }
+
+ void verbose(Object... vals){
+ outer.verbose(vals);
+ }
+
+ void outln(Object... vals){
+ outer.outln(vals);
+ }
+
+ void out(Object... vals){
+ outer.out(vals);
+ }
+
+ //! Adds the given test script to the to-test list.
+ public void addTestScript(String filename){
+ listInFiles.add(filename);
+ //verbose("Added file ",filename);
+ }
+
+ private void setupInitialDb() throws DbException {
+ if( null==aDb[0] ){
+ Util.unlink(initialDbName);
+ openDb(0, initialDbName, true);
+ }else{
+ outln("WARNING: setupInitialDb() unexpectedly ",
+ "triggered while it is opened.");
+ }
+ }
+
+ static final String[] startEmoji = {
+ "🚴", "🏄", "🏇", "🤸", "⛹", "🏊", "⛷", "🧗", "🏋"
+ };
+ static final int nStartEmoji = startEmoji.length;
+ static int iStartEmoji = 0;
+
+ private static String nextStartEmoji(){
+ return startEmoji[iStartEmoji++ % nStartEmoji];
+ }
+
+ public void runTests() throws Exception {
+ final long tStart = System.nanoTime();
+ for(String f : listInFiles){
+ reset();
+ ++nTestFile;
+ final TestScript ts = new TestScript(f);
+ outln(nextStartEmoji(), " starting [",f,"]");
+ boolean threw = false;
+ final long timeStart = System.nanoTime();
+ try{
+ ts.run(this);
+ }catch(SQLTesterException e){
+ threw = true;
+ outln("🔥EXCEPTION: ",e.getClass().getSimpleName(),": ",e.getMessage());
+ ++nAbortedScript;
+ if( keepGoing ) outln("Continuing anyway becaure of the keep-going option.");
+ else if( e.isFatal() ) throw e;
+ }finally{
+ final long timeEnd = System.nanoTime();
+ outln("🏁",(threw ? "❌" : "✅")," ",nTest," test(s) in ",
+ ((timeEnd-timeStart)/1000000.0),"ms.");
+ //ts.getFilename());
+ }
+ }
+ final long tEnd = System.nanoTime();
+ outln("Total run-time: ",((tEnd-tStart)/1000000.0),"ms");
+ Util.unlink(initialDbName);
+ }
+
+ private StringBuilder clearBuffer(StringBuilder b){
+ b.setLength(0);;
+ return b;
+ }
+
+ StringBuilder clearInputBuffer(){
+ return clearBuffer(inputBuffer);
+ }
+
+ StringBuilder clearResultBuffer(){
+ return clearBuffer(resultBuffer);
+ }
+
+ StringBuilder getInputBuffer(){ return inputBuffer; }
+
+ void appendInput(String n, boolean addNL){
+ inputBuffer.append(n);
+ if(addNL) inputBuffer.append('\n');
+ }
+
+ void appendResult(String n, boolean addNL){
+ resultBuffer.append(n);
+ if(addNL) resultBuffer.append('\n');
+ }
+
+ void appendDbInitSql(String n) throws DbException {
+ dbInitSql.append(n).append('\n');
+ if( null!=getCurrentDb() ){
+ //outln("RUNNING DB INIT CODE: ",n);
+ execSql(null, true, ResultBufferMode.NONE, null, n);
+ }
+ }
+ String getDbInitSql(){ return dbInitSql.toString(); }
+
+ String getInputText(){ return inputBuffer.toString(); }
+
+ String getResultText(){ return resultBuffer.toString(); }
+
+ private String takeBuffer(StringBuilder b){
+ final String rc = b.toString();
+ clearBuffer(b);
+ return rc;
+ }
+
+ String takeInputBuffer(){ return takeBuffer(inputBuffer); }
+
+ String takeResultBuffer(){ return takeBuffer(resultBuffer); }
+
+ int getCurrentDbId(){ return iCurrentDb; }
+
+ SQLTester affirmDbId(int n) throws IndexOutOfBoundsException {
+ if(n<0 || n>=aDb.length){
+ throw new IndexOutOfBoundsException("illegal db number: "+n);
+ }
+ return this;
+ }
+
+ sqlite3 setCurrentDb(int n) throws Exception{
+ return affirmDbId(n).aDb[n];
+ }
+
+ sqlite3 getCurrentDb(){ return aDb[iCurrentDb]; }
+
+ sqlite3 getDbById(int id) throws Exception{
+ return affirmDbId(id).aDb[id];
+ }
+
+ void closeDb(int id) {
+ final sqlite3 db = affirmDbId(id).aDb[id];
+ if( null != db ){
+ sqlite3_close_v2(db);
+ aDb[id] = null;
+ }
+ }
+
+ void closeDb() { closeDb(iCurrentDb); }
+
+ void closeAllDbs(){
+ for(int i = 0; i 0){
+ //outln("RUNNING DB INIT CODE: ",dbInitSql.toString());
+ rc = execSql(db, false, ResultBufferMode.NONE,
+ null, dbInitSql.toString());
+ }
+ if( 0!=rc ){
+ throw new DbException(db, rc, true);
+ }
+ return aDb[iCurrentDb] = db;
+ }
+
+ sqlite3 openDb(int slot, String name, boolean createIfNeeded) throws DbException {
+ affirmDbId(slot);
+ iCurrentDb = slot;
+ return openDb(name, createIfNeeded);
+ }
+
+ /**
+ Resets all tester context state except for that related to
+ tracking running totals.
+ */
+ void reset(){
+ clearInputBuffer();
+ clearResultBuffer();
+ clearBuffer(dbInitSql);
+ closeAllDbs();
+ nTest = 0;
+ nullView = "nil";
+ emitColNames = false;
+ iCurrentDb = 0;
+ dbInitSql.append("SELECT 1;");
+ }
+
+ void setNullValue(String v){nullView = v;}
+
+ /**
+ If true, encountering an unknown command in a script causes the
+ remainder of the script to be skipped, rather than aborting the
+ whole script run.
+ */
+ boolean skipUnknownCommands(){
+ // Currently hard-coded. Potentially a flag someday.
+ return true;
+ }
+
+ void incrementTestCounter(){ ++nTest; ++nTotalTest; }
+
+ //! "Special" characters - we have to escape output if it contains any.
+ static final Pattern patternSpecial = Pattern.compile(
+ "[\\x00-\\x20\\x22\\x5c\\x7b\\x7d]"
+ );
+ //! Either of '{' or '}'.
+ static final Pattern patternSquiggly = Pattern.compile("[{}]");
+
+ /**
+ Returns v or some escaped form of v, as defined in the tester's
+ spec doc.
+ */
+ String escapeSqlValue(String v){
+ if( "".equals(v) ) return "{}";
+ Matcher m = patternSpecial.matcher(v);
+ if( !m.find() ){
+ return v /* no escaping needed */;
+ }
+ m = patternSquiggly.matcher(v);
+ if( !m.find() ){
+ return "{"+v+"}";
+ }
+ final StringBuilder sb = new StringBuilder("\"");
+ final int n = v.length();
+ for(int i = 0; i < n; ++i){
+ final char ch = v.charAt(i);
+ switch(ch){
+ case '\\': sb.append("\\\\"); break;
+ case '"': sb.append("\\\""); break;
+ default:
+ //verbose("CHAR ",(int)ch," ",ch," octal=",String.format("\\%03o", (int)ch));
+ if( (int)ch < 32 ) sb.append(String.format("\\%03o", (int)ch));
+ else sb.append(ch);
+ break;
+ }
+ }
+ sb.append("\"");
+ return sb.toString();
+ }
+
+ private void appendDbErr(sqlite3 db, StringBuilder sb, int rc){
+ sb.append(org.sqlite.jni.ResultCode.getEntryForInt(rc)).append(' ');
+ final String msg = escapeSqlValue(sqlite3_errmsg(db));
+ if( '{' == msg.charAt(0) ){
+ sb.append(msg);
+ }else{
+ sb.append('{').append(msg).append('}');
+ }
+ }
+
+ /**
+ Runs SQL on behalf of test commands and outputs the results following
+ the very specific rules of the test framework.
+
+ If db is null, getCurrentDb() is assumed. If throwOnError is true then
+ any db-side error will result in an exception, else they result in
+ the db's result code.
+
+ appendMode specifies how/whether to append results to the result
+ buffer. lineMode specifies whether to output all results in a
+ single line or one line per row. If appendMode is
+ ResultBufferMode.NONE then lineMode is ignored and may be null.
+ */
+ public int execSql(sqlite3 db, boolean throwOnError,
+ ResultBufferMode appendMode, ResultRowMode lineMode,
+ String sql) throws SQLTesterException {
+ if( null==db && null==aDb[0] ){
+ // Delay opening of the initial db to enable tests to change its
+ // name and inject on-connect code via, e.g., the MEMDB
+ // directive. this setup as the potential to misinteract with
+ // auto-extension timing and must be done carefully.
+ setupInitialDb();
+ }
+ final OutputPointer.Int32 oTail = new OutputPointer.Int32();
+ final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt();
+ final byte[] sqlUtf8 = sql.getBytes(StandardCharsets.UTF_8);
+ if( null==db ) db = getCurrentDb();
+ int pos = 0, n = 1;
+ byte[] sqlChunk = sqlUtf8;
+ int rc = 0;
+ sqlite3_stmt stmt = null;
+ int spacing = 0 /* emit a space for --result if>0 */ ;
+ final StringBuilder sb = (ResultBufferMode.NONE==appendMode)
+ ? null : resultBuffer;
+ //outln("sqlChunk len= = ",sqlChunk.length);
+ try{
+ while(pos < sqlChunk.length){
+ if(pos > 0){
+ sqlChunk = Arrays.copyOfRange(sqlChunk, pos,
+ sqlChunk.length);
+ }
+ if( 0==sqlChunk.length ) break;
+ rc = sqlite3_prepare_v2(db, sqlChunk, outStmt, oTail);
+ /*outln("PREPARE rc ",rc," oTail=",oTail.get(),": ",
+ new String(sqlChunk,StandardCharsets.UTF_8),"\n");*/
+ if( 0!=rc ){
+ if(throwOnError){
+ throw new DbException(db, rc);
+ }else if( null!=sb ){
+ appendDbErr(db, sb, rc);
+ }
+ break;
+ }
+ pos = oTail.value;
+ stmt = outStmt.take();
+ if( null == stmt ){
+ // empty statement was parsed.
+ continue;
+ }
+ if( null!=sb ){
+ // Add the output to the result buffer...
+ final int nCol = sqlite3_column_count(stmt);
+ String colName = null, val = null;
+ while( SQLITE_ROW == (rc = sqlite3_step(stmt)) ){
+ for(int i = 0; i < nCol; ++i){
+ if( spacing++ > 0 ) sb.append(' ');
+ if( emitColNames ){
+ colName = sqlite3_column_name(stmt, i);
+ switch(appendMode){
+ case ASIS:
+ sb.append( colName );
+ break;
+ case ESCAPED:
+ sb.append( escapeSqlValue(colName) );
+ break;
+ default:
+ throw new SQLTesterException("Unhandled ResultBufferMode: "+appendMode);
+ }
+ sb.append(' ');
+ }
+ val = sqlite3_column_text16(stmt, i);
+ if( null==val ){
+ sb.append( nullView );
+ continue;
+ }
+ switch(appendMode){
+ case ASIS:
+ sb.append( val );
+ break;
+ case ESCAPED:
+ sb.append( escapeSqlValue(val) );
+ break;
+ default:
+ throw new SQLTesterException("Unhandled ResultBufferMode: "+appendMode);
+ }
+ }
+ if( ResultRowMode.NEWLINE == lineMode ){
+ spacing = 0;
+ sb.append('\n');
+ }
+ }
+ }else{
+ while( SQLITE_ROW == (rc = sqlite3_step(stmt)) ){}
+ }
+ sqlite3_finalize(stmt);
+ stmt = null;
+ if(SQLITE_ROW==rc || SQLITE_DONE==rc) rc = 0;
+ else if( rc!=0 ){
+ if( null!=sb ){
+ appendDbErr(db, sb, rc);
+ }
+ break;
+ }
+ }
+ }finally{
+ sqlite3_finalize(stmt);
+ }
+ if( 0!=rc && throwOnError ){
+ throw new DbException(db, rc);
+ }
+ return rc;
+ }
+
+ public static void main(String[] argv) throws Exception{
+ installCustomExtensions();
+ boolean dumpInternals = false;
+ final SQLTester t = new SQLTester();
+ for(String a : argv){
+ if(a.startsWith("-")){
+ final String flag = a.replaceFirst("-+","");
+ if( flag.equals("verbose") ){
+ // Use --verbose up to 3 times
+ t.setVerbosity(t.getVerbosity() + 1);
+ }else if( flag.equals("keep-going") ){
+ t.keepGoing = true;
+ }else if( flag.equals("internals") ){
+ dumpInternals = true;
+ }else{
+ throw new IllegalArgumentException("Unhandled flag: "+flag);
+ }
+ continue;
+ }
+ t.addTestScript(a);
+ }
+ final AutoExtension ax = new AutoExtension() {
+ private final SQLTester tester = t;
+ public int xEntryPoint(sqlite3 db){
+ final String init = tester.getDbInitSql();
+ if( !init.isEmpty() ){
+ tester.execSql(db, true, ResultBufferMode.NONE, null, init);
+ }
+ return 0;
+ }
+ };
+ sqlite3_auto_extension(ax);
+ try {
+ t.runTests();
+ }finally{
+ sqlite3_cancel_auto_extension(ax);
+ t.outln("Processed ",t.nTotalTest," test(s) in ",t.nTestFile," file(s).");
+ if( t.nAbortedScript > 0 ){
+ t.outln("Aborted ",t.nAbortedScript," script(s).");
+ }
+ if( dumpInternals ){
+ sqlite3_do_something_for_developer();
+ }
+ }
+ }
+
+ /**
+ Internal impl of the public strglob() method. Neither argument
+ may be NULL and both _MUST_ be NUL-terminated.
+ */
+ private static native int strglob(byte[] glob, byte[] txt);
+
+ /**
+ Works essentially the same as sqlite3_strglob() except that the
+ glob character '#' matches a sequence of one or more digits. It
+ does not match when it appears at the start or middle of a series
+ of digits, e.g. "#23" or "1#3", but will match at the end,
+ e.g. "12#".
+ */
+ static int strglob(String glob, String txt){
+ return strglob(
+ (glob+"\0").getBytes(StandardCharsets.UTF_8),
+ (txt+"\0").getBytes(StandardCharsets.UTF_8)
+ );
+ }
+
+ /**
+ Sets up C-side components needed by the test framework. This must
+ not be called until main() is triggered so that it does not
+ interfere with library clients who don't use this class.
+ */
+ static native void installCustomExtensions();
+ static {
+ System.loadLibrary("sqlite3-jni")
+ /* Interestingly, when SQLTester is the main app, we have to
+ load that lib from here. The same load from SQLite3Jni does
+ not happen early enough. Without this,
+ installCustomExtensions() is an unresolved symbol. */;
+ }
+
+}
+
+/**
+ General utilities for the SQLTester bits.
+*/
+final class Util {
+
+ //! Throws a new T, appending all msg args into a string for the message.
+ static void toss(Class extends Exception> errorType, Object... msg) throws Exception {
+ StringBuilder sb = new StringBuilder();
+ for(Object s : msg) sb.append(s);
+ final java.lang.reflect.Constructor extends Exception> ctor =
+ errorType.getConstructor(String.class);
+ throw ctor.newInstance(sb.toString());
+ }
+
+ static void toss(Object... msg) throws Exception{
+ toss(RuntimeException.class, msg);
+ }
+
+ //! Tries to delete the given file, silently ignoring failure.
+ static void unlink(String filename){
+ try{
+ final java.io.File f = new java.io.File(filename);
+ f.delete();
+ }catch(Exception e){
+ /* ignore */
+ }
+ }
+
+ /**
+ Appends all entries in argv[1..end] into a space-separated
+ string, argv[0] is not included because it's expected to be a
+ command name.
+ */
+ static String argvToString(String[] argv){
+ StringBuilder sb = new StringBuilder();
+ for(int i = 1; i < argv.length; ++i ){
+ if( i>1 ) sb.append(" ");
+ sb.append( argv[i] );
+ }
+ return sb.toString();
+ }
+
+}
+
+/**
+ Base class for test script commands. It provides a set of utility
+ APIs for concrete command implementations.
+
+ Each subclass must have a public no-arg ctor and must implement
+ the process() method which is abstract in this class.
+
+ Commands are intended to be stateless, except perhaps for counters
+ and similar internals. Specifically, no state which changes the
+ behavior between any two invocations of process() should be
+ retained.
+*/
+abstract class Command {
+ protected Command(){}
+
+ /**
+ Must process one command-unit of work and either return
+ (on success) or throw (on error).
+
+ The first two arguments specify the context of the test. The TestScript
+ provides the content of the test and the SQLTester providers the sandbox
+ in which that script is being evaluated.
+
+ argv is a list with the command name followed by any arguments to
+ that command. The argcCheck() method from this class provides
+ very basic argc validation.
+ */
+ public abstract void process(
+ SQLTester st, TestScript ts, String[] argv
+ ) throws Exception;
+
+ /**
+ If argv.length-1 (-1 because the command's name is in argv[0]) does not
+ fall in the inclusive range (min,max) then this function throws. Use
+ a max value of -1 to mean unlimited.
+ */
+ protected final void argcCheck(TestScript ts, String[] argv, int min, int max) throws Exception{
+ int argc = argv.length-1;
+ if(argc=0 && argc>max)){
+ if( min==max ){
+ ts.toss(argv[0]," requires exactly ",min," argument(s)");
+ }else if(max>0){
+ ts.toss(argv[0]," requires ",min,"-",max," arguments.");
+ }else{
+ ts.toss(argv[0]," requires at least ",min," arguments.");
+ }
+ }
+ }
+
+ /**
+ Equivalent to argcCheck(argv,argc,argc).
+ */
+ protected final void argcCheck(TestScript ts, String[] argv, int argc) throws Exception{
+ argcCheck(ts, argv, argc, argc);
+ }
+}
+
+//! --close command
+class CloseDbCommand extends Command {
+ public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+ argcCheck(ts,argv,0,1);
+ Integer id;
+ if(argv.length>1){
+ String arg = argv[1];
+ if("all".equals(arg)){
+ t.closeAllDbs();
+ return;
+ }
+ else{
+ id = Integer.parseInt(arg);
+ }
+ }else{
+ id = t.getCurrentDbId();
+ }
+ t.closeDb(id);
+ }
+}
+
+//! --column-names command
+class ColumnNamesCommand extends Command {
+ public void process(
+ SQLTester st, TestScript ts, String[] argv
+ ) throws Exception{
+ argcCheck(ts,argv,1);
+ st.outputColumnNames( Integer.parseInt(argv[1])!=0 );
+ }
+}
+
+//! --db command
+class DbCommand extends Command {
+ public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+ argcCheck(ts,argv,1);
+ t.setCurrentDb( Integer.parseInt(argv[1]) );
+ }
+}
+
+//! --glob command
+class GlobCommand extends Command {
+ private boolean negate = false;
+ public GlobCommand(){}
+ protected GlobCommand(boolean negate){ this.negate = negate; }
+
+ public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+ argcCheck(ts,argv,1,-1);
+ t.incrementTestCounter();
+ final String sql = t.takeInputBuffer();
+ int rc = t.execSql(null, true, ResultBufferMode.ESCAPED,
+ ResultRowMode.ONELINE, sql);
+ final String result = t.getResultText();
+ final String sArgs = Util.argvToString(argv);
+ //t2.verbose2(argv[0]," rc = ",rc," result buffer:\n", result,"\nargs:\n",sArgs);
+ final String glob = Util.argvToString(argv);
+ rc = SQLTester.strglob(glob, result);
+ if( (negate && 0==rc) || (!negate && 0!=rc) ){
+ ts.toss(argv[0], " mismatch: ", glob," vs input: ",result);
+ }
+ }
+}
+
+//! --json command
+class JsonCommand extends ResultCommand {
+ public JsonCommand(){ super(ResultBufferMode.ASIS); }
+}
+
+//! --json-block command
+class JsonBlockCommand extends TableResultCommand {
+ public JsonBlockCommand(){ super(true); }
+}
+
+//! --new command
+class NewDbCommand extends OpenDbCommand {
+ public NewDbCommand(){ super(true); }
+}
+
+//! Placeholder dummy/no-op commands
+class NoopCommand extends Command {
+ public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+ }
+}
+
+//! --notglob command
+class NotGlobCommand extends GlobCommand {
+ public NotGlobCommand(){
+ super(true);
+ }
+}
+
+//! --null command
+class NullCommand extends Command {
+ public void process(
+ SQLTester st, TestScript ts, String[] argv
+ ) throws Exception{
+ argcCheck(ts,argv,1);
+ st.setNullValue( argv[1] );
+ }
+}
+
+//! --open command
+class OpenDbCommand extends Command {
+ private boolean createIfNeeded = false;
+ public OpenDbCommand(){}
+ protected OpenDbCommand(boolean c){createIfNeeded = c;}
+ public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+ argcCheck(ts,argv,1);
+ t.openDb(argv[1], createIfNeeded);
+ }
+}
+
+//! --print command
+class PrintCommand extends Command {
+ public void process(
+ SQLTester st, TestScript ts, String[] argv
+ ) throws Exception{
+ st.out(ts.getOutputPrefix(),": ");
+ if( 1==argv.length ){
+ st.out( st.getInputText() );
+ }else{
+ st.outln( Util.argvToString(argv) );
+ }
+ }
+}
+
+//! --result command
+class ResultCommand extends Command {
+ private final ResultBufferMode bufferMode;
+ protected ResultCommand(ResultBufferMode bm){ bufferMode = bm; }
+ public ResultCommand(){ this(ResultBufferMode.ESCAPED); }
+ public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+ argcCheck(ts,argv,0,-1);
+ t.incrementTestCounter();
+ final String sql = t.takeInputBuffer();
+ //ts.verbose2(argv[0]," SQL =\n",sql);
+ int rc = t.execSql(null, false, bufferMode, ResultRowMode.ONELINE, sql);
+ final String result = t.getResultText().trim();
+ final String sArgs = argv.length>1 ? Util.argvToString(argv) : "";
+ if( !result.equals(sArgs) ){
+ t.outln(argv[0]," FAILED comparison. Result buffer:\n",
+ result,"\nExpected result:\n",sArgs);
+ ts.toss(argv[0]+" comparison failed.");
+ }
+ }
+}
+
+//! --run command
+class RunCommand extends Command {
+ public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+ argcCheck(ts,argv,0,1);
+ final sqlite3 db = (1==argv.length)
+ ? t.getCurrentDb() : t.getDbById( Integer.parseInt(argv[1]) );
+ final String sql = t.takeInputBuffer();
+ int rc = t.execSql(db, false, ResultBufferMode.NONE,
+ ResultRowMode.ONELINE, sql);
+ if( 0!=rc && t.isVerbose() ){
+ String msg = sqlite3_errmsg(db);
+ ts.verbose1(argv[0]," non-fatal command error #",rc,": ",
+ msg,"\nfor SQL:\n",sql);
+ }
+ }
+}
+
+//! --tableresult command
+class TableResultCommand extends Command {
+ private final boolean jsonMode;
+ protected TableResultCommand(boolean jsonMode){ this.jsonMode = jsonMode; }
+ public TableResultCommand(){ this(false); }
+ public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+ argcCheck(ts,argv,0);
+ t.incrementTestCounter();
+ String body = ts.fetchCommandBody(t);
+ if( null==body ) ts.toss("Missing ",argv[0]," body.");
+ body = body.trim();
+ if( !body.endsWith("\n--end") ){
+ ts.toss(argv[0], " must be terminated with --end.");
+ }else{
+ int n = body.length();
+ body = body.substring(0, n-6);
+ }
+ final String[] globs = body.split("\\s*\\n\\s*");
+ if( globs.length < 1 ){
+ ts.toss(argv[0], " requires 1 or more ",
+ (jsonMode ? "json snippets" : "globs"),".");
+ }
+ final String sql = t.takeInputBuffer();
+ t.execSql(null, true,
+ jsonMode ? ResultBufferMode.ASIS : ResultBufferMode.ESCAPED,
+ ResultRowMode.NEWLINE, sql);
+ final String rbuf = t.getResultText();
+ final String[] res = rbuf.split("\n");
+ if( res.length != globs.length ){
+ ts.toss(argv[0], " failure: input has ", res.length,
+ " row(s) but expecting ",globs.length);
+ }
+ for(int i = 0; i < res.length; ++i){
+ final String glob = globs[i].replaceAll("\\s+"," ").trim();
+ //ts.verbose2(argv[0]," <<",glob,">> vs <<",res[i],">>");
+ if( jsonMode ){
+ if( !glob.equals(res[i]) ){
+ ts.toss(argv[0], " json <<",glob, ">> does not match: <<",
+ res[i],">>");
+ }
+ }else if( 0 != SQLTester.strglob(glob, res[i]) ){
+ ts.toss(argv[0], " glob <<",glob,">> does not match: <<",res[i],">>");
+ }
+ }
+ }
+}
+
+//! --testcase command
+class TestCaseCommand extends Command {
+ public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+ argcCheck(ts,argv,1);
+ ts.setTestCaseName(argv[1]);
+ t.clearResultBuffer();
+ t.clearInputBuffer();
+ }
+}
+
+//! --verbosity command
+class VerbosityCommand extends Command {
+ public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
+ argcCheck(ts,argv,1);
+ ts.setVerbosity( Integer.parseInt(argv[1]) );
+ }
+}
+
+class CommandDispatcher {
+
+ private static java.util.Map commandMap =
+ new java.util.HashMap<>();
+
+ /**
+ Returns a (cached) instance mapped to name, or null if no match
+ is found.
+ */
+ static Command getCommandByName(String name){
+ Command rv = commandMap.get(name);
+ if( null!=rv ) return rv;
+ switch(name){
+ case "close": rv = new CloseDbCommand(); break;
+ case "column-names": rv = new ColumnNamesCommand(); break;
+ case "db": rv = new DbCommand(); break;
+ case "glob": rv = new GlobCommand(); break;
+ case "json": rv = new JsonCommand(); break;
+ case "json-block": rv = new JsonBlockCommand(); break;
+ case "new": rv = new NewDbCommand(); break;
+ case "notglob": rv = new NotGlobCommand(); break;
+ case "null": rv = new NullCommand(); break;
+ case "oom": rv = new NoopCommand(); break;
+ case "open": rv = new OpenDbCommand(); break;
+ case "print": rv = new PrintCommand(); break;
+ case "result": rv = new ResultCommand(); break;
+ case "run": rv = new RunCommand(); break;
+ case "tableresult": rv = new TableResultCommand(); break;
+ case "testcase": rv = new TestCaseCommand(); break;
+ case "verbosity": rv = new VerbosityCommand(); break;
+ default: rv = null; break;
+ }
+ if( null!=rv ) commandMap.put(name, rv);
+ return rv;
+ }
+
+ /**
+ Treats argv[0] as a command name, looks it up with
+ getCommandByName(), and calls process() on that instance, passing
+ it arguments given to this function.
+ */
+ static void dispatch(SQLTester tester, TestScript ts, String[] argv) throws Exception{
+ final Command cmd = getCommandByName(argv[0]);
+ if(null == cmd){
+ throw new UnknownCommand(ts, argv[0]);
+ }
+ cmd.process(tester, ts, argv);
+ }
+}
+
+
+/**
+ This class represents a single test script. It handles (or
+ delegates) its the reading-in and parsing, but the details of
+ evaluation are delegated elsewhere.
+*/
+class TestScript {
+ //! input file
+ private String filename = null;
+ //! Name pulled from the SCRIPT_MODULE_NAME directive of the file
+ private String moduleName = null;
+ //! Current test case name.
+ private String testCaseName = null;
+ //! Content buffer state.
+ private final Cursor cur = new Cursor();
+ //! Utility for console output.
+ private final Outer outer = new Outer();
+
+ //! File content and parse state.
+ private static final class Cursor {
+ private final StringBuilder sb = new StringBuilder();
+ byte[] src = null;
+ //! Current position in this.src.
+ int pos = 0;
+ //! Current line number. Starts at 0 for internal reasons and will
+ // line up with 1-based reality once parsing starts.
+ int lineNo = 0 /* yes, zero */;
+ //! Putback value for this.pos.
+ int putbackPos = 0;
+ //! Putback line number
+ int putbackLineNo = 0;
+ //! Peeked-to pos, used by peekLine() and consumePeeked().
+ int peekedPos = 0;
+ //! Peeked-to line number.
+ int peekedLineNo = 0;
+
+ //! Restore parsing state to the start of the stream.
+ void rewind(){
+ sb.setLength(0);
+ pos = lineNo = putbackPos = putbackLineNo = peekedPos = peekedLineNo = 0
+ /* kinda missing memset() about now. */;
+ }
+ }
+
+ private byte[] readFile(String filename) throws Exception {
+ return java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(filename));
+ }
+
+ /**
+ Initializes the script with the content of the given file.
+ Throws if it cannot read the file.
+ */
+ public TestScript(String filename) throws Exception{
+ this.filename = filename;
+ setVerbosity(2);
+ cur.src = readFile(filename);
+ }
+
+ public String getFilename(){
+ return filename;
+ }
+
+ public String getModuleName(){
+ return moduleName;
+ }
+
+ /**
+ Verbosity level 0 produces no debug/verbose output. Level 1 produces
+ some and level 2 produces more.
+ */
+ public void setVerbosity(int level){
+ outer.setVerbosity(level);
+ }
+
+ public String getOutputPrefix(){
+ String rc = "["+(moduleName==null ? filename : moduleName)+"]";
+ if( null!=testCaseName ) rc += "["+testCaseName+"]";
+ return rc + " line "+ cur.lineNo;
+ }
+
+ static final String[] verboseLabel = {"🔈",/*"🔉",*/"🔊","📢"};
+ //! Output vals only if level<=current verbosity level.
+ private TestScript verboseN(int level, Object... vals){
+ final int verbosity = outer.getVerbosity();
+ if(verbosity>=level){
+ outer.out( verboseLabel[level-1], getOutputPrefix(), " ",level,": "
+ ).outln(vals);
+ }
+ return this;
+ }
+
+ TestScript verbose1(Object... vals){return verboseN(1,vals);}
+ TestScript verbose2(Object... vals){return verboseN(2,vals);}
+ TestScript verbose3(Object... vals){return verboseN(3,vals);}
+
+ private void reset(){
+ testCaseName = null;
+ cur.rewind();
+ }
+
+ void setTestCaseName(String n){ testCaseName = n; }
+
+ /**
+ Returns the next line from the buffer, minus the trailing EOL.
+
+ Returns null when all input is consumed. Throws if it reads
+ illegally-encoded input, e.g. (non-)characters in the range
+ 128-256.
+ */
+ String getLine(){
+ if( cur.pos==cur.src.length ){
+ return null /* EOF */;
+ }
+ cur.putbackPos = cur.pos;
+ cur.putbackLineNo = cur.lineNo;
+ cur.sb.setLength(0);
+ final boolean skipLeadingWs = false;
+ byte b = 0, prevB = 0;
+ int i = cur.pos;
+ if(skipLeadingWs) {
+ /* Skip any leading spaces, including newlines. This will eliminate
+ blank lines. */
+ for(; i < cur.src.length; ++i, prevB=b){
+ b = cur.src[i];
+ switch((int)b){
+ case 32/*space*/: case 9/*tab*/: case 13/*CR*/: continue;
+ case 10/*NL*/: ++cur.lineNo; continue;
+ default: break;
+ }
+ break;
+ }
+ if( i==cur.src.length ){
+ return null /* EOF */;
+ }
+ }
+ boolean doBreak = false;
+ final byte[] aChar = {0,0,0,0} /* multi-byte char buffer */;
+ int nChar = 0 /* number of bytes in the char */;
+ for(; i < cur.src.length && !doBreak; ++i){
+ b = cur.src[i];
+ switch( (int)b ){
+ case 13/*CR*/: continue;
+ case 10/*NL*/:
+ ++cur.lineNo;
+ if(cur.sb.length()>0) doBreak = true;
+ // Else it's an empty string
+ break;
+ default:
+ /* Multi-byte chars need to be gathered up and appended at
+ one time. Appending individual bytes to the StringBuffer
+ appends their integer value. */
+ nChar = 1;
+ switch( b & 0xF0 ){
+ case 0xC0: nChar = 2; break;
+ case 0xE0: nChar = 3; break;
+ case 0xF0: nChar = 4; break;
+ default:
+ if( b > 127 ) this.toss("Invalid character (#"+(int)b+").");
+ break;
+ }
+ if( 1==nChar ){
+ cur.sb.append((char)b);
+ }else{
+ for(int x = 0; x < nChar; ++x) aChar[x] = cur.src[i+x];
+ cur.sb.append(new String(Arrays.copyOf(aChar, nChar),
+ StandardCharsets.UTF_8));
+ i += nChar-1;
+ }
+ break;
+ }
+ }
+ cur.pos = i;
+ final String rv = cur.sb.toString();
+ if( i==cur.src.length && 0==rv.length() ){
+ return null /* EOF */;
+ }
+ return rv;
+ }/*getLine()*/
+
+ /**
+ Fetches the next line then resets the cursor to its pre-call
+ state. consumePeeked() can be used to consume this peeked line
+ without having to re-parse it.
+ */
+ String peekLine(){
+ final int oldPos = cur.pos;
+ final int oldPB = cur.putbackPos;
+ final int oldPBL = cur.putbackLineNo;
+ final int oldLine = cur.lineNo;
+ final String rc = getLine();
+ cur.peekedPos = cur.pos;
+ cur.peekedLineNo = cur.lineNo;
+ cur.pos = oldPos;
+ cur.lineNo = oldLine;
+ cur.putbackPos = oldPB;
+ cur.putbackLineNo = oldPBL;
+ return rc;
+ }
+
+ /**
+ Only valid after calling peekLine() and before calling getLine().
+ This places the cursor to the position it would have been at had
+ the peekLine() had been fetched with getLine().
+ */
+ void consumePeeked(){
+ cur.pos = cur.peekedPos;
+ cur.lineNo = cur.peekedLineNo;
+ }
+
+ /**
+ Restores the cursor to the position it had before the previous
+ call to getLine().
+ */
+ void putbackLine(){
+ cur.pos = cur.putbackPos;
+ cur.lineNo = cur.putbackLineNo;
+ }
+
+ private boolean checkRequiredProperties(SQLTester t, String[] props) throws SQLTesterException{
+ int nOk = 0;
+ for(String rp : props){
+ verbose1("REQUIRED_PROPERTIES: ",rp);
+ switch(rp){
+ case "RECURSIVE_TRIGGERS":
+ t.appendDbInitSql("pragma recursive_triggers=on;");
+ ++nOk;
+ break;
+ case "TEMPSTORE_FILE":
+ /* This _assumes_ that the lib is built with SQLITE_TEMP_STORE=1 or 2,
+ which we just happen to know is the case */
+ t.appendDbInitSql("pragma temp_store=1;");
+ ++nOk;
+ break;
+ case "TEMPSTORE_MEM":
+ /* This _assumes_ that the lib is built with SQLITE_TEMP_STORE=1 or 2,
+ which we just happen to know is the case */
+ t.appendDbInitSql("pragma temp_store=0;");
+ ++nOk;
+ break;
+ default:
+ break;
+ }
+ }
+ return props.length == nOk;
+ }
+
+ private static final Pattern patternRequiredProperties =
+ Pattern.compile(" REQUIRED_PROPERTIES:[ \\t]*(\\S.*)\\s*$");
+ private static final Pattern patternScriptModuleName =
+ Pattern.compile(" SCRIPT_MODULE_NAME:[ \\t]*(\\S+)\\s*$");
+ private static final Pattern patternMixedModuleName =
+ Pattern.compile(" ((MIXED_)?MODULE_NAME):[ \\t]*(\\S+)\\s*$");
+ private static final Pattern patternCommand =
+ Pattern.compile("^--(([a-z-]+)( .*)?)$");
+
+ /**
+ Looks for "directives." If a compatible one is found, it is
+ processed and this function returns. If an incompatible one is found,
+ a description of it is returned and processing of the test must
+ end immediately.
+ */
+ private void checkForDirective(
+ SQLTester tester, String line
+ ) throws IncompatibleDirective {
+ if(line.startsWith("#")){
+ throw new IncompatibleDirective(this, "C-preprocessor input: "+line);
+ }else if(line.startsWith("---")){
+ new IncompatibleDirective(this, "triple-dash: "+line);
+ }
+ Matcher m = patternScriptModuleName.matcher(line);
+ if( m.find() ){
+ moduleName = m.group(1);
+ return;
+ }
+ m = patternRequiredProperties.matcher(line);
+ if( m.find() ){
+ final String rp = m.group(1);
+ //if( ! checkRequiredProperties( tester, rp.split("\\s+") ) ){
+ throw new IncompatibleDirective(this, "REQUIRED_PROPERTIES: "+rp);
+ //}
+ }
+ m = patternMixedModuleName.matcher(line);
+ if( m.find() ){
+ throw new IncompatibleDirective(this, m.group(1)+": "+m.group(3));
+ }
+ if( line.indexOf("\n|")>=0 ){
+ throw new IncompatibleDirective(this, "newline-pipe combination.");
+ }
+ return;
+ }
+
+ boolean isCommandLine(String line, boolean checkForImpl){
+ final Matcher m = patternCommand.matcher(line);
+ boolean rc = m.find();
+ if( rc && checkForImpl ){
+ rc = null!=CommandDispatcher.getCommandByName(m.group(2));
+ }
+ return rc;
+ }
+
+ /**
+ If line looks like a command, returns an argv for that command
+ invocation, else returns null.
+ */
+ String[] getCommandArgv(String line){
+ final Matcher m = patternCommand.matcher(line);
+ return m.find() ? m.group(1).trim().split("\\s+") : null;
+ }
+
+ /**
+ Fetches lines until the next recognized command. Throws if
+ checkForDirective() does. Returns null if there is no input or
+ it's only whitespace. The returned string retains all whitespace.
+
+ Note that "subcommands", --command-like constructs in the body
+ which do not match a known command name are considered to be
+ content, not commands.
+ */
+ String fetchCommandBody(SQLTester tester){
+ final StringBuilder sb = new StringBuilder();
+ String line;
+ while( (null != (line = peekLine())) ){
+ checkForDirective(tester, line);
+ if( !isCommandLine(line, true) ){
+ sb.append(line).append("\n");
+ consumePeeked();
+ }else{
+ break;
+ }
+ }
+ line = sb.toString();
+ return line.trim().isEmpty() ? null : line;
+ }
+
+ private void processCommand(SQLTester t, String[] argv) throws Exception{
+ verbose1("running command: ",argv[0], " ", Util.argvToString(argv));
+ if(outer.getVerbosity()>1){
+ final String input = t.getInputText();
+ if( !input.isEmpty() ) verbose3("Input buffer = ",input);
+ }
+ CommandDispatcher.dispatch(t, this, argv);
+ }
+
+ void toss(Object... msg) throws TestScriptFailed {
+ StringBuilder sb = new StringBuilder();
+ for(Object s : msg) sb.append(s);
+ throw new TestScriptFailed(this, sb.toString());
+ }
+
+ /**
+ Runs this test script in the context of the given tester object.
+ */
+ public boolean run(SQLTester tester) throws Exception {
+ reset();
+ setVerbosity(tester.getVerbosity());
+ String line, directive;
+ String[] argv;
+ while( null != (line = getLine()) ){
+ verbose3("input line: ",line);
+ checkForDirective(tester, line);
+ argv = getCommandArgv(line);
+ if( null!=argv ){
+ processCommand(tester, argv);
+ continue;
+ }
+ tester.appendInput(line,true);
+ }
+ return true;
+ }
+}
diff --git a/ext/jni/src/org/sqlite/jni/tester/test-script-interpreter.md b/ext/jni/src/org/sqlite/jni/tester/test-script-interpreter.md
new file mode 100644
index 000000000..a51d64d10
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/tester/test-script-interpreter.md
@@ -0,0 +1,269 @@
+# Specifications For A Rudimentary SQLite Test Script Interpreter
+
+## Overview
+
+The purpose of the Test Script Interpreter is to read and interpret
+script files that contain SQL commands and desired results. The
+interpreter will check results and report an discrepencies found.
+
+The test script files are ASCII text files. The filename always ends with
+".test". Each script is evaluated independently; context does not carry
+forward from one script to the next. So, for example, the --null command
+run in one test script does not cause any changes in the behavior of
+subsequent test scripts. All open database connections are closed
+at the end of each test script. All database files created by a test
+script are deleted when the script finishes.
+
+## Parsing Rules:
+
+ 1. The test script is read line by line, where a line is a sequence of
+ characters that runs up to the next '\\n' (0x0a) character or until
+ the end of the file. There is never a need to read ahead past the
+ end of the current line.
+
+ 2. If any line contains the string " MODULE_NAME:" (with a space before
+ the initial "M") or "MIXED_MODULE_NAME:" then that test script is
+ incompatible with this spec. Processing of the test script should
+ end immediately. There is no need to read any more of the file.
+ In verbose mode, the interpreter might choose to emit an informational
+ messages saying that the test script was abandoned due to an
+ incompatible module type.
+
+ 3. If any line contains the string "SCRIPT_MODULE_NAME:" then the input
+ script is known to be of the correct type for this specification and
+ processing may continue. The "MODULE_NAME" checking in steps 2 and 3
+ may optionally be discontinued after sighting a "SCRIPT_MODULE_NAME".
+
+ 4. If any line contains "REQUIRED_PROPERTIES:" and that substring is followed
+ by any non-whitespace text, then the script is not compatible with this
+ spec. Processing should stop immediately. In verbose mode, the
+ interpreter might choose to emit an information message saying that the
+ test script was abandoned due to unsupported requirement properties.
+
+ 5. If any line begins with the "\|" (0x7c) character, that indicates that
+ the input script is not compatible with this specification. Processing
+ of the script should stop immediately. In verbose mode, the interpreter
+ might choose to emit an informational message indicating that the
+ test script was abandoned because it contained "a dbtotxt format database
+ specification".
+
+ 6. Any line that begins with "#" is a C-preprocessor line. The interpreter
+ described by this spec does not know how to deal with C-preprocessor lines.
+ Hence, processing should be abandoned. In verbose mode, the interpreter
+ might emit an informational message similar to
+ "script NAME abandoned due to C-preprocessor line: ..."
+
+ 7. If a line begins with exactly two minus signs followed by a
+ lowercase letter, that is a command. Process commands as described
+ below.
+
+ 8. All other lines should be accumulated into the "input buffer".
+ The various commands will have access to this input buffer.
+ Some commands will reset the buffer.
+
+## Initialization
+
+The initial state of the interpreter at the start of processing each script
+is as if the following command sequence had been run:
+
+> ~~~
+--close all
+--db 0
+--new test.db
+--null nil
+~~~
+
+In words, all database connections are closed except for connection 0 (the
+default) which is open on an empty database named "test.db". The string
+"nil" is displayed for NULL column values.
+
+The only context carried forward after the evaluation of one test script
+into the evaluation of the next test script is the count of the number of
+tests run and the number of failures seen.
+
+## Commands:
+
+Each command looks like an SQL comment. The command begins at the left
+margin (no leading space) and starts with exactly 2 minus signs ("-").
+The command name consists of lowercase letters and maybe a "-" or two.
+Some commands have arguments.
+The arguments are separated from the command name by one or more spaces.
+
+Commands have access to the input buffer and might reset the input buffer.
+The command can also optionally read (and consume) additional text from
+script that comes after the command.
+
+Unknown or unrecognized commands indicate that the script contains features
+that are not (yet) supported by this specification. Processing of the
+script should terminate immediately. When this happens and when the
+interpreter is in a "verbose" mode, the interpreter might choose to emit
+an informational message along the lines of "test script NAME abandoned
+due to unsupported command: --whatever".
+
+The initial implemention will only recognize a few commands. Other
+commands may be added later. The following is the initial set of
+commands:
+
+### The --testcase command
+
+Every test case starts with a --testcase command. The --testcase
+command resets both the "input buffer" and the "result buffer". The
+argument to the --testcase command is the name of the test case. That
+test case name is used for logging and debugging and when printing
+errors. The input buffer is set to the body of the test case.
+
+### The --result command
+
+The --result command tries to execute the text in the input buffer as SQL.
+For each row of result coming out of this SQL, the text of that result is
+appended to the "result buffer". If a result row contains multiple columns,
+the columns are processed from left to right. For each column, text is
+appended to the result buffer according to the following rules:
+
+ * If the result buffer already contains some text, append a space.
+ (In this way, all column values and all row values are separated from
+ each other by a single space.)
+
+ * If sqlite3_column_text() returns NULL, then append "nil" - or
+ some other text that is specified by the --null command - and skip
+ all subsequent rules.
+
+ * If sqlite3_column_text() is an empty string, append `{}` to the
+ result buffer and skip all subsequent rules.
+
+ * If sqlite3_column_text() does not contain any special
+ characters, append it to the result buffer without any
+ formatting and skip all subsequent rules. Special characters are:
+ 0x00 to 0x20 (inclusive), double-quote (0x22), backslash (0x5c),
+ curly braces (0x7b and 0x7d).
+
+ * If sqlite3_column_text() does not contains curly braces, then put
+ the text inside of `{...}` and append it and skip all subsequent rules.
+
+ * Append the text within double-quotes (`"..."`) and within the text
+ escape '"' and '\\' by prepending a single '\\' and escape any
+ control characters (characters less than 0x20) using octal notation:
+ '\\NNN'.
+
+If an error is encountered while running the SQL, then append the
+symbolic C-preprocessor name for the error
+code (ex: "SQLITE_CONSTRAINT") as if it were a column value. Then append
+the error message text as if it where a column value. Then stop processing.
+
+After the SQL text has been run, compare the content of the result buffer
+against the argument to the --result command and report a testing error if
+there are any differences.
+
+The --result command resets the input buffer, but it does not reset
+the result buffer. This distinction does not matter for the --result
+command itself, but it is important for related commands like --glob
+and --notglob. Sometimes test cases will contains a bunch of SQL
+followed by multiple --glob and/or --notglob statements. All of the
+globs should be evaluted agains the result buffer correct, but the SQL
+should only be run once. This is accomplished by resetting the input
+buffer but not the result buffer.
+
+### The --glob command
+
+The --glob command works just like --result except that the argument to
+--glob is interpreted as a TEST-GLOB pattern and the results are compared
+using that glob pattern rather than using strcmp(). Other than that,
+the two operate the same.
+
+The TEST-GLOB pattern is slightly different for a standard GLOB:
+
+ * The '*' character matches zero or more characters.
+
+ * The '?' character matches any single character
+
+ * The '[...]' character sequence machines a single character
+ in between the brackets.
+
+ * The '#' character matches one or more digits (This is the main
+ difference between standard unix-glob and TEST-GLOB. unix-glob
+ does not have this feature. It was added to because it comes
+ up a lot during SQLite testing.)
+
+### The --notglob command
+
+The --notglob command works just like --glob except that it reports an
+error if the GLOB does match, rather than if the GLOB does not matches.
+
+### The --oom command
+
+This command is to be used for out-of-memory testing. It means that
+OOM errors should be simulated to ensure that SQLite is able to deal with
+them. This command can be silently ignored for now. We might add support
+for this later.
+
+### The --tableresult command
+
+The --tableresult command works like --glob except that the GLOB pattern
+to be matched is taken from subsequent lines of the input script up to
+the next --end. Every span of one or more whitespace characters in this
+pattern text is collapsed into a single space (0x20).
+Leading and trailing whitespace are removed from the pattern.
+The --end that ends the GLOB pattern is not part of the GLOB pattern, but
+the --end is consumed from the script input.
+
+### The --new and --open commands
+
+The --new and --open commands cause a database file to be opened.
+The name of the file is the argument to the command. The --new command
+opens an initially empty database (it deletes the file before opening it)
+whereas the --open command opens an existing database if it already
+exists.
+
+### The --db command
+
+The script interpreter can have up to 7 different SQLite database
+connections open at a time. The --db command is used to switch between
+them. The argument to --db is an integer between 0 and 6 that selects
+which database connection to use moving forward.
+
+### The --close command
+
+The --close command causes an existing database connection to close.
+This command is a no-op if the database connection is not currently
+open. There can be up to 7 different database connections, numbered 0
+through 6. The number of the database connection to close is an
+argument to the --close command, which will fail if an out-of-range
+value is provided. Or if the argument to --close is "all" then all
+open database connections are closed. If passed no argument, the
+currently-active database is assumed.
+
+### The --null command
+
+The NULL command changes the text that is used to represent SQL NULL
+values in the result buffer.
+
+### The --run command
+
+The --run command executes text in the input buffer as if it where SQL.
+However, nothing is added to the result buffer. Any output from the SQL
+is silently ignored. Errors in the SQL are silently ignored.
+
+The --run command normally executes the SQL in the current database
+connection. However, if --run has an argument that is an integer between
+0 and 6 then the SQL is run in the alternative database connection specified
+by that argument.
+
+### The --json and --json-block commands
+
+The --json and --json-block commands work like --result and --tableresult,
+respectively. The difference is that column values are appended to the
+result buffer literally, without ever enclosing the values in `{...}` or
+`"..."` and without escaping any characters in the column value and comparison
+is always an exact strcmp() not a GLOB.
+
+### The --print command
+
+The --print command emits both its arguments and its body (if any) to
+stdout, indenting each line of output.
+
+### The --column-names command
+
+The --column-names command requires 0 or 1 as an argument, to disable
+resp. enable it, and modifies SQL execution to include column names
+in output. When this option is on, each column value emitted gets
+prefixed by its column name, with a single space between them.
diff --git a/ext/jni/src/tests/000-000-sanity.test b/ext/jni/src/tests/000-000-sanity.test
new file mode 100644
index 000000000..2bfacb1ce
--- /dev/null
+++ b/ext/jni/src/tests/000-000-sanity.test
@@ -0,0 +1,52 @@
+/*
+** This is a comment. There are many like it but this one is mine.
+**
+** SCRIPT_MODULE_NAME: sanity-check
+** xMIXED_MODULE_NAME: mixed-module
+** xMODULE_NAME: module-name
+** xREQUIRED_PROPERTIES: small fast reliable
+** xREQUIRED_PROPERTIES: RECURSIVE_TRIGGERS
+** xREQUIRED_PROPERTIES: TEMPSTORE_MEM TEMPSTORE_FILE
+**
+*/
+--print starting up 😃
+--close all
+--oom
+--db 0
+--new my.db
+--null zilch
+--testcase 1.0
+SELECT 1, null;
+--result 1 zilch
+--glob *zil*
+--notglob *ZIL*
+SELECT 1, 2;
+intentional error;
+--run
+--testcase json-1
+SELECT json_array(1,2,3)
+--json [1,2,3]
+--testcase tableresult-1
+ select 1, 'a';
+ select 2, 'b';
+--tableresult
+ # [a-z]
+ 2 b
+--end
+--testcase json-block-1
+ select json_array(1,2,3);
+ select json_object('a',1,'b',2);
+--json-block
+ [1,2,3]
+ {"a":1,"b":2}
+--end
+--testcase col-names-on
+--column-names 1
+ select 1 as 'a', 2 as 'b';
+--result a 1 b 2
+--testcase col-names-off
+--column-names 0
+ select 1 as 'a', 2 as 'b';
+--result 1 2
+--close
+--print reached the end 😃
diff --git a/ext/jni/src/tests/000-001-ignored.test b/ext/jni/src/tests/000-001-ignored.test
new file mode 100644
index 000000000..5af852e19
--- /dev/null
+++ b/ext/jni/src/tests/000-001-ignored.test
@@ -0,0 +1,9 @@
+/*
+** This script must be marked as ignored because it contains
+** content which triggers that condition.
+**
+** SCRIPT_MODULE_NAME: ignored
+**
+*/
+
+|
diff --git a/ext/misc/decimal.c b/ext/misc/decimal.c
index 4495cae46..9365ae68b 100644
--- a/ext/misc/decimal.c
+++ b/ext/misc/decimal.c
@@ -58,41 +58,24 @@ static void decimal_free(Decimal *p){
}
/*
-** Allocate a new Decimal object. Initialize it to the number given
-** by the input string.
+** Allocate a new Decimal object initialized to the text in zIn[].
+** Return NULL if any kind of error occurs.
*/
-static Decimal *decimal_new(
- sqlite3_context *pCtx,
- sqlite3_value *pIn,
- int nAlt,
- const unsigned char *zAlt
-){
- Decimal *p;
- int n, i;
- const unsigned char *zIn;
+static Decimal *decimalNewFromText(const char *zIn, int n){
+ Decimal *p = 0;
+ int i;
int iExp = 0;
+
p = sqlite3_malloc( sizeof(*p) );
- if( p==0 ) goto new_no_mem;
+ if( p==0 ) goto new_from_text_failed;
p->sign = 0;
p->oom = 0;
p->isInit = 1;
p->isNull = 0;
p->nDigit = 0;
p->nFrac = 0;
- if( zAlt ){
- n = nAlt,
- zIn = zAlt;
- }else{
- if( sqlite3_value_type(pIn)==SQLITE_NULL ){
- p->a = 0;
- p->isNull = 1;
- return p;
- }
- n = sqlite3_value_bytes(pIn);
- zIn = sqlite3_value_text(pIn);
- }
p->a = sqlite3_malloc64( n+1 );
- if( p->a==0 ) goto new_no_mem;
+ if( p->a==0 ) goto new_from_text_failed;
for(i=0; isspace(zIn[i]); i++){}
if( zIn[i]=='-' ){
p->sign = 1;
@@ -143,7 +126,7 @@ static Decimal *decimal_new(
}
if( iExp>0 ){
p->a = sqlite3_realloc64(p->a, p->nDigit + iExp + 1 );
- if( p->a==0 ) goto new_no_mem;
+ if( p->a==0 ) goto new_from_text_failed;
memset(p->a+p->nDigit, 0, iExp);
p->nDigit += iExp;
}
@@ -162,7 +145,7 @@ static Decimal *decimal_new(
}
if( iExp>0 ){
p->a = sqlite3_realloc64(p->a, p->nDigit + iExp + 1 );
- if( p->a==0 ) goto new_no_mem;
+ if( p->a==0 ) goto new_from_text_failed;
memmove(p->a+iExp, p->a, p->nDigit);
memset(p->a, 0, iExp);
p->nDigit += iExp;
@@ -171,7 +154,76 @@ static Decimal *decimal_new(
}
return p;
-new_no_mem:
+new_from_text_failed:
+ if( p ){
+ if( p->a ) sqlite3_free(p->a);
+ sqlite3_free(p);
+ }
+ return 0;
+}
+
+/* Forward reference */
+static Decimal *decimalFromDouble(double);
+
+/*
+** Allocate a new Decimal object from an sqlite3_value. Return a pointer
+** to the new object, or NULL if there is an error. If the pCtx argument
+** is not NULL, then errors are reported on it as well.
+**
+** If the pIn argument is SQLITE_TEXT or SQLITE_INTEGER, it is converted
+** directly into a Decimal. For SQLITE_FLOAT or for SQLITE_BLOB of length
+** 8 bytes, the resulting double value is expanded into its decimal equivalent.
+** If pIn is NULL or if it is a BLOB that is not exactly 8 bytes in length,
+** then NULL is returned.
+*/
+static Decimal *decimal_new(
+ sqlite3_context *pCtx, /* Report error here, if not null */
+ sqlite3_value *pIn, /* Construct the decimal object from this */
+ int bTextOnly /* Always interpret pIn as text if true */
+){
+ Decimal *p = 0;
+ int eType = sqlite3_value_type(pIn);
+ if( bTextOnly && (eType==SQLITE_FLOAT || eType==SQLITE_BLOB) ){
+ eType = SQLITE_TEXT;
+ }
+ switch( eType ){
+ case SQLITE_TEXT:
+ case SQLITE_INTEGER: {
+ const char *zIn = (const char*)sqlite3_value_text(pIn);
+ int n = sqlite3_value_bytes(pIn);
+ p = decimalNewFromText(zIn, n);
+ if( p==0 ) goto new_failed;
+ break;
+ }
+
+ case SQLITE_FLOAT: {
+ p = decimalFromDouble(sqlite3_value_double(pIn));
+ break;
+ }
+
+ case SQLITE_BLOB: {
+ const unsigned char *x;
+ unsigned int i;
+ sqlite3_uint64 v = 0;
+ double r;
+
+ if( sqlite3_value_bytes(pIn)!=sizeof(r) ) break;
+ x = sqlite3_value_blob(pIn);
+ for(i=0; ioom ){
+ sqlite3_result_error_nomem(pCtx);
+ return;
+ }
+ if( p->isNull ){
+ sqlite3_result_null(pCtx);
+ return;
+ }
+ for(nDigit=p->nDigit; nDigit>0 && p->a[nDigit-1]==0; nDigit--){}
+ for(nZero=0; nZeroa[nZero]==0; nZero++){}
+ nFrac = p->nFrac + (nDigit - p->nDigit);
+ nDigit -= nZero;
+ z = sqlite3_malloc( nDigit+20 );
+ if( z==0 ){
+ sqlite3_result_error_nomem(pCtx);
+ return;
+ }
+ if( nDigit==0 ){
+ zero = 0;
+ a = &zero;
+ nDigit = 1;
+ nFrac = 0;
+ }else{
+ a = &p->a[nZero];
+ }
+ if( p->sign && nDigit>0 ){
+ z[0] = '-';
+ }else{
+ z[0] = '+';
+ }
+ z[1] = a[0]+'0';
+ z[2] = '.';
+ if( nDigit==1 ){
+ z[3] = '0';
+ i = 4;
+ }else{
+ for(i=1; iisNull ) goto cmp_done;
- pB = decimal_new(context, argv[1], 0, 0);
+ pB = decimal_new(context, argv[1], 1);
if( pB==0 || pB->isNull ) goto cmp_done;
rc = decimal_cmp(pA, pB);
if( rc<0 ) rc = -1;
@@ -338,7 +435,7 @@ static void decimal_expand(Decimal *p, int nDigit, int nFrac){
}
/*
-** Add the value pB into pA.
+** Add the value pB into pA. A := A + B.
**
** Both pA and pB might become denormalized by this routine.
*/
@@ -407,6 +504,172 @@ static void decimal_add(Decimal *pA, Decimal *pB){
}
}
+/*
+** Multiply A by B. A := A * B
+**
+** All significant digits after the decimal point are retained.
+** Trailing zeros after the decimal point are omitted as long as
+** the number of digits after the decimal point is no less than
+** either the number of digits in either input.
+*/
+static void decimalMul(Decimal *pA, Decimal *pB){
+ signed char *acc = 0;
+ int i, j, k;
+ int minFrac;
+
+ if( pA==0 || pA->oom || pA->isNull
+ || pB==0 || pB->oom || pB->isNull
+ ){
+ goto mul_end;
+ }
+ acc = sqlite3_malloc64( pA->nDigit + pB->nDigit + 2 );
+ if( acc==0 ){
+ pA->oom = 1;
+ goto mul_end;
+ }
+ memset(acc, 0, pA->nDigit + pB->nDigit + 2);
+ minFrac = pA->nFrac;
+ if( pB->nFracnFrac;
+ for(i=pA->nDigit-1; i>=0; i--){
+ signed char f = pA->a[i];
+ int carry = 0, x;
+ for(j=pB->nDigit-1, k=i+j+3; j>=0; j--, k--){
+ x = acc[k] + f*pB->a[j] + carry;
+ acc[k] = x%10;
+ carry = x/10;
+ }
+ x = acc[k] + carry;
+ acc[k] = x%10;
+ acc[k-1] += x/10;
+ }
+ sqlite3_free(pA->a);
+ pA->a = acc;
+ acc = 0;
+ pA->nDigit += pB->nDigit + 2;
+ pA->nFrac += pB->nFrac;
+ pA->sign ^= pB->sign;
+ while( pA->nFrac>minFrac && pA->a[pA->nDigit-1]==0 ){
+ pA->nFrac--;
+ pA->nDigit--;
+ }
+
+mul_end:
+ sqlite3_free(acc);
+}
+
+/*
+** Create a new Decimal object that contains an integer power of 2.
+*/
+static Decimal *decimalPow2(int N){
+ Decimal *pA = 0; /* The result to be returned */
+ Decimal *pX = 0; /* Multiplier */
+ if( N<-20000 || N>20000 ) goto pow2_fault;
+ pA = decimalNewFromText("1.0", 3);
+ if( pA==0 || pA->oom ) goto pow2_fault;
+ if( N==0 ) return pA;
+ if( N>0 ){
+ pX = decimalNewFromText("2.0", 3);
+ }else{
+ N = -N;
+ pX = decimalNewFromText("0.5", 3);
+ }
+ if( pX==0 || pX->oom ) goto pow2_fault;
+ while( 1 /* Exit by break */ ){
+ if( N & 1 ){
+ decimalMul(pA, pX);
+ if( pA->oom ) goto pow2_fault;
+ }
+ N >>= 1;
+ if( N==0 ) break;
+ decimalMul(pX, pX);
+ }
+ decimal_free(pX);
+ return pA;
+
+pow2_fault:
+ decimal_free(pA);
+ decimal_free(pX);
+ return 0;
+}
+
+/*
+** Use an IEEE754 binary64 ("double") to generate a new Decimal object.
+*/
+static Decimal *decimalFromDouble(double r){
+ sqlite3_int64 m, a;
+ int e;
+ int isNeg;
+ Decimal *pA;
+ Decimal *pX;
+ char zNum[100];
+ if( r<0.0 ){
+ isNeg = 1;
+ r = -r;
+ }else{
+ isNeg = 0;
+ }
+ memcpy(&a,&r,sizeof(a));
+ if( a==0 ){
+ e = 0;
+ m = 0;
+ }else{
+ e = a>>52;
+ m = a & ((((sqlite3_int64)1)<<52)-1);
+ if( e==0 ){
+ m <<= 1;
+ }else{
+ m |= ((sqlite3_int64)1)<<52;
+ }
+ while( e<1075 && m>0 && (m&1)==0 ){
+ m >>= 1;
+ e++;
+ }
+ if( isNeg ) m = -m;
+ e = e - 1075;
+ if( e>971 ){
+ return 0; /* A NaN or an Infinity */
+ }
+ }
+
+ /* At this point m is the integer significand and e is the exponent */
+ sqlite3_snprintf(sizeof(zNum), zNum, "%lld", m);
+ pA = decimalNewFromText(zNum, (int)strlen(zNum));
+ pX = decimalPow2(e);
+ decimalMul(pA, pX);
+ decimal_free(pX);
+ return pA;
+}
+
+/*
+** SQL Function: decimal(X)
+** OR: decimal_exp(X)
+**
+** Convert input X into decimal and then back into text.
+**
+** If X is originally a float, then a full decimal expansion of that floating
+** point value is done. Or if X is an 8-byte blob, it is interpreted
+** as a float and similarly expanded.
+**
+** The decimal_exp(X) function returns the result in exponential notation.
+** decimal(X) returns a complete decimal, without the e+NNN at the end.
+*/
+static void decimalFunc(
+ sqlite3_context *context,
+ int argc,
+ sqlite3_value **argv
+){
+ Decimal *p = decimal_new(context, argv[0], 0);
+ UNUSED_PARAMETER(argc);
+ if( p ){
+ if( sqlite3_user_data(context)!=0 ){
+ decimal_result_sci(context, p);
+ }else{
+ decimal_result(context, p);
+ }
+ decimal_free(p);
+ }
+}
+
/*
** Compare text in decimal order.
*/
@@ -417,8 +680,8 @@ static int decimalCollFunc(
){
const unsigned char *zA = (const unsigned char*)pKey1;
const unsigned char *zB = (const unsigned char*)pKey2;
- Decimal *pA = decimal_new(0, 0, nKey1, zA);
- Decimal *pB = decimal_new(0, 0, nKey2, zB);
+ Decimal *pA = decimalNewFromText((const char*)zA, nKey1);
+ Decimal *pB = decimalNewFromText((const char*)zB, nKey2);
int rc;
UNUSED_PARAMETER(notUsed);
if( pA==0 || pB==0 ){
@@ -443,8 +706,8 @@ static void decimalAddFunc(
int argc,
sqlite3_value **argv
){
- Decimal *pA = decimal_new(context, argv[0], 0, 0);
- Decimal *pB = decimal_new(context, argv[1], 0, 0);
+ Decimal *pA = decimal_new(context, argv[0], 1);
+ Decimal *pB = decimal_new(context, argv[1], 1);
UNUSED_PARAMETER(argc);
decimal_add(pA, pB);
decimal_result(context, pA);
@@ -456,8 +719,8 @@ static void decimalSubFunc(
int argc,
sqlite3_value **argv
){
- Decimal *pA = decimal_new(context, argv[0], 0, 0);
- Decimal *pB = decimal_new(context, argv[1], 0, 0);
+ Decimal *pA = decimal_new(context, argv[0], 1);
+ Decimal *pB = decimal_new(context, argv[1], 1);
UNUSED_PARAMETER(argc);
if( pB ){
pB->sign = !pB->sign;
@@ -495,7 +758,7 @@ static void decimalSumStep(
p->nFrac = 0;
}
if( sqlite3_value_type(argv[0])==SQLITE_NULL ) return;
- pArg = decimal_new(context, argv[0], 0, 0);
+ pArg = decimal_new(context, argv[0], 1);
decimal_add(p, pArg);
decimal_free(pArg);
}
@@ -510,7 +773,7 @@ static void decimalSumInverse(
p = sqlite3_aggregate_context(context, sizeof(*p));
if( p==0 ) return;
if( sqlite3_value_type(argv[0])==SQLITE_NULL ) return;
- pArg = decimal_new(context, argv[0], 0, 0);
+ pArg = decimal_new(context, argv[0], 1);
if( pArg ) pArg->sign = !pArg->sign;
decimal_add(p, pArg);
decimal_free(pArg);
@@ -531,66 +794,49 @@ static void decimalSumFinalize(sqlite3_context *context){
** SQL Function: decimal_mul(X, Y)
**
** Return the product of X and Y.
-**
-** All significant digits after the decimal point are retained.
-** Trailing zeros after the decimal point are omitted as long as
-** the number of digits after the decimal point is no less than
-** either the number of digits in either input.
*/
static void decimalMulFunc(
sqlite3_context *context,
int argc,
sqlite3_value **argv
){
- Decimal *pA = decimal_new(context, argv[0], 0, 0);
- Decimal *pB = decimal_new(context, argv[1], 0, 0);
- signed char *acc = 0;
- int i, j, k;
- int minFrac;
+ Decimal *pA = decimal_new(context, argv[0], 1);
+ Decimal *pB = decimal_new(context, argv[1], 1);
UNUSED_PARAMETER(argc);
if( pA==0 || pA->oom || pA->isNull
|| pB==0 || pB->oom || pB->isNull
){
goto mul_end;
}
- acc = sqlite3_malloc64( pA->nDigit + pB->nDigit + 2 );
- if( acc==0 ){
- sqlite3_result_error_nomem(context);
+ decimalMul(pA, pB);
+ if( pA->oom ){
goto mul_end;
}
- memset(acc, 0, pA->nDigit + pB->nDigit + 2);
- minFrac = pA->nFrac;
- if( pB->nFracnFrac;
- for(i=pA->nDigit-1; i>=0; i--){
- signed char f = pA->a[i];
- int carry = 0, x;
- for(j=pB->nDigit-1, k=i+j+3; j>=0; j--, k--){
- x = acc[k] + f*pB->a[j] + carry;
- acc[k] = x%10;
- carry = x/10;
- }
- x = acc[k] + carry;
- acc[k] = x%10;
- acc[k-1] += x/10;
- }
- sqlite3_free(pA->a);
- pA->a = acc;
- acc = 0;
- pA->nDigit += pB->nDigit + 2;
- pA->nFrac += pB->nFrac;
- pA->sign ^= pB->sign;
- while( pA->nFrac>minFrac && pA->a[pA->nDigit-1]==0 ){
- pA->nFrac--;
- pA->nDigit--;
- }
decimal_result(context, pA);
mul_end:
- sqlite3_free(acc);
decimal_free(pA);
decimal_free(pB);
}
+/*
+** SQL Function: decimal_pow2(N)
+**
+** Return the N-th power of 2. N must be an integer.
+*/
+static void decimalPow2Func(
+ sqlite3_context *context,
+ int argc,
+ sqlite3_value **argv
+){
+ UNUSED_PARAMETER(argc);
+ if( sqlite3_value_type(argv[0])==SQLITE_INTEGER ){
+ Decimal *pA = decimalPow2(sqlite3_value_int(argv[0]));
+ decimal_result_sci(context, pA);
+ decimal_free(pA);
+ }
+}
+
#ifdef _WIN32
__declspec(dllexport)
#endif
@@ -603,13 +849,16 @@ int sqlite3_decimal_init(
static const struct {
const char *zFuncName;
int nArg;
+ int iArg;
void (*xFunc)(sqlite3_context*,int,sqlite3_value**);
} aFunc[] = {
- { "decimal", 1, decimalFunc },
- { "decimal_cmp", 2, decimalCmpFunc },
- { "decimal_add", 2, decimalAddFunc },
- { "decimal_sub", 2, decimalSubFunc },
- { "decimal_mul", 2, decimalMulFunc },
+ { "decimal", 1, 0, decimalFunc },
+ { "decimal_exp", 1, 1, decimalFunc },
+ { "decimal_cmp", 2, 0, decimalCmpFunc },
+ { "decimal_add", 2, 0, decimalAddFunc },
+ { "decimal_sub", 2, 0, decimalSubFunc },
+ { "decimal_mul", 2, 0, decimalMulFunc },
+ { "decimal_pow2", 1, 0, decimalPow2Func },
};
unsigned int i;
(void)pzErrMsg; /* Unused parameter */
@@ -619,7 +868,7 @@ int sqlite3_decimal_init(
for(i=0; i<(int)(sizeof(aFunc)/sizeof(aFunc[0])) && rc==SQLITE_OK; i++){
rc = sqlite3_create_function(db, aFunc[i].zFuncName, aFunc[i].nArg,
SQLITE_UTF8|SQLITE_INNOCUOUS|SQLITE_DETERMINISTIC,
- 0, aFunc[i].xFunc, 0, 0);
+ aFunc[i].iArg ? db : 0, aFunc[i].xFunc, 0, 0);
}
if( rc==SQLITE_OK ){
rc = sqlite3_create_window_function(db, "decimal_sum", 1,
diff --git a/ext/misc/ieee754.c b/ext/misc/ieee754.c
index ff5d2d333..99489fe9c 100644
--- a/ext/misc/ieee754.c
+++ b/ext/misc/ieee754.c
@@ -256,6 +256,37 @@ static void ieee754func_to_blob(
}
}
+/*
+** SQL Function: ieee754_inc(r,N)
+**
+** Move the floating point value r by N quantums and return the new
+** values.
+**
+** Behind the scenes: this routine merely casts r into a 64-bit unsigned
+** integer, adds N, then casts the value back into float.
+**
+** Example: To find the smallest positive number:
+**
+** SELECT ieee754_inc(0.0,+1);
+*/
+static void ieee754inc(
+ sqlite3_context *context,
+ int argc,
+ sqlite3_value **argv
+){
+ double r;
+ sqlite3_int64 N;
+ sqlite3_uint64 m1, m2;
+ double r2;
+ UNUSED_PARAMETER(argc);
+ r = sqlite3_value_double(argv[0]);
+ N = sqlite3_value_int64(argv[1]);
+ memcpy(&m1, &r, 8);
+ m2 = m1 + N;
+ memcpy(&r2, &m2, 8);
+ sqlite3_result_double(context, r2);
+}
+
#ifdef _WIN32
__declspec(dllexport)
@@ -277,7 +308,7 @@ int sqlite3_ieee_init(
{ "ieee754_exponent", 1, 2, ieee754func },
{ "ieee754_to_blob", 1, 0, ieee754func_to_blob },
{ "ieee754_from_blob", 1, 0, ieee754func_from_blob },
-
+ { "ieee754_inc", 2, 0, ieee754inc },
};
unsigned int i;
int rc = SQLITE_OK;
diff --git a/ext/misc/pcachetrace.c b/ext/misc/pcachetrace.c
new file mode 100644
index 000000000..3757d8d4d
--- /dev/null
+++ b/ext/misc/pcachetrace.c
@@ -0,0 +1,179 @@
+/*
+** 2023-06-21
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+**
+** This file implements an extension that uses the SQLITE_CONFIG_PCACHE2
+** mechanism to add a tracing layer on top of pluggable page cache of
+** SQLite. If this extension is registered prior to sqlite3_initialize(),
+** it will cause all page cache activities to be logged on standard output,
+** or to some other FILE specified by the initializer.
+**
+** This file needs to be compiled into the application that uses it.
+**
+** This extension is used to implement the --pcachetrace option of the
+** command-line shell.
+*/
+#include
+#include
+#include
+
+/* The original page cache routines */
+static sqlite3_pcache_methods2 pcacheBase;
+static FILE *pcachetraceOut;
+
+/* Methods that trace pcache activity */
+static int pcachetraceInit(void *pArg){
+ int nRes;
+ if( pcachetraceOut ){
+ fprintf(pcachetraceOut, "PCACHETRACE: xInit(%p)\n", pArg);
+ }
+ nRes = pcacheBase.xInit(pArg);
+ if( pcachetraceOut ){
+ fprintf(pcachetraceOut, "PCACHETRACE: xInit(%p) -> %d\n", pArg, nRes);
+ }
+ return nRes;
+}
+static void pcachetraceShutdown(void *pArg){
+ if( pcachetraceOut ){
+ fprintf(pcachetraceOut, "PCACHETRACE: xShutdown(%p)\n", pArg);
+ }
+ pcacheBase.xShutdown(pArg);
+}
+static sqlite3_pcache *pcachetraceCreate(int szPage, int szExtra, int bPurge){
+ sqlite3_pcache *pRes;
+ if( pcachetraceOut ){
+ fprintf(pcachetraceOut, "PCACHETRACE: xCreate(%d,%d,%d)\n",
+ szPage, szExtra, bPurge);
+ }
+ pRes = pcacheBase.xCreate(szPage, szExtra, bPurge);
+ if( pcachetraceOut ){
+ fprintf(pcachetraceOut, "PCACHETRACE: xCreate(%d,%d,%d) -> %p\n",
+ szPage, szExtra, bPurge, pRes);
+ }
+ return pRes;
+}
+static void pcachetraceCachesize(sqlite3_pcache *p, int nCachesize){
+ if( pcachetraceOut ){
+ fprintf(pcachetraceOut, "PCACHETRACE: xCachesize(%p, %d)\n", p, nCachesize);
+ }
+ pcacheBase.xCachesize(p, nCachesize);
+}
+static int pcachetracePagecount(sqlite3_pcache *p){
+ int nRes;
+ if( pcachetraceOut ){
+ fprintf(pcachetraceOut, "PCACHETRACE: xPagecount(%p)\n", p);
+ }
+ nRes = pcacheBase.xPagecount(p);
+ if( pcachetraceOut ){
+ fprintf(pcachetraceOut, "PCACHETRACE: xPagecount(%p) -> %d\n", p, nRes);
+ }
+ return nRes;
+}
+static sqlite3_pcache_page *pcachetraceFetch(
+ sqlite3_pcache *p,
+ unsigned key,
+ int crFg
+){
+ sqlite3_pcache_page *pRes;
+ if( pcachetraceOut ){
+ fprintf(pcachetraceOut, "PCACHETRACE: xFetch(%p,%u,%d)\n", p, key, crFg);
+ }
+ pRes = pcacheBase.xFetch(p, key, crFg);
+ if( pcachetraceOut ){
+ fprintf(pcachetraceOut, "PCACHETRACE: xFetch(%p,%u,%d) -> %p\n",
+ p, key, crFg, pRes);
+ }
+ return pRes;
+}
+static void pcachetraceUnpin(
+ sqlite3_pcache *p,
+ sqlite3_pcache_page *pPg,
+ int bDiscard
+){
+ if( pcachetraceOut ){
+ fprintf(pcachetraceOut, "PCACHETRACE: xUnpin(%p, %p, %d)\n",
+ p, pPg, bDiscard);
+ }
+ pcacheBase.xUnpin(p, pPg, bDiscard);
+}
+static void pcachetraceRekey(
+ sqlite3_pcache *p,
+ sqlite3_pcache_page *pPg,
+ unsigned oldKey,
+ unsigned newKey
+){
+ if( pcachetraceOut ){
+ fprintf(pcachetraceOut, "PCACHETRACE: xRekey(%p, %p, %u, %u)\n",
+ p, pPg, oldKey, newKey);
+ }
+ pcacheBase.xRekey(p, pPg, oldKey, newKey);
+}
+static void pcachetraceTruncate(sqlite3_pcache *p, unsigned n){
+ if( pcachetraceOut ){
+ fprintf(pcachetraceOut, "PCACHETRACE: xTruncate(%p, %u)\n", p, n);
+ }
+ pcacheBase.xTruncate(p, n);
+}
+static void pcachetraceDestroy(sqlite3_pcache *p){
+ if( pcachetraceOut ){
+ fprintf(pcachetraceOut, "PCACHETRACE: xDestroy(%p)\n", p);
+ }
+ pcacheBase.xDestroy(p);
+}
+static void pcachetraceShrink(sqlite3_pcache *p){
+ if( pcachetraceOut ){
+ fprintf(pcachetraceOut, "PCACHETRACE: xShrink(%p)\n", p);
+ }
+ pcacheBase.xShrink(p);
+}
+
+/* The substitute pcache methods */
+static sqlite3_pcache_methods2 ersaztPcacheMethods = {
+ 0,
+ 0,
+ pcachetraceInit,
+ pcachetraceShutdown,
+ pcachetraceCreate,
+ pcachetraceCachesize,
+ pcachetracePagecount,
+ pcachetraceFetch,
+ pcachetraceUnpin,
+ pcachetraceRekey,
+ pcachetraceTruncate,
+ pcachetraceDestroy,
+ pcachetraceShrink
+};
+
+/* Begin tracing memory allocations to out. */
+int sqlite3PcacheTraceActivate(FILE *out){
+ int rc = SQLITE_OK;
+ if( pcacheBase.xFetch==0 ){
+ rc = sqlite3_config(SQLITE_CONFIG_GETPCACHE2, &pcacheBase);
+ if( rc==SQLITE_OK ){
+ rc = sqlite3_config(SQLITE_CONFIG_PCACHE2, &ersaztPcacheMethods);
+ }
+ }
+ pcachetraceOut = out;
+ return rc;
+}
+
+/* Deactivate memory tracing */
+int sqlite3PcacheTraceDeactivate(void){
+ int rc = SQLITE_OK;
+ if( pcacheBase.xFetch!=0 ){
+ rc = sqlite3_config(SQLITE_CONFIG_PCACHE2, &pcacheBase);
+ if( rc==SQLITE_OK ){
+ memset(&pcacheBase, 0, sizeof(pcacheBase));
+ }
+ }
+ pcachetraceOut = 0;
+ return rc;
+}
diff --git a/ext/misc/series.c b/ext/misc/series.c
index 0deabf95a..3bd567b2e 100644
--- a/ext/misc/series.c
+++ b/ext/misc/series.c
@@ -330,6 +330,10 @@ static int seriesColumn(
return SQLITE_OK;
}
+#ifndef LARGEST_UINT64
+#define LARGEST_UINT64 (0xffffffff|(((sqlite3_uint64)0xffffffff)<<32))
+#endif
+
/*
** Return the rowid for the current row, logically equivalent to n+1 where
** "n" is the ascending integer in the aforesaid production definition.
@@ -337,7 +341,7 @@ static int seriesColumn(
static int seriesRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){
series_cursor *pCur = (series_cursor*)cur;
sqlite3_uint64 n = pCur->ss.uSeqIndexNow;
- *pRowid = (sqlite3_int64)((n<0xffffffffffffffff)? n+1 : 0);
+ *pRowid = (sqlite3_int64)((n
#include
@@ -664,8 +663,14 @@ static int dbdataNext(sqlite3_vtab_cursor *pCursor){
if( pCsr->pHdrPtr>&pCsr->pRec[pCsr->nRec] ){
bNextPage = 1;
}else{
+ int szField = 0;
pCsr->pHdrPtr += dbdataGetVarintU32(pCsr->pHdrPtr, &iType);
- pCsr->pPtr += dbdataValueBytes(iType);
+ szField = dbdataValueBytes(iType);
+ if( (pCsr->nRec - (pCsr->pPtr - pCsr->pRec))pPtr = &pCsr->pRec[pCsr->nRec];
+ }else{
+ pCsr->pPtr += szField;
+ }
}
}
}
@@ -938,15 +943,11 @@ static int sqlite3DbdataRegister(sqlite3 *db){
return rc;
}
-#ifdef _WIN32
-__declspec(dllexport)
-#endif
int sqlite3_dbdata_init(
sqlite3 *db,
char **pzErrMsg,
const sqlite3_api_routines *pApi
){
- SQLITE_EXTENSION_INIT2(pApi);
(void)pzErrMsg;
return sqlite3DbdataRegister(db);
}
diff --git a/ext/recover/recovercorrupt2.test b/ext/recover/recovercorrupt2.test
index 7147c67e9..29acc27a3 100644
--- a/ext/recover/recovercorrupt2.test
+++ b/ext/recover/recovercorrupt2.test
@@ -285,5 +285,244 @@ do_test 5.1 {
list [catch { $R finish } msg] $msg
} {0 {}}
+#-------------------------------------------------------------------------
+#
+reset_db
+do_test 6.0 {
+ sqlite3 db {}
+ db deserialize [decode_hexdb {
+| size 8192 pagesize 4096 filename abc.db
+| page 1 offset 0
+| 0: 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00 SQLite format 3.
+| 16: 10 00 01 01 00 40 20 20 00 00 00 02 00 00 00 02 .....@ ........
+| 32: 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 04 ................
+| 48: 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 ................
+| 80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 ................
+| 96: 00 2e 6e b8 0d 00 00 00 01 0f dc 00 0f dc 00 00 ..n.............
+| 4048: 00 00 00 00 00 00 00 00 00 00 00 00 22 01 06 17 ................
+| 4064: 11 11 01 31 74 61 62 6c 65 74 31 74 31 02 43 52 ...1tablet1t1.CR
+| 4080: 45 41 54 45 20 54 41 42 4c 45 20 74 31 28 78 29 EATE TABLE t1(x)
+| page 2 offset 4096
+| 0: 0d 00 00 00 01 0f e2 00 0f e2 00 00 00 00 00 00 ................
+| 4064: 00 00 1c 01 02 41 61 62 63 64 65 66 67 68 69 6a .....Aabcdefghij
+| 4080: 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a klmnopqrstuvwxyz
+| end abc.db
+}]} {}
+do_test 6.1 {
+ set R [sqlite3_recover_init db main test.db2]
+ catch { $R run }
+ list [catch { $R finish } msg] $msg
+} {0 {}}
+
+reset_db
+breakpoint
+do_test 6.2 {
+ sqlite3 db {}
+ db deserialize [decode_hexdb {
+| size 8192 pagesize 4096 filename abc.db
+| page 1 offset 0
+| 0: 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00 SQLite format 3.
+| 16: 10 00 01 01 00 40 20 20 00 00 00 02 00 00 00 02 .....@ ........
+| 32: 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 04 ................
+| 48: 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 ................
+| 80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 ................
+| 96: 00 2e 6e b8 0d 00 00 00 01 0f dc 00 0f dc 00 00 ..n.............
+| 4048: 00 00 00 00 00 00 00 00 00 00 00 00 22 01 06 17 ................
+| 4064: 11 11 01 31 74 61 62 6c 65 74 31 74 31 02 43 52 ...1tablet1t1.CR
+| 4080: 45 41 54 45 20 54 41 42 4c 45 20 74 31 28 78 29 EATE TABLE t1(x)
+| page 2 offset 4096
+| 0: 0d 00 00 00 01 0f e2 00 0f e2 00 00 00 00 00 00 ................
+| 4064: 00 00 1c 01 02 8F FF FF FF 7E 65 66 67 68 69 6a .....Aabcdefghij
+| 4080: 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a klmnopqrstuvwxyz
+| end abc.db
+}]} {}
+do_test 6.3 {
+ set R [sqlite3_recover_init db main test.db2]
+ catch { $R run }
+ list [catch { $R finish } msg] $msg
+} {0 {}}
+
+reset_db
+breakpoint
+do_test 7.0 {
+ sqlite3 db {}
+ db deserialize [decode_hexdb {
+| size 4108 pagesize 4096 filename x1.db
+| page 1 offset 0
+| 0: 02 01 00 00 00 00 14 15 40 00 00 00 00 00 00 00 ........@.......
+| 16: 33 3a 6d 65 6d 6f 72 79 3a 02 02 02 02 02 02 02 3:memory:.......
+| 32: 02 02 02 02 02 02 12 02 02 02 63 6f 6c 6f 72 20 ..........color
+| 48: 73 70 61 63 00 f3 a0 81 a1 00 00 a0 02 02 02 02 spac............
+| 64: 69 95 73 6f 36 00 ff 0d 00 97 8c 90 3f 0a 70 02 i.so6.......?.p.
+| 80: 02 02 02 02 02 02 02 02 02 02 02 02 02 01 00 00 ................
+| 96: 06 02 02 02 02 5f 02 02 02 2c 02 02 02 02 02 02 ....._...,......
+| 112: 02 02 02 02 02 02 02 02 02 12 02 02 02 63 6f 6c .............col
+| 128: 6f 72 20 73 70 61 63 00 f3 a0 81 a1 00 00 a0 02 or spac.........
+| 144: 02 02 02 69 95 73 6f 36 00 ff 0d 00 97 8c 90 3f ...i.so6.......?
+| 160: 0a 70 02 02 02 02 02 02 02 02 02 02 02 02 02 02 .p..............
+| 176: 01 00 00 06 02 02 02 02 5f 02 02 02 2c 02 02 00 ........_...,...
+| 192: 00 01 00 01 00 00 00 01 00 02 fe 00 00 03 00 01 ................
+| 208: 00 00 00 01 c5 04 00 00 00 01 00 01 00 00 00 01 ................
+| 224: 00 fa 02 00 00 00 03 00 01 00 00 00 81 00 04 00 ................
+| 240: 00 00 01 00 01 00 00 00 01 00 02 00 fe 00 03 00 ................
+| 256: 01 00 00 00 01 00 04 00 00 00 01 00 01 00 00 00 ................
+| 272: 01 00 02 00 00 00 03 00 01 00 00 00 01 00 04 00 ................
+| 288: 00 00 01 00 01 00 00 00 01 00 02 00 00 00 03 00 ................
+| 304: 01 00 00 00 01 00 04 00 00 00 01 00 01 00 00 00 ................
+| 320: 01 00 02 00 00 00 03 00 01 00 00 00 40 00 84 00 ............@...
+| 336: 84 00 84 00 01 00 00 00 09 00 06 00 f5 00 01 00 ................
+| 352: 08 01 03 00 03 00 62 00 62 00 23 00 01 00 62 00 ......b.b.#...b.
+| 368: 04 00 1e 00 62 00 62 00 62 00 01 00 00 00 0a 00 ....b.b.b.......
+| 384: 01 00 03 00 01 00 03 00 04 00 02 00 01 00 01 00 ................
+| 400: 08 00 01 00 31 c6 00 03 00 0c 00 12 00 18 00 02 ....1...........
+| 416: 00 05 00 08 00 02 00 06 00 08 00 02 00 07 00 08 ................
+| 432: 00 02 00 01 00 01 00 08 00 01 00 0c 00 03 00 16 ................
+| 448: 00 1c 00 22 00 01 00 03 00 05 00 06 00 07 00 02 ................
+| 464: 00 05 00 09 00 02 00 06 00 09 00 02 00 07 00 09 ................
+| 480: 00 00 00 00 01 00 05 00 00 00 01 00 01 00 00 00 ................
+| 496: 01 00 02 00 00 00 03 00 01 00 00 00 01 00 04 00 ................
+| 512: 00 00 01 00 01 00 00 00 01 00 02 00 00 00 03 00 ................
+| 528: 01 00 00 00 01 00 04 00 00 00 01 00 01 00 00 00 ................
+| 544: 01 00 02 00 00 f6 03 00 00 02 00 00 01 00 04 00 ................
+| 560: 00 00 01 00 01 00 00 00 01 00 02 00 00 00 03 00 ................
+| 576: 01 00 00 00 01 00 04 00 00 00 01 00 01 00 00 00 ................
+| 592: 01 00 02 00 00 00 03 00 01 00 00 00 01 00 04 00 ................
+| 608: 00 00 01 00 01 00 00 00 01 00 02 00 00 00 03 00 ................
+| 624: 01 00 00 00 01 00 04 00 00 00 01 00 01 00 00 00 ................
+| 640: 01 3d 02 00 00 00 03 00 06 00 00 00 01 00 01 00 .=..............
+| 656: 00 00 01 00 01 00 00 00 01 00 02 00 00 00 03 00 ................
+| 672: 01 00 00 00 01 00 04 00 00 00 01 00 01 00 00 00 ................
+| 688: 01 00 02 00 00 00 55 52 4c 52 65 71 75 65 73 74 ......URLRequest
+| 704: 43 6f 6e 00 00 00 01 01 0e d4 00 04 00 00 00 01 Con.............
+| 720: 0e f8 00 04 00 00 00 01 0f 1c 00 04 00 00 00 01 ................
+| 736: 0f 00 00 01 00 00 00 01 0f 86 00 01 00 00 00 01 ................
+| 752: 0f 84 00 01 00 00 00 01 00 00 01 0f c0 00 01 00 ................
+| 768: 00 00 01 0f e8 00 d6 0f 00 01 6f 00 02 0f d6 00 ..........o.....
+| 784: 02 34 03 03 03 00 01 00 00 00 01 00 05 00 00 00 .4..............
+| 800: 01 00 01 00 00 00 01 00 02 00 00 00 03 00 01 00 ................
+| 816: 00 00 01 00 04 00 00 00 01 00 01 00 00 00 01 00 ................
+| 832: 02 00 00 00 03 00 01 00 10 00 01 00 04 00 00 00 ................
+| 848: 01 00 01 00 00 00 01 00 02 00 00 00 03 00 00 02 ................
+| 864: 00 00 01 40 04 00 00 03 01 00 01 00 00 00 01 00 ...@............
+| 880: 02 00 00 00 03 00 01 00 00 00 00 00 01 0e f8 00 ................
+| 896: 04 77 4f 46 32 73 40 23 70 00 00 00 70 00 1f 00 .wOF2s@#p...p...
+| 912: 00 00 d8 00 00 00 ff ff 00 00 00 00 43 00 00 00 ............C...
+| 928: 00 00 ff ff ff ff ff 00 00 a8 00 00 0c 00 00 00 ................
+| 1024: 00 00 00 00 00 00 00 00 00 00 10 22 00 22 0f 00 ................
+| 1040: 00 00 00 00 00 00 10 22 00 00 70 00 1f 00 00 0f ..........p.....
+| 1056: d8 00 00 00 00 00 00 00 00 00 03 00 00 00 00 00 ................
+| 1072: 00 01 00 00 00 3f 23 70 00 00 00 01 0f 1c 00 04 .....?#p........
+| 1088: 00 00 00 01 0f 40 00 01 00 00 00 01 0f 86 00 01 .....@..........
+| 1104: 00 00 00 01 0f 84 00 01 00 00 00 01 00 00 01 0f ................
+| 1120: c0 00 01 00 00 00 01 0f e8 00 01 0f d6 00 6f 00 ..............o.
+| 1136: 02 0f d6 00 03 02 31 03 2b 03 2a f2 00 0f d4 00 ......1.+.*.....
+| 1152: 01 00 08 00 01 00 04 03 2b 00 02 02 32 00 01 0f ........+...2...
+| 1168: c8 01 15 00 02 20 c8 00 02 12 ad 02 00 24 06 c0 ..... .......$..
+| 1184: 00 00 00 03 00 00 01 24 00 2a 06 e4 00 00 00 03 .......$.*......
+| 1200: 00 00 01 25 00 38 07 0e 00 00 00 03 00 00 01 26 ...%.8.........&
+| 1216: 00 34 07 46 00 00 00 03 00 00 01 27 00 1c 07 7a .4.F.......'...z
+| 1232: 00 00 00 03 00 00 01 28 00 2a 07 96 00 00 00 03 .......(.*......
+| 1248: 00 e5 01 29 00 34 07 c0 00 00 00 03 00 00 01 2a ...).4.........*
+| 1264: 67 34 07 f4 00 00 00 03 00 00 01 2b 00 22 08 28 g4.........+...(
+| 1280: 00 00 00 00 01 00 01 00 00 00 01 00 02 00 00 00 ................
+| 1296: 03 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 ................
+| 1312: 01 00 00 00 01 00 02 00 00 00 03 00 00 02 00 00 ................
+| 1328: 01 00 04 00 00 00 01 00 01 00 00 00 01 00 02 00 ................
+| 1344: 00 00 03 00 01 00 00 00 01 00 04 00 00 00 01 00 ................
+| 1360: 01 00 00 00 01 00 02 00 00 00 03 00 01 00 00 21 ...............!
+| 1376: 04 00 01 00 00 00 00 00 01 00 00 00 01 00 02 00 ................
+| 1392: 00 00 03 00 01 00 00 00 01 00 04 00 00 00 01 00 ................
+| 1408: 01 00 00 00 01 00 02 00 00 00 03 00 01 00 00 00 ................
+| 1424: 01 00 05 00 00 00 01 00 01 00 00 01 00 02 02 02 ................
+| 1440: 12 02 02 02 63 6f 6c 6f 72 20 73 70 61 63 00 f3 ....color spac..
+| 1456: a0 81 a1 00 00 a0 02 02 02 02 69 95 73 6f 36 00 ..........i.so6.
+| 1472: ff 0d 00 97 8c 90 3f 0a 70 02 02 02 02 02 02 02 ......?.p.......
+| 1488: 02 02 02 02 02 02 02 01 00 00 06 02 02 02 02 5f ..............._
+| 1504: 02 02 02 2c 02 02 00 00 01 00 01 00 00 00 01 00 ...,............
+| 1520: 02 fe 00 00 03 00 01 00 00 00 01 c5 04 00 00 00 ................
+| 1536: 01 00 01 00 00 00 01 00 02 00 00 00 03 00 01 00 ................
+| 1552: 00 00 81 00 04 00 00 00 01 00 01 00 00 00 01 00 ................
+| 1568: 02 00 fe 00 03 00 01 00 00 00 01 00 04 00 00 00 ................
+| 1584: 01 00 01 00 00 00 01 00 02 00 00 00 03 00 01 00 ................
+| 1600: 00 00 01 00 04 00 00 00 01 00 01 00 00 00 01 00 ................
+| 1616: 02 00 00 00 03 00 01 00 00 00 01 00 04 00 00 00 ................
+| 1632: 01 00 01 00 00 00 01 00 02 00 00 00 03 00 01 00 ................
+| 1648: 00 00 40 00 84 00 84 00 84 00 01 00 00 00 09 00 ..@.............
+| 1664: 06 00 f5 00 01 00 08 01 03 15 15 15 15 15 15 15 ................
+| 1680: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 ................
+| 1696: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 ................
+| 1712: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 ................
+| 1728: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 ................
+| 1744: 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 ................
+| 1760: 15 15 15 15 15 15 15 15 15 15 15 00 03 00 62 00 ..............b.
+| 1776: 62 00 23 00 01 00 62 00 04 00 1e 00 62 00 62 00 b.#...b.....b.b.
+| 1792: 62 00 01 00 00 00 0a 00 01 00 03 00 01 00 03 00 b...............
+| 1808: 04 00 02 00 01 00 01 00 08 00 01 00 31 c6 00 03 ............1...
+| 1824: 00 0c 00 12 00 18 00 02 00 05 00 08 00 02 00 06 ................
+| 1840: 00 08 00 02 00 07 00 08 00 02 00 01 00 01 00 08 ................
+| 1856: 00 01 00 0c 00 03 00 16 00 1c 00 22 00 01 00 03 ................
+| 1872: 00 05 00 06 00 07 00 02 00 05 00 09 00 02 00 06 ................
+| 1888: 00 09 00 02 00 07 00 09 00 00 00 00 01 00 05 00 ................
+| 1904: 00 00 01 00 01 00 00 00 01 00 02 00 00 00 03 00 ................
+| 1920: 01 00 00 00 01 00 04 00 00 00 01 00 01 00 00 00 ................
+| 1936: 01 00 02 00 00 00 03 00 01 00 00 00 01 00 04 00 ................
+| 1952: 00 00 01 0f d6 00 02 34 03 03 03 00 01 00 00 00 .......4........
+| 1968: 01 00 05 00 00 00 01 00 01 00 00 00 01 00 02 00 ................
+| 1984: 00 00 03 00 01 00 00 00 01 00 04 00 00 00 01 00 ................
+| 2000: 01 00 00 00 01 00 02 fc 42 dc 19 5c 74 23 18 cd ........B...t#..
+| 2016: b3 a5 a8 7a 90 40 1d 66 12 5d e5 4f 85 00 68 f4 ...z.@.f.].O..h.
+| 2032: 05 98 86 25 24 dd bc c2 f6 f6 4e a3 e2 61 d2 c6 ...%$.....N..a..
+| 2048: aa c1 56 50 d4 80 82 35 f1 e2 59 41 50 a6 da 51 ..VP...5..YAP..Q
+| 2064: d4 62 9c 19 94 58 aa 31 30 8a 22 c2 5f 33 2b c9 .b...X.10..._3+.
+| 2080: b6 e6 b4 11 4e 51 82 c4 d8 b6 d8 b4 06 04 fb 68 ....NQ.........h
+| 2096: f4 d2 6f e7 cb 8a a8 82 d5 74 00 00 00 00 00 00 ..o......t......
+| 2368: 00 00 00 00 00 00 00 00 00 08 00 00 00 00 00 00 ................
+| 2432: 00 00 00 00 00 03 00 01 00 10 00 01 00 04 00 00 ................
+| 2448: 00 01 00 01 00 00 00 01 00 02 00 00 00 03 00 00 ................
+| 2464: 02 00 00 01 40 04 00 00 03 01 00 01 00 00 00 01 ....@...........
+| 2480: 00 02 00 00 00 03 00 01 00 00 00 00 00 01 0e f8 ................
+| 2496: 00 04 77 4f 46 32 73 40 23 70 00 00 00 70 00 1f ..wOF2s@#p...p..
+| 2512: 00 00 00 d8 00 00 00 ff ff 00 00 00 00 43 00 00 .............C..
+| 2528: 00 00 00 ff ff ff ff ff 00 00 a8 00 00 0c 00 00 ................
+| 2624: 00 00 00 00 00 00 00 00 00 00 00 10 22 00 22 0f ................
+| 2640: 00 00 00 00 00 00 00 10 22 00 00 70 00 1f 00 00 ...........p....
+| 2656: 0f d8 00 00 00 00 00 00 00 00 00 03 00 00 00 00 ................
+| 2672: 00 00 01 00 00 00 3f 23 70 00 00 00 01 0f 1c 00 ......?#p.......
+| 2688: 04 00 00 00 01 0f 40 00 01 00 00 00 01 0f 86 00 ......@.........
+| 2704: 01 00 00 00 01 0f 84 00 01 00 00 00 01 00 00 01 ................
+| 2720: 0f c0 00 01 00 00 00 01 0f e8 00 01 0f d6 00 6f ...............o
+| 2736: 00 02 0f d6 00 03 02 31 03 2b 03 2a f2 00 0f d4 .......1.+.*....
+| 2752: 00 01 00 08 00 01 00 04 03 2b 00 02 02 32 00 01 .........+...2..
+| 2768: 0f c8 01 15 00 02 20 c8 00 02 12 ad 02 00 24 06 ...... .......$.
+| 2784: c0 00 00 5a 03 00 00 01 24 00 2a 06 e4 00 00 00 ...Z....$.*.....
+| 2800: 03 00 00 01 25 00 38 07 0e 00 00 00 03 00 00 01 ....%.8.........
+| 2816: 26 00 34 07 46 00 00 00 03 00 00 01 27 00 1c 07 &.4.F.......'...
+| 2832: 7a 00 00 00 03 00 00 01 28 00 2a 07 96 00 00 00 z.......(.*.....
+| 2848: 03 00 e5 01 29 00 34 07 c0 00 00 00 03 00 00 01 ....).4.........
+| 2864: 2a 67 34 07 f4 00 00 00 03 00 00 01 2b 00 22 08 *g4.........+...
+| 2880: 28 00 00 00 00 01 00 01 00 00 00 01 00 02 00 00 (...............
+| 2896: 00 03 00 01 00 00 00 01 00 00 00 01 00 00 00 01 ................
+| 2912: 00 01 00 00 00 01 00 02 00 00 00 03 00 00 02 00 ................
+| 2928: 00 01 00 04 00 00 00 01 00 01 00 00 00 00 00 00 ................
+| 2992: 00 48 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .H..............
+| 3504: 00 00 00 00 00 00 00 00 00 00 00 97 00 00 00 00 ................
+| 3904: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 09 ................
+| 3920: 03 fe 00 00 01 36 00 3c 0a 38 00 00 00 03 00 00 .....6.<.8......
+| 3936: 01 37 00 20 0a 74 00 00 00 fb ff ff 00 38 00 2a .7. .t.......8.*
+| 3952: 0a 94 00 00 00 03 00 00 01 39 4f 54 54 4f 00 0e .........9OTTO..
+| 3968: 00 80 00 03 00 60 43 46 46 20 e3 ae 89 2a 00 00 .....`CFF ...*..
+| 3984: 02 b0 00 00 02 76 42 50 4f 53 00 15 00 0a 00 00 .....vBPOS......
+| 4000: 05 28 00 00 00 0c 54 53 55 42 c9 70 c3 06 00 00 .(....TSUB.p....
+| 4016: 05 34 1f 00 40 00 48 00 00 00 00 00 00 00 00 00 .4..@.H.........
+| 4064: 00 00 00 00 00 08 00 01 00 01 00 01 00 01 00 06 ................
+| 4080: 00 02 00 08 00 01 00 01 00 01 00 01 00 00 00 00 ................
+| end x1.db
+}]} {}
+do_test 7.1 {
+ set R [sqlite3_recover_init db main test.db2]
+ catch { $R run }
+ list [catch { $R finish } msg] $msg
+} {1 {file is not a database}}
+
finish_test
diff --git a/ext/recover/sqlite3recover.c b/ext/recover/sqlite3recover.c
index 29fff0e7e..c445c5179 100644
--- a/ext/recover/sqlite3recover.c
+++ b/ext/recover/sqlite3recover.c
@@ -2103,7 +2103,7 @@ static int recoverIsValidPage(u8 *aTmp, const u8 *a, int n){
if( iFree>(n-4) ) return 0;
iNext = recoverGetU16(&a[iFree]);
nByte = recoverGetU16(&a[iFree+2]);
- if( iFree+nByte>n ) return 0;
+ if( iFree+nByte>n || nByte<4 ) return 0;
if( iNext && iNext
#include
#include
@@ -502,7 +507,7 @@ static int readInt16(u8 *p){
return (p[0]<<8) + p[1];
}
static void readCoord(u8 *p, RtreeCoord *pCoord){
- assert( (((sqlite3_uint64)p)&3)==0 ); /* p is always 4-byte aligned */
+ assert( FOUR_BYTE_ALIGNED(p) );
#if SQLITE_BYTEORDER==1234 && MSVC_VERSION>=1300
pCoord->u = _byteswap_ulong(*(u32*)p);
#elif SQLITE_BYTEORDER==1234 && GCC_VERSION>=4003000
@@ -556,7 +561,7 @@ static void writeInt16(u8 *p, int i){
}
static int writeCoord(u8 *p, RtreeCoord *pCoord){
u32 i;
- assert( (((sqlite3_uint64)p)&3)==0 ); /* p is always 4-byte aligned */
+ assert( FOUR_BYTE_ALIGNED(p) );
assert( sizeof(RtreeCoord)==4 );
assert( sizeof(u32)==4 );
#if SQLITE_BYTEORDER==1234 && GCC_VERSION>=4003000
@@ -1284,7 +1289,7 @@ static void rtreeNonleafConstraint(
assert(p->op==RTREE_LE || p->op==RTREE_LT || p->op==RTREE_GE
|| p->op==RTREE_GT || p->op==RTREE_EQ || p->op==RTREE_TRUE
|| p->op==RTREE_FALSE );
- assert( (((sqlite3_uint64)pCellData)&3)==0 ); /* 4-byte aligned */
+ assert( FOUR_BYTE_ALIGNED(pCellData) );
switch( p->op ){
case RTREE_TRUE: return; /* Always satisfied */
case RTREE_FALSE: break; /* Never satisfied */
@@ -1337,7 +1342,7 @@ static void rtreeLeafConstraint(
|| p->op==RTREE_GT || p->op==RTREE_EQ || p->op==RTREE_TRUE
|| p->op==RTREE_FALSE );
pCellData += 8 + p->iCoord*4;
- assert( (((sqlite3_uint64)pCellData)&3)==0 ); /* 4-byte aligned */
+ assert( FOUR_BYTE_ALIGNED(pCellData) );
RTREE_DECODE_COORD(eInt, pCellData, xN);
switch( p->op ){
case RTREE_TRUE: return; /* Always satisfied */
@@ -1907,7 +1912,20 @@ static int rtreeFilter(
p->pInfo->nCoord = pRtree->nDim2;
p->pInfo->anQueue = pCsr->anQueue;
p->pInfo->mxLevel = pRtree->iDepth + 1;
- }else if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){
+ }else if( eType==SQLITE_INTEGER ){
+ sqlite3_int64 iVal = sqlite3_value_int64(argv[ii]);
+#ifdef SQLITE_RTREE_INT_ONLY
+ p->u.rValue = iVal;
+#else
+ p->u.rValue = (double)iVal;
+ if( iVal>=((sqlite3_int64)1)<<48
+ || iVal<=-(((sqlite3_int64)1)<<48)
+ ){
+ if( p->op==RTREE_LT ) p->op = RTREE_LE;
+ if( p->op==RTREE_GT ) p->op = RTREE_GE;
+ }
+#endif
+ }else if( eType==SQLITE_FLOAT ){
#ifdef SQLITE_RTREE_INT_ONLY
p->u.rValue = sqlite3_value_int64(argv[ii]);
#else
@@ -2038,11 +2056,12 @@ static int rtreeBestIndex(sqlite3_vtab *tab, sqlite3_index_info *pIdxInfo){
|| p->op==SQLITE_INDEX_CONSTRAINT_MATCH)
){
u8 op;
+ u8 doOmit = 1;
switch( p->op ){
- case SQLITE_INDEX_CONSTRAINT_EQ: op = RTREE_EQ; break;
- case SQLITE_INDEX_CONSTRAINT_GT: op = RTREE_GT; break;
+ case SQLITE_INDEX_CONSTRAINT_EQ: op = RTREE_EQ; doOmit = 0; break;
+ case SQLITE_INDEX_CONSTRAINT_GT: op = RTREE_GT; doOmit = 0; break;
case SQLITE_INDEX_CONSTRAINT_LE: op = RTREE_LE; break;
- case SQLITE_INDEX_CONSTRAINT_LT: op = RTREE_LT; break;
+ case SQLITE_INDEX_CONSTRAINT_LT: op = RTREE_LT; doOmit = 0; break;
case SQLITE_INDEX_CONSTRAINT_GE: op = RTREE_GE; break;
case SQLITE_INDEX_CONSTRAINT_MATCH: op = RTREE_MATCH; break;
default: op = 0; break;
@@ -2051,7 +2070,7 @@ static int rtreeBestIndex(sqlite3_vtab *tab, sqlite3_index_info *pIdxInfo){
zIdxStr[iIdx++] = op;
zIdxStr[iIdx++] = (char)(p->iColumn - 1 + '0');
pIdxInfo->aConstraintUsage[ii].argvIndex = (iIdx/2);
- pIdxInfo->aConstraintUsage[ii].omit = 1;
+ pIdxInfo->aConstraintUsage[ii].omit = doOmit;
}
}
}
diff --git a/ext/rtree/rtree1.test b/ext/rtree/rtree1.test
index 034155341..633d0a5d5 100644
--- a/ext/rtree/rtree1.test
+++ b/ext/rtree/rtree1.test
@@ -756,4 +756,32 @@ do_execsql_test 20.4 {
SELECT * FROM t1 JOIN t0 ON true RIGHT JOIN rt0 ON x0>a WHERE x0 = 0;
} {- - 0 0.0 0.0}
+# 2023-05-19 https://sqlite.org/forum/forumpost/da61c4a1b5b4af19
+# Do not omit constraints that involve equality comparisons of
+# floating-point values.
+#
+reset_db
+do_execsql_test 21.0 {
+ CREATE VIRTUAL TABLE t1 USING rtree(id, x0, x1);
+ INSERT INTO t1 VALUES(0, 1, 9223372036854775807);
+ SELECT count(*) FROM t1 WHERE x1=9223372036854775807;
+} {0}
+do_execsql_test 21.1 {
+ SELECT x1=9223372036854775807 FROM t1;
+} {0}
+
+# 2023-05-22 https://sqlite.org/forum/forumpost/da70ee0d0d
+# Round-off error associated with using large integer constraints on
+# a rtree search.
+#
+reset_db
+do_execsql_test 22.0 {
+ CREATE VIRTUAL TABLE t1 USING rtree ( id, x0, x1 );
+ INSERT INTO t1 VALUES (123, 9223372036854775799, 9223372036854775800);
+ SELECT id FROM t1 WHERE x0 > 9223372036854775807;
+} {123}
+do_execsql_test 22.1 {
+ SELECT id, x0 > 9223372036854775807 AS 'a0' FROM t1;
+} {123 1}
+
finish_test
diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c
index 679408849..9f862f246 100644
--- a/ext/session/sqlite3session.c
+++ b/ext/session/sqlite3session.c
@@ -891,6 +891,7 @@ static int sessionPreupdateEqual(
rc = pSession->hook.xOld(pSession->hook.pCtx, iCol, &pVal);
}
assert( rc==SQLITE_OK );
+ (void)rc; /* Suppress warning about unused variable */
if( sqlite3_value_type(pVal)!=eType ) return 0;
/* A SessionChange object never has a NULL value in a PK column */
diff --git a/ext/wasm/GNUmakefile b/ext/wasm/GNUmakefile
index a99513bfa..080c42704 100644
--- a/ext/wasm/GNUmakefile
+++ b/ext/wasm/GNUmakefile
@@ -18,8 +18,6 @@
# quick, q = do just a minimal build (sqlite3.js/wasm, tester1) for
# faster development-mode turnaround.
#
-# qo2, qoz = a combination of quick+o2/oz.
-#
# dist = create end user deliverables. Add dist.build=oX to build
# with a specific optimization level, where oX is one of the
# above-listed o? or qo? target names.
@@ -46,11 +44,12 @@
# $(eval), or at least centralize the setup of the numerous vars
# related to each build variant $(JS_BUILD_MODES).
#
+default: all
+#default: quick
SHELL := $(shell which bash 2>/dev/null)
MAKEFILE := $(lastword $(MAKEFILE_LIST))
CLEAN_FILES :=
DISTCLEAN_FILES := ./--dummy--
-default: all
release: oz
# JS_BUILD_MODES exists solely to reduce repetition in documentation
# below.
@@ -68,13 +67,6 @@ ifeq (,$(emcc.version))
else
$(info using emcc version [$(emcc.version)])
endif
-emcc.version := $(shell "$(emcc.bin)" --version | sed -n 1p \
- | sed -e 's/^.* \([3-9][^ ]*\) .*$$/\1/;')
-ifeq (,$(emcc.version))
- $(warning Cannot determine emcc version. This might unduly impact build flags.)
-else
- $(info using emcc version [$(emcc.version)])
-endif
wasm-strip ?= $(shell which wasm-strip 2>/dev/null)
ifeq (,$(filter clean,$(MAKECMDGOALS)))
@@ -159,6 +151,9 @@ endif
# bundle.
#
# A custom sqlite3.c must not have any spaces in its name.
+# $(sqlite3.canonical.c) must point to the sqlite3.c in
+# the sqlite3 canonical source tree, as that source file
+# is required for certain utility and test code.
sqlite3.canonical.c := $(dir.top)/sqlite3.c
sqlite3.c ?= $(firstword $(wildcard $(dir.top)/sqlite3-see.c) $(sqlite3.canonical.c))
sqlite3.h := $(dir.top)/sqlite3.h
@@ -184,14 +179,19 @@ SQLITE_OPT = \
-DSQLITE_OMIT_DEPRECATED \
-DSQLITE_OMIT_UTF16 \
-DSQLITE_OMIT_SHARED_CACHE \
- -DSQLITE_OMIT_WAL \
-DSQLITE_THREADSAFE=0 \
- -DSQLITE_TEMP_STORE=3 \
+ -DSQLITE_TEMP_STORE=2 \
-DSQLITE_OS_KV_OPTIONAL=1 \
'-DSQLITE_DEFAULT_UNIX_VFS="unix-none"' \
-DSQLITE_USE_URI=1 \
-DSQLITE_WASM_ENABLE_C_TESTS \
-DSQLITE_C=$(sqlite3.c)
+#SQLITE_OPT += -DSQLITE_DEBUG
+# Enabling SQLITE_DEBUG will break sqlite3_wasm_vfs_create_file()
+# (and thus sqlite3_js_vfs_create_file()). Those functions are
+# deprecated and alternatives are in place, but this crash behavior
+# can be used to find errant uses of sqlite3_js_vfs_create_file()
+# in client code.
.NOTPARALLEL: $(sqlite3.h)
$(sqlite3.h):
@@ -246,10 +246,10 @@ endif
# embedding in the JS files and in building the distribution zip file.
# It must NOT be in $(dir.tmp) because we need it to survive the
# cleanup process for the dist build to work properly.
-bin.version-info := $(dir.wasm)/version-info
-$(bin.version-info): $(dir.wasm)/version-info.c $(sqlite3.h) $(MAKEFILE)
- $(CC) -O0 -I$(dir $(sqlite3.c)) -o $@ $<
-DISTCLEAN_FILES += $(bin.version-info)
+bin.version-info := $(dir.top)/version-info
+.NOTPARALLEL: $(bin.version-info)
+$(bin.version-info): $(dir.tool)/version-info.c $(sqlite3.h) $(dir.top)/Makefile
+ $(MAKE) -C $(dir.top) version-info
# bin.stripcomments is used for stripping C/C++-style comments from JS
# files. The JS files contain large chunks of documentation which we
@@ -379,20 +379,17 @@ sqlite3-api.jses += $(dir.api)/sqlite3-api-oo1.js
sqlite3-api.jses += $(dir.api)/sqlite3-api-worker1.js
sqlite3-api.jses += $(dir.api)/sqlite3-v-helper.js
sqlite3-api.jses += $(dir.api)/sqlite3-vfs-opfs.c-pp.js
+sqlite3-api.jses += $(dir.api)/sqlite3-vfs-opfs-sahpool.c-pp.js
sqlite3-api.jses += $(dir.api)/sqlite3-api-cleanup.js
-# "External" API files which are part of our distribution
+# SOAP.js is an external API file which is part of our distribution
# but not part of the sqlite3-api.js amalgamation.
SOAP.js := $(dir.api)/sqlite3-opfs-async-proxy.js
-# COPY_XAPI = a $(call)able function to copy $1 to $(dir.dout), where
-# $1 must be one of the "external" JS API files.
-define COPY_XAPI
-sqlite3-api.ext.jses += $$(dir.dout)/$$(notdir $(1))
-$$(dir.dout)/$$(notdir $(1)): $(1) $$(MAKEFILE)
- cp $$< $$@
-endef
-$(foreach X,$(SOAP.js),\
- $(eval $(call COPY_XAPI,$(X))))
+SOAP.js.bld := $(dir.dout)/$(notdir $(SOAP.js))
+sqlite3-api.ext.jses += $(SOAP.js.bld)
+$(SOAP.js.bld): $(SOAP.js)
+ cp $< $@
+
all quick: $(sqlite3-api.ext.jses)
q: quick
@@ -459,6 +456,13 @@ emcc.exportedRuntimeMethods := \
emcc.jsflags += $(emcc.exportedRuntimeMethods)
emcc.jsflags += -sUSE_CLOSURE_COMPILER=0
emcc.jsflags += -sIMPORTED_MEMORY
+#emcc.jsflags += -sASYNCIFY=2
+# ^^^ ASYNCIFY=2 is for experimental JSPI support
+# (https://v8.dev/blog/jspi), but enabling it causes the lib-level
+# init code to throw inexplicable complaints about C-level function
+# signatures not matching what we expect them to be. JSPI requires, as of
+# this writing, requires an experimental Chrome flag:
+# chrome://flags/#enable-experimental-webassembly-stack-switching
emcc.jsflags += -sSTRICT_JS=0
# STRICT_JS disabled due to:
# https://github.com/emscripten-core/emscripten/issues/18610
@@ -615,26 +619,40 @@ $(post-js.js.in): $(post-jses.js) $(MAKEFILE)
########################################################################
# call-make-pre-post is a $(call)able which creates rules for
-# pre-js-$(1).js. $1 = the base name of the JS file on whose behalf
-# this pre-js is for (one of: sqlite3, sqlite3-wasmfs). $2 is the
-# build mode: one of $(JS_BUILD_MODES). This
-# sets up --[extern-][pre/post]-js flags in
-# $(pre-post-$(1).flags.$(2)) and dependencies in
-# $(pre-post-$(1).deps.$(2)).
+# pre-js-$(1)-$(2).js. $1 = the base name of the JS file on whose
+# behalf this pre-js is for (one of: sqlite3, sqlite3-wasmfs). $2 is
+# the build mode: one of $(JS_BUILD_MODES). This sets up
+# --[extern-][pre/post]-js flags in $(pre-post-$(1)-$(2).flags) and
+# dependencies in $(pre-post-$(1)-$(2).deps). The resulting files get
+# filtered using $(C-PP.FILTER). Any flags necessary for such
+# filtering need to be set in $(c-pp.D.$(1)-$(2)) before $(call)ing
+# this.
define call-make-pre-post
-pre-post-$(1).flags.$(2) ?=
-$$(dir.tmp)/pre-js-$(1)-$(2).js: $$(pre-js.js.$(2)) $$(MAKEFILE)
- cp $$(pre-js.js.$(2)) $$@
+pre-post-$(1)-$(2).flags ?=
+pre-js.js.$(1)-$(2) := $$(dir.tmp)/pre-js.$(1)-$(2).intermediary.js
+$$(eval $$(call C-PP.FILTER,$$(pre-js.js.in),$$(pre-js.js.$(1)-$(2)),$$(c-pp.D.$(1)-$(2))))
+post-js.js.$(1)-$(2) := $$(dir.tmp)/post-js.$(1)-$(2).js
+$$(eval $$(call C-PP.FILTER,$$(post-js.js.in),$$(post-js.js.$(1)-$(2)),$$(c-pp.D.$(1)-$(2))))
+extern-post-js.js.$(1)-$(2) := $$(dir.tmp)/extern-post-js.$(1)-$(2).js
+$$(eval $$(call C-PP.FILTER,$$(extern-post-js.js.in),$$(extern-post-js.js.$(1)-$(2)),$$(c-pp.D.$(1)-$(2))))
+pre-post-common.flags.$(1)-$(2) := \
+ $$(pre-post-common.flags) \
+ --post-js=$$(post-js.js.$(1)-$(2)) \
+ --extern-post-js=$$(extern-post-js.js.$(1)-$(2))
+pre-post-jses.$(1)-$(2).deps := $$(pre-post-jses.deps.common) \
+ $$(post-js.js.$(1)-$(2)) $$(extern-post-js.js.$(1)-$(2))
+$$(dir.tmp)/pre-js-$(1)-$(2).js: $$(pre-js.js.$(1)-$(2)) $$(MAKEFILE)
+ cp $$(pre-js.js.$(1)-$(2)) $$@
@if [ sqlite3-wasmfs = $(1) ]; then \
echo "delete Module[xNameOfInstantiateWasm] /*for WASMFS build*/;"; \
elif [ sqlite3 != $(1) ]; then \
echo "Module[xNameOfInstantiateWasm].uri = '$(1).wasm';"; \
fi >> $$@
-pre-post-$(1).deps.$(2) := \
- $$(pre-post-jses.deps.$(2)) \
+pre-post-$(1)-$(2).deps := \
+ $$(pre-post-jses.$(1)-$(2).deps) \
$$(dir.tmp)/pre-js-$(1)-$(2).js
-pre-post-$(1).flags.$(2) += \
- $$(pre-post-common.flags.$(2)) \
+pre-post-$(1)-$(2).flags += \
+ $$(pre-post-common.flags.$(1)-$(2)) \
--pre-js=$$(dir.tmp)/pre-js-$(1)-$(2).js
endef
# /post-js and pre-js
@@ -645,7 +663,8 @@ endef
# https://github.com/emscripten-core/emscripten/issues/14383
sqlite3.wasm := $(dir.dout)/sqlite3.wasm
sqlite3-wasm.c := $(dir.api)/sqlite3-wasm.c
-sqlite3-wasm.cses := $(sqlite3-wasm.c) $(sqlite3_wasm_extra_init.c)
+sqlite3-wasm.cfiles := $(sqlite3-wasm.c) $(sqlite3_wasm_extra_init.c)
+sqlite3-wasmfs.cfiles := $(sqlite3-wasm.cfiles)
# sqlite3-wasm.o vs sqlite3-wasm.c: building against the latter
# (predictably) results in a slightly faster binary. We're close
# enough to the target speed requirements that the 500ms makes a
@@ -655,7 +674,8 @@ sqlite3-wasm.cses := $(sqlite3-wasm.c) $(sqlite3_wasm_extra_init.c)
# SQLITE3.xJS.EXPORT-DEFAULT is part of SQLITE3-WASMFS.xJS.RECIPE and
# SETUP_LIB_BUILD_MODE, factored into a separate piece to avoid code
# duplication. $1 is 1 if the build mode needs this workaround (esm,
-# bundler-friendly) and 0 if not (vanilla).
+# bundler-friendly, node) and 0 if not (vanilla). $2 must be empty for
+# all builds except sqlite3-wasmfs.mjs, in which case it must be 1.
#
# Reminder for ESM builds: even if we use -sEXPORT_ES6=0, emcc _still_
# adds:
@@ -669,13 +689,20 @@ sqlite3-wasm.cses := $(sqlite3-wasm.c) $(sqlite3_wasm_extra_init.c)
#
# Upstream RFE:
# https://github.com/emscripten-core/emscripten/issues/18237
+#
+# Maintenance reminder: Mac sed works differently than GNU sed, so
+# don't use sed for this.
define SQLITE3.xJS.ESM-EXPORT-DEFAULT
if [ x1 = x$(1) ]; then \
- echo "Fragile workaround for an Emscripten annoyance. See SQLITE3.xJS.RECIPE."; \
- sed -i -e '0,/^export default/{/^export default/d;}' $@ || exit $$?; \
- if ! grep -q '^export default' $@; then \
- echo "Cannot find export default." 1>&2; \
- exit 1; \
+ echo "Fragile workaround for emscripten/issues/18237. See SQLITE3.xJS.RECIPE."; \
+ {\
+ awk '/^export default/ && !f{f=1; next} 1' $@ > $@.tmp && mv $@.tmp $@; \
+ } || exit $$?; \
+ if [ x != x$(2) ]; then \
+ if ! grep -q '^export default' $@; then \
+ echo "Cannot find export default." 1>&2; \
+ exit 1; \
+ fi; \
fi; \
fi
endef
@@ -695,53 +722,57 @@ pre-post-jses.deps.common := $(extern-pre-js.js) $(sqlite3-license-version.js)
# SETUP_LIB_BUILD_MODE is a $(call)'able which sets up numerous pieces
# for one of the build modes.
#
-# $1 = build mode name: one of $(JS_BUILD_MODES)
-# $2 = 1 for ESM build mode, else 0
-# $3 = resulting sqlite-api JS/MJS file
-# $4 = resulting JS/MJS file
-# $5 = -D... flags for $(bin.c-pp)
-# $6 = emcc -sXYZ flags (CURRENTLY UNUSED - was factored out)
+# $1 = one of: sqlite3, sqlite3-wasmfs
+# $2 = build mode name: one of $(JS_BUILD_MODES)
+# $3 = 1 for ESM build mode, else 0
+# $4 = resulting sqlite-api JS/MJS file
+# $5 = resulting JS/MJS file
+# $6 = -D... flags for $(bin.c-pp)
+# $7 = emcc -sXYZ flags (CURRENTLY UNUSED - was factored out)
+#
+# Maintenance reminder: be careful not to introduce spaces around args
+# ($1, $2), otherwise string concatenation will malfunction.
#
-# emcc.environment.$(1) must be set to a value for the -sENVIRONMENT flag.
+# emcc.environment.$(2) must be set to a value for the -sENVIRONMENT flag.
+#
+# $(cflags.$(1)) and $(cflags.$(1).$(2)) may be defined to append
+# CFLAGS to a given build mode.
+#
+# $(emcc.flags.$(1)) and $(emcc.flags.$(1).$(2)) may be defined to
+# append emcc-specific flags to a given build mode.
define SETUP_LIB_BUILD_MODE
-$(info Setting up build [$(1)]: $(4))
-c-pp.D.$(1) := $(5)
-pre-js.js.$(1) := $$(dir.tmp)/pre-js.$(1).js
-$$(eval $$(call C-PP.FILTER,$$(pre-js.js.in),$$(pre-js.js.$(1)),$$(c-pp.D.$(1))))
-post-js.js.$(1) := $$(dir.tmp)/post-js.$(1).js
-$$(eval $$(call C-PP.FILTER,$$(post-js.js.in),$$(post-js.js.$(1)),$$(c-pp.D.$(1))))
-extern-post-js.js.$(1) := $$(dir.tmp)/extern-post-js.$(1).js
-$$(eval $$(call C-PP.FILTER,$$(extern-post-js.js.in),$$(extern-post-js.js.$(1)),$$(c-pp.D.$(1))))
-pre-post-common.flags.$(1) := \
- $$(pre-post-common.flags) \
- --post-js=$$(post-js.js.$(1)) \
- --extern-post-js=$$(extern-post-js.js.$(1))
-pre-post-jses.deps.$(1) := $$(pre-post-jses.deps.common) \
- $$(post-js.js.$(1)) $$(extern-post-js.js.$(1))
-$$(eval $$(call call-make-pre-post,sqlite3,$(1)))
-emcc.flags.sqlite3.$(1) := $(6)
-$$(eval $$(call C-PP.FILTER, $$(sqlite3-api.js.in), $(3), $(5)))
-$(4): $(3) $$(MAKEFILE) $$(sqlite3-wasm.cses) $$(EXPORTED_FUNCTIONS.api) $$(pre-post-sqlite3.deps.$(1))
+$(info Setting up build [$(1)-$(2)]: $(5))
+c-pp.D.$(1)-$(2) := $(6)
+$$(eval $$(call call-make-pre-post,$(1),$(2)))
+emcc.flags.$(1).$(2) ?=
+emcc.flags.$(1).$(2) += $(7)
+$$(eval $$(call C-PP.FILTER, $$(sqlite3-api.js.in), $(4), $(6)))
+$(5): $(4) $$(MAKEFILE) $$(sqlite3-wasm.cfiles) $$(EXPORTED_FUNCTIONS.api) $$(pre-post-$(1)-$(2).deps)
@echo "Building $$@ ..."
$$(emcc.bin) -o $$@ $$(emcc_opt_full) $$(emcc.flags) \
$$(emcc.jsflags) \
- -sENVIRONMENT=$$(emcc.environment.$(1)) \
- $$(pre-post-sqlite3.flags.$(1)) $$(emcc.flags.sqlite3.$(1)) \
- $$(cflags.common) $$(SQLITE_OPT) $$(cflags.wasm_extra_init) $$(sqlite3-wasm.cses)
- @$$(call SQLITE3.xJS.ESM-EXPORT-DEFAULT,$(2))
- @case $(1) in \
+ -sENVIRONMENT=$$(emcc.environment.$(2)) \
+ $$(pre-post-$(1)-$(2).flags) \
+ $$(emcc.flags.$(1)) $$(emcc.flags.$(1).$(2)) \
+ $$(cflags.common) $$(SQLITE_OPT) \
+ $$(cflags.$(1)) $$(cflags.$(1).$(2)) \
+ $$(cflags.wasm_extra_init) $$(sqlite3-wasm.cfiles)
+ @$$(call SQLITE3.xJS.ESM-EXPORT-DEFAULT,$(3))
+ @dotwasm=$$(basename $$@).wasm; \
+ chmod -x $$$$dotwasm; \
+ $(maybe-wasm-strip) $$$$dotwasm; \
+ case $(2) in \
bundler-friendly|node) \
- echo "Patching $(3) for sqlite3.wasm..."; \
- rm -f $$(dir.dout)/sqlite3-$(1).wasm; \
- sed -i -e 's/sqlite3-$(1).wasm/sqlite3.wasm/g' $$@ || exit $$$$?; \
+ echo "Patching $$@ for $(1).wasm..."; \
+ rm -f $$$$dotwasm; \
+ dotwasm=; \
+ sed -i -e 's/$(1)-$(2).wasm/$(1).wasm/g' $$@ || exit $$$$?; \
;; \
- esac
- chmod -x $$(sqlite3.wasm)
- $$(maybe-wasm-strip) $$(sqlite3.wasm)
- @ls -la $@ $$(sqlite3.wasm)
-all: $(4)
-quick: $(4)
-CLEAN_FILES += $(3) $(4)
+ esac; \
+ ls -la $$$$dotwasm $$@
+all: $(5)
+#quick: $(5)
+CLEAN_FILES += $(4) $(5)
endef
# ^^^ /SETUP_LIB_BUILD_MODE
########################################################################
@@ -753,21 +784,24 @@ sqlite3-api-bundler-friendly.mjs := $(dir.dout)/sqlite3-api-bundler-friendly.mjs
sqlite3-bundler-friendly.mjs := $(dir.dout)/sqlite3-bundler-friendly.mjs
sqlite3-api-node.mjs := $(dir.dout)/sqlite3-api-node.mjs
sqlite3-node.mjs := $(dir.dout)/sqlite3-node.mjs
-# Maintenance reminder: careful not to introduce spaces around args $1, $2
-#$(info $(call SETUP_LIB_BUILD_MODE,vanilla,0, $(sqlite3-api.js), $(sqlite3.js)))
-$(eval $(call SETUP_LIB_BUILD_MODE,vanilla,0, $(sqlite3-api.js), $(sqlite3.js)))
-$(eval $(call SETUP_LIB_BUILD_MODE,esm,1, $(sqlite3-api.mjs), $(sqlite3.mjs), \
+#$(info $(call SETUP_LIB_BUILD_MODE,sqlite3,vanilla,0, $(sqlite3-api.js), $(sqlite3.js)))
+$(eval $(call SETUP_LIB_BUILD_MODE,sqlite3,vanilla,0,\
+ $(sqlite3-api.js), $(sqlite3.js)))
+$(eval $(call SETUP_LIB_BUILD_MODE,sqlite3,esm,1,\
+ $(sqlite3-api.mjs), $(sqlite3.mjs), \
-Dtarget=es6-module, -sEXPORT_ES6 -sUSE_ES6_IMPORT_META))
-$(eval $(call SETUP_LIB_BUILD_MODE,bundler-friendly,1,\
+$(eval $(call SETUP_LIB_BUILD_MODE,sqlite3,bundler-friendly,1,\
$(sqlite3-api-bundler-friendly.mjs),$(sqlite3-bundler-friendly.mjs),\
- $(c-pp.D.esm) -Dtarget=es6-bundler-friendly))
-$(eval $(call SETUP_LIB_BUILD_MODE,node,1,\
+ $(c-pp.D.sqlite3-esm) -Dtarget=es6-bundler-friendly))
+$(eval $(call SETUP_LIB_BUILD_MODE,sqlite3,node,1,\
$(sqlite3-api-node.mjs),$(sqlite3-node.mjs),\
- $(c-pp.D.bundler-friendly) -Dtarget=node))
+ $(c-pp.D.sqlite3-bundler-friendly) -Dtarget=node))
# The various -D... values used by *.c-pp.js include:
#
# -Dtarget=es6-module: for all ESM module builds
#
+# -Dtarget=node: for node.js builds
+#
# -Dtarget=es6-module -Dtarget=es6-bundler-friendly: intended for
# "bundler-friendly" ESM module build. These have some restrictions
# on how URL() objects are constructed in some contexts: URLs which
@@ -841,7 +875,7 @@ clean-batch:
# a regular basis with different -Ox flags and rebuilding the batch
# pieces each time is an unnecessary time sink.
batch: batch-runner.list
-all: batch
+#all: batch
# end batch-runner.js
########################################################################
# Wasmified speedtest1 is our primary benchmarking tool.
@@ -849,7 +883,7 @@ all: batch
# emcc.speedtest1.common = emcc flags used by multiple builds of speedtest1
# emcc.speedtest1 = emcc flags used by main build of speedtest1
emcc.speedtest1.common := $(emcc_opt_full)
-emcc.speedtest1 :=
+emcc.speedtest1 := -I. -I$(dir $(sqlite3.canonical.c))
emcc.speedtest1 += -sENVIRONMENT=web
emcc.speedtest1 += -sALLOW_MEMORY_GROWTH
emcc.speedtest1 += -sINITIAL_MEMORY=$(emcc.INITIAL_MEMORY.$(emcc.INITIAL_MEMORY))
@@ -892,22 +926,24 @@ $(EXPORTED_FUNCTIONS.speedtest1): $(EXPORTED_FUNCTIONS.api.main)
@{ echo _wasm_main; cat $(EXPORTED_FUNCTIONS.api.main); } > $@
speedtest1.js := $(dir.dout)/speedtest1.js
speedtest1.wasm := $(dir.dout)/speedtest1.wasm
-cflags.speedtest1 := $(cflags.common) -DSQLITE_SPEEDTEST1_WASM
-speedtest1.cses := $(speedtest1.c) $(sqlite3-wasm.c)
+emcc.flags.speedtest1-vanilla := $(cflags.common) -DSQLITE_SPEEDTEST1_WASM
+
+speedtest1.cfiles := $(speedtest1.c) $(sqlite3-wasm.c)
$(eval $(call call-make-pre-post,speedtest1,vanilla))
-$(speedtest1.js): $(MAKEFILE) $(speedtest1.cses) \
- $(pre-post-speedtest1.deps.vanilla) \
+$(speedtest1.js): $(MAKEFILE) $(speedtest1.cfiles) \
+ $(pre-post-speedtest1-vanilla.deps) \
$(EXPORTED_FUNCTIONS.speedtest1)
@echo "Building $@ ..."
$(emcc.bin) \
- $(emcc.speedtest1) -I$(dir $(sqlite3.canonical.c)) \
+ $(emcc.speedtest1) \
$(emcc.speedtest1.common) \
- $(cflags.speedtest1) $(pre-post-speedtest1.flags.vanilla) \
+ $(emcc.flags.speedtest1-vanilla) $(pre-post-speedtest1-vanilla.flags) \
$(SQLITE_OPT) \
-USQLITE_C -DSQLITE_C=$(sqlite3.canonical.c) \
$(speedtest1.exit-runtime0) \
- -o $@ $(speedtest1.cses) -lm
+ -o $@ $(speedtest1.cfiles) -lm
$(maybe-wasm-strip) $(speedtest1.wasm)
+ chmod -x $(speedtest1.wasm)
ls -la $@ $(speedtest1.wasm)
speedtest1: $(speedtest1.js)
@@ -933,13 +969,14 @@ CLEAN_FILES += $(speedtest1.js) $(speedtest1.wasm)
#
# To create those, we filter tester1.c-pp.js with $(bin.c-pp)...
$(eval $(call C-PP.FILTER,tester1.c-pp.js,tester1.js))
-$(eval $(call C-PP.FILTER,tester1.c-pp.js,tester1.mjs,$(c-pp.D.esm)))
+$(eval $(call C-PP.FILTER,tester1.c-pp.js,tester1.mjs,$(c-pp.D.sqlite3-esm)))
$(eval $(call C-PP.FILTER,tester1.c-pp.html,tester1.html))
-$(eval $(call C-PP.FILTER,tester1.c-pp.html,tester1-esm.html,$(c-pp.D.esm)))
+$(eval $(call C-PP.FILTER,tester1.c-pp.html,tester1-esm.html,$(c-pp.D.sqlite3-esm)))
tester1: tester1.js tester1.mjs tester1.html tester1-esm.html
# Note that we do not include $(sqlite3-bundler-friendly.mjs) in this
# because bundlers are client-specific.
all quick: tester1
+quick: $(sqlite3.js)
########################################################################
# Convenience rules to rebuild with various -Ox levels. Much
@@ -959,8 +996,6 @@ o1: clean
$(MAKE) -e "emcc_opt=-O1 $(o-xtra)"
o2: clean
$(MAKE) -j2 -e "emcc_opt=-O2 $(o-xtra)"
-qo2: clean
- $(MAKE) -j2 -e "emcc_opt=-O2 $(o-xtra)" quick
o3: clean
$(MAKE) -e "emcc_opt=-O3 $(o-xtra)"
os: clean
@@ -968,8 +1003,6 @@ os: clean
$(MAKE) -e "emcc_opt=-Os $(o-xtra)"
oz: clean
$(MAKE) -j2 -e "emcc_opt=-Oz $(o-xtra)"
-qoz: clean
- $(MAKE) -j2 -e "emcc_opt=-Oz $(o-xtra)" quick
########################################################################
# Sub-makes...
@@ -981,6 +1014,8 @@ include fiddle.make
ifneq (,$(filter wasmfs,$(MAKECMDGOALS)))
wasmfs.enable ?= 1
else
+# Unconditionally enable wasmfs for [dist]clean so that the wasmfs
+# sub-make can clean up.
wasmfs.enable ?= $(if $(filter %clean,$(MAKECMDGOALS)),1,0)
endif
ifeq (1,$(wasmfs.enable))
@@ -1011,8 +1046,7 @@ endif
# Push files to public wasm-testing.sqlite.org server
wasm-testing.include = *.js *.mjs *.html \
./tests \
- batch-runner.list \
- $(dir.dout) $(dir.sql) $(dir.common) $(dir.fiddle) $(dir.jacc)
+ $(dir.dout) $(dir.common) $(dir.fiddle) $(dir.jacc)
wasm-testing.exclude = sql/speedtest1.sql
wasm-testing.dir = /jail/sites/wasm-testing
wasm-testing.dest ?= wasm-testing:$(wasm-testing.dir)
diff --git a/ext/wasm/README.md b/ext/wasm/README.md
index e8d66865d..0c328310d 100644
--- a/ext/wasm/README.md
+++ b/ext/wasm/README.md
@@ -82,7 +82,7 @@ features in the apps which use them.
# Testing on a remote machine that is accessed via SSH
-*NB: The following are developer notes, last validated on 2022-08-18*
+*NB: The following are developer notes, last validated on 2023-07-19*
* Remote: Install git, emsdk, and althttpd
* Use a [version of althttpd][althttpd] from
@@ -93,13 +93,14 @@ features in the apps which use them.
* Local: `ssh -L 8180:localhost:8080 remote`
* Local: Point your web-browser at http://localhost:8180/index.html
-In order to enable [SharedArrayBuffers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer),
-the web-browser requires that the two extra Cross-Origin lines be present
-in HTTP reply headers and that the request must come from "localhost".
-Since the web-server is on a different machine from
-the web-broser, the localhost requirement means that the connection must be tunneled
-using SSH.
+In order to enable [SharedArrayBuffer][], the web-browser requires
+that the two extra Cross-Origin lines be present in HTTP reply headers
+and that the request must come from "localhost" (_or_ over an SSL
+connection). Since the web-server is on a different machine from the
+web-broser, the localhost requirement means that the connection must
+be tunneled using SSH.
[emscripten]: https://emscripten.org
[althttpd]: https://sqlite.org/althttpd
+[SharedArrayBuffer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer
diff --git a/ext/wasm/api/README.md b/ext/wasm/api/README.md
index 6618666ae..eb0f073cf 100644
--- a/ext/wasm/api/README.md
+++ b/ext/wasm/api/README.md
@@ -83,15 +83,18 @@ browser client:
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...
+ is an sqlite3 VFS implementation which supports the Origin-Private
+ FileSystem (OPFS) as a storage layer to provide persistent storage
+ for database files in a browser. It requires...
- **`sqlite3-opfs-async-proxy.js`**\
is the asynchronous backend part of the OPFS proxy. It speaks
directly to the (async) OPFS API and channels those results back
to its synchronous counterpart. This file, because it must be
started in its own Worker, is not part of the amalgamation.
-- **`api/sqlite3-api-cleanup.js`**\
+- **`sqlite3-vfs-opfs-sahpool.c-pp.js`**\
+ is another sqlite3 VFS supporting the OPFS, but uses a completely
+ different approach that the above-listed one.
+- **`sqlite3-api-cleanup.js`**\
The previous files do not immediately extend the library. Instead
they add callback functions to be called during its
bootstrapping. Some also temporarily create global objects in order
@@ -108,13 +111,15 @@ browser client:
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.
+those files contain constructs which may not run as-is in any given
+JavaScript environment.
The build process glues those files together, resulting in
-`sqlite3-api.js`, which is everything except for the `post-js-*.js`
-files, and `sqlite3.js`, which is the Emscripten-generated amalgamated
-output and includes the `post-js-*.js` parts, as well as the
-Emscripten-provided module loading pieces.
+`sqlite3-api.js`, which is everything except for the
+`pre/post-js-*.js` files, and `sqlite3.js`, which is the
+Emscripten-generated amalgamated output and includes the
+`pre/post-js-*.js` parts, as well as the Emscripten-provided module
+loading pieces.
The non-JS outlier file is `sqlite3-wasm.c`: it is a proxy for
`sqlite3.c` which `#include`'s that file and adds a couple more
@@ -152,8 +157,8 @@ 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
+parts which differ between vanilla JS, ES6 Modules, and node.js
+builds. The preprocessor application itself is in
[`c-pp.c`](/file/ext/wasm/c-pp.c) and the complete technical details
of such preprocessing are maintained in
[`GNUMakefile`](/file/ext/wasm/GNUmakefile).
diff --git a/ext/wasm/api/extern-post-js.c-pp.js b/ext/wasm/api/extern-post-js.c-pp.js
index 927bf64f9..63e55051c 100644
--- a/ext/wasm/api/extern-post-js.c-pp.js
+++ b/ext/wasm/api/extern-post-js.c-pp.js
@@ -23,10 +23,7 @@ const toExportForESM =
impls which Emscripten installs at some point in the file above
this.
*/
- 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;
+ const originalInit = sqlite3InitModule;
if(!originalInit){
throw new Error("Expecting globalThis.sqlite3InitModule to be defined by the Emscripten build.");
}
@@ -65,19 +62,19 @@ const toExportForESM =
globalThis.sqlite3InitModule = function ff(...args){
//console.warn("Using replaced sqlite3InitModule()",globalThis.location);
return originalInit(...args).then((EmscriptenModule)=>{
+//#if wasmfs
if('undefined'!==typeof WorkerGlobalScope &&
- (EmscriptenModule['ENVIRONMENT_IS_PTHREAD']
- || EmscriptenModule['_pthread_self']
- || 'function'===typeof threadAlert
- || globalThis?.location?.pathname?.endsWith?.('.worker.js')
- )){
+ EmscriptenModule['ENVIRONMENT_IS_PTHREAD']){
/** Workaround for wasmfs-generated worker, which calls this
routine from each individual thread and requires that its
- argument be returned. All of the criteria above are fragile,
- based solely on inspection of the offending code, not public
- Emscripten details. */
+ argument be returned. The conditional criteria above are
+ fragile, based solely on inspection of the offending code,
+ not public Emscripten details. */
+ //console.warn("sqlite3InitModule() returning E-module.",EmscriptenModule);
return EmscriptenModule;
}
+//#endif
+ //console.warn("sqlite3InitModule() returning sqlite3 object.");
const s = EmscriptenModule.sqlite3;
s.scriptInfo = initModuleState;
//console.warn("sqlite3.scriptInfo =",s.scriptInfo);
@@ -124,5 +121,6 @@ const toExportForESM =
return globalThis.sqlite3InitModule /* required for ESM */;
})();
//#if target=es6-module
-export default toExportForESM;
+sqlite3InitModule = toExportForESM;
+export default sqlite3InitModule;
//#endif
diff --git a/ext/wasm/api/sqlite3-api-cleanup.js b/ext/wasm/api/sqlite3-api-cleanup.js
index d38b401bf..65dbb4eb6 100644
--- a/ext/wasm/api/sqlite3-api-cleanup.js
+++ b/ext/wasm/api/sqlite3-api-cleanup.js
@@ -22,8 +22,10 @@ if('undefined' !== typeof Module){ // presumably an Emscripten build
*/
const SABC = Object.assign(
Object.create(null), {
- exports: Module['asm'],
- memory: Module.wasmMemory /* gets set if built with -sIMPORT_MEMORY */
+ exports: ('undefined'===typeof wasmExports)
+ ? Module['asm']/* emscripten <=3.1.43 */
+ : wasmExports /* emscripten >=3.1.44 */,
+ memory: Module.wasmMemory /* gets set if built with -sIMPORTED_MEMORY */
},
globalThis.sqlite3ApiConfig || {}
);
diff --git a/ext/wasm/api/sqlite3-api-glue.js b/ext/wasm/api/sqlite3-api-glue.js
index f444ec975..60050461c 100644
--- a/ext/wasm/api/sqlite3-api-glue.js
+++ b/ext/wasm/api/sqlite3-api-glue.js
@@ -608,6 +608,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
["sqlite3_wasm_db_vfs", "sqlite3_vfs*", "sqlite3*","string"],
["sqlite3_wasm_vfs_create_file", "int",
"sqlite3_vfs*","string","*", "int"],
+ ["sqlite3_wasm_posix_create_file", "int", "string","*", "int"],
["sqlite3_wasm_vfs_unlink", "int", "sqlite3_vfs*","string"]
];
@@ -728,6 +729,15 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
Populate api object with sqlite3_...() by binding the "raw" wasm
exports into type-converting proxies using wasm.xWrap().
*/
+ if(0 === wasm.exports.sqlite3_step.length){
+ /* This environment wraps exports in nullary functions, which means
+ we must disable the arg-count validation we otherwise perform
+ on the wrappers. */
+ wasm.xWrap.doArgcCheck = false;
+ sqlite3.config.warn(
+ "Disabling sqlite3.wasm.xWrap.doArgcCheck due to environmental quirks."
+ );
+ }
for(const e of wasm.bindingSignatures){
capi[e[0]] = wasm.xWrap.apply(null, e);
}
diff --git a/ext/wasm/api/sqlite3-api-oo1.js b/ext/wasm/api/sqlite3-api-oo1.js
index ac6678c88..4677b8976 100644
--- a/ext/wasm/api/sqlite3-api-oo1.js
+++ b/ext/wasm/api/sqlite3-api-oo1.js
@@ -55,6 +55,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
if(sqliteResultCode){
if(dbPtr instanceof DB) dbPtr = dbPtr.pointer;
toss3(
+ sqliteResultCode,
"sqlite3 result code",sqliteResultCode+":",
(dbPtr
? capi.sqlite3_errmsg(dbPtr)
@@ -330,10 +331,15 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
- `db`: the DB object which created the statement.
- - `columnCount`: the number of result columns in the query, or 0 for
- queries which cannot return results.
+ - `columnCount`: the number of result columns in the query, or 0
+ for queries which cannot return results. This property is a proxy
+ for sqlite3_column_count() and its use in loops should be avoided
+ because of the call overhead associated with that. The
+ `columnCount` is not cached when the Stmt is created because a
+ schema change made via a separate db connection between this
+ statement's preparation and when it is stepped may invalidate it.
- - `parameterCount`: the number of bindable paramters in the query.
+ - `parameterCount`: the number of bindable parameters in the query.
*/
const Stmt = function(){
if(BindTypes!==arguments[2]){
@@ -341,7 +347,6 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
}
this.db = arguments[0];
__ptrMap.set(this, arguments[1]);
- this.columnCount = capi.sqlite3_column_count(this.pointer);
this.parameterCount = capi.sqlite3_bind_parameter_count(this.pointer);
};
@@ -473,7 +478,9 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
const __selectFirstRow = (db, sql, bind, ...getArgs)=>{
const stmt = db.prepare(sql);
try {
- return stmt.bind(bind).step() ? stmt.get(...getArgs) : undefined;
+ const rc = stmt.bind(bind).step() ? stmt.get(...getArgs) : undefined;
+ stmt.reset(/*for INSERT...RETURNING locking case*/);
+ return rc;
}finally{
stmt.finalize();
}
@@ -499,6 +506,9 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
"Not an error." The various non-0 non-error codes need to be
checked for in client code where they are expected.
+ The thrown exception's `resultCode` property will be the value of
+ the second argument to this function.
+
If it does not throw, it returns its first argument.
*/
DB.checkRc = (db,resultCode)=>checkSqlite3Rc(db,resultCode);
@@ -546,7 +556,10 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
}
const pDb = this.pointer;
Object.keys(__stmtMap.get(this)).forEach((k,s)=>{
- if(s && s.pointer) s.finalize();
+ if(s && s.pointer){
+ try{s.finalize()}
+ catch(e){/*ignore*/}
+ }
});
__ptrMap.delete(this);
__stmtMap.delete(this);
@@ -701,18 +714,18 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
with identical names.
- `callback` = a function which gets called for each row of the
- result set, but only if that statement has any result
- _rows_. The callback's "this" is the options object, noting
- that this function synthesizes one if the caller does not pass
- one to exec(). The second argument passed to the callback is
- always the current Stmt object, as it's needed if the caller
- wants to fetch the column names or some such (noting that they
- could also be fetched via `this.columnNames`, if the client
- provides the `columnNames` option). If the callback returns a
- literal `false` (as opposed to any other falsy value, e.g. an
- implicit `undefined` return), any ongoing statement-`step()`
- iteration stops without an error. The return value of the
- callback is otherwise ignored.
+ result set, but only if that statement has any result rows. The
+ callback's "this" is the options object, noting that this
+ function synthesizes one if the caller does not pass one to
+ exec(). The second argument passed to the callback is always
+ the current Stmt object, as it's needed if the caller wants to
+ fetch the column names or some such (noting that they could
+ also be fetched via `this.columnNames`, if the client provides
+ the `columnNames` option). If the callback returns a literal
+ `false` (as opposed to any other falsy value, e.g. an implicit
+ `undefined` return), any ongoing statement-`step()` iteration
+ stops without an error. The return value of the callback is
+ otherwise ignored.
ACHTUNG: The callback MUST NOT modify the Stmt object. Calling
any of the Stmt.get() variants, Stmt.getColumnName(), or
@@ -733,7 +746,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
A.1) `'array'` (the default) causes the results of
`stmt.get([])` to be passed to the `callback` and/or appended
- to `resultRows`
+ to `resultRows`.
A.2) `'object'` causes the results of
`stmt.get(Object.create(null))` to be passed to the
@@ -744,8 +757,8 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
A.3) `'stmt'` causes the current Stmt to be passed to the
callback, but this mode will trigger an exception if
- `resultRows` is an array because appending the statement to
- the array would be downright unhelpful.
+ `resultRows` is an array because appending the transient
+ statement to the array would be downright unhelpful.
B) An integer, indicating a zero-based column in the result
row. Only that one single value will be passed on.
@@ -775,7 +788,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
should return:
A) The default value is (usually) `"this"`, meaning that the
- DB object itself should be returned. The exceptions is if
+ DB object itself should be returned. The exception is if
the caller passes neither of `callback` nor `returnValue`
but does pass an explicit `rowMode` then the default
`returnValue` is `"resultRows"`, described below.
@@ -857,38 +870,53 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
bind = null;
}
if(evalFirstResult && stmt.columnCount){
- /* Only forward SELECT results for the FIRST query
+ /* Only forward SELECT-style results for the FIRST query
in the SQL which potentially has them. */
+ let gotColNames = Array.isArray(
+ opt.columnNames
+ /* As reported in
+ https://sqlite.org/forum/forumpost/7774b773937cbe0a
+ we need to delay fetching of the column names until
+ after the first step() (if we step() at all) because
+ a schema change between the prepare() and step(), via
+ another connection, may invalidate the column count
+ and names. */) ? 0 : 1;
evalFirstResult = false;
- if(Array.isArray(opt.columnNames)){
- stmt.getColumnNames(opt.columnNames);
- }
if(arg.cbArg || resultRows){
- for(; stmt.step(); stmt._isLocked = false){
- stmt._isLocked = true;
+ for(; stmt.step(); stmt._lockedByExec = false){
+ if(0===gotColNames++) stmt.getColumnNames(opt.columnNames);
+ stmt._lockedByExec = true;
const row = arg.cbArg(stmt);
if(resultRows) resultRows.push(row);
if(callback && false === callback.call(opt, row, stmt)){
break;
}
}
- stmt._isLocked = false;
+ stmt._lockedByExec = false;
+ }
+ if(0===gotColNames){
+ /* opt.columnNames was provided but we visited no result rows */
+ stmt.getColumnNames(opt.columnNames);
}
}else{
stmt.step();
}
- stmt.finalize();
+ stmt.reset(
+ /* In order to trigger an exception in the
+ INSERT...RETURNING locking scenario:
+ https://sqlite.org/forum/forumpost/36f7a2e7494897df
+ */).finalize();
stmt = null;
- }
+ }/*prepare() loop*/
}/*catch(e){
sqlite3.config.warn("DB.exec() is propagating exception",opt,e);
throw e;
}*/finally{
+ wasm.scopedAllocPop(stack);
if(stmt){
- delete stmt._isLocked;
+ delete stmt._lockedByExec;
stmt.finalize();
}
- wasm.scopedAllocPop(stack);
}
return arg.returnVal();
}/*exec()*/,
@@ -1107,6 +1135,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
try {
stmt.bind(bind);
while(stmt.step()) rc.push(stmt.get(0,asType));
+ stmt.reset(/*for INSERT...RETURNING locking case*/);
}finally{
stmt.finalize();
}
@@ -1241,7 +1270,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
not throw, it returns this object.
*/
checkRc: function(resultCode){
- return DB.checkRc(this, resultCode);
+ return checkSqlite3Rc(this, resultCode);
}
}/*DB.prototype*/;
@@ -1302,15 +1331,15 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
};
/**
- If stmt._isLocked is truthy, this throws an exception
+ If stmt._lockedByExec is truthy, this throws an exception
complaining that the 2nd argument (an operation name,
e.g. "bind()") is not legal while the statement is "locked".
Locking happens before an exec()-like callback is passed a
statement, to ensure that the callback does not mutate or
finalize the statement. If it does not throw, it returns stmt.
*/
- const affirmUnlocked = function(stmt,currentOpName){
- if(stmt._isLocked){
+ const affirmNotLockedByExec = function(stmt,currentOpName){
+ if(stmt._lockedByExec){
toss3("Operation is illegal when statement is locked:",currentOpName);
}
return stmt;
@@ -1323,14 +1352,11 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
success.
*/
const bindOne = function f(stmt,ndx,bindType,val){
- affirmUnlocked(affirmStmtOpen(stmt), 'bind()');
+ affirmNotLockedByExec(affirmStmtOpen(stmt), 'bind()');
if(!f._){
f._tooBigInt = (v)=>toss3(
"BigInt value is too big to store without precision loss:", v
);
- /* Reminder: when not in BigInt mode, it's impossible for
- JS to represent a number out of the range we can bind,
- so we have no range checking. */
f._ = {
string: function(stmt, ndx, val, asBlob){
const [pStr, n] = wasm.allocCString(val, true);
@@ -1404,46 +1430,67 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
Stmt.prototype = {
/**
- "Finalizes" this statement. This is a no-op if the
- statement has already been finalizes. Returns
- undefined. Most methods in this class will throw if called
- after this is.
+ "Finalizes" this statement. This is a no-op if the statement
+ has already been finalized. Returns the result of
+ sqlite3_finalize() (0 on success, non-0 on error), or the
+ undefined value if the statement has already been
+ finalized. Regardless of success or failure, most methods in
+ this class will throw if called after this is.
+
+ This method always throws if called when it is illegal to do
+ so. Namely, when triggered via a per-row callback handler of a
+ DB.exec() call.
*/
finalize: function(){
if(this.pointer){
- affirmUnlocked(this,'finalize()');
+ affirmNotLockedByExec(this,'finalize()');
+ const rc = capi.sqlite3_finalize(this.pointer);
delete __stmtMap.get(this.db)[this.pointer];
- capi.sqlite3_finalize(this.pointer);
__ptrMap.delete(this);
delete this._mayGet;
- delete this.columnCount;
delete this.parameterCount;
+ delete this._lockedByExec;
delete this.db;
- delete this._isLocked;
+ return rc;
}
},
- /** Clears all bound values. Returns this object.
- Throws if this statement has been finalized. */
+ /**
+ Clears all bound values. Returns this object. Throws if this
+ statement has been finalized or if modification of the
+ statement is currently illegal (e.g. in the per-row callback of
+ a DB.exec() call).
+ */
clearBindings: function(){
- affirmUnlocked(affirmStmtOpen(this), 'clearBindings()')
+ affirmNotLockedByExec(affirmStmtOpen(this), 'clearBindings()')
capi.sqlite3_clear_bindings(this.pointer);
this._mayGet = false;
return this;
},
/**
- Resets this statement so that it may be step()ed again
- from the beginning. Returns this object. Throws if this
- statement has been finalized.
+ Resets this statement so that it may be step()ed again from the
+ beginning. Returns this object. Throws if this statement has
+ been finalized, if it may not legally be reset because it is
+ currently being used from a DB.exec() callback, or if the
+ underlying call to sqlite3_reset() returns non-0.
If passed a truthy argument then this.clearBindings() is
also called, otherwise any existing bindings, along with
any memory allocated for them, are retained.
+
+ In versions 3.42.0 and earlier, this function did not throw if
+ sqlite3_reset() returns non-0, but it was discovered that
+ throwing (or significant extra client-side code) is necessary
+ in order to avoid certain silent failure scenarios, as
+ discussed at:
+
+ https://sqlite.org/forum/forumpost/36f7a2e7494897df
*/
reset: function(alsoClearBinds){
- affirmUnlocked(this,'reset()');
+ affirmNotLockedByExec(this,'reset()');
if(alsoClearBinds) this.clearBindings();
- capi.sqlite3_reset(affirmStmtOpen(this).pointer);
+ const rc = capi.sqlite3_reset(affirmStmtOpen(this).pointer);
this._mayGet = false;
+ checkSqlite3Rc(this.db, rc);
return this;
},
/**
@@ -1592,7 +1639,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
value is returned. Throws on error.
*/
step: function(){
- affirmUnlocked(this, 'step()');
+ affirmNotLockedByExec(this, 'step()');
const rc = capi.sqlite3_step(affirmStmtOpen(this).pointer);
switch(rc){
case capi.SQLITE_DONE: return this._mayGet = false;
@@ -1627,11 +1674,9 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
return this.reset();
},
/**
- Functions like step() except that it finalizes this statement
- immediately after stepping unless the step cannot be performed
- because the statement is locked. Throws on error, but any error
- other than the statement-is-locked case will also trigger
- finalization of this statement.
+ Functions like step() except that it calls finalize() on this
+ statement immediately after stepping, even if the step() call
+ throws.
On success, it returns true if the step indicated that a row of
data was available, else it returns false.
@@ -1643,9 +1688,14 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
```
*/
stepFinalize: function(){
- const rc = this.step();
- this.finalize();
- return rc;
+ try{
+ const rc = this.step();
+ this.reset(/*for INSERT...RETURNING locking case*/);
+ return rc;
+ }finally{
+ try{this.finalize()}
+ catch(e){/*ignored*/}
+ }
},
/**
Fetches the value from the given 0-based column index of
@@ -1686,13 +1736,15 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
}
if(Array.isArray(ndx)){
let i = 0;
- while(itoss3("The columnCount property is read-only.")
+ });
/** The OO API's public namespace. */
sqlite3.oo1 = {
diff --git a/ext/wasm/api/sqlite3-api-prologue.js b/ext/wasm/api/sqlite3-api-prologue.js
index c882d5b24..ca5d1c44f 100644
--- a/ext/wasm/api/sqlite3-api-prologue.js
+++ b/ext/wasm/api/sqlite3-api-prologue.js
@@ -53,7 +53,7 @@
- `memory`[^1]: optional WebAssembly.Memory object, defaulting to
`exports.memory`. In Emscripten environments this should be set
- to `Module.wasmMemory` if the build uses `-sIMPORT_MEMORY`, or be
+ to `Module.wasmMemory` if the build uses `-sIMPORTED_MEMORY`, or be
left undefined/falsy to default to `exports.memory` when using
WASM-exported memory.
@@ -88,12 +88,12 @@
can be replaced with (e.g.) empty functions to squelch all such
output.
- - `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.
+ - `wasmfsOpfsDir`[^1]: Specifies the "mount point" of the OPFS-backed
+ filesystem in WASMFS-capable builds.
- [^1] = This property may optionally be a function, in which case this
- function re-assigns calls that function to fetch the value,
+
+ [^1] = This property may optionally be a function, in which case
+ this function calls that function to fetch the value,
enabling delayed evaluation.
The returned object is the top-level sqlite3 namespace object.
@@ -125,11 +125,11 @@ globalThis.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
log: console.log.bind(console),
wasmfsOpfsDir: '/opfs',
/**
- useStdAlloc is just for testing an allocator discrepancy. The
+ useStdAlloc is just for testing allocator discrepancies. The
docs guarantee that this is false in the canonical builds. For
99% of purposes it doesn't matter which allocators we use, but
- it becomes significant with, e.g., sqlite3_deserialize()
- and certain wasm.xWrap.resultAdapter()s.
+ it becomes significant with, e.g., sqlite3_deserialize() and
+ certain wasm.xWrap.resultAdapter()s.
*/
useStdAlloc: false
}, apiConfig || {});
@@ -149,11 +149,6 @@ globalThis.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,
mimicking the C API as closely as we can. The numerous members
@@ -809,7 +804,7 @@ globalThis.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
|| toss3("Missing API config.exports (WASM module exports)."),
/**
- When Emscripten compiles with `-sIMPORT_MEMORY`, it
+ When Emscripten compiles with `-sIMPORTED_MEMORY`, it
initalizes the heap and imports it into wasm, as opposed to
the other way around. In this case, the memory is not
available via this.exports.memory.
@@ -1177,31 +1172,31 @@ globalThis.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).
The first time this is called, this function inspects the current
environment to determine whether persistence support is available
- and, if it is, enables it (if needed).
+ and, if it is, enables it (if needed). After the first call it
+ always returns the cached result.
+
+ If the returned string is not empty, any files stored under the
+ given path (recursively) are housed in OPFS storage. If the
+ returned string is empty, this particular persistent storage
+ option is not available on the client.
+
+ Though the mount point name returned by this function is intended
+ to remain stable, clients should not hard-coded it anywhere. Always call this function to get the path.
- This function currently only recognizes the WASMFS/OPFS storage
- combination and its path refers to storage rooted in the
- Emscripten-managed virtual filesystem.
+ Note that this function is a no-op in must builds of this
+ library, as the WASMFS capability requires a custom
+ build.
*/
capi.sqlite3_wasmfs_opfs_dir = function(){
if(undefined !== __wasmfsOpfsDir) return __wasmfsOpfsDir;
// If we have no OPFS, there is no persistent dir
const pdir = config.wasmfsOpfsDir;
- console.error("sqlite3_wasmfs_opfs_dir() can no longer work due "+
- "to incompatible WASMFS changes. It will be removed.");
if(!pdir
|| !globalThis.FileSystemHandle
|| !globalThis.FileSystemDirectoryHandle
@@ -1223,8 +1218,6 @@ globalThis.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
};
/**
- Experimental and subject to change or removal.
-
Returns true if sqlite3.capi.sqlite3_wasmfs_opfs_dir() is a
non-empty string and the given name starts with (that string +
'/'), else returns false.
@@ -1234,13 +1227,6 @@ globalThis.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
return (p && name) ? name.startsWith(p+'/') : false;
};
- // This bit is highly arguable and is incompatible with the fiddle shell.
- if(false && 0===wasm.exports.sqlite3_vfs_find(0)){
- /* Assume that sqlite3_initialize() has not yet been called.
- This will be the case in an SQLITE_OS_KV build. */
- wasm.exports.sqlite3_initialize();
- }
-
/**
Given an `sqlite3*`, an sqlite3_vfs name, and an optional db name
(defaulting to "main"), returns a truthy value (see below) if
@@ -1371,6 +1357,74 @@ globalThis.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
};
/**
+ If the current environment supports the POSIX file APIs, this routine
+ creates (or overwrites) the given file using those APIs. This is
+ primarily intended for use in Emscripten-based builds where the POSIX
+ APIs are transparently proxied by an in-memory virtual filesystem.
+ It may behave diffrently in other environments.
+
+ The first argument must be either a JS string or WASM C-string
+ holding the filename. Note that this routine does _not_ create
+ intermediary directories if the filename has a directory part.
+
+ The 2nd argument may either a valid WASM memory pointer, an
+ ArrayBuffer, or a Uint8Array. The 3rd must be the length, in
+ bytes, of the data array to copy. If the 2nd argument is an
+ ArrayBuffer or Uint8Array and the 3rd is not a positive integer
+ then the 3rd defaults to the array's byteLength value.
+
+ Results are undefined if data is a WASM pointer and dataLen is
+ exceeds data's bounds.
+
+ Throws if any arguments are invalid or if creating or writing to
+ the file fails.
+
+ Added in 3.43 as an alternative for the deprecated
+ sqlite3_js_vfs_create_file().
+ */
+ capi.sqlite3_js_posix_create_file = function(filename, data, dataLen){
+ let pData;
+ if(data && wasm.isPtr(data)){
+ pData = data;
+ }else if(data instanceof ArrayBuffer || data instanceof Uint8Array){
+ pData = wasm.allocFromTypedArray(data);
+ if(arguments.length<3 || !util.isInt32(dataLen) || dataLen<0){
+ dataLen = data.byteLength;
+ }
+ }else{
+ SQLite3Error.toss("Invalid 2nd argument for sqlite3_js_posix_create_file().");
+ }
+ try{
+ if(!util.isInt32(dataLen) || dataLen<0){
+ SQLite3Error.toss("Invalid 3rd argument for sqlite3_js_posix_create_file().");
+ }
+ const rc = wasm.sqlite3_wasm_posix_create_file(filename, pData, dataLen);
+ if(rc) SQLite3Error.toss("Creation of file failed with sqlite3 result code",
+ capi.sqlite3_js_rc_str(rc));
+ }finally{
+ wasm.dealloc(pData);
+ }
+ };
+
+ /**
+ Deprecation warning: this function does not work properly in
+ debug builds of sqlite3 because its out-of-scope use of the
+ sqlite3_vfs API triggers assertions in the core library. That
+ was unfortunately not discovered until 2023-08-11. This function
+ is now deprecated and should not be used in new code.
+
+ Alternative options:
+
+ - "unix" VFS and its variants can get equivalent functionality
+ with sqlite3_js_posix_create_file().
+
+ - OPFS: use either sqlite3.oo1.OpfsDb.importDb(), for the "opfs"
+ VFS, or the importDb() method of the PoolUtil object provided
+ by the "opfs-sahpool" OPFS (noting that its VFS name may differ
+ depending on client-side configuration). We cannot proxy those
+ from here because the former is necessarily asynchronous and
+ the latter requires information not available to this function.
+
Creates a file using the storage appropriate for the given
sqlite3_vfs. The first argument may be a VFS name (JS string
only, NOT a WASM C-string), WASM-managed `sqlite3_vfs*`, or
@@ -1416,9 +1470,13 @@ globalThis.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
VFS nor the WASM environment imposes requirements which break it.
- "opfs": uses OPFS storage and creates directory parts of the
- filename.
+ filename. It can only be used to import an SQLite3 database
+ file and will fail if given anything else.
*/
capi.sqlite3_js_vfs_create_file = function(vfs, filename, data, dataLen){
+ config.warn("sqlite3_js_vfs_create_file() is deprecated and",
+ "should be avoided because it can lead to C-level crashes.",
+ "See its documentation for alternative options.");
let pData;
if(data){
if(wasm.isPtr(data)){
@@ -1446,7 +1504,7 @@ globalThis.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
if(rc) SQLite3Error.toss("Creation of file failed with sqlite3 result code",
capi.sqlite3_js_rc_str(rc));
}finally{
- wasm.dealloc(pData);
+ wasm.dealloc(pData);
}
};
@@ -1659,7 +1717,8 @@ globalThis.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
do not.
*/
tgt.push(capi.sqlite3_value_to_js(
- wasm.peekPtr(pArgv + (wasm.ptrSizeof * i))
+ wasm.peekPtr(pArgv + (wasm.ptrSizeof * i)),
+ throwIfCannotConvert
));
}
return tgt;
@@ -1875,6 +1934,9 @@ globalThis.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
client: undefined,
/**
+ This function is not part of the public interface, but a
+ piece of internal bootstrapping infrastructure.
+
Performs any optional asynchronous library-level initialization
which might be required. This function returns a Promise which
resolves to the sqlite3 namespace object. Any error in the
@@ -1890,27 +1952,19 @@ globalThis.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
then it must be called by client-level code, which must not use
the library until the returned promise resolves.
- Bug: if called while a prior call is still resolving, the 2nd
- call will resolve prematurely, before the 1st call has finished
- resolving. The current build setup precludes that possibility,
- so it's only a hypothetical problem if/when this function
- ever needs to be invoked by clients.
+ If called multiple times it will return the same promise on
+ subsequent calls. The current build setup precludes that
+ possibility, so it's only a hypothetical problem if/when this
+ function ever needs to be invoked by clients.
In Emscripten-based builds, this function is called
automatically and deleted from this object.
*/
- asyncPostInit: async function(){
- let lip = sqlite3ApiBootstrap.initializersAsync;
+ asyncPostInit: async function ff(){
+ if(ff.isReady instanceof Promise) return ff.isReady;
+ let lia = sqlite3ApiBootstrap.initializersAsync;
delete sqlite3ApiBootstrap.initializersAsync;
- if(!lip || !lip.length) return Promise.resolve(sqlite3);
- lip = lip.map((f)=>{
- const p = (f instanceof Promise) ? f : f(sqlite3);
- return p.catch((e)=>{
- console.error("an async sqlite3 initializer failed:",e);
- throw e;
- });
- });
- const postInit = ()=>{
+ const postInit = async ()=>{
if(!sqlite3.__isUnderTest){
/* Delete references to internal-only APIs which are used by
some initializers. Retain them when running in test mode
@@ -1919,23 +1973,25 @@ globalThis.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
/* 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. */
+ own 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);
+ const catcher = (e)=>{
+ config.error("an async sqlite3 initializer failed:",e);
+ throw e;
+ };
+ if(!lia || !lia.length){
+ return ff.isReady = postInit().catch(catcher);
}
+ lia = lia.map((f)=>{
+ return (f instanceof Function) ? async x=>f(sqlite3) : f;
+ });
+ lia.push(postInit);
+ let p = Promise.resolve(sqlite3);
+ while(lia.length) p = p.then(lia.shift());
+ return ff.isReady = p.catch(catcher);
},
/**
scriptInfo ideally gets injected into this object by the
@@ -1995,7 +2051,7 @@ globalThis.sqlite3ApiBootstrap.initializers = [];
specifically for initializers which are asynchronous. All entries in
this list must be either async functions, non-async functions which
return a Promise, or a Promise. Each function in the list is called
- with the sqlite3 ojbect as its only argument.
+ with the sqlite3 object as its only argument.
The resolved value of any Promise is ignored and rejection will kill
the asyncPostInit() process (at an indeterminate point because all
diff --git a/ext/wasm/api/sqlite3-api-worker1.js b/ext/wasm/api/sqlite3-api-worker1.js
index d1c63c96e..9a386c13e 100644
--- a/ext/wasm/api/sqlite3-api-worker1.js
+++ b/ext/wasm/api/sqlite3-api-worker1.js
@@ -278,6 +278,19 @@
The arguments are in the same form accepted by oo1.DB.exec(), with
the exceptions noted below.
+ If the `countChanges` arguments property (added in version 3.43) is
+ truthy then the `result` property contained by the returned object
+ will have a `changeCount` property which holds the number of changes
+ made by the provided SQL. Because the SQL may contain an arbitrary
+ number of statements, the `changeCount` is calculated by calling
+ `sqlite3_total_changes()` before and after the SQL is evaluated. If
+ the value of `countChanges` is 64 then the `changeCount` property
+ will be returned as a 64-bit integer in the form of a BigInt (noting
+ that that will trigger an exception if used in a BigInt-incapable
+ build). In the latter case, the number of changes is calculated by
+ calling `sqlite3_total_changes64()` before and after the SQL is
+ evaluated.
+
A function-type args.callback property cannot cross
the window/Worker boundary, so is not useful here. If
args.callback is a string then it is assumed to be a
@@ -523,7 +536,13 @@ sqlite3.initWorker1API = function(){
}
}
try {
+ const changeCount = !!rc.countChanges
+ ? db.changes(true,(64===rc.countChanges))
+ : undefined;
db.exec(rc);
+ if(undefined !== changeCount){
+ rc.changeCount = db.changes(true,64===rc.countChanges) - changeCount;
+ }
if(rc.callback instanceof Function){
rc.callback = theCallback;
/* Post a sentinel message to tell the client that the end
diff --git a/ext/wasm/api/sqlite3-opfs-async-proxy.js b/ext/wasm/api/sqlite3-opfs-async-proxy.js
index ddcad8f61..cafd296c6 100644
--- a/ext/wasm/api/sqlite3-opfs-async-proxy.js
+++ b/ext/wasm/api/sqlite3-opfs-async-proxy.js
@@ -35,6 +35,9 @@
https://developer.chrome.com/blog/sync-methods-for-accesshandles/
+ Firefox v111 and Safari 16.4, both released in March 2023, also
+ include this.
+
We cannot change to the sync forms at this point without breaking
clients who use Chrome v104-ish or higher. truncate(), getSize(),
flush(), and close() are now (as of v108) synchronous. Calling them
@@ -818,9 +821,24 @@ const installAsyncProxy = function(self){
}
while(!flagAsyncShutdown){
try {
- if('timed-out'===Atomics.wait(
+ if('not-equal'!==Atomics.wait(
state.sabOPView, state.opIds.whichOp, 0, state.asyncIdleWaitTime
)){
+ /* Maintenance note: we compare against 'not-equal' because
+
+ https://github.com/tomayac/sqlite-wasm/issues/12
+
+ is reporting that this occassionally, under high loads,
+ returns 'ok', which leads to the whichOp being 0 (which
+ isn't a valid operation ID and leads to an exception,
+ along with a corresponding ugly console log
+ message). Unfortunately, the conditions for that cannot
+ be reliably reproduced. The only place in our code which
+ writes a 0 to the state.opIds.whichOp SharedArrayBuffer
+ index is a few lines down from here, and that instance
+ is required in order for clear communication between
+ the sync half of this proxy and this half.
+ */
await releaseImplicitLocks();
continue;
}
diff --git a/ext/wasm/api/sqlite3-v-helper.js b/ext/wasm/api/sqlite3-v-helper.js
index 80ab7c5b0..e63da8afc 100644
--- a/ext/wasm/api/sqlite3-v-helper.js
+++ b/ext/wasm/api/sqlite3-v-helper.js
@@ -100,7 +100,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
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
+ 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...
@@ -295,7 +295,8 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
- If `struct.$zName` is falsy and the entry has a string-type
`name` property, `struct.$zName` is set to the C-string form of
- that `name` value before registerVfs() is called.
+ that `name` value before registerVfs() is called. That string
+ gets added to the on-dispose state of the struct.
On success returns this object. Throws on error.
*/
@@ -608,7 +609,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
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.
+ one.
The `catchExceptions`-installed handlers will account for
identical references to the above functions and will install the
diff --git a/ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js b/ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js
new file mode 100644
index 000000000..709d3414c
--- /dev/null
+++ b/ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js
@@ -0,0 +1,1230 @@
+//#ifnot target=node
+/*
+ 2023-07-14
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ This file holds a sqlite3_vfs backed by OPFS storage which uses a
+ different implementation strategy than the "opfs" VFS. This one is a
+ port of Roy Hashimoto's OPFS SyncAccessHandle pool:
+
+ https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/AccessHandlePoolVFS.js
+
+ As described at:
+
+ https://github.com/rhashimoto/wa-sqlite/discussions/67
+
+ with Roy's explicit permission to permit us to port his to our
+ infrastructure rather than having to clean-room reverse-engineer it:
+
+ https://sqlite.org/forum/forumpost/e140d84e71
+
+ Primary differences from the "opfs" VFS include:
+
+ - This one avoids the need for a sub-worker to synchronize
+ communication between the synchronous C API and the
+ only-partly-synchronous OPFS API.
+
+ - It does so by opening a fixed number of OPFS files at
+ library-level initialization time, obtaining SyncAccessHandles to
+ each, and manipulating those handles via the synchronous sqlite3_vfs
+ interface. If it cannot open them (e.g. they are already opened by
+ another tab) then the VFS will not be installed.
+
+ - Because of that, this one lacks all library-level concurrency
+ support.
+
+ - Also because of that, it does not require the SharedArrayBuffer,
+ so can function without the COOP/COEP HTTP response headers.
+
+ - It can hypothetically support Safari 16.4+, whereas the "opfs" VFS
+ requires v17 due to a subworker/storage bug in 16.x which makes it
+ incompatible with that VFS.
+
+ - This VFS requires the "semi-fully-sync" FileSystemSyncAccessHandle
+ (hereafter "SAH") APIs released with Chrome v108 (and all other
+ major browsers released since March 2023). If that API is not
+ detected, the VFS is not registered.
+*/
+globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
+ 'use strict';
+ const toss = sqlite3.util.toss;
+ const toss3 = sqlite3.util.toss3;
+ const initPromises = Object.create(null);
+ const capi = sqlite3.capi;
+ const wasm = sqlite3.wasm;
+ // Config opts for the VFS...
+ const SECTOR_SIZE = 4096;
+ const HEADER_MAX_PATH_SIZE = 512;
+ const HEADER_FLAGS_SIZE = 4;
+ const HEADER_DIGEST_SIZE = 8;
+ const HEADER_CORPUS_SIZE = HEADER_MAX_PATH_SIZE + HEADER_FLAGS_SIZE;
+ const HEADER_OFFSET_FLAGS = HEADER_MAX_PATH_SIZE;
+ const HEADER_OFFSET_DIGEST = HEADER_CORPUS_SIZE;
+ const HEADER_OFFSET_DATA = SECTOR_SIZE;
+ /* Bitmask of file types which may persist across sessions.
+ SQLITE_OPEN_xyz types not listed here may be inadvertently
+ left in OPFS but are treated as transient by this VFS and
+ they will be cleaned up during VFS init. */
+ const PERSISTENT_FILE_TYPES =
+ capi.SQLITE_OPEN_MAIN_DB |
+ capi.SQLITE_OPEN_MAIN_JOURNAL |
+ capi.SQLITE_OPEN_SUPER_JOURNAL |
+ capi.SQLITE_OPEN_WAL /* noting that WAL support is
+ unavailable in the WASM build.*/;
+
+ /** Subdirectory of the VFS's space where "opaque" (randomly-named)
+ files are stored. Changing this effectively invalidates the data
+ stored under older names (orphaning it), so don't do that. */
+ const OPAQUE_DIR_NAME = ".opaque";
+
+ /**
+ Returns short a string of random alphanumeric characters
+ suitable for use as a random filename.
+ */
+ const getRandomName = ()=>Math.random().toString(36).slice(2);
+
+ const textDecoder = new TextDecoder();
+ const textEncoder = new TextEncoder();
+
+ const optionDefaults = Object.assign(Object.create(null),{
+ name: 'opfs-sahpool',
+ directory: undefined /* derived from .name */,
+ initialCapacity: 6,
+ clearOnInit: false,
+ /* Logging verbosity 3+ == everything, 2 == warnings+errors, 1 ==
+ errors only. */
+ verbosity: 2
+ });
+
+ /** Logging routines, from most to least serious. */
+ const loggers = [
+ sqlite3.config.error,
+ sqlite3.config.warn,
+ sqlite3.config.log
+ ];
+ const log = sqlite3.config.log;
+ const warn = sqlite3.config.warn;
+ const error = sqlite3.config.error;
+
+ /* Maps (sqlite3_vfs*) to OpfsSAHPool instances */
+ const __mapVfsToPool = new Map();
+ const getPoolForVfs = (pVfs)=>__mapVfsToPool.get(pVfs);
+ const setPoolForVfs = (pVfs,pool)=>{
+ if(pool) __mapVfsToPool.set(pVfs, pool);
+ else __mapVfsToPool.delete(pVfs);
+ };
+ /* Maps (sqlite3_file*) to OpfsSAHPool instances */
+ const __mapSqlite3File = new Map();
+ const getPoolForPFile = (pFile)=>__mapSqlite3File.get(pFile);
+ const setPoolForPFile = (pFile,pool)=>{
+ if(pool) __mapSqlite3File.set(pFile, pool);
+ else __mapSqlite3File.delete(pFile);
+ };
+
+ /**
+ Impls for the sqlite3_io_methods methods. Maintenance reminder:
+ members are in alphabetical order to simplify finding them.
+ */
+ const ioMethods = {
+ xCheckReservedLock: function(pFile,pOut){
+ const pool = getPoolForPFile(pFile);
+ pool.log('xCheckReservedLock');
+ pool.storeErr();
+ wasm.poke32(pOut, 1);
+ return 0;
+ },
+ xClose: function(pFile){
+ const pool = getPoolForPFile(pFile);
+ pool.storeErr();
+ const file = pool.getOFileForS3File(pFile);
+ if(file) {
+ try{
+ pool.log(`xClose ${file.path}`);
+ pool.mapS3FileToOFile(pFile, false);
+ file.sah.flush();
+ if(file.flags & capi.SQLITE_OPEN_DELETEONCLOSE){
+ pool.deletePath(file.path);
+ }
+ }catch(e){
+ pool.storeErr(e);
+ return capi.SQLITE_IOERR;
+ }
+ }
+ return 0;
+ },
+ xDeviceCharacteristics: function(pFile){
+ return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
+ },
+ xFileControl: function(pFile, opId, pArg){
+ return capi.SQLITE_NOTFOUND;
+ },
+ xFileSize: function(pFile,pSz64){
+ const pool = getPoolForPFile(pFile);
+ pool.log(`xFileSize`);
+ const file = pool.getOFileForS3File(pFile);
+ const size = file.sah.getSize() - HEADER_OFFSET_DATA;
+ //log(`xFileSize ${file.path} ${size}`);
+ wasm.poke64(pSz64, BigInt(size));
+ return 0;
+ },
+ xLock: function(pFile,lockType){
+ const pool = getPoolForPFile(pFile);
+ pool.log(`xLock ${lockType}`);
+ pool.storeErr();
+ const file = pool.getOFileForS3File(pFile);
+ file.lockType = lockType;
+ return 0;
+ },
+ xRead: function(pFile,pDest,n,offset64){
+ const pool = getPoolForPFile(pFile);
+ pool.storeErr();
+ const file = pool.getOFileForS3File(pFile);
+ pool.log(`xRead ${file.path} ${n} @ ${offset64}`);
+ try {
+ const nRead = file.sah.read(
+ wasm.heap8u().subarray(pDest, pDest+n),
+ {at: HEADER_OFFSET_DATA + Number(offset64)}
+ );
+ if(nRead < n){
+ wasm.heap8u().fill(0, pDest + nRead, pDest + n);
+ return capi.SQLITE_IOERR_SHORT_READ;
+ }
+ return 0;
+ }catch(e){
+ pool.storeErr(e);
+ return capi.SQLITE_IOERR;
+ }
+ },
+ xSectorSize: function(pFile){
+ return SECTOR_SIZE;
+ },
+ xSync: function(pFile,flags){
+ const pool = getPoolForPFile(pFile);
+ pool.log(`xSync ${flags}`);
+ pool.storeErr();
+ const file = pool.getOFileForS3File(pFile);
+ //log(`xSync ${file.path} ${flags}`);
+ try{
+ file.sah.flush();
+ return 0;
+ }catch(e){
+ pool.storeErr(e);
+ return capi.SQLITE_IOERR;
+ }
+ },
+ xTruncate: function(pFile,sz64){
+ const pool = getPoolForPFile(pFile);
+ pool.log(`xTruncate ${sz64}`);
+ pool.storeErr();
+ const file = pool.getOFileForS3File(pFile);
+ //log(`xTruncate ${file.path} ${iSize}`);
+ try{
+ file.sah.truncate(HEADER_OFFSET_DATA + Number(sz64));
+ return 0;
+ }catch(e){
+ pool.storeErr(e);
+ return capi.SQLITE_IOERR;
+ }
+ },
+ xUnlock: function(pFile,lockType){
+ const pool = getPoolForPFile(pFile);
+ pool.log('xUnlock');
+ const file = pool.getOFileForS3File(pFile);
+ file.lockType = lockType;
+ return 0;
+ },
+ xWrite: function(pFile,pSrc,n,offset64){
+ const pool = getPoolForPFile(pFile);
+ pool.storeErr();
+ const file = pool.getOFileForS3File(pFile);
+ pool.log(`xWrite ${file.path} ${n} ${offset64}`);
+ try{
+ const nBytes = file.sah.write(
+ wasm.heap8u().subarray(pSrc, pSrc+n),
+ { at: HEADER_OFFSET_DATA + Number(offset64) }
+ );
+ return nBytes === n ? 0 : capi.SQLITE_IOERR;
+ }catch(e){
+ pool.storeErr(e);
+ return capi.SQLITE_IOERR;
+ }
+ }
+ }/*ioMethods*/;
+
+ const opfsIoMethods = new capi.sqlite3_io_methods();
+ opfsIoMethods.$iVersion = 1;
+ sqlite3.vfs.installVfs({
+ io: {struct: opfsIoMethods, methods: ioMethods}
+ });
+
+ /**
+ Impls for the sqlite3_vfs methods. Maintenance reminder: members
+ are in alphabetical order to simplify finding them.
+ */
+ const vfsMethods = {
+ xAccess: function(pVfs,zName,flags,pOut){
+ //log(`xAccess ${wasm.cstrToJs(zName)}`);
+ const pool = getPoolForVfs(pVfs);
+ pool.storeErr();
+ try{
+ const name = pool.getPath(zName);
+ wasm.poke32(pOut, pool.hasFilename(name) ? 1 : 0);
+ }catch(e){
+ /*ignored*/
+ wasm.poke32(pOut, 0);
+ }
+ return 0;
+ },
+ xCurrentTime: function(pVfs,pOut){
+ wasm.poke(pOut, 2440587.5 + (new Date().getTime()/86400000),
+ 'double');
+ return 0;
+ },
+ xCurrentTimeInt64: function(pVfs,pOut){
+ wasm.poke(pOut, (2440587.5 * 86400000) + new Date().getTime(),
+ 'i64');
+ return 0;
+ },
+ xDelete: function(pVfs, zName, doSyncDir){
+ const pool = getPoolForVfs(pVfs);
+ pool.log(`xDelete ${wasm.cstrToJs(zName)}`);
+ pool.storeErr();
+ try{
+ pool.deletePath(pool.getPath(zName));
+ return 0;
+ }catch(e){
+ pool.storeErr(e);
+ return capi.SQLITE_IOERR_DELETE;
+ }
+ },
+ xFullPathname: function(pVfs,zName,nOut,pOut){
+ //const pool = getPoolForVfs(pVfs);
+ //pool.log(`xFullPathname ${wasm.cstrToJs(zName)}`);
+ const i = wasm.cstrncpy(pOut, zName, nOut);
+ return i nOut) wasm.poke8(pOut + nOut - 1, 0);
+ }catch(e){
+ return capi.SQLITE_NOMEM;
+ }finally{
+ wasm.scopedAllocPop(scope);
+ }
+ }
+ return 0;
+ },
+ //xSleep is optionally defined below
+ xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){
+ const pool = getPoolForVfs(pVfs);
+ try{
+ pool.log(`xOpen ${wasm.cstrToJs(zName)} ${flags}`);
+ // First try to open a path that already exists in the file system.
+ const path = (zName && wasm.peek8(zName))
+ ? pool.getPath(zName)
+ : getRandomName();
+ let sah = pool.getSAHForPath(path);
+ if(!sah && (flags & capi.SQLITE_OPEN_CREATE)) {
+ // File not found so try to create it.
+ if(pool.getFileCount() < pool.getCapacity()) {
+ // Choose an unassociated OPFS file from the pool.
+ sah = pool.nextAvailableSAH();
+ pool.setAssociatedPath(sah, path, flags);
+ }else{
+ // File pool is full.
+ toss('SAH pool is full. Cannot create file',path);
+ }
+ }
+ if(!sah){
+ toss('file not found:',path);
+ }
+ // Subsequent I/O methods are only passed the sqlite3_file
+ // pointer, so map the relevant info we need to that pointer.
+ const file = {path, flags, sah};
+ pool.mapS3FileToOFile(pFile, file);
+ file.lockType = capi.SQLITE_LOCK_NONE;
+ const sq3File = new capi.sqlite3_file(pFile);
+ sq3File.$pMethods = opfsIoMethods.pointer;
+ sq3File.dispose();
+ wasm.poke32(pOutFlags, flags);
+ return 0;
+ }catch(e){
+ pool.storeErr(e);
+ return capi.SQLITE_CANTOPEN;
+ }
+ }/*xOpen()*/
+ }/*vfsMethods*/;
+
+ /**
+ Creates and initializes an sqlite3_vfs instance for an
+ OpfsSAHPool. The argument is the VFS's name (JS string).
+
+ Throws if the VFS name is already registered or if something
+ goes terribly wrong via sqlite3.vfs.installVfs().
+
+ Maintenance reminder: the only detail about the returned object
+ which is specific to any given OpfsSAHPool instance is the $zName
+ member. All other state is identical.
+ */
+ const createOpfsVfs = function(vfsName){
+ if( sqlite3.capi.sqlite3_vfs_find(vfsName)){
+ toss3("VFS name is already registered:", vfsName);
+ }
+ const opfsVfs = new capi.sqlite3_vfs();
+ /* We fetch the default VFS so that we can inherit some
+ methods from it. */
+ const pDVfs = capi.sqlite3_vfs_find(null);
+ const dVfs = pDVfs
+ ? new capi.sqlite3_vfs(pDVfs)
+ : null /* dVfs will be null when sqlite3 is built with
+ SQLITE_OS_OTHER. */;
+ opfsVfs.$iVersion = 2/*yes, two*/;
+ opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof;
+ opfsVfs.$mxPathname = HEADER_MAX_PATH_SIZE;
+ opfsVfs.addOnDispose(
+ opfsVfs.$zName = wasm.allocCString(vfsName),
+ ()=>setPoolForVfs(opfsVfs.pointer, 0)
+ );
+
+ if(dVfs){
+ /* Inherit certain VFS members from the default VFS,
+ if available. */
+ opfsVfs.$xRandomness = dVfs.$xRandomness;
+ opfsVfs.$xSleep = dVfs.$xSleep;
+ dVfs.dispose();
+ }
+ if(!opfsVfs.$xRandomness && !vfsMethods.xRandomness){
+ /* If the default VFS has no xRandomness(), add a basic JS impl... */
+ vfsMethods.xRandomness = function(pVfs, nOut, pOut){
+ const heap = wasm.heap8u();
+ let i = 0;
+ for(; i < nOut; ++i) heap[pOut + i] = (Math.random()*255000) & 0xFF;
+ return i;
+ };
+ }
+ if(!opfsVfs.$xSleep && !vfsMethods.xSleep){
+ vfsMethods.xSleep = (pVfs,ms)=>0;
+ }
+ sqlite3.vfs.installVfs({
+ vfs: {struct: opfsVfs, methods: vfsMethods}
+ });
+ return opfsVfs;
+ };
+
+ /**
+ Class for managing OPFS-related state for the
+ OPFS SharedAccessHandle Pool sqlite3_vfs.
+ */
+ class OpfsSAHPool {
+ /* OPFS dir in which VFS metadata is stored. */
+ vfsDir;
+ /* Directory handle to this.vfsDir. */
+ #dhVfsRoot;
+ /* Directory handle to the subdir of this.#dhVfsRoot which holds
+ the randomly-named "opaque" files. This subdir exists in the
+ hope that we can eventually support client-created files in
+ this.#dhVfsRoot. */
+ #dhOpaque;
+ /* Directory handle to this.dhVfsRoot's parent dir. Needed
+ for a VFS-wipe op. */
+ #dhVfsParent;
+ /* Maps SAHs to their opaque file names. */
+ #mapSAHToName = new Map();
+ /* Maps client-side file names to SAHs. */
+ #mapFilenameToSAH = new Map();
+ /* Set of currently-unused SAHs. */
+ #availableSAH = new Set();
+ /* Maps (sqlite3_file*) to xOpen's file objects. */
+ #mapS3FileToOFile_ = new Map();
+
+ /* Maps SAH to an abstract File Object which contains
+ various metadata about that handle. */
+ //#mapSAHToMeta = new Map();
+
+ /** Buffer used by [sg]etAssociatedPath(). */
+ #apBody = new Uint8Array(HEADER_CORPUS_SIZE);
+ // DataView for this.#apBody
+ #dvBody;
+
+ // associated sqlite3_vfs instance
+ #cVfs;
+
+ // Logging verbosity. See optionDefaults.verbosity.
+ #verbosity;
+
+ constructor(options = Object.create(null)){
+ this.#verbosity = options.verbosity ?? optionDefaults.verbosity;
+ this.vfsName = options.name || optionDefaults.name;
+ this.#cVfs = createOpfsVfs(this.vfsName);
+ setPoolForVfs(this.#cVfs.pointer, this);
+ this.vfsDir = options.directory || ("."+this.vfsName);
+ this.#dvBody =
+ new DataView(this.#apBody.buffer, this.#apBody.byteOffset);
+ this.isReady = this
+ .reset(!!(options.clearOnInit ?? optionDefaults.clearOnInit))
+ .then(()=>{
+ if(this.$error) throw this.$error;
+ return this.getCapacity()
+ ? Promise.resolve(undefined)
+ : this.addCapacity(options.initialCapacity
+ || optionDefaults.initialCapacity);
+ });
+ }
+
+ #logImpl(level,...args){
+ if(this.#verbosity>level) loggers[level](this.vfsName+":",...args);
+ };
+ log(...args){this.#logImpl(2, ...args)};
+ warn(...args){this.#logImpl(1, ...args)};
+ error(...args){this.#logImpl(0, ...args)};
+
+ getVfs(){return this.#cVfs}
+
+ /* Current pool capacity. */
+ getCapacity(){return this.#mapSAHToName.size}
+
+ /* Current number of in-use files from pool. */
+ getFileCount(){return this.#mapFilenameToSAH.size}
+
+ /* Returns an array of the names of all
+ currently-opened client-specified filenames. */
+ getFileNames(){
+ const rc = [];
+ const iter = this.#mapFilenameToSAH.keys();
+ for(const n of iter) rc.push(n);
+ return rc;
+ }
+
+// #createFileObject(sah,clientName,opaqueName){
+// const f = Object.assign(Object.create(null),{
+// clientName, opaqueName
+// });
+// this.#mapSAHToMeta.set(sah, f);
+// return f;
+// }
+// #unmapFileObject(sah){
+// this.#mapSAHToMeta.delete(sah);
+// }
+
+ /**
+ Adds n files to the pool's capacity. This change is
+ persistent across settings. Returns a Promise which resolves
+ to the new capacity.
+ */
+ async addCapacity(n){
+ for(let i = 0; i < n; ++i){
+ const name = getRandomName();
+ const h = await this.#dhOpaque.getFileHandle(name, {create:true});
+ const ah = await h.createSyncAccessHandle();
+ this.#mapSAHToName.set(ah,name);
+ this.setAssociatedPath(ah, '', 0);
+ //this.#createFileObject(ah,undefined,name);
+ }
+ return this.getCapacity();
+ }
+
+ /**
+ Reduce capacity by n, but can only reduce up to the limit
+ of currently-available SAHs. Returns a Promise which resolves
+ to the number of slots really removed.
+ */
+ async reduceCapacity(n){
+ let nRm = 0;
+ for(const ah of Array.from(this.#availableSAH)){
+ if(nRm === n || this.getFileCount() === this.getCapacity()){
+ break;
+ }
+ const name = this.#mapSAHToName.get(ah);
+ //this.#unmapFileObject(ah);
+ ah.close();
+ await this.#dhOpaque.removeEntry(name);
+ this.#mapSAHToName.delete(ah);
+ this.#availableSAH.delete(ah);
+ ++nRm;
+ }
+ return nRm;
+ }
+
+ /**
+ Releases all currently-opened SAHs. The only legal
+ operation after this is acquireAccessHandles().
+ */
+ releaseAccessHandles(){
+ for(const ah of this.#mapSAHToName.keys()) ah.close();
+ this.#mapSAHToName.clear();
+ this.#mapFilenameToSAH.clear();
+ this.#availableSAH.clear();
+ }
+
+ /**
+ Opens all files under this.vfsDir/this.#dhOpaque and acquires
+ a SAH for each. returns a Promise which resolves to no value
+ but completes once all SAHs are acquired. If acquiring an SAH
+ throws, SAHPool.$error will contain the corresponding
+ exception.
+
+ If clearFiles is true, the client-stored state of each file is
+ cleared when its handle is acquired, including its name, flags,
+ and any data stored after the metadata block.
+ */
+ async acquireAccessHandles(clearFiles){
+ const files = [];
+ for await (const [name,h] of this.#dhOpaque){
+ if('file'===h.kind){
+ files.push([name,h]);
+ }
+ }
+ return Promise.all(files.map(async([name,h])=>{
+ try{
+ const ah = await h.createSyncAccessHandle()
+ this.#mapSAHToName.set(ah, name);
+ if(clearFiles){
+ ah.truncate(HEADER_OFFSET_DATA);
+ this.setAssociatedPath(ah, '', 0);
+ }else{
+ const path = this.getAssociatedPath(ah);
+ if(path){
+ this.#mapFilenameToSAH.set(path, ah);
+ }else{
+ this.#availableSAH.add(ah);
+ }
+ }
+ }catch(e){
+ this.storeErr(e);
+ this.releaseAccessHandles();
+ throw e;
+ }
+ }));
+ }
+
+ /**
+ Given an SAH, returns the client-specified name of
+ that file by extracting it from the SAH's header.
+
+ On error, it disassociates SAH from the pool and
+ returns an empty string.
+ */
+ getAssociatedPath(sah){
+ sah.read(this.#apBody, {at: 0});
+ // Delete any unexpected files left over by previous
+ // untimely errors...
+ const flags = this.#dvBody.getUint32(HEADER_OFFSET_FLAGS);
+ if(this.#apBody[0] &&
+ ((flags & capi.SQLITE_OPEN_DELETEONCLOSE) ||
+ (flags & PERSISTENT_FILE_TYPES)===0)){
+ warn(`Removing file with unexpected flags ${flags.toString(16)}`,
+ this.#apBody);
+ this.setAssociatedPath(sah, '', 0);
+ return '';
+ }
+
+ const fileDigest = new Uint32Array(HEADER_DIGEST_SIZE / 4);
+ sah.read(fileDigest, {at: HEADER_OFFSET_DIGEST});
+ const compDigest = this.computeDigest(this.#apBody);
+ if(fileDigest.every((v,i) => v===compDigest[i])){
+ // Valid digest
+ const pathBytes = this.#apBody.findIndex((v)=>0===v);
+ if(0===pathBytes){
+ // This file is unassociated, so truncate it to avoid
+ // leaving stale db data laying around.
+ sah.truncate(HEADER_OFFSET_DATA);
+ }
+ return pathBytes
+ ? textDecoder.decode(this.#apBody.subarray(0,pathBytes))
+ : '';
+ }else{
+ // Invalid digest
+ warn('Disassociating file with bad digest.');
+ this.setAssociatedPath(sah, '', 0);
+ return '';
+ }
+ }
+
+ /**
+ Stores the given client-defined path and SQLITE_OPEN_xyz flags
+ into the given SAH. If path is an empty string then the file is
+ disassociated from the pool but its previous name is preserved
+ in the metadata.
+ */
+ setAssociatedPath(sah, path, flags){
+ const enc = textEncoder.encodeInto(path, this.#apBody);
+ if(HEADER_MAX_PATH_SIZE <= enc.written + 1/*NUL byte*/){
+ toss("Path too long:",path);
+ }
+ this.#apBody.fill(0, enc.written, HEADER_MAX_PATH_SIZE);
+ this.#dvBody.setUint32(HEADER_OFFSET_FLAGS, flags);
+
+ const digest = this.computeDigest(this.#apBody);
+ sah.write(this.#apBody, {at: 0});
+ sah.write(digest, {at: HEADER_OFFSET_DIGEST});
+ sah.flush();
+
+ if(path){
+ this.#mapFilenameToSAH.set(path, sah);
+ this.#availableSAH.delete(sah);
+ }else{
+ // This is not a persistent file, so eliminate the contents.
+ sah.truncate(HEADER_OFFSET_DATA);
+ this.#availableSAH.add(sah);
+ }
+ }
+
+ /**
+ Computes a digest for the given byte array and returns it as a
+ two-element Uint32Array. This digest gets stored in the
+ metadata for each file as a validation check. Changing this
+ algorithm invalidates all existing databases for this VFS, so
+ don't do that.
+ */
+ computeDigest(byteArray){
+ let h1 = 0xdeadbeef;
+ let h2 = 0x41c6ce57;
+ for(const v of byteArray){
+ h1 = 31 * h1 + (v * 307);
+ h2 = 31 * h2 + (v * 307);
+ }
+ return new Uint32Array([h1>>>0, h2>>>0]);
+ }
+
+ /**
+ Re-initializes the state of the SAH pool, releasing and
+ re-acquiring all handles.
+
+ See acquireAccessHandles() for the specifics of the clearFiles
+ argument.
+ */
+ async reset(clearFiles){
+ await this.isReady;
+ let h = await navigator.storage.getDirectory();
+ let prev, prevName;
+ for(const d of this.vfsDir.split('/')){
+ if(d){
+ prev = h;
+ h = await h.getDirectoryHandle(d,{create:true});
+ }
+ }
+ this.#dhVfsRoot = h;
+ this.#dhVfsParent = prev;
+ this.#dhOpaque = await this.#dhVfsRoot.getDirectoryHandle(
+ OPAQUE_DIR_NAME,{create:true}
+ );
+ this.releaseAccessHandles();
+ return this.acquireAccessHandles(clearFiles);
+ }
+
+ /**
+ Returns the pathname part of the given argument,
+ which may be any of:
+
+ - a URL object
+ - A JS string representing a file name
+ - Wasm C-string representing a file name
+
+ All "../" parts and duplicate slashes are resolve/removed from
+ the returned result.
+ */
+ getPath(arg) {
+ if(wasm.isPtr(arg)) arg = wasm.cstrToJs(arg);
+ return ((arg instanceof URL)
+ ? arg
+ : new URL(arg, 'file://localhost/')).pathname;
+ }
+
+ /**
+ Removes the association of the given client-specified file
+ name (JS string) from the pool. Returns true if a mapping
+ is found, else false.
+ */
+ deletePath(path) {
+ const sah = this.#mapFilenameToSAH.get(path);
+ if(sah) {
+ // Un-associate the name from the SAH.
+ this.#mapFilenameToSAH.delete(path);
+ this.setAssociatedPath(sah, '', 0);
+ }
+ return !!sah;
+ }
+
+ /**
+ Sets e as this object's current error. Pass a falsy
+ (or no) value to clear it.
+ */
+ storeErr(e){
+ if(e) this.error(e);
+ return this.$error = e;
+ }
+ /**
+ Pops this object's Error object and returns
+ it (a falsy value if no error is set).
+ */
+ popErr(){
+ const rc = this.$error;
+ this.$error = undefined;
+ return rc;
+ }
+
+ /**
+ Returns the next available SAH without removing
+ it from the set.
+ */
+ nextAvailableSAH(){
+ const [rc] = this.#availableSAH.keys();
+ return rc;
+ }
+
+ /**
+ Given an (sqlite3_file*), returns the mapped
+ xOpen file object.
+ */
+ getOFileForS3File(pFile){
+ return this.#mapS3FileToOFile_.get(pFile);
+ }
+ /**
+ Maps or unmaps (if file is falsy) the given (sqlite3_file*)
+ to an xOpen file object and to this pool object.
+ */
+ mapS3FileToOFile(pFile,file){
+ if(file){
+ this.#mapS3FileToOFile_.set(pFile, file);
+ setPoolForPFile(pFile, this);
+ }else{
+ this.#mapS3FileToOFile_.delete(pFile);
+ setPoolForPFile(pFile, false);
+ }
+ }
+
+ /**
+ Returns true if the given client-defined file name is in this
+ object's name-to-SAH map.
+ */
+ hasFilename(name){
+ return this.#mapFilenameToSAH.has(name)
+ }
+
+ /**
+ Returns the SAH associated with the given
+ client-defined file name.
+ */
+ getSAHForPath(path){
+ return this.#mapFilenameToSAH.get(path);
+ }
+
+ /**
+ Removes this object's sqlite3_vfs registration and shuts down
+ this object, releasing all handles, mappings, and whatnot,
+ including deleting its data directory. There is currently no
+ way to "revive" the object and reaquire its resources.
+
+ This function is intended primarily for testing.
+
+ Resolves to true if it did its job, false if the
+ VFS has already been shut down.
+ */
+ async removeVfs(){
+ if(!this.#cVfs.pointer || !this.#dhOpaque) return false;
+ capi.sqlite3_vfs_unregister(this.#cVfs.pointer);
+ this.#cVfs.dispose();
+ try{
+ this.releaseAccessHandles();
+ await this.#dhVfsRoot.removeEntry(OPAQUE_DIR_NAME, {recursive: true});
+ this.#dhOpaque = undefined;
+ await this.#dhVfsParent.removeEntry(
+ this.#dhVfsRoot.name, {recursive: true}
+ );
+ this.#dhVfsRoot = this.#dhVfsParent = undefined;
+ }catch(e){
+ sqlite3.config.error(this.vfsName,"removeVfs() failed:",e);
+ /*otherwise ignored - there is no recovery strategy*/
+ }
+ return true;
+ }
+
+
+ //! Documented elsewhere in this file.
+ exportFile(name){
+ const sah = this.#mapFilenameToSAH.get(name) || toss("File not found:",name);
+ const n = sah.getSize() - HEADER_OFFSET_DATA;
+ const b = new Uint8Array(n>0 ? n : 0);
+ if(n>0){
+ const nRead = sah.read(b, {at: HEADER_OFFSET_DATA});
+ if(nRead != n){
+ toss("Expected to read "+n+" bytes but read "+nRead+".");
+ }
+ }
+ return b;
+ }
+
+ //! Documented elsewhere in this file.
+ importDb(name, bytes){
+ if(bytes instanceof ArrayBuffer) bytes = new Uint8Array(bytes);
+ const n = bytes.byteLength;
+ if(n<512 || n%512!=0){
+ toss("Byte array size is invalid for an SQLite db.");
+ }
+ const header = "SQLite format 3";
+ for(let i = 0; i < header.length; ++i){
+ if( header.charCodeAt(i) !== bytes[i] ){
+ toss("Input does not contain an SQLite database header.");
+ }
+ }
+ const sah = this.#mapFilenameToSAH.get(name)
+ || this.nextAvailableSAH()
+ || toss("No available handles to import to.");
+ const nWrote = sah.write(bytes, {at: HEADER_OFFSET_DATA});
+ if(nWrote != n){
+ this.setAssociatedPath(sah, '', 0);
+ toss("Expected to write "+n+" bytes but wrote "+nWrote+".");
+ }else{
+ this.setAssociatedPath(sah, name, capi.SQLITE_OPEN_MAIN_DB);
+ }
+ }
+
+ }/*class OpfsSAHPool*/;
+
+
+ /**
+ A OpfsSAHPoolUtil instance is exposed to clients in order to
+ manipulate an OpfsSAHPool object without directly exposing that
+ object and allowing for some semantic changes compared to that
+ class.
+
+ Class docs are in the client-level docs for
+ installOpfsSAHPoolVfs().
+ */
+ class OpfsSAHPoolUtil {
+ /* This object's associated OpfsSAHPool. */
+ #p;
+
+ constructor(sahPool){
+ this.#p = sahPool;
+ this.vfsName = sahPool.vfsName;
+ }
+
+ async addCapacity(n){ return this.#p.addCapacity(n) }
+
+ async reduceCapacity(n){ return this.#p.reduceCapacity(n) }
+
+ getCapacity(){ return this.#p.getCapacity(this.#p) }
+
+ getFileCount(){ return this.#p.getFileCount() }
+ getFileNames(){ return this.#p.getFileNames() }
+
+ async reserveMinimumCapacity(min){
+ const c = this.#p.getCapacity();
+ return (c < min) ? this.#p.addCapacity(min - c) : c;
+ }
+
+ exportFile(name){ return this.#p.exportFile(name) }
+
+ importDb(name, bytes){ return this.#p.importDb(name,bytes) }
+
+ async wipeFiles(){ return this.#p.reset(true) }
+
+ unlink(filename){ return this.#p.deletePath(filename) }
+
+ async removeVfs(){ return this.#p.removeVfs() }
+
+ }/* class OpfsSAHPoolUtil */;
+
+ /**
+ Returns a resolved Promise if the current environment
+ has a "fully-sync" SAH impl, else a rejected Promise.
+ */
+ const apiVersionCheck = async ()=>{
+ const dh = await navigator.storage.getDirectory();
+ const fn = '.opfs-sahpool-sync-check-'+getRandomName();
+ const fh = await dh.getFileHandle(fn, { create: true });
+ const ah = await fh.createSyncAccessHandle();
+ const close = ah.close();
+ await close;
+ await dh.removeEntry(fn);
+ if(close?.then){
+ toss("The local OPFS API is too old for opfs-sahpool:",
+ "it has an async FileSystemSyncAccessHandle.close() method.");
+ }
+ return true;
+ };
+
+ /** Only for testing a rejection case. */
+ let instanceCounter = 0;
+
+ /**
+ installOpfsSAHPoolVfs() asynchronously initializes the OPFS
+ SyncAccessHandle (a.k.a. SAH) Pool VFS. It returns a Promise which
+ either resolves to a utility object described below or rejects with
+ an Error value.
+
+ Initialization of this VFS is not automatic because its
+ registration requires that it lock all resources it
+ will potentially use, even if client code does not want
+ to use them. That, in turn, can lead to locking errors
+ when, for example, one page in a given origin has loaded
+ this VFS but does not use it, then another page in that
+ origin tries to use the VFS. If the VFS were automatically
+ registered, the second page would fail to load the VFS
+ due to OPFS locking errors.
+
+ If this function is called more than once with a given "name"
+ option (see below), it will return the same Promise. Calls for
+ different names will return different Promises which resolve to
+ independent objects and refer to different VFS registrations.
+
+ On success, the resulting Promise resolves to a utility object
+ which can be used to query and manipulate the pool. Its API is
+ described at the end of these docs.
+
+ This function accepts an options object to configure certain
+ parts but it is only acknowledged for the very first call and
+ ignored for all subsequent calls.
+
+ The options, in alphabetical order:
+
+ - `clearOnInit`: (default=false) if truthy, contents and filename
+ mapping are removed from each SAH it is acquired during
+ initalization of the VFS, leaving the VFS's storage in a pristine
+ state. Use this only for databases which need not survive a page
+ reload.
+
+ - `initialCapacity`: (default=6) Specifies the default capacity of
+ the VFS. This should not be set unduly high because the VFS has
+ to open (and keep open) a file for each entry in the pool. This
+ setting only has an effect when the pool is initially empty. It
+ does not have any effect if a pool already exists.
+
+ - `directory`: (default="."+`name`) Specifies the OPFS directory
+ name in which to store metadata for the `"opfs-sahpool"`
+ sqlite3_vfs. Only one instance of this VFS can be installed per
+ JavaScript engine, and any two engines with the same storage
+ directory name will collide with each other, leading to locking
+ errors and the inability to register the VFS in the second and
+ subsequent engine. Using a different directory name for each
+ application enables different engines in the same HTTP origin to
+ co-exist, but their data are invisible to each other. Changing
+ this name will effectively orphan any databases stored under
+ previous names. The default is unspecified but descriptive. This
+ option may contain multiple path elements, e.g. "foo/bar/baz",
+ and they are created automatically. In practice there should be
+ no driving need to change this. ACHTUNG: all files in this
+ directory are assumed to be managed by the VFS. Do not place
+ other files in that directory, as they may be deleted or
+ otherwise modified by the VFS.
+
+ - `name`: (default="opfs-sahpool") sets the name to register this
+ VFS under. Normally this should not be changed, but it is
+ possible to register this VFS under multiple names so long as
+ each has its own separate directory to work from. The storage for
+ each is invisible to all others. The name must be a string
+ compatible with `sqlite3_vfs_register()` and friends and suitable
+ for use in URI-style database file names.
+
+ Achtung: if a custom `name` is provided, a custom `directory`
+ must also be provided if any other instance is registered with
+ the default directory. If no directory is explicitly provided
+ then a directory name is synthesized from the `name` option.
+
+ Peculiarities of this VFS:
+
+ - Paths given to it _must_ be absolute. Relative paths will not
+ be properly recognized. This is arguably a bug but correcting it
+ requires some hoop-jumping in routines which have no business
+ doing tricks.
+
+ - It is possible to install multiple instances under different
+ names, each sandboxed from one another inside their own private
+ directory. This feature exists primarily as a way for disparate
+ applications within a given HTTP origin to use this VFS without
+ introducing locking issues between them.
+
+
+ The API for the utility object passed on by this function's
+ Promise, in alphabetical order...
+
+ - [async] number addCapacity(n)
+
+ Adds `n` entries to the current pool. This change is persistent
+ across sessions so should not be called automatically at each app
+ startup (but see `reserveMinimumCapacity()`). Its returned Promise
+ resolves to the new capacity. Because this operation is necessarily
+ asynchronous, the C-level VFS API cannot call this on its own as
+ needed.
+
+ - byteArray exportFile(name)
+
+ Synchronously reads the contents of the given file into a Uint8Array
+ and returns it. This will throw if the given name is not currently
+ in active use or on I/O error. Note that the given name is _not_
+ visible directly in OPFS (or, if it is, it's not from this VFS).
+
+ - number getCapacity()
+
+ Returns the number of files currently contained
+ in the SAH pool. The default capacity is only large enough for one
+ or two databases and their associated temp files.
+
+ - number getFileCount()
+
+ Returns the number of files from the pool currently allocated to
+ slots. This is not the same as the files being "opened".
+
+ - array getFileNames()
+
+ Returns an array of the names of the files currently allocated to
+ slots. This list is the same length as getFileCount().
+
+ - void importDb(name, bytes)
+
+ Imports the contents of an SQLite database, provided as a byte
+ array or ArrayBuffer, under the given name, overwriting any
+ existing content. Throws if the pool has no available file slots,
+ on I/O error, or if the input does not appear to be a
+ database. In the latter case, only a cursory examination is made.
+ Note that this routine is _only_ for importing database files,
+ not arbitrary files, the reason being that this VFS will
+ automatically clean up any non-database files so importing them
+ is pointless.
+
+ On a write error, the handle is removed from the pool and made
+ available for re-use.
+
+ - [async] number reduceCapacity(n)
+
+ Removes up to `n` entries from the pool, with the caveat that it can
+ only remove currently-unused entries. It returns a Promise which
+ resolves to the number of entries actually removed.
+
+ - [async] boolean removeVfs()
+
+ Unregisters the opfs-sahpool VFS and removes its directory from OPFS
+ (which means that _all client content_ is removed). After calling
+ this, the VFS may no longer be used and there is no way to re-add it
+ aside from reloading the current JavaScript context.
+
+ Results are undefined if a database is currently in use with this
+ VFS.
+
+ The returned Promise resolves to true if it performed the removal
+ and false if the VFS was not installed.
+
+ If the VFS has a multi-level directory, e.g. "/foo/bar/baz", _only_
+ the bottom-most directory is removed because this VFS cannot know for
+ certain whether the higher-level directories contain data which
+ should be removed.
+
+ - [async] number reserveMinimumCapacity(min)
+
+ If the current capacity is less than `min`, the capacity is
+ increased to `min`, else this returns with no side effects. The
+ resulting Promise resolves to the new capacity.
+
+ - boolean unlink(filename)
+
+ If a virtual file exists with the given name, disassociates it from
+ the pool and returns true, else returns false without side
+ effects. Results are undefined if the file is currently in active
+ use.
+
+ - string vfsName
+
+ The SQLite VFS name under which this pool's VFS is registered.
+
+ - [async] void wipeFiles()
+
+ Clears all client-defined state of all SAHs and makes all of them
+ available for re-use by the pool. Results are undefined if any such
+ handles are currently in use, e.g. by an sqlite3 db.
+ */
+ sqlite3.installOpfsSAHPoolVfs = async function(options=Object.create(null)){
+ const vfsName = options.name || optionDefaults.name;
+ if(0 && 2===++instanceCounter){
+ throw new Error("Just testing rejection.");
+ }
+ if(initPromises[vfsName]){
+ //console.warn("Returning same OpfsSAHPool result",options,vfsName,initPromises[vfsName]);
+ return initPromises[vfsName];
+ }
+ if(!globalThis.FileSystemHandle ||
+ !globalThis.FileSystemDirectoryHandle ||
+ !globalThis.FileSystemFileHandle ||
+ !globalThis.FileSystemFileHandle.prototype.createSyncAccessHandle ||
+ !navigator?.storage?.getDirectory){
+ return (initPromises[vfsName] = Promise.reject(new Error("Missing required OPFS APIs.")));
+ }
+
+ /**
+ Maintenance reminder: the order of ASYNC ops in this function
+ is significant. We need to have them all chained at the very
+ end in order to be able to catch a race condition where
+ installOpfsSAHPoolVfs() is called twice in rapid succession,
+ e.g.:
+
+ installOpfsSAHPoolVfs().then(console.warn.bind(console));
+ installOpfsSAHPoolVfs().then(console.warn.bind(console));
+
+ If the timing of the async calls is not "just right" then that
+ second call can end up triggering the init a second time and chaos
+ ensues.
+ */
+ return initPromises[vfsName] = apiVersionCheck().then(async function(){
+ if(options.$testThrowInInit){
+ throw options.$testThrowInInit;
+ }
+ const thePool = new OpfsSAHPool(options);
+ return thePool.isReady.then(async()=>{
+ /** The poolUtil object will be the result of the
+ resolved Promise. */
+ const poolUtil = new OpfsSAHPoolUtil(thePool);
+ if(sqlite3.oo1){
+ const oo1 = sqlite3.oo1;
+ const theVfs = thePool.getVfs();
+ const OpfsSAHPoolDb = function(...args){
+ const opt = oo1.DB.dbCtorHelper.normalizeArgs(...args);
+ opt.vfs = theVfs.$zName;
+ oo1.DB.dbCtorHelper.call(this, opt);
+ };
+ OpfsSAHPoolDb.prototype = Object.create(oo1.DB.prototype);
+ // yes or no? OpfsSAHPoolDb.PoolUtil = poolUtil;
+ poolUtil.OpfsSAHPoolDb = OpfsSAHPoolDb;
+ oo1.DB.dbCtorHelper.setVfsPostOpenSql(
+ theVfs.pointer,
+ function(oo1Db, sqlite3){
+ sqlite3.capi.sqlite3_exec(oo1Db, [
+ /* See notes in sqlite3-vfs-opfs.js */
+ "pragma journal_mode=DELETE;",
+ "pragma cache_size=-16384;"
+ ], 0, 0, 0);
+ }
+ );
+ }/*extend sqlite3.oo1*/
+ thePool.log("VFS initialized.");
+ return poolUtil;
+ }).catch(async (e)=>{
+ await thePool.removeVfs().catch(()=>{});
+ return e;
+ });
+ }).catch((err)=>{
+ //error("rejecting promise:",err);
+ return initPromises[vfsName] = Promise.reject(err);
+ });
+ }/*installOpfsSAHPoolVfs()*/;
+}/*sqlite3ApiBootstrap.initializers*/);
+//#else
+/*
+ The OPFS SAH Pool VFS parts are elided from builds targeting
+ node.js.
+*/
+//#endif target=node
diff --git a/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js b/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js
index 5c584702d..93482505a 100644
--- a/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js
+++ b/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js
@@ -1,3 +1,4 @@
+//#ifnot target=node
/*
2022-09-18
@@ -23,7 +24,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
installOpfsVfs() returns a Promise which, on success, installs an
sqlite3_vfs named "opfs", suitable for use with all sqlite3 APIs
which accept a VFS. It is intended to be called via
- sqlite3ApiBootstrap.initializersAsync or an equivalent mechanism.
+ sqlite3ApiBootstrap.initializers or an equivalent mechanism.
The installed VFS uses the Origin-Private FileSystem API for
all file storage. On error it is rejected with an exception
@@ -101,6 +102,10 @@ const installOpfsVfs = function callee(options){
options = Object.create(null);
}
const urlParams = new URL(globalThis.location.href).searchParams;
+ if(urlParams.has('opfs-disable')){
+ //sqlite3.config.warn('Explicitly not installing "opfs" VFS due to opfs-disable flag.');
+ return Promise.resolve(sqlite3);
+ }
if(undefined===options.verbose){
options.verbose = urlParams.has('opfs-verbose')
? (+urlParams.get('opfs-verbose') || 2) : 1;
@@ -118,11 +123,11 @@ const installOpfsVfs = function callee(options){
options.proxyUri = options.proxyUri();
}
const thePromise = new Promise(function(promiseResolve_, promiseReject_){
- const loggers = {
- 0:sqlite3.config.error,
- 1:sqlite3.config.warn,
- 2:sqlite3.config.log
- };
+ const loggers = [
+ sqlite3.config.error,
+ sqlite3.config.warn,
+ sqlite3.config.log
+ ];
const logImpl = (level,...args)=>{
if(options.verbose>level) loggers[level]("OPFS syncer:",...args);
};
@@ -191,17 +196,18 @@ const installOpfsVfs = function callee(options){
s.count = s.time = 0;
}
}/*metrics*/;
- const opfsVfs = new sqlite3_vfs();
const opfsIoMethods = new sqlite3_io_methods();
+ const opfsVfs = new sqlite3_vfs()
+ .addOnDispose( ()=>opfsIoMethods.dispose());
let promiseWasRejected = undefined;
const promiseReject = (err)=>{
promiseWasRejected = true;
opfsVfs.dispose();
return promiseReject_(err);
};
- const promiseResolve = (value)=>{
+ const promiseResolve = ()=>{
promiseWasRejected = false;
- return promiseResolve_(value);
+ return promiseResolve_(sqlite3);
};
const W =
//#if target=es6-bundler-friendly
@@ -235,17 +241,17 @@ const installOpfsVfs = function callee(options){
? new sqlite3_vfs(pDVfs)
: null /* dVfs will be null when sqlite3 is built with
SQLITE_OS_OTHER. */;
+ opfsIoMethods.$iVersion = 1;
opfsVfs.$iVersion = 2/*yes, two*/;
opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof;
opfsVfs.$mxPathname = 1024/*sure, why not?*/;
opfsVfs.$zName = wasm.allocCString("opfs");
// All C-side memory of opfsVfs is zeroed out, but just to be explicit:
opfsVfs.$xDlOpen = opfsVfs.$xDlError = opfsVfs.$xDlSym = opfsVfs.$xDlClose = null;
- opfsVfs.ondispose = [
+ opfsVfs.addOnDispose(
'$zName', opfsVfs.$zName,
- 'cleanup default VFS wrapper', ()=>(dVfs ? dVfs.dispose() : null),
- 'cleanup opfsIoMethods', ()=>opfsIoMethods.dispose()
- ];
+ 'cleanup default VFS wrapper', ()=>(dVfs ? dVfs.dispose() : null)
+ );
/**
Pedantic sidebar about opfsVfs.ondispose: the entries in that array
are items to clean up when opfsVfs.dispose() is called, but in this
@@ -298,6 +304,7 @@ const installOpfsVfs = function callee(options){
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
@@ -484,7 +491,8 @@ const installOpfsVfs = function callee(options){
This proxy de/serializes cross-thread function arguments and
output-pointer values via the state.sabIO SharedArrayBuffer,
using the region defined by (state.sabS11nOffset,
- state.sabS11nOffset]. Only one dataset is recorded at a time.
+ state.sabS11nOffset + state.sabS11nSize]. Only one dataset is
+ recorded at a time.
This is not a general-purpose format. It only supports the
range of operations, and data sizes, needed by the
@@ -831,22 +839,19 @@ const installOpfsVfs = function callee(options){
/* If it turns out that we need to adjust for timezone, see:
https://stackoverflow.com/a/11760121/1458521 */
wasm.poke(pOut, 2440587.5 + (new Date().getTime()/86400000),
- 'double');
+ 'double');
return 0;
},
xCurrentTimeInt64: function(pVfs,pOut){
- // TODO: confirm that this calculation is correct
wasm.poke(pOut, (2440587.5 * 86400000) + new Date().getTime(),
- 'i64');
+ 'i64');
return 0;
},
xDelete: function(pVfs, zName, doSyncDir){
mTimeStart('xDelete');
- opRun('xDelete', wasm.cstrToJs(zName), doSyncDir, false);
- /* We're ignoring errors because we cannot yet differentiate
- between harmless and non-harmless failures. */
+ const rc = opRun('xDelete', wasm.cstrToJs(zName), doSyncDir, false);
mTimeEnd();
- return 0;
+ return rc;
},
xFullPathname: function(pVfs,zName,nOut,pOut){
/* Until/unless we have some notion of "current dir"
@@ -1088,7 +1093,7 @@ const installOpfsVfs = function callee(options){
propagate any exception on error, rather than returning false.
*/
opfsUtil.unlink = async function(fsEntryName, recursive = false,
- throwOnError = false){
+ throwOnError = false){
try {
const [hDir, filenamePart] =
await opfsUtil.getDirForFilename(fsEntryName, false);
@@ -1163,11 +1168,41 @@ const installOpfsVfs = function callee(options){
doDir(opt.directory, 0);
};
- //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.
+ /**
+ Asynchronously imports the given bytes (a byte array or
+ ArrayBuffer) into the given database file.
+
+ It very specifically requires the input to be an SQLite3
+ database and throws if that's not the case. It does so in
+ order to prevent this function from taking on a larger scope
+ than it is specifically intended to. i.e. we do not want it to
+ become a convenience for importing arbitrary files into OPFS.
+
+ Throws on error. Resolves to the number of bytes written.
+ */
+ opfsUtil.importDb = async function(filename, bytes){
+ if(bytes instanceof ArrayBuffer) bytes = new Uint8Array(bytes);
+ const n = bytes.byteLength;
+ if(n<512 || n%512!=0){
+ toss("Byte array size is invalid for an SQLite db.");
+ }
+ const header = "SQLite format 3";
+ for(let i = 0; i < header.length; ++i){
+ if( header.charCodeAt(i) !== bytes[i] ){
+ toss("Input does not contain an SQLite database header.");
+ }
+ }
+ const [hDir, fnamePart] = await opfsUtil.getDirForFilename(filename, true);
+ const hFile = await hDir.getFileHandle(fnamePart, {create:true});
+ const sah = await hFile.createSyncAccessHandle();
+ sah.truncate(0);
+ const nWrote = sah.write(bytes, {at: 0});
+ sah.close();
+ if(nWrote != n){
+ toss("Expected to write "+n+" bytes but wrote "+nWrote+".");
+ }
+ return nWrote;
+ };
if(sqlite3.oo1){
const OpfsDb = function(...args){
@@ -1177,6 +1212,7 @@ const installOpfsVfs = function callee(options){
};
OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype);
sqlite3.oo1.OpfsDb = OpfsDb;
+ OpfsDb.importDb = opfsUtil.importDb;
sqlite3.oo1.DB.dbCtorHelper.setVfsPostOpenSql(
opfsVfs.pointer,
function(oo1Db, sqlite3){
@@ -1185,19 +1221,23 @@ const installOpfsVfs = function callee(options){
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.
+ /* As of July 2023, the PERSIST journal mode on OPFS is
+ somewhat slower than DELETE or TRUNCATE (it was faster
+ before Chrome version 108 or 109). TRUNCATE and DELETE
+ have very similar performance on OPFS.
- For later analysis: Roy Hashimoto notes that TRUNCATE
- and PERSIST modes may decrease OPFS concurrency because
- multiple connections can open the journal file in those
- modes:
+ Roy Hashimoto notes that TRUNCATE and PERSIST modes may
+ decrease OPFS concurrency because multiple connections
+ can open the journal file in those modes:
https://github.com/rhashimoto/wa-sqlite/issues/68
+
+ Given that, and the fact that testing has not revealed
+ any appreciable difference between performance of
+ TRUNCATE and DELETE modes on OPFS, we currently (as of
+ 2023-07-13) default to DELETE mode.
*/
- "pragma journal_mode=persist;",
+ "pragma journal_mode=DELETE;",
/*
This vfs benefits hugely from cache on moderate/large
speedtest1 --size 50 and --size 100 workloads. We
@@ -1318,10 +1358,10 @@ const installOpfsVfs = function callee(options){
sqlite3.opfs = opfsUtil;
opfsUtil.rootDirectory = d;
log("End of OPFS sqlite3_vfs setup.", opfsVfs);
- promiseResolve(sqlite3);
+ promiseResolve();
}).catch(promiseReject);
}else{
- promiseResolve(sqlite3);
+ promiseResolve();
}
}catch(e){
error(e);
@@ -1358,7 +1398,10 @@ globalThis.sqlite3ApiBootstrap.initializersAsync.push(async (sqlite3)=>{
});
}catch(e){
sqlite3.config.error("installOpfsVfs() exception:",e);
- throw e;
+ return Promise.reject(e);
}
});
}/*sqlite3ApiBootstrap.initializers.push()*/);
+//#else
+/* The OPFS VFS parts are elided from builds targeting node.js. */
+//#endif target=node
diff --git a/ext/wasm/api/sqlite3-wasi.h b/ext/wasm/api/sqlite3-wasi.h
deleted file mode 100644
index 096f45dfe..000000000
--- a/ext/wasm/api/sqlite3-wasi.h
+++ /dev/null
@@ -1,69 +0,0 @@
-/**
- Dummy function stubs to get sqlite3.c compiling with
- wasi-sdk. This requires, in addition:
-
- -D_WASI_EMULATED_MMAN -D_WASI_EMULATED_GETPID
-
- -lwasi-emulated-getpid
-*/
-typedef unsigned mode_t;
-int fchmod(int fd, mode_t mode);
-int fchmod(int fd, mode_t mode){
- return (fd && mode) ? 0 : 0;
-}
-typedef unsigned uid_t;
-typedef uid_t gid_t;
-int fchown(int fd, uid_t owner, gid_t group);
-int fchown(int fd, uid_t owner, gid_t group){
- return (fd && owner && group) ? 0 : 0;
-}
-uid_t geteuid(void);
-uid_t geteuid(void){return 0;}
-#if !defined(F_WRLCK)
-enum {
-F_WRLCK,
-F_RDLCK,
-F_GETLK,
-F_SETLK,
-F_UNLCK
-};
-#endif
-
-#undef HAVE_PREAD
-
-#include
-#define WASM__KEEP __attribute__((used))
-
-#if 0
-/**
- wasi-sdk cannot build sqlite3's default VFS without at least the following
- functions. They are apparently syscalls which clients have to implement or
- otherwise obtain.
-
- https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md
-*/
-environ_get
-environ_sizes_get
-clock_time_get
-fd_close
-fd_fdstat_get
-fd_fdstat_set_flags
-fd_filestat_get
-fd_filestat_set_size
-fd_pread
-fd_prestat_get
-fd_prestat_dir_name
-fd_read
-fd_seek
-fd_sync
-fd_write
-path_create_directory
-path_filestat_get
-path_filestat_set_times
-path_open
-path_readlink
-path_remove_directory
-path_unlink_file
-poll_oneoff
-proc_exit
-#endif
diff --git a/ext/wasm/api/sqlite3-wasm.c b/ext/wasm/api/sqlite3-wasm.c
index dbe594dc3..fcfbc0692 100644
--- a/ext/wasm/api/sqlite3-wasm.c
+++ b/ext/wasm/api/sqlite3-wasm.c
@@ -141,9 +141,6 @@
#ifndef SQLITE_OMIT_UTF16
# define SQLITE_OMIT_UTF16 1
#endif
-#ifndef SQLITE_OMIT_WAL
-# define SQLITE_OMIT_WAL 1
-#endif
#ifndef SQLITE_OS_KV_OPTIONAL
# define SQLITE_OS_KV_OPTIONAL 1
#endif
@@ -151,7 +148,7 @@
/**********************************************************************/
/* SQLITE_T... */
#ifndef SQLITE_TEMP_STORE
-# define SQLITE_TEMP_STORE 3
+# define SQLITE_TEMP_STORE 2
#endif
#ifndef SQLITE_THREADSAFE
# define SQLITE_THREADSAFE 0
@@ -295,7 +292,7 @@ SQLITE_WASM_EXPORT void * sqlite3_wasm_pstack_ptr(void){
*/
SQLITE_WASM_EXPORT void sqlite3_wasm_pstack_restore(unsigned char * p){
assert(p>=PStack.pBegin && p<=PStack.pEnd && p>=PStack.pPos);
- assert(0==(p & 0x7));
+ assert(0==((unsigned long long)p & 0x7));
if(p>=PStack.pBegin && p<=PStack.pEnd /*&& p>=PStack.pPos*/){
PStack.pPos = p;
}
@@ -1353,6 +1350,13 @@ int sqlite3_wasm_db_serialize( sqlite3 *pDb, const char *zSchema,
** This function is NOT part of the sqlite3 public API. It is strictly
** for use by the sqlite project's own JS/WASM bindings.
**
+** ACHTUNG: it was discovered on 2023-08-11 that, with SQLITE_DEBUG,
+** this function's out-of-scope use of the sqlite3_vfs/file/io_methods
+** APIs leads to triggering of assertions in the core library. Its use
+** is now deprecated and VFS-specific APIs for importing files need to
+** be found to replace it. sqlite3_wasm_posix_create_file() is
+** suitable for the "unix" family of VFSes.
+**
** Creates a new file using the I/O API of the given VFS, containing
** the given number of bytes of the given data. If the file exists, it
** is truncated to the given length and populated with the given
@@ -1398,7 +1402,14 @@ int sqlite3_wasm_vfs_create_file( sqlite3_vfs *pVfs,
int rc;
sqlite3_file *pFile = 0;
sqlite3_io_methods const *pIo;
- const int openFlags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
+ const int openFlags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE
+#if 0 && defined(SQLITE_DEBUG)
+ | SQLITE_OPEN_MAIN_JOURNAL
+ /* ^^^^ This is for testing a horrible workaround to avoid
+ triggering a specific assert() in os_unix.c:unixOpen(). Please
+ do not enable this in real builds. */
+#endif
+ ;
int flagsOut = 0;
int fileExisted = 0;
int doUnlock = 0;
@@ -1464,6 +1475,34 @@ int sqlite3_wasm_vfs_create_file( sqlite3_vfs *pVfs,
return rc;
}
+/**
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** Creates or overwrites a file using the POSIX file API,
+** i.e. Emscripten's virtual filesystem. Creates or truncates
+** zFilename, appends pData bytes to it, and returns 0 on success or
+** SQLITE_IOERR on error.
+*/
+SQLITE_WASM_EXPORT
+int sqlite3_wasm_posix_create_file( const char *zFilename,
+ const unsigned char * pData,
+ int nData ){
+ int rc;
+ FILE * pFile = 0;
+ int fileExisted = 0;
+ size_t nWrote = 1;
+
+ if( !zFilename || nData<0 || (pData==0 && nData>0) ) return SQLITE_MISUSE;
+ pFile = fopen(zFilename, "w");
+ if( 0==pFile ) return SQLITE_IOERR;
+ if( nData>0 ){
+ nWrote = fwrite(pData, (size_t)nData, 1, pFile);
+ }
+ fclose(pFile);
+ return 1==nWrote ? 0 : SQLITE_IOERR;
+}
+
/*
** This function is NOT part of the sqlite3 public API. It is strictly
** for use by the sqlite project's own JS/WASM bindings.
diff --git a/ext/wasm/api/sqlite3-worker1-promiser.c-pp.js b/ext/wasm/api/sqlite3-worker1-promiser.c-pp.js
index 48a74d472..bad599673 100644
--- a/ext/wasm/api/sqlite3-worker1-promiser.c-pp.js
+++ b/ext/wasm/api/sqlite3-worker1-promiser.c-pp.js
@@ -196,10 +196,9 @@ globalThis.sqlite3Worker1Promiser = function callee(config = callee.defaultConfi
if(1===arguments.length){
msg = arguments[0];
}else if(2===arguments.length){
- msg = {
- type: arguments[0],
- args: arguments[1]
- };
+ msg = Object.create(null);
+ msg.type = arguments[0];
+ msg.args = arguments[1];
}else{
toss("Invalid arugments for sqlite3Worker1Promiser()-created factory.");
}
diff --git a/ext/wasm/common/SqliteTestUtil.js b/ext/wasm/common/SqliteTestUtil.js
index 5ed423785..2c17824c5 100644
--- a/ext/wasm/common/SqliteTestUtil.js
+++ b/ext/wasm/common/SqliteTestUtil.js
@@ -156,7 +156,6 @@
}
};
-
/**
This is a module object for use with the emscripten-installed
sqlite3InitModule() factory function.
diff --git a/ext/wasm/common/testing.css b/ext/wasm/common/testing.css
index fb44f1d61..a9be76764 100644
--- a/ext/wasm/common/testing.css
+++ b/ext/wasm/common/testing.css
@@ -40,8 +40,41 @@ span.labeled-input {
.tests-pass { background-color: green; color: white }
.tests-fail { background-color: red; color: yellow }
.faded { opacity: 0.5; }
-.group-start { color: blue; }
-.group-end { color: blue; }
+.group-start {
+ color: blue;
+ background-color: skyblue;
+ font-weight: bold;
+ border-top: 1px dotted blue;
+ padding: 0.5em;
+ margin-top: 0.5em;
+}
+.group-end {
+ padding: 0.5em;
+ margin-bottom: 0.25em;
+ /*border-bottom: 1px dotted blue;*/
+}
+.group-end.green {
+ background: lightgreen;
+ border-bottom: 1px dotted green;
+}
+.one-test-line, .skipping-group {
+ margin-left: 3em;
+}
+.skipping-test, .skipping-group {
+ padding: 0.25em 0.5em;
+ background-color: #ffff73;
+}
+.skipping-test {
+ margin-left: 6em;
+}
+.one-test-summary {
+ margin-left: 6em;
+}
+.full-test-summary {
+ padding-bottom: 0.5em;
+ padding-top: 0.5em;
+ border-top: 1px solid black;
+}
.input-wrapper {
white-space: nowrap;
display: flex;
diff --git a/ext/wasm/common/whwasmutil.js b/ext/wasm/common/whwasmutil.js
index 489979941..0437ef35d 100644
--- a/ext/wasm/common/whwasmutil.js
+++ b/ext/wasm/common/whwasmutil.js
@@ -111,7 +111,8 @@
of `target.instance` (a WebAssembly.Module instance) and it must
contain the symbols exported by the WASM module associated with
this code. In an Enscripten environment it must be set to
- `Module['asm']`. The exports object must contain a minimum of the
+ `Module['asm']` (versions <=3.1.43) or `wasmExports` (versions
+ >=3.1.44). The exports object must contain a minimum of the
following symbols:
- `memory`: a WebAssembly.Memory object representing the WASM
@@ -174,7 +175,7 @@
globalThis.WhWasmUtilInstaller = function(target){
'use strict';
if(undefined===target.bigIntEnabled){
- target.bigIntEnabled = !!self['BigInt64Array'];
+ target.bigIntEnabled = !!globalThis['BigInt64Array'];
}
/** Throws a new Error, the message of which is the concatenation of
@@ -355,8 +356,8 @@ globalThis.WhWasmUtilInstaller = function(target){
break;
default:
if(target.bigIntEnabled){
- if(n===self['BigUint64Array']) return c.HEAP64U;
- else if(n===self['BigInt64Array']) return c.HEAP64;
+ if(n===globalThis['BigUint64Array']) return c.HEAP64U;
+ else if(n===globalThis['BigInt64Array']) return c.HEAP64;
break;
}
}
@@ -612,8 +613,6 @@ globalThis.WhWasmUtilInstaller = function(target){
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().
@@ -1179,7 +1178,7 @@ globalThis.WhWasmUtilInstaller = function(target){
cache.scopedAlloc.splice(n,1);
for(let p; (p = state.pop()); ){
if(target.functionEntry(p)){
- //console.warn("scopedAllocPop() uninstalling transient function",p);
+ //console.warn("scopedAllocPop() uninstalling function",p);
target.uninstallFunction(p);
}
else target.dealloc(p);
@@ -1636,6 +1635,7 @@ globalThis.WhWasmUtilInstaller = function(target){
'and is not intended to be invoked from',
'client-level code. Invoked with:',opt);
}
+ this.name = opt.name || "unnamed";
this.signature = opt.signature;
if(opt.contextKey instanceof Function){
this.contextKey = opt.contextKey;
@@ -1656,25 +1656,11 @@ globalThis.WhWasmUtilInstaller = function(target){
? opt.callProxy : undefined;
}
- /** If true, the constructor emits a warning. The intent is that
- this be set to true after bootstrapping of the higher-level
- client library is complete, to warn downstream clients that
- they shouldn't be relying on this implemenation detail which
- does not have a stable interface. */
- static warnOnUse = false;
-
- /** If true, convertArg() will FuncPtrAdapter.debugOut() when it
- (un)installs a function binding to/from WASM. Note that
- deinstallation of bindScope=transient bindings happens
- via scopedAllocPop() so will not be output. */
- static debugFuncInstall = false;
-
- /** Function used for debug output. */
- static debugOut = console.debug.bind(console);
-
- static bindScopes = [
- 'transient', 'context', 'singleton', 'permanent'
- ];
+ /**
+ Note that static class members are defined outside of the class
+ to work around an emcc toolchain build problem: one of the
+ tools in emsdk v3.1.42 does not support the static keyword.
+ */
/* Dummy impl. Overwritten per-instance as needed. */
contextKey(argv,argIndex){
@@ -1711,14 +1697,16 @@ globalThis.WhWasmUtilInstaller = function(target){
exactly the 2nd and 3rd arguments are.
*/
convertArg(v,argv,argIndex){
- //FuncPtrAdapter.debugOut("FuncPtrAdapter.convertArg()",this.signature,this.transient,v);
+ //FuncPtrAdapter.debugOut("FuncPtrAdapter.convertArg()",this.name,this.signature,this.transient,v);
let pair = this.singleton;
if(!pair && this.isContext){
pair = this.contextMap(this.contextKey(argv,argIndex));
+ //FuncPtrAdapter.debugOut(this.name, this.signature, "contextKey() =",this.contextKey(argv,argIndex), pair);
}
if(pair && pair[0]===v) return pair[1];
if(v instanceof Function){
/* Install a WASM binding and return its pointer. */
+ //FuncPtrAdapter.debugOut("FuncPtrAdapter.convertArg()",this.name,this.signature,this.transient,v,pair);
if(this.callProxy) v = this.callProxy(v);
const fp = __installFunction(v, this.signature, this.isTransient);
if(FuncPtrAdapter.debugFuncInstall){
@@ -1732,7 +1720,18 @@ globalThis.WhWasmUtilInstaller = function(target){
FuncPtrAdapter.debugOut("FuncPtrAdapter uninstalling", this,
this.contextKey(argv,argIndex), '@'+pair[1], v);
}
- try{target.uninstallFunction(pair[1])}
+ try{
+ /* Because the pending native call might rely on the
+ pointer we're replacing, e.g. as is normally the case
+ with sqlite3's xDestroy() methods, we don't
+ immediately uninstall but instead add its pointer to
+ the scopedAlloc stack, which will be cleared when the
+ xWrap() mechanism is done calling the native
+ function. We're relying very much here on xWrap()
+ having pushed an alloc scope.
+ */
+ cache.scopedAlloc[cache.scopedAlloc.length-1].push(pair[1]);
+ }
catch(e){/*ignored*/}
}
pair[0] = v;
@@ -1740,13 +1739,14 @@ globalThis.WhWasmUtilInstaller = function(target){
}
return fp;
}else if(target.isPtr(v) || null===v || undefined===v){
+ //FuncPtrAdapter.debugOut("FuncPtrAdapter.convertArg()",this.name,this.signature,this.transient,v,pair);
if(pair && pair[1] && pair[1]!==v){
/* uninstall stashed mapping and replace stashed mapping with v. */
if(FuncPtrAdapter.debugFuncInstall){
FuncPtrAdapter.debugOut("FuncPtrAdapter uninstalling", this,
this.contextKey(argv,argIndex), '@'+pair[1], v);
}
- try{target.uninstallFunction(pair[1])}
+ try{ cache.scopedAlloc[cache.scopedAlloc.length-1].push(pair[1]) }
catch(e){/*ignored*/}
pair[0] = pair[1] = (v | 0);
}
@@ -1761,6 +1761,26 @@ globalThis.WhWasmUtilInstaller = function(target){
}/*convertArg()*/
}/*FuncPtrAdapter*/;
+ /** If true, the constructor emits a warning. The intent is that
+ this be set to true after bootstrapping of the higher-level
+ client library is complete, to warn downstream clients that
+ they shouldn't be relying on this implemenation detail which
+ does not have a stable interface. */
+ xArg.FuncPtrAdapter.warnOnUse = false;
+
+ /** If true, convertArg() will FuncPtrAdapter.debugOut() when it
+ (un)installs a function binding to/from WASM. Note that
+ deinstallation of bindScope=transient bindings happens
+ via scopedAllocPop() so will not be output. */
+ xArg.FuncPtrAdapter.debugFuncInstall = false;
+
+ /** Function used for debug output. */
+ xArg.FuncPtrAdapter.debugOut = console.debug.bind(console);
+
+ xArg.FuncPtrAdapter.bindScopes = [
+ 'transient', 'context', 'singleton', 'permanent'
+ ];
+
const __xArgAdapterCheck =
(t)=>xArg.get(t) || toss("Argument adapter not found:",t);
diff --git a/ext/wasm/demo-123.js b/ext/wasm/demo-123.js
index 311afcc82..8e03ee80f 100644
--- a/ext/wasm/demo-123.js
+++ b/ext/wasm/demo-123.js
@@ -235,10 +235,10 @@
- get change count (total or statement-local, 32- or 64-bit)
- get a DB's file name
-
+
Misc. Stmt features:
- - Various forms of bind()
+ - Various forms of bind()
- clearBindings()
- reset()
- Various forms of step()
diff --git a/ext/wasm/demo-worker1-promiser.js b/ext/wasm/demo-worker1-promiser.js
index e8d3d4e5a..c2d24623a 100644
--- a/ext/wasm/demo-worker1-promiser.js
+++ b/ext/wasm/demo-worker1-promiser.js
@@ -64,15 +64,17 @@
callback = msgArgs;
msgArgs = undefined;
}
- const p = workerPromise({type: msgType, args:msgArgs});
+ const p = 1
+ ? workerPromise({type: msgType, args:msgArgs})
+ : workerPromise(msgType, msgArgs);
return callback ? p.then(callback).finally(testCount) : p;
};
+ let sqConfig;
const runTests = async function(){
const dbFilename = '/testing2.sqlite3';
startTime = performance.now();
- let sqConfig;
await wtest('config-get', (ev)=>{
const r = ev.result;
log('sqlite3.config subset:', r);
@@ -102,12 +104,15 @@
sql: ["create table t(a,b)",
"insert into t(a,b) values(1,2),(3,4),(5,6)"
].join(';'),
- multi: true,
- resultRows: [], columnNames: []
+ resultRows: [], columnNames: [],
+ countChanges: sqConfig.bigIntEnabled ? 64 : true
}, function(ev){
ev = ev.result;
T.assert(0===ev.resultRows.length)
- .assert(0===ev.columnNames.length);
+ .assert(0===ev.columnNames.length)
+ .assert(sqConfig.bigIntEnabled
+ ? (3n===ev.changeCount)
+ : (3===ev.changeCount));
});
await wtest('exec',{
@@ -125,12 +130,14 @@
await wtest('exec',{
sql: 'select a a, b b from t order by a',
resultRows: [], columnNames: [],
- rowMode: 'object'
+ rowMode: 'object',
+ countChanges: true
}, function(ev){
ev = ev.result;
T.assert(3===ev.resultRows.length)
.assert(1===ev.resultRows[0].a)
.assert(6===ev.resultRows[2].b)
+ .assert(0===ev.changeCount);
});
await wtest(
@@ -143,12 +150,13 @@
await wtest('exec',{
sql:'select 1 union all select 3',
- resultRows: [],
+ resultRows: []
}, function(ev){
ev = ev.result;
T.assert(2 === ev.resultRows.length)
.assert(1 === ev.resultRows[0][0])
- .assert(3 === ev.resultRows[1][0]);
+ .assert(3 === ev.resultRows[1][0])
+ .assert(undefined === ev.changeCount);
});
const resultRowTest1 = function f(ev){
@@ -218,13 +226,12 @@
});
await wtest('exec',{
- multi: true,
sql:[
'pragma foreign_keys=0;',
// ^^^ arbitrary query with no result columns
'select a, b from t order by a desc; select a from t;'
- // multi-exec only honors results from the first
- // statement with result columns (regardless of whether)
+ // exec() only honors SELECT results from the first
+ // statement with result columns (regardless of whether
// it has any rows).
],
rowMode: 1,
diff --git a/ext/wasm/demo-worker1.js b/ext/wasm/demo-worker1.js
index f70179c5e..60f5e8dec 100644
--- a/ext/wasm/demo-worker1.js
+++ b/ext/wasm/demo-worker1.js
@@ -209,8 +209,8 @@
// ^^^ arbitrary query with no result columns
"select a, b from t order by a desc;",
"select a from t;"
- // multi-statement exec only honors results from the first
- // statement with result columns (regardless of whether)
+ // exec() only honors SELECT results from the first
+ // statement with result columns (regardless of whether
// it has any rows).
],
rowMode: 1,
diff --git a/ext/wasm/dist.make b/ext/wasm/dist.make
index 3f99ad5a5..5d610e37b 100644
--- a/ext/wasm/dist.make
+++ b/ext/wasm/dist.make
@@ -38,7 +38,7 @@ dist-name := $(dist-name-prefix)-TEMP
# 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.build ?= oz
dist-dir.top := $(dist-name)
dist-dir.jswasm := $(dist-dir.top)/$(notdir $(dir.dout))
diff --git a/ext/wasm/fiddle.make b/ext/wasm/fiddle.make
index cbe6ab351..57141d7e2 100644
--- a/ext/wasm/fiddle.make
+++ b/ext/wasm/fiddle.make
@@ -18,7 +18,7 @@ endif
ifeq (,$(SHELL_SRC))
$(error Could not parse SHELL_SRC from $(dir.top)/Makefile.)
endif
-$(dir.top)/shell.c: $(SHELL_SRC) $(dir.top)/tool/mkshellc.tcl
+$(dir.top)/shell.c: $(SHELL_SRC) $(dir.top)/tool/mkshellc.tcl $(sqlite3.c)
$(MAKE) -C $(dir.top) shell.c
# /shell.c
########################################################################
@@ -61,9 +61,9 @@ $(fiddle.SOAP.js): $(SOAP.js)
$(eval $(call call-make-pre-post,fiddle-module,vanilla))
$(fiddle-module.js): $(MAKEFILE) $(MAKEFILE.fiddle) \
$(EXPORTED_FUNCTIONS.fiddle) \
- $(fiddle.cses) $(pre-post-fiddle-module.deps.vanilla) $(fiddle.SOAP.js)
+ $(fiddle.cses) $(pre-post-fiddle-module-vanilla.deps) $(fiddle.SOAP.js)
$(emcc.bin) -o $@ $(fiddle.emcc-flags) \
- $(pre-post-fiddle-module.flags.vanilla) \
+ $(pre-post-fiddle-module-vanilla.flags) \
$(fiddle.cses)
$(maybe-wasm-strip) $(fiddle-module.wasm)
gzip < $@ > $@.gz
diff --git a/ext/wasm/fiddle/fiddle-worker.js b/ext/wasm/fiddle/fiddle-worker.js
index e239cbf51..67f61008f 100644
--- a/ext/wasm/fiddle/fiddle-worker.js
+++ b/ext/wasm/fiddle/fiddle-worker.js
@@ -221,7 +221,7 @@
f._();
}
};
-
+
self.onmessage = function f(ev){
ev = ev.data;
if(!f.cache){
@@ -371,12 +371,14 @@
sqlite3InitModule(fiddleModule).then((_sqlite3)=>{
sqlite3 = _sqlite3;
console.warn("Installing sqlite3 module globally (in Worker)",
- "for use in the dev console.");
- self.sqlite3 = sqlite3;
+ "for use in the dev console.", sqlite3);
+ globalThis.sqlite3 = sqlite3;
const dbVfs = sqlite3.wasm.xWrap('fiddle_db_vfs', "*", ['string']);
fiddleModule.fsUnlink = (fn)=>{
return sqlite3.wasm.sqlite3_wasm_vfs_unlink(dbVfs(0), fn);
};
wMsg('fiddle-ready');
- })/*then()*/;
+ }).catch(e=>{
+ console.error("Fiddle worker init failed:",e);
+ });
})();
diff --git a/ext/wasm/index.html b/ext/wasm/index.html
index a9872a9ea..70ce0441e 100644
--- a/ext/wasm/index.html
+++ b/ext/wasm/index.html
@@ -93,9 +93,13 @@
speedtest1: a main-thread WASM build of speedtest1.
sqlite3_js_db_vfs: 'wasm:/api-c-style.md#sqlite3_js_db_vfs',
sqlite3_js_kvvfs_clear: 'wasm:/api-c-style.md#sqlite3_js_kvvfs',
sqlite3_js_kvvfs_size: 'wasm:/api-c-style.md#sqlite3_js_kvvfs',
+ sqlite3_js_posix_create_file: 'wasm:/api-c-style.md#sqlite3_js_posix_create_file',
sqlite3_js_rc_str: 'wasm:/api-c-style.md#sqlite3_js_rc_str',
sqlite3_js_vfs_create_file: 'wasm:/api-c-style.md#sqlite3_js_vfs_create_file',
sqlite3_js_vfs_list: 'wasm:/api-c-style.md#sqlite3_js_vfs_list',
diff --git a/ext/wasm/scratchpad-wasmfs-main.js b/ext/wasm/scratchpad-wasmfs-main.js
deleted file mode 100644
index 56f9325de..000000000
--- a/ext/wasm/scratchpad-wasmfs-main.js
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- 2022-05-22
-
- The author disclaims copyright to this source code. In place of a
- legal notice, here is a blessing:
-
- * May you do good and not evil.
- * May you find forgiveness for yourself and forgive others.
- * May you share freely, never taking more than you give.
-
- ***********************************************************************
-
- A basic test script for sqlite3-api.js. This file must be run in
- main JS thread and sqlite3.js must have been loaded before it.
-*/
-'use strict';
-(function(){
- const toss = function(...args){throw new Error(args.join(' '))};
- const log = console.log.bind(console),
- warn = console.warn.bind(console),
- error = console.error.bind(console);
-
- const stdout = log;
- const stderr = error;
-
- const test1 = function(db){
- db.exec("create table if not exists t(a);")
- .transaction(function(db){
- db.prepare("insert into t(a) values(?)")
- .bind(new Date().getTime())
- .stepFinalize();
- stdout("Number of values in table t:",
- db.selectValue("select count(*) from t"));
- });
- };
-
- const runTests = function(sqlite3){
- const capi = sqlite3.capi,
- oo = sqlite3.oo1,
- wasm = sqlite3.wasm;
- stdout("Loaded sqlite3:",capi.sqlite3_libversion(), capi.sqlite3_sourceid());
- const persistentDir = capi.sqlite3_wasmfs_opfs_dir();
- if(persistentDir){
- stdout("Persistent storage dir:",persistentDir);
- }else{
- stderr("No persistent storage available.");
- }
- const startTime = performance.now();
- let db;
- try {
- db = new oo.DB(persistentDir+'/foo.db');
- stdout("DB filename:",db.filename);
- const banner1 = '>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>',
- banner2 = '<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<';
- [
- test1
- ].forEach((f)=>{
- const n = performance.now();
- stdout(banner1,"Running",f.name+"()...");
- f(db, sqlite3);
- stdout(banner2,f.name+"() took ",(performance.now() - n),"ms");
- });
- }finally{
- if(db) db.close();
- }
- stdout("Total test time:",(performance.now() - startTime),"ms");
- };
-
- sqlite3InitModule(self.sqlite3TestModule).then(runTests);
-})();
diff --git a/ext/wasm/scratchpad-wasmfs-main.html b/ext/wasm/scratchpad-wasmfs.html
similarity index 54%
rename from ext/wasm/scratchpad-wasmfs-main.html
rename to ext/wasm/scratchpad-wasmfs.html
index 91f61526c..c37febff1 100644
--- a/ext/wasm/scratchpad-wasmfs-main.html
+++ b/ext/wasm/scratchpad-wasmfs.html
@@ -10,20 +10,6 @@
sqlite3 WASMFS/OPFS Main-thread Scratchpad
-
-
-
-
Initializing app...
-
- On a slow internet connection this may take a moment. If this
- message displays for "a long time", intialization may have
- failed and the JavaScript console may contain clues as to why.
-
-
-
Downloading...
-
-
-
Scratchpad/test app for the WASMF/OPFS integration in the
main window thread. This page requires that the sqlite3 API have
been built with WASMFS support. If OPFS support is available then
@@ -33,8 +19,12 @@
All stuff on this page happens in the dev console.