From: stephan Date: Tue, 6 Dec 2022 06:09:03 +0000 (+0000) Subject: Add a demonstration sqlite3_vtab/module implemented in JS, based on ext/misc/template... X-Git-Tag: version-3.41.0~291^2~12 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=6b271abc9853e77a19bb208893c6e2a3924e9f50;p=thirdparty%2Fsqlite.git Add a demonstration sqlite3_vtab/module implemented in JS, based on ext/misc/templatevtab.c. Add oo1.selectArrays() and selectObjects(). FossilOrigin-Name: 60482c97e02bc4cafefef281be0cf0bc8c5c53232162829c137f3f7a80cdc534 --- diff --git a/ext/wasm/GNUmakefile b/ext/wasm/GNUmakefile index 968d4f440c..1b18437682 100644 --- a/ext/wasm/GNUmakefile +++ b/ext/wasm/GNUmakefile @@ -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 diff --git a/ext/wasm/api/README.md b/ext/wasm/api/README.md index f9955a8957..ce34b26b8a 100644 --- a/ext/wasm/api/README.md +++ b/ext/wasm/api/README.md @@ -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 diff --git a/ext/wasm/api/sqlite3-api-oo1.js b/ext/wasm/api/sqlite3-api-oo1.js index b377efc242..6253c659fc 100644 --- a/ext/wasm/api/sqlite3-api-oo1.js +++ b/ext/wasm/api/sqlite3-api-oo1.js @@ -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 @@ -1098,6 +1107,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. diff --git a/ext/wasm/api/sqlite3-api-prologue.js b/ext/wasm/api/sqlite3-api-prologue.js index a51e957f8f..558fcda1e0 100644 --- a/ext/wasm/api/sqlite3-api-prologue.js +++ b/ext/wasm/api/sqlite3-api-prologue.js @@ -1642,7 +1642,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; diff --git a/ext/wasm/api/sqlite3-v-helper.js b/ext/wasm/api/sqlite3-v-helper.js new file mode 100644 index 0000000000..84272266e8 --- /dev/null +++ b/ext/wasm/api/sqlite3-v-helper.js @@ -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 */; + 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()*/); diff --git a/ext/wasm/api/sqlite3-vfs-helper.js b/ext/wasm/api/sqlite3-vfs-helper.js deleted file mode 100644 index f9d3c18c7b..0000000000 --- a/ext/wasm/api/sqlite3-vfs-helper.js +++ /dev/null @@ -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()*/); diff --git a/ext/wasm/jaccwabyt/jaccwabyt.js b/ext/wasm/jaccwabyt/jaccwabyt.js index 0bd7869bc6..1fa6baf154 100644 --- a/ext/wasm/jaccwabyt/jaccwabyt.js +++ b/ext/wasm/jaccwabyt/jaccwabyt.js @@ -231,6 +231,7 @@ self.Jaccwabyt = function StructBinderFactory(config){ obj.ondispose.forEach(function(x){ 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. diff --git a/ext/wasm/jaccwabyt/jaccwabyt.md b/ext/wasm/jaccwabyt/jaccwabyt.md index c6cf5c8a25..17ada1edf0 100644 --- a/ext/wasm/jaccwabyt/jaccwabyt.md +++ b/ext/wasm/jaccwabyt/jaccwabyt.md @@ -426,7 +426,8 @@ 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. +// 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 diff --git a/ext/wasm/tester1.c-pp.js b/ext/wasm/tester1.c-pp.js index 5db00cee64..6fcc895329 100644 --- a/ext/wasm/tester1.c-pp.js +++ b/ext/wasm/tester1.c-pp.js @@ -419,6 +419,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..."); @@ -974,7 +975,7 @@ 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'); T.assert(Number.isInteger(db.pointer)) .mustThrowMatching(()=>db.pointer=1, /read-only/) .assert(0===sqlite3.capi.sqlite3_extended_result_codes(db.pointer,1)) @@ -1197,6 +1198,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({ @@ -1503,6 +1527,173 @@ self.sqlite3InitModule = sqlite3InitModule; T.mustThrow(()=>db.exec("select * from foo.bar")); }) + //////////////////////////////////////////////////////////////////////// + .t({ + name: 'Custom virtual tables', + predicate: ()=>wasm.bigIntEnabled, + 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 rc = capi.sqlite3_declare_vtab( + pDb, "CREATE TABLE ignored(a,b)" + ); + if(0===rc){ + const t = vth.vtab2js(); + wasm.setPtrValue(ppVtab, t.pointer); + T.assert(t === vth.vtab2js(wasm.getPtrValue(ppVtab))); + } + return rc; + }catch(e){ + if(!(e instanceof sqlite3.WasmAllocError)){ + wasm.setPtrValue(pzErr, wasm.allocCString(e.message)); + } + return vth.xMethodError('xConnect',e); + } + }, + xDisconnect: function(pVtab){ + try { + const t = vth.vtab2js(pVtab, true); + t.dispose(); + return 0; + }catch(e){ + return vth.xMethodError('xDisconnect',e); + } + }, + xOpen: function(pVtab, ppCursor){ + try{ + const t = vth.vtab2js(pVtab), c = vth.vcur2js(); + T.assert(t instanceof capi.sqlite3_vtab); + T.assert(c instanceof capi.sqlite3_vtab_cursor); + wasm.setPtrValue(ppCursor, c.pointer); + c._rowId = 0; + return 0; + }catch(e){ + return vth.xMethodError('xOpen',e); + } + }, + xClose: function(pCursor){ + try{ + const c = vth.vcur2js(pCursor,true); + T.assert(c instanceof capi.sqlite3_vtab_cursor) + .assert(!vth.vcur2js(pCursor)); + c.dispose(); + return 0; + }catch(e){ + return vth.xMethodError('xClose',e); + } + }, + xNext: function(pCursor){ + try{ + const c = vth.vcur2js(pCursor); + ++c._rowId; + return 0; + }catch(e){ + return vth.xMethodError('xNext',e); + } + + }, + xColumn: function(pCursor, pCtx, iCol){ + try{ + const c = vth.vcur2js(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.xMethodError('xColumn',e); + } + }, + xRowid: function(pCursor, ppRowid64){ + try{ + const c = vth.vcur2js(pCursor); + vth.setRowId(ppRowid64, c._rowId); + return 0; + }catch(e){ + return vth.xMethodError('xRowid',e); + } + }, + xEof: function(pCursor){ + const c = vth.vcur2js(pCursor); + return c._rowId>=10; + }, + xFilter: function(pCursor, idxNum, idxCStr, + argc, argv/* [sqlite3_value* ...] */){ + try{ + const c = vth.vcur2js(pCursor); + c._rowId = 0; + return 0; + }catch(e){ + return vth.xMethodError('xFilter',e); + } + }, + xBestIndex: function(pVtab, pIdxInfo){ + try{ + const t = vth.vtab2js(pVtab); + const pii = new capi.sqlite3_index_info(pIdxInfo); + pii.$estimatedRows = 10; + pii.$estimatedCost = 10.0; + pii.dispose(); + return 0; + }catch(e){ + return vth.xMethodError('xBestIndex',e); + } + } + }; + /** + Problem to resolve: 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.ondispose = []; + tmplMod.$iVersion = 0; + vth.installMethods(tmplMod, tmplMethods, true); + if(tmplMethods.xCreate){ + T.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 order by a" + ); + T.assert(10===list.length) + .assert(1000===list[0][0]) + .assert(2009===list[list.length-1][1]) + } + })/*vtab sanity checks*/ + //////////////////////////////////////////////////////////////////// .t({ name: 'C-side WASM tests (if compiled in)', @@ -1590,8 +1781,8 @@ self.sqlite3InitModule = sqlite3InitModule; }/* jaccwabyt-specific tests */) .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); this.db.close(); T.assert(!this.db.pointer); }) diff --git a/manifest b/manifest index a3f0d928c3..3a917c3196 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Remove\stwo\sfeatures\sof\sjaccwabyt\swhich\swere\sfundamentally\sflawed,\salong\swith\sapprox.\s250\slines\sof\sunit\stests\swhich\sheavily\srelied\son\sthem.\sThankfully,\snone\sof\sthe\ssqlite3.js-level\scode\sused\sthose\sbits. -D 2022-12-05T15:05:46.306 +C Add\sa\sdemonstration\ssqlite3_vtab/module\simplemented\sin\sJS,\sbased\son\sext/misc/templatevtab.c.\sAdd\soo1.selectArrays()\sand\sselectObjects(). +D 2022-12-06T06:09:03.466 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -491,12 +491,12 @@ F ext/userauth/sqlite3userauth.h 7f3ea8c4686db8e40b0a0e7a8e0b00fac13aa7a3 F ext/userauth/user-auth.txt e6641021a9210364665fe625d067617d03f27b04 F ext/userauth/userauth.c 7f00cded7dcaa5d47f54539b290a43d2e59f4b1eb5f447545fa865f002fc80cb F ext/wasm/EXPORTED_FUNCTIONS.fiddle.in 27450c8b8c70875a260aca55435ec927068b34cef801a96205adb81bdcefc65c -F ext/wasm/GNUmakefile bfa47f169468ca9db031105b0e336db29a88e93c3abd217d0bbb2b8731fa5413 +F ext/wasm/GNUmakefile 54c0db93a5493f625c0a993c12aee5d83951440eee03b2aecfc8aeb998182998 F ext/wasm/README-dist.txt 2d670b426fc7c613b90a7d2f2b05b433088fe65181abead970980f0a4a75ea20 F ext/wasm/README.md ef39861aa21632fdbca0bdd469f78f0096f6449a720f3f39642594af503030e9 F ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api ffa70413409e922ce0f761779787a1d9100b34b43c8e3106bb7ccf2786a41326 F ext/wasm/api/EXPORTED_RUNTIME_METHODS.sqlite3-api 1ec3c73e7d66e95529c3c64ac3de2470b0e9e7fbf7a5b41261c367cf4f1b7287 -F ext/wasm/api/README.md 20a256f4aaae80035d2bb1c9e3e0a125570313a8d137d427471d7be10edde87a +F ext/wasm/api/README.md 17fb1e10335cc87e366dec496c5b17b061f3f75cdf216e825258de34d97a3e53 F ext/wasm/api/extern-post-js.c-pp.js 8923f76c3d2213159e12d641dc750523ead5c848185dc4996fae5cc12397f88d F ext/wasm/api/extern-pre-js.js cc61c09c7a24a07dbecb4c352453c3985170cec12b4e7e7e7a4d11d43c5c8f41 F ext/wasm/api/post-js-footer.js cd0a8ec768501d9bd45d325ab0442037fb0e33d1f3b4f08902f15c34720ee4a1 @@ -504,12 +504,12 @@ F ext/wasm/api/post-js-header.js 47b6b281f39ad59fa6e8b658308cd98ea292c286a68407b F ext/wasm/api/pre-js.c-pp.js b88499dc303c21fc3f55f2c364a0f814f587b60a95784303881169f9e91c1d5f F ext/wasm/api/sqlite3-api-cleanup.js 680d5ccfff54459db136a49b2199d9f879c8405d9c99af1dda0cc5e7c29056f4 F ext/wasm/api/sqlite3-api-glue.js c3a11e1d0e6fd381f68f9e76ad01f3616a6b809fbf9f5aa8e323955c128a6811 -F ext/wasm/api/sqlite3-api-oo1.js 793883953d4024e7b8c5ee1c7a6cb49c18ca53a1d235a203f93746f8907d32ba -F ext/wasm/api/sqlite3-api-prologue.js 815fef5ee93e1bb11ebec5a1d6a1b8ae2e47cfeb66dc5f6e93380ccce045f194 +F ext/wasm/api/sqlite3-api-oo1.js b970787aaf0bdd3a3df59cf66aeb84d0decaaa0529aed7eaf45121087181245f +F ext/wasm/api/sqlite3-api-prologue.js 31cffd8ce212fad8d316a08decd864b0f614c5fce3686153707dffe40f8279e8 F ext/wasm/api/sqlite3-api-worker1.js e94ba98e44afccfa482874cd9acb325883ade50ed1f9f9526beb9de1711f182f F ext/wasm/api/sqlite3-license-version-header.js a661182fc93fc2cf212dfd0b987f8e138a3ac98f850b1112e29b5fbdaecc87c3 F ext/wasm/api/sqlite3-opfs-async-proxy.js f79dd8d98ef3e0b55c10bb2bee7a3840fa967318e1f577c156aafc34664271d1 -F ext/wasm/api/sqlite3-vfs-helper.js 4ad4faf02e1524bf0296be8452c00b5708dce6faf649468d0377e26a0b299263 +F ext/wasm/api/sqlite3-v-helper.js 4451763a0cd85734f0afe18b48918cb3c88ca99cef399b7c5f12119281e7b6a8 w ext/wasm/api/sqlite3-vfs-helper.js F ext/wasm/api/sqlite3-vfs-opfs.c-pp.js 29d6487a26b2fb6a471cde52c37ffee7c27ed6a91914b308c247e0706f454ffb F ext/wasm/api/sqlite3-wasi.h 25356084cfe0d40458a902afb465df8c21fc4152c1d0a59b563a3fba59a068f9 F ext/wasm/api/sqlite3-wasm.c 723522a6c2a2463884a83fa1cc7ae5770deaaf0856a1058cc1023b2bfa1c898b @@ -539,8 +539,8 @@ F ext/wasm/fiddle/fiddle.js 974b995119ac443685d7d94d3b3c58c6a36540e9eb3fed7069d5 F ext/wasm/fiddle/index.html 5daf54e8f3d7777cbb1ca4f93affe28858dbfff25841cb4ab81d694efed28ec2 F ext/wasm/index-dist.html c806b6005145b71d64240606e9c6e0bf56878ee8829c66fe7486cebf34b0e6b1 F ext/wasm/index.html f151b7c7b5cfdc066567d556acd168e769efd4e982286dc5f849a5ee69ecd0ff -F ext/wasm/jaccwabyt/jaccwabyt.js b7261221133cda8d363f16ddbac8e5b671fd51ce962fc34dc10e738a293b696d -F ext/wasm/jaccwabyt/jaccwabyt.md 72742a3205f1477de68086e7e4a854ed5f7d08dfcd6db54cdbea4dba4866f159 +F ext/wasm/jaccwabyt/jaccwabyt.js f4fc93375e9c40ef60f56cbecca1b4dc8bf4f53fab2c3abc860ed34890c5d32d +F ext/wasm/jaccwabyt/jaccwabyt.md 4bf62f7519857cdd674594aba7436cc4fae177eefbfaabc00740e16d9a828bee F ext/wasm/module-symbols.html 980680c8acfa3c8ae6a5aa223512d1b8e78040ced20f8ba2c382129bc73ec028 F ext/wasm/scratchpad-wasmfs-main.html 20cf6f1a8f368e70d01e8c17200e3eaa90f1c8e1029186d836d14b83845fbe06 F ext/wasm/scratchpad-wasmfs-main.js 4c140457f4d6da9d646a49addd91edb6e9ad1643c6c48e3258b5bce24725dc18 @@ -555,7 +555,7 @@ F ext/wasm/test-opfs-vfs.html 1f2d672f3f3fce810dfd48a8d56914aba22e45c6834e262555 F ext/wasm/test-opfs-vfs.js 44363db07b2a20e73b0eb1808de4400ca71b703af718d0fa6d962f15e73bf2ac F ext/wasm/tester1-worker.html d43f3c131d88f10d00aff3e328fed13c858d674ea2ff1ff90225506137f85aa9 F ext/wasm/tester1.c-pp.html d34bef3d48e5cbc1c7c06882ad240fec49bf88f5f65696cc2c72c416933aa406 -F ext/wasm/tester1.c-pp.js 8ed17c0e1f271e536cb7ccd86d3992785fc8bf2f94f9c2e0088ca670601ee087 +F ext/wasm/tester1.c-pp.js d96a77dbf0d8af11e3f3adb013bee2bfb5bf9410e3f5eade528d70104451dd10 F ext/wasm/tests/opfs/concurrency/index.html 86d8ac435074d1e7007b91105f4897f368c165e8cecb6a9aa3d81f5cf5dcbe70 F ext/wasm/tests/opfs/concurrency/test.js a98016113eaf71e81ddbf71655aa29b0fed9a8b79a3cdd3620d1658eb1cc9a5d F ext/wasm/tests/opfs/concurrency/worker.js 0a8c1a3e6ebb38aabbee24f122693f1fb29d599948915c76906681bb7da1d3d2 @@ -2065,8 +2065,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P a329a809b5da135a9c251e4d5f637d45d01d0248110ac05f2ad8f01d9df38c64 -R 3d4ed37777b015bac598ce3a6734f5aa +P a190abc307847174f36421eaa3f47ef349c6f84a2bb35857fa64f64bbe722708 +R 5c37541d673cc882e16d46aec14f6ca8 U stephan -Z 002b2dd6406bdc41efd7e7f9f5d7918f +Z d89d1c25adf51ede4f60b7c8a7ea4184 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 14e600f31e..17c068d5db 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -a190abc307847174f36421eaa3f47ef349c6f84a2bb35857fa64f64bbe722708 \ No newline at end of file +60482c97e02bc4cafefef281be0cf0bc8c5c53232162829c137f3f7a80cdc534 \ No newline at end of file