Skip to content

Commit e20bb74

Browse files
Add db.iterateStatements (sql-js#429)
db.iterateStatements allows giving sql.js an SQL string and iterating over statements objects created from that string * Initial working version of statement iteration. * Tests written and running green. * Resolved linter issues. * Modified approach based on PR feedback; simple testing works, automated tests and documentation to be written. * Testing and documentation written. * Undid prior commit (accidentally committed change from sql-wasm.js to sql-wasm-debug.js) * Applied all suggested modifications. * Documentation fixes. * Improve the documentation of db#iterateStatements * Add @implements annotations for StatementIterator * Reformat test code * Fix the type definition of StatementIterator.StatementIteratorResult Co-authored-by: ophir <[email protected]>
1 parent 0cfeaef commit e20bb74

6 files changed

+300
-3
lines changed

Makefile

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ CFLAGS = \
2323
-DSQLITE_DISABLE_LFS \
2424
-DSQLITE_ENABLE_FTS3 \
2525
-DSQLITE_ENABLE_FTS3_PARENTHESIS \
26-
-DSQLITE_THREADSAFE=0
26+
-DSQLITE_THREADSAFE=0 \
27+
-DSQLITE_ENABLE_NORMALIZE
2728

2829
# When compiling to WASM, enabling memory-growth is not expected to make much of an impact, so we enable it for all builds
2930
# Since tihs is a library and not a standalone executable, we don't want to catch unhandled Node process exceptions

examples/repl.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@
3131
document.getElementById('error').innerHTML = error;
3232
};
3333
</script>
34-
</body>
34+
</body>

src/api.js

+186
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
FS
44
HEAP8
55
Module
6+
_malloc
67
_free
78
addFunction
89
allocate
@@ -14,6 +15,9 @@
1415
stackAlloc
1516
stackRestore
1617
stackSave
18+
UTF8ToString
19+
stringToUTF8
20+
lengthBytesUTF8
1721
*/
1822

1923
"use strict";
@@ -80,6 +84,12 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
8084
"number",
8185
["number", "string", "number", "number", "number"]
8286
);
87+
var sqlite3_sql = cwrap("sqlite3_sql", "string", ["number"]);
88+
var sqlite3_normalized_sql = cwrap(
89+
"sqlite3_normalized_sql",
90+
"string",
91+
["number"]
92+
);
8393
var sqlite3_prepare_v2_sqlptr = cwrap(
8494
"sqlite3_prepare_v2",
8595
"number",
@@ -446,6 +456,29 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
446456
return rowObject;
447457
};
448458

