blob: 124aca573328f1846747aaeb9871bc353bc3657e [file] [log] [blame] [edit]
/*
** 2023-08-03
**
** 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.fts5, a namespace which exists to assist
in JavaScript-side extension of FTS5.
*/
'use strict';
globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
const wasm = sqlite3.wasm, capi = sqlite3.capi, toss = sqlite3.util.toss3;
if(!capi.fts5_api_from_db){
return /*this build does not have FTS5*/;
}
const fts = sqlite3.fts5 = Object.create(null);
const __xArgDb = wasm.xWrap.argAdapter('sqlite3*');
/**
Move FTS-specific APIs (installed via automation) from
sqlite3.capi to sqlite3.fts.
*/
for(const c of [
'Fts5ExtensionApi', 'Fts5PhraseIter', 'fts5_api',
'fts5_api_from_db', 'fts5_tokenizer'
]){
fts[c] = capi[c] || toss("Cannot find capi."+c);
delete capi[c];
}
/**
Requires a JS Function intended to be used as an xFunction()
implementation. This function returns a proxy xFunction
wrapper which:
- Converts all of its sqlite3_value arguments to an array
of JS values using sqlite3_values_to_js().
- Calls the given callback, passing it:
(pFtsXApi, pFtsCx, pCtx, array-of-values)
where the first 3 arguments are the first 3 pointers
in the xFunction interface.
The call is intended to set a result value into the db, and may
do so be either (A) explicitly returning non-undefined or (B)
using one of the sqlite3_result_XYZ() functions and returning
undefined. If the callback throws, its exception will be passed
to sqlite3_result_error_js().
*/
fts.xFunctionProxy1 = function(callback){
return (pFtsXApi, pFtsCx, pCtx, argc, pArgv)=>{
try{
capi.sqlite3_result_js(pCtx, callback(
pFtsXApi, pFtsCx, pCtx,
capi.sqlite3_values_to_js(argc, pArgv)
));
}catch(e){
capi.sqlite3_result_error_js(pCtx, e);
}
}
};
/**
Identical to xFunctionProxy1 except that the callback wrapper it
creates does _not_ perform sqlite3_value-to-JS conversion in
advance and calls the callback with:
(pFtsXApi, pFtsCx, pCtx, array-of-ptr-to-sqlite3_value)
It is up to the callback to use the sqlite3_value_XYZ() family of
functions to inspect or convert the values.
*/
fts.xFunctionProxy2 = function(callback){
return (pFtsXApi, pFtsCx, pCtx, argc, pArgv)=>{
try{
const list = [];
let i;
for(i = 0; i < argc; ++i){
list.push( wasm.peekPtr(pArgv + (wasm.ptrSizeof * i)) );
}
capi.sqlite3_result_js(pCtx, callback(
pFtsXApi, pFtsCx, pCtx, list
));
}catch(e){
capi.sqlite3_result_error_js(pCtx, e);
}
}
};
/**
JS-to-WASM arg adapter for xCreateFunction()'s xFunction arg.
This binds JS impls of xFunction to WASM so that they can be
called from native code. Its context is limited to the
combination of ((fts5_api*) + functionNameCaseInsensitive), and
will replace any existing impl for subsequent invocations for the
same combination.
The functions is creates are intended to set a result value into
the db, and may do so be either (A) explicitly returning
non-undefined or (B) using one of the sqlite3_result_XYZ()
functions and returning undefined. If the callback throws, its
exception will be passed to sqlite3_result_error_js().
PENDING DESIGN DECISION: this framework currently converts each
argument in its JS equivalent before passing them on to the
xFunction impl. We could, and possibly should, instead pass a JS
array of sqlite3_value pointers. The advantages would be:
- No in-advance to-JS overhead which xFunction might not use.
Disadvantages include:
- xFunction would be required to call sqlite3_value_to_js(),
or one of the many sqlite3_value_XYZ() functions on their own.
This would be more cumbersome for most users.
Regardless of which approach is chosen here, clients could
provide a function of their own which takes the _other_ approach,
install it with wasm.installFunction(), and then pass that
generated pointer to createFunction(), in which case this layer
does not proxying and passes all native-level arguments as-is to
the client-defined function.
*/
const xFunctionArgAdapter = new wasm.xWrap.FuncPtrAdapter({
name: 'fts5_api::xCreateFunction(xFunction)',
signature: 'v(pppip)',
contextKey: (argv,argIndex)=>{
return (argv[0]/*(fts5_api*)*/
+ wasm.cstrToJs(argv[1]).toLowerCase()/*name*/)
},
callProxy: fts.xFunctionProxy1
});
/** Map of (sqlite3*) to fts.fts5_api. */
const __ftsApiToStruct = Object.create(null);
const __fts5_api_from_db = function(pDb, createIfNeeded){
let rc = __ftsApiToStruct[pDb];
if(!rc && createIfNeeded){
const fapi = fts.fts5_api_from_db(pDb)
|| toss("Internal error - cannot get FTS5 API object for db.");
rc = new fts.fts5_api(fapi);
__ftsApiToStruct[pDb] = rc;
}
return rc;
};
/**
Arrange for WASM functions dynamically created via this API to be
uninstalled when the db they were installed for is closed... */
const __addCleanupForFunc = function(sfapi, name, pDestroy){
if(!sfapi.$$cleanup){
sfapi.$$cleanup = [];
}
sfapi.$$cleanup.push([name.toLowerCase(), pDestroy]);
};
/**
Callback to be invoked via the JS binding of sqlite3_close_v2(),
after the db has been closed, meaning that the argument to this
function is not a valid object. We use its address only as a
lookup key.
*/
sqlite3.__dbCleanupMap.postCloseCallbacks.push(function(pDb){
const sfapi = __fts5_api_from_db(pDb, false);
if(sfapi){
delete __ftsApiToStruct[pDb];
if(sfapi.$$cleanup){
const fapi = sfapi.pointer;
const scope = wasm.scopedAllocPush();
//wasm.xWrap.FuncPtrAdapter.debugFuncInstall = true;
try{
for(const [name, pDestroy] of sfapi.$$cleanup){
try{
/* Uninstall xFunctionArgAdapter's bindings via a
roundabout approach: its scoping rules uninstall each
new installation at the earliest opportunity, so we
simply need to fake a call with a 0-pointer for the
xFunction callback to uninstall the most recent
one. */
const zName = wasm.scopedAllocCString(name);
const argv = [fapi, zName, 0, 0, 0];
xFunctionArgAdapter.convertArg(argv[3], argv, 3);
/* xDestroy, on the other hand, requires some
hand-holding to ensure we don't prematurely
uninstall these when a function is replaced
(shadowed). */
if(pDestroy) wasm.uninstallFunction(pDestroy);
}catch(e){
sqlite3.config.warn("Could not remove FTS func",name,e);
}
}
}finally{
wasm.scopedAllocPop(scope);
}
//wasm.xWrap.FuncPtrAdapter.debugFuncInstall = false;
}
sfapi.dispose();
}
});
const __affirmDbArg = (arg)=>{
arg = __xArgDb(arg);
if(!arg || !wasm.isPtr(arg)) toss("Invalid db argument.");
return arg;
};
/**
Convenience wrapper to fts5_api::xCreateFunction.
Creates a new FTS5 function for the given database. The arguments are:
- db must be either an sqlite3.oo1.DB instance or a WASM pointer
to (sqlite3*).
- name: name (JS string) of the function
- xFunction either a Function or a pointer to a WASM function. In
the former case a WASM-bound wrapper, behaving as documented
for fts5.xFunctionProxy1(), gets installed for the life of the
given db handle. In the latter case the function is
passed-through as-is, with no argument conversion or lifetime
tracking. In the former case the function is called as
documented for xFunctionProxy1() and in the latter it must
return void and is called with args (ptrToFts5ExtensionApi,
ptrToFts5Context, ptrToSqlite3Context, int argc,
C-array-of-sqlite3_value-pointers).
- xDestroy optional Function or pointer to WASM function to call
when the binding is destroyed (when the db handle is
closed). The function will, in this context, always be passed 0
as its only argument. A passed-in function must, however,
have one parameter so that type signature checks will pass.
It must return void and must not throw.
The 2nd and subsequent aruguments may optionally be packed into
a single Object with like-named properties.
This function throws on error, of which there are many potential
candidates. It returns `undefined`.
*/
fts.createFunction = function(db, name, xFunction, xDestroy = 0){
db = __affirmDbArg(db);
if( 2 === arguments.length && 'string' !== typeof name){
xDestroy = name.xDestroy || null;
xFunction = name.xFunction || null;
name = name.name;
}
if( !name || 'string' !== typeof name ) toss("Invalid name argument.");
const sfapi = __fts5_api_from_db(db, true);
let pDestroy = 0;
try{
/** Because of how fts5_api::xCreateFunction() replaces
functions (by prepending new ones to a linked list but
retaining old ones), we cannot use a FuncPtrAdapter to
automatically convert xDestroy, lest we end up uninstalling
a bound-to-wasm JS function's wasm pointer before fts5
cleans it up when the db is closed. */
if(xDestroy instanceof Function){
pDestroy = wasm.installFunction(xDestroy, 'v(p)');
}
const xcf = sfapi.$$xCreateFunction || (
sfapi.$$xCreateFunction = wasm.xWrap(sfapi.$xCreateFunction, 'int', [
'*', 'string', '*', xFunctionArgAdapter, '*'
])
);
const rc = xcf(sfapi.pointer, name, 0, xFunction || 0, pDestroy || xDestroy || 0 );
if(rc) toss(rc,"FTS5::xCreateFunction() failed.");
__addCleanupForFunc(sfapi, name, pDestroy);
}catch(e){
if(pDestroy) wasm.uninstallFunction(pDestroy);
sfapi.dispose();
throw e;
}
};
/**
! UNTESTED
Convenience wrapper for fts5_api::xCreateTokenizer().
- db = the db to install the tokenizer into.
- name = the JS string name of the tokenizer.
- pTokenizer = the tokenizer instance, which must be a
fts5.fts5_tokenizer instance or a valid WASM pointer to one.
- xDestroy = as documented for createFunction().
The C layer makes a bitwise copy of the tokenizer, so any
changes made to it after installation will have no effect.
Throws on error.
*/
const createTokenizer = function(db, name, pTokenizer, xDestroy = 0){
db = __affirmDbArg(db);
if( 2 === arguments.length && 'string' !== typeof name){
pTokenizer = name.pTokenizer;
xDestroy = name.xDestroy || null;
name = name.name;
}
if( !name || 'string' !== typeof name ) toss("Invalid name argument.");
if(pTokenizer instanceof fts.fts5_tokenizer){
pTokenizer = pTokenizer.pointer;
}
if(!pTokenizer || !wasm.isPtr(pTokenizer)){
toss("Invalid pTokenizer argument - must be a valid fts5.fts5_tokenizer",
"instance or a WASM pointer to one.");
}
const sfapi = __fts5_api_from_db(db, true);
let pDestroy = 0;
const stackPos = wasm.pstack.pointer;
try{
if(xDestroy instanceof Function){
pDestroy = wasm.installFunction(xDestroy, 'v(p)');
}
const xct = sfapi.$$xCreateTokenizer || (
sfapi.$$xCreateTokenizer = wasm.xWrap(sfapi.$xCreateTokenizer, 'int', [
'*', 'string', '*', '*', '*'
/* fts5_api*, const char *zName, void *pContext,
fts5_tokenizer *pTokenizer, void(*xDestroy)(void*) */
])
);
const outPtr = wasm.pstack.allocPtr();
const rc = xct(fapi.pointer, name, 0, pTokenizer, pDestroy || xDestroy || 0 );
if(rc) toss(rc,"FTS5::xCreateFunction() failed.");
if(pDestroy) __addCleanupForFunc(sfapi, name, pDestroy);
}catch(e){
if(pDestroy) wasm.uninstallFunction(pDestroy);
sfapi.dispose();
throw e;
}finally{
wasm.pstack.restore(stackPost);
}
};
//fts.createTokenizer = createTokenizer;
}/*sqlite3ApiBootstrap.initializers.push()*/);