toolkit/modules/Sqlite.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/modules/Sqlite.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,945 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +this.EXPORTED_SYMBOLS = [
    1.11 +  "Sqlite",
    1.12 +];
    1.13 +
    1.14 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
    1.15 +
    1.16 +Cu.import("resource://gre/modules/Promise.jsm");
    1.17 +Cu.import("resource://gre/modules/osfile.jsm");
    1.18 +Cu.import("resource://gre/modules/Services.jsm");
    1.19 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.20 +Cu.import("resource://gre/modules/Log.jsm");
    1.21 +
    1.22 +XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
    1.23 +                                  "resource://gre/modules/AsyncShutdown.jsm");
    1.24 +XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
    1.25 +                                  "resource://services-common/utils.js");
    1.26 +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
    1.27 +                                  "resource://gre/modules/FileUtils.jsm");
    1.28 +XPCOMUtils.defineLazyModuleGetter(this, "Task",
    1.29 +                                  "resource://gre/modules/Task.jsm");
    1.30 +
    1.31 +
    1.32 +// Counts the number of created connections per database basename(). This is
    1.33 +// used for logging to distinguish connection instances.
    1.34 +let connectionCounters = new Map();
    1.35 +
    1.36 +
    1.37 +/**
    1.38 + * Opens a connection to a SQLite database.
    1.39 + *
    1.40 + * The following parameters can control the connection:
    1.41 + *
    1.42 + *   path -- (string) The filesystem path of the database file to open. If the
    1.43 + *       file does not exist, a new database will be created.
    1.44 + *
    1.45 + *   sharedMemoryCache -- (bool) Whether multiple connections to the database
    1.46 + *       share the same memory cache. Sharing the memory cache likely results
    1.47 + *       in less memory utilization. However, sharing also requires connections
    1.48 + *       to obtain a lock, possibly making database access slower. Defaults to
    1.49 + *       true.
    1.50 + *
    1.51 + *   shrinkMemoryOnConnectionIdleMS -- (integer) If defined, the connection
    1.52 + *       will attempt to minimize its memory usage after this many
    1.53 + *       milliseconds of connection idle. The connection is idle when no
    1.54 + *       statements are executing. There is no default value which means no
    1.55 + *       automatic memory minimization will occur. Please note that this is
    1.56 + *       *not* a timer on the idle service and this could fire while the
    1.57 + *       application is active.
    1.58 + *
    1.59 + * FUTURE options to control:
    1.60 + *
    1.61 + *   special named databases
    1.62 + *   pragma TEMP STORE = MEMORY
    1.63 + *   TRUNCATE JOURNAL
    1.64 + *   SYNCHRONOUS = full
    1.65 + *
    1.66 + * @param options
    1.67 + *        (Object) Parameters to control connection and open options.
    1.68 + *
    1.69 + * @return Promise<OpenedConnection>
    1.70 + */
    1.71 +function openConnection(options) {
    1.72 +  let log = Log.repository.getLogger("Sqlite.ConnectionOpener");
    1.73 +
    1.74 +  if (!options.path) {
    1.75 +    throw new Error("path not specified in connection options.");
    1.76 +  }
    1.77 +
    1.78 +  // Retains absolute paths and normalizes relative as relative to profile.
    1.79 +  let path = OS.Path.join(OS.Constants.Path.profileDir, options.path);
    1.80 +
    1.81 +  let sharedMemoryCache = "sharedMemoryCache" in options ?
    1.82 +                            options.sharedMemoryCache : true;
    1.83 +
    1.84 +  let openedOptions = {};
    1.85 +
    1.86 +  if ("shrinkMemoryOnConnectionIdleMS" in options) {
    1.87 +    if (!Number.isInteger(options.shrinkMemoryOnConnectionIdleMS)) {
    1.88 +      throw new Error("shrinkMemoryOnConnectionIdleMS must be an integer. " +
    1.89 +                      "Got: " + options.shrinkMemoryOnConnectionIdleMS);
    1.90 +    }
    1.91 +
    1.92 +    openedOptions.shrinkMemoryOnConnectionIdleMS =
    1.93 +      options.shrinkMemoryOnConnectionIdleMS;
    1.94 +  }
    1.95 +
    1.96 +  let file = FileUtils.File(path);
    1.97 +
    1.98 +  let basename = OS.Path.basename(path);
    1.99 +  let number = connectionCounters.get(basename) || 0;
   1.100 +  connectionCounters.set(basename, number + 1);
   1.101 +
   1.102 +  let identifier = basename + "#" + number;
   1.103 +
   1.104 +  log.info("Opening database: " + path + " (" + identifier + ")");
   1.105 +  let deferred = Promise.defer();
   1.106 +  let options = null;
   1.107 +  if (!sharedMemoryCache) {
   1.108 +    options = Cc["@mozilla.org/hash-property-bag;1"].
   1.109 +      createInstance(Ci.nsIWritablePropertyBag);
   1.110 +    options.setProperty("shared", false);
   1.111 +  }
   1.112 +  Services.storage.openAsyncDatabase(file, options, function(status, connection) {
   1.113 +    if (!connection) {
   1.114 +      log.warn("Could not open connection: " + status);
   1.115 +      deferred.reject(new Error("Could not open connection: " + status));
   1.116 +      return;
   1.117 +    }
   1.118 +    log.info("Connection opened");
   1.119 +    try {
   1.120 +      deferred.resolve(
   1.121 +        new OpenedConnection(connection.QueryInterface(Ci.mozIStorageAsyncConnection), basename, number,
   1.122 +        openedOptions));
   1.123 +    } catch (ex) {
   1.124 +      log.warn("Could not open database: " + CommonUtils.exceptionStr(ex));
   1.125 +      deferred.reject(ex);
   1.126 +    }
   1.127 +  });
   1.128 +  return deferred.promise;
   1.129 +}
   1.130 +
   1.131 +/**
   1.132 + * Creates a clone of an existing and open Storage connection.  The clone has
   1.133 + * the same underlying characteristics of the original connection and is
   1.134 + * returned in form of on OpenedConnection handle.
   1.135 + *
   1.136 + * The following parameters can control the cloned connection:
   1.137 + *
   1.138 + *   connection -- (mozIStorageAsyncConnection) The original Storage connection
   1.139 + *       to clone.  It's not possible to clone connections to memory databases.
   1.140 + *
   1.141 + *   readOnly -- (boolean) - If true the clone will be read-only.  If the
   1.142 + *       original connection is already read-only, the clone will be, regardless
   1.143 + *       of this option.  If the original connection is using the shared cache,
   1.144 + *       this parameter will be ignored and the clone will be as privileged as
   1.145 + *       the original connection.
   1.146 + *   shrinkMemoryOnConnectionIdleMS -- (integer) If defined, the connection
   1.147 + *       will attempt to minimize its memory usage after this many
   1.148 + *       milliseconds of connection idle. The connection is idle when no
   1.149 + *       statements are executing. There is no default value which means no
   1.150 + *       automatic memory minimization will occur. Please note that this is
   1.151 + *       *not* a timer on the idle service and this could fire while the
   1.152 + *       application is active.
   1.153 + *
   1.154 + *
   1.155 + * @param options
   1.156 + *        (Object) Parameters to control connection and clone options.
   1.157 + *
   1.158 + * @return Promise<OpenedConnection>
   1.159 + */
   1.160 +function cloneStorageConnection(options) {
   1.161 +  let log = Log.repository.getLogger("Sqlite.ConnectionCloner");
   1.162 +
   1.163 +  let source = options && options.connection;
   1.164 +  if (!source) {
   1.165 +    throw new TypeError("connection not specified in clone options.");
   1.166 +  }
   1.167 +  if (!source instanceof Ci.mozIStorageAsyncConnection) {
   1.168 +    throw new TypeError("Connection must be a valid Storage connection.")
   1.169 +  }
   1.170 +
   1.171 +  let openedOptions = {};
   1.172 +
   1.173 +  if ("shrinkMemoryOnConnectionIdleMS" in options) {
   1.174 +    if (!Number.isInteger(options.shrinkMemoryOnConnectionIdleMS)) {
   1.175 +      throw new TypeError("shrinkMemoryOnConnectionIdleMS must be an integer. " +
   1.176 +                          "Got: " + options.shrinkMemoryOnConnectionIdleMS);
   1.177 +    }
   1.178 +    openedOptions.shrinkMemoryOnConnectionIdleMS =
   1.179 +      options.shrinkMemoryOnConnectionIdleMS;
   1.180 +  }
   1.181 +
   1.182 +  let path = source.databaseFile.path;
   1.183 +  let basename = OS.Path.basename(path);
   1.184 +  let number = connectionCounters.get(basename) || 0;
   1.185 +  connectionCounters.set(basename, number + 1);
   1.186 +  let identifier = basename + "#" + number;
   1.187 +
   1.188 +  log.info("Cloning database: " + path + " (" + identifier + ")");
   1.189 +  let deferred = Promise.defer();
   1.190 +
   1.191 +  source.asyncClone(!!options.readOnly, (status, connection) => {
   1.192 +    if (!connection) {
   1.193 +      log.warn("Could not clone connection: " + status);
   1.194 +      deferred.reject(new Error("Could not clone connection: " + status));
   1.195 +    }
   1.196 +    log.info("Connection cloned");
   1.197 +    try {
   1.198 +      let conn = connection.QueryInterface(Ci.mozIStorageAsyncConnection);
   1.199 +      deferred.resolve(new OpenedConnection(conn, basename, number,
   1.200 +                                            openedOptions));
   1.201 +    } catch (ex) {
   1.202 +      log.warn("Could not clone database: " + CommonUtils.exceptionStr(ex));
   1.203 +      deferred.reject(ex);
   1.204 +    }
   1.205 +  });
   1.206 +  return deferred.promise;
   1.207 +}
   1.208 +
   1.209 +/**
   1.210 + * Handle on an opened SQLite database.
   1.211 + *
   1.212 + * This is essentially a glorified wrapper around mozIStorageConnection.
   1.213 + * However, it offers some compelling advantages.
   1.214 + *
   1.215 + * The main functions on this type are `execute` and `executeCached`. These are
   1.216 + * ultimately how all SQL statements are executed. It's worth explaining their
   1.217 + * differences.
   1.218 + *
   1.219 + * `execute` is used to execute one-shot SQL statements. These are SQL
   1.220 + * statements that are executed one time and then thrown away. They are useful
   1.221 + * for dynamically generated SQL statements and clients who don't care about
   1.222 + * performance (either their own or wasting resources in the overall
   1.223 + * application). Because of the performance considerations, it is recommended
   1.224 + * to avoid `execute` unless the statement you are executing will only be
   1.225 + * executed once or seldomly.
   1.226 + *
   1.227 + * `executeCached` is used to execute a statement that will presumably be
   1.228 + * executed multiple times. The statement is parsed once and stuffed away
   1.229 + * inside the connection instance. Subsequent calls to `executeCached` will not
   1.230 + * incur the overhead of creating a new statement object. This should be used
   1.231 + * in preference to `execute` when a specific SQL statement will be executed
   1.232 + * multiple times.
   1.233 + *
   1.234 + * Instances of this type are not meant to be created outside of this file.
   1.235 + * Instead, first open an instance of `UnopenedSqliteConnection` and obtain
   1.236 + * an instance of this type by calling `open`.
   1.237 + *
   1.238 + * FUTURE IMPROVEMENTS
   1.239 + *
   1.240 + *   Ability to enqueue operations. Currently there can be race conditions,
   1.241 + *   especially as far as transactions are concerned. It would be nice to have
   1.242 + *   an enqueueOperation(func) API that serially executes passed functions.
   1.243 + *
   1.244 + *   Support for SAVEPOINT (named/nested transactions) might be useful.
   1.245 + *
   1.246 + * @param connection
   1.247 + *        (mozIStorageConnection) Underlying SQLite connection.
   1.248 + * @param basename
   1.249 + *        (string) The basename of this database name. Used for logging.
   1.250 + * @param number
   1.251 + *        (Number) The connection number to this database.
   1.252 + * @param options
   1.253 + *        (object) Options to control behavior of connection. See
   1.254 + *        `openConnection`.
   1.255 + */
   1.256 +function OpenedConnection(connection, basename, number, options) {
   1.257 +  this._log = Log.repository.getLoggerWithMessagePrefix("Sqlite.Connection." + basename,
   1.258 +                                                        "Conn #" + number + ": ");
   1.259 +
   1.260 +  this._log.info("Opened");
   1.261 +
   1.262 +  this._connection = connection;
   1.263 +  this._connectionIdentifier = basename + " Conn #" + number;
   1.264 +  this._open = true;
   1.265 +
   1.266 +  this._cachedStatements = new Map();
   1.267 +  this._anonymousStatements = new Map();
   1.268 +  this._anonymousCounter = 0;
   1.269 +
   1.270 +  // A map from statement index to mozIStoragePendingStatement, to allow for
   1.271 +  // canceling prior to finalizing the mozIStorageStatements.
   1.272 +  this._pendingStatements = new Map();
   1.273 +
   1.274 +  // Increments for each executed statement for the life of the connection.
   1.275 +  this._statementCounter = 0;
   1.276 +
   1.277 +  this._inProgressTransaction = null;
   1.278 +
   1.279 +  this._idleShrinkMS = options.shrinkMemoryOnConnectionIdleMS;
   1.280 +  if (this._idleShrinkMS) {
   1.281 +    this._idleShrinkTimer = Cc["@mozilla.org/timer;1"]
   1.282 +                              .createInstance(Ci.nsITimer);
   1.283 +    // We wait for the first statement execute to start the timer because
   1.284 +    // shrinking now would not do anything.
   1.285 +  }
   1.286 +}
   1.287 +
   1.288 +OpenedConnection.prototype = Object.freeze({
   1.289 +  TRANSACTION_DEFERRED: "DEFERRED",
   1.290 +  TRANSACTION_IMMEDIATE: "IMMEDIATE",
   1.291 +  TRANSACTION_EXCLUSIVE: "EXCLUSIVE",
   1.292 +
   1.293 +  TRANSACTION_TYPES: ["DEFERRED", "IMMEDIATE", "EXCLUSIVE"],
   1.294 +
   1.295 +  /**
   1.296 +   * The integer schema version of the database.
   1.297 +   *
   1.298 +   * This is 0 if not schema version has been set.
   1.299 +   *
   1.300 +   * @return Promise<int>
   1.301 +   */
   1.302 +  getSchemaVersion: function() {
   1.303 +    let self = this;
   1.304 +    return this.execute("PRAGMA user_version").then(
   1.305 +      function onSuccess(result) {
   1.306 +        if (result == null) {
   1.307 +          return 0;
   1.308 +        }
   1.309 +        return JSON.stringify(result[0].getInt32(0));
   1.310 +      }
   1.311 +    );
   1.312 +  },
   1.313 +
   1.314 +  setSchemaVersion: function(value) {
   1.315 +    if (!Number.isInteger(value)) {
   1.316 +      // Guarding against accidental SQLi
   1.317 +      throw new TypeError("Schema version must be an integer. Got " + value);
   1.318 +    }
   1.319 +    this._ensureOpen();
   1.320 +    return this.execute("PRAGMA user_version = " + value);
   1.321 +  },
   1.322 +
   1.323 +  /**
   1.324 +   * Close the database connection.
   1.325 +   *
   1.326 +   * This must be performed when you are finished with the database.
   1.327 +   *
   1.328 +   * Closing the database connection has the side effect of forcefully
   1.329 +   * cancelling all active statements. Therefore, callers should ensure that
   1.330 +   * all active statements have completed before closing the connection, if
   1.331 +   * possible.
   1.332 +   *
   1.333 +   * The returned promise will be resolved once the connection is closed.
   1.334 +   *
   1.335 +   * IMPROVEMENT: Resolve the promise to a closed connection which can be
   1.336 +   * reopened.
   1.337 +   *
   1.338 +   * @return Promise<>
   1.339 +   */
   1.340 +  close: function () {
   1.341 +    if (!this._connection) {
   1.342 +      return Promise.resolve();
   1.343 +    }
   1.344 +
   1.345 +    this._log.debug("Request to close connection.");
   1.346 +    this._clearIdleShrinkTimer();
   1.347 +    let deferred = Promise.defer();
   1.348 +
   1.349 +    AsyncShutdown.profileBeforeChange.addBlocker(
   1.350 +      "Sqlite.jsm: " + this._connectionIdentifier,
   1.351 +      deferred.promise
   1.352 +    );
   1.353 +
   1.354 +    // We need to take extra care with transactions during shutdown.
   1.355 +    //
   1.356 +    // If we don't have a transaction in progress, we can proceed with shutdown
   1.357 +    // immediately.
   1.358 +    if (!this._inProgressTransaction) {
   1.359 +      this._finalize(deferred);
   1.360 +      return deferred.promise;
   1.361 +    }
   1.362 +
   1.363 +    // Else if we do have a transaction in progress, we forcefully roll it
   1.364 +    // back. This is an async task, so we wait on it to finish before
   1.365 +    // performing finalization.
   1.366 +    this._log.warn("Transaction in progress at time of close. Rolling back.");
   1.367 +
   1.368 +    let onRollback = this._finalize.bind(this, deferred);
   1.369 +
   1.370 +    this.execute("ROLLBACK TRANSACTION").then(onRollback, onRollback);
   1.371 +    this._inProgressTransaction.reject(new Error("Connection being closed."));
   1.372 +    this._inProgressTransaction = null;
   1.373 +
   1.374 +    return deferred.promise;
   1.375 +  },
   1.376 +
   1.377 +  /**
   1.378 +   * Clones this connection to a new Sqlite one.
   1.379 +   *
   1.380 +   * The following parameters can control the cloned connection:
   1.381 +   *
   1.382 +   * @param readOnly
   1.383 +   *        (boolean) - If true the clone will be read-only.  If the original
   1.384 +   *        connection is already read-only, the clone will be, regardless of
   1.385 +   *        this option.  If the original connection is using the shared cache,
   1.386 +   *        this parameter will be ignored and the clone will be as privileged as
   1.387 +   *        the original connection.
   1.388 +   *
   1.389 +   * @return Promise<OpenedConnection>
   1.390 +   */
   1.391 +  clone: function (readOnly=false) {
   1.392 +    this._ensureOpen();
   1.393 +
   1.394 +    this._log.debug("Request to clone connection.");
   1.395 +
   1.396 +    let options = {
   1.397 +      connection: this._connection,
   1.398 +      readOnly: readOnly,
   1.399 +    };
   1.400 +    if (this._idleShrinkMS)
   1.401 +      options.shrinkMemoryOnConnectionIdleMS = this._idleShrinkMS;
   1.402 +
   1.403 +    return cloneStorageConnection(options);
   1.404 +  },
   1.405 +
   1.406 +  _finalize: function (deferred) {
   1.407 +    this._log.debug("Finalizing connection.");
   1.408 +    // Cancel any pending statements.
   1.409 +    for (let [k, statement] of this._pendingStatements) {
   1.410 +      statement.cancel();
   1.411 +    }
   1.412 +    this._pendingStatements.clear();
   1.413 +
   1.414 +    // We no longer need to track these.
   1.415 +    this._statementCounter = 0;
   1.416 +
   1.417 +    // Next we finalize all active statements.
   1.418 +    for (let [k, statement] of this._anonymousStatements) {
   1.419 +      statement.finalize();
   1.420 +    }
   1.421 +    this._anonymousStatements.clear();
   1.422 +
   1.423 +    for (let [k, statement] of this._cachedStatements) {
   1.424 +      statement.finalize();
   1.425 +    }
   1.426 +    this._cachedStatements.clear();
   1.427 +
   1.428 +    // This guards against operations performed between the call to this
   1.429 +    // function and asyncClose() finishing. See also bug 726990.
   1.430 +    this._open = false;
   1.431 +
   1.432 +    this._log.debug("Calling asyncClose().");
   1.433 +    this._connection.asyncClose({
   1.434 +      complete: function () {
   1.435 +        this._log.info("Closed");
   1.436 +        this._connection = null;
   1.437 +        deferred.resolve();
   1.438 +      }.bind(this),
   1.439 +    });
   1.440 +  },
   1.441 +
   1.442 +  /**
   1.443 +   * Execute a SQL statement and cache the underlying statement object.
   1.444 +   *
   1.445 +   * This function executes a SQL statement and also caches the underlying
   1.446 +   * derived statement object so subsequent executions are faster and use
   1.447 +   * less resources.
   1.448 +   *
   1.449 +   * This function optionally binds parameters to the statement as well as
   1.450 +   * optionally invokes a callback for every row retrieved.
   1.451 +   *
   1.452 +   * By default, no parameters are bound and no callback will be invoked for
   1.453 +   * every row.
   1.454 +   *
   1.455 +   * Bound parameters can be defined as an Array of positional arguments or
   1.456 +   * an object mapping named parameters to their values. If there are no bound
   1.457 +   * parameters, the caller can pass nothing or null for this argument.
   1.458 +   *
   1.459 +   * Callers are encouraged to pass objects rather than Arrays for bound
   1.460 +   * parameters because they prevent foot guns. With positional arguments, it
   1.461 +   * is simple to modify the parameter count or positions without fixing all
   1.462 +   * users of the statement. Objects/named parameters are a little safer
   1.463 +   * because changes in order alone won't result in bad things happening.
   1.464 +   *
   1.465 +   * When `onRow` is not specified, all returned rows are buffered before the
   1.466 +   * returned promise is resolved. For INSERT or UPDATE statements, this has
   1.467 +   * no effect because no rows are returned from these. However, it has
   1.468 +   * implications for SELECT statements.
   1.469 +   *
   1.470 +   * If your SELECT statement could return many rows or rows with large amounts
   1.471 +   * of data, for performance reasons it is recommended to pass an `onRow`
   1.472 +   * handler. Otherwise, the buffering may consume unacceptable amounts of
   1.473 +   * resources.
   1.474 +   *
   1.475 +   * If a `StopIteration` is thrown during execution of an `onRow` handler,
   1.476 +   * the execution of the statement is immediately cancelled. Subsequent
   1.477 +   * rows will not be processed and no more `onRow` invocations will be made.
   1.478 +   * The promise is resolved immediately.
   1.479 +   *
   1.480 +   * If a non-`StopIteration` exception is thrown by the `onRow` handler, the
   1.481 +   * exception is logged and processing of subsequent rows occurs as if nothing
   1.482 +   * happened. The promise is still resolved (not rejected).
   1.483 +   *
   1.484 +   * The return value is a promise that will be resolved when the statement
   1.485 +   * has completed fully.
   1.486 +   *
   1.487 +   * The promise will be rejected with an `Error` instance if the statement
   1.488 +   * did not finish execution fully. The `Error` may have an `errors` property.
   1.489 +   * If defined, it will be an Array of objects describing individual errors.
   1.490 +   * Each object has the properties `result` and `message`. `result` is a
   1.491 +   * numeric error code and `message` is a string description of the problem.
   1.492 +   *
   1.493 +   * @param name
   1.494 +   *        (string) The name of the registered statement to execute.
   1.495 +   * @param params optional
   1.496 +   *        (Array or object) Parameters to bind.
   1.497 +   * @param onRow optional
   1.498 +   *        (function) Callback to receive each row from result.
   1.499 +   */
   1.500 +  executeCached: function (sql, params=null, onRow=null) {
   1.501 +    this._ensureOpen();
   1.502 +
   1.503 +    if (!sql) {
   1.504 +      throw new Error("sql argument is empty.");
   1.505 +    }
   1.506 +
   1.507 +    let statement = this._cachedStatements.get(sql);
   1.508 +    if (!statement) {
   1.509 +      statement = this._connection.createAsyncStatement(sql);
   1.510 +      this._cachedStatements.set(sql, statement);
   1.511 +    }
   1.512 +
   1.513 +    this._clearIdleShrinkTimer();
   1.514 +
   1.515 +    let deferred = Promise.defer();
   1.516 +
   1.517 +    try {
   1.518 +      this._executeStatement(sql, statement, params, onRow).then(
   1.519 +        function onResult(result) {
   1.520 +          this._startIdleShrinkTimer();
   1.521 +          deferred.resolve(result);
   1.522 +        }.bind(this),
   1.523 +        function onError(error) {
   1.524 +          this._startIdleShrinkTimer();
   1.525 +          deferred.reject(error);
   1.526 +        }.bind(this)
   1.527 +      );
   1.528 +    } catch (ex) {
   1.529 +      this._startIdleShrinkTimer();
   1.530 +      throw ex;
   1.531 +    }
   1.532 +
   1.533 +    return deferred.promise;
   1.534 +  },
   1.535 +
   1.536 +  /**
   1.537 +   * Execute a one-shot SQL statement.
   1.538 +   *
   1.539 +   * If you find yourself feeding the same SQL string in this function, you
   1.540 +   * should *not* use this function and instead use `executeCached`.
   1.541 +   *
   1.542 +   * See `executeCached` for the meaning of the arguments and extended usage info.
   1.543 +   *
   1.544 +   * @param sql
   1.545 +   *        (string) SQL to execute.
   1.546 +   * @param params optional
   1.547 +   *        (Array or Object) Parameters to bind to the statement.
   1.548 +   * @param onRow optional
   1.549 +   *        (function) Callback to receive result of a single row.
   1.550 +   */
   1.551 +  execute: function (sql, params=null, onRow=null) {
   1.552 +    if (typeof(sql) != "string") {
   1.553 +      throw new Error("Must define SQL to execute as a string: " + sql);
   1.554 +    }
   1.555 +
   1.556 +    this._ensureOpen();
   1.557 +
   1.558 +    let statement = this._connection.createAsyncStatement(sql);
   1.559 +    let index = this._anonymousCounter++;
   1.560 +
   1.561 +    this._anonymousStatements.set(index, statement);
   1.562 +    this._clearIdleShrinkTimer();
   1.563 +
   1.564 +    let onFinished = function () {
   1.565 +      this._anonymousStatements.delete(index);
   1.566 +      statement.finalize();
   1.567 +      this._startIdleShrinkTimer();
   1.568 +    }.bind(this);
   1.569 +
   1.570 +    let deferred = Promise.defer();
   1.571 +
   1.572 +    try {
   1.573 +      this._executeStatement(sql, statement, params, onRow).then(
   1.574 +        function onResult(rows) {
   1.575 +          onFinished();
   1.576 +          deferred.resolve(rows);
   1.577 +        }.bind(this),
   1.578 +
   1.579 +        function onError(error) {
   1.580 +          onFinished();
   1.581 +          deferred.reject(error);
   1.582 +        }.bind(this)
   1.583 +      );
   1.584 +    } catch (ex) {
   1.585 +      onFinished();
   1.586 +      throw ex;
   1.587 +    }
   1.588 +
   1.589 +    return deferred.promise;
   1.590 +  },
   1.591 +
   1.592 +  /**
   1.593 +   * Whether a transaction is currently in progress.
   1.594 +   */
   1.595 +  get transactionInProgress() {
   1.596 +    return this._open && !!this._inProgressTransaction;
   1.597 +  },
   1.598 +
   1.599 +  /**
   1.600 +   * Perform a transaction.
   1.601 +   *
   1.602 +   * A transaction is specified by a user-supplied function that is a
   1.603 +   * generator function which can be used by Task.jsm's Task.spawn(). The
   1.604 +   * function receives this connection instance as its argument.
   1.605 +   *
   1.606 +   * The supplied function is expected to yield promises. These are often
   1.607 +   * promises created by calling `execute` and `executeCached`. If the
   1.608 +   * generator is exhausted without any errors being thrown, the
   1.609 +   * transaction is committed. If an error occurs, the transaction is
   1.610 +   * rolled back.
   1.611 +   *
   1.612 +   * The returned value from this function is a promise that will be resolved
   1.613 +   * once the transaction has been committed or rolled back. The promise will
   1.614 +   * be resolved to whatever value the supplied function resolves to. If
   1.615 +   * the transaction is rolled back, the promise is rejected.
   1.616 +   *
   1.617 +   * @param func
   1.618 +   *        (function) What to perform as part of the transaction.
   1.619 +   * @param type optional
   1.620 +   *        One of the TRANSACTION_* constants attached to this type.
   1.621 +   */
   1.622 +  executeTransaction: function (func, type=this.TRANSACTION_DEFERRED) {
   1.623 +    if (this.TRANSACTION_TYPES.indexOf(type) == -1) {
   1.624 +      throw new Error("Unknown transaction type: " + type);
   1.625 +    }
   1.626 +
   1.627 +    this._ensureOpen();
   1.628 +
   1.629 +    if (this._inProgressTransaction) {
   1.630 +      throw new Error("A transaction is already active. Only one transaction " +
   1.631 +                      "can be active at a time.");
   1.632 +    }
   1.633 +
   1.634 +    this._log.debug("Beginning transaction");
   1.635 +    let deferred = Promise.defer();
   1.636 +    this._inProgressTransaction = deferred;
   1.637 +    Task.spawn(function doTransaction() {
   1.638 +      // It's tempting to not yield here and rely on the implicit serial
   1.639 +      // execution of issued statements. However, the yield serves an important
   1.640 +      // purpose: catching errors in statement execution.
   1.641 +      yield this.execute("BEGIN " + type + " TRANSACTION");
   1.642 +
   1.643 +      let result;
   1.644 +      try {
   1.645 +        result = yield Task.spawn(func(this));
   1.646 +      } catch (ex) {
   1.647 +        // It's possible that a request to close the connection caused the
   1.648 +        // error.
   1.649 +        // Assertion: close() will unset this._inProgressTransaction when
   1.650 +        // called.
   1.651 +        if (!this._inProgressTransaction) {
   1.652 +          this._log.warn("Connection was closed while performing transaction. " +
   1.653 +                         "Received error should be due to closed connection: " +
   1.654 +                         CommonUtils.exceptionStr(ex));
   1.655 +          throw ex;
   1.656 +        }
   1.657 +
   1.658 +        this._log.warn("Error during transaction. Rolling back: " +
   1.659 +                       CommonUtils.exceptionStr(ex));
   1.660 +        try {
   1.661 +          yield this.execute("ROLLBACK TRANSACTION");
   1.662 +        } catch (inner) {
   1.663 +          this._log.warn("Could not roll back transaction. This is weird: " +
   1.664 +                         CommonUtils.exceptionStr(inner));
   1.665 +        }
   1.666 +
   1.667 +        throw ex;
   1.668 +      }
   1.669 +
   1.670 +      // See comment above about connection being closed during transaction.
   1.671 +      if (!this._inProgressTransaction) {
   1.672 +        this._log.warn("Connection was closed while performing transaction. " +
   1.673 +                       "Unable to commit.");
   1.674 +        throw new Error("Connection closed before transaction committed.");
   1.675 +      }
   1.676 +
   1.677 +      try {
   1.678 +        yield this.execute("COMMIT TRANSACTION");
   1.679 +      } catch (ex) {
   1.680 +        this._log.warn("Error committing transaction: " +
   1.681 +                       CommonUtils.exceptionStr(ex));
   1.682 +        throw ex;
   1.683 +      }
   1.684 +
   1.685 +      throw new Task.Result(result);
   1.686 +    }.bind(this)).then(
   1.687 +      function onSuccess(result) {
   1.688 +        this._inProgressTransaction = null;
   1.689 +        deferred.resolve(result);
   1.690 +      }.bind(this),
   1.691 +      function onError(error) {
   1.692 +        this._inProgressTransaction = null;
   1.693 +        deferred.reject(error);
   1.694 +      }.bind(this)
   1.695 +    );
   1.696 +
   1.697 +    return deferred.promise;
   1.698 +  },
   1.699 +
   1.700 +  /**
   1.701 +   * Whether a table exists in the database (both persistent and temporary tables).
   1.702 +   *
   1.703 +   * @param name
   1.704 +   *        (string) Name of the table.
   1.705 +   *
   1.706 +   * @return Promise<bool>
   1.707 +   */
   1.708 +  tableExists: function (name) {
   1.709 +    return this.execute(
   1.710 +      "SELECT name FROM (SELECT * FROM sqlite_master UNION ALL " +
   1.711 +                        "SELECT * FROM sqlite_temp_master) " +
   1.712 +      "WHERE type = 'table' AND name=?",
   1.713 +      [name])
   1.714 +      .then(function onResult(rows) {
   1.715 +        return Promise.resolve(rows.length > 0);
   1.716 +      }
   1.717 +    );
   1.718 +  },
   1.719 +
   1.720 +  /**
   1.721 +   * Whether a named index exists (both persistent and temporary tables).
   1.722 +   *
   1.723 +   * @param name
   1.724 +   *        (string) Name of the index.
   1.725 +   *
   1.726 +   * @return Promise<bool>
   1.727 +   */
   1.728 +  indexExists: function (name) {
   1.729 +    return this.execute(
   1.730 +      "SELECT name FROM (SELECT * FROM sqlite_master UNION ALL " +
   1.731 +                        "SELECT * FROM sqlite_temp_master) " +
   1.732 +      "WHERE type = 'index' AND name=?",
   1.733 +      [name])
   1.734 +      .then(function onResult(rows) {
   1.735 +        return Promise.resolve(rows.length > 0);
   1.736 +      }
   1.737 +    );
   1.738 +  },
   1.739 +
   1.740 +  /**
   1.741 +   * Free up as much memory from the underlying database connection as possible.
   1.742 +   *
   1.743 +   * @return Promise<>
   1.744 +   */
   1.745 +  shrinkMemory: function () {
   1.746 +    this._log.info("Shrinking memory usage.");
   1.747 +
   1.748 +    let onShrunk = this._clearIdleShrinkTimer.bind(this);
   1.749 +
   1.750 +    return this.execute("PRAGMA shrink_memory").then(onShrunk, onShrunk);
   1.751 +  },
   1.752 +
   1.753 +  /**
   1.754 +   * Discard all cached statements.
   1.755 +   *
   1.756 +   * Note that this relies on us being non-interruptible between
   1.757 +   * the insertion or retrieval of a statement in the cache and its
   1.758 +   * execution: we finalize all statements, which is only safe if
   1.759 +   * they will not be executed again.
   1.760 +   *
   1.761 +   * @return (integer) the number of statements discarded.
   1.762 +   */
   1.763 +  discardCachedStatements: function () {
   1.764 +    let count = 0;
   1.765 +    for (let [k, statement] of this._cachedStatements) {
   1.766 +      ++count;
   1.767 +      statement.finalize();
   1.768 +    }
   1.769 +    this._cachedStatements.clear();
   1.770 +    this._log.debug("Discarded " + count + " cached statements.");
   1.771 +    return count;
   1.772 +  },
   1.773 +
   1.774 +  /**
   1.775 +   * Helper method to bind parameters of various kinds through
   1.776 +   * reflection.
   1.777 +   */
   1.778 +  _bindParameters: function (statement, params) {
   1.779 +    if (!params) {
   1.780 +      return;
   1.781 +    }
   1.782 +
   1.783 +    if (Array.isArray(params)) {
   1.784 +      // It's an array of separate params.
   1.785 +      if (params.length && (typeof(params[0]) == "object")) {
   1.786 +        let paramsArray = statement.newBindingParamsArray();
   1.787 +        for (let p of params) {
   1.788 +          let bindings = paramsArray.newBindingParams();
   1.789 +          for (let [key, value] of Iterator(p)) {
   1.790 +            bindings.bindByName(key, value);
   1.791 +          }
   1.792 +          paramsArray.addParams(bindings);
   1.793 +        }
   1.794 +
   1.795 +        statement.bindParameters(paramsArray);
   1.796 +        return;
   1.797 +      }
   1.798 +
   1.799 +      // Indexed params.
   1.800 +      for (let i = 0; i < params.length; i++) {
   1.801 +        statement.bindByIndex(i, params[i]);
   1.802 +      }
   1.803 +      return;
   1.804 +    }
   1.805 +
   1.806 +    // Named params.
   1.807 +    if (params && typeof(params) == "object") {
   1.808 +      for (let k in params) {
   1.809 +        statement.bindByName(k, params[k]);
   1.810 +      }
   1.811 +      return;
   1.812 +    }
   1.813 +
   1.814 +    throw new Error("Invalid type for bound parameters. Expected Array or " +
   1.815 +                    "object. Got: " + params);
   1.816 +  },
   1.817 +
   1.818 +  _executeStatement: function (sql, statement, params, onRow) {
   1.819 +    if (statement.state != statement.MOZ_STORAGE_STATEMENT_READY) {
   1.820 +      throw new Error("Statement is not ready for execution.");
   1.821 +    }
   1.822 +
   1.823 +    if (onRow && typeof(onRow) != "function") {
   1.824 +      throw new Error("onRow must be a function. Got: " + onRow);
   1.825 +    }
   1.826 +
   1.827 +    this._bindParameters(statement, params);
   1.828 +
   1.829 +    let index = this._statementCounter++;
   1.830 +
   1.831 +    let deferred = Promise.defer();
   1.832 +    let userCancelled = false;
   1.833 +    let errors = [];
   1.834 +    let rows = [];
   1.835 +
   1.836 +    // Don't incur overhead for serializing params unless the messages go
   1.837 +    // somewhere.
   1.838 +    if (this._log.level <= Log.Level.Trace) {
   1.839 +      let msg = "Stmt #" + index + " " + sql;
   1.840 +
   1.841 +      if (params) {
   1.842 +        msg += " - " + JSON.stringify(params);
   1.843 +      }
   1.844 +      this._log.trace(msg);
   1.845 +    } else {
   1.846 +      this._log.debug("Stmt #" + index + " starting");
   1.847 +    }
   1.848 +
   1.849 +    let self = this;
   1.850 +    let pending = statement.executeAsync({
   1.851 +      handleResult: function (resultSet) {
   1.852 +        // .cancel() may not be immediate and handleResult() could be called
   1.853 +        // after a .cancel().
   1.854 +        for (let row = resultSet.getNextRow(); row && !userCancelled; row = resultSet.getNextRow()) {
   1.855 +          if (!onRow) {
   1.856 +            rows.push(row);
   1.857 +            continue;
   1.858 +          }
   1.859 +
   1.860 +          try {
   1.861 +            onRow(row);
   1.862 +          } catch (e if e instanceof StopIteration) {
   1.863 +            userCancelled = true;
   1.864 +            pending.cancel();
   1.865 +            break;
   1.866 +          } catch (ex) {
   1.867 +            self._log.warn("Exception when calling onRow callback: " +
   1.868 +                           CommonUtils.exceptionStr(ex));
   1.869 +          }
   1.870 +        }
   1.871 +      },
   1.872 +
   1.873 +      handleError: function (error) {
   1.874 +        self._log.info("Error when executing SQL (" + error.result + "): " +
   1.875 +                       error.message);
   1.876 +        errors.push(error);
   1.877 +      },
   1.878 +
   1.879 +      handleCompletion: function (reason) {
   1.880 +        self._log.debug("Stmt #" + index + " finished.");
   1.881 +        self._pendingStatements.delete(index);
   1.882 +
   1.883 +        switch (reason) {
   1.884 +          case Ci.mozIStorageStatementCallback.REASON_FINISHED:
   1.885 +            // If there is an onRow handler, we always resolve to null.
   1.886 +            let result = onRow ? null : rows;
   1.887 +            deferred.resolve(result);
   1.888 +            break;
   1.889 +
   1.890 +          case Ci.mozIStorageStatementCallback.REASON_CANCELLED:
   1.891 +            // It is not an error if the user explicitly requested cancel via
   1.892 +            // the onRow handler.
   1.893 +            if (userCancelled) {
   1.894 +              let result = onRow ? null : rows;
   1.895 +              deferred.resolve(result);
   1.896 +            } else {
   1.897 +              deferred.reject(new Error("Statement was cancelled."));
   1.898 +            }
   1.899 +
   1.900 +            break;
   1.901 +
   1.902 +          case Ci.mozIStorageStatementCallback.REASON_ERROR:
   1.903 +            let error = new Error("Error(s) encountered during statement execution.");
   1.904 +            error.errors = errors;
   1.905 +            deferred.reject(error);
   1.906 +            break;
   1.907 +
   1.908 +          default:
   1.909 +            deferred.reject(new Error("Unknown completion reason code: " +
   1.910 +                                      reason));
   1.911 +            break;
   1.912 +        }
   1.913 +      },
   1.914 +    });
   1.915 +
   1.916 +    this._pendingStatements.set(index, pending);
   1.917 +    return deferred.promise;
   1.918 +  },
   1.919 +
   1.920 +  _ensureOpen: function () {
   1.921 +    if (!this._open) {
   1.922 +      throw new Error("Connection is not open.");
   1.923 +    }
   1.924 +  },
   1.925 +
   1.926 +  _clearIdleShrinkTimer: function () {
   1.927 +    if (!this._idleShrinkTimer) {
   1.928 +      return;
   1.929 +    }
   1.930 +
   1.931 +    this._idleShrinkTimer.cancel();
   1.932 +  },
   1.933 +
   1.934 +  _startIdleShrinkTimer: function () {
   1.935 +    if (!this._idleShrinkTimer) {
   1.936 +      return;
   1.937 +    }
   1.938 +
   1.939 +    this._idleShrinkTimer.initWithCallback(this.shrinkMemory.bind(this),
   1.940 +                                           this._idleShrinkMS,
   1.941 +                                           this._idleShrinkTimer.TYPE_ONE_SHOT);
   1.942 +  },
   1.943 +});
   1.944 +
   1.945 +this.Sqlite = {
   1.946 +  openConnection: openConnection,
   1.947 +  cloneStorageConnection: cloneStorageConnection
   1.948 +};

mercurial