1
0
mirror of https://github.com/sqlite/sqlite.git synced 2025-07-30 19:03:16 +03:00

Initial support for virtual tables implemented in JavaScript.

FossilOrigin-Name: a1454744c770a30a32a6d7b7fc59ef7be48cf67348b238540592850d7c2c7757
This commit is contained in:
stephan
2022-12-08 15:00:53 +00:00
23 changed files with 1859 additions and 967 deletions

View File

@ -289,7 +289,7 @@ sqlite3-api.jses += $(dir.api)/sqlite3-api-glue.js
sqlite3-api.jses += $(sqlite3-api-build-version.js)
sqlite3-api.jses += $(dir.api)/sqlite3-api-oo1.js
sqlite3-api.jses += $(dir.api)/sqlite3-api-worker1.js
sqlite3-api.jses += $(dir.api)/sqlite3-vfs-helper.js
sqlite3-api.jses += $(dir.api)/sqlite3-v-helper.js
sqlite3-api.jses += $(dir.api)/sqlite3-vfs-opfs.c-pp.js
sqlite3-api.jses += $(dir.api)/sqlite3-api-cleanup.js

View File

@ -6,12 +6,15 @@ _sqlite3_bind_int64
_sqlite3_bind_null
_sqlite3_bind_parameter_count
_sqlite3_bind_parameter_index
_sqlite3_bind_pointer
_sqlite3_bind_text
_sqlite3_busy_handler
_sqlite3_busy_timeout
_sqlite3_changes
_sqlite3_changes64
_sqlite3_clear_bindings
_sqlite3_close_v2
_sqlite3_collation_needed
_sqlite3_column_blob
_sqlite3_column_bytes
_sqlite3_column_count
@ -24,14 +27,21 @@ _sqlite3_column_text
_sqlite3_column_type
_sqlite3_compileoption_get
_sqlite3_compileoption_used
_sqlite3_complete
_sqlite3_create_collation
_sqlite3_create_collation_v2
_sqlite3_create_function
_sqlite3_create_function_v2
_sqlite3_create_module
_sqlite3_create_module_v2
_sqlite3_create_window_function
_sqlite3_data_count
_sqlite3_db_filename
_sqlite3_db_handle
_sqlite3_db_name
_sqlite3_declare_vtab
_sqlite3_deserialize
_sqlite3_drop_modules
_sqlite3_errmsg
_sqlite3_error_offset
_sqlite3_errstr
@ -42,6 +52,7 @@ _sqlite3_extended_result_codes
_sqlite3_file_control
_sqlite3_finalize
_sqlite3_free
_sqlite3_get_auxdata
_sqlite3_initialize
_sqlite3_libversion
_sqlite3_libversion_number
@ -50,6 +61,7 @@ _sqlite3_malloc64
_sqlite3_msize
_sqlite3_open
_sqlite3_open_v2
_sqlite3_overload_function
_sqlite3_prepare_v2
_sqlite3_prepare_v3
_sqlite3_randomness
@ -65,16 +77,20 @@ _sqlite3_result_error_toobig
_sqlite3_result_int
_sqlite3_result_int64
_sqlite3_result_null
_sqlite3_result_pointer
_sqlite3_result_text
_sqlite3_result_zeroblob
_sqlite3_result_zeroblob64
_sqlite3_serialize
_sqlite3_set_auxdata
_sqlite3_shutdown
_sqlite3_sourceid
_sqlite3_sql
_sqlite3_step
_sqlite3_strglob
_sqlite3_stricmp
_sqlite3_strlike
_sqlite3_strnicmp
_sqlite3_total_changes
_sqlite3_total_changes64
_sqlite3_trace_v2
@ -88,11 +104,20 @@ _sqlite3_value_bytes
_sqlite3_value_double
_sqlite3_value_int
_sqlite3_value_int64
_sqlite3_value_pointer
_sqlite3_value_text
_sqlite3_value_type
_sqlite3_vfs_find
_sqlite3_vfs_register
_sqlite3_vfs_unregister
_sqlite3_vtab_collation
_sqlite3_vtab_distinct
_sqlite3_vtab_in
_sqlite3_vtab_in_first
_sqlite3_vtab_in_next
_sqlite3_vtab_nochange
_sqlite3_vtab_on_conflict
_sqlite3_vtab_rhs_value
_malloc
_free
_realloc

View File

@ -78,11 +78,10 @@ browser client:
a Promise-based interface into the Worker #1 API. This is
a far user-friendlier way to interface with databases running
in a Worker thread.
- **`sqlite3-vfs-helper.js`**\
This internal-use-only file installs `sqlite3.VfsHelper` for use by
`sqlite3-*.js` files which create `sqlite3_vfs` implementations.
`sqlite3.VfsHelper` gets removed from the the `sqlite3` object after
the library is finished initializing.
- **`sqlite3-v-helper.js`**\
Installs `sqlite3.VfsHelper` and `sqlite3.VtabHelper` 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

View File

