diff --git a/Makefile b/Makefile index a5e6614a..287de6fd 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/examples/simple.html b/examples/simple.html index af841ea6..3d175b43 100644 --- a/examples/simple.html +++ b/examples/simple.html @@ -35,4 +35,4 @@ Output is in Javscript console - \ No newline at end of file + diff --git a/src/api.js b/src/api.js index 27125c21..17b3b13c 100644 --- a/src/api.js +++ b/src/api.js @@ -14,6 +14,7 @@ stackAlloc stackRestore stackSave + UTF8ToString */ "use strict"; @@ -85,6 +86,12 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { "number", ["number", "number", "number", "number", "number"] ); + var sqlite3_sql = cwrap("sqlite3_sql", "string", ["number"]); + var sqlite3_normalized_sql = cwrap( + "sqlite3_normalized_sql", + "string", + ["number"] + ); var sqlite3_bind_text = cwrap( "sqlite3_bind_text", "number", @@ -683,8 +690,8 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { * and {@link Statement.free}. * * The result is an array of result elements. There are as many result - * elements as the number of statements in your sql string (statements are - * separated by a semicolon) + * elements as the number of statements which return at least one row + * in your sql string (statements are separated by a semicolon) * * ## Example use * We will create the following table, named *test* and query it with a @@ -695,7 +702,7 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { * | 1 | 1 | Ling | * | 2 | 18 | Paul | * - * We query it like that: + * We query it like this: * ```javascript * var db = new SQL.Database(); * var res = db.exec( @@ -780,6 +787,217 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() { } }; + /** Execute multiple queries, with detailed results for each. + * Queries may not use parameters. + * + * The result is an array of objects of varying structure. There are as + * many result elements as the number of statements in your sql string + * (statements are separated by a semicolon). + * + * Result objects may contain the following members: + * - success: true/false (all queries) + * - error: error message if success is false + * - columns: column headers, if query returns a result + * (even if empty) + * - values: query results, if query returns a + * result (even if empty) + * - rowsModified: rows affected, if query is INSERT, UPDATE, or DELETE + * - sql: the SQL executed for this result (if no error) + * + * ## Example use + * We will create the following table, named *test* and query it with a + * multi-line statement: + * + * | id | age | name | + * |:--:|:---:|:------:| + * | 1 | 1 | Ling | + * | 2 | 18 | Paul | + * + * We query it like this: + * ```javascript + * var db = new SQL.Database(); + * var res = db.execMany( + * "DROP TABLE IF EXISTS test;" + * + "CREATE TABLE test (id INTEGER, age INTEGER, name TEXT);" + * + "INSERT INTO test VALUES (1, 1, 'Ling');" + * + "INSERT INTO test VALUES (2, 18, 'Paul');" + * + "SELECT id FROM test;" + * + "SELECT age, name FROM test WHERE id = 1;" + * + "SELECT age, name FROM test WHERE id = 4;" + * + "INSERT INTO test (blah, foo) VALUES ('hello', 42);" + * + "DELETE FROM test;" + * ); + * ``` + * + * `res` is now : + * ```javascript + * [ + * {"success": true, "sql": "DROP TABLE IF EXISTS test;"}, + * { + * "success": true, + * "sql": + * "CREATE TABLE test (id INTEGER, age INTEGER, name TEXT);" + * }, + * { + * "success": true, + * "rowsModified": 1 + * "sql": "INSERT INTO test VALUES (1, 1, 'Ling');" + * }, + * { + * "success": true, + * "rowsModified": 1 + * "sql": "INSERT INTO test VALUES (2, 18, 'Paul');" + * }, + * { + * "success": true, + * "columns": ["id"], + * "values": [[1],[2]]}, + * "sql": "SELECT id FROM test;" + * }, + * { + * "success": true, + * "columns": ["age","name"], + * "values" :[[1,"Ling"]]}, + * "sql": "SELECT age, name FROM test WHERE id = 1;" + * }, + * { + * "success": true, + * "columns": ["age","name"], + * "values":[] + * "sql": "SELECT age, name FROM test WHERE id = 4;" + * }, + * { + * "success": false, + * "error": "table test has no column named blah", + * "sql": "INSERT INTO test (blah, foo) VALUES ('hello', 42);" + * }, + * {"success": true, "rowsModified": 2, "sql": "DELETE FROM test;"} + * ] + * ``` + * + @param {string} sql a string containing some SQL text to execute + @param {boolean} exitOnError whether or not to continue after error + @return {Object[]} as described above + */ + Database.prototype["execMany"] = function execMany(sql, exitOnError) { + var answer = []; + var result; + var data; + var columns; + var stack; + var lastSql; + var nextSql; + var thisSql; + var normalizedSql; + var sqlType; + var stmt; + var pStmt; + var pzTail; + var returnCode; + var errorMessage; + var errorAnswer; + + if (!this.db) { + throw "Database closed"; + } + + stack = stackSave(); + + nextSql = sql; + pzTail = stackAlloc(4); + + // eslint-disable-next-line no-constant-condition + while (true) { + setValue(apiTemp, 0, "i32"); + setValue(pzTail, 0, "i32"); + returnCode = sqlite3_prepare_v2( + this.db, + nextSql, + -1, + apiTemp, + pzTail + ); + lastSql = nextSql; + nextSql = UTF8ToString(getValue(pzTail, "i32")); + if (returnCode !== SQLITE_OK) { + errorMessage = sqlite3_errmsg(this.db); + + // there's no valid statement pointer, so we have to do + // a hack to discover the most recent SQL statement: + thisSql = lastSql.substr(0, lastSql.length - nextSql.length); + + errorAnswer = {}; + errorAnswer["success"] = false; + errorAnswer["error"] = errorMessage; + errorAnswer["sql"] = thisSql; + answer.push(errorAnswer); + + if (exitOnError) { + stackRestore(stack); + return answer; + } + // eslint-disable-next-line no-continue + continue; + } + + // pointer to a statement, or NULL if nothing there + pStmt = getValue(apiTemp, "i32"); + if (pStmt === NULL) break; + + // get most recent sql statement + thisSql = sqlite3_sql(pStmt); + + // wrap in a Statement so we can use existing methods + stmt = new Statement(pStmt, this); + + // get column headers, if any + columns = stmt["getColumnNames"](); + data = []; + try { + while (stmt["step"]()) { + data.push(stmt["get"]()); + } + } catch (e) { + errorAnswer = {}; + errorAnswer["success"] = false; + errorAnswer["error"] = e.toString(); + errorAnswer["sql"] = thisSql; + answer.push(errorAnswer); + if (exitOnError) { + stackRestore(stack); + return answer; + } + // eslint-disable-next-line no-continue + continue; + } + + result = {}; + result["success"] = true; + result["sql"] = thisSql; + + if (columns.length > 0) { + result["columns"] = columns; + result["values"] = data; + } else { + // bit of a kludge: determine if last + // query was modification query + normalizedSql = sqlite3_normalized_sql(pStmt); + sqlType = normalizedSql.trim().substr(0, 6).toLowerCase(); + if (sqlType === "insert" + || sqlType === "update" + || sqlType === "delete") { + result["rowsModified"] = this["getRowsModified"](); + } + } + answer.push(result); + + // clean up + stmt["free"](); + } + stackRestore(stack); + return answer; + }; + /** Execute an sql statement, and call a callback for each row of result. Currently this method is synchronous, it will not return until the callback diff --git a/src/exported_functions.json b/src/exported_functions.json index b6882dfe..b93b07d2 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" ] diff --git a/test/test_exec_many.js b/test/test_exec_many.js new file mode 100644 index 00000000..143bf435 --- /dev/null +++ b/test/test_exec_many.js @@ -0,0 +1,192 @@ +exports.test = function(sql, assert){ + // Create a database + var db = new sql.Database(); + + // test DDL + var result = db.execMany("DROP TABLE IF EXISTS test;"); + assert.deepEqual( + result, + [{ + "success": true, + "sql": "DROP TABLE IF EXISTS test;" + }], + "DDL: DROP" + ); + + result = db.execMany("CREATE TABLE test (id INTEGER, age INTEGER, name TEXT)"); + assert.deepEqual( + result, + [{ + "success": true, + "sql": "CREATE TABLE test (id INTEGER, age INTEGER, name TEXT)" + }], + "DDL: CREATE" + ); + + // test modification queries + result = db.execMany("INSERT INTO test VALUES (1, 1, 'Ling')"); + assert.deepEqual( + result, + [{ + "success": true, + "rowsModified": 1, + "sql": "INSERT INTO test VALUES (1, 1, 'Ling')" + }], + "INSERT 1" + ); + + result = db.execMany("INSERT INTO test VALUES (2, 10, 'Wendy'), (3, 7, 'Jeff')"); + assert.deepEqual( + result, + [{ + "success": true, + "rowsModified": 2, + "sql": "INSERT INTO test VALUES (2, 10, 'Wendy'), (3, 7, 'Jeff')" + }], + "INSERT many" + ); + + result = db.execMany("UPDATE test SET age = age + 1"); + assert.deepEqual( + result, + [{ + "success": true, + "rowsModified": 3, + "sql": "UPDATE test SET age = age + 1" + }], + "UPDATE" + ); + + result = db.execMany("DELETE FROM TEST WHERE name = 'Priya'"); + assert.deepEqual( + result, + [{ + "success": true, + "rowsModified": 0, + "sql": "DELETE FROM TEST WHERE name = 'Priya'" + }], + "DELETE" + ); + + // SELECT queries + result = db.execMany("SELECT * FROM test"); + assert.deepEqual( + result, + [{ + "success": true, + "columns": ["id", "age", "name"], + "values": [[1, 2, "Ling"], [2, 11, "Wendy"], [3, 8, "Jeff"]], + "sql": "SELECT * FROM test" + }], + "SELECT many" + ); + + result = db.execMany("SELECT * FROM test WHERE name = 'Priya'"); + assert.deepEqual( + result, + [{ + "success": true, + "columns": ["id", "age", "name"], + "values": [], + "sql": "SELECT * FROM test WHERE name = 'Priya'" + }], + "SELECT 0" + ); + + // test errors in query + result = db.execMany("INSERT INTO test (blah, foo) VALUES ('hello', 42);"); + assert.deepEqual( + result, + [{ + "success": false, + "error": "table test has no column named blah", + "sql": "INSERT INTO test (blah, foo) VALUES ('hello', 42);" + }], + "Error result: INSERT" + ); + + result = db.execMany("SELECT blah FROM test"); + assert.deepEqual( + result, + [{ + "success": false, + "error": "no such column: blah", + "sql": "SELECT blah FROM test" + }], + "Error result: SELECT" + ); + + // test multiple statement and stop/no stop on error + + result = db.execMany( + "INSERT INTO test VALUES (4, 17, 'Priya');" + + "SELECT blah FROM test;" + + "DELETE FROM test", + true + ); + assert.deepEqual( + result, + [{ + "success": true, + "rowsModified": 1, + "sql": "INSERT INTO test VALUES (4, 17, 'Priya');" + }, + { + "success": false, + "error": "no such column: blah", + "sql": "SELECT blah FROM test;" + }], + "Multiple statements with stop on error" + ); + + result = db.execMany( + "INSERT INTO test VALUES (5, 9, 'Azam');" + + "SELECT blah FROM test;" + + "DELETE FROM test;" + + "SELECT id FROM test" + ); + assert.deepEqual( + result, + [{ + "success": true, + "rowsModified": 1, + "sql": "INSERT INTO test VALUES (5, 9, 'Azam');" + }, + { + "success": false, + "error": "no such column: blah", + "sql": "SELECT blah FROM test;" + }, + { + "success": true, + "rowsModified": 5, + "sql": "DELETE FROM test;" + }, + { + "success": true, + "columns": ["id"], + "values": [], + "sql": "SELECT id FROM test" + }], + "Multiple statements with no stop on error" + ); + + // Close the database + db.close(); +}; + +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 statement': function(assert){ + exports.test(sql, assert); + } + }); + }) + .catch((e)=>{ + console.error(e); + assert.fail(e); + }); +}