|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 this.EXPORTED_SYMBOLS = [ |
|
8 "Sqlite", |
|
9 ]; |
|
10 |
|
11 const {classes: Cc, interfaces: Ci, utils: Cu} = Components; |
|
12 |
|
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"); |
|
18 |
|
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"); |
|
27 |
|
28 |
|
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(); |
|
32 |
|
33 |
|
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"); |
|
70 |
|
71 if (!options.path) { |
|
72 throw new Error("path not specified in connection options."); |
|
73 } |
|
74 |
|
75 // Retains absolute paths and normalizes relative as relative to profile. |
|
76 let path = OS.Path.join(OS.Constants.Path.profileDir, options.path); |
|
77 |
|
78 let sharedMemoryCache = "sharedMemoryCache" in options ? |
|
79 options.sharedMemoryCache : true; |
|
80 |
|
81 let openedOptions = {}; |
|
82 |
|
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 } |
|
88 |
|
89 openedOptions.shrinkMemoryOnConnectionIdleMS = |
|
90 options.shrinkMemoryOnConnectionIdleMS; |
|
91 } |
|
92 |
|
93 let file = FileUtils.File(path); |
|
94 |
|
95 let basename = OS.Path.basename(path); |
|
96 let number = connectionCounters.get(basename) || 0; |
|
97 connectionCounters.set(basename, number + 1); |
|
98 |
|
99 let identifier = basename + "#" + number; |
|
100 |
|
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 } |
|
127 |
|
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"); |
|
159 |
|
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 } |
|
167 |
|
168 let openedOptions = {}; |
|
169 |
|
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 } |
|
178 |
|
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; |
|
184 |
|
185 log.info("Cloning database: " + path + " (" + identifier + ")"); |
|
186 let deferred = Promise.defer(); |
|
187 |
|
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 } |
|
205 |
|
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 + ": "); |
|
256 |
|
257 this._log.info("Opened"); |
|
258 |
|
259 this._connection = connection; |
|
260 this._connectionIdentifier = basename + " Conn #" + number; |
|
261 this._open = true; |
|
262 |
|
263 this._cachedStatements = new Map(); |
|
264 this._anonymousStatements = new Map(); |
|
265 this._anonymousCounter = 0; |
|
266 |
|
267 // A map from statement index to mozIStoragePendingStatement, to allow for |
|
268 // canceling prior to finalizing the mozIStorageStatements. |
|
269 this._pendingStatements = new Map(); |
|
270 |
|
271 // Increments for each executed statement for the life of the connection. |
|
272 this._statementCounter = 0; |
|
273 |
|
274 this._inProgressTransaction = null; |
|
275 |
|
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 } |
|
284 |
|
285 OpenedConnection.prototype = Object.freeze({ |
|
286 TRANSACTION_DEFERRED: "DEFERRED", |
|
287 TRANSACTION_IMMEDIATE: "IMMEDIATE", |
|
288 TRANSACTION_EXCLUSIVE: "EXCLUSIVE", |
|
289 |
|
290 TRANSACTION_TYPES: ["DEFERRED", "IMMEDIATE", "EXCLUSIVE"], |
|
291 |
|
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 }, |
|
310 |
|
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 }, |
|
319 |
|
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 } |
|
341 |
|
342 this._log.debug("Request to close connection."); |
|
343 this._clearIdleShrinkTimer(); |
|
344 let deferred = Promise.defer(); |
|
345 |
|
346 AsyncShutdown.profileBeforeChange.addBlocker( |
|
347 "Sqlite.jsm: " + this._connectionIdentifier, |
|
348 deferred.promise |
|
349 ); |
|
350 |
|
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 } |
|
359 |
|
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."); |
|
364 |
|
365 let onRollback = this._finalize.bind(this, deferred); |
|
366 |
|
367 this.execute("ROLLBACK TRANSACTION").then(onRollback, onRollback); |
|
368 this._inProgressTransaction.reject(new Error("Connection being closed.")); |
|
369 this._inProgressTransaction = null; |
|
370 |
|
371 return deferred.promise; |
|
372 }, |
|
373 |
|
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(); |
|
390 |
|
391 this._log.debug("Request to clone connection."); |
|
392 |
|
393 let options = { |
|
394 connection: this._connection, |
|
395 readOnly: readOnly, |
|
396 }; |
|
397 if (this._idleShrinkMS) |
|
398 options.shrinkMemoryOnConnectionIdleMS = this._idleShrinkMS; |
|
399 |
|
400 return cloneStorageConnection(options); |
|
401 }, |
|
402 |
|
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(); |
|
410 |
|
411 // We no longer need to track these. |
|
412 this._statementCounter = 0; |
|
413 |
|
414 // Next we finalize all active statements. |
|
415 for (let [k, statement] of this._anonymousStatements) { |
|
416 statement.finalize(); |
|
417 } |
|
418 this._anonymousStatements.clear(); |
|
419 |
|
420 for (let [k, statement] of this._cachedStatements) { |
|
421 statement.finalize(); |
|
422 } |
|
423 this._cachedStatements.clear(); |
|
424 |
|
425 // This guards against operations performed between the call to this |
|
426 // function and asyncClose() finishing. See also bug 726990. |
|
427 this._open = false; |
|
428 |
|
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 }, |
|
438 |
|
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(); |
|
499 |
|
500 if (!sql) { |
|
501 throw new Error("sql argument is empty."); |
|
502 } |
|
503 |
|
504 let statement = this._cachedStatements.get(sql); |
|
505 if (!statement) { |
|
506 statement = this._connection.createAsyncStatement(sql); |
|
507 this._cachedStatements.set(sql, statement); |
|
508 } |
|
509 |
|
510 this._clearIdleShrinkTimer(); |
|
511 |
|
512 let deferred = Promise.defer(); |
|
513 |
|
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 } |
|
529 |
|
530 return deferred.promise; |
|
531 }, |
|
532 |
|
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 } |
|
552 |
|
553 this._ensureOpen(); |
|
554 |
|
555 let statement = this._connection.createAsyncStatement(sql); |
|
556 let index = this._anonymousCounter++; |
|
557 |
|
558 this._anonymousStatements.set(index, statement); |
|
559 this._clearIdleShrinkTimer(); |
|
560 |
|
561 let onFinished = function () { |
|
562 this._anonymousStatements.delete(index); |
|
563 statement.finalize(); |
|
564 this._startIdleShrinkTimer(); |
|
565 }.bind(this); |
|
566 |
|
567 let deferred = Promise.defer(); |
|
568 |
|
569 try { |
|
570 this._executeStatement(sql, statement, params, onRow).then( |
|
571 function onResult(rows) { |
|
572 onFinished(); |
|
573 deferred.resolve(rows); |
|
574 }.bind(this), |
|
575 |
|
576 function onError(error) { |
|
577 onFinished(); |
|
578 deferred.reject(error); |
|
579 }.bind(this) |
|
580 ); |
|
581 } catch (ex) { |
|
582 onFinished(); |
|
583 throw ex; |
|
584 } |
|
585 |
|
586 return deferred.promise; |
|
587 }, |
|
588 |
|
589 /** |
|
590 * Whether a transaction is currently in progress. |
|
591 */ |
|
592 get transactionInProgress() { |
|
593 return this._open && !!this._inProgressTransaction; |
|
594 }, |
|
595 |
|
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 } |
|
623 |
|
624 this._ensureOpen(); |
|
625 |
|
626 if (this._inProgressTransaction) { |
|
627 throw new Error("A transaction is already active. Only one transaction " + |
|
628 "can be active at a time."); |
|
629 } |
|
630 |
|
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"); |
|
639 |
|
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 } |
|
654 |
|
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 } |
|
663 |
|
664 throw ex; |
|
665 } |
|
666 |
|
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 } |
|
673 |
|
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 } |
|
681 |
|
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 ); |
|
693 |
|
694 return deferred.promise; |
|
695 }, |
|
696 |
|
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 }, |
|
716 |
|
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 }, |
|
736 |
|
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."); |
|
744 |
|
745 let onShrunk = this._clearIdleShrinkTimer.bind(this); |
|
746 |
|
747 return this.execute("PRAGMA shrink_memory").then(onShrunk, onShrunk); |
|
748 }, |
|
749 |
|
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 }, |
|
770 |
|
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 } |
|
779 |
|
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 } |
|
791 |
|
792 statement.bindParameters(paramsArray); |
|
793 return; |
|
794 } |
|
795 |
|
796 // Indexed params. |
|
797 for (let i = 0; i < params.length; i++) { |
|
798 statement.bindByIndex(i, params[i]); |
|
799 } |
|
800 return; |
|
801 } |
|
802 |
|
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 } |
|
810 |
|
811 throw new Error("Invalid type for bound parameters. Expected Array or " + |
|
812 "object. Got: " + params); |
|
813 }, |
|
814 |
|
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 } |
|
819 |
|
820 if (onRow && typeof(onRow) != "function") { |
|
821 throw new Error("onRow must be a function. Got: " + onRow); |
|
822 } |
|
823 |
|
824 this._bindParameters(statement, params); |
|
825 |
|
826 let index = this._statementCounter++; |
|
827 |
|
828 let deferred = Promise.defer(); |
|
829 let userCancelled = false; |
|
830 let errors = []; |
|
831 let rows = []; |
|
832 |
|
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; |
|
837 |
|
838 if (params) { |
|
839 msg += " - " + JSON.stringify(params); |
|
840 } |
|
841 this._log.trace(msg); |
|
842 } else { |
|
843 this._log.debug("Stmt #" + index + " starting"); |
|
844 } |
|
845 |
|
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 } |
|
856 |
|
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 }, |
|
869 |
|
870 handleError: function (error) { |
|
871 self._log.info("Error when executing SQL (" + error.result + "): " + |
|
872 error.message); |
|
873 errors.push(error); |
|
874 }, |
|
875 |
|
876 handleCompletion: function (reason) { |
|
877 self._log.debug("Stmt #" + index + " finished."); |
|
878 self._pendingStatements.delete(index); |
|
879 |
|
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; |
|
886 |
|
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 } |
|
896 |
|
897 break; |
|
898 |
|
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; |
|
904 |
|
905 default: |
|
906 deferred.reject(new Error("Unknown completion reason code: " + |
|
907 reason)); |
|
908 break; |
|
909 } |
|
910 }, |
|
911 }); |
|
912 |
|
913 this._pendingStatements.set(index, pending); |
|
914 return deferred.promise; |
|
915 }, |
|
916 |
|
917 _ensureOpen: function () { |
|
918 if (!this._open) { |
|
919 throw new Error("Connection is not open."); |
|
920 } |
|
921 }, |
|
922 |
|
923 _clearIdleShrinkTimer: function () { |
|
924 if (!this._idleShrinkTimer) { |
|
925 return; |
|
926 } |
|
927 |
|
928 this._idleShrinkTimer.cancel(); |
|
929 }, |
|
930 |
|
931 _startIdleShrinkTimer: function () { |
|
932 if (!this._idleShrinkTimer) { |
|
933 return; |
|
934 } |
|
935 |
|
936 this._idleShrinkTimer.initWithCallback(this.shrinkMemory.bind(this), |
|
937 this._idleShrinkMS, |
|
938 this._idleShrinkTimer.TYPE_ONE_SHOT); |
|
939 }, |
|
940 }); |
|
941 |
|
942 this.Sqlite = { |
|
943 openConnection: openConnection, |
|
944 cloneStorageConnection: cloneStorageConnection |
|
945 }; |