@ -31,33 +31,50 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
heap: 0 ? wasm.memory : wasm.heap8u,
alloc: wasm.alloc,
dealloc: wasm.dealloc,
functionTable: wasm.functionTable,
bigIntEnabled: wasm.bigIntEnabled,
memberPrefix: '$'
memberPrefix: /* Never change this: this prefix is baked into any
amount of code and client-facing docs. */ '$'
});
delete self.Jaccwabyt;
if(0){
/* "The problem" is that the following isn't even remotely
type-safe. OTOH, nothing about WASM pointers is. */
const argPointer = wasm.xWrap.argAdapter('*');
wasm.xWrap.argAdapter('StructType', (v)=>{
if(v && v.constructor && v instanceof StructBinder.StructType){
v = v.pointer;
}
return wasm.isPtr(v)
? argPointer(v)
: toss("Invalid (object) type for StructType-type argument.");
});
}
{/* Convert Arrays and certain TypedArrays to strings for
'flexible-string'-type arguments */
'string:flexible'-type arguments */
const xString = wasm.xWrap.argAdapter('string');
wasm.xWrap.argAdapter(
'flexible-string', (v)=>xString(util.flexibleString(v))
'string:flexible', (v)=>xString(util.flexibleString(v))
);
}
/**
The 'string:static' argument adapter treats its argument as
either...
- WASM pointer: assumed to be a long-lived C-string which gets
returned as-is.
- Anything else: gets coerced to a JS string for use as a map
key. If a matching entry is found (as described next), it is
returned, else wasm.allocCString() is used to create a a new
string, map its pointer to (''+v) for the remainder of the
application's life, and returns that pointer value for this
call and all future calls which are passed a
string-equivalent argument.
Use case: sqlite3_bind_pointer() and sqlite3_result_pointer()
call for "a static string and preferably a string
literal". This converter is used to ensure that the string
value seen by those functions is long-lived and behaves as they
need it to.
*/
wasm.xWrap.argAdapter(
'string:static',
function(v){
if(wasm.isPtr(v)) return v;
v = ''+v;
let rc = this[v];
return rc || (rc = this[v] = wasm.allocCString(v));
}.bind(Object.create(null))
);
}/* special-case string-type argument conversions */
if(1){// WhWasmUtil.xWrap() bindings...
/**
@ -68,15 +85,21 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
`sqlite3_vfs*` via capi.sqlite3_vfs.pointer.
*/
const aPtr = wasm.xWrap.argAdapter('*');
const nilType = function(){};
wasm.xWrap.argAdapter('sqlite3_filename', aPtr)
('sqlite3_stmt*', aPtr)
('sqlite3_context*', aPtr)
('sqlite3_value*', aPtr)
('void*', aPtr)
('sqlite3*', (v)=>{
if(sqlite3.oo1 && v instanceof sqlite3.oo1.DB) v = v.pointer;
return aPtr(v);
})
('sqlite3*', (v)=>
aPtr((v instanceof (sqlite3?.oo1?.DB || nilType))
? v.pointer : v))
('sqlite3_index_info*', (v)=>
aPtr((v instanceof (capi.sqlite3_index_info || nilType))
? v.pointer : v))
('sqlite3_module*', (v)=>
aPtr((v instanceof (capi.sqlite3_module || nilType))
? v.pointer : v))
/**
`sqlite3_vfs*`:
@ -87,14 +110,13 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
*/
('sqlite3_vfs*', (v)=>{
if('string'===typeof v){
const x = capi.sqlite3_vfs_find(v);
/* A NULL sqlite3_vfs pointer will be treated as the default
VFS in many contexts. We specifically do not want that
behavior here. */
if(!x) sqlite3.SQLite3Error.toss("Unknown sqlite3_vfs name:",v);
return x;
}else if(v instanceof sqlite3.capi.sqlite3_vfs) v = v.pointer;
return aPtr(v);
return capi.sqlite3_vfs_find(v)
|| sqlite3.SQLite3Error.toss("Unknown sqlite3_vfs name:",v);
}
return aPtr((v instanceof capi.sqlite3_vfs) ? v.pointer : v);
});
wasm.xWrap.resultAdapter('sqlite3*', aPtr)
@ -127,7 +149,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
: fI64Disabled(e[0]);
}
/* There's no(?) need to expose bindingSignatures to clients,
/* There's no need to expose bindingSignatures to clients,
implicitly making it part of the public interface. */
delete wasm.bindingSignatures;
@ -141,21 +163,8 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
return errCode;
};
}
}/*xWrap() bindings*/;
/**
When registering a VFS and its related components it may be
necessary to ensure that JS keeps a reference to them to keep
them from getting garbage collected. Simply pass each such value
to this function and a reference will be held to it for the life
of the app.
*/
capi.sqlite3_vfs_register.addReference = function f(...args){
if(!f._) f._ = [];
f._.push(...args);
};
/**
Internal helper to assist in validating call argument counts in
the hand-written sqlite3_xyz() wrappers. We do this only for
@ -167,29 +176,9 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
(1===n?"":'s')+".");
};
/**
Helper for flexible-string conversions which require a
byte-length counterpart argument. Passed a value and its
ostensible length, this function returns [V,N], where V
is either v or a transformed copy of v and N is either n,
-1, or the byte length of v (if it's a byte array).
*/
const __flexiString = function(v,n){
if('string'===typeof v){
n = -1;
}else if(util.isSQLableTypedArray(v)){
n = v.byteLength;
v = util.typedArrayToString(v);
}else if(Array.isArray(v)){
v = v.join("");
n = -1;
}
return [v, n];
};
if(1){/* Special-case handling of sqlite3_exec() */
const __exec = wasm.xWrap("sqlite3_exec", "int",
["sqlite3*", "flexible-string", "*", "*", "**"]);
["sqlite3*", "string:flexible", "*", "*", "**"]);
/* Documented in the api object's initializer. */
capi.sqlite3_exec = function f(pDb, sql, callback, pVoid, pErrMsg){
if(f.length!==arguments.length){
@ -204,8 +193,8 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
try {
let aVals = [], aNames = [], i = 0, offset = 0;
for( ; i < nCols; offset += (wasm.ptrSizeof * ++i) ){
aVals.push( wasm.cstringToJs(wasm.getPtrValue(pColVals + offset)) );
aNames.push( wasm.cstringToJs(wasm.getPtrValue(pColNames + offset)) );
aVals.push( wasm.cstrToJs(wasm.getPtrValue(pColVals + offset)) );
aNames.push( wasm.cstrToJs(wasm.getPtrValue(pColNames + offset)) );
}
rc = callback(pVoid, nCols, aVals, aNames) | 0;
/* The first 2 args of the callback are useless for JS but
@ -540,6 +529,26 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
if(1){/* Special-case handling of sqlite3_prepare_v2() and
sqlite3_prepare_v3() */
/**
Helper for string:flexible conversions which require a
byte-length counterpart argument. Passed a value and its
ostensible length, this function returns [V,N], where V
is either v or a transformed copy of v and N is either n,
-1, or the byte length of v (if it's a byte array).
*/
const __flexiString = (v,n)=>{
if('string'===typeof v){
n = -1;
}else if(util.isSQLableTypedArray(v)){
n = v.byteLength;
v = util.typedArrayToString(v);
}else if(Array.isArray(v)){
v = v.join("");
n = -1;
}
return [v, n];
};
/**
Scope-local holder of the two impls of sqlite3_prepare_v2/v3().
*/
@ -570,7 +579,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
"int", ["sqlite3*", "*", "int", "int",
"**", "**"]);
/* Documented in the api object's initializer. */
/* Documented in the capi object's initializer. */
capi.sqlite3_prepare_v3 = function f(pDb, sql, sqlLen, prepFlags, ppStmt, pzTail){
if(f.length!==arguments.length){
return __dbArgcMismatch(pDb,"sqlite3_prepare_v3",f.length);
@ -587,7 +596,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
}
};
/* Documented in the api object's initializer. */
/* Documented in the capi object's initializer. */
capi.sqlite3_prepare_v2 = function f(pDb, sql, sqlLen, ppStmt, pzTail){
return (f.length===arguments.length)
? capi.sqlite3_prepare_v3(pDb, sql, sqlLen, 0, ppStmt, pzTail)
@ -601,15 +610,19 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
toss("Maintenance required: increase sqlite3_wasm_enum_json()'s",
"static buffer size!");
}
wasm.ctype = JSON.parse(wasm.cstringToJs(cJson));
wasm.ctype = JSON.parse(wasm.cstrToJs(cJson));
//console.debug('wasm.ctype length =',wasm.cstrlen(cJson));
for(const t of ['access', 'blobFinalizers', 'dataTypes',
'encodings', 'fcntl', 'flock', 'ioCap',
'limits',
'openFlags', 'prepareFlags', 'resultCodes',
'serialize', 'syncFlags', 'trace', 'udfFlags',
'version'
]){
const defineGroups = ['access', 'authorizer',
'blobFinalizers', 'dataTypes',
'encodings', 'fcntl', 'flock', 'ioCap',
'limits', 'openFlags',
'prepareFlags', 'resultCodes',
'serialize', 'syncFlags', 'trace', 'udfFlags',
'version' ];
if(wasm.bigIntEnabled){
defineGroups.push('vtab');
}
for(const t of defineGroups){
for(const e of Object.entries(wasm.ctype[t])){
// ^^^ [k,v] there triggers a buggy code transformation via
// one of the Emscripten-driven optimizers.
@ -629,19 +642,37 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
capi.sqlite3_js_rc_str = (rc)=>__rcMap[rc];
/* Bind all registered C-side structs... */
const notThese = Object.assign(Object.create(null),{
// Structs NOT to register
WasmTestStruct: true
// For each struct to NOT register, map its name to true:
WasmTestStruct: true,
/* We unregister the kvvfs VFS from Worker threads below. */
sqlite3_kvvfs_methods: !util.isUIThread(),
/* sqlite3_index_info and friends require int64: */
sqlite3_index_info: !wasm.bigIntEnabled,
sqlite3_index_constraint: !wasm.bigIntEnabled,
sqlite3_index_orderby: !wasm.bigIntEnabled,
sqlite3_index_constraint_usage: !wasm.bigIntEnabled
});
if(!util.isUIThread()){
/* We remove the kvvfs VFS from Worker threads below. */
notThese.sqlite3_kvvfs_methods = true;
}
for(const s of wasm.ctype.structs){
if(!notThese[s.name]){
capi[s.name] = sqlite3.StructBinder(s);
}
}
}/*end C constant imports*/
if(capi.sqlite3_index_info){
/* Move these inner structs into sqlite3_index_info. Binding
** them to WASM requires that we create global-scope structs to
** model them with, but those are no longer needed after we've
** passed them to StructBinder. */
for(const k of ['sqlite3_index_constraint',
'sqlite3_index_orderby',
'sqlite3_index_constraint_usage']){
capi.sqlite3_index_info[k] = capi[k];
delete capi[k];
}
capi.sqlite3_vtab_config =
(pDb, op, arg=0)=>wasm.exports.sqlite3_wasm_vtab_config(
wasm.xWrap.argAdapter('sqlite3*')(pDb), op, arg);
}/* end vtab-related setup */
}/*end C constant and struct imports*/
const pKvvfs = capi.sqlite3_vfs_find("kvvfs");
if( pKvvfs ){/* kvvfs-specific glue */
@ -652,8 +683,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
delete capi.sqlite3_kvvfs_methods;
const kvvfsMakeKey = wasm.exports.sqlite3_wasm_kvvfsMakeKeyOnPstack,
pstack = wasm.pstack,
pAllocRaw = wasm.exports.sqlite3_wasm_pstack_alloc;
pstack = wasm.pstack;
const kvvfsStorage = (zClass)=>
((115/*=='s'*/===wasm.getMemValue(zClass))
@ -672,7 +702,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
try {
const zXKey = kvvfsMakeKey(zClass,zKey);
if(!zXKey) return -3/*OOM*/;
const jKey = wasm.cstringToJs(zXKey);
const jKey = wasm.cstrToJs(zXKey);
const jV = kvvfsStorage(zClass).getItem(jKey);
if(!jV) return -1;
const nV = jV.length /* Note that we are relying 100% on v being
@ -701,8 +731,8 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
try {
const zXKey = kvvfsMakeKey(zClass,zKey);
if(!zXKey) return 1/*OOM*/;
const jKey = wasm.cstringToJs(zXKey);
kvvfsStorage(zClass).setItem(jKey, wasm.cstringToJs(zData));
const jKey = wasm.cstrToJs(zXKey);
kvvfsStorage(zClass).setItem(jKey, wasm.cstrToJs(zData));
return 0;
}catch(e){
console.error("kvstorageWrite()",e);
@ -716,7 +746,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
try {
const zXKey = kvvfsMakeKey(zClass,zKey);
if(!zXKey) return 1/*OOM*/;
kvvfsStorage(zClass).removeItem(wasm.cstringToJs(zXKey));
kvvfsStorage(zClass).removeItem(wasm.cstrToJs(zXKey));
return 0;
}catch(e){
console.error("kvstorageDelete()",e);

View File

@ -73,7 +73,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
if(capi.SQLITE_TRACE_STMT===t){
// x == SQL, p == sqlite3_stmt*
console.log("SQL TRACE #"+(++this.counter),
wasm.cstringToJs(x));
wasm.cstrToJs(x));
}
}.bind({counter: 0}));
@ -139,7 +139,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
console.error("Invalid DB ctor args",opt,arguments);
toss3("Invalid arguments for DB constructor.");
}
let fnJs = ('number'===typeof fn) ? wasm.cstringToJs(fn) : fn;
let fnJs = ('number'===typeof fn) ? wasm.cstrToJs(fn) : fn;
const vfsCheck = ctor._name2vfs[fnJs];
if(vfsCheck){
vfsName = vfsCheck.vfs;
@ -192,13 +192,13 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
/**
Sets SQL which should be exec()'d on a DB instance after it is
opened with the given VFS pointer. The SQL may be any type
supported by the "flexible-string" function argument
conversion. Alternately, the 2nd argument may be a function, in
which case it is called with (theOo1DbObject,sqlite3Namespace) at
the end of the DB() constructor. The function must throw on
error, in which case the db is closed and the exception is
propagated. This function is intended only for use by DB
subclasses or sqlite3_vfs implementations.
supported by the "string:flexible" function argument conversion.
Alternately, the 2nd argument may be a function, in which case it
is called with (theOo1DbObject,sqlite3Namespace) at the end of
the DB() constructor. The function must throw on error, in which
case the db is closed and the exception is propagated. This
function is intended only for use by DB subclasses or sqlite3_vfs
implementations.
*/
dbCtorHelper.setVfsPostOpenSql = function(pVfs, sql){
__vfsPostOpenSql[pVfs] = sql;
@ -473,6 +473,15 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
return rc;
};
/**
Internal impl of the DB.selectArrays() and
selectObjects() methods.
*/
const __selectAll =
(db, sql, bind, rowMode)=>db.exec({
sql, bind, rowMode, returnValue: 'resultRows'
});
/**
Expects to be given a DB instance or an `sqlite3*` pointer (may
be null) and an sqlite3 API result code. If the result code is
@ -511,10 +520,12 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
db is closed but before auxiliary state like this.filename is
cleared.
Both onclose handlers are passed this object. If this db is not
opened, neither of the handlers are called. Any exceptions the
handlers throw are ignored because "destructors must not
throw."
Both onclose handlers are passed this object, with the onclose
object as their "this," noting that the db will have been
closed when onclose.after is called. If this db is not opened
when close() is called, neither of the handlers are called. Any
exceptions the handlers throw are ignored because "destructors
must not throw."
Note that garbage collection of a db handle, if it happens at
all, will never trigger close(), so onclose handlers are not a
@ -591,7 +602,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
);
if(pVfs){
const v = new capi.sqlite3_vfs(pVfs);
try{ rc = wasm.cstringToJs(v.$zName) }
try{ rc = wasm.cstrToJs(v.$zName) }
finally { v.dispose() }
}
return rc;
@ -1098,6 +1109,26 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
return __selectFirstRow(this, sql, bind, {});
},
/**
Runs the given SQL and returns an array of all results, with
each row represented as an array, as per the 'array' `rowMode`
option to `exec()`. An empty result set resolves
to an empty array. The second argument, if any, is treated as
the 'bind' option to a call to exec().
*/
selectArrays: function(sql,bind){
return __selectAll(this, sql, bind, 'array');
},
/**
Works identically to selectArrays() except that each value
in the returned array is an object, as per the 'object' `rowMode`
option to `exec()`.
*/
selectObjects: function(sql,bind){
return __selectAll(this, sql, bind, 'object');
},
/**
Returns the number of currently-opened Stmt handles for this db
handle, or 0 if this DB instance is closed.

View File

@ -185,28 +185,49 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
/**
Constructs this object with a message depending on its arguments:
- If it's passed only a single integer argument, it is assumed
to be an sqlite3 C API result code. The message becomes the
result of sqlite3.capi.sqlite3_js_rc_str() or (if that returns
falsy) a synthesized string which contains that integer.
If its first argument is an integer, it is assumed to be
an SQLITE_... result code and it is passed to
sqlite3.capi.sqlite3_js_rc_str() to stringify it.
- If passed 2 arguments and the 2nd is a object, it behaves
like the Error(string,object) constructor except that the first
argument is subject to the is-integer semantics from the
previous point.
If called with exactly 2 arguments and the 2nd is an object,
that object is treated as the 2nd argument to the parent
constructor.
- Else all arguments are concatenated with a space between each
one, using args.join(' '), to create the error message.
The exception's message is created by concatenating its
arguments with a space between each, except for the
two-args-with-an-objec form and that the first argument will
get coerced to a string, as described above, if it's an
integer.
If passed an integer first argument, the error object's
`resultCode` member will be set to the given integer value,
else it will be set to capi.SQLITE_ERROR.
*/
constructor(...args){
if(1===args.length && __isInt(args[0])){
super(__rcStr(args[0]));
}else if(2===args.length && 'object'===typeof args[1]){
if(__isInt(args[0])) super(__rcStr(args[0]), args[1]);
else super(...args);
}else{
super(args.join(' '));
let rc;
if(args.length){
if(__isInt(args[0])){
rc = args[0];
if(1===args.length){
super(__rcStr(args[0]));
}else{
const rcStr = __rcStr(rc);
if('object'===typeof args[1]){
super(rcStr,args[1]);
}else{
args[0] = rcStr+':';
super(args.join(' '));
}
}
}else{
if(2===args.length && 'object'===typeof args[1]){
super(...args);
}else{
super(args.join(' '));
}
}
}
this.resultCode = rc || capi.SQLITE_ERROR;
this.name = 'SQLite3Error';
}
};
@ -348,13 +369,13 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
/**
If v is-a Array, its join("") result is returned. If
isSQLableTypedArray(v) is true then typedArrayToString(v) is
returned. If it looks like a WASM pointer, wasm.cstringToJs(v) is
returned. If it looks like a WASM pointer, wasm.cstrToJs(v) is
returned. Else v is returned as-is.
*/
const flexibleString = function(v){
if(isSQLableTypedArray(v)) return typedArrayToString(v);
else if(Array.isArray(v)) return v.join("");
else if(wasm.isPtr(v)) v = wasm.cstringToJs(v);
else if(wasm.isPtr(v)) v = wasm.cstrToJs(v);
return v;
};
@ -602,7 +623,7 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
If the callback is not a JS function then this binding performs
no translation of the callback, but the sql argument is still
converted to a WASM string for the call using the
"flexible-string" argument converter.
"string:flexible" argument converter.
*/
sqlite3_exec: (pDb, sql, callback, pVoid, pErrMsg)=>{}/*installed later*/,
@ -884,16 +905,23 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
["sqlite3_bind_null",undefined, "sqlite3_stmt*", "int"],
["sqlite3_bind_parameter_count", "int", "sqlite3_stmt*"],
["sqlite3_bind_parameter_index","int", "sqlite3_stmt*", "string"],
["sqlite3_bind_pointer", "int",
"sqlite3_stmt*", "int", "*", "string:static", "*"],
["sqlite3_bind_text","int", "sqlite3_stmt*", "int", "string", "int", "int"
/* We should arguably create a hand-written binding of
bind_text() which does more flexible text conversion, along
the lines of sqlite3_prepare_v3(). The slightly problematic
part is the final argument (text destructor). */
],
//["sqlite3_busy_handler","int", "sqlite3*", "*", "*"],
// ^^^^ TODO: custom binding which auto-converts JS function arg
// to a WASM function, noting that calling it multiple times
// would introduce a leak.
["sqlite3_busy_timeout","int", "sqlite3*", "int"],
["sqlite3_close_v2", "int", "sqlite3*"],
["sqlite3_changes", "int", "sqlite3*"],
["sqlite3_clear_bindings","int", "sqlite3_stmt*"],
["sqlite3_collation_needed", "int", "sqlite3*", "*", "*"/*=>v(ppis)*/],
["sqlite3_column_blob","*", "sqlite3_stmt*", "int"],
["sqlite3_column_bytes","int", "sqlite3_stmt*", "int"],
["sqlite3_column_count", "int", "sqlite3_stmt*"],
@ -904,9 +932,16 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
["sqlite3_column_type","int", "sqlite3_stmt*", "int"],
["sqlite3_compileoption_get", "string", "int"],
["sqlite3_compileoption_used", "int", "string"],
["sqlite3_complete", "int", "string:flexible"],
/* sqlite3_create_function(), sqlite3_create_function_v2(), and
sqlite3_create_window_function() use hand-written bindings to
simplify handling of their function-type arguments. */
["sqlite3_create_collation", "int",
"sqlite3*", "string", "int"/*SQLITE_UTF8 is the only legal value*/,
"*", "*"],
["sqlite3_create_collation_v2", "int",
"sqlite3*", "string", "int"/*SQLITE_UTF8 is the only legal value*/,
"*", "*", "*"],
["sqlite3_data_count", "int", "sqlite3_stmt*"],
["sqlite3_db_filename", "string", "sqlite3*", "string"],
["sqlite3_db_handle", "sqlite3*", "sqlite3_stmt*"],
@ -928,6 +963,7 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
["sqlite3_file_control", "int", "sqlite3*", "string", "int", "*"],
["sqlite3_finalize", "int", "sqlite3_stmt*"],
["sqlite3_free", undefined,"*"],
["sqlite3_get_auxdata", "*", "sqlite3_context*", "int"],
["sqlite3_initialize", undefined],
/*["sqlite3_interrupt", undefined, "sqlite3*"
^^^ we cannot actually currently support this because JS is
@ -945,23 +981,28 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
the range of supported argument types. */
["sqlite3_realloc", "*","*","int"],
["sqlite3_reset", "int", "sqlite3_stmt*"],
["sqlite3_result_blob",undefined, "sqlite3_context*", "*", "int", "*"],
["sqlite3_result_double",undefined, "sqlite3_context*", "f64"],
["sqlite3_result_error",undefined, "sqlite3_context*", "string", "int"],
["sqlite3_result_blob", undefined, "sqlite3_context*", "*", "int", "*"],
["sqlite3_result_double", undefined, "sqlite3_context*", "f64"],
["sqlite3_result_error", undefined, "sqlite3_context*", "string", "int"],
["sqlite3_result_error_code", undefined, "sqlite3_context*", "int"],
["sqlite3_result_error_nomem", undefined, "sqlite3_context*"],
["sqlite3_result_error_toobig", undefined, "sqlite3_context*"],
["sqlite3_result_int",undefined, "sqlite3_context*", "int"],
["sqlite3_result_null",undefined, "sqlite3_context*"],
["sqlite3_result_text",undefined, "sqlite3_context*", "string", "int", "*"],
["sqlite3_result_int", undefined, "sqlite3_context*", "int"],
["sqlite3_result_null", undefined, "sqlite3_context*"],
["sqlite3_result_pointer", undefined,
"sqlite3_context*", "*", "string:static", "*"],
["sqlite3_result_text", undefined, "sqlite3_context*", "string", "int", "*"],
["sqlite3_result_zeroblob", undefined, "sqlite3_context*", "int"],
["sqlite3_serialize","*", "sqlite3*", "string", "*", "int"],
["sqlite3_set_auxdata", undefined, "sqlite3_context*", "int", "*", "*"/* => v(*) */],
["sqlite3_shutdown", undefined],
["sqlite3_sourceid", "string"],
["sqlite3_sql", "string", "sqlite3_stmt*"],
["sqlite3_step", "int", "sqlite3_stmt*"],
["sqlite3_strglob", "int", "string","string"],
["sqlite3_strlike", "int", "string","string","int"],
["sqlite3_stricmp", "int", "string", "string"],
["sqlite3_strlike", "int", "string", "string","int"],
["sqlite3_strnicmp", "int", "string", "string", "int"],
["sqlite3_trace_v2", "int", "sqlite3*", "int", "*", "*"],
["sqlite3_total_changes", "int", "sqlite3*"],
/* Note sqlite3_uri_...() has very specific requirements
@ -975,6 +1016,7 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
["sqlite3_value_bytes","int", "sqlite3_value*"],
["sqlite3_value_double","f64", "sqlite3_value*"],
["sqlite3_value_int","int", "sqlite3_value*"],
["sqlite3_value_pointer", "*", "sqlite3_value*", "string:static"],
["sqlite3_value_text", "string", "sqlite3_value*"],
["sqlite3_value_type", "int", "sqlite3_value*"],
["sqlite3_vfs_find", "*", "string"],
@ -993,19 +1035,40 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
Functions which require BigInt (int64) support are separated from
the others because we need to conditionally bind them or apply
dummy impls, depending on the capabilities of the environment.
Note that not all of these functions directly require int64
but are only for use with APIs which require int64. For example,
the vtab-related functions.
*/
wasm.bindingSignatures.int64 = [
["sqlite3_bind_int64","int", ["sqlite3_stmt*", "int", "i64"]],
["sqlite3_changes64","i64", ["sqlite3*"]],
["sqlite3_column_int64","i64", ["sqlite3_stmt*", "int"]],
["sqlite3_create_module", "int",
["sqlite3*","string","sqlite3_module*","*"]],
["sqlite3_create_module_v2", "int",
["sqlite3*","string","sqlite3_module*","*","*"]],
["sqlite3_declare_vtab", "int", ["sqlite3*", "string:flexible"]],
["sqlite3_drop_modules", "int", ["sqlite3*", "**"]],
["sqlite3_malloc64", "*","i64"],
["sqlite3_msize", "i64", "*"],
["sqlite3_overload_function", "int", ["sqlite3*","string","int"]],
["sqlite3_realloc64", "*","*", "i64"],
["sqlite3_result_int64",undefined, "*", "i64"],
["sqlite3_result_int64", undefined, "*", "i64"],
["sqlite3_result_zeroblob64", "int", "*", "i64"],
["sqlite3_total_changes64", "i64", ["sqlite3*"]],
["sqlite3_uri_int64", "i64", ["sqlite3_filename", "string", "i64"]],
["sqlite3_value_int64","i64", "sqlite3_value*"],
["sqlite3_vtab_collation","string","sqlite3_index_info*","int"],
["sqlite3_vtab_distinct","int", "sqlite3_index_info*"],
["sqlite3_vtab_in","int", "sqlite3_index_info*", "int", "int"],
["sqlite3_vtab_in_first", "int", "sqlite3_value*", "**"],
["sqlite3_vtab_in_next", "int", "sqlite3_value*", "**"],
/*["sqlite3_vtab_config" is variadic and requires a hand-written
proxy.] */
["sqlite3_vtab_nochange","int", "sqlite3_context*"],
["sqlite3_vtab_on_conflict","int", "sqlite3*"],
["sqlite3_vtab_rhs_value","int", "sqlite3_index_info*", "int", "**"]
];
/**
@ -1297,7 +1360,7 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
let pVfs = capi.sqlite3_vfs_find(0);
while(pVfs){
const oVfs = new capi.sqlite3_vfs(pVfs);
rc.push(wasm.cstringToJs(oVfs.$zName));
rc.push(wasm.cstrToJs(oVfs.$zName));
pVfs = oVfs.$pNext;
oVfs.dispose();
}
@ -1600,7 +1663,6 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
some initializers. Retain them when running in test mode
so that we can add tests for them. */
delete sqlite3.util;
delete sqlite3.VfsHelper;
delete sqlite3.StructBinder;
}
return sqlite3;

View File

@ -13,7 +13,7 @@
A Worker which manages asynchronous OPFS handles on behalf of a
synchronous API which controls it via a combination of Worker
messages, SharedArrayBuffer, and Atomics. It is the asynchronous
counterpart of the API defined in sqlite3-api-opfs.js.
counterpart of the API defined in sqlite3-vfs-opfs.js.
Highly indebted to:
@ -343,16 +343,6 @@ const installAsyncProxy = function(self){
const affirmNotRO = function(opName,fh){
if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs);
};
const affirmLocked = function(opName,fh){
//if(!fh.syncHandle) toss(opName+"(): File does not have a lock: "+fh.filenameAbs);
/**
Currently a no-op, as speedtest1 triggers xRead() without a
lock (that seems like a bug but it's currently uninvestigated).
This means, however, that some OPFS VFS routines may trigger
acquisition of a lock but never let it go until xUnlock() is
called (which it likely won't be if xLock() was not called).
*/
};
/**
We track 2 different timers: the "metrics" timer records how much
@ -393,7 +383,6 @@ const installAsyncProxy = function(self){
*/
let flagAsyncShutdown = false;
/**
Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods
methods, as well as helpers like mkdir(). Maintenance reminder:
@ -427,11 +416,11 @@ const installAsyncProxy = function(self){
},
xAccess: async (filename)=>{
mTimeStart('xAccess');
/* OPFS cannot support the full range of xAccess() queries sqlite3
calls for. We can essentially just tell if the file is
accessible, but if it is it's automatically writable (unless
it's locked, which we cannot(?) know without trying to open
it). OPFS does not have the notion of read-only.
/* OPFS cannot support the full range of xAccess() queries
sqlite3 calls for. We can essentially just tell if the file
is accessible, but if it is then it's automatically writable
(unless it's locked, which we cannot(?) know without trying
to open it). OPFS does not have the notion of read-only.
The return semantics of this function differ from sqlite3's
xAccess semantics because we are limited in what we can
@ -519,7 +508,6 @@ const installAsyncProxy = function(self){
let rc = 0;
wTimeStart('xFileSize');
try{
affirmLocked('xFileSize',fh);
const sz = await (await getSyncHandle(fh,'xFileSize')).getSize();
state.s11n.serialize(Number(sz));
}catch(e){
@ -615,7 +603,6 @@ const installAsyncProxy = function(self){
let rc = 0, nRead;
const fh = __openFiles[fid];
try{
affirmLocked('xRead',fh);
wTimeStart('xRead');
nRead = (await getSyncHandle(fh,'xRead')).read(
fh.sabView.subarray(0, n),
@ -659,7 +646,6 @@ const installAsyncProxy = function(self){
const fh = __openFiles[fid];
wTimeStart('xTruncate');
try{
affirmLocked('xTruncate',fh);
affirmNotRO('xTruncate', fh);
await (await getSyncHandle(fh,'xTruncate')).truncate(size);
}catch(e){
@ -696,7 +682,6 @@ const installAsyncProxy = function(self){
const fh = __openFiles[fid];
wTimeStart('xWrite');
try{
affirmLocked('xWrite',fh);
affirmNotRO('xWrite', fh);
rc = (
n === (await getSyncHandle(fh,'xWrite'))

View File

@ -0,0 +1,599 @@
/*
** 2022-11-30
**
** The author disclaims copyright to this source code. In place of a
** legal notice, here is a blessing:
**
** * May you do good and not evil.
** * May you find forgiveness for yourself and forgive others.
** * May you share freely, never taking more than you give.
*/
/**
This file installs sqlite3.VfsHelper, and object which exists to
assist in the creation of JavaScript implementations of sqlite3_vfs,
along with its virtual table counterpart, sqlite3.VtabHelper.
*/
'use strict';
self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
const wasm = sqlite3.wasm, capi = sqlite3.capi, toss = sqlite3.util.toss3;
const vh = Object.create(null), vt = Object.create(null);
sqlite3.VfsHelper = vh;
sqlite3.VtabHelper = vt;
const sii = capi.sqlite3_index_info;
/**
If n is >=0 and less than this.$nConstraint, this function
returns either a WASM pointer to the 0-based nth entry of
this.$aConstraint (if passed a truthy 2nd argument) or an
sqlite3_index_info.sqlite3_index_constraint object wrapping that
address (if passed a falsy value or no 2nd argument). Returns a
falsy value if n is out of range.
*/
sii.prototype.nthConstraint = function(n, asPtr=false){
if(n<0 || n>=this.$nConstraint) return false;
const ptr = this.$aConstraint + (
sii.sqlite3_index_constraint.structInfo.sizeof * n
);
return asPtr ? ptr : new sii.sqlite3_index_constraint(ptr);
};
/**
Works identically to nthConstraint() but returns state from
this.$aConstraintUsage, so returns an
sqlite3_index_info.sqlite3_index_constraint_usage instance
if passed no 2nd argument or a falsy 2nd argument.
*/
sii.prototype.nthConstraintUsage = function(n, asPtr=false){
if(n<0 || n>=this.$nConstraint) return false;
const ptr = this.$aConstraintUsage + (
sii.sqlite3_index_constraint_usage.structInfo.sizeof * n
);
return asPtr ? ptr : new sii.sqlite3_index_constraint_usage(ptr);
};
/**
If n is >=0 and less than this.$nOrderBy, this function
returns either a WASM pointer to the 0-based nth entry of
this.$aOrderBy (if passed a truthy 2nd argument) or an
sqlite3_index_info.sqlite3_index_orderby object wrapping that
address (if passed a falsy value or no 2nd argument). Returns a
falsy value if n is out of range.
*/
sii.prototype.nthOrderBy = function(n, asPtr=false){
if(n<0 || n>=this.$nOrderBy) return false;
const ptr = this.$aOrderBy + (
sii.sqlite3_index_orderby.structInfo.sizeof * n
);
return asPtr ? ptr : new sii.sqlite3_index_orderby(ptr);
};
/**
Installs a StructBinder-bound function pointer member of the
given name and function in the given StructType target object.
It creates a WASM proxy for the given function and arranges for
that proxy to be cleaned up when tgt.dispose() is called. Throws
on the slightest hint of error, e.g. tgt is-not-a StructType,
name does not map to a struct-bound member, etc.
As a special case, if the given function is a pointer, it is
assumed to be an existing WASM-bound function pointer and is used
as-is with no extra level of proxying or cleanup. Results are
undefined if it's a pointer and it's _not_ a function pointer.
It is legal to pass a value of 0, indicating a NULL pointer, with
the caveat that 0 _is_ a legal function pointer in WASM but it
will not be accepted as such _here_. (Justification: the function
at address zero must be one which initially came from the WASM
module, not a method we want to bind to a virtual table or VFS.)
This function returns a proxy for itself which is bound to tgt
and takes 2 args (name,func). That function returns the same
thing as this one, permitting calls to be chained.
If called with only 1 arg, it has no side effects but returns a
func with the same signature as described above.
ACHTUNG: because we cannot generically know how to transform JS
exceptions into result codes, the installed functions do no
automatic catching of exceptions. It is critical, to avoid
undefined behavior in the C layer, that methods mapped via
this function do not throw. The exception, as it were, to that
rule is...
If applyArgcCheck is true then each JS function (as opposed to
function pointers) gets wrapped in a proxy which asserts that it
is passed the expected number of arguments, throwing if the
argument count does not match expectations. That is only intended
for dev-time usage for sanity checking, and will leave the C
environment in an undefined state.
*/
vh.installMethod = vt.installMethod = function callee(
tgt, name, func, applyArgcCheck = callee.installMethodArgcCheck
){
if(!(tgt instanceof sqlite3.StructBinder.StructType)){
toss("Usage error: target object is-not-a StructType.");
}else if(!(func instanceof Function) && !wasm.isPtr(func)){
toss("Usage errror: expecting a Function or WASM pointer to one.");
}
if(1===arguments.length){
return (n,f)=>callee(tgt, n, f, applyArgcCheck);
}
if(!callee.argcProxy){
callee.argcProxy = function(tgt, funcName, func,sig){
return function(...args){
if(func.length!==arguments.length){
toss("Argument mismatch for",
tgt.structInfo.name+"::"+funcName
+": Native signature is:",sig);
}
return func.apply(this, args);
}
};
/* An ondispose() callback for use with
sqlite3.StructBinder-created types. */
callee.removeFuncList = function(){
if(this.ondispose.__removeFuncList){
this.ondispose.__removeFuncList.forEach(
(v,ndx)=>{
if('number'===typeof v){
try{wasm.uninstallFunction(v)}
catch(e){/*ignore*/}
}
/* else it's a descriptive label for the next number in
the list. */
}
);
delete this.ondispose.__removeFuncList;
}
};
}/*static init*/
const sigN = tgt.memberSignature(name);
if(sigN.length<2){
toss("Member",name,"does not have a function pointer signature:",sigN);
}
const memKey = tgt.memberKey(name);
const fProxy = (applyArgcCheck && !wasm.isPtr(func))
/** This middle-man proxy is only for use during development, to
confirm that we always pass the proper number of
arguments. We know that the C-level code will always use the
correct argument count. */
? callee.argcProxy(tgt, memKey, func, sigN)
: func;
if(wasm.isPtr(fProxy)){
if(fProxy && !wasm.functionEntry(fProxy)){
toss("Pointer",fProxy,"is not a WASM function table entry.");
}
tgt[memKey] = fProxy;
}else{
const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true));
tgt[memKey] = pFunc;
if(!tgt.ondispose) tgt.ondispose = [];
else if(!Array.isArray(tgt.ondispose)) tgt.ondispose = [tgt.ondispose];
if(!tgt.ondispose || !tgt.ondispose.__removeFuncList){
tgt.addOnDispose('ondispose.__removeFuncList handler',
callee.removeFuncList);
tgt.ondispose.__removeFuncList = [];
}
tgt.ondispose.__removeFuncList.push(memKey, pFunc);
}
return (n,f)=>callee(tgt, n, f, applyArgcCheck);
}/*installMethod*/;
vh.installMethod.installMethodArgcCheck = false;
/**
Installs methods into the given StructType-type instance. Each
entry in the given methods object must map to a known member of
the given StructType, else an exception will be triggered. See
installMethod() for more details, including the semantics of the
3rd argument.
As an exception to the above, if any two or more methods in the
2nd argument are the exact same function, installMethod() is
_not_ called for the 2nd and subsequent instances, and instead
those instances get assigned the same method pointer which is
created for the first instance. This optimization is primarily to
accommodate special handling of sqlite3_module::xConnect and
xCreate methods.
On success, returns this object. Throws on error.
*/
vh.installMethods = vt.installMethods = function(
structType, methods, applyArgcCheck = vh.installMethod.installMethodArgcCheck
){
const seen = new Map /* map of <Function, memberName> */;
for(const k of Object.keys(methods)){
const m = methods[k];
const prior = seen.get(m);
if(prior){
const mkey = structType.memberKey(k);
structType[mkey] = structType[structType.memberKey(prior)];
}else{
vh.installMethod(structType, k, m, applyArgcCheck);
seen.set(m, k);
}
}
return this;
};
/**
Uses sqlite3_vfs_register() to register the
sqlite3.capi.sqlite3_vfs-type vfs, which must have already been
filled out properly. If the 2nd argument is truthy, the VFS is
registered as the default VFS, else it is not.
On success, returns this object. Throws on error.
*/
vh.registerVfs = function(vfs, asDefault=false){
if(!(vfs instanceof sqlite3.capi.sqlite3_vfs)){
toss("Expecting a sqlite3_vfs-type argument.");
}
const rc = capi.sqlite3_vfs_register(vfs.pointer, asDefault ? 1 : 0);
if(rc){
toss("sqlite3_vfs_register(",vfs,") failed with rc",rc);
}
if(vfs.pointer !== capi.sqlite3_vfs_find(vfs.$zName)){
toss("BUG: sqlite3_vfs_find(vfs.$zName) failed for just-installed VFS",
vfs);
}
return this;
};
/**
A wrapper for installMethods() or registerVfs() to reduce
installation of a VFS and/or its I/O methods to a single
call.
Accepts an object which contains the properties "io" and/or
"vfs", each of which is itself an object with following properties:
- `struct`: an sqlite3.StructType-type struct. This must be a
populated (except for the methods) object of type
sqlite3_io_methods (for the "io" entry) or sqlite3_vfs (for the
"vfs" entry).
- `methods`: an object mapping sqlite3_io_methods method names
(e.g. 'xClose') to JS implementations of those methods. The JS
implementations must be call-compatible with their native
counterparts.
For each of those object, this function passes its (`struct`,
`methods`, (optional) `applyArgcCheck`) properties to
this.installMethods().
If the `vfs` entry is set then:
- Its `struct` property is passed to this.registerVfs(). The
`vfs` entry may optionally have an `asDefault` property, which
gets passed as the 2nd argument to registerVfs().
- If `struct.$zName` is falsy and the entry has a string-type
`name` property, `struct.$zName` is set to the C-string form of
that `name` value before registerVfs() is called.
On success returns this object. Throws on error.
*/
vh.installVfs = function(opt){
let count = 0;
const propList = ['io','vfs'];
for(const key of propList){
const o = opt[key];
if(o){
++count;
this.installMethods(o.struct, o.methods, !!o.applyArgcCheck);
if('vfs'===key){
if(!o.struct.$zName && 'string'===typeof o.name){
o.struct.addOnDispose(
o.struct.$zName = wasm.allocCString(o.name)
);
}
this.registerVfs(o.struct, !!o.asDefault);
}
}
}
if(!count) toss("Misuse: installVfs() options object requires at least",
"one of:", propList);
return this;
};
/**
Expects to be passed the (argc,argv) arguments of
sqlite3_module::xFilter(), or an equivalent API. This function
transforms the arguments (an array of (sqlite3_value*)) into a JS
array of equivalent JS values. It uses the same type conversions
as sqlite3_create_function_v2() and friends. Throws on error,
e.g. if it cannot figure out a sensible data conversion.
*/
vt.sqlite3ValuesToJs = capi.sqlite3_create_function_v2.udfConvertArgs;
/**
Factory function for xAbc() impls.
*/
const __xWrapFactory = function(methodName,structType){
return function(ptr,removeMapping=false){
if(0===arguments.length) ptr = new structType;
if(ptr instanceof structType){
//T.assert(!this.has(ptr.pointer));
this.set(ptr.pointer, ptr);
return ptr;
}else if(!wasm.isPtr(ptr)){
sqlite3.SQLite3Error.toss("Invalid argument to",methodName+"()");
}
let rc = this.get(ptr);
if(removeMapping) this.delete(ptr);
return rc;
}.bind(new Map);
};
/**
EXPERIMENTAL. DO NOT USE IN CLIENT CODE.
Has 3 distinct uses:
- wrapVtab() instantiates a new capi.sqlite3_vtab instance, maps
its pointer for later by-pointer lookup, and returns that
object. This is intended to be called from
sqlite3_module::xConnect() or xCreate() implementations.
- wrapVtab(pVtab) accepts a WASM pointer to a C-level
(sqlite3_vtab*) instance and returns the capi.sqlite3_vtab
object created by the first form of this function, or undefined
if that form has not been used. This is intended to be called
from sqlite3_module methods which take a (sqlite3_vtab*)
pointer _except_ for xDestroy() (if there is a distinct
xCreate()) or xDisconnect() (if xCreate() is 0 or is the same
as xConnect()), in which case use...
- wrapVtab(pVtab,true) as for the previous form, but removes the
pointer-to-object mapping before returning. The caller must
call dispose() on the returned object. This is intended to be
called from sqlite3_module::xDisconnect() implementations or
in error handling of a failed xCreate() or xConnect().
*/
vt.xVtab = __xWrapFactory('xVtab',capi.sqlite3_vtab);
/**
EXPERIMENTAL. DO NOT USE IN CLIENT CODE.
Works identically to wrapVtab() except that it deals with
sqlite3_cursor objects and pointers instead of sqlite3_vtab.
- wrapCursor() is intended to be called from sqlite3_module::xOpen()
- wrapCursor(pCursor) is intended to be called from all sqlite3_module
methods which take a (sqlite3_vtab_cursor*) _except_ for
xClose(), in which case use...
- wrapCursor(pCursor, true) will remove the mapping of pCursor to a
capi.sqlite3_vtab_cursor object and return that object. The
caller must call dispose() on the returned object. This is
intended to be called from xClose() or in error handling of a
failed xOpen().
*/
vt.xCursor = __xWrapFactory('xCursor',capi.sqlite3_vtab_cursor);
/**
Convenience form of creating an sqlite3_index_info wrapper,
intended for use in xBestIndex implementations. Note that the
caller is expected to call dispose() on the returned object
before returning. Though not _strictly_ required, as that object
does not own the pIdxInfo memory, it is nonetheless good form.
*/
vt.xIndexInfo = (pIdxInfo)=>new capi.sqlite3_index_info(pIdxInfo);
/**
Given an error object, this function returns
sqlite3.capi.SQLITE_NOMEM if (e instanceof
sqlite3.WasmAllocError), else it returns its
second argument. Its intended usage is in the methods
of a sqlite3_vfs or sqlite3_module:
```
try{
let rc = ...
return rc;
}catch(e){
return sqlite3.VtabHelper.exceptionToRc(e, sqlite3.capi.SQLITE_XYZ);
// where SQLITE_XYZ is some call-appropriate result code.
}
```
*/
/**vh.exceptionToRc = vt.exceptionToRc =
(e, defaultRc=capi.SQLITE_ERROR)=>(
(e instanceof sqlite3.WasmAllocError)
? capi.SQLITE_NOMEM
: defaultRc
);*/
/**
Given an sqlite3_module method name and error object, this
function returns sqlite3.capi.SQLITE_NOMEM if (e instanceof
sqlite3.WasmAllocError), else it returns its second argument. Its
intended usage is in the methods of a sqlite3_vfs or
sqlite3_module:
```
try{
let rc = ...
return rc;
}catch(e){
return sqlite3.VtabHelper.xError(
'xColumn', e, sqlite3.capi.SQLITE_XYZ);
// where SQLITE_XYZ is some call-appropriate result code.
}
```
If no 3rd argument is provided, its default depends on
the error type:
- An sqlite3.WasmAllocError always resolves to capi.SQLITE_NOMEM.
- If err is an SQLite3Error then its `resultCode` property
is used.
- If all else fails, capi.SQLITE_ERROR is used.
If xError.errorReporter is a function, it is called in
order to report the error, else the error is not reported.
If that function throws, that exception is ignored.
*/
vt.xError = function f(methodName, err, defaultRc){
if(f.errorReporter instanceof Function){
try{f.errorReporter("sqlite3_module::"+methodName+"(): "+err.message);}
catch(e){/*ignored*/}
}
let rc;
if(err instanceof sqlite3.WasmAllocError) rc = capi.SQLITE_NOMEM;
else if(arguments.length>2) rc = defaultRc;
else if(err instanceof sqlite3.SQLite3Error) rc = err.resultCode;
return rc || capi.SQLITE_ERROR;
};
vt.xError.errorReporter = 1 ? console.error.bind(console) : false;
/**
"The problem" with this is that it introduces an outer function with
a different arity than the passed-in method callback. That means we
cannot do argc validation on these. Additionally, some methods (namely
xConnect) may have call-specific error handling. It would be a shame to
hard-coded that per-method support in this function.
*/
/** vt.methodCatcher = function(methodName, method, defaultErrRc=capi.SQLITE_ERROR){
return function(...args){
try { method(...args); }
}catch(e){ return vt.xError(methodName, e, defaultRc) }
};
*/
/**
A helper for sqlite3_vtab::xRowid() implementations. It must be
passed that function's 2nd argument and the value for that
pointer. Returns the same as wasm.setMemValue() and will throw
if the 1st or 2nd arguments are invalid for that function.
*/
vt.xRowid = (ppRowid64, value)=>wasm.setMemValue(ppRowid64, value, 'i64');
/**
Sets up an sqlite3_module() object for later installation into
individual databases using sqlite3_create_module(). Requires an
object with the following properties:
- `methods`: an object containing a mapping of properties with
the C-side names of the sqlite3_module methods, e.g. xCreate,
xBestIndex, etc., to JS implementations for those functions.
Certain special-case handling is performed, as described below.
- `catchExceptions` (default=false): if truthy, the given methods
are not mapped as-is, but are instead wrapped inside wrappers
which translate exceptions into result codes of SQLITE_ERROR or
SQLITE_NOMEM, depending on whether the exception is an
sqlite3.WasmAllocError. In the case of the xConnect and xCreate
methods, the exception handler also sets the output error
string to the exception's error string.
- OPTIONAL `struct`: a sqlite3.capi.sqlite3_module() instance. If
not set, one will be created automatically and (on success)
added to the object.
- OPTIONAL `iVersion`: if set, it must be an integer value and it
gets assigned to the `$iVersion` member of the struct object.
If it's _not_ set, and the passed-in `struct` object's `$iVersion`
is 0 (the default) then this function attempts to define a value
for that property based on the list of methods it has.
If `catchExceptions` is false, it is up to the client to ensure
that no exceptions escape the methods, as doing so would move
them through the C API, leading to undefined
behavior. (VtabHelper.xError() is intended to assist in reporting
such exceptions.)
If `methods.xConnect` is `true` then the value of
`methods.xCreate` is used in its place, and vice versa. This is
to facilitate creation of those methods inline in the passed-in
object without requiring the client to explicitly get a reference
to one of them in order to assign it to the other one. Note that
sqlite treats those two functions specially if they are exactly
the same function (same pointer value). The
`catchExceptions`-installed handlers will account for identical
references to those two functions and will install the same
wrapper function for both.
The given methods are expected to return integer values, as
expected by the C API. If `catchExceptions` is truthy, the return
value of the wrapped function will be used as-is and will be
translated to 0 if the function returns a falsy value (e.g. if it
does not have an explicit return). If `catchExceptions` is _not_
active, the method implementations must explicitly return integer
values.
Throws on error. Returns the sqlite3_module object on success.
*/
vt.setupModule = function(opt){
const mod = opt.struct || new capi.sqlite3_module();
try{
const methods = opt.methods || toss("Missing 'methods' object.");
if(true===methods.xConnect) methods.xConnect = methods.xCreate;
else if(true===methods.xCreate) methods.xCreate = methods.xConnect;
if(opt.catchExceptions){
const fwrap = function(methodName, func){
if(['xConnect','xCreate'].indexOf(methodName) >= 0){
return function(pDb, pAux, argc, argv, ppVtab, pzErr){
try{return func(...arguments) || 0;}
catch(e){
if(!(e instanceof sqlite3.WasmAllocError)){
wasm.dealloc(wasm.getPtrValue(pzErr));
wasm.setPtrValue(pzErr, wasm.allocCString(e.message));
}
return vt.xError(methodName, e);
}
};
}else{
return function(...args){
try{return func(...args) || 0;}
catch(e){
return vt.xError(methodName, e);
}
};
}
};
const mnames = [
'xCreate', 'xConnect', 'xBestIndex', 'xDisconnect',
'xDestroy', 'xOpen', 'xClose', 'xFilter', 'xNext',
'xEof', 'xColumn', 'xRowid', 'xUpdate',
'xBegin', 'xSync', 'xCommit', 'xRollback',
'xFindFunction', 'xRename', 'xSavepoint', 'xRelease',
'xRollbackTo', 'xShadowName'
];
const remethods = Object.create(null);
for(const k of mnames){
const m = methods[k];
if(!(m instanceof Function)) continue;
else if('xConnect'===k && methods.xCreate===m){
remethods[k] = methods.xCreate;
}else if('xCreate'===k && methods.xConnect===m){
remethods[k] = methods.xConnect;
}else{
remethods[k] = fwrap(k, m);
}
}
this.installMethods(mod, remethods, false);
}else{
this.installMethods(
mod, methods, !!opt.applyArgcCheck/*undocumented option*/
);
}
if(0===mod.$iVersion){
let v;
if('number'===typeof opt.iVersion) v = opt.iVersion;
else if(mod.$xShadowName) v = 3;
else if(mod.$xSavePoint || mod.$xRelease || mod.$xRollbackTo) v = 2;
else v = 1;
mod.$iVersion = v;
}
}catch(e){
if(!opt.struct) mod.dispose();
throw e;
}
if(!opt.struct) opt.struct = mod;
return mod;
}/*setupModule()*/;
}/*sqlite3ApiBootstrap.initializers.push()*/);

