/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see .
***** END LICENSE BLOCK *****
*/
"use strict";
// Exclusive locking mode (default) prevents access to Zotero database while Zotero is open
// and speeds up DB access (http://www.sqlite.org/pragma.html#pragma_locking_mode).
// Normal mode is more convenient for development, but risks database corruption, particularly if
// the same database is accessed simultaneously by multiple Zotero instances.
const DB_LOCK_EXCLUSIVE = true;
Zotero.DBConnection = function(dbName) {
if (!dbName) {
throw ('DB name not provided in Zotero.DBConnection()');
}
this.MAX_BOUND_PARAMETERS = 999;
Components.utils.import("resource://gre/modules/Sqlite.jsm", this);
this.skipBackup = false;
this.transactionVacuum = false;
// JS Date
this.__defineGetter__('transactionDate', function () {
if (this._transactionDate) {
this._lastTransactionDate = this._transactionDate;
return this._transactionDate;
}
throw new Error("Transaction not in progress");
// Use second granularity rather than millisecond
// for comparison purposes
var d = new Date(Math.floor(new Date / 1000) * 1000);
this._lastTransactionDate = d;
return d;
});
// SQL DATETIME
this.__defineGetter__('transactionDateTime', function () {
var d = this.transactionDate;
return Zotero.Date.dateToSQL(d, true);
});
// Unix timestamp
this.__defineGetter__('transactionTimestamp', function () {
var d = this.transactionDate;
return Zotero.Date.toUnixTimestamp(d);
});
// Private members
this._dbName = dbName;
this._shutdown = false;
this._connection = null;
this._transactionID = null;
this._transactionDate = null;
this._lastTransactionDate = null;
this._transactionRollback = false;
this._transactionNestingLevel = 0;
this._callbacks = {
begin: [],
commit: [],
rollback: [],
current: {
commit: [],
rollback: []
}
};
this._dbIsCorrupt = null
this._transactionPromise = null;
}
/////////////////////////////////////////////////////////////////
//
// Public methods
//
/////////////////////////////////////////////////////////////////
/**
* Test a read-only connection to the database, throwing any errors that occur
*
* @return void
*/
Zotero.DBConnection.prototype.test = function () {
return this._getConnectionAsync().return();
}
Zotero.DBConnection.prototype.getAsyncStatement = Zotero.Promise.coroutine(function* (sql) {
var conn = yield this._getConnectionAsync();
conn = conn._connection;
try {
this._debug(sql, 4);
return conn.createAsyncStatement(sql);
}
catch (e) {
var dberr = (conn.lastErrorString != 'not an error')
? ' [ERROR: ' + conn.lastErrorString + ']' : '';
throw new Error(e + ' [QUERY: ' + sql + ']' + dberr);
}
});
Zotero.DBConnection.prototype.parseQueryAndParams = function (sql, params) {
// If single scalar value, wrap in an array
if (!Array.isArray(params)) {
if (typeof params == 'string' || typeof params == 'number' || params === null) {
params = [params];
}
else {
params = [];
}
}
// Otherwise, since we might make changes, only work on a copy of the array
else {
params = params.concat();
}
// Find placeholders
if (params.length) {
let matches = sql.match(/\?\d*/g);
if (!matches) {
throw new Error("Parameters provided for query without placeholders "
+ "[QUERY: " + sql + "]");
}
else {
// Count numbered parameters (?1) properly
let num = 0;
let numbered = {};
for (let i = 0; i < matches.length; i++) {
let match = matches[i];
if (match == '?') {
num++;
}
else {
numbered[match] = true;
}
}
num += Object.keys(numbered).length;
if (params.length != num) {
throw new Error("Incorrect number of parameters provided for query "
+ "(" + params.length + ", expecting " + num + ") "
+ "[QUERY: " + sql + "]");
}
}
// First, determine the type of query using first word
let queryMethod = sql.match(/^[^\s\(]*/)[0].toLowerCase();
// Reset lastIndex, since regexp isn't recompiled dynamically
let placeholderRE = /\s*[=,(]\s*\?/g;
for (var i=0; i "a"
// // '1' => "b"
// // '2' => "c"
// // '3' => "d"
//
// let rows = yield Zotero.DB.queryAsync("SELECT * FROM tmpFoo");
// for (let i=0; i} A promise for either the value or FALSE if no result
*/
Zotero.DBConnection.prototype.valueQueryAsync = Zotero.Promise.coroutine(function* (sql, params, options = {}) {
try {
let conn = this._getConnection(options) || (yield this._getConnectionAsync(options));
[sql, params] = this.parseQueryAndParams(sql, params);
if (Zotero.Debug.enabled) {
this.logQuery(sql, params);
}
let rows = yield conn.executeCached(sql, params);
return rows.length ? rows[0].getResultByIndex(0) : false;
}
catch (e) {
if (e.errors && e.errors[0]) {
var eStr = e + "";
eStr = eStr.indexOf("Error: ") == 0 ? eStr.substr(7): e;
throw new Error(eStr + ' [QUERY: ' + sql + '] '
+ (params ? '[PARAMS: ' + params.join(', ') + '] ' : '')
+ '[ERROR: ' + e.errors[0].message + ']');
}
else {
throw e;
}
}
});
/**
* @param {String} sql SQL statement to run
* @param {Array|String|Integer} [params] SQL parameters to bind
* @return {Promise