michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: * http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: michael@0: "use strict"; michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu} = Components; michael@0: michael@0: do_get_profile(); michael@0: michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: Cu.import("resource://gre/modules/osfile.jsm"); michael@0: Cu.import("resource://gre/modules/FileUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/Sqlite.jsm"); michael@0: Cu.import("resource://gre/modules/Task.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: // To spin the event loop in test. michael@0: Cu.import("resource://services-common/async.js"); michael@0: michael@0: function sleep(ms) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: let timer = Cc["@mozilla.org/timer;1"] michael@0: .createInstance(Ci.nsITimer); michael@0: michael@0: timer.initWithCallback({ michael@0: notify: function () { michael@0: deferred.resolve(); michael@0: }, michael@0: }, ms, timer.TYPE_ONE_SHOT); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function getConnection(dbName, extraOptions={}) { michael@0: let path = dbName + ".sqlite"; michael@0: let options = {path: path}; michael@0: for (let [k, v] in Iterator(extraOptions)) { michael@0: options[k] = v; michael@0: } michael@0: michael@0: return Sqlite.openConnection(options); michael@0: } michael@0: michael@0: function getDummyDatabase(name, extraOptions={}) { michael@0: const TABLES = { michael@0: dirs: "id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT", michael@0: files: "id INTEGER PRIMARY KEY AUTOINCREMENT, dir_id INTEGER, path TEXT", michael@0: }; michael@0: michael@0: let c = yield getConnection(name, extraOptions); michael@0: c._initialStatementCount = 0; michael@0: michael@0: for (let [k, v] in Iterator(TABLES)) { michael@0: yield c.execute("CREATE TABLE " + k + "(" + v + ")"); michael@0: c._initialStatementCount++; michael@0: } michael@0: michael@0: throw new Task.Result(c); michael@0: } michael@0: michael@0: function getDummyTempDatabase(name, extraOptions={}) { michael@0: const TABLES = { michael@0: dirs: "id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT", michael@0: files: "id INTEGER PRIMARY KEY AUTOINCREMENT, dir_id INTEGER, path TEXT", michael@0: }; michael@0: michael@0: let c = yield getConnection(name, extraOptions); michael@0: c._initialStatementCount = 0; michael@0: michael@0: for (let [k, v] in Iterator(TABLES)) { michael@0: yield c.execute("CREATE TEMP TABLE " + k + "(" + v + ")"); michael@0: c._initialStatementCount++; michael@0: } michael@0: michael@0: throw new Task.Result(c); michael@0: } michael@0: michael@0: function run_test() { michael@0: Cu.import("resource://testing-common/services-common/logging.js"); michael@0: initTestLogging("Trace"); michael@0: michael@0: run_next_test(); michael@0: } michael@0: michael@0: add_task(function test_open_normal() { michael@0: let c = yield Sqlite.openConnection({path: "test_open_normal.sqlite"}); michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_open_unshared() { michael@0: let path = OS.Path.join(OS.Constants.Path.profileDir, "test_open_unshared.sqlite"); michael@0: michael@0: let c = yield Sqlite.openConnection({path: path, sharedMemoryCache: false}); michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_get_dummy_database() { michael@0: let db = yield getDummyDatabase("get_dummy_database"); michael@0: michael@0: do_check_eq(typeof(db), "object"); michael@0: yield db.close(); michael@0: }); michael@0: michael@0: add_task(function test_schema_version() { michael@0: let db = yield getDummyDatabase("schema_version"); michael@0: michael@0: let version = yield db.getSchemaVersion(); michael@0: do_check_eq(version, 0); michael@0: michael@0: db.setSchemaVersion(14); michael@0: version = yield db.getSchemaVersion(); michael@0: do_check_eq(version, 14); michael@0: michael@0: for (let v of [0.5, "foobar", NaN]) { michael@0: let success; michael@0: try { michael@0: yield db.setSchemaVersion(v); michael@0: do_print("Schema version " + v + " should have been rejected"); michael@0: success = false; michael@0: } catch (ex if ex.message.startsWith("Schema version must be an integer.")) { michael@0: success = true; michael@0: } michael@0: do_check_true(success); michael@0: michael@0: version = yield db.getSchemaVersion(); michael@0: do_check_eq(version, 14); michael@0: } michael@0: michael@0: yield db.close(); michael@0: }); michael@0: michael@0: add_task(function test_simple_insert() { michael@0: let c = yield getDummyDatabase("simple_insert"); michael@0: michael@0: let result = yield c.execute("INSERT INTO dirs VALUES (NULL, 'foo')"); michael@0: do_check_true(Array.isArray(result)); michael@0: do_check_eq(result.length, 0); michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_simple_bound_array() { michael@0: let c = yield getDummyDatabase("simple_bound_array"); michael@0: michael@0: let result = yield c.execute("INSERT INTO dirs VALUES (?, ?)", [1, "foo"]); michael@0: do_check_eq(result.length, 0); michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_simple_bound_object() { michael@0: let c = yield getDummyDatabase("simple_bound_object"); michael@0: let result = yield c.execute("INSERT INTO dirs VALUES (:id, :path)", michael@0: {id: 1, path: "foo"}); michael@0: do_check_eq(result.length, 0); michael@0: result = yield c.execute("SELECT id, path FROM dirs"); michael@0: do_check_eq(result.length, 1); michael@0: do_check_eq(result[0].getResultByName("id"), 1); michael@0: do_check_eq(result[0].getResultByName("path"), "foo"); michael@0: yield c.close(); michael@0: }); michael@0: michael@0: // This is mostly a sanity test to ensure simple executions work. michael@0: add_task(function test_simple_insert_then_select() { michael@0: let c = yield getDummyDatabase("simple_insert_then_select"); michael@0: michael@0: yield c.execute("INSERT INTO dirs VALUES (NULL, 'foo')"); michael@0: yield c.execute("INSERT INTO dirs (path) VALUES (?)", ["bar"]); michael@0: michael@0: let result = yield c.execute("SELECT * FROM dirs"); michael@0: do_check_eq(result.length, 2); michael@0: michael@0: let i = 0; michael@0: for (let row of result) { michael@0: i++; michael@0: michael@0: do_check_eq(row.numEntries, 2); michael@0: do_check_eq(row.getResultByIndex(0), i); michael@0: michael@0: let expected = {1: "foo", 2: "bar"}[i]; michael@0: do_check_eq(row.getResultByName("path"), expected); michael@0: } michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_repeat_execution() { michael@0: let c = yield getDummyDatabase("repeat_execution"); michael@0: michael@0: let sql = "INSERT INTO dirs (path) VALUES (:path)"; michael@0: yield c.executeCached(sql, {path: "foo"}); michael@0: yield c.executeCached(sql); michael@0: michael@0: let result = yield c.execute("SELECT * FROM dirs"); michael@0: michael@0: do_check_eq(result.length, 2); michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_table_exists() { michael@0: let c = yield getDummyDatabase("table_exists"); michael@0: michael@0: do_check_false(yield c.tableExists("does_not_exist")); michael@0: do_check_true(yield c.tableExists("dirs")); michael@0: do_check_true(yield c.tableExists("files")); michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_index_exists() { michael@0: let c = yield getDummyDatabase("index_exists"); michael@0: michael@0: do_check_false(yield c.indexExists("does_not_exist")); michael@0: michael@0: yield c.execute("CREATE INDEX my_index ON dirs (path)"); michael@0: do_check_true(yield c.indexExists("my_index")); michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_temp_table_exists() { michael@0: let c = yield getDummyTempDatabase("temp_table_exists"); michael@0: michael@0: do_check_false(yield c.tableExists("temp_does_not_exist")); michael@0: do_check_true(yield c.tableExists("dirs")); michael@0: do_check_true(yield c.tableExists("files")); michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_temp_index_exists() { michael@0: let c = yield getDummyTempDatabase("temp_index_exists"); michael@0: michael@0: do_check_false(yield c.indexExists("temp_does_not_exist")); michael@0: michael@0: yield c.execute("CREATE INDEX my_index ON dirs (path)"); michael@0: do_check_true(yield c.indexExists("my_index")); michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_close_cached() { michael@0: let c = yield getDummyDatabase("close_cached"); michael@0: michael@0: yield c.executeCached("SELECT * FROM dirs"); michael@0: yield c.executeCached("SELECT * FROM files"); michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_execute_invalid_statement() { michael@0: let c = yield getDummyDatabase("invalid_statement"); michael@0: michael@0: let deferred = Promise.defer(); michael@0: michael@0: do_check_eq(c._anonymousStatements.size, 0); michael@0: michael@0: c.execute("SELECT invalid FROM unknown").then(do_throw, function onError(error) { michael@0: deferred.resolve(); michael@0: }); michael@0: michael@0: yield deferred.promise; michael@0: michael@0: // Ensure we don't leak the statement instance. michael@0: do_check_eq(c._anonymousStatements.size, 0); michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_on_row_exception_ignored() { michael@0: let c = yield getDummyDatabase("on_row_exception_ignored"); michael@0: michael@0: let sql = "INSERT INTO dirs (path) VALUES (?)"; michael@0: for (let i = 0; i < 10; i++) { michael@0: yield c.executeCached(sql, ["dir" + i]); michael@0: } michael@0: michael@0: let i = 0; michael@0: yield c.execute("SELECT * FROM DIRS", null, function onRow(row) { michael@0: i++; michael@0: michael@0: throw new Error("Some silly error."); michael@0: }); michael@0: michael@0: do_check_eq(i, 10); michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: // Ensure StopIteration during onRow causes processing to stop. michael@0: add_task(function test_on_row_stop_iteration() { michael@0: let c = yield getDummyDatabase("on_row_stop_iteration"); michael@0: michael@0: let sql = "INSERT INTO dirs (path) VALUES (?)"; michael@0: for (let i = 0; i < 10; i++) { michael@0: yield c.executeCached(sql, ["dir" + i]); michael@0: } michael@0: michael@0: let i = 0; michael@0: let result = yield c.execute("SELECT * FROM dirs", null, function onRow(row) { michael@0: i++; michael@0: michael@0: if (i == 5) { michael@0: throw StopIteration; michael@0: } michael@0: }); michael@0: michael@0: do_check_null(result); michael@0: do_check_eq(i, 5); michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_invalid_transaction_type() { michael@0: let c = yield getDummyDatabase("invalid_transaction_type"); michael@0: michael@0: let errored = false; michael@0: try { michael@0: c.executeTransaction(function () {}, "foobar"); michael@0: } catch (ex) { michael@0: errored = true; michael@0: do_check_true(ex.message.startsWith("Unknown transaction type")); michael@0: } finally { michael@0: do_check_true(errored); michael@0: } michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_execute_transaction_success() { michael@0: let c = yield getDummyDatabase("execute_transaction_success"); michael@0: michael@0: do_check_false(c.transactionInProgress); michael@0: michael@0: yield c.executeTransaction(function transaction(conn) { michael@0: do_check_eq(c, conn); michael@0: do_check_true(conn.transactionInProgress); michael@0: michael@0: yield conn.execute("INSERT INTO dirs (path) VALUES ('foo')"); michael@0: }); michael@0: michael@0: do_check_false(c.transactionInProgress); michael@0: let rows = yield c.execute("SELECT * FROM dirs"); michael@0: do_check_true(Array.isArray(rows)); michael@0: do_check_eq(rows.length, 1); michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_execute_transaction_rollback() { michael@0: let c = yield getDummyDatabase("execute_transaction_rollback"); michael@0: michael@0: let deferred = Promise.defer(); michael@0: michael@0: c.executeTransaction(function transaction(conn) { michael@0: yield conn.execute("INSERT INTO dirs (path) VALUES ('foo')"); michael@0: print("Expecting error with next statement."); michael@0: yield conn.execute("INSERT INTO invalid VALUES ('foo')"); michael@0: michael@0: // We should never get here. michael@0: do_throw(); michael@0: }).then(do_throw, function onError(error) { michael@0: deferred.resolve(); michael@0: }); michael@0: michael@0: yield deferred.promise; michael@0: michael@0: let rows = yield c.execute("SELECT * FROM dirs"); michael@0: do_check_eq(rows.length, 0); michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_close_during_transaction() { michael@0: let c = yield getDummyDatabase("close_during_transaction"); michael@0: michael@0: yield c.execute("INSERT INTO dirs (path) VALUES ('foo')"); michael@0: michael@0: let errored = false; michael@0: try { michael@0: yield c.executeTransaction(function transaction(conn) { michael@0: yield c.execute("INSERT INTO dirs (path) VALUES ('bar')"); michael@0: yield c.close(); michael@0: }); michael@0: } catch (ex) { michael@0: errored = true; michael@0: do_check_eq(ex.message, "Connection being closed."); michael@0: } finally { michael@0: do_check_true(errored); michael@0: } michael@0: michael@0: let c2 = yield getConnection("close_during_transaction"); michael@0: let rows = yield c2.execute("SELECT * FROM dirs"); michael@0: do_check_eq(rows.length, 1); michael@0: michael@0: yield c2.close(); michael@0: }); michael@0: michael@0: add_task(function test_detect_multiple_transactions() { michael@0: let c = yield getDummyDatabase("detect_multiple_transactions"); michael@0: michael@0: yield c.executeTransaction(function main() { michael@0: yield c.execute("INSERT INTO dirs (path) VALUES ('foo')"); michael@0: michael@0: let errored = false; michael@0: try { michael@0: yield c.executeTransaction(function child() { michael@0: yield c.execute("INSERT INTO dirs (path) VALUES ('bar')"); michael@0: }); michael@0: } catch (ex) { michael@0: errored = true; michael@0: do_check_true(ex.message.startsWith("A transaction is already active.")); michael@0: } finally { michael@0: do_check_true(errored); michael@0: } michael@0: }); michael@0: michael@0: let rows = yield c.execute("SELECT * FROM dirs"); michael@0: do_check_eq(rows.length, 1); michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_shrink_memory() { michael@0: let c = yield getDummyDatabase("shrink_memory"); michael@0: michael@0: // It's just a simple sanity test. We have no way of measuring whether this michael@0: // actually does anything. michael@0: michael@0: yield c.shrinkMemory(); michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_no_shrink_on_init() { michael@0: let c = yield getConnection("no_shrink_on_init", michael@0: {shrinkMemoryOnConnectionIdleMS: 200}); michael@0: michael@0: let oldShrink = c.shrinkMemory; michael@0: let count = 0; michael@0: Object.defineProperty(c, "shrinkMemory", { michael@0: value: function () { michael@0: count++; michael@0: }, michael@0: }); michael@0: michael@0: // We should not shrink until a statement has been executed. michael@0: yield sleep(220); michael@0: do_check_eq(count, 0); michael@0: michael@0: yield c.execute("SELECT 1"); michael@0: yield sleep(220); michael@0: do_check_eq(count, 1); michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_idle_shrink_fires() { michael@0: let c = yield getDummyDatabase("idle_shrink_fires", michael@0: {shrinkMemoryOnConnectionIdleMS: 200}); michael@0: c._clearIdleShrinkTimer(); michael@0: michael@0: let oldShrink = c.shrinkMemory; michael@0: let shrinkPromises = []; michael@0: michael@0: let count = 0; michael@0: Object.defineProperty(c, "shrinkMemory", { michael@0: value: function () { michael@0: count++; michael@0: let promise = oldShrink.call(c); michael@0: shrinkPromises.push(promise); michael@0: return promise; michael@0: }, michael@0: }); michael@0: michael@0: // We reset the idle shrink timer after monkeypatching because otherwise the michael@0: // installed timer callback will reference the non-monkeypatched function. michael@0: c._startIdleShrinkTimer(); michael@0: michael@0: yield sleep(220); michael@0: do_check_eq(count, 1); michael@0: do_check_eq(shrinkPromises.length, 1); michael@0: yield shrinkPromises[0]; michael@0: shrinkPromises.shift(); michael@0: michael@0: // We shouldn't shrink again unless a statement was executed. michael@0: yield sleep(300); michael@0: do_check_eq(count, 1); michael@0: michael@0: yield c.execute("SELECT 1"); michael@0: yield sleep(300); michael@0: michael@0: do_check_eq(count, 2); michael@0: do_check_eq(shrinkPromises.length, 1); michael@0: yield shrinkPromises[0]; michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_idle_shrink_reset_on_operation() { michael@0: const INTERVAL = 500; michael@0: let c = yield getDummyDatabase("idle_shrink_reset_on_operation", michael@0: {shrinkMemoryOnConnectionIdleMS: INTERVAL}); michael@0: michael@0: c._clearIdleShrinkTimer(); michael@0: michael@0: let oldShrink = c.shrinkMemory; michael@0: let shrinkPromises = []; michael@0: let count = 0; michael@0: michael@0: Object.defineProperty(c, "shrinkMemory", { michael@0: value: function () { michael@0: count++; michael@0: let promise = oldShrink.call(c); michael@0: shrinkPromises.push(promise); michael@0: return promise; michael@0: }, michael@0: }); michael@0: michael@0: let now = new Date(); michael@0: c._startIdleShrinkTimer(); michael@0: michael@0: let initialIdle = new Date(now.getTime() + INTERVAL); michael@0: michael@0: // Perform database operations until initial scheduled time has been passed. michael@0: let i = 0; michael@0: while (new Date() < initialIdle) { michael@0: yield c.execute("INSERT INTO dirs (path) VALUES (?)", ["" + i]); michael@0: i++; michael@0: } michael@0: michael@0: do_check_true(i > 0); michael@0: michael@0: // We should not have performed an idle while doing operations. michael@0: do_check_eq(count, 0); michael@0: michael@0: // Wait for idle timer. michael@0: yield sleep(INTERVAL); michael@0: michael@0: // Ensure we fired. michael@0: do_check_eq(count, 1); michael@0: do_check_eq(shrinkPromises.length, 1); michael@0: yield shrinkPromises[0]; michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_in_progress_counts() { michael@0: let c = yield getDummyDatabase("in_progress_counts"); michael@0: do_check_eq(c._statementCounter, c._initialStatementCount); michael@0: do_check_eq(c._pendingStatements.size, 0); michael@0: yield c.executeCached("INSERT INTO dirs (path) VALUES ('foo')"); michael@0: do_check_eq(c._statementCounter, c._initialStatementCount + 1); michael@0: do_check_eq(c._pendingStatements.size, 0); michael@0: michael@0: let expectOne; michael@0: let expectTwo; michael@0: michael@0: // Please forgive me. michael@0: let inner = Async.makeSpinningCallback(); michael@0: let outer = Async.makeSpinningCallback(); michael@0: michael@0: // We want to make sure that two queries executing simultaneously michael@0: // result in `_pendingStatements.size` reaching 2, then dropping back to 0. michael@0: // michael@0: // To do so, we kick off a second statement within the row handler michael@0: // of the first, then wait for both to finish. michael@0: michael@0: yield c.executeCached("SELECT * from dirs", null, function onRow() { michael@0: // In the onRow handler, we're still an outstanding query. michael@0: // Expect a single in-progress entry. michael@0: expectOne = c._pendingStatements.size; michael@0: michael@0: // Start another query, checking that after its statement has been created michael@0: // there are two statements in progress. michael@0: let p = c.executeCached("SELECT 10, path from dirs"); michael@0: expectTwo = c._pendingStatements.size; michael@0: michael@0: // Now wait for it to be done before we return from the row handler … michael@0: p.then(function onInner() { michael@0: inner(); michael@0: }); michael@0: }).then(function onOuter() { michael@0: // … and wait for the inner to be done before we finish … michael@0: inner.wait(); michael@0: outer(); michael@0: }); michael@0: michael@0: // … and wait for both queries to have finished before we go on and michael@0: // test postconditions. michael@0: outer.wait(); michael@0: michael@0: do_check_eq(expectOne, 1); michael@0: do_check_eq(expectTwo, 2); michael@0: do_check_eq(c._statementCounter, c._initialStatementCount + 3); michael@0: do_check_eq(c._pendingStatements.size, 0); michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_discard_while_active() { michael@0: let c = yield getDummyDatabase("discard_while_active"); michael@0: michael@0: yield c.executeCached("INSERT INTO dirs (path) VALUES ('foo')"); michael@0: yield c.executeCached("INSERT INTO dirs (path) VALUES ('bar')"); michael@0: michael@0: let discarded = -1; michael@0: let first = true; michael@0: let sql = "SELECT * FROM dirs"; michael@0: yield c.executeCached(sql, null, function onRow(row) { michael@0: if (!first) { michael@0: return; michael@0: } michael@0: first = false; michael@0: discarded = c.discardCachedStatements(); michael@0: }); michael@0: michael@0: // We discarded everything, because the SELECT had already started to run. michael@0: do_check_eq(3, discarded); michael@0: michael@0: // And again is safe. michael@0: do_check_eq(0, c.discardCachedStatements()); michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_discard_cached() { michael@0: let c = yield getDummyDatabase("discard_cached"); michael@0: michael@0: yield c.executeCached("SELECT * from dirs"); michael@0: do_check_eq(1, c._cachedStatements.size); michael@0: michael@0: yield c.executeCached("SELECT * from files"); michael@0: do_check_eq(2, c._cachedStatements.size); michael@0: michael@0: yield c.executeCached("SELECT * from dirs"); michael@0: do_check_eq(2, c._cachedStatements.size); michael@0: michael@0: c.discardCachedStatements(); michael@0: do_check_eq(0, c._cachedStatements.size); michael@0: michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_programmatic_binding() { michael@0: let c = yield getDummyDatabase("programmatic_binding"); michael@0: michael@0: let bindings = [ michael@0: {id: 1, path: "foobar"}, michael@0: {id: null, path: "baznoo"}, michael@0: {id: 5, path: "toofoo"}, michael@0: ]; michael@0: michael@0: let sql = "INSERT INTO dirs VALUES (:id, :path)"; michael@0: let result = yield c.execute(sql, bindings); michael@0: do_check_eq(result.length, 0); michael@0: michael@0: let rows = yield c.executeCached("SELECT * from dirs"); michael@0: do_check_eq(rows.length, 3); michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_programmatic_binding_transaction() { michael@0: let c = yield getDummyDatabase("programmatic_binding_transaction"); michael@0: michael@0: let bindings = [ michael@0: {id: 1, path: "foobar"}, michael@0: {id: null, path: "baznoo"}, michael@0: {id: 5, path: "toofoo"}, michael@0: ]; michael@0: michael@0: let sql = "INSERT INTO dirs VALUES (:id, :path)"; michael@0: yield c.executeTransaction(function transaction() { michael@0: let result = yield c.execute(sql, bindings); michael@0: do_check_eq(result.length, 0); michael@0: michael@0: let rows = yield c.executeCached("SELECT * from dirs"); michael@0: do_check_eq(rows.length, 3); michael@0: }); michael@0: michael@0: // Transaction committed. michael@0: let rows = yield c.executeCached("SELECT * from dirs"); michael@0: do_check_eq(rows.length, 3); michael@0: yield c.close(); michael@0: }); michael@0: michael@0: add_task(function test_programmatic_binding_transaction_partial_rollback() { michael@0: let c = yield getDummyDatabase("programmatic_binding_transaction_partial_rollback"); michael@0: michael@0: let bindings = [ michael@0: {id: 2, path: "foobar"}, michael@0: {id: 3, path: "toofoo"}, michael@0: ]; michael@0: michael@0: let sql = "INSERT INTO dirs VALUES (:id, :path)"; michael@0: michael@0: // Add some data in an implicit transaction before beginning the batch insert. michael@0: yield c.execute(sql, {id: 1, path: "works"}); michael@0: michael@0: let secondSucceeded = false; michael@0: try { michael@0: yield c.executeTransaction(function transaction() { michael@0: // Insert one row. This won't implicitly start a transaction. michael@0: let result = yield c.execute(sql, bindings[0]); michael@0: michael@0: // Insert multiple rows. mozStorage will want to start a transaction. michael@0: // One of the inserts will fail, so the transaction should be rolled back. michael@0: let result = yield c.execute(sql, bindings); michael@0: secondSucceeded = true; michael@0: }); michael@0: } catch (ex) { michael@0: print("Caught expected exception: " + ex); michael@0: } michael@0: michael@0: // We did not get to the end of our in-transaction block. michael@0: do_check_false(secondSucceeded); michael@0: michael@0: // Everything that happened in *our* transaction, not mozStorage's, got michael@0: // rolled back, but the first row still exists. michael@0: let rows = yield c.executeCached("SELECT * from dirs"); michael@0: do_check_eq(rows.length, 1); michael@0: do_check_eq(rows[0].getResultByName("path"), "works"); michael@0: yield c.close(); michael@0: }); michael@0: michael@0: /** michael@0: * Just like the previous test, but relying on the implicit michael@0: * transaction established by mozStorage. michael@0: */ michael@0: add_task(function test_programmatic_binding_implicit_transaction() { michael@0: let c = yield getDummyDatabase("programmatic_binding_implicit_transaction"); michael@0: michael@0: let bindings = [ michael@0: {id: 2, path: "foobar"}, michael@0: {id: 1, path: "toofoo"}, michael@0: ]; michael@0: michael@0: let sql = "INSERT INTO dirs VALUES (:id, :path)"; michael@0: let secondSucceeded = false; michael@0: yield c.execute(sql, {id: 1, path: "works"}); michael@0: try { michael@0: let result = yield c.execute(sql, bindings); michael@0: secondSucceeded = true; michael@0: } catch (ex) { michael@0: print("Caught expected exception: " + ex); michael@0: } michael@0: michael@0: do_check_false(secondSucceeded); michael@0: michael@0: // The entire batch failed. michael@0: let rows = yield c.executeCached("SELECT * from dirs"); michael@0: do_check_eq(rows.length, 1); michael@0: do_check_eq(rows[0].getResultByName("path"), "works"); michael@0: yield c.close(); michael@0: }); michael@0: michael@0: /** michael@0: * Test that direct binding of params and execution through mozStorage doesn't michael@0: * error when we manually create a transaction. See Bug 856925. michael@0: */ michael@0: add_task(function test_direct() { michael@0: let file = FileUtils.getFile("TmpD", ["test_direct.sqlite"]); michael@0: file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); michael@0: print("Opening " + file.path); michael@0: michael@0: let db = Services.storage.openDatabase(file); michael@0: print("Opened " + db); michael@0: michael@0: db.executeSimpleSQL("CREATE TABLE types (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, UNIQUE (name))"); michael@0: print("Executed setup."); michael@0: michael@0: let statement = db.createAsyncStatement("INSERT INTO types (name) VALUES (:name)"); michael@0: let params = statement.newBindingParamsArray(); michael@0: let one = params.newBindingParams(); michael@0: one.bindByName("name", null); michael@0: params.addParams(one); michael@0: let two = params.newBindingParams(); michael@0: two.bindByName("name", "bar"); michael@0: params.addParams(two); michael@0: michael@0: print("Beginning transaction."); michael@0: let begin = db.createAsyncStatement("BEGIN DEFERRED TRANSACTION"); michael@0: let end = db.createAsyncStatement("COMMIT TRANSACTION"); michael@0: michael@0: let deferred = Promise.defer(); michael@0: begin.executeAsync({ michael@0: handleCompletion: function (reason) { michael@0: deferred.resolve(); michael@0: } michael@0: }); michael@0: yield deferred.promise; michael@0: michael@0: statement.bindParameters(params); michael@0: michael@0: deferred = Promise.defer(); michael@0: print("Executing async."); michael@0: statement.executeAsync({ michael@0: handleResult: function (resultSet) { michael@0: }, michael@0: michael@0: handleError: function (error) { michael@0: print("Error when executing SQL (" + error.result + "): " + michael@0: error.message); michael@0: print("Original error: " + error.error); michael@0: errors.push(error); michael@0: deferred.reject(); michael@0: }, michael@0: michael@0: handleCompletion: function (reason) { michael@0: print("Completed."); michael@0: deferred.resolve(); michael@0: } michael@0: }); michael@0: michael@0: yield deferred.promise; michael@0: michael@0: deferred = Promise.defer(); michael@0: end.executeAsync({ michael@0: handleCompletion: function (reason) { michael@0: deferred.resolve(); michael@0: } michael@0: }); michael@0: yield deferred.promise; michael@0: michael@0: statement.finalize(); michael@0: begin.finalize(); michael@0: end.finalize(); michael@0: michael@0: deferred = Promise.defer(); michael@0: db.asyncClose(function () { michael@0: deferred.resolve() michael@0: }); michael@0: yield deferred.promise; michael@0: }); michael@0: michael@0: /** michael@0: * Test Sqlite.cloneStorageConnection. michael@0: */ michael@0: add_task(function* test_cloneStorageConnection() { michael@0: let file = new FileUtils.File(OS.Path.join(OS.Constants.Path.profileDir, michael@0: "test_cloneStorageConnection.sqlite")); michael@0: let c = yield new Promise((success, failure) => { michael@0: Services.storage.openAsyncDatabase(file, null, (status, db) => { michael@0: if (Components.isSuccessCode(status)) { michael@0: success(db.QueryInterface(Ci.mozIStorageAsyncConnection)); michael@0: } else { michael@0: failure(new Error(status)); michael@0: } michael@0: }); michael@0: }); michael@0: michael@0: let clone = yield Sqlite.cloneStorageConnection({ connection: c, readOnly: true }); michael@0: // Just check that it works. michael@0: yield clone.execute("SELECT 1"); michael@0: michael@0: let clone2 = yield Sqlite.cloneStorageConnection({ connection: c, readOnly: false }); michael@0: // Just check that it works. michael@0: yield clone2.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)"); michael@0: michael@0: // Closing order should not matter. michael@0: yield c.asyncClose(); michael@0: yield clone2.close(); michael@0: yield clone.close(); michael@0: }); michael@0: michael@0: /** michael@0: * Test Sqlite.cloneStorageConnection invalid argument. michael@0: */ michael@0: add_task(function* test_cloneStorageConnection() { michael@0: try { michael@0: let clone = yield Sqlite.cloneStorageConnection({ connection: null }); michael@0: do_throw(new Error("Should throw on invalid connection")); michael@0: } catch (ex if ex.name == "TypeError") {} michael@0: }); michael@0: michael@0: /** michael@0: * Test clone() method. michael@0: */ michael@0: add_task(function* test_clone() { michael@0: let c = yield getDummyDatabase("clone"); michael@0: michael@0: let clone = yield c.clone(); michael@0: // Just check that it works. michael@0: yield clone.execute("SELECT 1"); michael@0: // Closing order should not matter. michael@0: yield c.close(); michael@0: yield clone.close(); michael@0: }); michael@0: michael@0: /** michael@0: * Test clone(readOnly) method. michael@0: */ michael@0: add_task(function* test_readOnly_clone() { michael@0: let path = OS.Path.join(OS.Constants.Path.profileDir, "test_readOnly_clone.sqlite"); michael@0: let c = yield Sqlite.openConnection({path: path, sharedMemoryCache: false}); michael@0: michael@0: let clone = yield c.clone(true); michael@0: // Just check that it works. michael@0: yield clone.execute("SELECT 1"); michael@0: // But should not be able to write. michael@0: try { michael@0: yield clone.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)"); michael@0: do_throw(new Error("Should not be able to write to a read-only clone.")); michael@0: } catch (ex) {} michael@0: // Closing order should not matter. michael@0: yield c.close(); michael@0: yield clone.close(); michael@0: });