Bug 1867925 - Mark some storage-access-api tests as intermittent after wpt-sync....
[gecko.git] / toolkit / modules / Sqlite.sys.mjs
blob8380b7a2c3c2d6e7697b4269dcd3beda94ac9ea8
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /**
6  * PRIVACY WARNING
7  * ===============
8  *
9  * Database file names can be exposed through telemetry and in crash reports on
10  * the https://crash-stats.mozilla.org site, to allow recognizing the affected
11  * database.
12  * if your database name may contain privacy sensitive information, e.g. an
13  * URL origin, you should use openDatabaseWithFileURL and pass an explicit
14  * TelemetryFilename to it. That name will be used both for telemetry and for
15  * thread names in crash reports.
16  * If you have different needs (e.g. using the javascript module or an async
17  * connection from the main thread) please coordinate with the mozStorage peers.
18  */
20 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
22 import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
24 const lazy = {};
26 ChromeUtils.defineESModuleGetters(lazy, {
27   AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
28   FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
29   Log: "resource://gre/modules/Log.sys.mjs",
30   PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
31 });
33 XPCOMUtils.defineLazyServiceGetter(
34   lazy,
35   "FinalizationWitnessService",
36   "@mozilla.org/toolkit/finalizationwitness;1",
37   "nsIFinalizationWitnessService"
40 // Regular expression used by isInvalidBoundLikeQuery
41 var likeSqlRegex = /\bLIKE\b\s(?![@:?])/i;
43 // Counts the number of created connections per database basename(). This is
44 // used for logging to distinguish connection instances.
45 var connectionCounters = new Map();
47 // Tracks identifiers of wrapped connections, that are Storage connections
48 // opened through mozStorage and then wrapped by Sqlite.sys.mjs to use its syntactic
49 // sugar API.  Since these connections have an unknown origin, we use this set
50 // to differentiate their behavior.
51 var wrappedConnections = new Set();
53 /**
54  * Once `true`, reject any attempt to open or close a database.
55  */
56 function isClosed() {
57   // If Barriers have not been initialized yet, just trust AppStartup.
58   if (
59     typeof Object.getOwnPropertyDescriptor(lazy, "Barriers").get == "function"
60   ) {
61     // It's still possible to open new connections at profile-before-change, so
62     // use the next phase here, as a fallback.
63     return Services.startup.isInOrBeyondShutdownPhase(
64       Ci.nsIAppStartup.SHUTDOWN_PHASE_XPCOMWILLSHUTDOWN
65     );
66   }
67   return lazy.Barriers.shutdown.client.isClosed;
70 var Debugging = {
71   // Tests should fail if a connection auto closes.  The exception is
72   // when finalization itself is tested, in which case this flag
73   // should be set to false.
74   failTestsOnAutoClose: true,
77 /**
78  * Helper function to check whether LIKE is implemented using proper bindings.
79  *
80  * @param sql
81  *        (string) The SQL query to be verified.
82  * @return boolean value telling us whether query was correct or not
83  */
84 function isInvalidBoundLikeQuery(sql) {
85   return likeSqlRegex.test(sql);
88 // Displays a script error message
89 function logScriptError(message) {
90   let consoleMessage = Cc["@mozilla.org/scripterror;1"].createInstance(
91     Ci.nsIScriptError
92   );
93   let stack = new Error();
94   consoleMessage.init(
95     message,
96     stack.fileName,
97     null,
98     stack.lineNumber,
99     0,
100     Ci.nsIScriptError.errorFlag,
101     "component javascript"
102   );
103   Services.console.logMessage(consoleMessage);
105   // This `Promise.reject` will cause tests to fail.  The debugging
106   // flag can be used to suppress this for tests that explicitly
107   // test auto closes.
108   if (Debugging.failTestsOnAutoClose) {
109     Promise.reject(new Error(message));
110   }
114  * Gets connection identifier from its database file name.
116  * @param fileName
117  *        A database file string name.
118  * @return the connection identifier.
119  */
120 function getIdentifierByFileName(fileName) {
121   let number = connectionCounters.get(fileName) || 0;
122   connectionCounters.set(fileName, number + 1);
123   return fileName + "#" + number;
127  * Convert mozIStorageError to common NS_ERROR_*
128  * The conversion is mostly based on the one in
129  * mozStoragePrivateHelpers::ConvertResultCode, plus a few additions.
131  * @param {integer} result a mozIStorageError result code.
132  * @returns {integer} an NS_ERROR_* result code.
133  */
134 function convertStorageErrorResult(result) {
135   switch (result) {
136     case Ci.mozIStorageError.PERM:
137     case Ci.mozIStorageError.AUTH:
138     case Ci.mozIStorageError.CANTOPEN:
139       return Cr.NS_ERROR_FILE_ACCESS_DENIED;
140     case Ci.mozIStorageError.LOCKED:
141       return Cr.NS_ERROR_FILE_IS_LOCKED;
142     case Ci.mozIStorageError.READONLY:
143       return Cr.NS_ERROR_FILE_READ_ONLY;
144     case Ci.mozIStorageError.ABORT:
145     case Ci.mozIStorageError.INTERRUPT:
146       return Cr.NS_ERROR_ABORT;
147     case Ci.mozIStorageError.TOOBIG:
148     case Ci.mozIStorageError.FULL:
149       return Cr.NS_ERROR_FILE_NO_DEVICE_SPACE;
150     case Ci.mozIStorageError.NOMEM:
151       return Cr.NS_ERROR_OUT_OF_MEMORY;
152     case Ci.mozIStorageError.BUSY:
153       return Cr.NS_ERROR_STORAGE_BUSY;
154     case Ci.mozIStorageError.CONSTRAINT:
155       return Cr.NS_ERROR_STORAGE_CONSTRAINT;
156     case Ci.mozIStorageError.NOLFS:
157     case Ci.mozIStorageError.IOERR:
158       return Cr.NS_ERROR_STORAGE_IOERR;
159     case Ci.mozIStorageError.SCHEMA:
160     case Ci.mozIStorageError.MISMATCH:
161     case Ci.mozIStorageError.MISUSE:
162     case Ci.mozIStorageError.RANGE:
163       return Ci.NS_ERROR_UNEXPECTED;
164     case Ci.mozIStorageError.CORRUPT:
165     case Ci.mozIStorageError.EMPTY:
166     case Ci.mozIStorageError.FORMAT:
167     case Ci.mozIStorageError.NOTADB:
168       return Cr.NS_ERROR_FILE_CORRUPTED;
169     default:
170       return Cr.NS_ERROR_FAILURE;
171   }
174  * Barriers used to ensure that Sqlite.sys.mjs is shutdown after all
175  * its clients.
176  */
177 ChromeUtils.defineLazyGetter(lazy, "Barriers", () => {
178   let Barriers = {
179     /**
180      * Public barrier that clients may use to add blockers to the
181      * shutdown of Sqlite.sys.mjs. Triggered by profile-before-change.
182      * Once all blockers of this barrier are lifted, we close the
183      * ability to open new connections.
184      */
185     shutdown: new lazy.AsyncShutdown.Barrier(
186       "Sqlite.sys.mjs: wait until all clients have completed their task"
187     ),
189     /**
190      * Private barrier blocked by connections that are still open.
191      * Triggered after Barriers.shutdown is lifted and `isClosed()` returns
192      * `true`.
193      */
194     connections: new lazy.AsyncShutdown.Barrier(
195       "Sqlite.sys.mjs: wait until all connections are closed"
196     ),
197   };
199   /**
200    * Observer for the event which is broadcasted when the finalization
201    * witness `_witness` of `OpenedConnection` is garbage collected.
202    *
203    * The observer is passed the connection identifier of the database
204    * connection that is being finalized.
205    */
206   let finalizationObserver = function (subject, topic, identifier) {
207     let connectionData = ConnectionData.byId.get(identifier);
209     if (connectionData === undefined) {
210       logScriptError(
211         "Error: Attempt to finalize unknown Sqlite connection: " +
212           identifier +
213           "\n"
214       );
215       return;
216     }
218     ConnectionData.byId.delete(identifier);
219     logScriptError(
220       "Warning: Sqlite connection '" +
221         identifier +
222         "' was not properly closed. Auto-close triggered by garbage collection.\n"
223     );
224     connectionData.close();
225   };
226   Services.obs.addObserver(finalizationObserver, "sqlite-finalization-witness");
228   /**
229    * Ensure that Sqlite.sys.mjs:
230    * - informs its clients before shutting down;
231    * - lets clients open connections during shutdown, if necessary;
232    * - waits for all connections to be closed before shutdown.
233    */
234   lazy.AsyncShutdown.profileBeforeChange.addBlocker(
235     "Sqlite.sys.mjs shutdown blocker",
236     async function () {
237       await Barriers.shutdown.wait();
238       // At this stage, all clients have had a chance to open (and close)
239       // their databases. Some previous close operations may still be pending,
240       // so we need to wait until they are complete before proceeding.
241       await Barriers.connections.wait();
243       // Everything closed, no finalization events to catch
244       Services.obs.removeObserver(
245         finalizationObserver,
246         "sqlite-finalization-witness"
247       );
248     },
250     function status() {
251       if (isClosed()) {
252         // We are waiting for the connections to close. The interesting
253         // status is therefore the list of connections still pending.
254         return {
255           description: "Waiting for connections to close",
256           state: Barriers.connections.state,
257         };
258       }
260       // We are still in the first stage: waiting for the barrier
261       // to be lifted. The interesting status is therefore that of
262       // the barrier.
263       return {
264         description: "Waiting for the barrier to be lifted",
265         state: Barriers.shutdown.state,
266       };
267     }
268   );
270   return Barriers;
273 const VACUUM_CATEGORY = "vacuum-participant";
274 const VACUUM_CONTRACTID = "@sqlite.module.js/vacuum-participant;";
275 var registeredVacuumParticipants = new Map();
277 function registerVacuumParticipant(connectionData) {
278   let contractId = VACUUM_CONTRACTID + connectionData._identifier;
279   let factory = {
280     createInstance(iid) {
281       return connectionData.QueryInterface(iid);
282     },
283     QueryInterface: ChromeUtils.generateQI(["nsIFactory"]),
284   };
285   let cid = Services.uuid.generateUUID();
286   Components.manager
287     .QueryInterface(Ci.nsIComponentRegistrar)
288     .registerFactory(cid, contractId, contractId, factory);
289   Services.catMan.addCategoryEntry(
290     VACUUM_CATEGORY,
291     contractId,
292     contractId,
293     false,
294     false
295   );
296   registeredVacuumParticipants.set(contractId, { cid, factory });
299 function unregisterVacuumParticipant(connectionData) {
300   let contractId = VACUUM_CONTRACTID + connectionData._identifier;
301   let component = registeredVacuumParticipants.get(contractId);
302   if (component) {
303     Components.manager
304       .QueryInterface(Ci.nsIComponentRegistrar)
305       .unregisterFactory(component.cid, component.factory);
306     Services.catMan.deleteCategoryEntry(VACUUM_CATEGORY, contractId, false);
307   }
311  * Connection data with methods necessary for closing the connection.
313  * To support auto-closing in the event of garbage collection, this
314  * data structure contains all the connection data of an opened
315  * connection and all of the methods needed for sucessfully closing
316  * it.
318  * By putting this information in its own separate object, it is
319  * possible to store an additional reference to it without preventing
320  * a garbage collection of a finalization witness in
321  * OpenedConnection. When the witness detects a garbage collection,
322  * this object can be used to close the connection.
324  * This object contains more methods than just `close`.  When
325  * OpenedConnection needs to use the methods in this object, it will
326  * dispatch its method calls here.
327  */
328 function ConnectionData(connection, identifier, options = {}) {
329   this._log = lazy.Log.repository.getLoggerWithMessagePrefix(
330     "Sqlite.sys.mjs",
331     `Connection ${identifier}: `
332   );
333   this._log.manageLevelFromPref("toolkit.sqlitejsm.loglevel");
334   this._log.debug("Opened");
336   this._dbConn = connection;
338   // This is a unique identifier for the connection, generated through
339   // getIdentifierByFileName.  It may be used for logging or as a key in Maps.
340   this._identifier = identifier;
342   this._open = true;
344   this._cachedStatements = new Map();
345   this._anonymousStatements = new Map();
346   this._anonymousCounter = 0;
348   // A map from statement index to mozIStoragePendingStatement, to allow for
349   // canceling prior to finalizing the mozIStorageStatements.
350   this._pendingStatements = new Map();
352   // Increments for each executed statement for the life of the connection.
353   this._statementCounter = 0;
355   // Increments whenever we request a unique operation id.
356   this._operationsCounter = 0;
358   if ("defaultTransactionType" in options) {
359     this.defaultTransactionType = options.defaultTransactionType;
360   } else {
361     this.defaultTransactionType = convertStorageTransactionType(
362       this._dbConn.defaultTransactionType
363     );
364   }
365   // Tracks whether this instance initiated a transaction.
366   this._initiatedTransaction = false;
367   // Manages a chain of transactions promises, so that new transactions
368   // always happen in queue to the previous ones.  It never rejects.
369   this._transactionQueue = Promise.resolve();
371   this._idleShrinkMS = options.shrinkMemoryOnConnectionIdleMS;
372   if (this._idleShrinkMS) {
373     this._idleShrinkTimer = Cc["@mozilla.org/timer;1"].createInstance(
374       Ci.nsITimer
375     );
376     // We wait for the first statement execute to start the timer because
377     // shrinking now would not do anything.
378   }
380   // Deferred whose promise is resolved when the connection closing procedure
381   // is complete.
382   this._deferredClose = lazy.PromiseUtils.defer();
383   this._closeRequested = false;
385   // An AsyncShutdown barrier used to make sure that we wait until clients
386   // are done before shutting down the connection.
387   this._barrier = new lazy.AsyncShutdown.Barrier(
388     `${this._identifier}: waiting for clients`
389   );
391   lazy.Barriers.connections.client.addBlocker(
392     this._identifier + ": waiting for shutdown",
393     this._deferredClose.promise,
394     () => ({
395       identifier: this._identifier,
396       isCloseRequested: this._closeRequested,
397       hasDbConn: !!this._dbConn,
398       initiatedTransaction: this._initiatedTransaction,
399       pendingStatements: this._pendingStatements.size,
400       statementCounter: this._statementCounter,
401     })
402   );
404   // We avoid creating a timer for every transaction, because in most cases they
405   // are not canceled and they are only used as a timeout.
406   // Instead the timer is reused when it's sufficiently close to the previous
407   // creation time (see `_getTimeoutPromise` for more info).
408   this._timeoutPromise = null;
409   // The last timestamp when we should consider using `this._timeoutPromise`.
410   this._timeoutPromiseExpires = 0;
412   this._useIncrementalVacuum = !!options.incrementalVacuum;
413   if (this._useIncrementalVacuum) {
414     this._log.debug("Set auto_vacuum INCREMENTAL");
415     this.execute("PRAGMA auto_vacuum = 2").catch(ex => {
416       this._log.error("Setting auto_vacuum to INCREMENTAL failed.");
417       console.error(ex);
418     });
419   }
421   this._expectedPageSize = options.pageSize ?? 0;
422   if (this._expectedPageSize) {
423     this._log.debug("Set page_size to " + this._expectedPageSize);
424     this.execute("PRAGMA page_size = " + this._expectedPageSize).catch(ex => {
425       this._log.error(`Setting page_size to ${this._expectedPageSize} failed.`);
426       console.error(ex);
427     });
428   }
430   this._vacuumOnIdle = options.vacuumOnIdle;
431   if (this._vacuumOnIdle) {
432     this._log.debug("Register as vacuum participant");
433     this.QueryInterface = ChromeUtils.generateQI([
434       Ci.mozIStorageVacuumParticipant,
435     ]);
436     registerVacuumParticipant(this);
437   }
441  * Map of connection identifiers to ConnectionData objects
443  * The connection identifier is a human-readable name of the
444  * database. Used by finalization witnesses to be able to close opened
445  * connections on garbage collection.
447  * Key: _identifier of ConnectionData
448  * Value: ConnectionData object
449  */
450 ConnectionData.byId = new Map();
452 ConnectionData.prototype = Object.freeze({
453   get expectedDatabasePageSize() {
454     return this._expectedPageSize;
455   },
457   get useIncrementalVacuum() {
458     return this._useIncrementalVacuum;
459   },
461   /**
462    * This should only be used by the VacuumManager component.
463    * @see unsafeRawConnection for an official (but still unsafe) API.
464    */
465   get databaseConnection() {
466     if (this._vacuumOnIdle) {
467       return this._dbConn;
468     }
469     return null;
470   },
472   onBeginVacuum() {
473     let granted = !this.transactionInProgress;
474     this._log.debug("Begin Vacuum - " + granted ? "granted" : "denied");
475     return granted;
476   },
478   onEndVacuum(succeeded) {
479     this._log.debug("End Vacuum - " + succeeded ? "success" : "failure");
480   },
482   /**
483    * Run a task, ensuring that its execution will not be interrupted by shutdown.
484    *
485    * As the operations of this module are asynchronous, a sequence of operations,
486    * or even an individual operation, can still be pending when the process shuts
487    * down. If any of this operations is a write, this can cause data loss, simply
488    * because the write has not been completed (or even started) by shutdown.
489    *
490    * To avoid this risk, clients are encouraged to use `executeBeforeShutdown` for
491    * any write operation, as follows:
492    *
493    * myConnection.executeBeforeShutdown("Bookmarks: Removing a bookmark",
494    *   async function(db) {
495    *     // The connection will not be closed and shutdown will not proceed
496    *     // until this task has completed.
497    *
498    *     // `db` exposes the same API as `myConnection` but provides additional
499    *     // logging support to help debug hard-to-catch shutdown timeouts.
500    *
501    *     await db.execute(...);
502    * }));
503    *
504    * @param {string} name A human-readable name for the ongoing operation, used
505    *  for logging and debugging purposes.
506    * @param {function(db)} task A function that takes as argument a Sqlite.sys.mjs
507    *  db and returns a Promise.
508    */
509   executeBeforeShutdown(parent, name, task) {
510     if (!name) {
511       throw new TypeError("Expected a human-readable name as first argument");
512     }
513     if (typeof task != "function") {
514       throw new TypeError("Expected a function as second argument");
515     }
516     if (this._closeRequested) {
517       throw new Error(
518         `${this._identifier}: cannot execute operation ${name}, the connection is already closing`
519       );
520     }
522     // Status, used for AsyncShutdown crash reports.
523     let status = {
524       // The latest command started by `task`, either as a
525       // sql string, or as one of "<not started>" or "<closing>".
526       command: "<not started>",
528       // `true` if `command` was started but not completed yet.
529       isPending: false,
530     };
532     // An object with the same API as `this` but with
533     // additional logging. To keep logging simple, we
534     // assume that `task` is not running several queries
535     // concurrently.
536     let loggedDb = Object.create(parent, {
537       execute: {
538         value: async (sql, ...rest) => {
539           status.isPending = true;
540           status.command = sql;
541           try {
542             return await this.execute(sql, ...rest);
543           } finally {
544             status.isPending = false;
545           }
546         },
547       },
548       close: {
549         value: async () => {
550           status.isPending = true;
551           status.command = "<close>";
552           try {
553             return await this.close();
554           } finally {
555             status.isPending = false;
556           }
557         },
558       },
559       executeCached: {
560         value: async (sql, ...rest) => {
561           status.isPending = true;
562           status.command = "cached: " + sql;
563           try {
564             return await this.executeCached(sql, ...rest);
565           } finally {
566             status.isPending = false;
567           }
568         },
569       },
570     });
572     let promiseResult = task(loggedDb);
573     if (
574       !promiseResult ||
575       typeof promiseResult != "object" ||
576       !("then" in promiseResult)
577     ) {
578       throw new TypeError("Expected a Promise");
579     }
580     let key = `${this._identifier}: ${name} (${this._getOperationId()})`;
581     let promiseComplete = promiseResult.catch(() => {});
582     this._barrier.client.addBlocker(key, promiseComplete, {
583       fetchState: () => status,
584     });
586     return (async () => {
587       try {
588         return await promiseResult;
589       } finally {
590         this._barrier.client.removeBlocker(key, promiseComplete);
591       }
592     })();
593   },
594   close() {
595     this._closeRequested = true;
597     if (!this._dbConn) {
598       return this._deferredClose.promise;
599     }
601     this._log.debug("Request to close connection.");
602     this._clearIdleShrinkTimer();
604     if (this._vacuumOnIdle) {
605       this._log.debug("Unregister as vacuum participant");
606       unregisterVacuumParticipant(this);
607     }
609     return this._barrier.wait().then(() => {
610       if (!this._dbConn) {
611         return undefined;
612       }
613       return this._finalize();
614     });
615   },
617   clone(readOnly = false) {
618     this.ensureOpen();
620     this._log.debug("Request to clone connection.");
622     let options = {
623       connection: this._dbConn,
624       readOnly,
625     };
626     if (this._idleShrinkMS) {
627       options.shrinkMemoryOnConnectionIdleMS = this._idleShrinkMS;
628     }
630     return cloneStorageConnection(options);
631   },
632   _getOperationId() {
633     return this._operationsCounter++;
634   },
635   _finalize() {
636     this._log.debug("Finalizing connection.");
637     // Cancel any pending statements.
638     for (let [, /* k */ statement] of this._pendingStatements) {
639       statement.cancel();
640     }
641     this._pendingStatements.clear();
643     // We no longer need to track these.
644     this._statementCounter = 0;
646     // Next we finalize all active statements.
647     for (let [, /* k */ statement] of this._anonymousStatements) {
648       statement.finalize();
649     }
650     this._anonymousStatements.clear();
652     for (let [, /* k */ statement] of this._cachedStatements) {
653       statement.finalize();
654     }
655     this._cachedStatements.clear();
657     // This guards against operations performed between the call to this
658     // function and asyncClose() finishing. See also bug 726990.
659     this._open = false;
661     // We must always close the connection at the Sqlite.sys.mjs-level, not
662     // necessarily at the mozStorage-level.
663     let markAsClosed = () => {
664       this._log.debug("Closed");
665       // Now that the connection is closed, no need to keep
666       // a blocker for Barriers.connections.
667       lazy.Barriers.connections.client.removeBlocker(
668         this._deferredClose.promise
669       );
670       this._deferredClose.resolve();
671     };
672     if (wrappedConnections.has(this._identifier)) {
673       wrappedConnections.delete(this._identifier);
674       this._dbConn = null;
675       markAsClosed();
676     } else {
677       this._log.debug("Calling asyncClose().");
678       try {
679         this._dbConn.asyncClose(markAsClosed);
680       } catch (ex) {
681         // If for any reason asyncClose fails, we must still remove the
682         // shutdown blockers and resolve _deferredClose.
683         markAsClosed();
684       } finally {
685         this._dbConn = null;
686       }
687     }
688     return this._deferredClose.promise;
689   },
691   executeCached(sql, params = null, onRow = null) {
692     this.ensureOpen();
694     if (!sql) {
695       throw new Error("sql argument is empty.");
696     }
698     let statement = this._cachedStatements.get(sql);
699     if (!statement) {
700       statement = this._dbConn.createAsyncStatement(sql);
701       this._cachedStatements.set(sql, statement);
702     }
704     this._clearIdleShrinkTimer();
706     return new Promise((resolve, reject) => {
707       try {
708         this._executeStatement(sql, statement, params, onRow).then(
709           result => {
710             this._startIdleShrinkTimer();
711             resolve(result);
712           },
713           error => {
714             this._startIdleShrinkTimer();
715             reject(error);
716           }
717         );
718       } catch (ex) {
719         this._startIdleShrinkTimer();
720         throw ex;
721       }
722     });
723   },
725   execute(sql, params = null, onRow = null) {
726     if (typeof sql != "string") {
727       throw new Error("Must define SQL to execute as a string: " + sql);
728     }
730     this.ensureOpen();
732     let statement = this._dbConn.createAsyncStatement(sql);
733     let index = this._anonymousCounter++;
735     this._anonymousStatements.set(index, statement);
736     this._clearIdleShrinkTimer();
738     let onFinished = () => {
739       this._anonymousStatements.delete(index);
740       statement.finalize();
741       this._startIdleShrinkTimer();
742     };
744     return new Promise((resolve, reject) => {
745       try {
746         this._executeStatement(sql, statement, params, onRow).then(
747           rows => {
748             onFinished();
749             resolve(rows);
750           },
751           error => {
752             onFinished();
753             reject(error);
754           }
755         );
756       } catch (ex) {
757         onFinished();
758         throw ex;
759       }
760     });
761   },
763   get transactionInProgress() {
764     return this._open && this._dbConn.transactionInProgress;
765   },
767   executeTransaction(func, type) {
768     // Identify the caller for debugging purposes.
769     let caller = new Error().stack
770       .split("\n", 3)
771       .pop()
772       .match(/^([^@]*@).*\/([^\/:]+)[:0-9]*$/);
773     caller = caller[1] + caller[2];
774     this._log.debug(`Transaction (type ${type}) requested by: ${caller}`);
776     if (type == OpenedConnection.prototype.TRANSACTION_DEFAULT) {
777       type = this.defaultTransactionType;
778     } else if (!OpenedConnection.TRANSACTION_TYPES.includes(type)) {
779       throw new Error("Unknown transaction type: " + type);
780     }
781     this.ensureOpen();
783     // If a transaction yields on a never resolved promise, or is mistakenly
784     // nested, it could hang the transactions queue forever.  Thus we timeout
785     // the execution after a meaningful amount of time, to ensure in any case
786     // we'll proceed after a while.
787     let timeoutPromise = this._getTimeoutPromise();
789     let promise = this._transactionQueue.then(() => {
790       if (this._closeRequested) {
791         throw new Error("Transaction canceled due to a closed connection.");
792       }
794       let transactionPromise = (async () => {
795         // At this point we should never have an in progress transaction, since
796         // they are enqueued.
797         if (this._initiatedTransaction) {
798           this._log.error(
799             "Unexpected transaction in progress when trying to start a new one."
800           );
801         }
802         try {
803           // We catch errors in statement execution to detect nested transactions.
804           try {
805             await this.execute("BEGIN " + type + " TRANSACTION");
806             this._log.debug(`Begin transaction`);
807             this._initiatedTransaction = true;
808           } catch (ex) {
809             // Unfortunately, if we are wrapping an existing connection, a
810             // transaction could have been started by a client of the same
811             // connection that doesn't use Sqlite.sys.mjs (e.g. C++ consumer).
812             // The best we can do is proceed without a transaction and hope
813             // things won't break.
814             if (wrappedConnections.has(this._identifier)) {
815               this._log.warn(
816                 "A new transaction could not be started cause the wrapped connection had one in progress",
817                 ex
818               );
819             } else {
820               this._log.warn(
821                 "A transaction was already in progress, likely a nested transaction",
822                 ex
823               );
824               throw ex;
825             }
826           }
828           let result;
829           try {
830             result = await Promise.race([func(), timeoutPromise]);
831           } catch (ex) {
832             // It's possible that the exception has been caused by trying to
833             // close the connection in the middle of a transaction.
834             if (this._closeRequested) {
835               this._log.warn(
836                 "Connection closed while performing a transaction",
837                 ex
838               );
839             } else {
840               // Otherwise the function didn't resolve before the timeout, or
841               // generated an unexpected error. Then we rollback.
842               if (ex.becauseTimedOut) {
843                 let caller_module = caller.split(":", 1)[0];
844                 Services.telemetry.keyedScalarAdd(
845                   "mozstorage.sqlitejsm_transaction_timeout",
846                   caller_module,
847                   1
848                 );
849                 this._log.error(
850                   `The transaction requested by ${caller} timed out. Rolling back`,
851                   ex
852                 );
853               } else {
854                 this._log.error(
855                   `Error during transaction requested by ${caller}. Rolling back`,
856                   ex
857                 );
858               }
859               // If we began a transaction, we must rollback it.
860               if (this._initiatedTransaction) {
861                 try {
862                   await this.execute("ROLLBACK TRANSACTION");
863                   this._initiatedTransaction = false;
864                   this._log.debug(`Roll back transaction`);
865                 } catch (inner) {
866                   this._log.error("Could not roll back transaction", inner);
867                 }
868               }
869             }
870             // Rethrow the exception.
871             throw ex;
872           }
874           // See comment above about connection being closed during transaction.
875           if (this._closeRequested) {
876             this._log.warn(
877               "Connection closed before committing the transaction."
878             );
879             throw new Error(
880               "Connection closed before committing the transaction."
881             );
882           }
884           // If we began a transaction, we must commit it.
885           if (this._initiatedTransaction) {
886             try {
887               await this.execute("COMMIT TRANSACTION");
888               this._log.debug(`Commit transaction`);
889             } catch (ex) {
890               this._log.warn("Error committing transaction", ex);
891               throw ex;
892             }
893           }
895           return result;
896         } finally {
897           this._initiatedTransaction = false;
898         }
899       })();
901       return Promise.race([transactionPromise, timeoutPromise]);
902     });
903     // Atomically update the queue before anyone else has a chance to enqueue
904     // further transactions.
905     this._transactionQueue = promise.catch(ex => {
906       this._log.error(ex);
907     });
909     // Make sure that we do not shutdown the connection during a transaction.
910     this._barrier.client.addBlocker(
911       `Transaction (${this._getOperationId()})`,
912       this._transactionQueue
913     );
914     return promise;
915   },
917   shrinkMemory() {
918     this._log.debug("Shrinking memory usage.");
919     return this.execute("PRAGMA shrink_memory").finally(() => {
920       this._clearIdleShrinkTimer();
921     });
922   },
924   discardCachedStatements() {
925     let count = 0;
926     for (let [, /* k */ statement] of this._cachedStatements) {
927       ++count;
928       statement.finalize();
929     }
930     this._cachedStatements.clear();
931     this._log.debug("Discarded " + count + " cached statements.");
932     return count;
933   },
935   interrupt() {
936     this._log.debug("Trying to interrupt.");
937     this.ensureOpen();
938     this._dbConn.interrupt();
939   },
941   /**
942    * Helper method to bind parameters of various kinds through
943    * reflection.
944    */
945   _bindParameters(statement, params) {
946     if (!params) {
947       return;
948     }
950     function bindParam(obj, key, val) {
951       let isBlob =
952         val && typeof val == "object" && val.constructor.name == "Uint8Array";
953       let args = [key, val];
954       if (isBlob) {
955         args.push(val.length);
956       }
957       let methodName = `bind${isBlob ? "Blob" : ""}By${
958         typeof key == "number" ? "Index" : "Name"
959       }`;
960       obj[methodName](...args);
961     }
963     if (Array.isArray(params)) {
964       // It's an array of separate params.
965       if (params.length && typeof params[0] == "object" && params[0] !== null) {
966         let paramsArray = statement.newBindingParamsArray();
967         for (let p of params) {
968           let bindings = paramsArray.newBindingParams();
969           for (let [key, value] of Object.entries(p)) {
970             bindParam(bindings, key, value);
971           }
972           paramsArray.addParams(bindings);
973         }
975         statement.bindParameters(paramsArray);
976         return;
977       }
979       // Indexed params.
980       for (let i = 0; i < params.length; i++) {
981         bindParam(statement, i, params[i]);
982       }
983       return;
984     }
986     // Named params.
987     if (params && typeof params == "object") {
988       for (let k in params) {
989         bindParam(statement, k, params[k]);
990       }
991       return;
992     }
994     throw new Error(
995       "Invalid type for bound parameters. Expected Array or " +
996         "object. Got: " +
997         params
998     );
999   },
1001   _executeStatement(sql, statement, params, onRow) {
1002     if (statement.state != statement.MOZ_STORAGE_STATEMENT_READY) {
1003       throw new Error("Statement is not ready for execution.");
1004     }
1006     if (onRow && typeof onRow != "function") {
1007       throw new Error("onRow must be a function. Got: " + onRow);
1008     }
1010     this._bindParameters(statement, params);
1012     let index = this._statementCounter++;
1014     let deferred = lazy.PromiseUtils.defer();
1015     let userCancelled = false;
1016     let errors = [];
1017     let rows = [];
1018     let handledRow = false;
1020     // Don't incur overhead for serializing params unless the messages go
1021     // somewhere.
1022     if (this._log.level <= lazy.Log.Level.Trace) {
1023       let msg = "Stmt #" + index + " " + sql;
1025       if (params) {
1026         msg += " - " + JSON.stringify(params);
1027       }
1028       this._log.trace(msg);
1029     } else {
1030       this._log.debug("Stmt #" + index + " starting");
1031     }
1033     let self = this;
1034     let pending = statement.executeAsync({
1035       handleResult(resultSet) {
1036         // .cancel() may not be immediate and handleResult() could be called
1037         // after a .cancel().
1038         for (
1039           let row = resultSet.getNextRow();
1040           row && !userCancelled;
1041           row = resultSet.getNextRow()
1042         ) {
1043           if (!onRow) {
1044             rows.push(row);
1045             continue;
1046           }
1048           handledRow = true;
1050           try {
1051             onRow(row, () => {
1052               userCancelled = true;
1053               pending.cancel();
1054             });
1055           } catch (e) {
1056             self._log.warn("Exception when calling onRow callback", e);
1057           }
1058         }
1059       },
1061       handleError(error) {
1062         self._log.warn(
1063           "Error when executing SQL (" + error.result + "): " + error.message
1064         );
1065         errors.push(error);
1066       },
1068       handleCompletion(reason) {
1069         self._log.debug("Stmt #" + index + " finished.");
1070         self._pendingStatements.delete(index);
1072         switch (reason) {
1073           case Ci.mozIStorageStatementCallback.REASON_FINISHED:
1074           case Ci.mozIStorageStatementCallback.REASON_CANCELED:
1075             // If there is an onRow handler, we always instead resolve to a
1076             // boolean indicating whether the onRow handler was called or not.
1077             let result = onRow ? handledRow : rows;
1078             deferred.resolve(result);
1079             break;
1081           case Ci.mozIStorageStatementCallback.REASON_ERROR:
1082             let error = new Error(
1083               "Error(s) encountered during statement execution: " +
1084                 errors.map(e => e.message).join(", ")
1085             );
1086             error.errors = errors;
1088             // Forward the error result.
1089             // Corruption is the most critical one so it's handled apart.
1090             if (errors.some(e => e.result == Ci.mozIStorageError.CORRUPT)) {
1091               error.result = Cr.NS_ERROR_FILE_CORRUPTED;
1092             } else {
1093               // Just use the first error result in the other cases.
1094               error.result = convertStorageErrorResult(errors[0]?.result);
1095             }
1097             deferred.reject(error);
1098             break;
1100           default:
1101             deferred.reject(
1102               new Error("Unknown completion reason code: " + reason)
1103             );
1104             break;
1105         }
1106       },
1107     });
1109     this._pendingStatements.set(index, pending);
1110     return deferred.promise;
1111   },
1113   ensureOpen() {
1114     if (!this._open) {
1115       throw new Error("Connection is not open.");
1116     }
1117   },
1119   _clearIdleShrinkTimer() {
1120     if (!this._idleShrinkTimer) {
1121       return;
1122     }
1124     this._idleShrinkTimer.cancel();
1125   },
1127   _startIdleShrinkTimer() {
1128     if (!this._idleShrinkTimer) {
1129       return;
1130     }
1132     this._idleShrinkTimer.initWithCallback(
1133       this.shrinkMemory.bind(this),
1134       this._idleShrinkMS,
1135       this._idleShrinkTimer.TYPE_ONE_SHOT
1136     );
1137   },
1139   /**
1140    * Returns a promise that will resolve after a time comprised between 80% of
1141    * `TRANSACTIONS_TIMEOUT_MS` and `TRANSACTIONS_TIMEOUT_MS`. Use
1142    * this method instead of creating several individual timers that may survive
1143    * longer than necessary.
1144    */
1145   _getTimeoutPromise() {
1146     if (this._timeoutPromise && Cu.now() <= this._timeoutPromiseExpires) {
1147       return this._timeoutPromise;
1148     }
1149     let timeoutPromise = new Promise((resolve, reject) => {
1150       setTimeout(() => {
1151         // Clear out this._timeoutPromise if it hasn't changed since we set it.
1152         if (this._timeoutPromise == timeoutPromise) {
1153           this._timeoutPromise = null;
1154         }
1155         let e = new Error(
1156           "Transaction timeout, most likely caused by unresolved pending work."
1157         );
1158         e.becauseTimedOut = true;
1159         reject(e);
1160       }, Sqlite.TRANSACTIONS_TIMEOUT_MS);
1161     });
1162     this._timeoutPromise = timeoutPromise;
1163     this._timeoutPromiseExpires =
1164       Cu.now() + Sqlite.TRANSACTIONS_TIMEOUT_MS * 0.2;
1165     return this._timeoutPromise;
1166   },
1170  * Opens a connection to a SQLite database.
1172  * The following parameters can control the connection:
1174  *   path -- (string) The filesystem path of the database file to open. If the
1175  *       file does not exist, a new database will be created.
1177  *   sharedMemoryCache -- (bool) Whether multiple connections to the database
1178  *       share the same memory cache. Sharing the memory cache likely results
1179  *       in less memory utilization. However, sharing also requires connections
1180  *       to obtain a lock, possibly making database access slower. Defaults to
1181  *       true.
1183  *   shrinkMemoryOnConnectionIdleMS -- (integer) If defined, the connection
1184  *       will attempt to minimize its memory usage after this many
1185  *       milliseconds of connection idle. The connection is idle when no
1186  *       statements are executing. There is no default value which means no
1187  *       automatic memory minimization will occur. Please note that this is
1188  *       *not* a timer on the idle service and this could fire while the
1189  *       application is active.
1191  *   readOnly -- (bool) Whether to open the database with SQLITE_OPEN_READONLY
1192  *       set. If used, writing to the database will fail. Defaults to false.
1194  *   ignoreLockingMode -- (bool) Whether to ignore locks on the database held
1195  *       by other connections. If used, implies readOnly. Defaults to false.
1196  *       USE WITH EXTREME CAUTION. This mode WILL produce incorrect results or
1197  *       return "false positive" corruption errors if other connections write
1198  *       to the DB at the same time.
1200  *   vacuumOnIdle -- (bool) Whether to register this connection to be vacuumed
1201  *       on idle by the VacuumManager component.
1202  *       If you're vacuum-ing an incremental vacuum database, ensure to also
1203  *       set incrementalVacuum to true, otherwise this will try to change it
1204  *       to full vacuum mode.
1206  *   incrementalVacuum -- (bool) if set to true auto_vacuum = INCREMENTAL will
1207  *       be enabled for the database.
1208  *       Changing auto vacuum of an already populated database requires a full
1209  *       VACUUM. You can evaluate to enable vacuumOnIdle for that.
1211  *   pageSize -- (integer) This allows to set a custom page size for the
1212  *       database. It is usually not necessary to set it, since the default
1213  *       value should be good for most consumers.
1214  *       Changing the page size of an already populated database requires a full
1215  *       VACUUM. You can evaluate to enable vacuumOnIdle for that.
1217  *   testDelayedOpenPromise -- (promise) Used by tests to delay the open
1218  *       callback handling and execute code between asyncOpen and its callback.
1220  * FUTURE options to control:
1222  *   special named databases
1223  *   pragma TEMP STORE = MEMORY
1224  *   TRUNCATE JOURNAL
1225  *   SYNCHRONOUS = full
1227  * @param options
1228  *        (Object) Parameters to control connection and open options.
1230  * @return Promise<OpenedConnection>
1231  */
1232 function openConnection(options) {
1233   let log = lazy.Log.repository.getLoggerWithMessagePrefix(
1234     "Sqlite.sys.mjs",
1235     `ConnectionOpener: `
1236   );
1237   log.manageLevelFromPref("toolkit.sqlitejsm.loglevel");
1239   if (!options.path) {
1240     throw new Error("path not specified in connection options.");
1241   }
1243   if (isClosed()) {
1244     throw new Error(
1245       "Sqlite.sys.mjs has been shutdown. Cannot open connection to: " +
1246         options.path
1247     );
1248   }
1250   // Retains absolute paths and normalizes relative as relative to profile.
1251   let path = options.path;
1252   let file;
1253   try {
1254     file = lazy.FileUtils.File(path);
1255   } catch (ex) {
1256     // For relative paths, we will get an exception from trying to initialize
1257     // the file. We must then join this path to the profile directory.
1258     if (ex.result == Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH) {
1259       path = PathUtils.joinRelative(
1260         Services.dirsvc.get("ProfD", Ci.nsIFile).path,
1261         options.path
1262       );
1263       file = lazy.FileUtils.File(path);
1264     } else {
1265       throw ex;
1266     }
1267   }
1269   let sharedMemoryCache =
1270     "sharedMemoryCache" in options ? options.sharedMemoryCache : true;
1272   let openedOptions = {};
1274   if ("shrinkMemoryOnConnectionIdleMS" in options) {
1275     if (!Number.isInteger(options.shrinkMemoryOnConnectionIdleMS)) {
1276       throw new Error(
1277         "shrinkMemoryOnConnectionIdleMS must be an integer. " +
1278           "Got: " +
1279           options.shrinkMemoryOnConnectionIdleMS
1280       );
1281     }
1283     openedOptions.shrinkMemoryOnConnectionIdleMS =
1284       options.shrinkMemoryOnConnectionIdleMS;
1285   }
1287   if ("defaultTransactionType" in options) {
1288     let defaultTransactionType = options.defaultTransactionType;
1289     if (!OpenedConnection.TRANSACTION_TYPES.includes(defaultTransactionType)) {
1290       throw new Error(
1291         "Unknown default transaction type: " + defaultTransactionType
1292       );
1293     }
1295     openedOptions.defaultTransactionType = defaultTransactionType;
1296   }
1298   if ("vacuumOnIdle" in options) {
1299     if (typeof options.vacuumOnIdle != "boolean") {
1300       throw new Error("Invalid vacuumOnIdle: " + options.vacuumOnIdle);
1301     }
1302     openedOptions.vacuumOnIdle = options.vacuumOnIdle;
1303   }
1305   if ("incrementalVacuum" in options) {
1306     if (typeof options.incrementalVacuum != "boolean") {
1307       throw new Error(
1308         "Invalid incrementalVacuum: " + options.incrementalVacuum
1309       );
1310     }
1311     openedOptions.incrementalVacuum = options.incrementalVacuum;
1312   }
1314   if ("pageSize" in options) {
1315     if (
1316       ![512, 1024, 2048, 4096, 8192, 16384, 32768, 65536].includes(
1317         options.pageSize
1318       )
1319     ) {
1320       throw new Error("Invalid pageSize: " + options.pageSize);
1321     }
1322     openedOptions.pageSize = options.pageSize;
1323   }
1325   let identifier = getIdentifierByFileName(PathUtils.filename(path));
1327   log.debug("Opening database: " + path + " (" + identifier + ")");
1329   return new Promise((resolve, reject) => {
1330     let dbOpenOptions = Ci.mozIStorageService.OPEN_DEFAULT;
1331     if (sharedMemoryCache) {
1332       dbOpenOptions |= Ci.mozIStorageService.OPEN_SHARED;
1333     }
1334     if (options.readOnly) {
1335       dbOpenOptions |= Ci.mozIStorageService.OPEN_READONLY;
1336     }
1337     if (options.ignoreLockingMode) {
1338       dbOpenOptions |= Ci.mozIStorageService.OPEN_IGNORE_LOCKING_MODE;
1339       dbOpenOptions |= Ci.mozIStorageService.OPEN_READONLY;
1340     }
1342     let dbConnectionOptions = Ci.mozIStorageService.CONNECTION_DEFAULT;
1344     Services.storage.openAsyncDatabase(
1345       file,
1346       dbOpenOptions,
1347       dbConnectionOptions,
1348       async (status, connection) => {
1349         if (!connection) {
1350           log.error(`Could not open connection to ${path}: ${status}`);
1351           let error = new Components.Exception(
1352             `Could not open connection to ${path}: ${status}`,
1353             status
1354           );
1355           reject(error);
1356           return;
1357         }
1358         log.debug("Connection opened");
1360         if (options.testDelayedOpenPromise) {
1361           await options.testDelayedOpenPromise;
1362         }
1364         if (isClosed()) {
1365           connection.QueryInterface(Ci.mozIStorageAsyncConnection).asyncClose();
1366           reject(
1367             new Error(
1368               "Sqlite.sys.mjs has been shutdown. Cannot open connection to: " +
1369                 options.path
1370             )
1371           );
1372           return;
1373         }
1375         try {
1376           resolve(
1377             new OpenedConnection(
1378               connection.QueryInterface(Ci.mozIStorageAsyncConnection),
1379               identifier,
1380               openedOptions
1381             )
1382           );
1383         } catch (ex) {
1384           log.error("Could not open database", ex);
1385           connection.asyncClose();
1386           reject(ex);
1387         }
1388       }
1389     );
1390   });
1394  * Creates a clone of an existing and open Storage connection.  The clone has
1395  * the same underlying characteristics of the original connection and is
1396  * returned in form of an OpenedConnection handle.
1398  * The following parameters can control the cloned connection:
1400  *   connection -- (mozIStorageAsyncConnection) The original Storage connection
1401  *       to clone.  It's not possible to clone connections to memory databases.
1403  *   readOnly -- (boolean) - If true the clone will be read-only.  If the
1404  *       original connection is already read-only, the clone will be, regardless
1405  *       of this option.  If the original connection is using the shared cache,
1406  *       this parameter will be ignored and the clone will be as privileged as
1407  *       the original connection.
1408  *   shrinkMemoryOnConnectionIdleMS -- (integer) If defined, the connection
1409  *       will attempt to minimize its memory usage after this many
1410  *       milliseconds of connection idle. The connection is idle when no
1411  *       statements are executing. There is no default value which means no
1412  *       automatic memory minimization will occur. Please note that this is
1413  *       *not* a timer on the idle service and this could fire while the
1414  *       application is active.
1417  * @param options
1418  *        (Object) Parameters to control connection and clone options.
1420  * @return Promise<OpenedConnection>
1421  */
1422 function cloneStorageConnection(options) {
1423   let log = lazy.Log.repository.getLoggerWithMessagePrefix(
1424     "Sqlite.sys.mjs",
1425     `ConnectionCloner: `
1426   );
1427   log.manageLevelFromPref("toolkit.sqlitejsm.loglevel");
1429   let source = options && options.connection;
1430   if (!source) {
1431     throw new TypeError("connection not specified in clone options.");
1432   }
1433   if (!(source instanceof Ci.mozIStorageAsyncConnection)) {
1434     throw new TypeError("Connection must be a valid Storage connection.");
1435   }
1437   if (isClosed()) {
1438     throw new Error(
1439       "Sqlite.sys.mjs has been shutdown. Cannot clone connection to: " +
1440         source.databaseFile.path
1441     );
1442   }
1444   let openedOptions = {};
1446   if ("shrinkMemoryOnConnectionIdleMS" in options) {
1447     if (!Number.isInteger(options.shrinkMemoryOnConnectionIdleMS)) {
1448       throw new TypeError(
1449         "shrinkMemoryOnConnectionIdleMS must be an integer. " +
1450           "Got: " +
1451           options.shrinkMemoryOnConnectionIdleMS
1452       );
1453     }
1454     openedOptions.shrinkMemoryOnConnectionIdleMS =
1455       options.shrinkMemoryOnConnectionIdleMS;
1456   }
1458   let path = source.databaseFile.path;
1459   let identifier = getIdentifierByFileName(PathUtils.filename(path));
1461   log.debug("Cloning database: " + path + " (" + identifier + ")");
1463   return new Promise((resolve, reject) => {
1464     source.asyncClone(!!options.readOnly, (status, connection) => {
1465       if (!connection) {
1466         log.error("Could not clone connection: " + status);
1467         reject(new Error("Could not clone connection: " + status));
1468         return;
1469       }
1470       log.debug("Connection cloned");
1472       if (isClosed()) {
1473         connection.QueryInterface(Ci.mozIStorageAsyncConnection).asyncClose();
1474         reject(
1475           new Error(
1476             "Sqlite.sys.mjs has been shutdown. Cannot open connection to: " +
1477               options.path
1478           )
1479         );
1480         return;
1481       }
1483       try {
1484         let conn = connection.QueryInterface(Ci.mozIStorageAsyncConnection);
1485         resolve(new OpenedConnection(conn, identifier, openedOptions));
1486       } catch (ex) {
1487         log.error("Could not clone database", ex);
1488         connection.asyncClose();
1489         reject(ex);
1490       }
1491     });
1492   });
1496  * Wraps an existing and open Storage connection with Sqlite.sys.mjs API.  The
1497  * wrapped connection clone has the same underlying characteristics of the
1498  * original connection and is returned in form of an OpenedConnection handle.
1500  * Clients are responsible for closing both the Sqlite.sys.mjs wrapper and the
1501  * underlying mozStorage connection.
1503  * The following parameters can control the wrapped connection:
1505  *   connection -- (mozIStorageAsyncConnection) The original Storage connection
1506  *       to wrap.
1508  * @param options
1509  *        (Object) Parameters to control connection and wrap options.
1511  * @return Promise<OpenedConnection>
1512  */
1513 function wrapStorageConnection(options) {
1514   let log = lazy.Log.repository.getLoggerWithMessagePrefix(
1515     "Sqlite.sys.mjs",
1516     `ConnectionCloner: `
1517   );
1518   log.manageLevelFromPref("toolkit.sqlitejsm.loglevel");
1520   let connection = options && options.connection;
1521   if (!connection || !(connection instanceof Ci.mozIStorageAsyncConnection)) {
1522     throw new TypeError("connection not specified or invalid.");
1523   }
1525   if (isClosed()) {
1526     throw new Error(
1527       "Sqlite.sys.mjs has been shutdown. Cannot wrap connection to: " +
1528         connection.databaseFile.path
1529     );
1530   }
1532   let identifier = getIdentifierByFileName(connection.databaseFile.leafName);
1534   log.debug("Wrapping database: " + identifier);
1535   return new Promise(resolve => {
1536     try {
1537       let conn = connection.QueryInterface(Ci.mozIStorageAsyncConnection);
1538       let wrapper = new OpenedConnection(conn, identifier);
1539       // We must not handle shutdown of a wrapped connection, since that is
1540       // already handled by the opener.
1541       wrappedConnections.add(identifier);
1542       resolve(wrapper);
1543     } catch (ex) {
1544       log.error("Could not wrap database", ex);
1545       throw ex;
1546     }
1547   });
1551  * Handle on an opened SQLite database.
1553  * This is essentially a glorified wrapper around mozIStorageConnection.
1554  * However, it offers some compelling advantages.
1556  * The main functions on this type are `execute` and `executeCached`. These are
1557  * ultimately how all SQL statements are executed. It's worth explaining their
1558  * differences.
1560  * `execute` is used to execute one-shot SQL statements. These are SQL
1561  * statements that are executed one time and then thrown away. They are useful
1562  * for dynamically generated SQL statements and clients who don't care about
1563  * performance (either their own or wasting resources in the overall
1564  * application). Because of the performance considerations, it is recommended
1565  * to avoid `execute` unless the statement you are executing will only be
1566  * executed once or seldomly.
1568  * `executeCached` is used to execute a statement that will presumably be
1569  * executed multiple times. The statement is parsed once and stuffed away
1570  * inside the connection instance. Subsequent calls to `executeCached` will not
1571  * incur the overhead of creating a new statement object. This should be used
1572  * in preference to `execute` when a specific SQL statement will be executed
1573  * multiple times.
1575  * Instances of this type are not meant to be created outside of this file.
1576  * Instead, first open an instance of `UnopenedSqliteConnection` and obtain
1577  * an instance of this type by calling `open`.
1579  * FUTURE IMPROVEMENTS
1581  *   Ability to enqueue operations. Currently there can be race conditions,
1582  *   especially as far as transactions are concerned. It would be nice to have
1583  *   an enqueueOperation(func) API that serially executes passed functions.
1585  *   Support for SAVEPOINT (named/nested transactions) might be useful.
1587  * @param connection
1588  *        (mozIStorageConnection) Underlying SQLite connection.
1589  * @param identifier
1590  *        (string) The unique identifier of this database. It may be used for
1591  *        logging or as a key in Maps.
1592  * @param options [optional]
1593  *        (object) Options to control behavior of connection. See
1594  *        `openConnection`.
1595  */
1596 function OpenedConnection(connection, identifier, options = {}) {
1597   // Store all connection data in a field distinct from the
1598   // witness. This enables us to store an additional reference to this
1599   // field without preventing garbage collection of
1600   // OpenedConnection. On garbage collection, we will still be able to
1601   // close the database using this extra reference.
1602   this._connectionData = new ConnectionData(connection, identifier, options);
1604   // Store the extra reference in a map with connection identifier as
1605   // key.
1606   ConnectionData.byId.set(
1607     this._connectionData._identifier,
1608     this._connectionData
1609   );
1611   // Make a finalization witness. If this object is garbage collected
1612   // before its `forget` method has been called, an event with topic
1613   // "sqlite-finalization-witness" is broadcasted along with the
1614   // connection identifier string of the database.
1615   this._witness = lazy.FinalizationWitnessService.make(
1616     "sqlite-finalization-witness",
1617     this._connectionData._identifier
1618   );
1621 OpenedConnection.TRANSACTION_TYPES = ["DEFERRED", "IMMEDIATE", "EXCLUSIVE"];
1623 // Converts a `mozIStorageAsyncConnection::TRANSACTION_*` constant into the
1624 // corresponding `OpenedConnection.TRANSACTION_TYPES` constant.
1625 function convertStorageTransactionType(type) {
1626   if (!(type in OpenedConnection.TRANSACTION_TYPES)) {
1627     throw new Error("Unknown storage transaction type: " + type);
1628   }
1629   return OpenedConnection.TRANSACTION_TYPES[type];
1632 OpenedConnection.prototype = Object.freeze({
1633   TRANSACTION_DEFAULT: "DEFAULT",
1634   TRANSACTION_DEFERRED: "DEFERRED",
1635   TRANSACTION_IMMEDIATE: "IMMEDIATE",
1636   TRANSACTION_EXCLUSIVE: "EXCLUSIVE",
1638   /**
1639    * Returns a handle to the underlying `mozIStorageAsyncConnection`. This is
1640    * ⚠️ **extremely unsafe** ⚠️ because `Sqlite.sys.mjs` continues to manage the
1641    * connection's lifecycle, including transactions and shutdown blockers.
1642    * Misusing the raw connection can easily lead to data loss, memory leaks,
1643    * and errors.
1644    *
1645    * Consumers of the raw connection **must not** close or re-wrap it,
1646    * and should not run statements concurrently with `Sqlite.sys.mjs`.
1647    *
1648    * It's _much_ safer to open a `mozIStorage{Async}Connection` yourself,
1649    * and access it from JavaScript via `Sqlite.wrapStorageConnection`.
1650    * `unsafeRawConnection` is an escape hatch for cases where you can't
1651    * do that.
1652    *
1653    * Please do _not_ add new uses of `unsafeRawConnection` without review
1654    * from a storage peer.
1655    */
1656   get unsafeRawConnection() {
1657     return this._connectionData._dbConn;
1658   },
1660   /**
1661    * Returns the maximum number of bound parameters for statements executed
1662    * on this connection.
1663    *
1664    * @type {number}
1665    */
1666   get variableLimit() {
1667     return this.unsafeRawConnection.variableLimit;
1668   },
1670   /**
1671    * The integer schema version of the database.
1672    *
1673    * This is 0 if not schema version has been set.
1674    *
1675    * @return Promise<int>
1676    */
1677   getSchemaVersion(schemaName = "main") {
1678     return this.execute(`PRAGMA ${schemaName}.user_version`).then(result =>
1679       result[0].getInt32(0)
1680     );
1681   },
1683   setSchemaVersion(value, schemaName = "main") {
1684     if (!Number.isInteger(value)) {
1685       // Guarding against accidental SQLi
1686       throw new TypeError("Schema version must be an integer. Got " + value);
1687     }
1688     this._connectionData.ensureOpen();
1689     return this.execute(`PRAGMA ${schemaName}.user_version = ${value}`);
1690   },
1692   /**
1693    * Close the database connection.
1694    *
1695    * This must be performed when you are finished with the database.
1696    *
1697    * Closing the database connection has the side effect of forcefully
1698    * cancelling all active statements. Therefore, callers should ensure that
1699    * all active statements have completed before closing the connection, if
1700    * possible.
1701    *
1702    * The returned promise will be resolved once the connection is closed.
1703    * Successive calls to close() return the same promise.
1704    *
1705    * IMPROVEMENT: Resolve the promise to a closed connection which can be
1706    * reopened.
1707    *
1708    * @return Promise<>
1709    */
1710   close() {
1711     // Unless cleanup has already been done by a previous call to
1712     // `close`, delete the database entry from map and tell the
1713     // finalization witness to forget.
1714     if (ConnectionData.byId.has(this._connectionData._identifier)) {
1715       ConnectionData.byId.delete(this._connectionData._identifier);
1716       this._witness.forget();
1717     }
1718     return this._connectionData.close();
1719   },
1721   /**
1722    * Clones this connection to a new Sqlite one.
1723    *
1724    * The following parameters can control the cloned connection:
1725    *
1726    * @param readOnly
1727    *        (boolean) - If true the clone will be read-only.  If the original
1728    *        connection is already read-only, the clone will be, regardless of
1729    *        this option.  If the original connection is using the shared cache,
1730    *        this parameter will be ignored and the clone will be as privileged as
1731    *        the original connection.
1732    *
1733    * @return Promise<OpenedConnection>
1734    */
1735   clone(readOnly = false) {
1736     return this._connectionData.clone(readOnly);
1737   },
1739   executeBeforeShutdown(name, task) {
1740     return this._connectionData.executeBeforeShutdown(this, name, task);
1741   },
1743   /**
1744    * Execute a SQL statement and cache the underlying statement object.
1745    *
1746    * This function executes a SQL statement and also caches the underlying
1747    * derived statement object so subsequent executions are faster and use
1748    * less resources.
1749    *
1750    * This function optionally binds parameters to the statement as well as
1751    * optionally invokes a callback for every row retrieved.
1752    *
1753    * By default, no parameters are bound and no callback will be invoked for
1754    * every row.
1755    *
1756    * Bound parameters can be defined as an Array of positional arguments or
1757    * an object mapping named parameters to their values. If there are no bound
1758    * parameters, the caller can pass nothing or null for this argument.
1759    *
1760    * Callers are encouraged to pass objects rather than Arrays for bound
1761    * parameters because they prevent foot guns. With positional arguments, it
1762    * is simple to modify the parameter count or positions without fixing all
1763    * users of the statement. Objects/named parameters are a little safer
1764    * because changes in order alone won't result in bad things happening.
1765    *
1766    * When `onRow` is not specified, all returned rows are buffered before the
1767    * returned promise is resolved. For INSERT or UPDATE statements, this has
1768    * no effect because no rows are returned from these. However, it has
1769    * implications for SELECT statements.
1770    *
1771    * If your SELECT statement could return many rows or rows with large amounts
1772    * of data, for performance reasons it is recommended to pass an `onRow`
1773    * handler. Otherwise, the buffering may consume unacceptable amounts of
1774    * resources.
1775    *
1776    * If the second parameter of an `onRow` handler is called during execution
1777    * of the `onRow` handler, the execution of the statement is immediately
1778    * cancelled. Subsequent rows will not be processed and no more `onRow`
1779    * invocations will be made. The promise is resolved immediately.
1780    *
1781    * If an exception is thrown by the `onRow` handler, the exception is logged
1782    * and processing of subsequent rows occurs as if nothing happened. The
1783    * promise is still resolved (not rejected).
1784    *
1785    * The return value is a promise that will be resolved when the statement
1786    * has completed fully.
1787    *
1788    * The promise will be rejected with an `Error` instance if the statement
1789    * did not finish execution fully. The `Error` may have an `errors` property.
1790    * If defined, it will be an Array of objects describing individual errors.
1791    * Each object has the properties `result` and `message`. `result` is a
1792    * numeric error code and `message` is a string description of the problem.
1793    *
1794    * @param name
1795    *        (string) The name of the registered statement to execute.
1796    * @param params optional
1797    *        (Array or object) Parameters to bind.
1798    * @param onRow optional
1799    *        (function) Callback to receive each row from result.
1800    */
1801   executeCached(sql, params = null, onRow = null) {
1802     if (isInvalidBoundLikeQuery(sql)) {
1803       throw new Error("Please enter a LIKE clause with bindings");
1804     }
1805     return this._connectionData.executeCached(sql, params, onRow);
1806   },
1808   /**
1809    * Execute a one-shot SQL statement.
1810    *
1811    * If you find yourself feeding the same SQL string in this function, you
1812    * should *not* use this function and instead use `executeCached`.
1813    *
1814    * See `executeCached` for the meaning of the arguments and extended usage info.
1815    *
1816    * @param sql
1817    *        (string) SQL to execute.
1818    * @param params optional
1819    *        (Array or Object) Parameters to bind to the statement.
1820    * @param onRow optional
1821    *        (function) Callback to receive result of a single row.
1822    */
1823   execute(sql, params = null, onRow = null) {
1824     if (isInvalidBoundLikeQuery(sql)) {
1825       throw new Error("Please enter a LIKE clause with bindings");
1826     }
1827     return this._connectionData.execute(sql, params, onRow);
1828   },
1830   /**
1831    * The default behavior for transactions run on this connection.
1832    */
1833   get defaultTransactionType() {
1834     return this._connectionData.defaultTransactionType;
1835   },
1837   /**
1838    * Whether a transaction is currently in progress.
1839    *
1840    * Note that this is true if a transaction is active on the connection,
1841    * regardless of whether it was started by `Sqlite.sys.mjs` or another consumer.
1842    * See the explanation above `mozIStorageConnection.transactionInProgress` for
1843    * why this distinction matters.
1844    */
1845   get transactionInProgress() {
1846     return this._connectionData.transactionInProgress;
1847   },
1849   /**
1850    * Perform a transaction.
1851    *
1852    * *****************************************************************************
1853    * YOU SHOULD _NEVER_ NEST executeTransaction CALLS FOR ANY REASON, NOR
1854    * DIRECTLY, NOR THROUGH OTHER PROMISES.
1855    * FOR EXAMPLE, NEVER DO SOMETHING LIKE:
1856    *   await executeTransaction(async function () {
1857    *     ...some_code...
1858    *     await executeTransaction(async function () { // WRONG!
1859    *       ...some_code...
1860    *     })
1861    *     await someCodeThatExecuteTransaction(); // WRONG!
1862    *     await neverResolvedPromise; // WRONG!
1863    *   });
1864    * NESTING CALLS WILL BLOCK ANY FUTURE TRANSACTION UNTIL A TIMEOUT KICKS IN.
1865    * *****************************************************************************
1866    *
1867    * A transaction is specified by a user-supplied function that is an
1868    * async function. The function receives this connection instance as its argument.
1869    *
1870    * The supplied function is expected to return promises. These are often
1871    * promises created by calling `execute` and `executeCached`. If the
1872    * generator is exhausted without any errors being thrown, the
1873    * transaction is committed. If an error occurs, the transaction is
1874    * rolled back.
1875    *
1876    * The returned value from this function is a promise that will be resolved
1877    * once the transaction has been committed or rolled back. The promise will
1878    * be resolved to whatever value the supplied function resolves to. If
1879    * the transaction is rolled back, the promise is rejected.
1880    *
1881    * @param func
1882    *        (function) What to perform as part of the transaction.
1883    * @param type optional
1884    *        One of the TRANSACTION_* constants attached to this type.
1885    */
1886   executeTransaction(func, type = this.TRANSACTION_DEFAULT) {
1887     return this._connectionData.executeTransaction(() => func(this), type);
1888   },
1890   /**
1891    * Whether a table exists in the database (both persistent and temporary tables).
1892    *
1893    * @param name
1894    *        (string) Name of the table.
1895    *
1896    * @return Promise<bool>
1897    */
1898   tableExists(name) {
1899     return this.execute(
1900       "SELECT name FROM (SELECT * FROM sqlite_master UNION ALL " +
1901         "SELECT * FROM sqlite_temp_master) " +
1902         "WHERE type = 'table' AND name=?",
1903       [name]
1904     ).then(function onResult(rows) {
1905       return Promise.resolve(!!rows.length);
1906     });
1907   },
1909   /**
1910    * Whether a named index exists (both persistent and temporary tables).
1911    *
1912    * @param name
1913    *        (string) Name of the index.
1914    *
1915    * @return Promise<bool>
1916    */
1917   indexExists(name) {
1918     return this.execute(
1919       "SELECT name FROM (SELECT * FROM sqlite_master UNION ALL " +
1920         "SELECT * FROM sqlite_temp_master) " +
1921         "WHERE type = 'index' AND name=?",
1922       [name]
1923     ).then(function onResult(rows) {
1924       return Promise.resolve(!!rows.length);
1925     });
1926   },
1928   /**
1929    * Free up as much memory from the underlying database connection as possible.
1930    *
1931    * @return Promise<>
1932    */
1933   shrinkMemory() {
1934     return this._connectionData.shrinkMemory();
1935   },
1937   /**
1938    * Discard all cached statements.
1939    *
1940    * Note that this relies on us being non-interruptible between
1941    * the insertion or retrieval of a statement in the cache and its
1942    * execution: we finalize all statements, which is only safe if
1943    * they will not be executed again.
1944    *
1945    * @return (integer) the number of statements discarded.
1946    */
1947   discardCachedStatements() {
1948     return this._connectionData.discardCachedStatements();
1949   },
1951   /**
1952    * Interrupts pending database operations returning at the first opportunity.
1953    * Statement execution will throw an NS_ERROR_ABORT failure.
1954    * Can only be used on read-only connections.
1955    */
1956   interrupt() {
1957     this._connectionData.interrupt();
1958   },
1961 export var Sqlite = {
1962   // The maximum time to wait before considering a transaction stuck and
1963   // issuing a ROLLBACK, see `executeTransaction`. Could be modified by tests.
1964   TRANSACTIONS_TIMEOUT_MS: 300000, // 5 minutes
1966   openConnection,
1967   cloneStorageConnection,
1968   wrapStorageConnection,
1969   /**
1970    * Shutdown barrier client. May be used by clients to perform last-minute
1971    * cleanup prior to the shutdown of this module.
1972    *
1973    * See the documentation of AsyncShutdown.Barrier.prototype.client.
1974    */
1975   get shutdown() {
1976     return lazy.Barriers.shutdown.client;
1977   },
1978   failTestsOnAutoClose(enabled) {
1979     Debugging.failTestsOnAutoClose = enabled;
1980   },