/* ** 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 */; 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 xDisconnect(), 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()*/);