View File

@ -1,221 +0,0 @@
/*
** 2022-11-30
**
** The author disclaims copyright to this source code. In place of a
** legal notice, here is a blessing:
**
** * May you do good and not evil.
** * May you find forgiveness for yourself and forgive others.
** * May you share freely, never taking more than you give.
*/
/**
This file installs sqlite.VfsHelper, an object which exists
to assist in the creation of JavaScript implementations of
sqlite3_vfs. It is NOT part of the public API, and is an
internal implemenation detail for use in this project's
own development of VFSes. It may be exposed to clients
at some point, provided there is value in doing so.
*/
'use strict';
self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
const wasm = sqlite3.wasm, capi = sqlite3.capi, toss = sqlite3.util.toss;
const vh = Object.create(null);
/**
Does nothing more than holds a permanent reference to each
argument. This is useful in some cases to ensure that, e.g., a
custom sqlite3_io_methods instance does not get
garbage-collected.
Returns this object.
*/
vh.holdReference = function(...args){
for(const v of args) this.refs.add(v);
return vh;
}.bind({refs: new Set});
/**
Installs a StructBinder-bound function pointer member of the
given name and function in the given StructType target object.
It creates a WASM proxy for the given function and arranges for
that proxy to be cleaned up when tgt.dispose() is called. Throws
on the slightest hint of error, e.g. tgt is-not-a StructType,
name does not map to a struct-bound member, etc.
If applyArgcCheck is true then each method gets wrapped in a
proxy which asserts that it is passed the expected number of
arguments, throwing if the argument count does not match
expectations. That is only recommended for dev-time usage for
sanity checking. Once a VFS implementation is known to be
working, it is a given that the C API will never call it with the
wrong argument count.
Returns a proxy for this function which is bound to tgt and takes
2 args (name,func). That function returns the same thing,
permitting calls to be chained.
If called with only 1 arg, it has no side effects but returns a
func with the same signature as described above.
If tgt.ondispose is set before this is called then it _must_
be an array, to which this function will append entries.
*/
vh.installMethod = function callee(tgt, name, func,
applyArgcCheck=callee.installMethodArgcCheck){
if(!(tgt instanceof sqlite3.StructBinder.StructType)){
toss("Usage error: target object is-not-a StructType.");
}
if(1===arguments.length){
return (n,f)=>callee(tgt, n, f, applyArgcCheck);
}
if(!callee.argcProxy){
callee.argcProxy = function(func,sig){
return function(...args){
if(func.length!==arguments.length){
toss("Argument mismatch. Native signature is:",sig);
}
return func.apply(this, args);
}
};
/* An ondispose() callback for use with
sqlite3.StructBinder-created types. */
callee.removeFuncList = function(){
if(this.ondispose.__removeFuncList){
this.ondispose.__removeFuncList.forEach(
(v,ndx)=>{
if('number'===typeof v){
try{wasm.uninstallFunction(v)}
catch(e){/*ignore*/}
}
/* else it's a descriptive label for the next number in
the list. */
}
);
delete this.ondispose.__removeFuncList;
}
};
}/*static init*/
const sigN = tgt.memberSignature(name);
if(sigN.length<2){
toss("Member",name," is not a function pointer. Signature =",sigN);
}
const memKey = tgt.memberKey(name);
const fProxy = applyArgcCheck
/** This middle-man proxy is only for use during development, to
confirm that we always pass the proper number of
arguments. We know that the C-level code will always use the
correct argument count. */
? callee.argcProxy(func, sigN)
: func;
const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true));
tgt[memKey] = pFunc;
if(!tgt.ondispose) tgt.ondispose = [];
if(!tgt.ondispose.__removeFuncList){
tgt.ondispose.push('ondispose.__removeFuncList handler',
callee.removeFuncList);
tgt.ondispose.__removeFuncList = [];
}
tgt.ondispose.__removeFuncList.push(memKey, pFunc);
return (n,f)=>callee(tgt, n, f, applyArgcCheck);
}/*installMethod*/;
vh.installMethod.installMethodArgcCheck = false;
/**
Installs methods into the given StructType-type object. Each
entry in the given methods object must map to a known member of
the given StructType, else an exception will be triggered.
See installMethod() for more details, including the semantics
of the 3rd argument.
On success, passes its first argument to holdRefence() and
returns this object. Throws on error.
*/
vh.installMethods = function(structType, methods,
applyArgcCheck=vh.installMethod.installMethodArgcCheck){
for(const k of Object.keys(methods)){
vh.installMethod(structType, k, methods[k], applyArgcCheck);
}
return vh.holdReference(structType);
};
/**
Uses sqlite3_vfs_register() to register the
sqlite3.capi.sqlite3_vfs-type vfs, which must have already been
filled out properly. If the 2nd argument is truthy, the VFS is
registered as the default VFS, else it is not.
On success, passes its first argument to this.holdReference() and
returns this object. Throws on error.
*/
vh.registerVfs = function(vfs, asDefault=false){
if(!(vfs instanceof sqlite3.capi.sqlite3_vfs)){
toss("Expecting a sqlite3_vfs-type argument.");
}
const rc = capi.sqlite3_vfs_register(vfs.pointer, asDefault ? 1 : 0);
if(rc){
toss("sqlite3_vfs_register(",vfs,") failed with rc",rc);
}
if(vfs.pointer !== capi.sqlite3_vfs_find(vfs.$zName)){
toss("BUG: sqlite3_vfs_find(vfs.$zName) failed for just-installed VFS",
vfs);
}
return vh.holdReference(vfs);
};
/**
A wrapper for installMethods() or registerVfs() to reduce
installation of a VFS and/or its I/O methods to a single
call.
Accepts an object which contains the properties "io" and/or
"vfs", each of which is itself an object with following properties:
- `struct`: an sqlite3.StructType-type struct. This must be a
populated (except for the methods) object of type
sqlite3_io_methods (for the "io" entry) or sqlite3_vfs (for the
"vfs" entry).
- `methods`: an object mapping sqlite3_io_methods method names
(e.g. 'xClose') to JS implementations of those methods.
For each of those object, this function passes its (`struct`,
`methods`, (optional) `applyArgcCheck`) properties to
this.installMethods().
If the `vfs` entry is set then:
- Its `struct` property is passed to this.registerVfs(). The
`vfs` entry may optionally have an `asDefault` property, which
gets passed as the 2nd argument to registerVfs().
- If `struct.$zName` is falsy and the entry has a string-type
`name` property, `struct.$zName` is set to the C-string form of
that `name` value before registerVfs() is called.
On success returns this object. Throws on error.
*/
vh.installVfs = function(opt){
let count = 0;
const propList = ['io','vfs'];
for(const key of propList){
const o = opt[key];
if(o){
++count;
this.installMethods(o.struct, o.methods, !!o.applyArgcCheck);
if('vfs'===key){
if(!o.struct.$zName && 'string'===typeof o.name){
o.struct.$zName = wasm.allocCString(o.name);
/* Note that we leak that C-string. */
}
this.registerVfs(o.struct, !!o.asDefault);
}
}
}
if(!count) toss("Misuse: installVfs() options object requires at least",
"one of:", propList);
return this;
};
sqlite3.VfsHelper = vh;
}/*sqlite3ApiBootstrap.initializers.push()*/);

