toolkit/modules/Sqlite.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     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 "use strict";
     7 this.EXPORTED_SYMBOLS = [
     8   "Sqlite",
     9 ];
    11 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
    13 Cu.import("resource://gre/modules/Promise.jsm");
    14 Cu.import("resource://gre/modules/osfile.jsm");
    15 Cu.import("resource://gre/modules/Services.jsm");
    16 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    17 Cu.import("resource://gre/modules/Log.jsm");
    19 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
    20                                   "resource://gre/modules/AsyncShutdown.jsm");
    21 XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
    22                                   "resource://services-common/utils.js");
    23 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
    24                                   "resource://gre/modules/FileUtils.jsm");
    25 XPCOMUtils.defineLazyModuleGetter(this, "Task",
    26                                   "resource://gre/modules/Task.jsm");
    29 // Counts the number of created connections per database basename(). This is
    30 // used for logging to distinguish connection instances.
    31 let connectionCounters = new Map();
    34 /**
    35  * Opens a connection to a SQLite database.
    36  *
    37  * The following parameters can control the connection:
    38  *
    39  *   path -- (string) The filesystem path of the database file to open. If the
    40  *       file does not exist, a new database will be created.
    41  *
    42  *   sharedMemoryCache -- (bool) Whether multiple connections to the database
    43  *       share the same memory cache. Sharing the memory cache likely results
    44  *       in less memory utilization. However, sharing also requires connections
    45  *       to obtain a lock, possibly making database access slower. Defaults to
    46  *       true.
    47  *
    48  *   shrinkMemoryOnConnectionIdleMS -- (integer) If defined, the connection
    49  *       will attempt to minimize its memory usage after this many
    50  *       milliseconds of connection idle. The connection is idle when no
    51  *       statements are executing. There is no default value which means no
    52  *       automatic memory minimization will occur. Please note that this is
    53  *       *not* a timer on the idle service and this could fire while the
    54  *       application is active.
    55  *
    56  * FUTURE options to control:
    57  *
    58  *   special named databases
    59  *   pragma TEMP STORE = MEMORY
    60  *   TRUNCATE JOURNAL
    61  *   SYNCHRONOUS = full
    62  *
    63  * @param options
    64  *        (Object) Parameters to control connection and open options.
    65  *
    66  * @return Promise<OpenedConnection>
    67  */
    68 function openConnection(options) {
    69   let log = Log.repository.getLogger("Sqlite.ConnectionOpener");
    71   if (!options.path) {
    72     throw new Error("path not specified in connection options.");
    73   }
    75   // Retains absolute paths and normalizes relative as relative to profile.
    76   let path = OS.Path.join(OS.Constants.Path.profileDir, options.path);
    78   let sharedMemoryCache = "sharedMemoryCache" in options ?
    79                             options.sharedMemoryCache : true;
    81   let openedOptions = {};
    83   if ("shrinkMemoryOnConnectionIdleMS" in options) {
    84     if (!Number.isInteger(options.shrinkMemoryOnConnectionIdleMS)) {
    85       throw new Error("shrinkMemoryOnConnectionIdleMS must be an integer. " +
    86                       "Got: " + options.shrinkMemoryOnConnectionIdleMS);
    87     }
    89     openedOptions.shrinkMemoryOnConnectionIdleMS =
    90       options.shrinkMemoryOnConnectionIdleMS;
    91   }
    93   let file = FileUtils.File(path);
    95   let basename = OS.Path.basename(path);
    96   let number = connectionCounters.get(basename) || 0;
    97   connectionCounters.set(basename, number + 1);
    99   let identifier = basename + "#" + number;
   101   log.info("Opening database: " + path + " (" + identifier + ")");
   102   let deferred = Promise.defer();
   103   let options = null;
   104   if (!sharedMemoryCache) {
   105     options = Cc["@mozilla.org/hash-property-bag;1"].
   106       createInstance(Ci.nsIWritablePropertyBag);
   107     options.setProperty("shared", false);
   108   }
   109   Services.storage.openAsyncDatabase(file, options, function(status, connection) {
   110     if (!connection) {
   111       log.warn("Could not open connection: " + status);
   112       deferred.reject(new Error("Could not open connection: " + status));
   113       return;
   114     }
   115     log.info("Connection opened");
   116     try {
   117       deferred.resolve(
   118         new OpenedConnection(connection.QueryInterface(Ci.mozIStorageAsyncConnection), basename, number,
   119         openedOptions));
   120     } catch (ex) {
   121       log.warn("Could not open database: " + CommonUtils.exceptionStr(ex));
   122       deferred.reject(ex);
   123     }
   124   });
   125   return deferred.promise;
   126 }
   128 /**
   129  * Creates a clone of an existing and open Storage connection.  The clone has
   130  * the same underlying characteristics of the original connection and is
   131  * returned in form of on OpenedConnection handle.
   132  *
   133  * The following parameters can control the cloned connection:
   134  *
   135  *   connection -- (mozIStorageAsyncConnection) The original Storage connection
   136  *       to clone.  It's not possible to clone connections to memory databases.
   137  *
   138  *   readOnly -- (boolean) - If true the clone will be read-only.  If the
   139  *       original connection is already read-only, the clone will be, regardless
   140  *       of this option.  If the original connection is using the shared cache,
   141  *       this parameter will be ignored and the clone will be as privileged as
   142  *       the original connection.
   143  *   shrinkMemoryOnConnectionIdleMS -- (integer) If defined, the connection
   144  *       will attempt to minimize its memory usage after this many
   145  *       milliseconds of connection idle. The connection is idle when no
   146  *       statements are executing. There is no default value which means no
   147  *       automatic memory minimization will occur. Please note that this is
   148  *       *not* a timer on the idle service and this could fire while the
   149  *       application is active.
   150  *
   151  *
   152  * @param options
   153  *        (Object) Parameters to control connection and clone options.
   154  *
   155  * @return Promise<OpenedConnection>
   156  */
   157 function cloneStorageConnection(options) {
   158   let log = Log.repository.getLogger("Sqlite.ConnectionCloner");
   160   let source = options && options.connection;
   161   if (!source) {
   162     throw new TypeError("connection not specified in clone options.");
   163   }
   164   if (!source instanceof Ci.mozIStorageAsyncConnection) {
   165     throw new TypeError("Connection must be a valid Storage connection.")
   166   }
   168   let openedOptions = {};
   170   if ("shrinkMemoryOnConnectionIdleMS" in options) {
   171     if (!Number.isInteger(options.shrinkMemoryOnConnectionIdleMS)) {
   172       throw new TypeError("shrinkMemoryOnConnectionIdleMS must be an integer. " +
   173                           "Got: " + options.shrinkMemoryOnConnectionIdleMS);
   174     }
   175     openedOptions.shrinkMemoryOnConnectionIdleMS =
   176       options.shrinkMemoryOnConnectionIdleMS;
   177   }
   179   let path = source.databaseFile.path;
   180   let basename = OS.Path.basename(path);
   181   let number = connectionCounters.get(basename) || 0;
   182   connectionCounters.set(basename, number + 1);
   183   let identifier = basename + "#" + number;
   185   log.info("Cloning database: " + path + " (" + identifier + ")");
   186   let deferred = Promise.defer();
   188   source.asyncClone(!!options.readOnly, (status, connection) => {
   189     if (!connection) {
   190       log.warn("Could not clone connection: " + status);
   191       deferred.reject(new Error("Could not clone connection: " + status));
   192     }
   193     log.info("Connection cloned");
   194     try {
   195       let conn = connection.QueryInterface(Ci.mozIStorageAsyncConnection);
   196       deferred.resolve(new OpenedConnection(conn, basename, number,
   197                                             openedOptions));
   198     } catch (ex) {
   199       log.warn("Could not clone database: " + CommonUtils.exceptionStr(ex));
   200       deferred.reject(ex);
   201     }
   202   });
   203   return deferred.promise;
   204 }
   206 /**
   207  * Handle on an opened SQLite database.
   208  *
   209  * This is essentially a glorified wrapper around mozIStorageConnection.
   210  * However, it offers some compelling advantages.
   211  *
   212  * The main functions on this type are `execute` and `executeCached`. These are
   213  * ultimately how all SQL statements are executed. It's worth explaining their
   214  * differences.
   215  *
   216  * `execute` is used to execute one-shot SQL statements. These are SQL
   217  * statements that are executed one time and then thrown away. They are useful
   218  * for dynamically generated SQL statements and clients who don't care about
   219  * performance (either their own or wasting resources in the overall
   220  * application). Because of the performance considerations, it is recommended
   221  * to avoid `execute` unless the statement you are executing will only be
   222  * executed once or seldomly.
   223  *
   224  * `executeCached` is used to execute a statement that will presumably be
   225  * executed multiple times. The statement is parsed once and stuffed away
   226  * inside the connection instance. Subsequent calls to `executeCached` will not
   227  * incur the overhead of creating a new statement object. This should be used
   228  * in preference to `execute` when a specific SQL statement will be executed
   229  * multiple times.
   230  *
   231  * Instances of this type are not meant to be created outside of this file.
   232  * Instead, first open an instance of `UnopenedSqliteConnection` and obtain
   233  * an instance of this type by calling `open`.
   234  *
   235  * FUTURE IMPROVEMENTS
   236  *
   237  *   Ability to enqueue operations. Currently there can be race conditions,
   238  *   especially as far as transactions are concerned. It would be nice to have
   239  *   an enqueueOperation(func) API that serially executes passed functions.
   240  *
   241  *   Support for SAVEPOINT (named/nested transactions) might be useful.
   242  *
   243  * @param connection
   244  *        (mozIStorageConnection) Underlying SQLite connection.
   245  * @param basename
   246  *        (string) The basename of this database name. Used for logging.
   247  * @param number
   248  *        (Number) The connection number to this database.
   249  * @param options
   250  *        (object) Options to control behavior of connection. See
   251  *        `openConnection`.
   252  */
   253 function OpenedConnection(connection, basename, number, options) {
   254   this._log = Log.repository.getLoggerWithMessagePrefix("Sqlite.Connection." + basename,
   255                                                         "Conn #" + number + ": ");
   257   this._log.info("Opened");
   259   this._connection = connection;
   260   this._connectionIdentifier = basename + " Conn #" + number;
   261   this._open = true;
   263   this._cachedStatements = new Map();
   264   this._anonymousStatements = new Map();
   265   this._anonymousCounter = 0;
   267   // A map from statement index to mozIStoragePendingStatement, to allow for
   268   // canceling prior to finalizing the mozIStorageStatements.
   269   this._pendingStatements = new Map();
   271   // Increments for each executed statement for the life of the connection.
   272   this._statementCounter = 0;
   274   this._inProgressTransaction = null;
   276   this._idleShrinkMS = options.shrinkMemoryOnConnectionIdleMS;
   277   if (this._idleShrinkMS) {
   278     this._idleShrinkTimer = Cc["@mozilla.org/timer;1"]
   279                               .createInstance(Ci.nsITimer);
   280     // We wait for the first statement execute to start the timer because
   281     // shrinking now would not do anything.
   282   }
   283 }
   285 OpenedConnection.prototype = Object.freeze({
   286   TRANSACTION_DEFERRED: "DEFERRED",
   287   TRANSACTION_IMMEDIATE: "IMMEDIATE",
   288   TRANSACTION_EXCLUSIVE: "EXCLUSIVE",
   290   TRANSACTION_TYPES: ["DEFERRED", "IMMEDIATE", "EXCLUSIVE"],
   292   /**
   293    * The integer schema version of the database.
   294    *
   295    * This is 0 if not schema version has been set.
   296    *
   297    * @return Promise<int>
   298    */
   299   getSchemaVersion: function() {
   300     let self = this;
   301     return this.execute("PRAGMA user_version").then(
   302       function onSuccess(result) {
   303         if (result == null) {
   304           return 0;
   305         }
   306         return JSON.stringify(result[0].getInt32(0));
   307       }
   308     );
   309   },
   311   setSchemaVersion: function(value) {
   312     if (!Number.isInteger(value)) {
   313       // Guarding against accidental SQLi
   314       throw new TypeError("Schema version must be an integer. Got " + value);
   315     }
   316     this._ensureOpen();
   317     return this.execute("PRAGMA user_version = " + value);
   318   },
   320   /**
   321    * Close the database connection.
   322    *
   323    * This must be performed when you are finished with the database.
   324    *
   325    * Closing the database connection has the side effect of forcefully
   326    * cancelling all active statements. Therefore, callers should ensure that
   327    * all active statements have completed before closing the connection, if
   328    * possible.
   329    *
   330    * The returned promise will be resolved once the connection is closed.
   331    *
   332    * IMPROVEMENT: Resolve the promise to a closed connection which can be
   333    * reopened.
   334    *
   335    * @return Promise<>
   336    */
   337   close: function () {
   338     if (!this._connection) {
   339       return Promise.resolve();
   340     }
   342     this._log.debug("Request to close connection.");
   343     this._clearIdleShrinkTimer();
   344     let deferred = Promise.defer();
   346     AsyncShutdown.profileBeforeChange.addBlocker(
   347       "Sqlite.jsm: " + this._connectionIdentifier,
   348       deferred.promise
   349     );
   351     // We need to take extra care with transactions during shutdown.
   352     //
   353     // If we don't have a transaction in progress, we can proceed with shutdown
   354     // immediately.
   355     if (!this._inProgressTransaction) {
   356       this._finalize(deferred);
   357       return deferred.promise;
   358     }
   360     // Else if we do have a transaction in progress, we forcefully roll it
   361     // back. This is an async task, so we wait on it to finish before
   362     // performing finalization.
   363     this._log.warn("Transaction in progress at time of close. Rolling back.");
   365     let onRollback = this._finalize.bind(this, deferred);
   367     this.execute("ROLLBACK TRANSACTION").then(onRollback, onRollback);
   368     this._inProgressTransaction.reject(new Error("Connection being closed."));
   369     this._inProgressTransaction = null;
   371     return deferred.promise;
   372   },
   374   /**
   375    * Clones this connection to a new Sqlite one.
   376    *
   377    * The following parameters can control the cloned connection:
   378    *
   379    * @param readOnly
   380    *        (boolean) - If true the clone will be read-only.  If the original
   381    *        connection is already read-only, the clone will be, regardless of
   382    *        this option.  If the original connection is using the shared cache,
   383    *        this parameter will be ignored and the clone will be as privileged as
   384    *        the original connection.
   385    *
   386    * @return Promise<OpenedConnection>
   387    */
   388   clone: function (readOnly=false) {
   389     this._ensureOpen();
   391     this._log.debug("Request to clone connection.");
   393     let options = {
   394       connection: this._connection,
   395       readOnly: readOnly,
   396     };
   397     if (this._idleShrinkMS)
   398       options.shrinkMemoryOnConnectionIdleMS = this._idleShrinkMS;
   400     return cloneStorageConnection(options);
   401   },
   403   _finalize: function (deferred) {
   404     this._log.debug("Finalizing connection.");
   405     // Cancel any pending statements.
   406     for (let [k, statement] of this._pendingStatements) {
   407       statement.cancel();
   408     }
   409     this._pendingStatements.clear();
   411     // We no longer need to track these.
   412     this._statementCounter = 0;
   414     // Next we finalize all active statements.
   415     for (let [k, statement] of this._anonymousStatements) {
   416       statement.finalize();
   417     }
   418     this._anonymousStatements.clear();
   420     for (let [k, statement] of this._cachedStatements) {
   421       statement.finalize();
   422     }
   423     this._cachedStatements.clear();
   425     // This guards against operations performed between the call to this
   426     // function and asyncClose() finishing. See also bug 726990.
   427     this._open = false;
   429     this._log.debug("Calling asyncClose().");
   430     this._connection.asyncClose({
   431       complete: function () {
   432         this._log.info("Closed");
   433         this._connection = null;
   434         deferred.resolve();
   435       }.bind(this),
   436     });
   437   },
   439   /**
   440    * Execute a SQL statement and cache the underlying statement object.
   441    *
   442    * This function executes a SQL statement and also caches the underlying
   443    * derived statement object so subsequent executions are faster and use
   444    * less resources.
   445    *
   446    * This function optionally binds parameters to the statement as well as
   447    * optionally invokes a callback for every row retrieved.
   448    *
   449    * By default, no parameters are bound and no callback will be invoked for
   450    * every row.
   451    *
   452    * Bound parameters can be defined as an Array of positional arguments or
   453    * an object mapping named parameters to their values. If there are no bound
   454    * parameters, the caller can pass nothing or null for this argument.
   455    *
   456    * Callers are encouraged to pass objects rather than Arrays for bound
   457    * parameters because they prevent foot guns. With positional arguments, it
   458    * is simple to modify the parameter count or positions without fixing all
   459    * users of the statement. Objects/named parameters are a little safer
   460    * because changes in order alone won't result in bad things happening.
   461    *
   462    * When `onRow` is not specified, all returned rows are buffered before the
   463    * returned promise is resolved. For INSERT or UPDATE statements, this has
   464    * no effect because no rows are returned from these. However, it has
   465    * implications for SELECT statements.
   466    *
   467    * If your SELECT statement could return many rows or rows with large amounts
   468    * of data, for performance reasons it is recommended to pass an `onRow`
   469    * handler. Otherwise, the buffering may consume unacceptable amounts of
   470    * resources.
   471    *
   472    * If a `StopIteration` is thrown during execution of an `onRow` handler,
   473    * the execution of the statement is immediately cancelled. Subsequent
   474    * rows will not be processed and no more `onRow` invocations will be made.
   475    * The promise is resolved immediately.
   476    *
   477    * If a non-`StopIteration` exception is thrown by the `onRow` handler, the
   478    * exception is logged and processing of subsequent rows occurs as if nothing
   479    * happened. The promise is still resolved (not rejected).
   480    *
   481    * The return value is a promise that will be resolved when the statement
   482    * has completed fully.
   483    *
   484    * The promise will be rejected with an `Error` instance if the statement
   485    * did not finish execution fully. The `Error` may have an `errors` property.
   486    * If defined, it will be an Array of objects describing individual errors.
   487    * Each object has the properties `result` and `message`. `result` is a
   488    * numeric error code and `message` is a string description of the problem.
   489    *
   490    * @param name
   491    *        (string) The name of the registered statement to execute.
   492    * @param params optional
   493    *        (Array or object) Parameters to bind.
   494    * @param onRow optional
   495    *        (function) Callback to receive each row from result.
   496    */
   497   executeCached: function (sql, params=null, onRow=null) {
   498     this._ensureOpen();
   500     if (!sql) {
   501       throw new Error("sql argument is empty.");
   502     }
   504     let statement = this._cachedStatements.get(sql);
   505     if (!statement) {
   506       statement = this._connection.createAsyncStatement(sql);
   507       this._cachedStatements.set(sql, statement);
   508     }
   510     this._clearIdleShrinkTimer();
   512     let deferred = Promise.defer();
   514     try {
   515       this._executeStatement(sql, statement, params, onRow).then(
   516         function onResult(result) {
   517           this._startIdleShrinkTimer();
   518           deferred.resolve(result);
   519         }.bind(this),
   520         function onError(error) {
   521           this._startIdleShrinkTimer();
   522           deferred.reject(error);
   523         }.bind(this)
   524       );
   525     } catch (ex) {
   526       this._startIdleShrinkTimer();
   527       throw ex;
   528     }
   530     return deferred.promise;
   531   },
   533   /**
   534    * Execute a one-shot SQL statement.
   535    *
   536    * If you find yourself feeding the same SQL string in this function, you
   537    * should *not* use this function and instead use `executeCached`.
   538    *
   539    * See `executeCached` for the meaning of the arguments and extended usage info.
   540    *
   541    * @param sql
   542    *        (string) SQL to execute.
   543    * @param params optional
   544    *        (Array or Object) Parameters to bind to the statement.
   545    * @param onRow optional
   546    *        (function) Callback to receive result of a single row.
   547    */
   548   execute: function (sql, params=null, onRow=null) {
   549     if (typeof(sql) != "string") {
   550       throw new Error("Must define SQL to execute as a string: " + sql);
   551     }
   553     this._ensureOpen();
   555     let statement = this._connection.createAsyncStatement(sql);
   556     let index = this._anonymousCounter++;
   558     this._anonymousStatements.set(index, statement);
   559     this._clearIdleShrinkTimer();
   561     let onFinished = function () {
   562       this._anonymousStatements.delete(index);
   563       statement.finalize();
   564       this._startIdleShrinkTimer();
   565     }.bind(this);
   567     let deferred = Promise.defer();
   569     try {
   570       this._executeStatement(sql, statement, params, onRow).then(
   571         function onResult(rows) {
   572           onFinished();
   573           deferred.resolve(rows);
   574         }.bind(this),
   576         function onError(error) {
   577           onFinished();
   578           deferred.reject(error);
   579         }.bind(this)
   580       );
   581     } catch (ex) {
   582       onFinished();
   583       throw ex;
   584     }
   586     return deferred.promise;
   587   },
   589   /**
   590    * Whether a transaction is currently in progress.
   591    */
   592   get transactionInProgress() {
   593     return this._open && !!this._inProgressTransaction;
   594   },
   596   /**
   597    * Perform a transaction.
   598    *
   599    * A transaction is specified by a user-supplied function that is a
   600    * generator function which can be used by Task.jsm's Task.spawn(). The
   601    * function receives this connection instance as its argument.
   602    *
   603    * The supplied function is expected to yield promises. These are often
   604    * promises created by calling `execute` and `executeCached`. If the
   605    * generator is exhausted without any errors being thrown, the
   606    * transaction is committed. If an error occurs, the transaction is
   607    * rolled back.
   608    *
   609    * The returned value from this function is a promise that will be resolved
   610    * once the transaction has been committed or rolled back. The promise will
   611    * be resolved to whatever value the supplied function resolves to. If
   612    * the transaction is rolled back, the promise is rejected.
   613    *
   614    * @param func
   615    *        (function) What to perform as part of the transaction.
   616    * @param type optional
   617    *        One of the TRANSACTION_* constants attached to this type.
   618    */
   619   executeTransaction: function (func, type=this.TRANSACTION_DEFERRED) {
   620     if (this.TRANSACTION_TYPES.indexOf(type) == -1) {
   621       throw new Error("Unknown transaction type: " + type);
   622     }
   624     this._ensureOpen();
   626     if (this._inProgressTransaction) {
   627       throw new Error("A transaction is already active. Only one transaction " +
   628                       "can be active at a time.");
   629     }
   631     this._log.debug("Beginning transaction");
   632     let deferred = Promise.defer();
   633     this._inProgressTransaction = deferred;
   634     Task.spawn(function doTransaction() {
   635       // It's tempting to not yield here and rely on the implicit serial
   636       // execution of issued statements. However, the yield serves an important
   637       // purpose: catching errors in statement execution.
   638       yield this.execute("BEGIN " + type + " TRANSACTION");
   640       let result;
   641       try {
   642         result = yield Task.spawn(func(this));
   643       } catch (ex) {
   644         // It's possible that a request to close the connection caused the
   645         // error.
   646         // Assertion: close() will unset this._inProgressTransaction when
   647         // called.
   648         if (!this._inProgressTransaction) {
   649           this._log.warn("Connection was closed while performing transaction. " +
   650                          "Received error should be due to closed connection: " +
   651                          CommonUtils.exceptionStr(ex));
   652           throw ex;
   653         }
   655         this._log.warn("Error during transaction. Rolling back: " +
   656                        CommonUtils.exceptionStr(ex));
   657         try {
   658           yield this.execute("ROLLBACK TRANSACTION");
   659         } catch (inner) {
   660           this._log.warn("Could not roll back transaction. This is weird: " +
   661                          CommonUtils.exceptionStr(inner));
   662         }
   664         throw ex;
   665       }
   667       // See comment above about connection being closed during transaction.
   668       if (!this._inProgressTransaction) {
   669         this._log.warn("Connection was closed while performing transaction. " +
   670                        "Unable to commit.");
   671         throw new Error("Connection closed before transaction committed.");
   672       }
   674       try {
   675         yield this.execute("COMMIT TRANSACTION");
   676       } catch (ex) {
   677         this._log.warn("Error committing transaction: " +
   678                        CommonUtils.exceptionStr(ex));
   679         throw ex;
   680       }
   682       throw new Task.Result(result);
   683     }.bind(this)).then(
   684       function onSuccess(result) {
   685         this._inProgressTransaction = null;
   686         deferred.resolve(result);
   687       }.bind(this),
   688       function onError(error) {
   689         this._inProgressTransaction = null;
   690         deferred.reject(error);
   691       }.bind(this)
   692     );
   694     return deferred.promise;
   695   },
   697   /**
   698    * Whether a table exists in the database (both persistent and temporary tables).
   699    *
   700    * @param name
   701    *        (string) Name of the table.
   702    *
   703    * @return Promise<bool>
   704    */
   705   tableExists: function (name) {
   706     return this.execute(
   707       "SELECT name FROM (SELECT * FROM sqlite_master UNION ALL " +
   708                         "SELECT * FROM sqlite_temp_master) " +
   709       "WHERE type = 'table' AND name=?",
   710       [name])
   711       .then(function onResult(rows) {
   712         return Promise.resolve(rows.length > 0);
   713       }
   714     );
   715   },
   717   /**
   718    * Whether a named index exists (both persistent and temporary tables).
   719    *
   720    * @param name
   721    *        (string) Name of the index.
   722    *
   723    * @return Promise<bool>
   724    */
   725   indexExists: function (name) {
   726     return this.execute(
   727       "SELECT name FROM (SELECT * FROM sqlite_master UNION ALL " +
   728                         "SELECT * FROM sqlite_temp_master) " +
   729       "WHERE type = 'index' AND name=?",
   730       [name])
   731       .then(function onResult(rows) {
   732         return Promise.resolve(rows.length > 0);
   733       }
   734     );
   735   },
   737   /**
   738    * Free up as much memory from the underlying database connection as possible.
   739    *
   740    * @return Promise<>
   741    */
   742   shrinkMemory: function () {
   743     this._log.info("Shrinking memory usage.");
   745     let onShrunk = this._clearIdleShrinkTimer.bind(this);
   747     return this.execute("PRAGMA shrink_memory").then(onShrunk, onShrunk);
   748   },
   750   /**
   751    * Discard all cached statements.
   752    *
   753    * Note that this relies on us being non-interruptible between
   754    * the insertion or retrieval of a statement in the cache and its
   755    * execution: we finalize all statements, which is only safe if
   756    * they will not be executed again.
   757    *
   758    * @return (integer) the number of statements discarded.
   759    */
   760   discardCachedStatements: function () {
   761     let count = 0;
   762     for (let [k, statement] of this._cachedStatements) {
   763       ++count;
   764       statement.finalize();
   765     }
   766     this._cachedStatements.clear();
   767     this._log.debug("Discarded " + count + " cached statements.");
   768     return count;
   769   },
   771   /**
   772    * Helper method to bind parameters of various kinds through
   773    * reflection.
   774    */
   775   _bindParameters: function (statement, params) {
   776     if (!params) {
   777       return;
   778     }
   780     if (Array.isArray(params)) {
   781       // It's an array of separate params.
   782       if (params.length && (typeof(params[0]) == "object")) {
   783         let paramsArray = statement.newBindingParamsArray();
   784         for (let p of params) {
   785           let bindings = paramsArray.newBindingParams();
   786           for (let [key, value] of Iterator(p)) {
   787             bindings.bindByName(key, value);
   788           }
   789           paramsArray.addParams(bindings);
   790         }
   792         statement.bindParameters(paramsArray);
   793         return;
   794       }
   796       // Indexed params.
   797       for (let i = 0; i < params.length; i++) {
   798         statement.bindByIndex(i, params[i]);
   799       }
   800       return;
   801     }
   803     // Named params.
   804     if (params && typeof(params) == "object") {
   805       for (let k in params) {
   806         statement.bindByName(k, params[k]);
   807       }
   808       return;
   809     }
   811     throw new Error("Invalid type for bound parameters. Expected Array or " +
   812                     "object. Got: " + params);
   813   },
   815   _executeStatement: function (sql, statement, params, onRow) {
   816     if (statement.state != statement.MOZ_STORAGE_STATEMENT_READY) {
   817       throw new Error("Statement is not ready for execution.");
   818     }
   820     if (onRow && typeof(onRow) != "function") {
   821       throw new Error("onRow must be a function. Got: " + onRow);
   822     }
   824     this._bindParameters(statement, params);
   826     let index = this._statementCounter++;
   828     let deferred = Promise.defer();
   829     let userCancelled = false;
   830     let errors = [];
   831     let rows = [];
   833     // Don't incur overhead for serializing params unless the messages go
   834     // somewhere.
   835     if (this._log.level <= Log.Level.Trace) {
   836       let msg = "Stmt #" + index + " " + sql;
   838       if (params) {
   839         msg += " - " + JSON.stringify(params);
   840       }
   841       this._log.trace(msg);
   842     } else {
   843       this._log.debug("Stmt #" + index + " starting");
   844     }
   846     let self = this;
   847     let pending = statement.executeAsync({
   848       handleResult: function (resultSet) {
   849         // .cancel() may not be immediate and handleResult() could be called
   850         // after a .cancel().
   851         for (let row = resultSet.getNextRow(); row && !userCancelled; row = resultSet.getNextRow()) {
   852           if (!onRow) {
   853             rows.push(row);
   854             continue;
   855           }
   857           try {
   858             onRow(row);
   859           } catch (e if e instanceof StopIteration) {
   860             userCancelled = true;
   861             pending.cancel();
   862             break;
   863           } catch (ex) {
   864             self._log.warn("Exception when calling onRow callback: " +
   865                            CommonUtils.exceptionStr(ex));
   866           }
   867         }
   868       },
   870       handleError: function (error) {
   871         self._log.info("Error when executing SQL (" + error.result + "): " +
   872                        error.message);
   873         errors.push(error);
   874       },
   876       handleCompletion: function (reason) {
   877         self._log.debug("Stmt #" + index + " finished.");
   878         self._pendingStatements.delete(index);
   880         switch (reason) {
   881           case Ci.mozIStorageStatementCallback.REASON_FINISHED:
   882             // If there is an onRow handler, we always resolve to null.
   883             let result = onRow ? null : rows;
   884             deferred.resolve(result);
   885             break;
   887           case Ci.mozIStorageStatementCallback.REASON_CANCELLED:
   888             // It is not an error if the user explicitly requested cancel via
   889             // the onRow handler.
   890             if (userCancelled) {
   891               let result = onRow ? null : rows;
   892               deferred.resolve(result);
   893             } else {
   894               deferred.reject(new Error("Statement was cancelled."));
   895             }
   897             break;
   899           case Ci.mozIStorageStatementCallback.REASON_ERROR:
   900             let error = new Error("Error(s) encountered during statement execution.");
   901             error.errors = errors;
   902             deferred.reject(error);
   903             break;
   905           default:
   906             deferred.reject(new Error("Unknown completion reason code: " +
   907                                       reason));
   908             break;
   909         }
   910       },
   911     });
   913     this._pendingStatements.set(index, pending);
   914     return deferred.promise;
   915   },
   917   _ensureOpen: function () {
   918     if (!this._open) {
   919       throw new Error("Connection is not open.");
   920     }
   921   },
   923   _clearIdleShrinkTimer: function () {
   924     if (!this._idleShrinkTimer) {
   925       return;
   926     }
   928     this._idleShrinkTimer.cancel();
   929   },
   931   _startIdleShrinkTimer: function () {
   932     if (!this._idleShrinkTimer) {
   933       return;
   934     }
   936     this._idleShrinkTimer.initWithCallback(this.shrinkMemory.bind(this),
   937                                            this._idleShrinkMS,
   938                                            this._idleShrinkTimer.TYPE_ONE_SHOT);
   939   },
   940 });
   942 this.Sqlite = {
   943   openConnection: openConnection,
   944   cloneStorageConnection: cloneStorageConnection
   945 };

mercurial