From 6eaa5c2433bc3d2b425213cb3614d45ffb526118 Mon Sep 17 00:00:00 2001 From: cpainter Date: Wed, 14 Oct 2020 18:23:35 -0600 Subject: [PATCH 01/12] Initial working version of statement iteration. --- Makefile | 3 +- src/api.js | 110 ++++++++++++++++++++++++++++++ src/exported_functions.json | 2 + src/exported_runtime_methods.json | 3 +- 4 files changed, 116 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 14e0e719..13d91b25 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,8 @@ CFLAGS = \ -DSQLITE_DISABLE_LFS \ -DSQLITE_ENABLE_FTS3 \ -DSQLITE_ENABLE_FTS3_PARENTHESIS \ - -DSQLITE_THREADSAFE=0 + -DSQLITE_THREADSAFE=0 \ + -DSQLITE_ENABLE_NORMALIZE # When compiling to WASM, enabling memory-growth is not expected to make much of an impact, so we enable it for all builds # Since tihs is a library and not a standalone executable, we don't want to catch unhandled Node process exceptions diff --git a/src/api.js b/src/api.js index 7113c312..1339fdb1 100644 --- a/src/api.js +++ b/src/api.js @@ -14,6 +14,7 @@ stackAlloc stackRestore stackSave + UTF8ToString */ "use strict"; @@ -80,6 +81,12 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { "number", ["number", "string", "number", "number", "number"] ); + var sqlite3_sql = cwrap("sqlite3_sql", "string", ["number"]); + var sqlite3_normalized_sql = cwrap( + "sqlite3_normalized_sql", + "string", + ["number"] + ); var sqlite3_prepare_v2_sqlptr = cwrap( "sqlite3_prepare_v2", "number", @@ -467,6 +474,21 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { return rowObject; }; + /** Get the SQL string used in preparing this statement. + @return {string} The SQL string + */ + Statement.prototype["getSQL"] = function getSQL() { + return sqlite3_sql(this.stmt); + }; + + /** Get the SQLite's normalized version of the SQL string used in + preparing this statement. + @return {string} The normalized SQL string + */ + Statement.prototype["getNormalizedSQL"] = function getNormalizedSQL() { + return sqlite3_normalized_sql(this.stmt); + }; + /** Shorthand for bind + step + reset Bind the values, execute the statement, ignoring the rows it returns, and resets it @@ -635,6 +657,53 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { }; + /** + * @classdesc + * Represents an iterator over multiple SQL statements in a string, + * preparing and returning a Statement object for the next SQL + * statement on each iteration. + * + * You can't instantiate this class directly, you have to use a + * {@link Database} object in order to create a statement. + * + * **Warning**: When you close a database (using db.close()), + * using any statement iterators created by the database will + * result in undefined behavior. + * + * StatementIterators can't be created by the API user directly, + * only by Database::iterateStatements + * + * @constructs StatementIterator + * @memberof module:SqlJs + * @param {string} sql A string containing multiple SQL statements + * @param {Database} db The database from which this iterator was created + */ + function StatementIterator(sql, db) { + this.nextSql = sql; + this.db = db; + // No SQL has been previously prepared at this time + this.lastSql = ""; + } + + /** + * @typedef {{ + value:Statement, + done:boolean + }} StatementIterator.StatementIteratorResult + * @property {Statement} the next available Statement + * (as returned by {@link Database.prepare}) + * @property {boolean} true if there are no more available statements + */ + + /** Prepare the next available SQL statement + @return {StatementIterator.StatementIteratorResult} + @throws {String} SQLite error + */ + StatementIterator.prototype["next"] = function next() { + return this.db["advanceIterator"](this); + }; + + /** @classdesc * Represents an SQLite database * @constructs Database @@ -880,6 +949,47 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { return stmt; }; + /** Iterate over multiple SQL statements in a SQL string + @param {string} sql a string of SQL + @return {StatementIterator} the resulting statement iterator + */ + Database.prototype["iterateStatements"] = function iterateStatements(sql) { + return new StatementIterator(sql, this); + }; + + /* Internal method to implement statement iteration */ + + Database.prototype["advanceIterator"] = function advanceIterator(iter) { + var stack = stackSave(); + var pzTail = stackAlloc(4); + setValue(apiTemp, 0, "i32"); + setValue(pzTail, 0, "i32"); + var returnCode = sqlite3_prepare_v2( + this.db, + iter.nextSql, + -1, + apiTemp, + pzTail + ); + var tail = UTF8ToString(getValue(pzTail, "i32")); + iter.lastSql = iter.nextSql.substr( + 0, + iter.nextSql.length - tail.length + ); + iter.nextSql = tail; + stackRestore(stack); + if (returnCode === SQLITE_OK) { + var pStmt = getValue(apiTemp, "i32"); + if (pStmt === NULL) { + return { value: null, done: true }; + } + var stmt = new Statement(pStmt, this.db); + return { value: stmt, done: false }; + } + var errmsg = sqlite3_errmsg(this.db); + throw new Error(errmsg); + }; + /** Exports the contents of the database to a binary array @return {Uint8Array} An array of bytes of the SQLite3 database file */ diff --git a/src/exported_functions.json b/src/exported_functions.json index af5085c7..b5467759 100644 --- a/src/exported_functions.json +++ b/src/exported_functions.json @@ -7,6 +7,8 @@ "_sqlite3_errmsg", "_sqlite3_changes", "_sqlite3_prepare_v2", +"_sqlite3_sql", +"_sqlite3_normalized_sql", "_sqlite3_bind_text", "_sqlite3_bind_blob", "_sqlite3_bind_double", diff --git a/src/exported_runtime_methods.json b/src/exported_runtime_methods.json index 644fd3ea..13a8efb8 100644 --- a/src/exported_runtime_methods.json +++ b/src/exported_runtime_methods.json @@ -2,5 +2,6 @@ "cwrap", "stackAlloc", "stackSave", -"stackRestore" +"stackRestore", +"UTF8ToString" ] From 3bbb200b8b7abbd0072149c0bc5053dee7214c12 Mon Sep 17 00:00:00 2001 From: cpainter Date: Wed, 14 Oct 2020 19:24:42 -0600 Subject: [PATCH 02/12] Tests written and running green. --- src/api.js | 3 +- test/test_statement_iterator.js | 61 +++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 test/test_statement_iterator.js diff --git a/src/api.js b/src/api.js index 1339fdb1..baf60bfa 100644 --- a/src/api.js +++ b/src/api.js @@ -983,7 +983,8 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { if (pStmt === NULL) { return { value: null, done: true }; } - var stmt = new Statement(pStmt, this.db); + var stmt = new Statement(pStmt, this); + this.statements[pStmt] = stmt; return { value: stmt, done: false }; } var errmsg = sqlite3_errmsg(this.db); diff --git a/test/test_statement_iterator.js b/test/test_statement_iterator.js new file mode 100644 index 00000000..2e697ba7 --- /dev/null +++ b/test/test_statement_iterator.js @@ -0,0 +1,61 @@ +exports.test = function(SQL, assert, done) { + // Create a database + var db = new SQL.Database(); + + // Multiline SQL, with comments + var sqlstr = "CREATE TABLE test (x text, y integer);\n" + + "INSERT INTO test -- here's a single line comment\n" + + "VALUES ('hello', 42), ('goodbye', 17);\n" + + "/* Here's a multiline \n" + + " comment */ \n" + + "SELECT * FROM test;\n" + + " -- nothing here"; + + // Get an iterator + var it = db.iterateStatements(sqlstr); + + // Get first item + var x = it.next(); + assert.equal(x.done, false, "Valid iterator object produced"); + assert.equal(x.value.getSQL(), "CREATE TABLE test (x text, y integer);", "Statement is for first query only"); + + // execute the first query + x.value.step(); + x.value.free() + + // get and execute the second query + x = it.next(); + assert.equal(x.done, false, "Second query found"); + x.value.step(); + x.value.free(); + + // get and execute the third query + x = it.next(); + assert.equal(x.done, false, "Third query found"); + x.value.step(); + assert.equal(x.value.get(), ['hello', 42], "Third query row results correct"); + x.value.free(); + + // check for additional queries + x = it.next(); + assert.equal(x.done, true, "No more queries reported"); + + x = it.next(); + assert.equal(x.done, true, "Advancing done iterator does nothing"); +}; + +if (module == require.main) { + const target_file = process.argv[2]; + const sql_loader = require('./load_sql_lib'); + sql_loader(target_file).then((sql)=>{ + require('test').run({ + 'test database': function(assert, done){ + exports.test(sql, assert, done); + } + }); + }) + .catch((e)=>{ + console.error(e); + assert.fail(e); + }); +} From be2c5319f3304cc88c7c65c32e9972d641e1a91f Mon Sep 17 00:00:00 2001 From: cpainter Date: Wed, 14 Oct 2020 19:54:10 -0600 Subject: [PATCH 03/12] Resolved linter issues. --- src/api.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/api.js b/src/api.js index 22d3ee6d..8cadda61 100644 --- a/src/api.js +++ b/src/api.js @@ -627,7 +627,6 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { return res; }; - /** * @classdesc * Represents an iterator over multiple SQL statements in a string, @@ -674,7 +673,6 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { return this.db["advanceIterator"](this); }; - /** @classdesc * Represents an SQLite database * @constructs Database From 0b4db07b87886f9443f0898fc7d5c23aea283fbf Mon Sep 17 00:00:00 2001 From: cpainter Date: Thu, 15 Oct 2020 23:06:55 -0600 Subject: [PATCH 04/12] Modified approach based on PR feedback; simple testing works, automated tests and documentation to be written. --- examples/repl.html | 4 +- src/api.js | 99 ++++++++++++++++++++++++--------- test/test_statement_iterator.js | 6 +- 3 files changed, 77 insertions(+), 32 deletions(-) diff --git a/examples/repl.html b/examples/repl.html index 61ef5b52..94ba39c7 100644 --- a/examples/repl.html +++ b/examples/repl.html @@ -5,7 +5,7 @@ SQL REPL - + @@ -31,4 +31,4 @@ document.getElementById('error').innerHTML = error; }; - \ No newline at end of file + diff --git a/src/api.js b/src/api.js index 8cadda61..195a3541 100644 --- a/src/api.js +++ b/src/api.js @@ -3,6 +3,7 @@ FS HEAP8 Module + _malloc _free addFunction allocate @@ -15,6 +16,8 @@ stackRestore stackSave UTF8ToString + stringToUTF8 + lengthBytesUTF8 */ "use strict"; @@ -461,7 +464,10 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { }; /** Get the SQLite's normalized version of the SQL string used in - preparing this statement. + preparing this statement. The meaning of "normlized" is not + well-defined: see {@link https://sqlite.org/c3ref/expanded_sql.html + the SQLite documentation}. + @return {string} The normalized SQL string */ Statement.prototype["getNormalizedSQL"] = function getNormalizedSQL() { @@ -629,30 +635,36 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { /** * @classdesc - * Represents an iterator over multiple SQL statements in a string, + * An iterator over multiple SQL statements in a string, * preparing and returning a Statement object for the next SQL * statement on each iteration. * * You can't instantiate this class directly, you have to use a - * {@link Database} object in order to create a statement. + * {@link Database} object in order to create a statement iterator + * + * {@see Database#iterateStatements} * * **Warning**: When you close a database (using db.close()), * using any statement iterators created by the database will * result in undefined behavior. * - * StatementIterators can't be created by the API user directly, - * only by Database::iterateStatements - * + * @example * @constructs StatementIterator * @memberof module:SqlJs * @param {string} sql A string containing multiple SQL statements * @param {Database} db The database from which this iterator was created */ function StatementIterator(sql, db) { - this.nextSql = sql; this.db = db; - // No SQL has been previously prepared at this time - this.lastSql = ""; + var sz = lengthBytesUTF8(sql) + 1; + this.sqlPtr = _malloc(sz); + if (this.sqlPtr === null) { + throw "Bad malloc"; + } + stringToUTF8(sql, this.sqlPtr, sz); + this.nextSqlPtr = this.sqlPtr; + this.nextSqlString = null; + this.activeStatement = null; } /** @@ -667,12 +679,30 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { /** Prepare the next available SQL statement @return {StatementIterator.StatementIteratorResult} - @throws {String} SQLite error + @throws {String} SQLite error or bad iterator error */ StatementIterator.prototype["next"] = function next() { - return this.db["advanceIterator"](this); + return this.db.advanceIterator(this); }; + /** Get any un-executed portions remaining of the original SQL string + @return {String} + */ + StatementIterator.prototype["getRemainingSql"] = function getRemainder() { + // iff an exception occurred, we set the nextSqlString + if (this.nextSqlString !== null) return this.nextSqlString; + // otherwise, convert from nextSqlPtr + return UTF8ToString(this.nextSqlPtr); + }; + + /* implement Iterable interface */ + + if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { + StatementIterator.prototype[Symbol.iterator] = function iterator() { + return this; + }; + } + /** @classdesc * Represents an SQLite database * @constructs Database @@ -920,38 +950,53 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { return new StatementIterator(sql, this); }; - /* Internal method to implement statement iteration */ + /* Internal methods to implement statement iteration */ - Database.prototype["advanceIterator"] = function advanceIterator(iter) { + Database.prototype.advanceIterator = function advanceIterator(iter) { + if (iter.sqlPtr === null) { + throw "Invalid iterator"; + } + if (iter.activeStatement !== null) { + iter.activeStatement["free"](); + iter.activeStatement = null; + } + if (!this.db) { + this.invalidateIterator(iter); + throw "Database closed"; + } var stack = stackSave(); var pzTail = stackAlloc(4); setValue(apiTemp, 0, "i32"); setValue(pzTail, 0, "i32"); - var returnCode = sqlite3_prepare_v2( + var returnCode = sqlite3_prepare_v2_sqlptr( this.db, - iter.nextSql, + iter.nextSqlPtr, -1, apiTemp, pzTail ); - var tail = UTF8ToString(getValue(pzTail, "i32")); - iter.lastSql = iter.nextSql.substr( - 0, - iter.nextSql.length - tail.length - ); - iter.nextSql = tail; + if (returnCode === SQLITE_OK) { + iter.nextSqlPtr = getValue(pzTail, "i32"); + } stackRestore(stack); if (returnCode === SQLITE_OK) { var pStmt = getValue(apiTemp, "i32"); if (pStmt === NULL) { - return { value: null, done: true }; + this.invalidateIterator(iter); + return { done: true }; } - var stmt = new Statement(pStmt, this); - this.statements[pStmt] = stmt; - return { value: stmt, done: false }; + iter.activeStatement = new Statement(pStmt, this); + this.statements[pStmt] = iter.activeStatement; + return { value: iter.activeStatement, done: false }; } - var errmsg = sqlite3_errmsg(this.db); - throw new Error(errmsg); + iter.nextSqlString = UTF8ToString(iter.nextSqlPtr); + this.invalidateIterator(iter); + return this.handleError(returnCode); + }; + + Database.prototype.invalidateIterator = function invalidateIterator(iter) { + _free(iter.sqlPtr); + iter.sqlPtr = null; }; /** Exports the contents of the database to a binary array diff --git a/test/test_statement_iterator.js b/test/test_statement_iterator.js index 2e697ba7..0d5c4407 100644 --- a/test/test_statement_iterator.js +++ b/test/test_statement_iterator.js @@ -1,4 +1,4 @@ -exports.test = function(SQL, assert, done) { +exports.test = function(SQL, assert) { // Create a database var db = new SQL.Database(); @@ -49,8 +49,8 @@ if (module == require.main) { const sql_loader = require('./load_sql_lib'); sql_loader(target_file).then((sql)=>{ require('test').run({ - 'test database': function(assert, done){ - exports.test(sql, assert, done); + 'test statement iterator': function(assert){ + exports.test(sql, assert); } }); }) From 022d1654fe0659d869e51aa5804aa259373a29df Mon Sep 17 00:00:00 2001 From: cpainter Date: Fri, 16 Oct 2020 07:13:47 -0600 Subject: [PATCH 05/12] Testing and documentation written. --- src/api.js | 30 +++++++++++++++---- test/test_statement_iterator.js | 53 +++++++++++++++++++++++++-------- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/src/api.js b/src/api.js index 195a3541..b99e1139 100644 --- a/src/api.js +++ b/src/api.js @@ -649,6 +649,26 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { * result in undefined behavior. * * @example + * // loop over and execute statements in string sql + * for (let statement of db.iterateStatements(sql) { + * statement.step(); + * // get results, etc. + * // do not call statement.free() + * } + * + * // capture any bad query exceptions with feedback + * // on the bad sql + * let it = db.iterateStatements(sql); + * try { + * for (let statement of it) { + * statement.step(); + * } + * } catch(e) { + * console.log(e); // error message + * console.log(" occurred while executing:"); + * console.log(it.getRemainingSQL()); + * } + * * @constructs StatementIterator * @memberof module:SqlJs * @param {string} sql A string containing multiple SQL statements @@ -688,7 +708,7 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { /** Get any un-executed portions remaining of the original SQL string @return {String} */ - StatementIterator.prototype["getRemainingSql"] = function getRemainder() { + StatementIterator.prototype["getRemainingSQL"] = function getRemainder() { // iff an exception occurred, we set the nextSqlString if (this.nextSqlString !== null) return this.nextSqlString; // otherwise, convert from nextSqlPtr @@ -961,7 +981,7 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { iter.activeStatement = null; } if (!this.db) { - this.invalidateIterator(iter); + this.finalizeIterator(iter); throw "Database closed"; } var stack = stackSave(); @@ -982,7 +1002,7 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { if (returnCode === SQLITE_OK) { var pStmt = getValue(apiTemp, "i32"); if (pStmt === NULL) { - this.invalidateIterator(iter); + this.finalizeIterator(iter); return { done: true }; } iter.activeStatement = new Statement(pStmt, this); @@ -990,11 +1010,11 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { return { value: iter.activeStatement, done: false }; } iter.nextSqlString = UTF8ToString(iter.nextSqlPtr); - this.invalidateIterator(iter); + this.finalizeIterator(iter); return this.handleError(returnCode); }; - Database.prototype.invalidateIterator = function invalidateIterator(iter) { + Database.prototype.finalizeIterator = function finalizeIterator(iter) { _free(iter.sqlPtr); iter.sqlPtr = null; }; diff --git a/test/test_statement_iterator.js b/test/test_statement_iterator.js index 0d5c4407..d55dea89 100644 --- a/test/test_statement_iterator.js +++ b/test/test_statement_iterator.js @@ -2,46 +2,75 @@ exports.test = function(SQL, assert) { // Create a database var db = new SQL.Database(); - // Multiline SQL, with comments + // Multiline SQL var sqlstr = "CREATE TABLE test (x text, y integer);\n" - + "INSERT INTO test -- here's a single line comment\n" + + "INSERT INTO test\n" + "VALUES ('hello', 42), ('goodbye', 17);\n" - + "/* Here's a multiline \n" - + " comment */ \n" + "SELECT * FROM test;\n" + " -- nothing here"; + var sqlstart = "CREATE TABLE test (x text, y integer);" + // Manual iteration // Get an iterator var it = db.iterateStatements(sqlstr); // Get first item var x = it.next(); assert.equal(x.done, false, "Valid iterator object produced"); - assert.equal(x.value.getSQL(), "CREATE TABLE test (x text, y integer);", "Statement is for first query only"); + assert.equal(x.value.getSQL(), sqlstart, "Statement is for first query only"); + assert.equal(it.getRemainingSQL(), sqlstr.slice(sqlstart.length), "Remaining sql retrievable"); // execute the first query x.value.step(); - x.value.free() // get and execute the second query x = it.next(); assert.equal(x.done, false, "Second query found"); x.value.step(); - x.value.free(); // get and execute the third query x = it.next(); assert.equal(x.done, false, "Third query found"); x.value.step(); - assert.equal(x.value.get(), ['hello', 42], "Third query row results correct"); - x.value.free(); + assert.deepEqual(x.value.getColumnNames(), ['x', 'y'], "Third query is SELECT"); // check for additional queries x = it.next(); - assert.equal(x.done, true, "No more queries reported"); + assert.equal(x.done, true, "Done reported after last query"); - x = it.next(); - assert.equal(x.done, true, "Advancing done iterator does nothing"); + // no more iterations should be allowed + assert.throws(function(){ it.next(); }, "Invalid iterator", "Cannot iterate past end"); + + db.run("DROP TABLE test;"); + + // for...of + var count = 0; + for (let statement of db.iterateStatements(sqlstr)) { + statement.step(); + count = count + 1; + } + assert.equal(count, 3, "For loop iterates correctly"); + + var badsql = "SELECT * FROM test;garbage in, garbage out"; + + // bad sql will stop iteration + assert.throws(function(){ + for (let statement of db.iterateStatements(badsql)) { + statement.step(); + } + }, "Bad SQL stops iteration"); + + // valid SQL executes, remaining SQL accessible after exception + it = db.iterateStatements(badsql); + var remains = ''; + try { + for (let statement of it) { + statement.step(); + } + } catch { + remains = it.getRemainingSQL(); + } + assert.equal(remains, "garbage in, garbage out", "Remaining SQL accessible after exception"); }; if (module == require.main) { From 2adbaa32733fe01dcc457d26eccd6798ca372ffb Mon Sep 17 00:00:00 2001 From: cpainter Date: Sun, 18 Oct 2020 15:52:03 -0600 Subject: [PATCH 06/12] Undid prior commit (accidentally committed change from sql-wasm.js to sql-wasm-debug.js) --- examples/repl.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/repl.html b/examples/repl.html index 94ba39c7..60b05154 100644 --- a/examples/repl.html +++ b/examples/repl.html @@ -5,7 +5,7 @@ SQL REPL - + From b8588d24511928d06055d9b10bb677299006921d Mon Sep 17 00:00:00 2001 From: cpainter Date: Sun, 18 Oct 2020 16:23:55 -0600 Subject: [PATCH 07/12] Applied all suggested modifications. --- src/api.js | 115 ++++++++++++++++---------------- test/test_statement_iterator.js | 20 +++--- 2 files changed, 69 insertions(+), 66 deletions(-) diff --git a/src/api.js b/src/api.js index b99e1139..db6822b5 100644 --- a/src/api.js +++ b/src/api.js @@ -464,10 +464,15 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { }; /** Get the SQLite's normalized version of the SQL string used in - preparing this statement. The meaning of "normlized" is not + preparing this statement. The meaning of "normalized" is not well-defined: see {@link https://sqlite.org/c3ref/expanded_sql.html the SQLite documentation}. + @example + db.run("create table test (x integer);"); + stmt = db.prepare("select * from test where x = 42"); + // returns "SELECT*FROM test WHERE x=?;" + @return {string} The normalized SQL string */ Statement.prototype["getNormalizedSQL"] = function getNormalizedSQL() { @@ -653,7 +658,8 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { * for (let statement of db.iterateStatements(sql) { * statement.step(); * // get results, etc. - * // do not call statement.free() + * // do not call statement.free() manually, each statement is freed + * // before the next one is parsed * } * * // capture any bad query exceptions with feedback @@ -664,9 +670,10 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { * statement.step(); * } * } catch(e) { - * console.log(e); // error message - * console.log(" occurred while executing:"); - * console.log(it.getRemainingSQL()); + * console.log( + * `The SQL string "${it.getRemainingSQL()}" ` + + * `contains the following error: ${e}` + * ); * } * * @constructs StatementIterator @@ -679,7 +686,7 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { var sz = lengthBytesUTF8(sql) + 1; this.sqlPtr = _malloc(sz); if (this.sqlPtr === null) { - throw "Bad malloc"; + throw new Error("Unable to allocate memory for the SQL string"); } stringToUTF8(sql, this.sqlPtr, sz); this.nextSqlPtr = this.sqlPtr; @@ -699,10 +706,53 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { /** Prepare the next available SQL statement @return {StatementIterator.StatementIteratorResult} - @throws {String} SQLite error or bad iterator error + @throws {String} SQLite error or invalid iterator error */ StatementIterator.prototype["next"] = function next() { - return this.db.advanceIterator(this); + if (this.sqlPtr === null) { + return { done: true }; + } + if (this.activeStatement !== null) { + this.activeStatement["free"](); + this.activeStatement = null; + } + if (!this.db.db) { + this.finalize(); + throw new Error("Database closed"); + } + var stack = stackSave(); + var pzTail = stackAlloc(4); + setValue(apiTemp, 0, "i32"); + setValue(pzTail, 0, "i32"); + try { + this.db.handleError(sqlite3_prepare_v2_sqlptr( + this.db.db, + this.nextSqlPtr, + -1, + apiTemp, + pzTail + )); + this.nextSqlPtr = getValue(pzTail, "i32"); + var pStmt = getValue(apiTemp, "i32"); + if (pStmt === NULL) { + this.finalize(); + return { done: true }; + } + this.activeStatement = new Statement(pStmt, this.db); + this.db.statements[pStmt] = this.activeStatement; + return { value: this.activeStatement, done: false }; + } catch (e) { + this.nextSqlString = UTF8ToString(this.nextSqlPtr); + this.finalize(); + throw e; + } finally { + stackRestore(stack); + } + }; + + StatementIterator.prototype.finalize = function finalize() { + _free(this.sqlPtr); + this.sqlPtr = null; }; /** Get any un-executed portions remaining of the original SQL string @@ -970,55 +1020,6 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { return new StatementIterator(sql, this); }; - /* Internal methods to implement statement iteration */ - - Database.prototype.advanceIterator = function advanceIterator(iter) { - if (iter.sqlPtr === null) { - throw "Invalid iterator"; - } - if (iter.activeStatement !== null) { - iter.activeStatement["free"](); - iter.activeStatement = null; - } - if (!this.db) { - this.finalizeIterator(iter); - throw "Database closed"; - } - var stack = stackSave(); - var pzTail = stackAlloc(4); - setValue(apiTemp, 0, "i32"); - setValue(pzTail, 0, "i32"); - var returnCode = sqlite3_prepare_v2_sqlptr( - this.db, - iter.nextSqlPtr, - -1, - apiTemp, - pzTail - ); - if (returnCode === SQLITE_OK) { - iter.nextSqlPtr = getValue(pzTail, "i32"); - } - stackRestore(stack); - if (returnCode === SQLITE_OK) { - var pStmt = getValue(apiTemp, "i32"); - if (pStmt === NULL) { - this.finalizeIterator(iter); - return { done: true }; - } - iter.activeStatement = new Statement(pStmt, this); - this.statements[pStmt] = iter.activeStatement; - return { value: iter.activeStatement, done: false }; - } - iter.nextSqlString = UTF8ToString(iter.nextSqlPtr); - this.finalizeIterator(iter); - return this.handleError(returnCode); - }; - - Database.prototype.finalizeIterator = function finalizeIterator(iter) { - _free(iter.sqlPtr); - iter.sqlPtr = null; - }; - /** Exports the contents of the database to a binary array @return {Uint8Array} An array of bytes of the SQLite3 database file */ diff --git a/test/test_statement_iterator.js b/test/test_statement_iterator.js index d55dea89..aa69e44d 100644 --- a/test/test_statement_iterator.js +++ b/test/test_statement_iterator.js @@ -36,10 +36,11 @@ exports.test = function(SQL, assert) { // check for additional queries x = it.next(); - assert.equal(x.done, true, "Done reported after last query"); + assert.deepEqual(x, { done: true }, "Done reported after last query"); - // no more iterations should be allowed - assert.throws(function(){ it.next(); }, "Invalid iterator", "Cannot iterate past end"); + // additional iteration does nothing + x = it.next(); + assert.deepEqual(x, { done: true }, "Done reported when iterating past completion"); db.run("DROP TABLE test;"); @@ -51,14 +52,15 @@ exports.test = function(SQL, assert) { } assert.equal(count, 3, "For loop iterates correctly"); - var badsql = "SELECT * FROM test;garbage in, garbage out"; + var badsql = "SELECT 1 as x;garbage in, garbage out"; // bad sql will stop iteration - assert.throws(function(){ - for (let statement of db.iterateStatements(badsql)) { - statement.step(); - } - }, "Bad SQL stops iteration"); + it = db.iterateStatements(badsql); + x = it.next(); + x.value.step(); + assert.deepEqual(x.value.getAsObject(), {x : 1}, "SQL before bad statement executes successfully"); + assert.throws(function() { it.next() }, /syntax error/, "Bad SQL stops iteration with exception"); + assert.deepEqual(it.next(), { done: true }, "Done reported when iterating after exception"); // valid SQL executes, remaining SQL accessible after exception it = db.iterateStatements(badsql); From 710f679c651e00b4461c62eef8f4b8eb025028ad Mon Sep 17 00:00:00 2001 From: cpainter Date: Mon, 19 Oct 2020 09:42:52 -0600 Subject: [PATCH 08/12] Documentation fixes. --- src/api.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/api.js b/src/api.js index db6822b5..eb4d5f4d 100644 --- a/src/api.js +++ b/src/api.js @@ -649,10 +649,6 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { * * {@see Database#iterateStatements} * - * **Warning**: When you close a database (using db.close()), - * using any statement iterators created by the database will - * result in undefined behavior. - * * @example * // loop over and execute statements in string sql * for (let statement of db.iterateStatements(sql) { @@ -695,10 +691,9 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { } /** - * @typedef {{ - value:Statement, - done:boolean - }} StatementIterator.StatementIteratorResult + * @typedef {{ done:false, value:undefined } | + * { done:false, value:Statement}} + * StatementIterator.StatementIteratorResult * @property {Statement} the next available Statement * (as returned by {@link Database.prepare}) * @property {boolean} true if there are no more available statements From f829a876770939285039cba550cb42c6d1373131 Mon Sep 17 00:00:00 2001 From: ophir Date: Mon, 19 Oct 2020 21:01:08 +0200 Subject: [PATCH 09/12] Improve the documentation of db#iterateStatements --- src/api.js | 19 ++++++++++++++++--- test/test_statement_iterator.js | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/api.js b/src/api.js index eb4d5f4d..45f6fc35 100644 --- a/src/api.js +++ b/src/api.js @@ -1007,9 +1007,22 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { return stmt; }; - /** Iterate over multiple SQL statements in a SQL string - @param {string} sql a string of SQL - @return {StatementIterator} the resulting statement iterator + /** Iterate over multiple SQL statements in a SQL string. + * This function returns an iterator over {@link Statement} objects. + * You can use a for..of loop to execute the returned statements one by one. + * @param {string} sql a string of SQL that can contain multiple statements + * @return {StatementIterator} the resulting statement iterator + * @example Get the results of multiple SQL queries + * const sql_queries = "SELECT 1 AS x; SELECT '2' as y"; + * for (const statement of db.iterateStatements(sql_queries)) { + * statement.step(); // Execute the statement + * const sql = statement.getSQL(); // Get the SQL source + * const result = statement.getAsObject(); // Get the row of data + * console.log(sql, result); + * } + * // This will print: + * // 'SELECT 1 AS x;' { x: 1 } + * // " SELECT '2' as y" { y: '2' } */ Database.prototype["iterateStatements"] = function iterateStatements(sql) { return new StatementIterator(sql, this); diff --git a/test/test_statement_iterator.js b/test/test_statement_iterator.js index aa69e44d..849c26c1 100644 --- a/test/test_statement_iterator.js +++ b/test/test_statement_iterator.js @@ -73,6 +73,21 @@ exports.test = function(SQL, assert) { remains = it.getRemainingSQL(); } assert.equal(remains, "garbage in, garbage out", "Remaining SQL accessible after exception"); + + // From the doc example on the iterateStatements method + const results = []; + const sql_queries = "SELECT 1 AS x; SELECT '2' as y"; + for (const statement of db.iterateStatements(sql_queries)) { + statement.step(); // Fetch one line of result from the statement + const sql = statement.getSQL(); + const result = statement.getAsObject(); + results.push({sql, result}); + } + console.log(results); + assert.deepEqual(results, [ + { sql: 'SELECT 1 AS x;', result: { x: 1 } }, + { sql: " SELECT '2' as y", result: { y: '2' } } + ], "The code example from the documentation works"); }; if (module == require.main) { From 2d7902e74b10c9e591feed4450a0795ed8212e9e Mon Sep 17 00:00:00 2001 From: ophir Date: Mon, 19 Oct 2020 21:10:07 +0200 Subject: [PATCH 10/12] Add @implements annotations for StatementIterator --- src/api.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api.js b/src/api.js index 45f6fc35..415d49ac 100644 --- a/src/api.js +++ b/src/api.js @@ -672,6 +672,8 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { * ); * } * + * @implements {Iterator} + * @implements {Iterable} * @constructs StatementIterator * @memberof module:SqlJs * @param {string} sql A string containing multiple SQL statements From d9392c13fbfcba6bc4ad6592b6240065bd0cc482 Mon Sep 17 00:00:00 2001 From: ophir Date: Mon, 19 Oct 2020 21:10:41 +0200 Subject: [PATCH 11/12] Reformat test code --- test/test_statement_iterator.js | 42 ++++++++++++++++----------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/test/test_statement_iterator.js b/test/test_statement_iterator.js index 849c26c1..582d254e 100644 --- a/test/test_statement_iterator.js +++ b/test/test_statement_iterator.js @@ -1,4 +1,4 @@ -exports.test = function(SQL, assert) { +exports.test = function (SQL, assert) { // Create a database var db = new SQL.Database(); @@ -47,8 +47,8 @@ exports.test = function(SQL, assert) { // for...of var count = 0; for (let statement of db.iterateStatements(sqlstr)) { - statement.step(); - count = count + 1; + statement.step(); + count = count + 1; } assert.equal(count, 3, "For loop iterates correctly"); @@ -58,19 +58,19 @@ exports.test = function(SQL, assert) { it = db.iterateStatements(badsql); x = it.next(); x.value.step(); - assert.deepEqual(x.value.getAsObject(), {x : 1}, "SQL before bad statement executes successfully"); - assert.throws(function() { it.next() }, /syntax error/, "Bad SQL stops iteration with exception"); + assert.deepEqual(x.value.getAsObject(), { x: 1 }, "SQL before bad statement executes successfully"); + assert.throws(function () { it.next() }, /syntax error/, "Bad SQL stops iteration with exception"); assert.deepEqual(it.next(), { done: true }, "Done reported when iterating after exception"); // valid SQL executes, remaining SQL accessible after exception it = db.iterateStatements(badsql); var remains = ''; try { - for (let statement of it) { - statement.step(); - } + for (let statement of it) { + statement.step(); + } } catch { - remains = it.getRemainingSQL(); + remains = it.getRemainingSQL(); } assert.equal(remains, "garbage in, garbage out", "Remaining SQL accessible after exception"); @@ -78,11 +78,11 @@ exports.test = function(SQL, assert) { const results = []; const sql_queries = "SELECT 1 AS x; SELECT '2' as y"; for (const statement of db.iterateStatements(sql_queries)) { - statement.step(); // Fetch one line of result from the statement - const sql = statement.getSQL(); - const result = statement.getAsObject(); - results.push({sql, result}); - } + statement.step(); // Fetch one line of result from the statement + const sql = statement.getSQL(); + const result = statement.getAsObject(); + results.push({ sql, result }); + } console.log(results); assert.deepEqual(results, [ { sql: 'SELECT 1 AS x;', result: { x: 1 } }, @@ -91,17 +91,17 @@ exports.test = function(SQL, assert) { }; if (module == require.main) { - const target_file = process.argv[2]; + const target_file = process.argv[2]; const sql_loader = require('./load_sql_lib'); - sql_loader(target_file).then((sql)=>{ + sql_loader(target_file).then((sql) => { require('test').run({ - 'test statement iterator': function(assert){ + 'test statement iterator': function (assert) { exports.test(sql, assert); } }); }) - .catch((e)=>{ - console.error(e); - assert.fail(e); - }); + .catch((e) => { + console.error(e); + assert.fail(e); + }); } From dbc9edcca4ec2b314ae94a1df69f8049f1b12bad Mon Sep 17 00:00:00 2001 From: ophir Date: Mon, 19 Oct 2020 21:17:08 +0200 Subject: [PATCH 12/12] Fix the type definition of StatementIterator.StatementIteratorResult --- src/api.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api.js b/src/api.js index 415d49ac..8d4aade5 100644 --- a/src/api.js +++ b/src/api.js @@ -693,12 +693,12 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { } /** - * @typedef {{ done:false, value:undefined } | + * @typedef {{ done:true, value:undefined } | * { done:false, value:Statement}} * StatementIterator.StatementIteratorResult - * @property {Statement} the next available Statement + * @property {Statement} value the next available Statement * (as returned by {@link Database.prepare}) - * @property {boolean} true if there are no more available statements + * @property {boolean} done true if there are no more available statements */ /** Prepare the next available SQL statement