"use strict"; const PROFILE_DIR = do_get_profile().path; 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.importESModule( "resource://testing-common/services/common/logging.sys.mjs" ); 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 = Promise.withResolvers(); 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 = Promise.withResolvers(); 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 = new FileUtils.File( PathUtils.join(PathUtils.tempDir, "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 = Promise.withResolvers(); begin.executeAsync({ handleCompletion(reason) { deferred.resolve(); }, }); await deferred.promise; statement.bindParameters(params); deferred = Promise.withResolvers(); 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 = Promise.withResolvers(); end.executeAsync({ handleCompletion(reason) { deferred.resolve(); }, }); await deferred.promise; statement.finalize(); begin.finalize(); end.finalize(); deferred = Promise.withResolvers(); 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 = Promise.withResolvers(); 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 = Promise.withResolvers(); 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 = Promise.withResolvers(); 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" ); }); add_task(async function test_pageSize() { // Testing the possibility to set the page size on database creation. await Assert.rejects( getDummyDatabase("pagesize", { pageSize: 1234 }), /Invalid pageSize/, "Check invalid pageSize value" ); let c = await getDummyDatabase("pagesize", { pageSize: 8192 }); Assert.equal( (await c.execute("PRAGMA page_size"))[0].getResultByIndex(0), 8192, "Check page size was set" ); await c.close(); });