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