diff options
Diffstat (limited to 'toolkit/modules/tests/xpcshell/test_sqlite.js')
-rw-r--r-- | toolkit/modules/tests/xpcshell/test_sqlite.js | 1383 |
1 files changed, 1383 insertions, 0 deletions
diff --git a/toolkit/modules/tests/xpcshell/test_sqlite.js b/toolkit/modules/tests/xpcshell/test_sqlite.js new file mode 100644 index 0000000000..2ad8f5a82c --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_sqlite.js @@ -0,0 +1,1383 @@ +"use strict"; + +const PROFILE_DIR = do_get_profile().path; + +const { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); +const { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); +const { Sqlite } = ChromeUtils.importESModule( + "resource://gre/modules/Sqlite.sys.mjs" +); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +// Enable the collection (during test) for all products so even products +// that don't collect the data will be able to run the test without failure. +Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true +); + +function sleep(ms) { + return new Promise(resolve => { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + timer.initWithCallback( + { + notify() { + resolve(); + }, + }, + ms, + timer.TYPE_ONE_SHOT + ); + }); +} + +// When testing finalization, use this to tell Sqlite.sys.mjs to not throw +// an uncatchable `Promise.reject` +function failTestsOnAutoClose(enabled) { + Sqlite.failTestsOnAutoClose(enabled); +} + +function getConnection(dbName, extraOptions = {}) { + let path = dbName + ".sqlite"; + let options = { path }; + for (let [k, v] of Object.entries(extraOptions)) { + options[k] = v; + } + + return Sqlite.openConnection(options); +} + +async function getDummyDatabase(name, extraOptions = {}) { + let c = await getConnection(name, extraOptions); + c._initialStatementCount = 0; + + if (!extraOptions.readOnly) { + const TABLES = new Map([ + ["dirs", "id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT"], + [ + "files", + "id INTEGER PRIMARY KEY AUTOINCREMENT, dir_id INTEGER, path TEXT", + ], + ]); + for (let [k, v] of TABLES) { + await c.execute("CREATE TABLE " + k + "(" + v + ")"); + c._initialStatementCount++; + } + } + + return c; +} + +async function getDummyTempDatabase(name, extraOptions = {}) { + const TABLES = { + dirs: "id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT", + files: "id INTEGER PRIMARY KEY AUTOINCREMENT, dir_id INTEGER, path TEXT", + }; + + let c = await getConnection(name, extraOptions); + c._initialStatementCount = 0; + + for (let [k, v] of Object.entries(TABLES)) { + await c.execute("CREATE TEMP TABLE " + k + "(" + v + ")"); + c._initialStatementCount++; + } + + return c; +} + +add_task(async function test_setup() { + const { initTestLogging } = ChromeUtils.import( + "resource://testing-common/services/common/logging.js" + ); + initTestLogging("Trace"); +}); + +add_task(async function test_open_normal() { + let c = await Sqlite.openConnection({ path: "test_open_normal.sqlite" }); + Assert.equal(c.defaultTransactionType, "DEFERRED"); + await c.close(); +}); + +add_task(async function test_open_with_defaultTransactionType() { + let c = await getConnection("execute_transaction_types", { + defaultTransactionType: "IMMEDIATE", + }); + Assert.equal(c.defaultTransactionType, "IMMEDIATE"); + await c.close(); +}); + +add_task(async function test_open_normal_error() { + let currentDir = do_get_cwd().path; + + let src = PathUtils.join(currentDir, "corrupt.sqlite"); + Assert.ok(await IOUtils.exists(src), "Database file found"); + + // Ensure that our database doesn't already exist. + let path = PathUtils.join(PROFILE_DIR, "corrupt.sqlite"); + await Assert.rejects( + IOUtils.stat(path), + /Could not stat file\(.*\) because it does not exist/, + "Database file should not exist yet" + ); + + await IOUtils.copy(src, path); + + let openPromise = Sqlite.openConnection({ path }); + await Assert.rejects( + openPromise, + reason => { + return reason.result == Cr.NS_ERROR_FILE_CORRUPTED; + }, + "Check error status" + ); +}); + +add_task(async function test_open_unshared() { + let path = PathUtils.join(PROFILE_DIR, "test_open_unshared.sqlite"); + + let c = await Sqlite.openConnection({ path, sharedMemoryCache: false }); + await c.close(); +}); + +add_task(async function test_get_dummy_database() { + let db = await getDummyDatabase("get_dummy_database"); + + Assert.equal(typeof db, "object"); + await db.close(); +}); + +add_task(async function test_schema_version() { + let db = await getDummyDatabase("schema_version"); + + let version = await db.getSchemaVersion(); + Assert.strictEqual(version, 0); + + db.setSchemaVersion(14); + version = await db.getSchemaVersion(); + Assert.strictEqual(version, 14); + + for (let v of [0.5, "foobar", NaN]) { + let success; + try { + await db.setSchemaVersion(v); + info("Schema version " + v + " should have been rejected"); + success = false; + } catch (ex) { + if (!ex.message.startsWith("Schema version must be an integer.")) { + throw ex; + } + success = true; + } + Assert.ok(success); + + version = await db.getSchemaVersion(); + Assert.strictEqual(version, 14); + } + + await db.execute("ATTACH :memory AS attached"); + + let attachedVersion = await db.getSchemaVersion("attached"); + Assert.equal( + attachedVersion, + 0, + "Should return 0 for initial attached schema version" + ); + + await db.setSchemaVersion(3, "attached"); + attachedVersion = await db.getSchemaVersion("attached"); + Assert.equal(attachedVersion, 3, "Should set attached schema version"); + + version = await db.getSchemaVersion(); + Assert.equal( + version, + 14, + "Setting attached schema version should not change main schema version" + ); + + await db.setSchemaVersion(15); + attachedVersion = await db.getSchemaVersion("attached"); + Assert.equal( + attachedVersion, + 3, + "Setting main schema version should not change attached schema version" + ); + + await db.close(); +}); + +add_task(async function test_simple_insert() { + let c = await getDummyDatabase("simple_insert"); + + let result = await c.execute("INSERT INTO dirs VALUES (NULL, 'foo')"); + Assert.ok(Array.isArray(result)); + Assert.equal(result.length, 0); + await c.close(); +}); + +add_task(async function test_simple_bound_array() { + let c = await getDummyDatabase("simple_bound_array"); + + let result = await c.execute("INSERT INTO dirs VALUES (?, ?)", [1, "foo"]); + Assert.equal(result.length, 0); + await c.close(); +}); + +add_task(async function test_simple_bound_object() { + let c = await getDummyDatabase("simple_bound_object"); + let result = await c.execute("INSERT INTO dirs VALUES (:id, :path)", { + id: 1, + path: "foo", + }); + Assert.equal(result.length, 0); + result = await c.execute("SELECT id, path FROM dirs"); + Assert.equal(result.length, 1); + Assert.equal(result[0].getResultByName("id"), 1); + Assert.equal(result[0].getResultByName("path"), "foo"); + await c.close(); +}); + +// This is mostly a sanity test to ensure simple executions work. +add_task(async function test_simple_insert_then_select() { + let c = await getDummyDatabase("simple_insert_then_select"); + + await c.execute("INSERT INTO dirs VALUES (NULL, 'foo')"); + await c.execute("INSERT INTO dirs (path) VALUES (?)", ["bar"]); + + let result = await c.execute("SELECT * FROM dirs"); + Assert.equal(result.length, 2); + + let i = 0; + for (let row of result) { + i++; + + Assert.equal(row.numEntries, 2); + Assert.equal(row.getResultByIndex(0), i); + + let expected = { 1: "foo", 2: "bar" }[i]; + Assert.equal(row.getResultByName("path"), expected); + } + + await c.close(); +}); + +add_task(async function test_repeat_execution() { + let c = await getDummyDatabase("repeat_execution"); + + let sql = "INSERT INTO dirs (path) VALUES (:path)"; + await c.executeCached(sql, { path: "foo" }); + await c.executeCached(sql); + + let result = await c.execute("SELECT * FROM dirs"); + + Assert.equal(result.length, 2); + + await c.close(); +}); + +add_task(async function test_table_exists() { + let c = await getDummyDatabase("table_exists"); + + Assert.equal(false, await c.tableExists("does_not_exist")); + Assert.ok(await c.tableExists("dirs")); + Assert.ok(await c.tableExists("files")); + + await c.close(); +}); + +add_task(async function test_index_exists() { + let c = await getDummyDatabase("index_exists"); + + Assert.equal(false, await c.indexExists("does_not_exist")); + + await c.execute("CREATE INDEX my_index ON dirs (path)"); + Assert.ok(await c.indexExists("my_index")); + + await c.close(); +}); + +add_task(async function test_temp_table_exists() { + let c = await getDummyTempDatabase("temp_table_exists"); + + Assert.equal(false, await c.tableExists("temp_does_not_exist")); + Assert.ok(await c.tableExists("dirs")); + Assert.ok(await c.tableExists("files")); + + await c.close(); +}); + +add_task(async function test_temp_index_exists() { + let c = await getDummyTempDatabase("temp_index_exists"); + + Assert.equal(false, await c.indexExists("temp_does_not_exist")); + + await c.execute("CREATE INDEX my_index ON dirs (path)"); + Assert.ok(await c.indexExists("my_index")); + + await c.close(); +}); + +add_task(async function test_close_cached() { + let c = await getDummyDatabase("close_cached"); + + await c.executeCached("SELECT * FROM dirs"); + await c.executeCached("SELECT * FROM files"); + + await c.close(); +}); + +add_task(async function test_execute_invalid_statement() { + let c = await getDummyDatabase("invalid_statement"); + + await new Promise(resolve => { + Assert.equal(c._connectionData._anonymousStatements.size, 0); + + c.execute("SELECT invalid FROM unknown").then(do_throw, function onError( + error + ) { + resolve(); + }); + }); + + // Ensure we don't leak the statement instance. + Assert.equal(c._connectionData._anonymousStatements.size, 0); + + await c.close(); +}); + +add_task(async function test_incorrect_like_bindings() { + let c = await getDummyDatabase("incorrect_like_bindings"); + + let sql = "select * from dirs where path LIKE 'non%'"; + Assert.throws(() => c.execute(sql), /Please enter a LIKE clause/); + Assert.throws(() => c.executeCached(sql), /Please enter a LIKE clause/); + + await c.close(); +}); +add_task(async function test_on_row_exception_ignored() { + let c = await getDummyDatabase("on_row_exception_ignored"); + + let sql = "INSERT INTO dirs (path) VALUES (?)"; + for (let i = 0; i < 10; i++) { + await c.executeCached(sql, ["dir" + i]); + } + + let i = 0; + let hasResult = await c.execute("SELECT * FROM DIRS", null, function onRow( + row + ) { + i++; + + throw new Error("Some silly error."); + }); + + Assert.equal(hasResult, true); + Assert.equal(i, 10); + + await c.close(); +}); + +// Ensure StopIteration during onRow causes processing to stop. +add_task(async function test_on_row_stop_iteration() { + let c = await getDummyDatabase("on_row_stop_iteration"); + + let sql = "INSERT INTO dirs (path) VALUES (?)"; + for (let i = 0; i < 10; i++) { + await c.executeCached(sql, ["dir" + i]); + } + + let i = 0; + let hasResult = await c.execute("SELECT * FROM dirs", null, function onRow( + row, + cancel + ) { + i++; + + if (i == 5) { + cancel(); + } + }); + + Assert.equal(hasResult, true); + Assert.equal(i, 5); + + await c.close(); +}); + +// Ensure execute resolves to false when no rows are selected. +add_task(async function test_on_row_stop_iteration() { + let c = await getDummyDatabase("no_on_row"); + + let i = 0; + let hasResult = await c.execute( + `SELECT * FROM dirs WHERE path="nonexistent"`, + null, + function onRow(row) { + i++; + } + ); + + Assert.equal(hasResult, false); + Assert.equal(i, 0); + + await c.close(); +}); + +add_task(async function test_invalid_transaction_type() { + let c = await getDummyDatabase("invalid_transaction_type"); + + Assert.throws( + () => c.executeTransaction(function() {}, "foobar"), + /Unknown transaction type/, + "Unknown transaction type should throw" + ); + + await c.close(); +}); + +add_task(async function test_execute_transaction_success() { + let c = await getDummyDatabase("execute_transaction_success"); + + Assert.ok(!c.transactionInProgress); + + await c.executeTransaction(async function transaction(conn) { + Assert.equal(c, conn); + Assert.ok(conn.transactionInProgress); + + await conn.execute("INSERT INTO dirs (path) VALUES ('foo')"); + }); + + Assert.ok(!c.transactionInProgress); + let rows = await c.execute("SELECT * FROM dirs"); + Assert.ok(Array.isArray(rows)); + Assert.equal(rows.length, 1); + + await c.close(); +}); + +add_task(async function test_execute_transaction_rollback() { + let c = await getDummyDatabase("execute_transaction_rollback"); + + let deferred = PromiseUtils.defer(); + + c.executeTransaction(async function transaction(conn) { + await conn.execute("INSERT INTO dirs (path) VALUES ('foo')"); + print("Expecting error with next statement."); + await conn.execute("INSERT INTO invalid VALUES ('foo')"); + + // We should never get here. + do_throw(); + }).then(do_throw, function onError(error) { + deferred.resolve(); + }); + + await deferred.promise; + + let rows = await c.execute("SELECT * FROM dirs"); + Assert.equal(rows.length, 0); + + await c.close(); +}); + +add_task(async function test_close_during_transaction() { + let c = await getDummyDatabase("close_during_transaction"); + + await c.execute("INSERT INTO dirs (path) VALUES ('foo')"); + + let promise = c.executeTransaction(async function transaction(conn) { + await c.execute("INSERT INTO dirs (path) VALUES ('bar')"); + }); + await c.close(); + + await Assert.rejects( + promise, + /Transaction canceled due to a closed connection/, + "closing a connection in the middle of a transaction should reject it" + ); + + let c2 = await getConnection("close_during_transaction"); + let rows = await c2.execute("SELECT * FROM dirs"); + Assert.equal(rows.length, 1); + + await c2.close(); +}); + +// Verify that we support concurrent transactions. +add_task(async function test_multiple_transactions() { + let c = await getDummyDatabase("detect_multiple_transactions"); + + for (let i = 0; i < 10; ++i) { + // We don't wait for these transactions. + c.executeTransaction(async function() { + await c.execute("INSERT INTO dirs (path) VALUES (:path)", { + path: `foo${i}`, + }); + await c.execute("SELECT * FROM dirs"); + }); + } + for (let i = 0; i < 10; ++i) { + await c.executeTransaction(async function() { + await c.execute("INSERT INTO dirs (path) VALUES (:path)", { + path: `bar${i}`, + }); + await c.execute("SELECT * FROM dirs"); + }); + } + + let rows = await c.execute("SELECT * FROM dirs"); + Assert.equal(rows.length, 20); + + await c.close(); +}); + +// Verify that wrapped transactions ignore a BEGIN TRANSACTION failure, when +// an externally opened transaction exists. +add_task(async function test_wrapped_connection_transaction() { + let file = new FileUtils.File( + PathUtils.join(PROFILE_DIR, "test_wrapStorageConnection.sqlite") + ); + let c = await new Promise((resolve, reject) => { + Services.storage.openAsyncDatabase( + file, + /* openFlags */ Ci.mozIStorageService.OPEN_DEFAULT, + /* connectionFlags */ Ci.mozIStorageService.CONNECTION_DEFAULT, + (status, db) => { + if (Components.isSuccessCode(status)) { + resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection)); + } else { + reject(new Error(status)); + } + } + ); + }); + + let wrapper = await Sqlite.wrapStorageConnection({ connection: c }); + // Start a transaction on the raw connection. + await c.executeSimpleSQLAsync("BEGIN"); + // Now use executeTransaction, it will be executed, but not in a transaction. + await wrapper.executeTransaction(async function() { + await wrapper.execute( + "CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT)" + ); + }); + // This should not fail cause the internal transaction has not been created. + await c.executeSimpleSQLAsync("COMMIT"); + + await wrapper.execute("SELECT * FROM test"); + + // Closing the wrapper should just finalize statements but not close the + // database. + await wrapper.close(); + await c.asyncClose(); +}); + +add_task(async function test_transaction_timeout() { + // Lower the transactions timeout for the test. + let defaultTimeout = Sqlite.TRANSACTIONS_TIMEOUT_MS; + Sqlite.TRANSACTIONS_TIMEOUT_MS = 500; + Services.telemetry.clearScalars(); + let myResolve = () => {}; + try { + let c = await getDummyDatabase("transaction_timeout"); + Assert.ok(!c.transactionInProgress, "Should not be in a transaction"); + let promise = c.executeTransaction(async function transaction(conn) { + // Make a change, we'll later check it is undone by ROLLBACK. + await conn.execute( + "CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT)" + ); + Assert.ok(c.transactionInProgress, "Should be in a transaction"); + // Return a never fulfilled promise. + await new Promise(resolve => { + // Keep this alive regardless GC, and clean it up in finally. + myResolve = resolve; + }); + }); + + await Assert.rejects( + promise, + /Transaction timeout, most likely caused by unresolved pending work./, + "A transaction timeout should reject it" + ); + + let rows = await c.execute("SELECT * FROM dirs"); + Assert.equal(rows.length, 0, "Changes should have been rolled back"); + await c.close(); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "mozstorage.sqlitejsm_transaction_timeout", + "test_transaction_timeout@test_sqlite.js", + 1 + ); + } finally { + Sqlite.TRANSACTIONS_TIMEOUT_MS = defaultTimeout; + myResolve(); + } +}); + +add_task(async function test_shrink_memory() { + let c = await getDummyDatabase("shrink_memory"); + + // It's just a simple sanity test. We have no way of measuring whether this + // actually does anything. + + await c.shrinkMemory(); + await c.close(); +}); + +add_task(async function test_no_shrink_on_init() { + let c = await getConnection("no_shrink_on_init", { + shrinkMemoryOnConnectionIdleMS: 200, + }); + + let count = 0; + Object.defineProperty(c._connectionData, "shrinkMemory", { + value() { + count++; + }, + }); + + // We should not shrink until a statement has been executed. + await sleep(220); + Assert.equal(count, 0); + + await c.execute("SELECT 1"); + await sleep(220); + Assert.equal(count, 1); + + await c.close(); +}); + +add_task(async function test_idle_shrink_fires() { + let c = await getDummyDatabase("idle_shrink_fires", { + shrinkMemoryOnConnectionIdleMS: 200, + }); + c._connectionData._clearIdleShrinkTimer(); + + let oldShrink = c._connectionData.shrinkMemory; + let shrinkPromises = []; + + let count = 0; + Object.defineProperty(c._connectionData, "shrinkMemory", { + value() { + count++; + let promise = oldShrink.call(c._connectionData); + shrinkPromises.push(promise); + return promise; + }, + }); + + // We reset the idle shrink timer after monkeypatching because otherwise the + // installed timer callback will reference the non-monkeypatched function. + c._connectionData._startIdleShrinkTimer(); + + await sleep(220); + Assert.equal(count, 1); + Assert.equal(shrinkPromises.length, 1); + await shrinkPromises[0]; + shrinkPromises.shift(); + + // We shouldn't shrink again unless a statement was executed. + await sleep(300); + Assert.equal(count, 1); + + await c.execute("SELECT 1"); + await sleep(300); + + Assert.equal(count, 2); + Assert.equal(shrinkPromises.length, 1); + await shrinkPromises[0]; + + await c.close(); +}); + +add_task(async function test_idle_shrink_reset_on_operation() { + const INTERVAL = 500; + let c = await getDummyDatabase("idle_shrink_reset_on_operation", { + shrinkMemoryOnConnectionIdleMS: INTERVAL, + }); + + c._connectionData._clearIdleShrinkTimer(); + + let oldShrink = c._connectionData.shrinkMemory; + let shrinkPromises = []; + let count = 0; + + Object.defineProperty(c._connectionData, "shrinkMemory", { + value() { + count++; + let promise = oldShrink.call(c._connectionData); + shrinkPromises.push(promise); + return promise; + }, + }); + + let now = new Date(); + c._connectionData._startIdleShrinkTimer(); + + let initialIdle = new Date(now.getTime() + INTERVAL); + + // Perform database operations until initial scheduled time has been passed. + let i = 0; + while (new Date() < initialIdle) { + await c.execute("INSERT INTO dirs (path) VALUES (?)", ["" + i]); + i++; + } + + Assert.ok(i > 0); + + // We should not have performed an idle while doing operations. + Assert.equal(count, 0); + + // Wait for idle timer. + await sleep(INTERVAL); + + // Ensure we fired. + Assert.equal(count, 1); + Assert.equal(shrinkPromises.length, 1); + await shrinkPromises[0]; + + await c.close(); +}); + +add_task(async function test_in_progress_counts() { + let c = await getDummyDatabase("in_progress_counts"); + Assert.equal(c._connectionData._statementCounter, c._initialStatementCount); + Assert.equal(c._connectionData._pendingStatements.size, 0); + await c.executeCached("INSERT INTO dirs (path) VALUES ('foo')"); + Assert.equal( + c._connectionData._statementCounter, + c._initialStatementCount + 1 + ); + Assert.equal(c._connectionData._pendingStatements.size, 0); + + let expectOne; + let expectTwo; + + // We want to make sure that two queries executing simultaneously + // result in `_pendingStatements.size` reaching 2, then dropping back to 0. + // + // To do so, we kick off a second statement within the row handler + // of the first, then wait for both to finish. + + let inner = PromiseUtils.defer(); + await c.executeCached("SELECT * from dirs", null, function onRow() { + // In the onRow handler, we're still an outstanding query. + // Expect a single in-progress entry. + expectOne = c._connectionData._pendingStatements.size; + + // Start another query, checking that after its statement has been created + // there are two statements in progress. + c.executeCached("SELECT 10, path from dirs").then(inner.resolve); + expectTwo = c._connectionData._pendingStatements.size; + }); + + await inner.promise; + + Assert.equal(expectOne, 1); + Assert.equal(expectTwo, 2); + Assert.equal( + c._connectionData._statementCounter, + c._initialStatementCount + 3 + ); + Assert.equal(c._connectionData._pendingStatements.size, 0); + + await c.close(); +}); + +add_task(async function test_discard_while_active() { + let c = await getDummyDatabase("discard_while_active"); + + await c.executeCached("INSERT INTO dirs (path) VALUES ('foo')"); + await c.executeCached("INSERT INTO dirs (path) VALUES ('bar')"); + + let discarded = -1; + let first = true; + let sql = "SELECT * FROM dirs"; + await c.executeCached(sql, null, function onRow(row) { + if (!first) { + return; + } + first = false; + discarded = c.discardCachedStatements(); + }); + + // We discarded everything, because the SELECT had already started to run. + Assert.equal(3, discarded); + + // And again is safe. + Assert.equal(0, c.discardCachedStatements()); + + await c.close(); +}); + +add_task(async function test_discard_cached() { + let c = await getDummyDatabase("discard_cached"); + + await c.executeCached("SELECT * from dirs"); + Assert.equal(1, c._connectionData._cachedStatements.size); + + await c.executeCached("SELECT * from files"); + Assert.equal(2, c._connectionData._cachedStatements.size); + + await c.executeCached("SELECT * from dirs"); + Assert.equal(2, c._connectionData._cachedStatements.size); + + c.discardCachedStatements(); + Assert.equal(0, c._connectionData._cachedStatements.size); + + await c.close(); +}); + +add_task(async function test_array_binding_with_null() { + let c = await getDummyDatabase("array_binding_null"); + + let bindings = [null, "test"]; + + let sql = "INSERT INTO files (dir_id, path) VALUES (?, ?)"; + let result = await c.execute(sql, bindings); + Assert.equal(result.length, 0); + + let rows = await c.executeCached("SELECT * from files"); + Assert.equal(rows.length, 1); + Assert.strictEqual(rows[0].getResultByName("dir_id"), null); + Assert.equal(rows[0].getResultByName("path"), "test"); + await c.close(); +}); + +add_task(async function test_programmatic_binding() { + let c = await getDummyDatabase("programmatic_binding"); + + let bindings = [ + { id: 1, path: "foobar" }, + { id: null, path: "baznoo" }, + { id: 5, path: "toofoo" }, + ]; + + let sql = "INSERT INTO dirs VALUES (:id, :path)"; + let result = await c.execute(sql, bindings); + Assert.equal(result.length, 0); + + let rows = await c.executeCached("SELECT * from dirs"); + Assert.equal(rows.length, 3); + await c.close(); +}); + +add_task(async function test_programmatic_binding_transaction() { + let c = await getDummyDatabase("programmatic_binding_transaction"); + + let bindings = [ + { id: 1, path: "foobar" }, + { id: null, path: "baznoo" }, + { id: 5, path: "toofoo" }, + ]; + + let sql = "INSERT INTO dirs VALUES (:id, :path)"; + await c.executeTransaction(async function transaction() { + let result = await c.execute(sql, bindings); + Assert.equal(result.length, 0); + + let rows = await c.executeCached("SELECT * from dirs"); + Assert.equal(rows.length, 3); + }); + + // Transaction committed. + let rows = await c.executeCached("SELECT * from dirs"); + Assert.equal(rows.length, 3); + await c.close(); +}); + +add_task( + async function test_programmatic_binding_transaction_partial_rollback() { + let c = await getDummyDatabase( + "programmatic_binding_transaction_partial_rollback" + ); + + let bindings = [ + { id: 2, path: "foobar" }, + { id: 3, path: "toofoo" }, + ]; + + let sql = "INSERT INTO dirs VALUES (:id, :path)"; + + // Add some data in an implicit transaction before beginning the batch insert. + await c.execute(sql, { id: 1, path: "works" }); + + let secondSucceeded = false; + try { + await c.executeTransaction(async function transaction() { + // Insert one row. This won't implicitly start a transaction. + await c.execute(sql, bindings[0]); + + // Insert multiple rows. mozStorage will want to start a transaction. + // One of the inserts will fail, so the transaction should be rolled back. + await c.execute(sql, bindings); + secondSucceeded = true; + }); + } catch (ex) { + print("Caught expected exception: " + ex); + Assert.greater( + ex.result, + 0x80000000, + "The ex.result value should be forwarded." + ); + } + + // We did not get to the end of our in-transaction block. + Assert.ok(!secondSucceeded); + + // Everything that happened in *our* transaction, not mozStorage's, got + // rolled back, but the first row still exists. + let rows = await c.executeCached("SELECT * from dirs"); + Assert.equal(rows.length, 1); + Assert.equal(rows[0].getResultByName("path"), "works"); + await c.close(); + } +); + +// Just like the previous test, but relying on the implicit +// transaction established by mozStorage. +add_task(async function test_programmatic_binding_implicit_transaction() { + let c = await getDummyDatabase("programmatic_binding_implicit_transaction"); + + let bindings = [ + { id: 2, path: "foobar" }, + { id: 1, path: "toofoo" }, + ]; + + let sql = "INSERT INTO dirs VALUES (:id, :path)"; + let secondSucceeded = false; + await c.execute(sql, { id: 1, path: "works" }); + try { + await c.execute(sql, bindings); + secondSucceeded = true; + } catch (ex) { + print("Caught expected exception: " + ex); + Assert.greater( + ex.result, + 0x80000000, + "The ex.result value should be forwarded." + ); + } + + Assert.ok(!secondSucceeded); + + // The entire batch failed. + let rows = await c.executeCached("SELECT * from dirs"); + Assert.equal(rows.length, 1); + Assert.equal(rows[0].getResultByName("path"), "works"); + await c.close(); +}); + +// Test that direct binding of params and execution through mozStorage doesn't +// error when we manually create a transaction. See Bug 856925. +add_task(async function test_direct() { + let file = FileUtils.getFile("TmpD", ["test_direct.sqlite"]); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); + print("Opening " + file.path); + + let db = Services.storage.openDatabase(file); + print("Opened " + db); + + db.executeSimpleSQL( + "CREATE TABLE types (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, UNIQUE (name))" + ); + print("Executed setup."); + + let statement = db.createAsyncStatement( + "INSERT INTO types (name) VALUES (:name)" + ); + let params = statement.newBindingParamsArray(); + let one = params.newBindingParams(); + one.bindByName("name", null); + params.addParams(one); + let two = params.newBindingParams(); + two.bindByName("name", "bar"); + params.addParams(two); + + print("Beginning transaction."); + let begin = db.createAsyncStatement("BEGIN DEFERRED TRANSACTION"); + let end = db.createAsyncStatement("COMMIT TRANSACTION"); + + let deferred = PromiseUtils.defer(); + begin.executeAsync({ + handleCompletion(reason) { + deferred.resolve(); + }, + }); + await deferred.promise; + + statement.bindParameters(params); + + deferred = PromiseUtils.defer(); + print("Executing async."); + statement.executeAsync({ + handleResult(resultSet) {}, + + handleError(error) { + print( + "Error when executing SQL (" + error.result + "): " + error.message + ); + print("Original error: " + error.error); + deferred.reject(); + }, + + handleCompletion(reason) { + print("Completed."); + deferred.resolve(); + }, + }); + + await deferred.promise; + + deferred = PromiseUtils.defer(); + end.executeAsync({ + handleCompletion(reason) { + deferred.resolve(); + }, + }); + await deferred.promise; + + statement.finalize(); + begin.finalize(); + end.finalize(); + + deferred = PromiseUtils.defer(); + db.asyncClose(function() { + deferred.resolve(); + }); + await deferred.promise; +}); + +// Test Sqlite.cloneStorageConnection. +add_task(async function test_cloneStorageConnection() { + let file = new FileUtils.File( + PathUtils.join(PROFILE_DIR, "test_cloneStorageConnection.sqlite") + ); + let c = await new Promise((resolve, reject) => { + Services.storage.openAsyncDatabase( + file, + /* openFlags */ Ci.mozIStorageService.OPEN_DEFAULT, + /* connectionFlags */ Ci.mozIStorageService.CONNECTION_DEFAULT, + (status, db) => { + if (Components.isSuccessCode(status)) { + resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection)); + } else { + reject(new Error(status)); + } + } + ); + }); + + let clone = await Sqlite.cloneStorageConnection({ + connection: c, + readOnly: true, + }); + Assert.equal(clone.defaultTransactionType, "DEFERRED"); + // Just check that it works. + await clone.execute("SELECT 1"); + + info("Set default transaction type on storage connection"); + c.defaultTransactionType = Ci.mozIStorageConnection.TRANSACTION_IMMEDIATE; + + let clone2 = await Sqlite.cloneStorageConnection({ + connection: c, + readOnly: false, + }); + Assert.equal(clone2.defaultTransactionType, "IMMEDIATE"); + // Just check that it works. + await clone2.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)"); + + // Closing order should not matter. + await c.asyncClose(); + await clone2.close(); + await clone.close(); +}); + +// Test Sqlite.cloneStorageConnection invalid argument. +add_task(async function test_cloneStorageConnection() { + try { + await Sqlite.cloneStorageConnection({ connection: null }); + do_throw(new Error("Should throw on invalid connection")); + } catch (ex) { + if (ex.name != "TypeError") { + throw ex; + } + } +}); + +// Test clone() method. +add_task(async function test_clone() { + let c = await getDummyDatabase("clone"); + + let clone = await c.clone(); + // Just check that it works. + await clone.execute("SELECT 1"); + // Closing order should not matter. + await c.close(); + await clone.close(); +}); + +// Test clone(readOnly) method. +add_task(async function test_readOnly_clone() { + let path = PathUtils.join(PROFILE_DIR, "test_readOnly_clone.sqlite"); + let c = await Sqlite.openConnection({ path, sharedMemoryCache: false }); + + let clone = await c.clone(true); + // Just check that it works. + await clone.execute("SELECT 1"); + // But should not be able to write. + + await Assert.rejects( + clone.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)"), + /readonly/ + ); + // Closing order should not matter. + await c.close(); + await clone.close(); +}); + +// Test Sqlite.wrapStorageConnection. +add_task(async function test_wrapStorageConnection() { + let file = new FileUtils.File( + PathUtils.join(PROFILE_DIR, "test_wrapStorageConnection.sqlite") + ); + let c = await new Promise((resolve, reject) => { + Services.storage.openAsyncDatabase( + file, + /* openFlags */ Ci.mozIStorageService.OPEN_DEFAULT, + /* connectionFlags */ Ci.mozIStorageService.CONNECTION_DEFAULT, + (status, db) => { + if (Components.isSuccessCode(status)) { + resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection)); + } else { + reject(new Error(status)); + } + } + ); + }); + + let wrapper = await Sqlite.wrapStorageConnection({ connection: c }); + Assert.equal(wrapper.defaultTransactionType, "DEFERRED"); + // Just check that it works. + await wrapper.execute("SELECT 1"); + await wrapper.executeCached("SELECT 1"); + + // Closing the wrapper should just finalize statements but not close the + // database. + await wrapper.close(); + + info("Set default transaction type on storage connection"); + c.defaultTransactionType = Ci.mozIStorageConnection.TRANSACTION_EXCLUSIVE; + + let wrapper2 = await Sqlite.wrapStorageConnection({ connection: c }); + Assert.equal(wrapper2.defaultTransactionType, "EXCLUSIVE"); + // Just check that it works. + await wrapper2.execute("SELECT 1"); + + await wrapper2.close(); + + await c.asyncClose(); +}); + +// Test finalization +add_task(async function test_closed_by_witness() { + failTestsOnAutoClose(false); + let c = await getDummyDatabase("closed_by_witness"); + + Services.obs.notifyObservers( + null, + "sqlite-finalization-witness", + c._connectionData._identifier + ); + // Since we triggered finalization ourselves, tell the witness to + // forget the connection so it does not trigger a finalization again + c._witness.forget(); + await c._connectionData._deferredClose.promise; + Assert.ok(!c._connectionData._open); + failTestsOnAutoClose(true); +}); + +add_task(async function test_warning_message_on_finalization() { + failTestsOnAutoClose(false); + let c = await getDummyDatabase("warning_message_on_finalization"); + let identifier = c._connectionData._identifier; + let deferred = PromiseUtils.defer(); + + let listener = { + observe(msg) { + let messageText = msg.message; + // Make sure the message starts with a warning containing the + // connection identifier + if ( + messageText.includes("Warning: Sqlite connection '" + identifier + "'") + ) { + deferred.resolve(); + } + }, + }; + Services.console.registerListener(listener); + + Services.obs.notifyObservers(null, "sqlite-finalization-witness", identifier); + // Since we triggered finalization ourselves, tell the witness to + // forget the connection so it does not trigger a finalization again + c._witness.forget(); + + await deferred.promise; + Services.console.unregisterListener(listener); + failTestsOnAutoClose(true); +}); + +add_task(async function test_error_message_on_unknown_finalization() { + failTestsOnAutoClose(false); + let deferred = PromiseUtils.defer(); + + let listener = { + observe(msg) { + let messageText = msg.message; + if ( + messageText.includes( + "Error: Attempt to finalize unknown Sqlite connection: foo" + ) + ) { + deferred.resolve(); + } + }, + }; + Services.console.registerListener(listener); + Services.obs.notifyObservers(null, "sqlite-finalization-witness", "foo"); + + await deferred.promise; + Services.console.unregisterListener(listener); + failTestsOnAutoClose(true); +}); + +add_task(async function test_forget_witness_on_close() { + let c = await getDummyDatabase("forget_witness_on_close"); + + let forgetCalled = false; + let oldWitness = c._witness; + c._witness = { + forget() { + forgetCalled = true; + oldWitness.forget(); + }, + }; + + await c.close(); + // After close, witness should have forgotten the connection + Assert.ok(forgetCalled); +}); + +add_task(async function test_close_database_on_gc() { + failTestsOnAutoClose(false); + let finalPromise; + + { + let collectedPromises = []; + for (let i = 0; i < 100; ++i) { + let deferred = PromiseUtils.defer(); + let c = await getDummyDatabase("gc_" + i); + c._connectionData._deferredClose.promise.then(deferred.resolve); + collectedPromises.push(deferred.promise); + } + finalPromise = Promise.all(collectedPromises); + } + + // Call getDummyDatabase once more to clear any remaining + // references. This is needed at the moment, otherwise + // garbage-collection takes place after the shutdown barrier and the + // test will timeout. Once that is fixed, we can remove this line + // and be fine as long as the connections are garbage-collected. + let last = await getDummyDatabase("gc_last"); + await last.close(); + + Cu.forceGC(); + Cu.forceCC(); + Cu.forceShrinkingGC(); + + await finalPromise; + failTestsOnAutoClose(true); +}); + +// Test all supported datatypes +add_task(async function test_datatypes() { + let c = await getConnection("datatypes"); + await c.execute("DROP TABLE IF EXISTS datatypes"); + await c.execute(`CREATE TABLE datatypes ( + null_col NULL, + integer_col INTEGER NOT NULL, + text_col TEXT NOT NULL, + blob_col BLOB NOT NULL, + real_col REAL NOT NULL, + numeric_col NUMERIC NOT NULL + )`); + const bindings = [ + { + null_col: null, + integer_col: 12345, + text_col: "qwerty", + blob_col: new Uint8Array(256).map((value, index) => index % 256), + real_col: 3.14159265359, + numeric_col: true, + }, + { + null_col: null, + integer_col: -12345, + text_col: "", + blob_col: new Uint8Array(256 * 2).map((value, index) => index % 256), + real_col: Number.NEGATIVE_INFINITY, + numeric_col: false, + }, + ]; + + await c.execute( + `INSERT INTO datatypes VALUES ( + :null_col, + :integer_col, + :text_col, + :blob_col, + :real_col, + :numeric_col + )`, + bindings + ); + + let rows = await c.execute("SELECT * FROM datatypes"); + Assert.ok(Array.isArray(rows)); + Assert.equal(rows.length, bindings.length); + for (let i = 0; i < bindings.length; ++i) { + let binding = bindings[i]; + let row = rows[i]; + for (let colName in binding) { + // In Sqlite bool is stored and then retrieved as numeric. + let val = + typeof binding[colName] == "boolean" + ? +binding[colName] + : binding[colName]; + Assert.deepEqual(val, row.getResultByName(colName)); + } + } + await c.close(); +}); + +add_task(async function test_interrupt() { + // Testing the interrupt functionality is left to mozStorage unit tests, here + // we'll just test error conditions. + let c = await getDummyDatabase("interrupt"); + await c.interrupt(); + ok(true, "Sqlite.interrupt() should not throw on a writable connection"); + await c.close(); + Assert.throws( + () => c.interrupt(), + /Connection is not open/, + "Sqlite.interrupt() should throw on a closed connection" + ); +}); |