diff options
author | stephan <stephan@noemail.net> | 2023-05-10 21:06:02 +0000 |
---|---|---|
committer | stephan <stephan@noemail.net> | 2023-05-10 21:06:02 +0000 |
commit | 161f742856aa54724e9b692069440cc0dd902c61 (patch) | |
tree | 99aa2ba384b57259b6659a05e1ddb0b46b290821 /ext/wasm/api/sqlite3-api-oo1.js | |
parent | 28f4e7b28692de9d98b07b4ad2cdc91f78d23422 (diff) | |
download | sqlite-161f742856aa54724e9b692069440cc0dd902c61.tar.gz sqlite-161f742856aa54724e9b692069440cc0dd902c61.zip |
Resolve the JS-side corner case reported in [forum:7774b773937cbe0a | forum post 7774b773937cbe0a] by not caching oo1.Stmt.columnCount.
FossilOrigin-Name: c3d25c3a25e79e01c4ad0cf11f7dc3b0fb1932f3bcd04935a728ef62f7e07cf1
Diffstat (limited to 'ext/wasm/api/sqlite3-api-oo1.js')
-rw-r--r-- | ext/wasm/api/sqlite3-api-oo1.js | 100 |
1 files changed, 66 insertions, 34 deletions
diff --git a/ext/wasm/api/sqlite3-api-oo1.js b/ext/wasm/api/sqlite3-api-oo1.js index ac6678c88..faf66e1ae 100644 --- a/ext/wasm/api/sqlite3-api-oo1.js +++ b/ext/wasm/api/sqlite3-api-oo1.js @@ -330,10 +330,15 @@ 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. - - - `parameterCount`: the number of bindable paramters in the query. + - `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. + + - `parameterCount`: the number of bindable parameters in the query. */ const Stmt = function(){ if(BindTypes!==arguments[2]){ @@ -341,7 +346,6 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ } this.db = arguments[0]; __ptrMap.set(this, arguments[1]); - this.columnCount = capi.sqlite3_column_count(this.pointer); this.parameterCount = capi.sqlite3_bind_parameter_count(this.pointer); }; @@ -701,18 +705,18 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ with identical names. - `callback` = a function which gets called for each row of the - result set, but only if that statement has any result - _rows_. The callback's "this" is the options object, noting - that this function synthesizes one if the caller does not pass - one to exec(). The second argument passed to the callback is - always the current Stmt object, as it's needed if the caller - wants to fetch the column names or some such (noting that they - could also be fetched via `this.columnNames`, if the client - provides the `columnNames` option). If the callback returns a - literal `false` (as opposed to any other falsy value, e.g. an - implicit `undefined` return), any ongoing statement-`step()` - iteration stops without an error. The return value of the - callback is otherwise ignored. + result set, but only if that statement has any result rows. The + callback's "this" is the options object, noting that this + function synthesizes one if the caller does not pass one to + exec(). The second argument passed to the callback is always + the current Stmt object, as it's needed if the caller wants to + fetch the column names or some such (noting that they could + also be fetched via `this.columnNames`, if the client provides + the `columnNames` option). If the callback returns a literal + `false` (as opposed to any other falsy value, e.g. an implicit + `undefined` return), any ongoing statement-`step()` iteration + stops without an error. The return value of the callback is + otherwise ignored. ACHTUNG: The callback MUST NOT modify the Stmt object. Calling any of the Stmt.get() variants, Stmt.getColumnName(), or @@ -733,7 +737,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ A.1) `'array'` (the default) causes the results of `stmt.get([])` to be passed to the `callback` and/or appended - to `resultRows` + to `resultRows`. A.2) `'object'` causes the results of `stmt.get(Object.create(null))` to be passed to the @@ -744,8 +748,8 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ A.3) `'stmt'` causes the current Stmt to be passed to the callback, but this mode will trigger an exception if - `resultRows` is an array because appending the statement to - the array would be downright unhelpful. + `resultRows` is an array because appending the transient + statement to the array would be downright unhelpful. B) An integer, indicating a zero-based column in the result row. Only that one single value will be passed on. @@ -857,14 +861,21 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ bind = null; } if(evalFirstResult && stmt.columnCount){ - /* Only forward SELECT results for the FIRST query + /* Only forward SELECT-style results for the FIRST query in the SQL which potentially has them. */ + let gotColNames = Array.isArray( + opt.columnNames + /* As reported in + https://sqlite.org/forum/forumpost/7774b773937cbe0a + we need to delay fetching of the column names until + after the first step() (if we step() at all) because + a schema change between the prepare() and step(), via + another connection, may invalidate the column count + and names. */) ? 0 : 1; evalFirstResult = false; - if(Array.isArray(opt.columnNames)){ - stmt.getColumnNames(opt.columnNames); - } if(arg.cbArg || resultRows){ for(; stmt.step(); stmt._isLocked = false){ + if(0===gotColNames++) stmt.getColumnNames(opt.columnNames); stmt._isLocked = true; const row = arg.cbArg(stmt); if(resultRows) resultRows.push(row); @@ -874,6 +885,10 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ } stmt._isLocked = false; } + if(0===gotColNames){ + /* opt.columnNames was provided but we visited no result rows */ + stmt.getColumnNames(opt.columnNames); + } }else{ stmt.step(); } @@ -1416,7 +1431,6 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ capi.sqlite3_finalize(this.pointer); __ptrMap.delete(this); delete this._mayGet; - delete this.columnCount; delete this.parameterCount; delete this.db; delete this._isLocked; @@ -1686,13 +1700,15 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ } if(Array.isArray(ndx)){ let i = 0; - while(i<this.columnCount){ + const n = this.columnCount; + while(i<n){ ndx[i] = this.get(i++); } return ndx; }else if(ndx && 'object'===typeof ndx){ let i = 0; - while(i<this.columnCount){ + const n = this.columnCount; + while(i<n){ ndx[capi.sqlite3_column_name(this.pointer,i)] = this.get(i++); } return ndx; @@ -1790,16 +1806,17 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ ); }, /** - If this statement potentially has result columns, this - function returns an array of all such names. If passed an - array, it is used as the target and all names are appended - to it. Returns the target array. Throws if this statement - cannot have result columns. This object's columnCount member - holds the number of columns. + If this statement potentially has result columns, this function + returns an array of all such names. If passed an array, it is + used as the target and all names are appended to it. Returns + the target array. Throws if this statement cannot have result + columns. This object's columnCount property holds the number of + columns. */ getColumnNames: function(tgt=[]){ affirmColIndex(affirmStmtOpen(this),0); - for(let i = 0; i < this.columnCount; ++i){ + const n = this.columnCount; + for(let i = 0; i < n; ++i){ tgt.push(capi.sqlite3_column_name(this.pointer, i)); } return tgt; @@ -1826,6 +1843,21 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ Object.defineProperty(Stmt.prototype, 'pointer', prop); Object.defineProperty(DB.prototype, 'pointer', prop); } + /** + A Stmt's columntCount property is an interceptor for + sqlite3_column_count(). + + This requires an unfortunate performance hit compared to caching + columnCount when the Stmt is created/prepared (as was done in + SQLite <=3.42.0), but is necessary in order to handle certain + corner cases, as described in + https://sqlite.org/forum/forumpost/7774b773937cbe0a. + */ + Object.defineProperty(Stmt.prototype, 'columnCount', { + enumerable: false, + get: function(){return capi.sqlite3_column_count(this.pointer)}, + set: ()=>toss3("The columnCount property is read-only.") + }); /** The OO API's public namespace. */ sqlite3.oo1 = { |