mirror of
https://github.com/sqlite/sqlite.git
synced 2025-07-30 19:03:16 +03:00
wasm: introduce the sqlite3.oo1.DB.wrapHandle() and Stmt.wrapHandle() APIs, which enable clients to wrap (sqlite3*) resp. (sqlite3_stmt*) pointers in their oo1 API counterparts, optionally with or without taking over ownership of the pointer.
FossilOrigin-Name: e5d079549594ca44852773b8919894866394e47ad725dadc7f65242413a219d3
This commit is contained in:
@ -37,6 +37,21 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
|
||||
it.
|
||||
*/
|
||||
const __ptrMap = new WeakMap();
|
||||
/**
|
||||
A Set of oo1.DB or oo1.Stmt objects which are proxies for
|
||||
(sqlite3*) resp. (sqlite3_stmt*) pointers which themselves are
|
||||
owned elsewhere. Objects in this Set do not own their underlying
|
||||
handle and that handle must be guaranteed (by the client) to
|
||||
outlive the proxy. DB.close()/Stmt.finalize() methods will remove
|
||||
the object from this Set _instead_ of closing/finalizing the
|
||||
pointer. These proxies are primarily intended as a way to briefly
|
||||
wrap an (sqlite3[_stmt]*) object as an oo1.DB/Stmt without taking
|
||||
over ownership, to take advantage of simplifies usage compared to
|
||||
the C API while not imposing any change of ownership.
|
||||
|
||||
See DB.wrapHandle() and Stmt.wrapHandle().
|
||||
*/
|
||||
const __doesNotOwnHandle = new Set();
|
||||
/**
|
||||
Map of DB instances to objects, each object being a map of Stmt
|
||||
wasm pointers to Stmt objects.
|
||||
@ -234,73 +249,89 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
|
||||
};
|
||||
}
|
||||
const opt = ctor.normalizeArgs(...args);
|
||||
let fn = opt.filename, vfsName = opt.vfs, flagsStr = opt.flags;
|
||||
if(('string'!==typeof fn && 'number'!==typeof fn)
|
||||
|| 'string'!==typeof flagsStr
|
||||
|| (vfsName && ('string'!==typeof vfsName && 'number'!==typeof vfsName))){
|
||||
sqlite3.config.error("Invalid DB ctor args",opt,arguments);
|
||||
toss3("Invalid arguments for DB constructor.");
|
||||
}
|
||||
let fnJs = ('number'===typeof fn) ? wasm.cstrToJs(fn) : fn;
|
||||
const vfsCheck = ctor._name2vfs[fnJs];
|
||||
if(vfsCheck){
|
||||
vfsName = vfsCheck.vfs;
|
||||
fn = fnJs = vfsCheck.filename(fnJs);
|
||||
}
|
||||
let pDb, oflags = 0;
|
||||
if( flagsStr.indexOf('c')>=0 ){
|
||||
oflags |= capi.SQLITE_OPEN_CREATE | capi.SQLITE_OPEN_READWRITE;
|
||||
}
|
||||
if( flagsStr.indexOf('w')>=0 ) oflags |= capi.SQLITE_OPEN_READWRITE;
|
||||
if( 0===oflags ) oflags |= capi.SQLITE_OPEN_READONLY;
|
||||
oflags |= capi.SQLITE_OPEN_EXRESCODE;
|
||||
const stack = wasm.pstack.pointer;
|
||||
try {
|
||||
const pPtr = wasm.pstack.allocPtr() /* output (sqlite3**) arg */;
|
||||
let rc = capi.sqlite3_open_v2(fn, pPtr, oflags, vfsName || 0);
|
||||
pDb = wasm.peekPtr(pPtr);
|
||||
checkSqlite3Rc(pDb, rc);
|
||||
capi.sqlite3_extended_result_codes(pDb, 1);
|
||||
if(flagsStr.indexOf('t')>=0){
|
||||
capi.sqlite3_trace_v2(pDb, capi.SQLITE_TRACE_STMT,
|
||||
__dbTraceToConsole, pDb);
|
||||
//sqlite3.config.debug("DB ctor",opt);
|
||||
let pDb;
|
||||
if( (pDb = opt['sqlite3*']) ){
|
||||
/* This property ^^^^^ is very specifically NOT DOCUMENTED and
|
||||
NOT part of the public API. This is a back door for functions
|
||||
like DB.wrapDbHandle(). */
|
||||
//sqlite3.config.debug("creating proxy db from",opt);
|
||||
if( !opt['sqlite3*:takeOwnership'] ){
|
||||
/* This is object does not own its handle. */
|
||||
__doesNotOwnHandle.add(this);
|
||||
}
|
||||
}catch( e ){
|
||||
if( pDb ) capi.sqlite3_close_v2(pDb);
|
||||
throw e;
|
||||
}finally{
|
||||
wasm.pstack.restore(stack);
|
||||
this.filename = capi.sqlite3_db_filename(pDb,'main');
|
||||
}else{
|
||||
let fn = opt.filename, vfsName = opt.vfs, flagsStr = opt.flags;
|
||||
if(('string'!==typeof fn && 'number'!==typeof fn)
|
||||
|| 'string'!==typeof flagsStr
|
||||
|| (vfsName && ('string'!==typeof vfsName && 'number'!==typeof vfsName))){
|
||||
sqlite3.config.error("Invalid DB ctor args",opt,arguments);
|
||||
toss3("Invalid arguments for DB constructor.");
|
||||
}
|
||||
let fnJs = ('number'===typeof fn) ? wasm.cstrToJs(fn) : fn;
|
||||
const vfsCheck = ctor._name2vfs[fnJs];
|
||||
if(vfsCheck){
|
||||
vfsName = vfsCheck.vfs;
|
||||
fn = fnJs = vfsCheck.filename(fnJs);
|
||||
}
|
||||
let oflags = 0;
|
||||
if( flagsStr.indexOf('c')>=0 ){
|
||||
oflags |= capi.SQLITE_OPEN_CREATE | capi.SQLITE_OPEN_READWRITE;
|
||||
}
|
||||
if( flagsStr.indexOf('w')>=0 ) oflags |= capi.SQLITE_OPEN_READWRITE;
|
||||
if( 0===oflags ) oflags |= capi.SQLITE_OPEN_READONLY;
|
||||
oflags |= capi.SQLITE_OPEN_EXRESCODE;
|
||||
const stack = wasm.pstack.pointer;
|
||||
try {
|
||||
const pPtr = wasm.pstack.allocPtr() /* output (sqlite3**) arg */;
|
||||
let rc = capi.sqlite3_open_v2(fn, pPtr, oflags, vfsName || 0);
|
||||
pDb = wasm.peekPtr(pPtr);
|
||||
checkSqlite3Rc(pDb, rc);
|
||||
capi.sqlite3_extended_result_codes(pDb, 1);
|
||||
if(flagsStr.indexOf('t')>=0){
|
||||
capi.sqlite3_trace_v2(pDb, capi.SQLITE_TRACE_STMT,
|
||||
__dbTraceToConsole, pDb);
|
||||
}
|
||||
}catch( e ){
|
||||
if( pDb ) capi.sqlite3_close_v2(pDb);
|
||||
throw e;
|
||||
}finally{
|
||||
wasm.pstack.restore(stack);
|
||||
}
|
||||
this.filename = fnJs;
|
||||
}
|
||||
this.filename = fnJs;
|
||||
__ptrMap.set(this, pDb);
|
||||
__stmtMap.set(this, Object.create(null));
|
||||
try{
|
||||
if( !opt['sqlite3*'] ){
|
||||
try{
|
||||
//#if enable-see
|
||||
dbCtorApplySEEKey(this,opt);
|
||||
dbCtorApplySEEKey(this,opt);
|
||||
//#endif
|
||||
// Check for per-VFS post-open SQL/callback...
|
||||
const pVfs = capi.sqlite3_js_db_vfs(pDb)
|
||||
|| toss3("Internal error: cannot get VFS for new db handle.");
|
||||
const postInitSql = __vfsPostOpenCallback[pVfs];
|
||||
if(postInitSql){
|
||||
/**
|
||||
Reminder: if this db is encrypted and the client did _not_ pass
|
||||
in the key, any init code will fail, causing the ctor to throw.
|
||||
We don't actually know whether the db is encrypted, so we cannot
|
||||
sensibly apply any heuristics which skip the init code only for
|
||||
encrypted databases for which no key has yet been supplied.
|
||||
*/
|
||||
if(postInitSql instanceof Function){
|
||||
postInitSql(this, sqlite3);
|
||||
}else{
|
||||
checkSqlite3Rc(
|
||||
pDb, capi.sqlite3_exec(pDb, postInitSql, 0, 0, 0)
|
||||
);
|
||||
// Check for per-VFS post-open SQL/callback...
|
||||
const pVfs = capi.sqlite3_js_db_vfs(pDb)
|
||||
|| toss3("Internal error: cannot get VFS for new db handle.");
|
||||
const postInitSql = __vfsPostOpenCallback[pVfs];
|
||||
if(postInitSql){
|
||||
/**
|
||||
Reminder: if this db is encrypted and the client did _not_ pass
|
||||
in the key, any init code will fail, causing the ctor to throw.
|
||||
We don't actually know whether the db is encrypted, so we cannot
|
||||
sensibly apply any heuristics which skip the init code only for
|
||||
encrypted databases for which no key has yet been supplied.
|
||||
*/
|
||||
if(postInitSql instanceof Function){
|
||||
postInitSql(this, sqlite3);
|
||||
}else{
|
||||
checkSqlite3Rc(
|
||||
pDb, capi.sqlite3_exec(pDb, postInitSql, 0, 0, 0)
|
||||
);
|
||||
}
|
||||
}
|
||||
}catch(e){
|
||||
this.close();
|
||||
throw e;
|
||||
}
|
||||
}catch(e){
|
||||
this.close();
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
@ -403,7 +434,6 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
|
||||
- `vfs`: the VFS fname
|
||||
|
||||
//#if enable-see
|
||||
|
||||
SEE-capable builds optionally support ONE of the following
|
||||
additional options:
|
||||
|
||||
@ -429,7 +459,6 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
|
||||
is supplied and the database is encrypted, execution of the
|
||||
post-initialization SQL will fail, causing the constructor to
|
||||
throw.
|
||||
|
||||
//#endif enable-see
|
||||
|
||||
The `filename` and `vfs` arguments may be either JS strings or
|
||||
@ -457,8 +486,8 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
|
||||
/**
|
||||
Internal-use enum for mapping JS types to DB-bindable types.
|
||||
These do not (and need not) line up with the SQLITE_type
|
||||
values. All values in this enum must be truthy and distinct
|
||||
but they need not be numbers.
|
||||
values. All values in this enum must be truthy and (mostly)
|
||||
distinct but they need not be numbers.
|
||||
*/
|
||||
const BindTypes = {
|
||||
null: 1,
|
||||
@ -467,7 +496,6 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
|
||||
boolean: 4,
|
||||
blob: 5
|
||||
};
|
||||
BindTypes['undefined'] == BindTypes.null;
|
||||
if(wasm.bigIntEnabled){
|
||||
BindTypes.bigint = BindTypes.number;
|
||||
}
|
||||
@ -486,26 +514,30 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
|
||||
- `db`: the DB object which created the statement.
|
||||
|
||||
- `columnCount`: the number of result columns in the query, or 0
|
||||
for queries which cannot return results. This property is a proxy
|
||||
for sqlite3_column_count() and its use in loops should be avoided
|
||||
because of the call overhead associated with that. The
|
||||
`columnCount` is not cached when the Stmt is created because a
|
||||
schema change made via a separate db connection between this
|
||||
statement's preparation and when it is stepped may invalidate it.
|
||||
for queries which cannot return results. This property is a
|
||||
read-only proxy for sqlite3_column_count() and its use in loops
|
||||
should be avoided because of the call overhead associated with
|
||||
that. The `columnCount` is not cached when the Stmt is created
|
||||
because a schema change made between this statement's preparation
|
||||
and when it is stepped may invalidate it.
|
||||
|
||||
- `parameterCount`: the number of bindable parameters in the query.
|
||||
- `parameterCount`: the number of bindable parameters in the
|
||||
query. Like `columnCount`, this property is ready-only and is a
|
||||
proxy for a C API call.
|
||||
|
||||
As a general rule, most methods of this class will throw if
|
||||
called on an instance which has been finalized. For brevity's
|
||||
sake, the method docs do not all repeat this warning.
|
||||
*/
|
||||
const Stmt = function(){
|
||||
const Stmt = function(/*oo1db, stmtPtr, BindTypes [,takeOwnership=true] */){
|
||||
if(BindTypes!==arguments[2]){
|
||||
toss3(capi.SQLITE_MISUSE, "Do not call the Stmt constructor directly. Use DB.prepare().");
|
||||
}
|
||||
this.db = arguments[0];
|
||||
__ptrMap.set(this, arguments[1]);
|
||||
this.parameterCount = capi.sqlite3_bind_parameter_count(this.pointer);
|
||||
if( arguments.length>3 && !arguments[3] ){
|
||||
__doesNotOwnHandle.add(this);
|
||||
}
|
||||
};
|
||||
|
||||
/** Throws if the given DB has been closed, else it is returned. */
|
||||
@ -698,10 +730,10 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
|
||||
},
|
||||
/**
|
||||
Finalizes all open statements and closes this database
|
||||
connection. This is a no-op if the db has already been
|
||||
closed. After calling close(), `this.pointer` will resolve to
|
||||
`undefined`, so that can be used to check whether the db
|
||||
instance is still opened.
|
||||
connection (with one exception noted below). This is a no-op if
|
||||
the db has already been closed. After calling close(),
|
||||
`this.pointer` will resolve to `undefined`, and that can be
|
||||
used to check whether the db instance is still opened.
|
||||
|
||||
If this.onclose.before is a function then it is called before
|
||||
any close-related cleanup.
|
||||
@ -721,14 +753,19 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
|
||||
all, will never trigger close(), so onclose handlers are not a
|
||||
reliable way to implement close-time cleanup or maintenance of
|
||||
a db.
|
||||
|
||||
If this instance was created using DB.wrapHandle() and does not
|
||||
own this.pointer then it does not close the db handle but it
|
||||
does perform all other work, such as calling onclose callbacks
|
||||
and disassociating this object from this.pointer.
|
||||
*/
|
||||
close: function(){
|
||||
if(this.pointer){
|
||||
const pDb = this.pointer;
|
||||
if(pDb){
|
||||
if(this.onclose && (this.onclose.before instanceof Function)){
|
||||
try{this.onclose.before(this)}
|
||||
catch(e){/*ignore*/}
|
||||
}
|
||||
const pDb = this.pointer;
|
||||
Object.keys(__stmtMap.get(this)).forEach((k,s)=>{
|
||||
if(s && s.pointer){
|
||||
try{s.finalize()}
|
||||
@ -737,7 +774,9 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
|
||||
});
|
||||
__ptrMap.delete(this);
|
||||
__stmtMap.delete(this);
|
||||
capi.sqlite3_close_v2(pDb);
|
||||
if( !__doesNotOwnHandle.delete(this) ){
|
||||
capi.sqlite3_close_v2(pDb);
|
||||
}
|
||||
if(this.onclose && (this.onclose.after instanceof Function)){
|
||||
try{this.onclose.after(this)}
|
||||
catch(e){/*ignore*/}
|
||||
@ -1450,9 +1489,63 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
|
||||
*/
|
||||
checkRc: function(resultCode){
|
||||
return checkSqlite3Rc(this, resultCode);
|
||||
}
|
||||
},
|
||||
}/*DB.prototype*/;
|
||||
|
||||
/**
|
||||
Returns a new oo1.DB instance which wraps the given (sqlite3*)
|
||||
WASM pointer, optionally with or without taking over ownership of
|
||||
that pointer.
|
||||
|
||||
The first argument must be either a non-NULL (sqlite3*) WASM
|
||||
pointer.
|
||||
|
||||
The second argument, defaulting to false, specifies ownership of
|
||||
the first argument. If it is truthy, the returned object will
|
||||
pass that pointer to sqlite3_close() when its close() method is
|
||||
called, otherwise it will not.
|
||||
|
||||
Throws if pDb is not a non-0 WASM pointer.
|
||||
|
||||
The caller MUST GUARANTEE that the passed-in handle will outlive
|
||||
the returned object, i.e. that it will not be closed. If it is closed,
|
||||
this object will hold a stale pointer and results are undefined.
|
||||
|
||||
Aside from its lifetime, the proxy is to be treated as any other
|
||||
DB instance, including the requirement of calling close() on
|
||||
it. close() will free up internal resources owned by the proxy
|
||||
and disassociate the proxy from that handle but will not
|
||||
actually close the proxied db handle unless this function is
|
||||
passed a thruthy second argument.
|
||||
|
||||
To stress:
|
||||
|
||||
- DO NOT call sqlite3_close() (or similar) on the being-proxied
|
||||
pointer while a proxy is active.
|
||||
|
||||
- ALWAYS eventually call close() on the returned object. If the
|
||||
proxy does not own the underlying handle then its MUST be
|
||||
closed BEFORE the being-proxied handle is closed.
|
||||
|
||||
Design notes:
|
||||
|
||||
- wrapHandle() "could" accept a DB object instance as its first
|
||||
argument and proxy thatDb.pointer but there is currently no use
|
||||
case where doing so would be useful, so it does not allow
|
||||
that. That restriction may be lifted in a future version.
|
||||
*/
|
||||
DB.wrapHandle = function(pDb, takeOwnership=false){
|
||||
if( !pDb || !wasm.isPtr(pDb) ){
|
||||
throw new sqlite3.SQLite3Error(capi.SQLITE_MISUSE,
|
||||
"Argument must be a WASM sqlite3 pointer");
|
||||
}
|
||||
return new DB({
|
||||
/* This ctor call style is very specifically internal-use-only.
|
||||
It is not documented and may change at any time. */
|
||||
"sqlite3*": pDb,
|
||||
"sqlite3*:takeOwnership": !!takeOwnership
|
||||
});
|
||||
};
|
||||
|
||||
/** Throws if the given Stmt has been finalized, else stmt is
|
||||
returned. */
|
||||
@ -1474,8 +1567,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
|
||||
case BindTypes.string:
|
||||
return t;
|
||||
case BindTypes.bigint:
|
||||
if(wasm.bigIntEnabled) return t;
|
||||
/* else fall through */
|
||||
return wasm.bigIntEnabled ? t : undefined;
|
||||
default:
|
||||
return util.isBindableTypedArray(v) ? BindTypes.blob : undefined;
|
||||
}
|
||||
@ -1641,12 +1733,19 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
|
||||
This method always throws if called when it is illegal to do
|
||||
so. Namely, when triggered via a per-row callback handler of a
|
||||
DB.exec() call.
|
||||
|
||||
If Stmt does not own its underlying (sqlite3_stmt*) (see
|
||||
Stmt.wrapHandle()) then this function will not pass it to
|
||||
sqlite3_finalize().
|
||||
*/
|
||||
finalize: function(){
|
||||
if(this.pointer){
|
||||
const ptr = this.pointer;
|
||||
if(ptr){
|
||||
affirmNotLockedByExec(this,'finalize()');
|
||||
const rc = capi.sqlite3_finalize(this.pointer);
|
||||
delete __stmtMap.get(this.db)[this.pointer];
|
||||
const rc = (__doesNotOwnHandle.delete(this)
|
||||
? 0
|
||||
: capi.sqlite3_finalize(ptr));
|
||||
delete __stmtMap.get(this.db)[ptr];
|
||||
__ptrMap.delete(this);
|
||||
__execLock.delete(this);
|
||||
__stmtMayGet.delete(this);
|
||||
@ -2134,6 +2233,64 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
|
||||
set: ()=>toss3("The columnCount property is read-only.")
|
||||
});
|
||||
|
||||
Object.defineProperty(Stmt.prototype, 'parameterCount', {
|
||||
enumerable: false,
|
||||
get: function(){return capi.sqlite3_bind_parameter_count(this.pointer)},
|
||||
set: ()=>toss3("The parameterCount property is read-only.")
|
||||
});
|
||||
|
||||
/**
|
||||
The Stmt counterpart of oo1.DB.wrapHandle(), this creates a Stmt
|
||||
instance which wraps a WASM (sqlite3_stmt*) in the oo1 API,
|
||||
optionally with or without taking over ownership of that pointer.
|
||||
|
||||
The first argument must be an oo1.DB instance[^1].
|
||||
|
||||
The second argument must be a valid WASM (sqlite3_stmt*), as
|
||||
produced by sqlite3_prepare_v2() and sqlite3_prepare_v3().
|
||||
|
||||
The third argument, defaulting to false, specifies whether the
|
||||
returned Stmt object takes over ownership of the underlying
|
||||
(sqlite3_stmt*). If true, the returned object's finalize() method
|
||||
will finalize that handle, else it will not. If it is false,
|
||||
ownership of pStmt is unchanged and pStmt MUST outlive the
|
||||
returned object or results are undefined.
|
||||
|
||||
This function throws if the arguments are invalid. On success it
|
||||
returns a new Stmt object which wraps the given statement
|
||||
pointer.
|
||||
|
||||
Like all Stmt objects, the finalize() method must eventually be
|
||||
called on the returned object to free up internal resources,
|
||||
regardless of whether this function's third argument is true or
|
||||
not.
|
||||
|
||||
[^1]: The first argument cannot be a (sqlite3*) because the
|
||||
resulting Stmt object requires a parent DB object. It is not yet
|
||||
determined whether it would be of general benefit to refactor the
|
||||
DB/Stmt pair internals to communicate in terms of the underlying
|
||||
(sqlite3*) rather than a DB object. If so, we could laxen the
|
||||
first argument's requirement and allow an (sqlite3*). Because
|
||||
DB.wrapHandle() enables multiple DB objects to proxy the same
|
||||
(sqlite3*), we cannot unambiguously translate the first arugment
|
||||
from (sqlite3*) to DB instances for us with this function's first
|
||||
argument.
|
||||
*/
|
||||
Stmt.wrapHandle = function(oo1db, pStmt, takeOwnership=false){
|
||||
let ctor = Stmt;
|
||||
if( !(oo1db instanceof DB) || !oo1db.pointer ){
|
||||
throw new sqlite3.SQLite3Error(sqlite3.SQLITE_MISUSE,
|
||||
"First argument must be an opened "+
|
||||
"sqlite3.oo1.DB instance");
|
||||
}
|
||||
if( !pStmt || !wasm.isPtr(pStmt) ){
|
||||
throw new sqlite3.SQLite3Error(sqlite3.SQLITE_MISUSE,
|
||||
"Second argument must be a WASM "+
|
||||
"sqlite3_stmt pointer");
|
||||
}
|
||||
return new Stmt(oo1db, pStmt, BindTypes, !!takeOwnership);
|
||||
}
|
||||
|
||||
/** The OO API's public namespace. */
|
||||
sqlite3.oo1 = {
|
||||
DB,
|
||||
|
Reference in New Issue
Block a user