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 +};