mirror of
https://github.com/sqlite/sqlite.git
synced 2025-07-27 20:41:58 +03:00
Add a demonstration sqlite3_vtab/module implemented in JS, based on ext/misc/templatevtab.c. Add oo1.selectArrays() and selectObjects().
FossilOrigin-Name: 60482c97e02bc4cafefef281be0cf0bc8c5c53232162829c137f3f7a80cdc534
This commit is contained in:
392
ext/wasm/api/sqlite3-v-helper.js
Normal file
392
ext/wasm/api/sqlite3-v-helper.js
Normal file
@ -0,0 +1,392 @@
|
||||
/*
|
||||
** 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.toss;
|
||||
const vh = Object.create(null), vt = Object.create(null);
|
||||
|
||||
sqlite3.VfsHelper = vh;
|
||||
sqlite3.VtabHelper = vt;
|
||||
|
||||
/**
|
||||
Installs a StructBinder-bound function pointer member of the
|
||||
given name and function in the given StructType target object.
|
||||
It creates a WASM proxy for the given function and arranges for
|
||||
that proxy to be cleaned up when tgt.dispose() is called. Throws
|
||||
on the slightest hint of error, e.g. tgt is-not-a StructType,
|
||||
name does not map to a struct-bound member, etc.
|
||||
|
||||
Returns a proxy for this function which is bound to tgt and takes
|
||||
2 args (name,func). That function returns the same thing,
|
||||
permitting calls to be chained.
|
||||
|
||||
If called with only 1 arg, it has no side effects but returns a
|
||||
func with the same signature as described above.
|
||||
|
||||
If tgt.ondispose is set before this is called then it _must_
|
||||
be an array, to which this function will append entries.
|
||||
|
||||
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 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 intended for dev-time usage for sanity
|
||||
checking, and will leave the C environment in an undefined
|
||||
state. For non-dev-time use, it is a given that the C API will
|
||||
never call one of the generated function wrappers with the wrong
|
||||
argument count.
|
||||
*/
|
||||
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.");
|
||||
}
|
||||
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," 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(tgt, memKey, 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 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.$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;
|
||||
};
|
||||
|
||||
/**
|
||||
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 xyz2js() impls.
|
||||
*/
|
||||
const __v2jsFactory = function(structType){
|
||||
return function(ptr,remove=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 v2jsFactory");
|
||||
}
|
||||
let rc = this.get(ptr);
|
||||
if(remove) this.delete(ptr);
|
||||
/*arguable else if(!rc){
|
||||
rc = new structType(ptr);
|
||||
this.set(ptr, rc);
|
||||
}*/
|
||||
return rc;
|
||||
}.bind(new Map);
|
||||
};
|
||||
/**
|
||||
EXPERIMENTAL. DO NOT USE IN CLIENT CODE.
|
||||
|
||||
Has 3 distinct uses:
|
||||
|
||||
- vtab2js() 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.
|
||||
|
||||
- vtab2js(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 xDisconnect(), in which case use...
|
||||
|
||||
- vtab2js(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.vtab2js = __v2jsFactory(capi.sqlite3_vtab);
|
||||
|
||||
/**
|
||||
EXPERIMENTAL. DO NOT USE IN CLIENT CODE.
|
||||
|
||||
Works identically to vtab2js() except that it deals with
|
||||
sqlite3_cursor objects and pointers instead of sqlite3_vtab.
|
||||
|
||||
- vcur2js() is intended to be called from sqlite3_module::xOpen()
|
||||
|
||||
- vcur2js(pCursor) is intended to be called from all sqlite3_module
|
||||
methods which take a (sqlite3_vtab_cursor*) _except_ for
|
||||
xClose(), in which case use...
|
||||
|
||||
- vcur2js(pCursor, true) will remove the m apping 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 form xClose() or in error handling of a
|
||||
failed xOpen().
|
||||
*/
|
||||
vt.vcur2js = __v2jsFactory(capi.sqlite3_vtab_cursor);
|
||||
|
||||
/**
|
||||
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.xMethodError(
|
||||
'xColumn', e, sqlite3.capi.SQLITE_XYZ);
|
||||
// where SQLITE_XYZ is some call-appropriate result code
|
||||
// defaulting to SQLITE_ERROR.
|
||||
}
|
||||
```
|
||||
|
||||
If xMethodError.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.xMethodError = function f(methodName, err, defaultRc=capi.SQLITE_ERROR){
|
||||
if(f.errorReporter instanceof Function){
|
||||
try{f.errorReporter("sqlite3_module::"+methodName+"(): "+err.message);}
|
||||
catch(e){/*ignored*/}
|
||||
}
|
||||
return (err instanceof sqlite3.WasmAllocError)
|
||||
? capi.SQLITE_NOMEM
|
||||
: defaultRc;
|
||||
};
|
||||
vt.xMethodError.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.xMethodError(methodName, e, defaultRc) }
|
||||
};
|
||||
*/
|
||||
|
||||
/**
|
||||
A helper for sqlite3_vtab::xRow() 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.setRowId = (ppRowid64, value)=>wasm.setMemValue(ppRowid64, value, 'i64');
|
||||
}/*sqlite3ApiBootstrap.initializers.push()*/);
|
Reference in New Issue
Block a user