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.

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

mercurial