459+
/** Get the SQL string used in preparing this statement.
460+
@return {string} The SQL string
461+
*/
462+
Statement.prototype["getSQL"] = function getSQL() {
463+
return sqlite3_sql(this.stmt);
464+
};
465+
466+
/** Get the SQLite's normalized version of the SQL string used in
467+
preparing this statement. The meaning of "normalized" is not
468+
well-defined: see {@link https://sqlite.org/c3ref/expanded_sql.html
469+
the SQLite documentation}.
470+
471+
@example
472+
db.run("create table test (x integer);");
473+
stmt = db.prepare("select * from test where x = 42");
474+
// returns "SELECT*FROM test WHERE x=?;"
475+
476+
@return {string} The normalized SQL string
477+
*/
478+
Statement.prototype["getNormalizedSQL"] = function getNormalizedSQL() {
479+
return sqlite3_normalized_sql(this.stmt);
480+
};
481+
449482
/** Shorthand for bind + step + reset
450483
Bind the values, execute the statement, ignoring the rows it returns,
451484
and resets it
@@ -605,6 +638,138 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
605638
return res;
606639
};
607640

641+
/**
642+
* @classdesc
643+
* An iterator over multiple SQL statements in a string,
644+
* preparing and returning a Statement object for the next SQL
645+
* statement on each iteration.
646+
*
647+
* You can't instantiate this class directly, you have to use a
648+
* {@link Database} object in order to create a statement iterator
649+
*
650+
* {@see Database#iterateStatements}
651+
*
652+
* @example
653+
* // loop over and execute statements in string sql
654+
* for (let statement of db.iterateStatements(sql) {
655+
* statement.step();
656+
* // get results, etc.
657+
* // do not call statement.free() manually, each statement is freed
658+
* // before the next one is parsed
659+
* }
660+
*
661+
* // capture any bad query exceptions with feedback
662+
* // on the bad sql
663+
* let it = db.iterateStatements(sql);
664+
* try {
665+
* for (let statement of it) {
666+
* statement.step();
667+
* }
668+
* } catch(e) {
669+
* console.log(
670+
* `The SQL string "${it.getRemainingSQL()}" ` +
671+
* `contains the following error: ${e}`
672+
* );
673+
* }
674+
*
675+
* @implements {Iterator<Statement>}
676+
* @implements {Iterable<Statement>}
677+
* @constructs StatementIterator
678+
* @memberof module:SqlJs
679+
* @param {string} sql A string containing multiple SQL statements
680+
* @param {Database} db The database from which this iterator was created
681+
*/
682+
function StatementIterator(sql, db) {
683+
this.db = db;
684+
var sz = lengthBytesUTF8(sql) + 1;
685+
this.sqlPtr = _malloc(sz);
686+
if (this.sqlPtr === null) {
687+
throw new Error("Unable to allocate memory for the SQL string");
688+
}
689+
stringToUTF8(sql, this.sqlPtr, sz);
690+
this.nextSqlPtr = this.sqlPtr;
691+
this.nextSqlString = null;
692+
this.activeStatement = null;
693+
}
694+
695+
/**
696+
* @typedef {{ done:true, value:undefined } |
697+
* { done:false, value:Statement}}
698+
* StatementIterator.StatementIteratorResult
699+
* @property {Statement} value the next available Statement
700+
* (as returned by {@link Database.prepare})
701+
* @property {boolean} done true if there are no more available statements
702+
*/
703+
704+
/** Prepare the next available SQL statement
705+
@return {StatementIterator.StatementIteratorResult}
706+
@throws {String} SQLite error or invalid iterator error
707+
*/
708+
StatementIterator.prototype["next"] = function next() {
709+
if (this.sqlPtr === null) {
710+
return { done: true };
711+
}
712+
if (this.activeStatement !== null) {
713+
this.activeStatement["free"]();
714+
this.activeStatement = null;
715+
}
716+
if (!this.db.db) {
717+
this.finalize();
718+
throw new Error("Database closed");
719+
}
720+
var stack = stackSave();
721+
var pzTail = stackAlloc(4);
722+
setValue(apiTemp, 0, "i32");
723+
setValue(pzTail, 0, "i32");
724+
try {
725+
this.db.handleError(sqlite3_prepare_v2_sqlptr(
726+
this.db.db,
727+
this.nextSqlPtr,
728+
-1,
729+
apiTemp,
730+
pzTail
731+
));
732+
this.nextSqlPtr = getValue(pzTail, "i32");
733+
var pStmt = getValue(apiTemp, "i32");
734+
if (pStmt === NULL) {
735+
this.finalize();
736+
return { done: true };
737+
}
738+
this.activeStatement = new Statement(pStmt, this.db);
739+
this.db.statements[pStmt] = this.activeStatement;
740+
return { value: this.activeStatement, done: false };
741+
} catch (e) {
742+
this.nextSqlString = UTF8ToString(this.nextSqlPtr);
743+
this.finalize();
744+
throw e;
745+
} finally {
746+
stackRestore(stack);
747+
}
748+
};
749+
750+
StatementIterator.prototype.finalize = function finalize() {
751+
_free(this.sqlPtr);
752+
this.sqlPtr = null;
753+
};
754+
755+
/** Get any un-executed portions remaining of the original SQL string
756+
@return {String}
757+
*/
758+
StatementIterator.prototype["getRemainingSQL"] = function getRemainder() {
759+
// iff an exception occurred, we set the nextSqlString
760+
if (this.nextSqlString !== null) return this.nextSqlString;
761+
// otherwise, convert from nextSqlPtr
762+
return UTF8ToString(this.nextSqlPtr);
763+
};
764+
765+
/* implement Iterable interface */
766+
767+
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
768+
StatementIterator.prototype[Symbol.iterator] = function iterator() {
769+
return this;
770+
};
771+
}
772+
608773
/** @classdesc
609774
* Represents an SQLite database
610775
* @constructs Database
@@ -844,6 +1009,27 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
8441009
return stmt;
8451010
};
8461011

1012+
/** Iterate over multiple SQL statements in a SQL string.
1013+
* This function returns an iterator over {@link Statement} objects.
1014+
* You can use a for..of loop to execute the returned statements one by one.
1015+
* @param {string} sql a string of SQL that can contain multiple statements
1016+
* @return {StatementIterator} the resulting statement iterator
1017+
* @example <caption>Get the results of multiple SQL queries</caption>
1018+
* const sql_queries = "SELECT 1 AS x; SELECT '2' as y";
1019+
* for (const statement of db.iterateStatements(sql_queries)) {
1020+
* statement.step(); // Execute the statement
1021+
* const sql = statement.getSQL(); // Get the SQL source
1022+
* const result = statement.getAsObject(); // Get the row of data
1023+
* console.log(sql, result);
1024+
* }
1025+
* // This will print:
1026+
* // 'SELECT 1 AS x;' { x: 1 }
1027+
* // " SELECT '2' as y" { y: '2' }
1028+
*/
1029+
Database.prototype["iterateStatements"] = function iterateStatements(sql) {
1030+
return new StatementIterator(sql, this);
1031+
};
1032+
8471033
/** Exports the contents of the database to a binary array
8481034
@return {Uint8Array} An array of bytes of the SQLite3 database file
8491035
*/

