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