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