From: stephan Date: Sun, 22 May 2022 16:25:43 +0000 (+0000) Subject: WASM: added bindings for sqlite3_compileoption_get/used(), moved OO #1 into sqlite3... X-Git-Tag: version-3.39.0~120 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=40b5b19a0ffaac4063df6fbf56c3854e3ce8ac72;p=thirdparty%2Fsqlite.git WASM: added bindings for sqlite3_compileoption_get/used(), moved OO #1 into sqlite3-api.js since it can only be used from the same thread as that API and separating them complicates client-side use. Started adding test utilities and tests for the OO1 API. FossilOrigin-Name: f3bc0328c87cac7d50513b0f13576d8fe7b411396f19c08fbe7e7c657b33cfbf --- diff --git a/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3 b/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3 index 09f6e8ba79..e6b57420c2 100644 --- a/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3 +++ b/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3 @@ -3,6 +3,7 @@ _sqlite3_bind_double _sqlite3_bind_int _sqlite3_bind_int64 _sqlite3_bind_null +_sqlite3_bind_parameter_count _sqlite3_bind_parameter_index _sqlite3_bind_text _sqlite3_changes @@ -16,6 +17,8 @@ _sqlite3_column_double _sqlite3_column_name _sqlite3_column_text _sqlite3_column_type +_sqlite3_compileoption_get +_sqlite3_compileoption_used _sqlite3_create_function_v2 _sqlite3_data_count _sqlite3_db_filename diff --git a/ext/fiddle/sqlite3-api.js b/ext/fiddle/sqlite3-api.js index 83e46bbcaf..0c24085784 100644 --- a/ext/fiddle/sqlite3-api.js +++ b/ext/fiddle/sqlite3-api.js @@ -10,21 +10,31 @@ *********************************************************************** - This file is intended to be loaded after loading - sqlite3-module.wasm. It sets one of any number of potential - bindings using that API, this one as closely matching the C-native - API as is feasible. + This file is intended to be loaded after loading sqlite3.wasm. It + sets one of any number of potential bindings using that API, this + one as closely matching the C-native API as is feasible. Note that this file is not named sqlite3.js because that file gets generated by emscripten as the JS-glue counterpart of sqlite3.wasm. The API gets installed as self.sqlite3, where self is expected to be - either the global window or Worker object. + either the global window or Worker object. In addition, a higher-level + OO API is installed as self.SQLite3. - Because using this API properly requires some degree of WASM-related - magic, it is not recommended that this API be used as-is in - client-level code, but instead is intended to be used as a basis for - APIs more appropriate for high-level client code. + Potential TODO: instead of exporting 2 symbols, export only SQLite3 + as {api: sqlite3, oo1: SQLite3}. The way we export this module is + not _really_ modern-JS-friendly because it exports global symbols + (which is admittedly poor form). Exporting it "cleanly" requires + using a module loader in all client code. As there are several + different approaches, none of which this developer is currently + truly familiar with, the current approach will have to do for the + time being. + + Because using the low-level API properly requires some degree of + WASM-related magic, it is not recommended that that API be used + as-is in client-level code. Rather, client code should use the + higher-level OO API or write such a wrapper on top of the + lower-level API. This file installs namespace.sqlite3, where namespace is `self`, meaning either the global window or worker, depending on where this @@ -74,8 +84,23 @@ /* It is important that the following integer values match those from the C code. Ideally we could fetch them from the C API, e.g., in the form of a JSON object, but getting that - JSON string constructed within our current confised is - currently not worth the effort. */ + JSON string constructed within our current confines is + currently not worth the effort. + + Reminder to self: we could probably do so by adding the + proverbial level of indirection, calling in to C to get it, + and having that C func call an + emscripten-installed/JS-implemented library function which + builds the result object: + + const obj = {}; + sqlite3__get_enum(function(key,val){ + obj[key] = val; + }); + + but whether or not we can pass a function that way, via a + (void*) is as yet unknown. + */ /* Minimum subset of sqlite result codes we'll need. */ SQLITE_OK: 0, SQLITE_ROW: 100, @@ -108,6 +133,7 @@ ["sqlite3_bind_int","number",["number", "number", "number"]], ["sqlite3_bind_int64","number",["number", "number", "number"]], ["sqlite3_bind_null","void",["number"]], + ["sqlite3_bind_parameter_count", "number", ["number"]], ["sqlite3_bind_parameter_index","number",["number", "string"]], ["sqlite3_bind_text","number",["number", "number", "number", "number", "number"]], ["sqlite3_changes", "number", ["number"]], @@ -121,6 +147,8 @@ ["sqlite3_column_name","string",["number", "number"]], ["sqlite3_column_text","string",["number", "number"]], ["sqlite3_column_type","number",["number", "number"]], + ["sqlite3_compileoption_get", "string", ["number"]], + ["sqlite3_compileoption_used", "number", ["string"]], ["sqlite3_create_function_v2", "number", ["number", "string", "number", "number","number", "number", "number", "number", "number"]], @@ -162,5 +190,447 @@ api[k] = cwrap.apply(this, a); }); //console.debug("libversion =",api.sqlite3_libversion()); - namespace.sqlite3 = api; + + /* What follows is colloquially known as "OO API #1". It is a + binding of the sqlite3 API which is designed to be run within + the same thread (main or worker) as the one in which the + sqlite3 WASM binding was initialized. This wrapper cannot use + the sqlite3 binding if, e.g., the wrapper is in the main thread + and the sqlite3 API is in a worker. */ + /* memory for use in some pointer-passing routines */ + const pPtrArg = stackAlloc(4); + const toss = function(){ + throw new Error(Array.prototype.join.call(arguments, ' ')); + }; + + const sqlite3/*canonical name*/ = S/*convenience alias*/ = api; + + /** + The DB class wraps a sqlite3 db handle. + */ + const DB = function(name/*TODO: openMode flags*/){ + if(!name) name = ':memory:'; + else if('string'!==typeof name){ + toss("TODO: support blob image of db here."); + } + this.checkRc(S.sqlite3_open(name, pPtrArg)); + this.pDb = getValue(pPtrArg, "i32"); + this.filename = name; + this._statements = {/*map of open Stmt _pointers_*/}; + }; + + /** + 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. + */ + const BindTypes = { + null: 1, + number: 2, + string: 3, + boolean: 4, + blob: 5 + }; + BindTypes['undefined'] == BindTypes.null; + + /** + This class wraps sqlite3_stmt. Calling this constructor + directly will trigger an exception. Use DB.prepare() to create + new instances. + */ + const Stmt = function(){ + if(BindTypes!=arguments[2]){ + toss("Do not call the Stmt constructor directly. Use DB.prepare()."); + } + this.db = arguments[0]; + this.pStmt = arguments[1]; + this.columnCount = S.sqlite3_column_count(this.pStmt); + this.parameterCount = S.sqlite3_bind_parameter_count(this.pStmt); + this._allocs = [/*list of alloc'd memory blocks for bind() values*/] + }; + + /** Throws if the given DB has been closed, else it is returned. */ + const affirmDbOpen = function(db){ + if(!db.pDb) toss("DB has been closed."); + return db; + }; + + DB.prototype = { + /** + Expects to be given an sqlite3 API result code. If it is + falsy, this function returns this object, else it throws an + exception with an error message from sqlite3_errmsg(), + using this object's db handle. + */ + checkRc: function(sqliteResultCode){ + if(!sqliteResultCode) return this; + toss(S.sqlite3_errmsg(this.pDb) || "Unknown db error."); + }, + /** + Finalizes all open statements and closes this database + connection. This is a no-op if the db has already been + closed. + */ + close: function(){ + if(this.pDb){ + let s; + const that = this; + Object.keys(this._statements).forEach(function(k,s){ + delete that._statements[k]; + if(s && s.pStmt) s.finalize(); + }); + S.sqlite3_close_v2(this.pDb); + delete this.pDb; + } + }, + /** + Similar to this.filename but will return NULL for + special names like ":memory:". Not of much use until + we have filesystem support. Throws if the DB has + been closed. If passed an argument it then it will return + the filename of the ATTACHEd db with that name, else it assumes + a name of `main`. + */ + fileName: function(dbName){ + return S.sqlite3_db_filename(affirmDbOpen(this).pDb, dbName||"main"); + }, + /** + Compiles the given SQL and returns a prepared Stmt. This is + the only way to create new Stmt objects. Throws on error. + */ + prepare: function(sql){ + affirmDbOpen(this); + setValue(pPtrArg,0,"i32"); + this.checkRc(S.sqlite3_prepare_v2(this.pDb, sql, -1, pPtrArg, null)); + const pStmt = getValue(pPtrArg, "i32"); + if(!pStmt) toss("Empty SQL is not permitted."); + const stmt = new Stmt(this, pStmt, BindTypes); + this._statements[pStmt] = stmt; + return stmt; + } + }; + + /** Returns an opaque truthy value from the BindTypes + enum if v's type is a valid bindable type, else + returns a falsy value. */ + const isSupportedBindType = function(v){ + let t = BindTypes[null===v ? 'null' : typeof v]; + if(t) return t; + // TODO: handle buffer/blob types. + return undefined; + } + + /** + If isSupportedBindType(v) returns a truthy value, this + function returns that value, else it throws. + */ + const affirmSupportedBindType = function(v){ + const t = isSupportedBindType(v); + if(t) return t; + toss("Unsupport bind() argument type."); + }; + + /** + If key is a number and within range of stmt's bound parameter + count, key is returned. + + If key is not a number then it is checked against named + parameters. If a match is found, its index is returned. + + Else it throws. + */ + const indexOfParam = function(stmt,key){ + const n = ('number'===typeof key) + ? key : S.sqlite3_bind_parameter_index(stmt.pStmt, key); + if(0===n || (n===key && (n!==(n|0)/*floating point*/))){ + toss("Invalid bind() parameter name: "+key); + } + else if(n<1 || n>=stmt.parameterCount) toss("Bind index",key,"is out of range."); + return n; + }; + + /** + Binds a single bound parameter value on the given stmt at the + given index (numeric or named) using the given bindType (see + the BindTypes enum) and value. Throws on error. Returns stmt on + success. + */ + const bindOne = function(stmt,ndx,bindType,val){ + affirmSupportedBindType(val); + ndx = indexOfParam(stmt,ndx); + let rc = 0; + switch(bindType){ + case BindType.null: + rc = S.sqlite3_bind_null(stmt.pStmt, ndx); + break; + case BindType.string:{ + const bytes = intArrayFromString(string,false); + const pStr = allocate(bytes, ALLOC_NORMAL); + stmt._allocs.push(pStr); + rc = S.sqlite3_bind_text(stmt.pStmt, ndx, pStr, + bytes.length, 0); + break; + } + case BindType.number: { + const m = ((val === (val|0)) + ? (val>0xefffffff + ? S.sqlite3_bind_int64 + : S.sqlite3_bind_int) + : S.sqlite3_bind_double); + rc = m(stmt.pStmt, ndx, val); + break; + } + case BindType.boolean: + rc = S.sqlite3_bind_int(stmt.pStmt, ndx, val ? 1 : 0); + break; + case BindType.blob: + default: toss("Unsupported bind() argument type."); + } + if(rc) stmt.db.checkRc(rc); + return stmt; + }; + + /** Throws if the given Stmt has been finalized, else + it is returned. */ + const affirmStmtOpen = function(stmt){ + if(!stmt.pStmt) toss("Stmt has been closed."); + return stmt; + }; + + /** Frees any memory explicitly allocated for the given + Stmt object. Returns stmt. */ + const freeBindMemory = function(stmt){ + let m; + while(undefined !== (m = stmt._allocs.pop())){ + _free(m); + } + return stmt; + }; + + Stmt.prototype = { + /** + "Finalizes" this statement. This is a no-op if the + statement has already been finalizes. Returns + undefined. Most methods in this class will throw if called + after this is. + */ + finalize: function(){ + if(this.pStmt){ + freeBindMemory(this); + delete this.db._statements[this.pStmt]; + S.sqlite3_finalize(this.pStmt); + delete this.pStmt; + delete this.db; + } + }, + /** Clears all bound values. Returns this object. + Throws if this statement has been finalized. */ + clearBindings: function(){ + freeBindMemory(affirmStmtOpen(this)); + S.sqlite3_clear_bindings(this.pStmt); + return this; + }, + /** + Resets this statement so that it may be step()ed again + from the beginning. Returns this object. Throws if this + statement has been finalized. + + If passed a truthy argument then this.clearBindings() is + also called, otherwise any existing bindings, along with + any memory allocated for them, are retained. + */ + reset: function(alsoClearBinds){ + if(alsoClearBinds) this.clearBindings(); + S.sqlite3_reset(affirmStmtOpen(this).pStmt); + return this; + }, + /** + Binds one or more values to its bindable parameters. It + accepts 1 or 2 arguments: + + If passed a single argument, it must be either an array, an + object, or a value of a bindable type (see below). + + If passed 2 arguments, the first one is the 1-based bind + index or bindable parameter name and the second one must be + a value of a bindable type. + + Bindable value types: + + - null or undefined is bound as NULL. + + - Numbers are bound as either doubles or integers: int64 if + they are larger than 0xEFFFFFFF, else int32. Booleans are + bound as integer 0 or 1. Note that doubles with no + fractional part are bound as integers. It is not expected + that that distinction is significant for the majority of + clients due to sqlite3's data typing model. This API does + not currently support the BigInt type. + + - Strings are bound as strings (use bindAsBlob() to force + blob binding). + + - buffers (blobs) are currently TODO but will be bound as + blobs. + + If passed an array, each element of the array is bound at + the parameter index equal to the array index plus 1 + (because arrays are 0-based but binding is 1-based). + + If passed an object, each object key is treated as a + bindable parameter name. The object keys _must_ match any + bindable parameter names, including any `$`, `@`, or `:` + prefix. Because `$` is a legal identifier chararacter in + JavaScript, that is the suggested prefix for bindable + parameters. + + It returns this object on success and throws on + error. Errors include: + + - Any bind index is out of range, a named bind parameter + does not match, or this statement has no bindable + parameters. + + - Any value to bind is of an unsupported type. + + - Passed no arguments or more than two. + + - The statement has been finalized. + */ + bind: function(/*[ndx,] value*/){ + if(!this.parameterCount){ + toss("This statement has no bindable parameters."); + } + let ndx, arg; + switch(arguments.length){ + case 1: ndx = 1; arg = arguments[0]; break; + case 2: ndx = arguments[0]; arg = arguments[1]; break; + default: toss("Invalid bind() arguments."); + } + affirmStmtOpen(this); + if(null===arg || undefined===arg){ + /* bind NULL */ + return bindOne(this, ndx, BindType.null, arg); + } + else if(Array.isArray(arg)){ + /* bind each entry by index */ + if(1!==arguments.length){ + toss("When binding an array, an index argument is not permitted."); + } + arg.forEach((v,i)=>bindOne(this, i+1, affirmSupportedBindType(v), v)); + return this; + } + else if('object'===typeof arg/*null was checked above*/){ + /* bind by name */ + if(1!==arguments.length){ + toss("When binding an object, an index argument is not permitted."); + } + Object.keys(arg) + .forEach(k=>bindOne(this, k, + affirmSupportedBindType(arg[k]), + arg[k])); + return this; + }else{ + return bindOne(this, ndx, + affirmSupportedBindType(arg), arg); + } + toss("Should not reach this point."); + }, + /** + Special case of bind() which binds the given value + using the BLOB binding mechanism instead of the default + selected one for the value. The ndx may be a numbered + or named bind index. The value must be of type string, + buffer, or null/undefined (both treated as null). + + If passed a single argument, a bind index of 1 is assumed. + */ + bindAsBlob: function(ndx,arg){ + affirmStmtOpen(this); + if(1===arguments.length){ + ndx = 1; + arg = arguments[0]; + } + const t = affirmSupportedBindType(arg); + if(BindTypes.string !== t && BindTypes.blob !== t + && BindTypes.null !== t){ + toss("Invalid value type for bindAsBlob()"); + } + return bindOne(this, ndx, BindType.blob, arg); + } + }; + + /** OO binding's namespace. */ + const SQLite3 = { + version: { + lib: sqlite3.sqlite3_libversion(), + ooApi: "0.0.1" + }, + DB, + Stmt, + /** + Reports whether a given compile-time option, named by the + given argument. + + If optName is an array then it is expected to be a list of + compilation options and this function returns an object + which maps each such option to true or false. That object + is returned. + + If optName is an object, its keys are expected to be + compilation options and this function sets each entry to + true or false. That object is returned. + + If passed no arguments then it returns an object mapping + all known compilation options to their compile-time values, + or true if the are defined with no value. + + In all other cases it returns true if the option was active + when when compiling the sqlite3 module, else false. + + Compile-time option names may optionally include their + "SQLITE_" prefix. When it returns an object of all options, + the prefix is elided. + */ + compileOptionUsed: function f(optName){ + if(!arguments.length){ + if(!f._opt){ + f._rx = /^([^=]+)=(.+)/; + f._rxInt = /^-?\d+/; + f._opt = function(opt, rv){ + const m = f._rx.exec(opt); + rv[0] = (m ? m[1] : opt); + rv[1] = m ? (f._rxInt.test(m[2]) ? +m[2] : m[2]) : true; + }; + } + const rc = {}, ov = [0,0]; + let i = 0; + while((k = S.sqlite3_compileoption_get(i++))){ + f._opt(k,ov); + rc[ov[0]] = ov[1]; + } + return rc; + } + else if(Array.isArray(optName)){ + const rc = {}; + optName.forEach((v)=>{ + rc[v] = S.sqlite3_compileoption_used(v); + }); + return rc; + } + else if('object' === typeof optName){ + Object.keys(optName).forEach((k)=> { + optName[k] = S.sqlite3_compileoption_used(k); + }); + return optName; + } + return ( + 'string'===typeof optName + ) ? !!S.sqlite3_compileoption_used(optName) : false; + } + }; + + namespace.sqlite3 = sqlite3; + namespace.SQLite3 = SQLite3; })(self/*worker or window*/); diff --git a/ext/fiddle/testing-common.js b/ext/fiddle/testing-common.js index 2fe9a860ba..762ce58668 100644 --- a/ext/fiddle/testing-common.js +++ b/ext/fiddle/testing-common.js @@ -28,7 +28,6 @@ const statusElement = E('#module-status'); const progressElement = E('#module-progress'); const spinnerElement = E('#module-spinner'); - self.Module = { /* ^^^ cannot declare that const because fiddle-module.js (auto-generated) includes a decl for it and runs in this scope. */ @@ -71,9 +70,10 @@ : 'All downloads complete.'); }, /* Loads sqlite3-api.js and calls the given callback (if - provided), passing it the sqlite3 module. Whether this is - synchronous or async depends on whether it's run in the - main thread or a worker.*/ + provided), passing it an object which contains the sqlite3 + and SQLite3 modules. Whether this is synchronous or async + depends on whether it's run in the main thread or a + worker.*/ loadSqliteAPI: function(callback){ const theScript = 'sqlite3-api.js'; if(self.importScripts){/*worker*/ @@ -88,9 +88,62 @@ script.async = true; script.src = theScript; }).then(() => { - if(callback) callback(self.sqlite3); + if(callback) callback({sqlite3:self.sqlite3, + SQLite3:self.SQLite3}); }); } } }; + + const _expr = function(expr){ + return (expr instanceof Function) ? expr() : !!expr; + }; + + /** + Helpers for writing sqlite3-specific tests. + */ + self.SqliteTester = { + /** Running total of the number of tests run via + this API. */ + counter: 0, + /** abort() if expr is false. If expr is a function, it + is called and its result is evaluated. + */ + assert: function(expr, msg){ + ++this.counter; + if(!_expr(expr)) abort(msg || "Assertion failed."); + return this; + }, + /** Identical to assert() but throws instead of calling + abort(). */ + affirm: function(expr, msg){ + ++this.counter; + if(!_expr(expr)) throw new Error(msg || "Affirmation failed."); + return this; + }, + /** Calls f() and squelches any exception it throws. If it + does not throw, this function throws. */ + mustThrow: function(f, msg){ + ++this.counter; + let err; + try{ f(); } catch(e){err=e;} + if(!err) throw new Error(msg || "Expected exception."); + return this; + }, + /** Throws if expr is truthy or expr is a function and expr() + returns truthy. */ + throwIf: function(expr, msg){ + ++this.counter; + if(_expr(expr)) throw new Error(msg || "throwIf() failed"); + return this; + }, + /** Throws if expr is falsy or expr is a function and expr() + returns falsy. */ + throwUnless: function(expr, msg){ + ++this.counter; + if(!_expr(expr)) throw new Error(msg || "throwUnless() failed"); + return this; + } + }; + })(self/*window or worker*/); diff --git a/ext/fiddle/testing1.js b/ext/fiddle/testing1.js index a24d63f105..6e79be0257 100644 --- a/ext/fiddle/testing1.js +++ b/ext/fiddle/testing1.js @@ -12,382 +12,32 @@ A basic test script for sqlite3-api.js. */ -const setupAPI = function(S/*sqlite3 module*/){ - /* memory for use in some pointer-passing routines */ - const pPtrArg = stackAlloc(4); - const dummyArg = {/*for restricting Stmt constructor to internal use*/}; - const toss = function(){ - throw new Error(Array.prototype.join.apply(arguments, ' ')); - }; - - /** - The DB class wraps a sqlite3 db handle. - */ - const DB = function(name/*TODO: openMode flags*/){ - if(!name) name = ':memory:'; - else if('string'!==typeof name){ - toss("TODO: support blob image of db here."); - } - this.checkRc(S.sqlite3_open(name, pPtrArg)); - this.pDb = getValue(pPtrArg, "i32"); - this.filename = name; - this._statements = {/*array of open Stmt _pointers_*/}; - }; - - /** - This class wraps sqlite3_stmt. Calling this constructor - directly will trigger an exception. Use DB.prepare() to create - new instances. - */ - const Stmt = function(){ - if(dummyArg!=arguments[2]){ - toss("Do not call the Stmt constructor directly. Use DB.prepare()."); - } - this.db = arguments[0]; - this.pStmt = arguments[1]; - this.columnCount = S.sqlite3_column_count(this.pStmt); - this._allocs = [/*list of alloc'd memory blocks for bind() values*/] - }; - - - /** Throws if the given DB has been closed, else it is returned. */ - const affirmDbOpen = function(db){ - if(!db.pDb) toss("DB has been closed."); - return db; - }; - - DB.prototype = { - /** - Expects to be given an sqlite3 API result code. If it is - falsy, this function returns this object, else it throws an - exception with an error message from sqlite3_errmsg(), - using this object's db handle. - */ - checkRc: function(sqliteResultCode){ - if(!sqliteResultCode) return this; - toss(S.sqlite3_errmsg(this.pDb) || "Unknown db error."); - }, - /** - Finalizes all open statements and closes this database - connection. This is a no-op if the db has already been - closed. - */ - close: function(){ - if(this.pDb){ - let s; - while(undefined!==(s = this._statements.pop())){ - if(s.pStmt) s.finalize(); - } - S.sqlite3_close_v2(this.pDb); - delete this.pDb; - } - }, - /** - Similar to this.filename but will return NULL for - special names like ":memory:". Not of much use until - we have filesystem support. Throws if the DB has - been closed. If passed an argument it then it will return - the filename of the ATTACHEd db with that name, else it assumes - a name of `main`. - */ - fileName: function(dbName){ - return S.sqlite3_db_filename(affirmDbOpen(this).pDb, dbName||"main"); - }, - - /** - Compiles the given SQL and returns a prepared Stmt. This is - the only way to create new Stmt objects. Throws on error. - */ - prepare: function(sql){ - affirmDbOpen(this); - setValue(pPtrArg,0,"i32"); - this.checkRc(S.sqlite3_prepare_v2(this.pDb, sql, -1, pPtrArg, null)); - const pStmt = getValue(pPtrArg, "i32"); - if(!pStmt) toss("Empty SQL is not permitted."); - const stmt = new Stmt(this, pStmt, dummyArg); - this._statements[pStmt] = stmt; - return stmt; - } - }; - - /** - 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. - */ - const BindTypes = { - null: 1, - number: 2, - string: 3, - boolean: 4, - blob: 5 - }; - BindTypes['undefined'] == BindTypes.null; - - /** Returns an opaque truthy value from the BindTypes - enum if v's type is a valid bindable type, else - returns a falsy value. */ - const isSupportedBindType = function(v){ - let t = BindTypes[null===v ? 'null' : typeof v]; - if(t) return t; - // TODO: handle buffer/blob types. - return undefined; - } - - /** - If isSupportedBindType(v) returns a truthy value, this - function returns that value, else it throws. - */ - const affirmSupportedBindType = function(v){ - const t = isSupportedBindType(v); - if(t) return t; - toss("Unsupport bind() argument type."); - }; - - /** - If key is a number and within range of stmt's bound parameter - count, key is returned. - - If key is not a number then it is checked against named - parameters. If a match is found, its index is returned. - - Else it throws. - */ - const indexOfParam = function(stmt,key){ - const n = ('number'===typeof key) - ? key : S.sqlite3_bind_parameter_index(stmt.pStmt, key); - if(0===n || (n===key && (n!==(n|0)/*floating point*/))){ - toss("Invalid bind() parameter name: "+key); - } - else if(n>=stmt.columnCount) toss("Bind index",key,"is out of range."); - return n; - }; - - /** - Binds a single bound parameter value on the given stmt at the - given index (numeric or named) using the given bindType (see - the BindTypes enum) and value. Throws on error. Returns stmt on - success. - */ - const bindOne = function(stmt,ndx,bindType,val){ - affirmSupportedBindType(val); - ndx = indexOfParam(stmt,ndx); - let rc = 0; - switch(bindType){ - case BindType.null: - rc = S.sqlite3_bind_null(stmt.pStmt, ndx); - break; - case BindType.string:{ - const bytes = intArrayFromString(string,false); - const pStr = allocate(bytes, ALLOC_NORMAL); - stmt._allocs.push(pStr); - rc = S.sqlite3_bind_text(stmt.pStmt, ndx, pStr, - bytes.length, 0); - break; - } - case BindType.number: { - const m = ((val === (val|0)) - ? (val>0xefffffff - ? S.sqlite3_bind_int64 - : S.sqlite3_bind_int) - : S.sqlite3_bind_double); - rc = m(stmt.pStmt, ndx, val); - break; - } - case BindType.boolean: - rc = S.sqlite3_bind_int(stmt.pStmt, ndx, val ? 1 : 0); - break; - case BindType.blob: - default: toss("Unsupported bind() argument type."); - } - if(rc) stmt.db.checkRc(rc); - return stmt; - }; - - /** Throws if the given Stmt has been finalized, else - it is returned. */ - const affirmStmtOpen = function(stmt){ - if(!stmt.pStmt) toss("Stmt has been closed."); - return stmt; - }; - - /** Frees any memory explicitly allocated for the given - Stmt object. Returns stmt. */ - const freeBindMemory = function(stmt){ - let m; - while(undefined !== (m = stmt._allocs.pop())){ - _free(m); - } - return stmt; - }; - - Stmt.prototype = { - /** - "Finalizes" this statement. This is a no-op if the - statement has already been finalizes. Returns - undefined. Most methods in this class will throw if called - after this is. - */ - finalize: function(){ - if(this.pStmt){ - freeBindMemory(this); - S.sqlite3_finalize(this.pStmt); - delete this.pStmt; - delete this.db; - } - }, - /** Clears all bound values. Returns this object. - Throws if this statement has been finalized. */ - clearBindings: function(){ - freeBindMemory(affirmStmtOpen(this)); - S.sqlite3_clear_bindings(this.pStmt); - return this; - }, - /** - Resets this statement so that it may be step()ed again - from the beginning. Returns this object. Throws if this - statement has been finalized. - - If passed a truthy argument then this.clearBindings() is - also called, otherwise any existing bindings, along with - any memory allocated for them, are retained. - */ - reset: function(alsoClearBinds){ - if(alsoClearBinds) this.clearBindings(); - S.sqlite3_reset(affirmStmtOpen(this).pStmt); - return this; - }, - /** - Binds one or more values to its bindable parameters. It - accepts 1 or 2 arguments: - - If passed a single argument, it must be either an array, an - object, or a value of a bindable type (see below). - - If passed 2 arguments, the first one is the 1-based bind - index or bindable parameter name and the second one must be - a value of a bindable type. - - Bindable value types: - - - null or undefined is bound as NULL. - - - Numbers are bound as either doubles or integers: int64 if - they are larger than 0xEFFFFFFF, else int32. Booleans are - bound as integer 0 or 1. Note that doubles with no - fractional part are bound as integers. It is not expected - that that distinction is significant for the majority of - clients due to sqlite3's data typing model. This API does - not currently support the BigInt type. - - - Strings are bound as strings (use bindAsBlob() to force - blob binding). - - - buffers (blobs) are currently TODO but will be bound as - blobs. - - If passed an array, each element of the array is bound at - the parameter index equal to the array index plus 1 - (because arrays are 0-based but binding is 1-based). - - If passed an object, each object key is treated as a - bindable parameter name. The object keys _must_ match any - bindable parameter names, including any `$`, `@`, or `:` - prefix. Because `$` is a legal identifier chararacter in - JavaScript, that is the suggested prefix for bindable - parameters. - - It returns this object on success and throws on - error. Errors include: - - - Any bind index is out of range or a named bind parameter - does not match. - - - Any value to bind is of an unsupported type. - - - Passed no arguments or more than two. - - - The statement has been finalized. - */ - bind: function(/*[ndx,] value*/){ - let ndx, arg; - switch(arguments.length){ - case 1: ndx = 1; arg = arguments[0]; break; - case 2: ndx = arguments[0]; arg = arguments[1]; break; - default: toss("Invalid bind() arguments."); - } - affirmStmtOpen(this); - if(null===arg || undefined===arg){ - /* bind NULL */ - return bindOne(this, ndx, BindType.null, arg); - } - else if(Array.isArray(arg)){ - /* bind each entry by index */ - if(1!==arguments.length){ - toss("When binding an array, an index argument is not permitted."); - } - arg.forEach((v,i)=>bindOne(this, i+1, affirmSupportedBindType(v), v)); - return this; - } - else if('object'===typeof arg/*null was checked above*/){ - /* bind by name */ - if(1!==arguments.length){ - toss("When binding an object, an index argument is not permitted."); - } - Object.keys(arg) - .forEach(k=>bindOne(this, k, - affirmSupportedBindType(arg[k]), - arg[k])); - return this; - }else{ - return bindOne(this, ndx, - affirmSupportedBindType(arg), arg); - } - toss("Should not reach this point."); - }, - /** - Special case of bind() which binds the given value - using the BLOB binding mechanism instead of the default - selected one for the value. The ndx may be a numbered - or named bind index. The value must be of type string, - buffer, or null/undefined (both treated as null). - - If passed a single argument, a bind index of 1 is assumed. - */ - bindAsBlob: function(ndx,arg){ - affirmStmtOpen(this); - if(1===arguments.length){ - ndx = 1; - arg = arguments[0]; - } - const t = affirmSupportedBindType(arg); - if(BindTypes.string !== t && BindTypes.blob !== t - && BindTypes.null !== t){ - toss("Invalid value type for bindAsBlob()"); - } - return bindOne(this, ndx, BindType.blob, arg); - } - }; - - const SQLite3 = { - version: { - lib: S.sqlite3_libversion(), - ooApi: "0.0.1" - }, - DB - }; - return SQLite3; -}; - -const mainTest1 = function(S/*sqlite3 module*/){ +const mainTest1 = function(namespace){ + const S = namespace.sqlite3; + const oo = namespace.SQLite3; + const T = self.SqliteTester; console.log("Loaded module:",S.sqlite3_libversion(), S.sqlite3_sourceid()); - const oo = setupAPI(S); - const db = new oo.DB(); - console.log("DB:",db.filename); + const log = console.log.bind(console); + T.assert(db.pDb); + log("DB:",db.filename); + log("Build options:",oo.compileOptionUsed()); + + let st = db.prepare("select 1"); + T.assert(st.pStmt); + log("statement =",st); + T.assert(st === db._statements[st.pStmt]) + .assert(1===st.columnCount) + .assert(0===st.parameterCount) + .mustThrow(()=>st.bind(1,null)); + + let pId = st.pStmt; + st.finalize(); + T.assert(!st.pStmt) + .assert(!db._statements[pId]); + log("Test count:",T.counter); }; self/*window or worker*/.Module.onRuntimeInitialized = function(){ diff --git a/manifest b/manifest index 54f4e5cb51..36dce9f18d 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C WASM\sOO\swrapper\s#1:\sprepare()\sand\sbind()\sAPIs\sare\sin\splace\sbut\sare\suntested,\spending\sfetch/get\sAPIs. -D 2022-05-22T14:07:44.577 +C WASM:\sadded\sbindings\sfor\ssqlite3_compileoption_get/used(),\smoved\sOO\s#1\sinto\ssqlite3-api.js\ssince\sit\scan\sonly\sbe\sused\sfrom\sthe\ssame\sthread\sas\sthat\sAPI\sand\sseparating\sthem\scomplicates\sclient-side\suse.\sStarted\sadding\stest\sutilities\sand\stests\sfor\sthe\sOO1\sAPI. +D 2022-05-22T16:25:43.076 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -56,7 +56,7 @@ F ext/expert/sqlite3expert.c 6ca30d73b9ed75bd56d6e0d7f2c962d2affaa72c505458619d0 F ext/expert/sqlite3expert.h ca81efc2679a92373a13a3e76a6138d0310e32be53d6c3bfaedabd158ea8969b F ext/expert/test_expert.c d56c194b769bdc90cf829a14c9ecbc1edca9c850b837a4d0b13be14095c32a72 F ext/fiddle/EXPORTED_FUNCTIONS.fiddle 487fc7c83d45c48326f731c89162ed17ab15767e5efede8999d7d6c6e2d04c0f -F ext/fiddle/EXPORTED_FUNCTIONS.sqlite3 a3a2862941270ae5e2633d21cbf44979901c4b75efa42a452c15ef879b47ad2b +F ext/fiddle/EXPORTED_FUNCTIONS.sqlite3 00553766051a038b1acd3992d04e50540d1284c3ea78bd11daa521383e57d653 F ext/fiddle/EXPORTED_RUNTIME_METHODS 91d5dcb0168ee056fa1a340cb8ab3c23d922622f8dad39d28919dd8af2b3ade0 F ext/fiddle/Makefile 9277c73e208b9c8093659256c9f07409c877e366480c7c22ec545ee345451d95 F ext/fiddle/emscripten.css 3d253a6fdb8983a2ac983855bfbdd4b6fa1ff267c28d69513dd6ef1f289ada3f @@ -64,10 +64,10 @@ F ext/fiddle/fiddle-worker.js c22557b641b47fa1473d3465a4e69fe06b8b09b924955805a4 F ext/fiddle/fiddle.html 657c6c3f860c322fba3c69fa4f7a1209e2d2ce44b4bc65a3e154e3a97c047a7c F ext/fiddle/fiddle.js f9c79164428e96a5909532f18a8bc8f8c8ec4f738bfc09ad3d2a532c2400f9f0 F ext/fiddle/index.md d9c1c308d8074341bc3b11d1d39073cd77754cb3ca9aeb949f23fdd8323d81cf -F ext/fiddle/sqlite3-api.js 5f256e3dc78ed0ac4f8556c0c77860812f9baf542b7a73b19b2abb72a6e13146 -F ext/fiddle/testing-common.js 37b014758db7e5e74278e37dc712ced2fc9b40d0617f5ed0b8b64a6bd9c0a45d +F ext/fiddle/sqlite3-api.js ab7e7ded7b3079ee7de43e8290f1942e757d90ebb47ae4654cfe03c980cd0cad +F ext/fiddle/testing-common.js 2b2826a1e7c8ca3e610dfa4255ff1077438b6570e08096cc139c226811e60dbb F ext/fiddle/testing1.html 68cec1b1c8646a071717e5979f22e4268e6d36d96ba13ad68333351acdbcf1d1 -F ext/fiddle/testing1.js 2e9aa40a17c97ab8e90a8ba942725ebf590ae5db3f0329583d7431e4524f5b11 +F ext/fiddle/testing1.js 6a314a10efc954bcd854af89d53ab768f48a42d3dcb80773b297f4ba0ac0236d F ext/fts1/README.txt 20ac73b006a70bcfd80069bdaf59214b6cf1db5e F ext/fts1/ft_hash.c 3927bd880e65329bdc6f506555b228b28924921b F ext/fts1/ft_hash.h 06df7bba40dadd19597aa400a875dbc2fed705ea @@ -1967,8 +1967,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 dea098b64eb95c395b346ebcae687afe42b7d21df48833527808c02226300a66 -R d6777acddc9dd48a02f29eb36eccd673 +P 84c8f63a1c446331a3afe52b0c8bdfa6980f24aa4cf600f576877fef5e650c39 +R d3343607f5b0d8f2cee2f7485f266550 U stephan -Z ec1032561dde0c5db030c66f88c029fe +Z 4ea03d4418e5522f96fc7a3cf30bf26a # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 8691610d23..47aa878ffb 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -84c8f63a1c446331a3afe52b0c8bdfa6980f24aa4cf600f576877fef5e650c39 \ No newline at end of file +f3bc0328c87cac7d50513b0f13576d8fe7b411396f19c08fbe7e7c657b33cfbf \ No newline at end of file