View File

@ -803,7 +803,7 @@ const installOpfsVfs = function callee(options){
const vfsSyncWrappers = {
xAccess: function(pVfs,zName,flags,pOut){
mTimeStart('xAccess');
const rc = opRun('xAccess', wasm.cstringToJs(zName));
const rc = opRun('xAccess', wasm.cstrToJs(zName));
wasm.setMemValue( pOut, (rc ? 0 : 1), 'i32' );
mTimeEnd();
return 0;
@ -823,7 +823,7 @@ const installOpfsVfs = function callee(options){
},
xDelete: function(pVfs, zName, doSyncDir){
mTimeStart('xDelete');
opRun('xDelete', wasm.cstringToJs(zName), doSyncDir, false);
opRun('xDelete', wasm.cstrToJs(zName), doSyncDir, false);
/* We're ignoring errors because we cannot yet differentiate
between harmless and non-harmless failures. */
mTimeEnd();
@ -855,7 +855,7 @@ const installOpfsVfs = function callee(options){
C-string here. */
opfsFlags |= state.opfsFlags.OPFS_UNLOCK_ASAP;
}
zName = wasm.cstringToJs(zName);
zName = wasm.cstrToJs(zName);
}
const fh = Object.create(null);
fh.fid = pFile;
@ -1156,11 +1156,8 @@ const installOpfsVfs = function callee(options){
opt.vfs = opfsVfs.$zName;
sqlite3.oo1.DB.dbCtorHelper.call(this, opt);
};
sqlite3.oo1.OpfsDb =
opfsUtil.OpfsDb /* sqlite3.opfs.OpfsDb => deprecated name -
will be phased out Real Soon */ =
OpfsDb;
OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype);
sqlite3.oo1.OpfsDb = OpfsDb;
sqlite3.oo1.DB.dbCtorHelper.setVfsPostOpenSql(
opfsVfs.pointer,
function(oo1Db, sqlite3){
@ -1185,7 +1182,7 @@ const installOpfsVfs = function callee(options){
], 0, 0, 0);
}
);
}
}/*extend sqlite3.oo1*/
const sanityCheck = function(){
const scope = wasm.scopedAllocPush();
@ -1231,7 +1228,7 @@ const installOpfsVfs = function callee(options){
const readBuf = wasm.scopedAlloc(16);
rc = ioSyncWrappers.xRead(sq3File.pointer, readBuf, 6, 2);
wasm.setMemValue(readBuf+6,0);
let jRead = wasm.cstringToJs(readBuf);
let jRead = wasm.cstrToJs(readBuf);
log("xRead() got:",jRead);
if("sanity"!==jRead) toss("Unexpected xRead() value.");
if(vfsSyncWrappers.xSleep){

View File

@ -368,7 +368,7 @@ void sqlite3_wasm_test_struct(WasmTestStruct * s){
*/
SQLITE_WASM_KEEP
const char * sqlite3_wasm_enum_json(void){
static char aBuffer[1024 * 12] = {0} /* where the JSON goes */;
static char aBuffer[1024 * 16] = {0} /* where the JSON goes */;
int n = 0, nChildren = 0, nStruct = 0
/* output counters for figuring out where commas go */;
char * zPos = &aBuffer[1] /* skip first byte for now to help protect
@ -410,6 +410,12 @@ const char * sqlite3_wasm_enum_json(void){
DefInt(SQLITE_ACCESS_READ)/*docs say this is unused*/;
} _DefGroup;
/* TODO? Authorizer... */
DefGroup(authorizer){
DefInt(SQLITE_DENY);
DefInt(SQLITE_IGNORE);
} _DefGroup;
DefGroup(blobFinalizers) {
/* SQLITE_STATIC/TRANSIENT need to be handled explicitly as
** integers to avoid casting-related warnings. */
@ -681,7 +687,36 @@ const char * sqlite3_wasm_enum_json(void){
DefStr(SQLITE_VERSION);
DefStr(SQLITE_SOURCE_ID);
} _DefGroup;
DefGroup(vtab) {
DefInt(SQLITE_INDEX_SCAN_UNIQUE);
DefInt(SQLITE_INDEX_CONSTRAINT_EQ);
DefInt(SQLITE_INDEX_CONSTRAINT_GT);
DefInt(SQLITE_INDEX_CONSTRAINT_LE);
DefInt(SQLITE_INDEX_CONSTRAINT_LT);
DefInt(SQLITE_INDEX_CONSTRAINT_GE);
DefInt(SQLITE_INDEX_CONSTRAINT_MATCH);
DefInt(SQLITE_INDEX_CONSTRAINT_LIKE);
DefInt(SQLITE_INDEX_CONSTRAINT_GLOB);
DefInt(SQLITE_INDEX_CONSTRAINT_REGEXP);
DefInt(SQLITE_INDEX_CONSTRAINT_NE);
DefInt(SQLITE_INDEX_CONSTRAINT_ISNOT);
DefInt(SQLITE_INDEX_CONSTRAINT_ISNOTNULL);
DefInt(SQLITE_INDEX_CONSTRAINT_ISNULL);
DefInt(SQLITE_INDEX_CONSTRAINT_IS);
DefInt(SQLITE_INDEX_CONSTRAINT_LIMIT);
DefInt(SQLITE_INDEX_CONSTRAINT_OFFSET);
DefInt(SQLITE_INDEX_CONSTRAINT_FUNCTION);
DefInt(SQLITE_VTAB_CONSTRAINT_SUPPORT);
DefInt(SQLITE_VTAB_INNOCUOUS);
DefInt(SQLITE_VTAB_DIRECTONLY);
DefInt(SQLITE_ROLLBACK);
//DefInt(SQLITE_IGNORE); // Also used by sqlite3_authorizer() callback
DefInt(SQLITE_FAIL);
//DefInt(SQLITE_ABORT); // Also an error code
DefInt(SQLITE_REPLACE);
} _DefGroup;
#undef DefGroup
#undef DefStr
#undef DefInt
@ -793,6 +828,128 @@ const char * sqlite3_wasm_enum_json(void){
} _StructBinder;
#undef CurrentStruct
#define CurrentStruct sqlite3_vtab
StructBinder {
M(pModule, "p");
M(nRef, "i");
M(zErrMsg, "p");
} _StructBinder;
#undef CurrentStruct
#define CurrentStruct sqlite3_vtab_cursor
StructBinder {
M(pVtab, "p");
} _StructBinder;
#undef CurrentStruct
#define CurrentStruct sqlite3_module
StructBinder {
M(iVersion, "i");
M(xCreate, "i(ppippp)");
M(xConnect, "i(ppippp)");
M(xBestIndex, "i(pp)");
M(xDisconnect, "i(p)");
M(xDestroy, "i(p)");
M(xOpen, "i(pp)");
M(xClose, "i(p)");
M(xFilter, "i(pisip)");
M(xNext, "i(p)");
M(xEof, "i(p)");
M(xColumn, "i(ppi)");
M(xRowid, "i(pp)");
M(xUpdate, "i(pipp)");
M(xBegin, "i(p)");
M(xSync, "i(p)");
M(xCommit, "i(p)");
M(xRollback, "i(p)");
M(xFindFunction, "i(pispp)");
M(xRename, "i(ps)");
// ^^^ v1. v2+ follows...
M(xSavepoint, "i(pi)");
M(xRelease, "i(pi)");
M(xRollbackTo, "i(pi)");
// ^^^ v2. v3+ follows...
M(xShadowName, "i(s)");
} _StructBinder;
#undef CurrentStruct
/**
** Workaround: in order to map the various inner structs from
** sqlite3_index_info, we have to uplift those into constructs we
** can access by type name. These structs _must_ match their
** in-sqlite3_index_info counterparts byte for byte.
*/
typedef struct {
int iColumn;
unsigned char op;
unsigned char usable;
int iTermOffset;
} sqlite3_index_constraint;
typedef struct {
int iColumn;
unsigned char desc;
} sqlite3_index_orderby;
typedef struct {
int argvIndex;
unsigned char omit;
} sqlite3_index_constraint_usage;
{ /* Validate that the above struct sizeof()s match
** expectations. We could improve upon this by
** checking the offsetof() for each member. */
const sqlite3_index_info siiCheck;
#define IndexSzCheck(T,M) \
(sizeof(T) == sizeof(*siiCheck.M))
if(!IndexSzCheck(sqlite3_index_constraint,aConstraint)
|| !IndexSzCheck(sqlite3_index_orderby,aOrderBy)
|| !IndexSzCheck(sqlite3_index_constraint_usage,aConstraintUsage)){
assert(!"sizeof mismatch in sqlite3_index_... struct(s)");
return 0;
}
#undef IndexSzCheck
}
#define CurrentStruct sqlite3_index_constraint
StructBinder {
M(iColumn, "i");
M(op, "C");
M(usable, "C");
M(iTermOffset, "i");
} _StructBinder;
#undef CurrentStruct
#define CurrentStruct sqlite3_index_orderby
StructBinder {
M(iColumn, "i");
M(desc, "C");
} _StructBinder;
#undef CurrentStruct
#define CurrentStruct sqlite3_index_constraint_usage
StructBinder {
M(argvIndex, "i");
M(omit, "C");
} _StructBinder;
#undef CurrentStruct
#define CurrentStruct sqlite3_index_info
StructBinder {
M(nConstraint, "i");
M(aConstraint, "p");
M(nOrderBy, "i");
M(aOrderBy, "p");
M(aConstraintUsage, "p");
M(idxNum, "i");
M(idxStr, "p");
M(needToFreeIdxStr, "i");
M(orderByConsumed, "i");
M(estimatedCost, "d");
M(estimatedRows, "j");
M(idxFlags, "i");
M(colUsed, "j");
} _StructBinder;
#undef CurrentStruct
#if SQLITE_WASM_TESTS
#define CurrentStruct WasmTestStruct
StructBinder {
@ -864,7 +1021,11 @@ sqlite3_vfs * sqlite3_wasm_db_vfs(sqlite3 *pDb, const char *zDbName){
**
** This function resets the given db pointer's database as described at
**
** https://www.sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigresetdatabase
** https://sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigresetdatabase
**
** But beware: virtual tables destroyed that way do not have their
** xDestroy() called, so will leak if they require that function for
** proper cleanup.
**
** Returns 0 on success, an SQLITE_xxx code on error. Returns
** SQLITE_MISUSE if pDb is NULL.
@ -873,6 +1034,7 @@ SQLITE_WASM_KEEP
int sqlite3_wasm_db_reset(sqlite3 *pDb){
int rc = SQLITE_MISUSE;
if( pDb ){
sqlite3_table_column_metadata(pDb, "main", 0, 0, 0, 0, 0, 0, 0);
rc = sqlite3_db_config(pDb, SQLITE_DBCONFIG_RESET_DATABASE, 1, 0);
if( 0==rc ){
rc = sqlite3_exec(pDb, "VACUUM", 0, 0, 0);
@ -1109,6 +1271,31 @@ sqlite3_kvvfs_methods * sqlite3_wasm_kvvfs_methods(void){
return &sqlite3KvvfsMethods;
}
/*
** This function is NOT part of the sqlite3 public API. It is strictly
** for use by the sqlite project's own JS/WASM bindings.
**
** This is a proxy for the variadic sqlite3_vtab_config() which passes
** its argument on, or not, to sqlite3_vtab_config(), depending on the
** value of its 2nd argument. Returns the result of
** sqlite3_vtab_config(), or SQLITE_MISUSE if the 2nd arg is not a
** valid value.
*/
SQLITE_WASM_KEEP
int sqlite3_wasm_vtab_config(sqlite3 *pDb, int op, int arg){
switch(op){
case SQLITE_VTAB_DIRECTONLY:
case SQLITE_VTAB_INNOCUOUS:
return sqlite3_vtab_config(pDb, op);
case SQLITE_VTAB_CONSTRAINT_SUPPORT:
return sqlite3_vtab_config(pDb, op, arg);
default:
return SQLITE_MISUSE;
}
}
#if defined(__EMSCRIPTEN__) && defined(SQLITE_ENABLE_WASMFS)
#include <emscripten/wasmfs.h>

View File

@ -1,4 +1,4 @@
/* emcscript-related styling, used during the module load/intialization processes... */
/* emscripten-related styling, used during the module load/intialization processes... */
.emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
div.emscripten { text-align: center; }
div.emscripten_border { border: 1px solid black; }

View File

@ -61,3 +61,9 @@ span.labeled-input {
flex-direction: column-reverse;
}
label[for] { cursor: pointer }
h1 {
border-radius: 0.25em;
padding: 0.15em 0.25em;
}
h1:first-of-type {margin: 0 0 0.5em 0;}

View File

@ -725,11 +725,12 @@ self.WhWasmUtilInstaller = function(target){
Expects ptr to be a pointer into the WASM heap memory which
refers to a NUL-terminated C-style string encoded as UTF-8.
Returns the length, in bytes, of the string, as for `strlen(3)`.
As a special case, if !ptr then it it returns `null`. Throws if
ptr is out of range for target.heap8u().
As a special case, if !ptr or if it's not a pointer then it
returns `null`. Throws if ptr is out of range for
target.heap8u().
*/
target.cstrlen = function(ptr){
if(!ptr) return null;
if(!ptr || !target.isPtr(ptr)) return null;
const h = heapWrappers().HEAP8U;
let pos = ptr;
for( ; h[pos] !== 0; ++pos ){}
@ -753,9 +754,9 @@ self.WhWasmUtilInstaller = function(target){
refers to a NUL-terminated C-style string encoded as UTF-8. This
function counts its byte length using cstrlen() then returns a
JS-format string representing its contents. As a special case, if
ptr is falsy, `null` is returned.
ptr is falsy or not a pointer, `null` is returned.
*/
target.cstringToJs = function(ptr){
target.cstrToJs = function(ptr){
const n = target.cstrlen(ptr);
return n ? __utf8Decode(heapWrappers().HEAP8U, ptr, ptr+n) : (null===n ? n : "");
};
@ -942,10 +943,19 @@ self.WhWasmUtilInstaller = function(target){
const __allocCStr = function(jstr, returnWithLength, allocator, funcName){
__affirmAlloc(target, funcName);
if('string'!==typeof jstr) return null;
const n = target.jstrlen(jstr),
ptr = allocator(n+1);
target.jstrcpy(jstr, target.heap8u(), ptr, n+1, true);
return returnWithLength ? [ptr, n] : ptr;
if(0){/* older impl, possibly more widely compatible? */
const n = target.jstrlen(jstr),
ptr = allocator(n+1);
target.jstrcpy(jstr, target.heap8u(), ptr, n+1, true);
return returnWithLength ? [ptr, n] : ptr;
}else{/* newer, (probably) faster and (certainly) simpler impl */
const u = cache.utf8Encoder.encode(jstr),
ptr = allocator(u.length+1),
heap = heapWrappers().HEAP8U;
heap.set(u, ptr);
heap[ptr + u.length] = 0;
return returnWithLength ? [ptr, u.length] : ptr;
}
};
/**
@ -1081,10 +1091,9 @@ self.WhWasmUtilInstaller = function(target){
// impl for allocMainArgv() and scopedAllocMainArgv().
const __allocMainArgv = function(isScoped, list){
if(!list.length) toss("Cannot allocate empty array.");
const pList = target[
isScoped ? 'scopedAlloc' : 'alloc'
](list.length * target.ptrSizeof);
]((list.length + 1) * target.ptrSizeof);
let i = 0;
list.forEach((e)=>{
target.setPtrValue(pList + (target.ptrSizeof * i++),
@ -1092,29 +1101,57 @@ self.WhWasmUtilInstaller = function(target){
isScoped ? 'scopedAllocCString' : 'allocCString'
](""+e));
});
target.setPtrValue(pList + (target.ptrSizeof * i), 0);
return pList;
};
/**
Creates an array, using scopedAlloc(), suitable for passing to a
C-level main() routine. The input is a collection with a length
property and a forEach() method. A block of memory list.length
entries long is allocated and each pointer-sized block of that
memory is populated with a scopedAllocCString() conversion of the
(""+value) of each element. Returns a pointer to the start of the
list, suitable for passing as the 2nd argument to a C-style
main() function.
property and a forEach() method. A block of memory
(list.length+1) entries long is allocated and each pointer-sized
block of that memory is populated with a scopedAllocCString()
conversion of the (""+value) of each element, with the exception
that the final entry is a NULL pointer. Returns a pointer to the
start of the list, suitable for passing as the 2nd argument to a
C-style main() function.
Throws if list.length is falsy or scopedAllocPush() is not active.
Throws if scopedAllocPush() is not active.
Design note: the returned array is allocated with an extra NULL
pointer entry to accommodate certain APIs, but client code which
does not need that functionality should treat the returned array
as list.length entries long.
*/
target.scopedAllocMainArgv = (list)=>__allocMainArgv(true, list);
/**
Identical to scopedAllocMainArgv() but uses alloc() instead of
scopedAllocMainArgv
scopedAlloc().
*/
target.allocMainArgv = (list)=>__allocMainArgv(false, list);
/**
Expects to be given a C-style string array and its length. It
returns a JS array of strings and/or nulls: any entry in the
pArgv array which is NULL results in a null entry in the result
array. If argc is 0 then an empty array is returned.
Results are undefined if any entry in the first argc entries of
pArgv are neither 0 (NULL) nor legal UTF-format C strings.
To be clear, the expected C-style arguments to be passed to this
function are `(int, char **)` (optionally const-qualified).
*/
target.cArgvToJs = (argc, pArgv)=>{
const list = [];
for(let i = 0; i < argc; ++i){
const arg = target.getPtrValue(pArgv + (target.ptrSizeof * i));
list.push( arg ? target.cstrToJs(arg) : null );
}
return list;
};
/**
Wraps function call func() in a scopedAllocPush() and
scopedAllocPop() block, such that all calls to scopedAlloc() and
@ -1278,14 +1315,14 @@ self.WhWasmUtilInstaller = function(target){
if('string'===typeof v) return target.scopedAllocCString(v);
return v ? xcv.arg[ptrIR](v) : null;
};
xcv.result.string = xcv.result.utf8 = (i)=>target.cstringToJs(i);
xcv.result.string = xcv.result.utf8 = (i)=>target.cstrToJs(i);
xcv.result['string:dealloc'] = xcv.result['utf8:dealloc'] = (i)=>{
try { return i ? target.cstringToJs(i) : null }
try { return i ? target.cstrToJs(i) : null }
finally{ target.dealloc(i) }
};
xcv.result.json = (i)=>JSON.parse(target.cstringToJs(i));
xcv.result.json = (i)=>JSON.parse(target.cstrToJs(i));
xcv.result['json:dealloc'] = (i)=>{
try{ return i ? JSON.parse(target.cstringToJs(i)) : null }
try{ return i ? JSON.parse(target.cstrToJs(i)) : null }
finally{ target.dealloc(i) }
}
xcv.result['void'] = (v)=>undefined;
@ -1423,7 +1460,7 @@ self.WhWasmUtilInstaller = function(target){
```js
target.xWrap.resultAdapter('string:my_free',(i)=>{
try { return i ? target.cstringToJs(i) : null }
try { return i ? target.cstrToJs(i) : null }
finally{ target.exports.my_free(i) }
};
```

View File

@ -10,9 +10,12 @@
***********************************************************************
The Jaccwabyt API is documented in detail in an external file.
The Jaccwabyt API is documented in detail in an external file,
_possibly_ called jaccwabyt.md in the same directory as this file.
Project home: https://fossil.wanderinghorse.net/r/jaccwabyt
Project homes:
- https://fossil.wanderinghorse.net/r/jaccwabyt
- https://sqlite.org/src/dir/ext/wasm/jaccwabyt
*/
'use strict';
@ -61,7 +64,6 @@ self.Jaccwabyt = function StructBinderFactory(config){
BigInt = self['BigInt'],
BigInt64Array = self['BigInt64Array'],
/* Undocumented (on purpose) config options: */
functionTable = config.functionTable/*EXPERIMENTAL, undocumented*/,
ptrSizeof = config.ptrSizeof || 4,
ptrIR = config.ptrIR || 'i32'
;
@ -121,6 +123,7 @@ self.Jaccwabyt = function StructBinderFactory(config){
at SIG s[0]. Throws for an unknown SIG. */
const sigIR = function(s){
switch(sigLetter(s)){
case 'c': case 'C': return 'i8';
case 'i': return 'i32';
case 'p': case 'P': case 's': return ptrIR;
case 'j': return 'i64';
@ -129,33 +132,9 @@ self.Jaccwabyt = function StructBinderFactory(config){
}
toss("Unhandled signature IR:",s);
};
/** Returns the sizeof value for the given SIG. Throws for an
unknown SIG. */
const sigSizeof = function(s){
switch(sigLetter(s)){
case 'i': return 4;
case 'p': case 'P': case 's': return ptrSizeof;
case 'j': return 8;
case 'f': return 4 /* C-side floats, not JS-side */;
case 'd': return 8;
}
toss("Unhandled signature sizeof:",s);
};
const affirmBigIntArray = BigInt64Array
? ()=>true : ()=>toss('BigInt64Array is not available.');
/** Returns the (signed) TypedArray associated with the type
described by the given SIG. Throws for an unknown SIG. */
/**********
const sigTypedArray = function(s){
switch(sigIR(s)) {
case 'i32': return Int32Array;
case 'i64': return affirmBigIntArray() && BigInt64Array;
case 'float': return Float32Array;
case 'double': return Float64Array;
}
toss("Unhandled signature TypedArray:",s);
};
**************/
/** Returns the name of a DataView getter method corresponding
to the given SIG. */
const sigDVGetter = function(s){
@ -168,6 +147,8 @@ self.Jaccwabyt = function StructBinderFactory(config){
break;
}
case 'i': return 'getInt32';
case 'c': return 'getInt8';
case 'C': return 'getUint8';
case 'j': return affirmBigIntArray() && 'getBigInt64';
case 'f': return 'getFloat32';
case 'd': return 'getFloat64';
@ -186,6 +167,8 @@ self.Jaccwabyt = function StructBinderFactory(config){
break;
}
case 'i': return 'setInt32';
case 'c': return 'setInt8';
case 'C': return 'setUint8';
case 'j': return affirmBigIntArray() && 'setBigInt64';
case 'f': return 'setFloat32';
case 'd': return 'setFloat64';
@ -199,7 +182,7 @@ self.Jaccwabyt = function StructBinderFactory(config){
*/
const sigDVSetWrapper = function(s){
switch(sigLetter(s)) {
case 'i': case 'f': case 'd': return Number;
case 'i': case 'f': case 'c': case 'C': case 'd': return Number;
case 'j': return affirmBigIntArray() && BigInt;
case 'p': case 'P': case 's':
switch(ptrSizeof){
@ -211,36 +194,14 @@ self.Jaccwabyt = function StructBinderFactory(config){
toss("Unhandled DataView set wrapper for signature:",s);
};
/** Returns the given struct and member name in a form suitable for
debugging and error output. */
const sPropName = (s,k)=>s+'::'+k;
const __propThrowOnSet = function(structName,propName){
return ()=>toss(sPropName(structName,propName),"is read-only.");
};
/**
When C code passes a pointer of a bound struct to back into
a JS function via a function pointer struct member, it
arrives in JS as a number (pointer).
StructType.instanceForPointer(ptr) can be used to get the
instance associated with that pointer, and __ptrBacklinks
holds that mapping. WeakMap keys must be objects, so we
cannot use a weak map to map pointers to instances. We use
the StructType constructor as the WeakMap key, mapped to a
plain, prototype-less Object which maps the pointers to
struct instances. That arrangement gives us a
per-StructType type-safe way to resolve pointers.
*/
const __ptrBacklinks = new WeakMap();
/**
Similar to __ptrBacklinks but is scoped at the StructBinder
level and holds pointer-to-object mappings for all struct
instances created by any struct from any StructFactory
which this specific StructBinder has created. The intention
of this is to help implement more transparent handling of
pointer-type property resolution.
*/
const __ptrBacklinksGlobal = Object.create(null);
/**
In order to completely hide StructBinder-bound struct
pointers from JS code, we store them in a scope-local
@ -261,17 +222,13 @@ self.Jaccwabyt = function StructBinderFactory(config){
const __freeStruct = function(ctor, obj, m){
if(!m) m = __instancePointerMap.get(obj);
if(m) {
if(obj.ondispose instanceof Function){
try{obj.ondispose()}
catch(e){
/*do not rethrow: destructors must not throw*/
console.warn("ondispose() for",ctor.structName,'@',
m,'threw. NOT propagating it.',e);
}
}else if(Array.isArray(obj.ondispose)){
obj.ondispose.forEach(function(x){
__instancePointerMap.delete(obj);
if(Array.isArray(obj.ondispose)){
let x;
while((x = obj.ondispose.shift())){
try{
if(x instanceof Function) x.call(obj);
else if(x instanceof StructType) x.dispose();
else if('number' === typeof x) dealloc(x);
// else ignore. Strings are permitted to annotate entries
// to assist in debugging.
@ -279,12 +236,16 @@ self.Jaccwabyt = function StructBinderFactory(config){
console.warn("ondispose() for",ctor.structName,'@',
m,'threw. NOT propagating it.',e);
}
});
}
}else if(obj.ondispose instanceof Function){
try{obj.ondispose()}
catch(e){
/*do not rethrow: destructors must not throw*/
console.warn("ondispose() for",ctor.structName,'@',
m,'threw. NOT propagating it.',e);
}
}
delete obj.ondispose;
delete __ptrBacklinks.get(ctor)[m];
delete __ptrBacklinksGlobal[m];
__instancePointerMap.delete(obj);
if(ctor.debugFlags.__flags.dealloc){
log("debug.dealloc:",(obj[xPtrPropName]?"EXTERNAL":""),
ctor.structName,"instance:",
@ -300,7 +261,7 @@ self.Jaccwabyt = function StructBinderFactory(config){
iterable: false, value: v}};
/** Allocates obj's memory buffer based on the size defined in
DEF.sizeof. */
ctor.structInfo.sizeof. */
const __allocStruct = function(ctor, obj, m){
let fill = !m;
if(m) Object.defineProperty(obj, xPtrPropName, rop(m));
@ -316,8 +277,6 @@ self.Jaccwabyt = function StructBinderFactory(config){
}
if(fill) heap().fill(0, m, m + ctor.structInfo.sizeof);
__instancePointerMap.set(obj, m);
__ptrBacklinks.get(ctor)[m] = obj;
__ptrBacklinksGlobal[m] = obj;
}catch(e){
__freeStruct(ctor, obj, m);
throw e;
@ -339,7 +298,7 @@ self.Jaccwabyt = function StructBinderFactory(config){
if tossIfNotFound is true, else returns undefined if not
found. The given name may be either the name of the
structInfo.members key (faster) or the key as modified by the
memberPrefix/memberSuffix settings.
memberPrefix and memberSuffix settings.
*/
const __lookupMember = function(structInfo, memberName, tossIfNotFound=true){
let m = structInfo.members[memberName];
@ -361,21 +320,11 @@ self.Jaccwabyt = function StructBinderFactory(config){
framework's native format or in Emscripten format.
*/
const __memberSignature = function f(obj,memberName,emscriptenFormat=false){
if(!f._) f._ = (x)=>x.replace(/[^vipPsjrd]/g,"").replace(/[pPs]/g,'i');
if(!f._) f._ = (x)=>x.replace(/[^vipPsjrdcC]/g,"").replace(/[pPscC]/g,'i');
const m = __lookupMember(obj.structInfo, memberName, true);
return emscriptenFormat ? f._(m.signature) : m.signature;
};
/**
Returns the instanceForPointer() impl for the given
StructType constructor.
*/
const __instanceBacklinkFactory = function(ctor){
const b = Object.create(null);
__ptrBacklinks.set(ctor, b);
return (ptr)=>b[ptr];
};
const __ptrPropDescriptor = {
configurable: false, enumerable: false,
get: function(){return __instancePointerMap.get(this)},
@ -388,7 +337,9 @@ self.Jaccwabyt = function StructBinderFactory(config){
/** Impl of X.memberKeys() for StructType and struct ctors. */
const __structMemberKeys = rop(function(){
const a = [];
Object.keys(this.structInfo.members).forEach((k)=>a.push(this.memberKey(k)));
for(const k of Object.keys(this.structInfo.members)){
a.push(this.memberKey(k));
}
return a;
});
@ -454,15 +405,15 @@ self.Jaccwabyt = function StructBinderFactory(config){
Adds value v to obj.ondispose, creating ondispose,
or converting it to an array, if needed.
*/
const __addOnDispose = function(obj, v){
const __addOnDispose = function(obj, ...v){
if(obj.ondispose){
if(obj.ondispose instanceof Function){
if(!Array.isArray(obj.ondispose)){
obj.ondispose = [obj.ondispose];
}/*else assume it's an array*/
}
}else{
obj.ondispose = [];
}
obj.ondispose.push(v);
obj.ondispose.push(...v);
};
/**
@ -477,8 +428,9 @@ self.Jaccwabyt = function StructBinderFactory(config){
const mem = alloc(u.length+1);
if(!mem) toss("Allocation error while duplicating string:",str);
const h = heap();
let i = 0;
for( ; i < u.length; ++i ) h[mem + i] = u[i];
//let i = 0;
//for( ; i < u.length; ++i ) h[mem + i] = u[i];
h.set(u, mem);
h[mem + u.length] = 0;
//log("allocCString @",mem," =",u);
return mem;
@ -490,6 +442,10 @@ self.Jaccwabyt = function StructBinderFactory(config){
to free any prior memory, if appropriate. The newly-allocated
string is added to obj.ondispose so will be freed when the object
is disposed.
The given name may be either the name of the structInfo.members
key (faster) or the key as modified by the memberPrefix and
memberSuffix settings.
*/
const __setMemberCString = function(obj, memberName, str){
const m = __lookupMember(obj.structInfo, memberName, true);
@ -544,13 +500,19 @@ self.Jaccwabyt = function StructBinderFactory(config){
return __setMemberCString(this, memberName, str);
})
});
// Function-type non-Property inherited members
Object.assign(StructType.prototype,{
addOnDispose: function(...v){
__addOnDispose(this,...v);
return this;
}
});
/**
"Static" properties for StructType.
*/
Object.defineProperties(StructType, {
allocCString: rop(__allocCString),
instanceForPointer: rop((ptr)=>__ptrBacklinksGlobal[ptr]),
isA: rop((v)=>v instanceof StructType),
hasExternalPointer: rop((v)=>(v instanceof StructType) && !!v[xPtrPropName]),
memberKey: __memberKeyProp
@ -570,7 +532,7 @@ self.Jaccwabyt = function StructBinderFactory(config){
/*cache all available getters/setters/set-wrappers for
direct reuse in each accessor function. */
f._ = {getters: {}, setters: {}, sw:{}};
const a = ['i','p','P','s','f','d','v()'];
const a = ['i','c','C','p','P','s','f','d','v()'];
if(bigIntEnabled) a.push('j');
a.forEach(function(v){
//const ir = sigIR(v);
@ -579,8 +541,8 @@ self.Jaccwabyt = function StructBinderFactory(config){
f._.sw[v] = sigDVSetWrapper(v) /* BigInt or Number ctor to wrap around values
for conversion */;
});
const rxSig1 = /^[ipPsjfd]$/,
rxSig2 = /^[vipPsjfd]\([ipPsjfd]*\)$/;
const rxSig1 = /^[ipPsjfdcC]$/,
rxSig2 = /^[vipPsjfdcC]\([ipPsjfdcC]*\)$/;
f.sigCheck = function(obj, name, key,sig){
if(Object.prototype.hasOwnProperty.call(obj, key)){
toss(obj.structName,'already has a property named',key+'.');
@ -594,7 +556,6 @@ self.Jaccwabyt = function StructBinderFactory(config){
f.sigCheck(ctor.prototype, name, key, descr.signature);
descr.key = key;
descr.name = name;
const sizeOf = sigSizeof(descr.signature);
const sigGlyph = sigLetter(descr.signature);
const xPropName = sPropName(ctor.prototype.structName,key);
const dbg = ctor.prototype.debugFlags.__flags;
@ -610,16 +571,12 @@ self.Jaccwabyt = function StructBinderFactory(config){
prop.get = function(){
if(dbg.getter){
log("debug.getter:",f._.getters[sigGlyph],"for", sigIR(sigGlyph),
xPropName,'@', this.pointer,'+',descr.offset,'sz',sizeOf);
xPropName,'@', this.pointer,'+',descr.offset,'sz',descr.sizeof);
}
let rc = (
new DataView(heap().buffer, this.pointer + descr.offset, sizeOf)
new DataView(heap().buffer, this.pointer + descr.offset, descr.sizeof)
)[f._.getters[sigGlyph]](0, isLittleEndian);
if(dbg.getter) log("debug.getter:",xPropName,"result =",rc);
if(rc && isAutoPtrSig(descr.signature)){
rc = StructType.instanceForPointer(rc) || rc;
if(dbg.getter) log("debug.getter:",xPropName,"resolved =",rc);
}
return rc;
};
if(descr.readOnly){
@ -628,7 +585,7 @@ self.Jaccwabyt = function StructBinderFactory(config){
prop.set = function(v){
if(dbg.setter){
log("debug.setter:",f._.setters[sigGlyph],"for", sigIR(sigGlyph),
xPropName,'@', this.pointer,'+',descr.offset,'sz',sizeOf, v);
xPropName,'@', this.pointer,'+',descr.offset,'sz',descr.sizeof, v);
}
if(!this.pointer){
toss("Cannot set struct property on disposed instance.");
@ -644,7 +601,7 @@ self.Jaccwabyt = function StructBinderFactory(config){
toss("Invalid value for pointer-type",xPropName+'.');
}
(
new DataView(heap().buffer, this.pointer + descr.offset, sizeOf)
new DataView(heap().buffer, this.pointer + descr.offset, descr.sizeof)
)[f._.setters[sigGlyph]](0, f._.sw[sigGlyph](v), isLittleEndian);
};
}
@ -665,13 +622,25 @@ self.Jaccwabyt = function StructBinderFactory(config){
if(!structName) toss("Struct name is required.");
let lastMember = false;
Object.keys(structInfo.members).forEach((k)=>{
// Sanity checks of sizeof/offset info...
const m = structInfo.members[k];
if(!m.sizeof) toss(structName,"member",k,"is missing sizeof.");
else if(0!==(m.sizeof%4)){
toss(structName,"member",k,"sizeof is not aligned.");
}
else if(0!==(m.offset%4)){
toss(structName,"member",k,"offset is not aligned.");
else if(m.sizeof===1){
(m.signature === 'c' || m.signature === 'C') ||
toss("Unexpected sizeof==1 member",
sPropName(structInfo.name,k),
"with signature",m.signature);
}else{
// sizes and offsets of size-1 members may be odd values, but
// others may not.
if(0!==(m.sizeof%4)){
console.warn("Invalid struct member description =",m,"from",structInfo);
toss(structName,"member",k,"sizeof is not aligned. sizeof="+m.sizeof);
}
if(0!==(m.offset%4)){
console.warn("Invalid struct member description =",m,"from",structInfo);
toss(structName,"member",k,"offset is not aligned. offset="+m.offset);
}
}
if(!lastMember || lastMember.offset < m.offset) lastMember = m;
});
@ -697,27 +666,9 @@ self.Jaccwabyt = function StructBinderFactory(config){
};
Object.defineProperties(StructCtor,{
debugFlags: debugFlags,
disposeAll: rop(function(){
const map = __ptrBacklinks.get(StructCtor);
Object.keys(map).forEach(function(ptr){
const b = map[ptr];
if(b) __freeStruct(StructCtor, b, ptr);
});
__ptrBacklinks.set(StructCtor, Object.create(null));
return StructCtor;
}),
instanceForPointer: rop(__instanceBacklinkFactory(StructCtor)),
isA: rop((v)=>v instanceof StructCtor),
memberKey: __memberKeyProp,
memberKeys: __structMemberKeys,
resolveToInstance: rop(function(v, throwIfNot=false){
if(!(v instanceof StructCtor)){
v = Number.isSafeInteger(v)
? StructCtor.instanceForPointer(v) : undefined;
}
if(!v && throwIfNot) toss("Value is-not-a",StructCtor.structName);
return v;
}),
methodInfoForKey: rop(function(mKey){
}),
structInfo: rop(structInfo),
@ -735,7 +686,6 @@ self.Jaccwabyt = function StructBinderFactory(config){
);
return StructCtor;
};
StructBinder.instanceForPointer = StructType.instanceForPointer;
StructBinder.StructType = StructType;
StructBinder.config = config;
StructBinder.allocCString = __allocCString;

View File

@ -4,7 +4,6 @@ Jaccwabyt 🐇
**Jaccwabyt**: _JavaScript ⇄ C Struct Communication via WASM Byte
Arrays_
Welcome to Jaccwabyt, a JavaScript API which creates bindings for
WASM-compiled C structs, defining them in such a way that changes to
their state in JS are visible in C/WASM, and vice versa, permitting
@ -27,8 +26,28 @@ are based solely on feature compatibility tables provided at
**Formalities:**
- Author: [Stephan Beal][sgb]
- License: Public Domain
- Project Home: <https://fossil.wanderinghorse.net/r/jaccwabyt>
- Project Homes:
- <https://fossil.wanderinghorse.net/r/jaccwabyt>\
Is the primary home but...
- <https://sqlite.org/src/dir/ext/wasm/jaccwabyt>\
... most development happens here.
The license for both this documentation and the software it documents
is the same as [sqlite3][], the project from which this spinoff
project was spawned:
-----
> 2022-06-30:
>
> The author disclaims copyright to this source code. In place of a
> legal notice, here is a blessing:
>
> May you do good and not evil.
> May you find forgiveness for yourself and forgive others.
> May you share freely, never taking more than you give.
-----
<a name='overview'></a>
Table of Contents
@ -205,7 +224,6 @@ simply look like:
The StructBinder factory function returns a function which can then be
used to create bindings for our structs.
<a name='step-2'></a>
Step 2: Create a Struct Description
------------------------------------------------------------
@ -281,21 +299,29 @@ supported letters are:
signature entry.
- **`f`** = `float` (4 bytes)
- **`d`** = `double` (8 bytes)
- **`p`** = `int32` (but see below!)
- **`c`** = `int8` (1 byte) char - see notes below!
- **`C`** = `uint8` (1 byte) unsigned char - see notes below!
- **`p`** = `int32` (see notes below!)
- **`P`** = Like `p` but with extra handling. Described below.
- **`s`** = like `int32` but is a _hint_ that it's a pointer to a string
so that _some_ (very limited) contexts may treat it as such, noting
such algorithms must, for lack of information to the contrary,
assume both that the encoding is UTF-8 and that the pointer's member
is NUL-terminated. If that is _not_ the case for a given string
member, do not use `s`: use `i` or `p` instead and do any string
handling yourself.
- **`s`** = like `int32` but is a _hint_ that it's a pointer to a
string so that _some_ (very limited) contexts may treat it as such,
noting that such algorithms must, for lack of information to the
contrary, assume both that the encoding is UTF-8 and that the
pointer's member is NUL-terminated. If that is _not_ the case for a
given string member, do not use `s`: use `i` or `p` instead and do
any string handling yourself.
Noting that:
- All of these types are numeric. Attempting to set any struct-bound
property to a non-numeric value will trigger an exception except in
cases explicitly noted otherwise.
- **All of these types are numeric**. Attempting to set any
struct-bound property to a non-numeric value will trigger an
exception except in cases explicitly noted otherwise.
- **"Char" types**: WASM does not define an `int8` type, nor does it
distinguish between signed and unsigned. This API treats `c` as
`int8` and `C` as `uint8` for purposes of getting and setting values
when using the `DataView` class. It is _not_ recommended that client
code use these types in new WASM-capable code, but they were added
for the sake of binding some immutable legacy code to WASM.
> Sidebar: Emscripten's public docs do not mention `p`, but their
generated code includes `p` as an alias for `i`, presumably to mean
@ -317,12 +343,12 @@ Signatures in the form `x(...)` denote function-pointer members and
form `x()`. For function-type signatures, the strings are formulated
such that they can be passed to Emscripten's `addFunction()` after
stripping out the `(` and `)` characters. For good measure, to match
the public Emscripten docs, `p` should also be replaced with `i`. In
JavaScript that might look like:
the public Emscripten docs, `p`, `c`, and `C`, should also be replaced
with `i`. In JavaScript that might look like:
>
```
signature.replace(/[^vipPsjfd]/g,'').replace(/[pPs]/g,'i');
signature.replace(/[^vipPsjfdcC]/g,'').replace(/[pPscC]/g,'i');
```
<a name='step-2-pvsp'></a>
@ -337,12 +363,6 @@ special use of unsigned numbers). A capital `P` changes the semantics
of plain member pointers (but not, as of this writing, function
pointer members) as follows:
- When a `P`-type member is **fetched** via `myStruct.x` and its value is
a non-0 integer, [`StructBinder.instanceForPointer()`][StructBinder]
is used to try to map that pointer to a struct instance. If a match
is found, the "get" operation returns that instance instead of the
integer. If no match is found, it behaves exactly as for `p`, returning
the integer value.
- When a `P`-type member is **set** via `myStruct.x=y`, if
[`(y instanceof StructType)`][StructType] then the value of `y.pointer` is
stored in `myStruct.x`. If `y` is neither a number nor
@ -388,14 +408,11 @@ It is important to understand that creating a new instance allocates
memory on the WASM heap. We must not simply rely on garbage collection
to clean up the instances because doing so will not free up the WASM
heap memory. The correct way to free up that memory is to use the
object's `dispose()` method. Alternately, there is a "nuclear option":
`MyBinder.disposeAll()` will free the memory allocated for _all_
instances which have not been manually disposed.
object's `dispose()` method.
The following usage pattern offers one way to easily ensure proper
cleanup of struct instances:
>
```javascript
const my = new MyStruct();
@ -409,11 +426,6 @@ try {
from the byte array. */
// Pass the struct to C code which takes a MyStruct pointer:
aCFunction( my.pointer );
// Type-safely check if a pointer returned from C is a MyStruct:
const x = MyStruct.instanceForPointer( anotherCFunction() );
// If it is a MyStruct, x now refers to that object. Note, however,
// that this only works for instances created in JS, as the
// pointer mapping only exists in JS space.
} finally {
my.dispose();
}
@ -426,6 +438,16 @@ to use `try`/`finally` without a `catch`, and doing so is an ideal
match for the memory management requirements of Jaccwaby-bound struct
instances.
It is often useful to wrap an existing instance of a C-side struct
without taking over ownership of its memory. That can be achieved by
simply passing a pointer to the constructor. For example:
```js
const m = new MyStruct( functionReturningASharedPtr() );
// calling m.dispose() will _not_ free the wrapped C-side instance
// but will trigger any ondispose handler.
```
Now that we have struct instances, there are a number of things we
can do with them, as covered in the rest of this document.
@ -549,20 +571,6 @@ The Struct Binder has the following members:
any of its "significant" configuration values may have undefined
results.
- `instanceForPointer(pointer)`
Given a pointer value relative to `config.memory`, if that pointer
resolves to a struct of _any type_ generated via the same Struct
Binder, this returns the struct instance associated with it, or
`undefined` if no struct object is mapped to that pointer. This
differs from the struct-type-specific member of the same name in
that this one is not "type-safe": it does not know the type of the
returned object (if any) and may return a struct of any
[StructType][] for which this Struct Binder has created a
constructor. It cannot return instances created via a different
[StructBinderFactory][] because each factory can hypothetically have
a different memory heap.
<a name='api-structtype'></a>
API: Struct Type
------------------------------------------------------------
@ -582,6 +590,14 @@ only called by the [StructBinder][]-generated
has the following "static" properties (^Which are accessible from
individual instances via `theInstance.constructor`.):
- `addOnDispose(...value)`\
If this object has no `ondispose` property, this function creates it
as an array and pushes the given value(s) onto it. If the object has
a function-typed `ondispose` property, this call replaces it with an
array and moves that function into the array. In all other cases,
`ondispose` is assumed to be an array and the argument(s) is/are
appended to it. Returns `this`.
- `allocCString(str)`
Identical to the [StructBinder][] method of the same name.
@ -591,9 +607,6 @@ individual instances via `theInstance.constructor`.):
[struct's constructor][StructCtors]. If true, the memory is owned by
someone other than the object and must outlive the object.
- `instanceForPointer(pointer)`
Works identically to the [StructBinder][] method of the same name.
- `isA(value)`
Returns true if its argument is a StructType instance _from the same
[StructBinder][]_ as this StructType.
@ -619,14 +632,15 @@ legally be called on concrete struct instances unless noted otherwise:
- If it is a function, it is called with the struct object as its `this`.
That method must not throw - if it does, the exception will be
ignored.
- If it is an array, it may contain functions, pointers, and/or JS
strings. If an entry is a function, it is called as described
above. If it's a number, it's assumed to be a pointer and is
passed to the `dealloc()` function configured for the parent
[StructBinder][]. If it's a JS string, it's assumed to be a
helpful description of the next entry in the list and is simply
ignored. Strings are supported primarily for use as debugging
information.
- If it is an array, it may contain functions, pointers, other
[StructType] instances, and/or JS strings. If an entry is a
function, it is called as described above. If it's a number, it's
assumed to be a pointer and is passed to the `dealloc()` function
configured for the parent [StructBinder][]. If it's a
[StructType][] instance then its `dispose()` method is called. If
it's a JS string, it's assumed to be a helpful description of the
next entry in the list and is simply ignored. Strings are
supported primarily for use as debugging information.
- Some struct APIs will manipulate the `ondispose` member, creating
it as an array or converting it from a function to array as
needed.
@ -738,21 +752,6 @@ pointer can be taken over using something like
These constructors have the following "static" members:
- `disposeAll()`
For each instance of this struct, the equivalent of its `dispose()`
method is called. This frees all WASM-allocated memory associated
with _all_ instances and clears the `instanceForPointer()`
mappings. Returns `this`.
- `instanceForPointer(pointer)`
Given a pointer value (accessible via the `pointer` property of all
struct instances) which ostensibly refers to an instance of this
class, this returns the instance associated with it, or `undefined`
if no object _of this specific struct type_ is mapped to that
pointer. When C-side code calls back into JS code and passes a
pointer to an object, this function can be used to type-safely
"cast" that pointer back to its original object.
- `isA(value)`
Returns true if its argument was created by this constructor.
@ -762,15 +761,6 @@ These constructors have the following "static" members:
- `memberKeys(string)`
Works exactly as documented for [StructType][].
- `resolveToInstance(value [,throwIfNot=false])`
Works like `instanceForPointer()` but accepts either an instance
of this struct type or a pointer which resolves to one.
It returns an instance of this struct type on success.
By default it returns a falsy value if its argument is not,
or does not resolve to, an instance of this struct type,
but if passed a truthy second argument then it will throw
instead.
- `structInfo`
The structure description passed to [StructBinder][] when this
constructor was generated.

View File

@ -75,7 +75,7 @@
</li>
<li>The easiest way to try different optimization levels is,
from this directory:
<pre>$ rm -f speedtest1.js; make -e emcc_opt='-O2' speedtest1.js</pre>
<pre>$ rm -f jswasm/speedtest1.js; make -e emcc_opt='-O2' speedtest1</pre>
Then reload this page. -O2 seems to consistently produce the fastest results.
</li>
</ul>

View File

@ -40,9 +40,9 @@
<script src="jswasm/speedtest1.js"></script>
<script>(function(){
/**
If this environment contains OPFS, this function initializes it and
returns the name of the dir on which OPFS is mounted, else it returns
an empty string.
If this environment contains WASMFS with OPFS, this function
initializes it and returns the name of the dir on which OPFS is
mounted, else it returns an empty string.
*/
const wasmfsDir = function f(wasmUtil){
if(undefined !== f._) return f._;

View File

@ -7,11 +7,7 @@
<link rel="stylesheet" href="../common/emscripten.css"/>
<link rel="stylesheet" href="../common/testing.css"/>
<title>sqlite3 tester #1: Worker thread</title>
<style>
body {
font-family: monospace;
}
</style>
<style></style>
</head>
<body>
<h1 id='color-target'>sqlite3 tester #1: Worker thread</h1>

View File

@ -13,14 +13,9 @@ ES6 Module in UI thread
UI thread
//#endif
</title>
<style>
body {
font-family: monospace;
}
</style>
<style></style>
</head>
<body>
<h1 id='color-target'></h1>
<body><h1 id='color-target'></h1>
<div>See <a href='tester1-worker.html' target='tester1-worker.html'>tester1-worker.html</a>
for the Worker-thread variant.</div>
<div class='input-wrapper'>

View File

@ -22,12 +22,14 @@
groups and individual tests can be assigned a predicate function
which determines whether to run them or not, and this is
specifically intended to be used to toggle certain tests on or off
for the main/worker threads.
for the main/worker threads or the availability (or not) of
optional features such as int64 support.
Each test group defines a state object which gets applied as each
test function's `this`. Test functions can use that to, e.g., set up
a db in an early test and close it in a later test. Each test gets
passed the sqlite3 namespace object as its only argument.
Each test group defines a single state object which gets applied as
the test functions' `this` for all tests in that group. Test
functions can use that to, e.g., set up a db in an early test and
close it in a later test. Each test gets passed the sqlite3
namespace object as its only argument.
*/
/*
This file is intended to be processed by c-pp to inject (or not)
@ -301,14 +303,12 @@ self.sqlite3InitModule = sqlite3InitModule;
addTest: function(name, callback){
let predicate;
if(1===arguments.length){
const opt = arguments[0];
predicate = opt.predicate;
name = opt.name;
callback = opt.test;
this.currentTestGroup.addTest(arguments[0]);
}else{
this.currentTestGroup.addTest({
name, predicate, test: callback
});
}
this.currentTestGroup.addTest({
name, predicate, test: callback
});
return this;
},
runTests: async function(sqlite3){
@ -399,12 +399,21 @@ self.sqlite3InitModule = sqlite3InitModule;
catch(e){T.assert("test ing ." === e.message)}
try{ throw new sqlite3.SQLite3Error(capi.SQLITE_SCHEMA) }
catch(e){ T.assert('SQLITE_SCHEMA' === e.message) }
catch(e){
T.assert('SQLITE_SCHEMA' === e.message)
.assert(capi.SQLITE_SCHEMA === e.resultCode);
}
try{ sqlite3.SQLite3Error.toss(capi.SQLITE_CORRUPT,{cause: true}) }
catch(e){
T.assert('SQLITE_CORRUPT'===e.message)
T.assert('SQLITE_CORRUPT' === e.message)
.assert(capi.SQLITE_CORRUPT === e.resultCode)
.assert(true===e.cause);
}
try{ sqlite3.SQLite3Error.toss("resultCode check") }
catch(e){
T.assert(capi.SQLITE_ERROR === e.resultCode)
.assert('resultCode check' === e.message);
}
})
////////////////////////////////////////////////////////////////////
.t('strglob/strlike', function(sqlite3){
@ -419,6 +428,7 @@ self.sqlite3InitModule = sqlite3InitModule;
////////////////////////////////////////////////////////////////////
T.g('C/WASM Utilities')
.t('sqlite3.wasm namespace', function(sqlite3){
// TODO: break this into smaller individual test functions.
const w = wasm;
const chr = (x)=>x.charCodeAt(0);
//log("heap getters...");
@ -546,13 +556,13 @@ self.sqlite3InitModule = sqlite3InitModule;
let cpy = w.scopedAlloc(n+10);
let rc = w.cstrncpy(cpy, cStr, n+10);
T.assert(n+1 === rc).
assert("hello" === w.cstringToJs(cpy)).
assert("hello" === w.cstrToJs(cpy)).
assert(chr('o') === w.getMemValue(cpy+n-1)).
assert(0 === w.getMemValue(cpy+n));
let cStr2 = w.scopedAllocCString("HI!!!");
rc = w.cstrncpy(cpy, cStr2, 3);
T.assert(3===rc).
assert("HI!lo" === w.cstringToJs(cpy)).
assert("HI!lo" === w.cstrToJs(cpy)).
assert(chr('!') === w.getMemValue(cpy+2)).
assert(chr('l') === w.getMemValue(cpy+3));
}finally{
@ -660,6 +670,26 @@ self.sqlite3InitModule = sqlite3InitModule;
T.assert(rc>0 && Number.isFinite(rc));
rc = w.xCallWrapped('sqlite3_wasm_enum_json','utf8');
T.assert('string'===typeof rc).assert(rc.length>300);
{ // 'string:static' argAdapter() sanity checks...
let argAd = w.xWrap.argAdapter('string:static');
let p0 = argAd('foo'), p1 = argAd('bar');
T.assert(w.isPtr(p0) && w.isPtr(p1))
.assert(p0 !== p1)
.assert(p0 === argAd('foo'))
.assert(p1 === argAd('bar'));
}
// 'string:flexible' argAdapter() sanity checks...
w.scopedAllocCall(()=>{
const argAd = w.xWrap.argAdapter('string:flexible');
const cj = (v)=>w.cstrToJs(argAd(v));
T.assert('Hi' === cj('Hi'))
.assert('hi' === cj(['h','i']))
.assert('HI' === cj(new Uint8Array([72, 73])));
});
if(haveWasmCTests()){
if(!sqlite3.config.useStdAlloc){
fw = w.xWrap('sqlite3_wasm_test_str_hello', 'utf8:dealloc',['i32']);
@ -689,7 +719,7 @@ self.sqlite3InitModule = sqlite3InitModule;
}/*WhWasmUtil*/)
////////////////////////////////////////////////////////////////////
.t('sqlite3.StructBinder (jaccwabyt)', function(sqlite3){
.t('sqlite3.StructBinder (jaccwabyt🐇)', function(sqlite3){
const S = sqlite3, W = S.wasm;
const MyStructDef = {
sizeof: 16,
@ -720,9 +750,6 @@ self.sqlite3InitModule = sqlite3InitModule;
assert(undefined === K.prototype.lookupMember('nope',false)).
assert(k1 instanceof StructType).
assert(StructType.isA(k1)).
assert(K.resolveToInstance(k1.pointer)===k1).
mustThrowMatching(()=>K.resolveToInstance(null,true), /is-not-a my_struct/).
assert(k1 === StructType.instanceForPointer(k1.pointer)).
mustThrowMatching(()=>k1.$ro = 1, /read-only/);
Object.keys(MyStructDef.members).forEach(function(key){
key = K.memberKey(key);
@ -732,8 +759,7 @@ self.sqlite3InitModule = sqlite3InitModule;
" from "+k1.memoryDump());
});
T.assert('number' === typeof k1.pointer).
mustThrowMatching(()=>k1.pointer = 1, /pointer/).
assert(K.instanceForPointer(k1.pointer) === k1);
mustThrowMatching(()=>k1.pointer = 1, /pointer/);
k1.$p4 = 1; k1.$pP = 2;
T.assert(1 === k1.$p4).assert(2 === k1.$pP);
if(MyStructDef.members.$p8){
@ -748,22 +774,13 @@ self.sqlite3InitModule = sqlite3InitModule;
assert('number' === typeof k1.$cstr).
assert('A C-string.' === k1.memberToJsString('cstr'));
k1.$pP = k2;
T.assert(k1.$pP === k2);
T.assert(k1.$pP === k2.pointer);
k1.$pP = null/*null is special-cased to 0.*/;
T.assert(0===k1.$pP);
let ptr = k1.pointer;
k1.dispose();
T.assert(undefined === k1.pointer).
assert(undefined === K.instanceForPointer(ptr)).
mustThrowMatching(()=>{k1.$pP=1}, /disposed instance/);
const k3 = new K();
ptr = k3.pointer;
T.assert(k3 === K.instanceForPointer(ptr));
K.disposeAll();
T.assert(ptr).
assert(undefined === k2.pointer).
assert(undefined === k3.pointer).
assert(undefined === K.instanceForPointer(ptr));
}finally{
k1.dispose();
k2.dispose();
@ -793,10 +810,8 @@ self.sqlite3InitModule = sqlite3InitModule;
assert(wts instanceof WTStruct).
assert(wts instanceof StructType).
assert(StructType.isA(wts)).
assert(wts === StructType.instanceForPointer(wts.pointer));
T.assert(wts.pointer>0).assert(0===wts.$v4).assert(0n===wts.$v8).
assert(0===wts.$ppV).assert(0===wts.$xFunc).
assert(WTStruct.instanceForPointer(wts.pointer) === wts);
assert(wts.pointer>0).assert(0===wts.$v4).assert(0n===wts.$v8).
assert(0===wts.$ppV).assert(0===wts.$xFunc);
const testFunc =
W.xGet('sqlite3_wasm_test_struct'/*name gets mangled in -O3 builds!*/);
let counter = 0;
@ -805,7 +820,6 @@ self.sqlite3InitModule = sqlite3InitModule;
/*log("This from a JS function called from C, "+
"which itself was called from JS. arg =",arg);*/
++counter;
T.assert(WTStruct.instanceForPointer(arg) === wts);
if(3===counter){
tossQuietly("Testing exception propagation.");
}
@ -829,7 +843,7 @@ self.sqlite3InitModule = sqlite3InitModule;
testFunc(wts.pointer);
//log("wts.pointer, wts.$ppV",wts.pointer, wts.$ppV);
T.assert(1===counter).assert(20 === wts.$v4).assert(40n === wts.$v8)
.assert(autoResolvePtr ? (wts.$ppV === wts) : (wts.$ppV === wts.pointer))
.assert(wts.$ppV === wts.pointer)
.assert('string' === typeof wts.memberToJsString('cstr'))
.assert(wts.memberToJsString('cstr') === wts.memberToJsString('$cstr'))
.mustThrowMatching(()=>wts.memberToJsString('xFunc'),
@ -837,279 +851,42 @@ self.sqlite3InitModule = sqlite3InitModule;
;
testFunc(wts.pointer);
T.assert(2===counter).assert(40 === wts.$v4).assert(80n === wts.$v8)
.assert(autoResolvePtr ? (wts.$ppV === wts) : (wts.$ppV === wts.pointer));
.assert(wts.$ppV === wts.pointer);
/** The 3rd call to wtsFunc throw from JS, which is called
from C, which is called from JS. Let's ensure that
that exception propagates back here... */
T.mustThrowMatching(()=>testFunc(wts.pointer),/^Testing/);
W.uninstallFunction(wts.$xFunc);
wts.$xFunc = 0;
if(autoResolvePtr){
wts.$ppV = 0;
T.assert(!wts.$ppV);
//WTStruct.debugFlags(0x03);
wts.$ppV = wts;
T.assert(wts === wts.$ppV)
//WTStruct.debugFlags(0);
}
wts.$ppV = 0;
T.assert(!wts.$ppV);
//WTStruct.debugFlags(0x03);
wts.$ppV = wts;
T.assert(wts.pointer === wts.$ppV)
wts.setMemberCString('cstr', "A C-string.");
T.assert(Array.isArray(wts.ondispose)).
assert(wts.ondispose[0] === wts.$cstr).
assert('A C-string.' === wts.memberToJsString('cstr'));
const ptr = wts.pointer;
wts.dispose();
T.assert(ptr).assert(undefined === wts.pointer).
assert(undefined === WTStruct.instanceForPointer(ptr))
T.assert(ptr).assert(undefined === wts.pointer);
}finally{
wts.dispose();
}
if(1){ // ondispose of other struct instances
const s1 = new WTStruct, s2 = new WTStruct, s3 = new WTStruct;
T.assert(s1.lookupMember instanceof Function)
.assert(s1.addOnDispose instanceof Function);
s1.addOnDispose(s2,"testing variadic args");
T.assert(2===s1.ondispose.length);
s2.addOnDispose(s3);
s1.dispose();
T.assert(!s2.pointer,"Expecting s2 to be ondispose'd by s1.");
T.assert(!s3.pointer,"Expecting s3 to be ondispose'd by s2.");
}
}/*StructBinder*/)
////////////////////////////////////////////////////////////////////
.t('sqlite3.StructBinder part 2', function(sqlite3){
// https://www.sqlite.org/c3ref/vfs.html
// https://www.sqlite.org/c3ref/io_methods.html
const sqlite3_io_methods = capi.sqlite3_io_methods,
sqlite3_vfs = capi.sqlite3_vfs,
sqlite3_file = capi.sqlite3_file;
//log("struct sqlite3_file", sqlite3_file.memberKeys());
//log("struct sqlite3_vfs", sqlite3_vfs.memberKeys());
//log("struct sqlite3_io_methods", sqlite3_io_methods.memberKeys());
const installMethod = function callee(tgt, name, func){
if(1===arguments.length){
return (n,f)=>callee(tgt,n,f);
}
if(!callee.argcProxy){
callee.argcProxy = function(func,sig){
return function(...args){
if(func.length!==arguments.length){
toss("Argument mismatch. Native signature is:",sig);
}
return func.apply(this, args);
}
};
callee.ondisposeRemoveFunc = function(){
if(this.__ondispose){
const who = this;
this.__ondispose.forEach(
(v)=>{
if('number'===typeof v){
try{wasm.uninstallFunction(v)}
catch(e){/*ignore*/}
}else{/*wasm function wrapper property*/
delete who[v];
}
}
);
delete this.__ondispose;
}
};
}/*static init*/
const sigN = tgt.memberSignature(name),
memKey = tgt.memberKey(name);
//log("installMethod",tgt, name, sigN);
if(!tgt.__ondispose){
T.assert(undefined === tgt.ondispose);
tgt.ondispose = [callee.ondisposeRemoveFunc];
tgt.__ondispose = [];
}
const fProxy = callee.argcProxy(func, sigN);
const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true));
tgt[memKey] = pFunc;
/**
ACHTUNG: function pointer IDs are from a different pool than
allocation IDs, starting at 1 and incrementing in steps of 1,
so if we set tgt[memKey] to those values, we'd very likely
later misinterpret them as plain old pointer addresses unless
unless we use some silly heuristic like "all values <5k are
presumably function pointers," or actually perform a function
lookup on every pointer to first see if it's a function. That
would likely work just fine, but would be kludgy.
It turns out that "all values less than X are functions" is
essentially how it works in wasm: a function pointer is
reported to the client as its index into the
__indirect_function_table.
So... once jaccwabyt can be told how to access the
function table, it could consider all pointer values less
than that table's size to be functions. As "real" pointer
values start much, much higher than the function table size,
that would likely work reasonably well. e.g. the object
pointer address for sqlite3's default VFS is (in this local
setup) 65104, whereas the function table has fewer than 600
entries.
*/
const wrapperKey = '$'+memKey;
tgt[wrapperKey] = fProxy;
tgt.__ondispose.push(pFunc, wrapperKey);
//log("tgt.__ondispose =",tgt.__ondispose);
return (n,f)=>callee(tgt, n, f);
}/*installMethod*/;
const installIOMethods = function instm(iom){
(iom instanceof capi.sqlite3_io_methods) || toss("Invalid argument type.");
if(!instm._requireFileArg){
instm._requireFileArg = function(arg,methodName){
arg = capi.sqlite3_file.resolveToInstance(arg);
if(!arg){
err("sqlite3_io_methods::xClose() was passed a non-sqlite3_file.");
}
return arg;
};
instm._methods = {
// https://sqlite.org/c3ref/io_methods.html
xClose: /*i(P)*/function(f){
/* int (*xClose)(sqlite3_file*) */
log("xClose(",f,")");
if(!(f = instm._requireFileArg(f,'xClose'))) return capi.SQLITE_MISUSE;
f.dispose(/*noting that f has externally-owned memory*/);
return 0;
},
xRead: /*i(Ppij)*/function(f,dest,n,offset){
/* int (*xRead)(sqlite3_file*, void*, int iAmt, sqlite3_int64 iOfst) */
log("xRead(",arguments,")");
if(!(f = instm._requireFileArg(f))) return capi.SQLITE_MISUSE;
wasm.heap8().fill(0, dest + offset, n);
return 0;
},
xWrite: /*i(Ppij)*/function(f,dest,n,offset){
/* int (*xWrite)(sqlite3_file*, const void*, int iAmt, sqlite3_int64 iOfst) */
log("xWrite(",arguments,")");
if(!(f=instm._requireFileArg(f,'xWrite'))) return capi.SQLITE_MISUSE;
return 0;
},
xTruncate: /*i(Pj)*/function(f){
/* int (*xTruncate)(sqlite3_file*, sqlite3_int64 size) */
log("xTruncate(",arguments,")");
if(!(f=instm._requireFileArg(f,'xTruncate'))) return capi.SQLITE_MISUSE;
return 0;
},
xSync: /*i(Pi)*/function(f){
/* int (*xSync)(sqlite3_file*, int flags) */
log("xSync(",arguments,")");
if(!(f=instm._requireFileArg(f,'xSync'))) return capi.SQLITE_MISUSE;
return 0;
},
xFileSize: /*i(Pp)*/function(f,pSz){
/* int (*xFileSize)(sqlite3_file*, sqlite3_int64 *pSize) */
log("xFileSize(",arguments,")");
if(!(f=instm._requireFileArg(f,'xFileSize'))) return capi.SQLITE_MISUSE;
wasm.setMemValue(pSz, 0/*file size*/);
return 0;
},
xLock: /*i(Pi)*/function(f){
/* int (*xLock)(sqlite3_file*, int) */
log("xLock(",arguments,")");
if(!(f=instm._requireFileArg(f,'xLock'))) return capi.SQLITE_MISUSE;
return 0;
},
xUnlock: /*i(Pi)*/function(f){
/* int (*xUnlock)(sqlite3_file*, int) */
log("xUnlock(",arguments,")");
if(!(f=instm._requireFileArg(f,'xUnlock'))) return capi.SQLITE_MISUSE;
return 0;
},
xCheckReservedLock: /*i(Pp)*/function(){
/* int (*xCheckReservedLock)(sqlite3_file*, int *pResOut) */
log("xCheckReservedLock(",arguments,")");
return 0;
},
xFileControl: /*i(Pip)*/function(){
/* int (*xFileControl)(sqlite3_file*, int op, void *pArg) */
log("xFileControl(",arguments,")");
return capi.SQLITE_NOTFOUND;
},
xSectorSize: /*i(P)*/function(){
/* int (*xSectorSize)(sqlite3_file*) */
log("xSectorSize(",arguments,")");
return 0/*???*/;
},
xDeviceCharacteristics:/*i(P)*/function(){
/* int (*xDeviceCharacteristics)(sqlite3_file*) */
log("xDeviceCharacteristics(",arguments,")");
return 0;
}
};
}/*static init*/
iom.$iVersion = 1;
Object.keys(instm._methods).forEach(
(k)=>installMethod(iom, k, instm._methods[k])
);
}/*installIOMethods()*/;
const iom = new sqlite3_io_methods, sfile = new sqlite3_file;
const err = console.error.bind(console);
try {
const IOM = sqlite3_io_methods, S3F = sqlite3_file;
//log("iom proto",iom,iom.constructor.prototype);
//log("sfile",sfile,sfile.constructor.prototype);
T.assert(0===sfile.$pMethods).assert(iom.pointer > 0);
//log("iom",iom);
sfile.$pMethods = iom.pointer;
T.assert(iom.pointer === sfile.$pMethods)
.assert(IOM.resolveToInstance(iom))
.assert(undefined ===IOM.resolveToInstance(sfile))
.mustThrow(()=>IOM.resolveToInstance(0,true))
.assert(S3F.resolveToInstance(sfile.pointer))
.assert(undefined===S3F.resolveToInstance(iom))
.assert(iom===IOM.resolveToInstance(sfile.$pMethods));
T.assert(0===iom.$iVersion);
installIOMethods(iom);
T.assert(1===iom.$iVersion);
//log("iom.__ondispose",iom.__ondispose);
T.assert(Array.isArray(iom.__ondispose)).assert(iom.__ondispose.length>10);
}finally{
iom.dispose();
T.assert(undefined === iom.__ondispose);
}
const dVfs = new sqlite3_vfs(capi.sqlite3_vfs_find(null));
try {
const SB = sqlite3.StructBinder;
T.assert(dVfs instanceof SB.StructType)
.assert(dVfs.pointer)
.assert('sqlite3_vfs' === dVfs.structName)
.assert(!!dVfs.structInfo)
.assert(SB.StructType.hasExternalPointer(dVfs))
.assert(dVfs.$iVersion>0)
.assert('number'===typeof dVfs.$zName)
.assert('number'===typeof dVfs.$xSleep)
.assert(wasm.functionEntry(dVfs.$xOpen))
.assert(dVfs.memberIsString('zName'))
.assert(dVfs.memberIsString('$zName'))
.assert(!dVfs.memberIsString('pAppData'))
.mustThrowMatching(()=>dVfs.memberToJsString('xSleep'),
/Invalid member type signature for C-string/)
.mustThrowMatching(()=>dVfs.memberSignature('nope'), /nope is not a mapped/)
.assert('string' === typeof dVfs.memberToJsString('zName'))
.assert(dVfs.memberToJsString('zName')===dVfs.memberToJsString('$zName'))
;
//log("Default VFS: @",dVfs.pointer);
Object.keys(sqlite3_vfs.structInfo.members).forEach(function(mname){
const mk = sqlite3_vfs.memberKey(mname), mbr = sqlite3_vfs.structInfo.members[mname],
addr = dVfs[mk], prefix = 'defaultVfs.'+mname;
if(1===mbr.signature.length){
let sep = '?', val = undefined;
switch(mbr.signature[0]){
// TODO: move this into an accessor, e.g. getPreferredValue(member)
case 'i': case 'j': case 'f': case 'd': sep = '='; val = dVfs[mk]; break
case 'p': case 'P': sep = '@'; val = dVfs[mk]; break;
case 's': sep = '=';
val = dVfs.memberToJsString(mname);
break;
}
//log(prefix, sep, val);
}else{
//log(prefix," = funcptr @",addr, wasm.functionEntry(addr));
}
});
}finally{
dVfs.dispose();
T.assert(undefined===dVfs.pointer);
}
}/*StructBinder part 2*/)
////////////////////////////////////////////////////////////////////
.t('sqlite3.wasm.pstack', function(sqlite3){
const P = wasm.pstack;
@ -1219,7 +996,40 @@ self.sqlite3InitModule = sqlite3InitModule;
.t('Create db', function(sqlite3){
const dbFile = '/tester1.db';
wasm.sqlite3_wasm_vfs_unlink(0, dbFile);
const db = this.db = new sqlite3.oo1.DB(dbFile);
const db = this.db = new sqlite3.oo1.DB(dbFile, 0 ? 'ct' : 'c');
db.onclose = {
disposeAfter: [],
disposeBefore: [],
before: function(db){
while(this.disposeBefore.length){
const v = this.disposeBefore.shift();
console.debug("db.onclose.before cleaning up:",v);
if(wasm.isPtr(v)) wasm.dealloc(v);
else if(v instanceof sqlite3.StructBinder.StructType){
v.dispose();
}else if(v instanceof Function){
try{ v(db) } catch(e){
console.warn("beforeDispose() callback threw:",e);
}
}
}
console.debug("db.onclose.before dropping modules");
sqlite3.capi.sqlite3_drop_modules(db.pointer, 0);
},
after: function(){
while(this.disposeAfter.length){
const v = this.disposeAfter.shift();
console.debug("db.onclose.after cleaning up:",v);
if(wasm.isPtr(v)) wasm.dealloc(v);
else if(v instanceof sqlite3.StructBinder.StructType){
v.dispose();
}else if(v instanceof Function){
try{v()} catch(e){/*ignored*/}
}
}
}
};
T.assert(Number.isInteger(db.pointer))
.mustThrowMatching(()=>db.pointer=1, /read-only/)
.assert(0===sqlite3.capi.sqlite3_extended_result_codes(db.pointer,1))
@ -1442,6 +1252,29 @@ self.sqlite3InitModule = sqlite3InitModule;
rc = db.selectArray('select a, b from t where b=-1');
T.assert(undefined === rc);
})
////////////////////////////////////////////////////////////////////////
.t('selectArrays/Objects()', function(sqlite3){
const db = this.db;
const sql = 'select a, b from t where a=? or b=? order by a';
let rc = db.selectArrays(sql, [1, 4]);
T.assert(Array.isArray(rc))
.assert(2===rc.length)
.assert(2===rc[0].length)
.assert(1===rc[0][0])
.assert(2===rc[0][1])
.assert(3===rc[1][0])
.assert(4===rc[1][1])
rc = db.selectArrays(sql, [99,99]);
T.assert(Array.isArray(rc)).assert(0===rc.length);
rc = db.selectObjects(sql, [1,4]);
T.assert(Array.isArray(rc))
.assert(2===rc.length)
.assert('object' === typeof rc[1])
.assert(1===rc[0].a)
.assert(2===rc[0].b)
.assert(3===rc[1].a)
.assert(4===rc[1].b);
})
////////////////////////////////////////////////////////////////////////
.t({
@ -1750,8 +1583,8 @@ self.sqlite3InitModule = sqlite3InitModule;
////////////////////////////////////////////////////////////////////
.t({
name: 'C-side WASM tests (if compiled in)',
predicate: haveWasmCTests,
name: 'C-side WASM tests',
predicate: ()=>(haveWasmCTests() || "Not compiled in."),
test: function(){
const w = wasm, db = this.db;
const stack = w.scopedAllocPush();
@ -1834,9 +1667,398 @@ self.sqlite3InitModule = sqlite3InitModule;
}
}/* jaccwabyt-specific tests */)
////////////////////////////////////////////////////////////////////////
.t({
name: 'virtual table #1',
predicate: ()=>!!capi.sqlite3_index_info,
test: function(sqlite3){
warn("The vtab/module JS bindings are experimental and subject to change.");
const vth = sqlite3.VtabHelper;
const tmplCols = Object.assign(Object.create(null),{
A: 0, B: 1
});
/**
The vtab demonstrated here is a JS-ification of
ext/misc/templatevtab.c.
*/
const tmplMethods = {
xConnect: function(pDb, pAux, argc, argv, ppVtab, pzErr){
try{
const args = wasm.cArgvToJs(argc, argv);
T.assert(args.length>=3)
.assert(args[0] === 'testvtab')
.assert(args[1] === 'main')
.assert(args[2] === 'testvtab');
console.debug("xConnect() args =",args);
const rc = capi.sqlite3_declare_vtab(
pDb, "CREATE TABLE ignored(a,b)"
);
if(0===rc){
const t = vth.xVtab();
wasm.setPtrValue(ppVtab, t.pointer);
T.assert(t === vth.xVtab(wasm.getPtrValue(ppVtab)));
}
return rc;
}catch(e){
if(!(e instanceof sqlite3.WasmAllocError)){
wasm.dealloc(wasm.getPtrValue, pzErr);
wasm.setPtrValue(pzErr, wasm.allocCString(e.message));
}
return vth.xError('xConnect',e);
}
},
xDisconnect: function(pVtab){
try {
const t = vth.xVtab(pVtab, true);
t.dispose();
return 0;
}catch(e){
return vth.xError('xDisconnect',e);
}
},
xOpen: function(pVtab, ppCursor){
try{
const t = vth.xVtab(pVtab), c = vth.xCursor();
T.assert(t instanceof capi.sqlite3_vtab)
.assert(c instanceof capi.sqlite3_vtab_cursor);
wasm.setPtrValue(ppCursor, c.pointer);
c._rowId = 0;
return 0;
}catch(e){
return vth.xError('xOpen',e);
}
},
xClose: function(pCursor){
try{
const c = vth.xCursor(pCursor,true);
T.assert(c instanceof capi.sqlite3_vtab_cursor)
.assert(!vth.xCursor(pCursor));
c.dispose();
return 0;
}catch(e){
return vth.xError('xClose',e);
}
},
xNext: function(pCursor){
try{
const c = vth.xCursor(pCursor);
++c._rowId;
return 0;
}catch(e){
return vth.xError('xNext',e);
}
},
xColumn: function(pCursor, pCtx, iCol){
try{
const c = vth.xCursor(pCursor);
switch(iCol){
case tmplCols.A:
capi.sqlite3_result_int(pCtx, 1000 + c._rowId);
break;
case tmplCols.B:
capi.sqlite3_result_int(pCtx, 2000 + c._rowId);
break;
default: sqlite3.SQLite3Error.toss("Invalid column id",iCol);
}
return 0;
}catch(e){
return vth.xError('xColumn',e);
}
},
xRowid: function(pCursor, ppRowid64){
try{
const c = vth.xCursor(pCursor);
vth.xRowid(ppRowid64, c._rowId);
return 0;
}catch(e){
return vth.xError('xRowid',e);
}
},
xEof: function(pCursor){
const c = vth.xCursor(pCursor),
rc = c._rowId>=10;
c.dispose();
return rc;
},
xFilter: function(pCursor, idxNum, idxCStr,
argc, argv/* [sqlite3_value* ...] */){
try{
const c = vth.xCursor(pCursor);
c._rowId = 0;
const list = vth.sqlite3ValuesToJs(argc, argv);
T.assert(argc === list.length);
//log(argc,"xFilter value(s):",list);
c.dispose();
return 0;
}catch(e){
return vth.xError('xFilter',e);
}
},
xBestIndex: function(pVtab, pIdxInfo){
try{
//const t = vth.xVtab(pVtab);
const sii = capi.sqlite3_index_info;
const pii = new sii(pIdxInfo);
pii.$estimatedRows = 10;
pii.$estimatedCost = 10.0;
//log("xBestIndex $nConstraint =",pii.$nConstraint);
if(pii.$nConstraint>0){
// Validate nthConstraint() and nthConstraintUsage()
const max = pii.$nConstraint;
for(let i=0; i < max; ++i ){
let v = pii.nthConstraint(i,true);
T.assert(wasm.isPtr(v));
v = pii.nthConstraint(i);
T.assert(v instanceof sii.sqlite3_index_constraint)
.assert(v.pointer >= pii.$aConstraint);
v.dispose();
v = pii.nthConstraintUsage(i,true);
T.assert(wasm.isPtr(v));
v = pii.nthConstraintUsage(i);
T.assert(v instanceof sii.sqlite3_index_constraint_usage)
.assert(v.pointer >= pii.$aConstraintUsage);
v.$argvIndex = i;//just to get some values into xFilter
v.dispose();
}
}
//log("xBestIndex $nOrderBy =",pii.$nOrderBy);
if(pii.$nOrderBy>0){
// Validate nthOrderBy()
const max = pii.$nOrderBy;
for(let i=0; i < max; ++i ){
let v = pii.nthOrderBy(i,true);
T.assert(wasm.isPtr(v));
v = pii.nthOrderBy(i);
T.assert(v instanceof sii.sqlite3_index_orderby)
.assert(v.pointer >= pii.$aOrderBy);
v.dispose();
}
}
pii.dispose();
return 0;
}catch(e){
return vth.xError('xBestIndex',e);
}
}
};
/**
The vtab API places relevance on whether xCreate and
xConnect are exactly the same function (same pointer
address). Two JS-side references to the same method will
end up, without acrobatics to counter it, being compiled as
two different WASM-side bindings, i.e. two different
pointers.
In order to account for this, VtabHelper.installMethods()
checks for duplicate function entries and maps them to the
same WASM-compiled instance.
*/
if(1){
tmplMethods.xCreate = tmplMethods.xConnect;
}
const tmplMod = new sqlite3.capi.sqlite3_module();
tmplMod.$iVersion = 0;
this.db.onclose.disposeAfter.push(tmplMod);
vth.installMethods(tmplMod, tmplMethods, true);
if(tmplMethods.xCreate){
T.assert(tmplMod.$xCreate)
.assert(tmplMod.$xCreate === tmplMod.$xConnect,
"installMethods() must avoid re-compiling identical functions");
tmplMod.$xCreate = 0;
}
let rc = capi.sqlite3_create_module(
this.db, "testvtab", tmplMod, 0
);
this.db.checkRc(rc);
const list = this.db.selectArrays(
"SELECT a,b FROM testvtab where a<9999 and b>1 order by a, b"
/* Query is shaped so that it will ensure that some constraints
end up in xBestIndex(). */
);
T.assert(10===list.length)
.assert(1000===list[0][0])
.assert(2009===list[list.length-1][1]);
}
})/*custom vtab #1*/
////////////////////////////////////////////////////////////////////////
.t({
name: 'virtual table #2 (non-eponymous w/ automated exception wrapping)',
predicate: ()=>!!capi.sqlite3_index_info,
test: function(sqlite3){
warn("The vtab/module JS bindings are experimental and subject to change.");
const vth = sqlite3.VtabHelper;
const tmplCols = Object.assign(Object.create(null),{
A: 0, B: 1
});
/**
The vtab demonstrated here is a JS-ification of
ext/misc/templatevtab.c.
*/
let throwOnCreate = 1 ? 0 : capi.SQLITE_CANTOPEN
/* ^^^ just for testing exception wrapping. Note that sqlite
always translates errors from a vtable to a generic
SQLITE_ERROR unless it's from xConnect()/xCreate() and that
callback sets an error string. */;
const vtabTrace = 1
? ()=>{}
: (methodName,...args)=>console.debug('sqlite3_module::'+methodName+'():',...args);
const modConfig = {
/* catchExceptions changes how the methods are wrapped */
catchExceptions: true,
name: "vtab2test",
methods:{
xCreate: function(pDb, pAux, argc, argv, ppVtab, pzErr){
vtabTrace("xCreate",...arguments);
if(throwOnCreate){
sqlite3.SQLite3Error.toss(
throwOnCreate,
"Throwing a test exception."
);
}
const args = wasm.cArgvToJs(argc, argv);
vtabTrace("xCreate","argv:",args);
T.assert(args.length>=3);
const rc = capi.sqlite3_declare_vtab(
pDb, "CREATE TABLE ignored(a,b)"
);
if(0===rc){
const t = vth.xVtab();
wasm.setPtrValue(ppVtab, t.pointer);
T.assert(t === vth.xVtab(wasm.getPtrValue(ppVtab)));
vtabTrace("xCreate",...arguments," ppVtab =",t.pointer);
}
return rc;
},
xDestroy: function(pVtab){
vtabTrace("sqlite3_xDestroy",pVtab);
const t = vth.xVtab(pVtab, true);
T.assert(t);
t.dispose();
},
/*xConnect: function(pDb, pAux, argc, argv, ppVtab, pzErr){
vtabTrace("xConnect",...arguments);
const t = vth.xVtab();
wasm.setPtrValue(ppVtab, t.pointer);
T.assert(t === vth.xVtab(wasm.getPtrValue(ppVtab)));
vtabTrace("xConnect",...arguments,"ppVtab =",t.pointer);
},
xDisconnect: function(pVtab){
vtabTrace("xDisconnect",pVtab);
const t = vth.xVtab(pVtab, true);
T.assert(t);
t.dispose();
},*/
xOpen: function(pVtab, ppCursor){
const t = vth.xVtab(pVtab), c = vth.xCursor();
T.assert(t instanceof capi.sqlite3_vtab)
.assert(c instanceof capi.sqlite3_vtab_cursor);
wasm.setPtrValue(ppCursor, c.pointer);
vtabTrace("xOpen",...arguments," cursor =",c.pointer);
c._rowId = 0;
},
xClose: function(pCursor){
vtabTrace("xClose",...arguments);
const c = vth.xCursor(pCursor,true);
T.assert(c instanceof capi.sqlite3_vtab_cursor)
.assert(!vth.xCursor(pCursor));
c.dispose();
},
xNext: function(pCursor){
vtabTrace("xNext",...arguments);
const c = vth.xCursor(pCursor);
++c._rowId;
},
xColumn: function(pCursor, pCtx, iCol){
vtabTrace("xColumn",...arguments);
const c = vth.xCursor(pCursor);
switch(iCol){
case tmplCols.A:
capi.sqlite3_result_int(pCtx, 1000 + c._rowId);
break;
case tmplCols.B:
capi.sqlite3_result_int(pCtx, 2000 + c._rowId);
break;
default: sqlite3.SQLite3Error.toss("Invalid column id",iCol);
}
},
xRowid: function(pCursor, ppRowid64){
vtabTrace("xRowid",...arguments);
const c = vth.xCursor(pCursor);
vth.xRowid(ppRowid64, c._rowId);
},
xEof: function(pCursor){
vtabTrace("xEof",...arguments);
return vth.xCursor(pCursor)._rowId>=10;
},
xFilter: function(pCursor, idxNum, idxCStr,
argc, argv/* [sqlite3_value* ...] */){
vtabTrace("xFilter",...arguments);
const c = vth.xCursor(pCursor);
c._rowId = 0;
const list = vth.sqlite3ValuesToJs(argc, argv);
T.assert(argc === list.length);
},
xBestIndex: function(pVtab, pIdxInfo){
vtabTrace("xBestIndex",...arguments);
//const t = vth.xVtab(pVtab);
const pii = vth.xIndexInfo(pIdxInfo);
pii.$estimatedRows = 10;
pii.$estimatedCost = 10.0;
pii.dispose();
}
}/*methods*/
};
const tmplMod = vth.setupModule(modConfig);
T.assert(tmplMod instanceof capi.sqlite3_module)
.assert(1===tmplMod.$iVersion);
this.db.onclose.disposeAfter.push(tmplMod);
this.db.checkRc(capi.sqlite3_create_module(
this.db.pointer, modConfig.name, tmplMod.pointer, 0
));
this.db.exec([
"create virtual table testvtab2 using ",
modConfig.name,
"(arg1 blah, arg2 bloop)"
]);
this.db.onclose.disposeBefore.push(function(db){
console.debug("testvtab2 disposeBefore handler...");
db.exec(
/**
DROP TABLE is the only way to get xDestroy() to be called.
If we DROP TABLE at the end of the containing
test function, xDestroy() is called. If we instead
delay it until db.onclose.before(), we're getting
"no such table"?
*/
"DROP TABLE testvtab2"
);
});
let list = this.db.selectArrays(
"SELECT a,b FROM testvtab2 where a<9999 and b>1 order by a, b"
/* Query is shaped so that it will ensure that some
constraints end up in xBestIndex(). */
);
T.assert(10===list.length)
.assert(1000===list[0][0])
.assert(2009===list[list.length-1][1]);
list = this.db.selectArrays(
"SELECT a,b FROM testvtab2 where a<9999 and b>1 order by b, a limit 5"
);
T.assert(5===list.length)
.assert(1000===list[0][0])
.assert(2004===list[list.length-1][1]);
}
})/*custom vtab #2*/
////////////////////////////////////////////////////////////////////////
.t('Close db', function(){
T.assert(this.db).assert(Number.isInteger(this.db.pointer));
wasm.exports.sqlite3_wasm_db_reset(this.db.pointer);
T.assert(this.db).assert(wasm.isPtr(this.db.pointer));
//wasm.sqlite3_wasm_db_reset(this.db); // will leak virtual tables!
this.db.close();
T.assert(!this.db.pointer);
})
@ -1856,7 +2078,8 @@ self.sqlite3InitModule = sqlite3InitModule;
})
.t({
name: 'kvvfs in main thread',
predicate: ()=>(isUIThread() ? true : "No local/sessionStorage in Worker"),
predicate: ()=>(isUIThread()
|| "local/sessionStorage are unavailable in a Worker"),
test: function(sqlite3){
const filename = this.kvvfsDbFile = 'session';
const pVfs = capi.sqlite3_vfs_find('kvvfs');
@ -1901,7 +2124,7 @@ self.sqlite3InitModule = sqlite3InitModule;
delete this.kvvfsUnlink;
delete this.JDb;
}
}/*kvvfs sqlite3_js_vfs_create_file()*/)
}/*kvvfs sqlite3_js_vfs_create_file()*/)
;/* end kvvfs tests */
////////////////////////////////////////////////////////////////////////