src/exported_functions.json

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
"_sqlite3_errmsg",
88
"_sqlite3_changes",
99
"_sqlite3_prepare_v2",
10+
"_sqlite3_sql",
11+
"_sqlite3_normalized_sql",
1012
"_sqlite3_bind_text",
1113
"_sqlite3_bind_blob",
1214
"_sqlite3_bind_double",

src/exported_runtime_methods.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
"cwrap",
33
"stackAlloc",
44
"stackSave",
5-
"stackRestore"
5+
"stackRestore",
6+
"UTF8ToString"
67
]

test/test_statement_iterator.js

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
exports.test = function (SQL, assert) {
2+
// Create a database
3+
var db = new SQL.Database();
4+
5+
// Multiline SQL
6+
var sqlstr = "CREATE TABLE test (x text, y integer);\n"
7+
+ "INSERT INTO test\n"
8+
+ "VALUES ('hello', 42), ('goodbye', 17);\n"
9+
+ "SELECT * FROM test;\n"
10+
+ " -- nothing here";
11+
var sqlstart = "CREATE TABLE test (x text, y integer);"
12+
13+
// Manual iteration
14+
// Get an iterator
15+
var it = db.iterateStatements(sqlstr);
16+
17+
// Get first item
18+
var x = it.next();
19+
assert.equal(x.done, false, "Valid iterator object produced");
20+
assert.equal(x.value.getSQL(), sqlstart, "Statement is for first query only");
21+
assert.equal(it.getRemainingSQL(), sqlstr.slice(sqlstart.length), "Remaining sql retrievable");
22+
23+
// execute the first query
24+
x.value.step();
25+
26+
// get and execute the second query
27+
x = it.next();
28+
assert.equal(x.done, false, "Second query found");
29+
x.value.step();
30+
31+
// get and execute the third query
32+
x = it.next();
33+
assert.equal(x.done, false, "Third query found");
34+
x.value.step();
35+
assert.deepEqual(x.value.getColumnNames(), ['x', 'y'], "Third query is SELECT");
36+
37+
// check for additional queries
38+
x = it.next();
39+
assert.deepEqual(x, { done: true }, "Done reported after last query");
40+
41+
// additional iteration does nothing
42+
x = it.next();
43+
assert.deepEqual(x, { done: true }, "Done reported when iterating past completion");
44+
45+
db.run("DROP TABLE test;");
46+
47+
// for...of
48+
var count = 0;
49+
for (let statement of db.iterateStatements(sqlstr)) {
50+
statement.step();
51+
count = count + 1;
52+
}
53+
assert.equal(count, 3, "For loop iterates correctly");
54+
55+
var badsql = "SELECT 1 as x;garbage in, garbage out";
56+
57+
// bad sql will stop iteration
58+
it = db.iterateStatements(badsql);
59+
x = it.next();
60+
x.value.step();
61+
assert.deepEqual(x.value.getAsObject(), { x: 1 }, "SQL before bad statement executes successfully");
62+
assert.throws(function () { it.next() }, /syntax error/, "Bad SQL stops iteration with exception");
63+
assert.deepEqual(it.next(), { done: true }, "Done reported when iterating after exception");
64+
65+
// valid SQL executes, remaining SQL accessible after exception
66+
it = db.iterateStatements(badsql);
67+
var remains = '';
68+
try {
69+
for (let statement of it) {
70+
statement.step();
71+
}
72+
} catch {
73+
remains = it.getRemainingSQL();
74+
}
75+
assert.equal(remains, "garbage in, garbage out", "Remaining SQL accessible after exception");
76+
77+
// From the doc example on the iterateStatements method
78+
const results = [];
79+
const sql_queries = "SELECT 1 AS x; SELECT '2' as y";
80+
for (const statement of db.iterateStatements(sql_queries)) {
81+
statement.step(); // Fetch one line of result from the statement
82+
const sql = statement.getSQL();
83+
const result = statement.getAsObject();
84+
results.push({ sql, result });
85+
}
86+
console.log(results);
87+
assert.deepEqual(results, [
88+
{ sql: 'SELECT 1 AS x;', result: { x: 1 } },
89+
{ sql: " SELECT '2' as y", result: { y: '2' } }
90+
], "The code example from the documentation works");
91+
};
92+
93+
if (module == require.main) {
94+
const target_file = process.argv[2];
95+
const sql_loader = require('./load_sql_lib');
96+
sql_loader(target_file).then((sql) => {
97+
require('test').run({
98+
'test statement iterator': function (assert) {
99+
exports.test(sql, assert);
100+
}
101+
});
102+
})
103+
.catch((e) => {
104+
console.error(e);
105+
assert.fail(e);
106+
});
107+
}

0 commit comments

Comments
 (0)