2018-03-09 17:49:35 +00:00
|
|
|
const { uuid } = require("lib/uuid.js");
|
|
|
|
const { promiseChain } = require("lib/promise-utils.js");
|
|
|
|
const { Logger } = require("lib/logger.js");
|
|
|
|
const { time } = require("lib/time-utils.js");
|
|
|
|
const { sprintf } = require("sprintf-js");
|
2017-05-07 21:02:17 +00:00
|
|
|
|
|
|
|
class Database {
|
2017-06-11 21:11:14 +00:00
|
|
|
constructor(driver) {
|
2017-05-11 20:14:01 +00:00
|
|
|
this.debugMode_ = false;
|
2017-06-11 21:11:14 +00:00
|
|
|
this.driver_ = driver;
|
2017-06-14 23:14:15 +00:00
|
|
|
this.inTransaction_ = false;
|
2017-06-23 21:32:24 +00:00
|
|
|
|
|
|
|
this.logger_ = new Logger();
|
2017-10-07 16:30:27 +00:00
|
|
|
this.logExcludedQueryTypes_ = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
setLogExcludedQueryTypes(v) {
|
|
|
|
this.logExcludedQueryTypes_ = v;
|
2017-06-23 21:32:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Converts the SQLite error to a regular JS error
|
|
|
|
// so that it prints a stacktrace when passed to
|
|
|
|
// console.error()
|
2017-07-04 18:09:47 +00:00
|
|
|
sqliteErrorToJsError(error, sql = null, params = null) {
|
2017-07-05 21:29:00 +00:00
|
|
|
return this.driver().sqliteErrorToJsError(error, sql, params);
|
2017-06-23 21:32:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
setLogger(l) {
|
|
|
|
this.logger_ = l;
|
|
|
|
}
|
|
|
|
|
|
|
|
logger() {
|
|
|
|
return this.logger_;
|
2017-05-11 20:14:01 +00:00
|
|
|
}
|
2017-05-07 21:02:17 +00:00
|
|
|
|
2017-06-11 21:11:14 +00:00
|
|
|
driver() {
|
|
|
|
return this.driver_;
|
|
|
|
}
|
|
|
|
|
2017-07-04 18:09:47 +00:00
|
|
|
async open(options) {
|
|
|
|
await this.driver().open(options);
|
2018-03-09 17:49:35 +00:00
|
|
|
this.logger().info("Database was open successfully");
|
2017-06-11 21:11:14 +00:00
|
|
|
}
|
|
|
|
|
2017-06-25 12:49:46 +00:00
|
|
|
escapeField(field) {
|
2018-03-09 17:49:35 +00:00
|
|
|
if (field == "*") return "*";
|
|
|
|
let p = field.split(".");
|
|
|
|
if (p.length == 1) return "`" + field + "`";
|
|
|
|
if (p.length == 2) return p[0] + ".`" + p[1] + "`";
|
|
|
|
|
|
|
|
throw new Error("Invalid field format: " + field);
|
2017-06-25 12:49:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
escapeFields(fields) {
|
2018-03-09 17:49:35 +00:00
|
|
|
if (fields == "*") return "*";
|
2017-07-03 18:58:01 +00:00
|
|
|
|
2017-06-25 12:49:46 +00:00
|
|
|
let output = [];
|
|
|
|
for (let i = 0; i < fields.length; i++) {
|
|
|
|
output.push(this.escapeField(fields[i]));
|
|
|
|
}
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
2017-07-03 18:58:01 +00:00
|
|
|
async tryCall(callName, sql, params) {
|
2018-03-09 17:49:35 +00:00
|
|
|
if (typeof sql === "object") {
|
2017-07-02 15:46:03 +00:00
|
|
|
params = sql.params;
|
|
|
|
sql = sql.sql;
|
|
|
|
}
|
|
|
|
|
2017-06-26 23:20:01 +00:00
|
|
|
let waitTime = 50;
|
|
|
|
let totalWaitTime = 0;
|
|
|
|
while (true) {
|
|
|
|
try {
|
|
|
|
this.logQuery(sql, params);
|
2017-07-03 18:58:01 +00:00
|
|
|
let result = await this.driver()[callName](sql, params);
|
|
|
|
return result; // No exception was thrown
|
2017-06-26 23:20:01 +00:00
|
|
|
} catch (error) {
|
2018-03-09 17:49:35 +00:00
|
|
|
if (error && (error.code == "SQLITE_IOERR" || error.code == "SQLITE_BUSY")) {
|
2017-07-03 18:58:01 +00:00
|
|
|
if (totalWaitTime >= 20000) throw this.sqliteErrorToJsError(error, sql, params);
|
2017-11-27 22:50:46 +00:00
|
|
|
// NOTE: don't put logger statements here because it might log to the database, which
|
|
|
|
// could result in an error being thrown again.
|
|
|
|
// this.logger().warn(sprintf('Error %s: will retry in %s milliseconds', error.code, waitTime));
|
|
|
|
// this.logger().warn('Error was: ' + error.toString());
|
2017-06-26 23:20:01 +00:00
|
|
|
await time.msleep(waitTime);
|
|
|
|
totalWaitTime += waitTime;
|
|
|
|
waitTime *= 1.5;
|
|
|
|
} else {
|
|
|
|
throw this.sqliteErrorToJsError(error, sql, params);
|
2017-07-03 18:58:01 +00:00
|
|
|
}
|
2017-06-26 23:20:01 +00:00
|
|
|
}
|
|
|
|
}
|
2017-06-11 21:11:14 +00:00
|
|
|
}
|
2017-05-07 21:02:17 +00:00
|
|
|
|
2017-07-03 18:58:01 +00:00
|
|
|
async selectOne(sql, params = null) {
|
2018-03-09 17:49:35 +00:00
|
|
|
return this.tryCall("selectOne", sql, params);
|
2017-07-03 18:58:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async selectAll(sql, params = null) {
|
2018-03-09 17:49:35 +00:00
|
|
|
return this.tryCall("selectAll", sql, params);
|
2017-07-03 18:58:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async exec(sql, params = null) {
|
2018-03-09 17:49:35 +00:00
|
|
|
return this.tryCall("exec", sql, params);
|
2017-07-03 18:58:01 +00:00
|
|
|
}
|
|
|
|
|
2017-12-20 19:45:25 +00:00
|
|
|
async transactionExecBatch(queries) {
|
|
|
|
if (queries.length <= 0) return;
|
2017-06-14 23:14:15 +00:00
|
|
|
|
|
|
|
if (queries.length == 1) {
|
2017-06-24 23:19:11 +00:00
|
|
|
let q = this.wrapQuery(queries[0]);
|
2017-12-20 19:45:25 +00:00
|
|
|
await this.exec(q.sql, q.params);
|
|
|
|
return;
|
2017-06-14 23:14:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// There can be only one transaction running at a time so queue
|
|
|
|
// any new transaction here.
|
|
|
|
if (this.inTransaction_) {
|
2017-12-20 19:45:25 +00:00
|
|
|
while (true) {
|
|
|
|
await time.msleep(100);
|
|
|
|
if (!this.inTransaction_) {
|
|
|
|
this.inTransaction_ = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// return new Promise((resolve, reject) => {
|
|
|
|
// let iid = setInterval(() => {
|
|
|
|
// if (!this.inTransaction_) {
|
|
|
|
// clearInterval(iid);
|
|
|
|
// this.transactionExecBatch(queries).then(() => {
|
|
|
|
// resolve();
|
|
|
|
// }).catch((error) => {
|
|
|
|
// reject(error);
|
|
|
|
// });
|
|
|
|
// }
|
|
|
|
// }, 100);
|
|
|
|
// });
|
2017-06-14 23:14:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.inTransaction_ = true;
|
|
|
|
|
2018-03-09 17:49:35 +00:00
|
|
|
queries.splice(0, 0, "BEGIN TRANSACTION");
|
|
|
|
queries.push("COMMIT"); // Note: ROLLBACK is currently not supported
|
2017-06-14 19:59:46 +00:00
|
|
|
|
2017-06-11 21:11:14 +00:00
|
|
|
for (let i = 0; i < queries.length; i++) {
|
|
|
|
let query = this.wrapQuery(queries[i]);
|
2017-12-20 19:45:25 +00:00
|
|
|
await this.exec(query.sql, query.params);
|
2017-06-11 21:11:14 +00:00
|
|
|
}
|
2017-06-14 19:59:46 +00:00
|
|
|
|
2017-12-20 19:45:25 +00:00
|
|
|
this.inTransaction_ = false;
|
|
|
|
|
|
|
|
// return promiseChain(chain).then(() => {
|
|
|
|
// this.inTransaction_ = false;
|
|
|
|
// });
|
|
|
|
|
|
|
|
// if (queries.length <= 0) return Promise.resolve();
|
|
|
|
|
|
|
|
// if (queries.length == 1) {
|
|
|
|
// let q = this.wrapQuery(queries[0]);
|
|
|
|
// return this.exec(q.sql, q.params);
|
|
|
|
// }
|
|
|
|
|
|
|
|
// // There can be only one transaction running at a time so queue
|
|
|
|
// // any new transaction here.
|
|
|
|
// if (this.inTransaction_) {
|
|
|
|
// return new Promise((resolve, reject) => {
|
|
|
|
// let iid = setInterval(() => {
|
|
|
|
// if (!this.inTransaction_) {
|
|
|
|
// clearInterval(iid);
|
|
|
|
// this.transactionExecBatch(queries).then(() => {
|
|
|
|
// resolve();
|
|
|
|
// }).catch((error) => {
|
|
|
|
// reject(error);
|
|
|
|
// });
|
|
|
|
// }
|
|
|
|
// }, 100);
|
|
|
|
// });
|
|
|
|
// }
|
|
|
|
|
|
|
|
// this.inTransaction_ = true;
|
|
|
|
|
|
|
|
// queries.splice(0, 0, 'BEGIN TRANSACTION');
|
|
|
|
// queries.push('COMMIT'); // Note: ROLLBACK is currently not supported
|
|
|
|
|
|
|
|
// let chain = [];
|
|
|
|
// for (let i = 0; i < queries.length; i++) {
|
|
|
|
// let query = this.wrapQuery(queries[i]);
|
|
|
|
// chain.push(() => {
|
|
|
|
// return this.exec(query.sql, query.params);
|
|
|
|
// });
|
|
|
|
// }
|
|
|
|
|
|
|
|
// return promiseChain(chain).then(() => {
|
|
|
|
// this.inTransaction_ = false;
|
|
|
|
// });
|
2017-05-12 20:17:23 +00:00
|
|
|
}
|
|
|
|
|
2017-05-19 22:16:50 +00:00
|
|
|
static enumId(type, s) {
|
2018-03-09 17:49:35 +00:00
|
|
|
if (type == "settings") {
|
|
|
|
if (s == "int") return 1;
|
|
|
|
if (s == "string") return 2;
|
2017-05-12 20:17:23 +00:00
|
|
|
}
|
2018-03-09 17:49:35 +00:00
|
|
|
if (type == "fieldType") {
|
2017-12-12 21:58:57 +00:00
|
|
|
if (s) s = s.toUpperCase();
|
2018-03-09 17:49:35 +00:00
|
|
|
if (s == "INTEGER") s = "INT";
|
|
|
|
if (!("TYPE_" + s in this)) throw new Error("Unkonwn fieldType: " + s);
|
|
|
|
return this["TYPE_" + s];
|
2017-05-19 22:16:50 +00:00
|
|
|
}
|
2018-03-09 17:49:35 +00:00
|
|
|
if (type == "syncTarget") {
|
|
|
|
if (s == "memory") return 1;
|
|
|
|
if (s == "filesystem") return 2;
|
|
|
|
if (s == "onedrive") return 3;
|
2017-07-16 12:53:59 +00:00
|
|
|
}
|
2018-03-09 17:49:35 +00:00
|
|
|
throw new Error("Unknown enum type or value: " + type + ", " + s);
|
2017-05-07 21:02:17 +00:00
|
|
|
}
|
|
|
|
|
2017-12-07 18:12:46 +00:00
|
|
|
static enumName(type, id) {
|
2018-03-09 17:49:35 +00:00
|
|
|
if (type === "fieldType") {
|
|
|
|
if (id === Database.TYPE_UNKNOWN) return "unknown";
|
|
|
|
if (id === Database.TYPE_INT) return "int";
|
|
|
|
if (id === Database.TYPE_TEXT) return "text";
|
|
|
|
if (id === Database.TYPE_NUMERIC) return "numeric";
|
|
|
|
throw new Error("Invalid type id: " + id);
|
2017-12-07 18:12:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-19 22:16:50 +00:00
|
|
|
static formatValue(type, value) {
|
|
|
|
if (value === null || value === undefined) return null;
|
|
|
|
if (type == this.TYPE_INT) return Number(value);
|
|
|
|
if (type == this.TYPE_TEXT) return value;
|
|
|
|
if (type == this.TYPE_NUMERIC) return Number(value);
|
2018-03-09 17:49:35 +00:00
|
|
|
throw new Error("Unknown type: " + type);
|
2017-05-19 22:16:50 +00:00
|
|
|
}
|
|
|
|
|
2017-05-07 21:02:17 +00:00
|
|
|
sqlStringToLines(sql) {
|
|
|
|
let output = [];
|
|
|
|
let lines = sql.split("\n");
|
2018-03-09 17:49:35 +00:00
|
|
|
let statement = "";
|
2017-05-07 21:02:17 +00:00
|
|
|
for (var i = 0; i < lines.length; i++) {
|
|
|
|
var line = lines[i];
|
2018-03-09 17:49:35 +00:00
|
|
|
if (line == "") continue;
|
2017-05-07 21:02:17 +00:00
|
|
|
if (line.substr(0, 2) == "--") continue;
|
2017-07-19 19:15:55 +00:00
|
|
|
statement += line.trim();
|
2018-03-09 17:49:35 +00:00
|
|
|
if (line[line.length - 1] == ",") statement += " ";
|
|
|
|
if (line[line.length - 1] == ";") {
|
2017-05-07 21:02:17 +00:00
|
|
|
output.push(statement);
|
2018-03-09 17:49:35 +00:00
|
|
|
statement = "";
|
2017-05-07 21:02:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
2017-05-11 20:14:01 +00:00
|
|
|
logQuery(sql, params = null) {
|
2017-10-07 16:30:27 +00:00
|
|
|
if (this.logExcludedQueryTypes_.length) {
|
|
|
|
const temp = sql.toLowerCase();
|
|
|
|
for (let i = 0; i < this.logExcludedQueryTypes_.length; i++) {
|
|
|
|
if (temp.indexOf(this.logExcludedQueryTypes_[i].toLowerCase()) === 0) return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-25 11:39:42 +00:00
|
|
|
this.logger().debug(sql);
|
2017-06-25 12:49:46 +00:00
|
|
|
if (params !== null && params.length) this.logger().debug(JSON.stringify(params));
|
2017-05-18 20:31:40 +00:00
|
|
|
}
|
|
|
|
|
2017-05-11 20:14:01 +00:00
|
|
|
static insertQuery(tableName, data) {
|
2018-03-09 17:49:35 +00:00
|
|
|
if (!data || !Object.keys(data).length) throw new Error("Data is empty");
|
2017-06-17 23:49:52 +00:00
|
|
|
|
2018-03-09 17:49:35 +00:00
|
|
|
let keySql = "";
|
|
|
|
let valueSql = "";
|
2017-05-11 20:14:01 +00:00
|
|
|
let params = [];
|
2017-05-10 19:51:43 +00:00
|
|
|
for (let key in data) {
|
2017-05-11 20:14:01 +00:00
|
|
|
if (!data.hasOwnProperty(key)) continue;
|
2018-03-09 17:49:35 +00:00
|
|
|
if (key[key.length - 1] == "_") continue;
|
|
|
|
if (keySql != "") keySql += ", ";
|
|
|
|
if (valueSql != "") valueSql += ", ";
|
|
|
|
keySql += "`" + key + "`";
|
|
|
|
valueSql += "?";
|
2017-05-11 20:14:01 +00:00
|
|
|
params.push(data[key]);
|
2017-05-10 19:51:43 +00:00
|
|
|
}
|
2017-05-11 20:14:01 +00:00
|
|
|
return {
|
2018-03-09 17:49:35 +00:00
|
|
|
sql: "INSERT INTO `" + tableName + "` (" + keySql + ") VALUES (" + valueSql + ")",
|
2017-05-11 20:14:01 +00:00
|
|
|
params: params,
|
|
|
|
};
|
2017-05-10 19:51:43 +00:00
|
|
|
}
|
|
|
|
|
2017-05-12 19:54:06 +00:00
|
|
|
static updateQuery(tableName, data, where) {
|
2018-03-09 17:49:35 +00:00
|
|
|
if (!data || !Object.keys(data).length) throw new Error("Data is empty");
|
2017-06-17 23:49:52 +00:00
|
|
|
|
2018-03-09 17:49:35 +00:00
|
|
|
let sql = "";
|
2017-05-12 19:54:06 +00:00
|
|
|
let params = [];
|
|
|
|
for (let key in data) {
|
|
|
|
if (!data.hasOwnProperty(key)) continue;
|
2018-03-09 17:49:35 +00:00
|
|
|
if (key[key.length - 1] == "_") continue;
|
|
|
|
if (sql != "") sql += ", ";
|
|
|
|
sql += "`" + key + "`=?";
|
2017-05-12 19:54:06 +00:00
|
|
|
params.push(data[key]);
|
|
|
|
}
|
|
|
|
|
2018-03-09 17:49:35 +00:00
|
|
|
if (typeof where != "string") {
|
2017-07-13 18:47:31 +00:00
|
|
|
let s = [];
|
|
|
|
for (let n in where) {
|
|
|
|
if (!where.hasOwnProperty(n)) continue;
|
|
|
|
params.push(where[n]);
|
2018-03-09 17:49:35 +00:00
|
|
|
s.push("`" + n + "`=?");
|
2017-07-13 18:47:31 +00:00
|
|
|
}
|
2018-03-09 17:49:35 +00:00
|
|
|
where = s.join(" AND ");
|
2017-05-12 19:54:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
2018-03-09 17:49:35 +00:00
|
|
|
sql: "UPDATE `" + tableName + "` SET " + sql + " WHERE " + where,
|
2017-05-12 19:54:06 +00:00
|
|
|
params: params,
|
|
|
|
};
|
|
|
|
}
|
2017-07-25 21:55:26 +00:00
|
|
|
|
2017-07-26 17:49:01 +00:00
|
|
|
alterColumnQueries(tableName, fields) {
|
|
|
|
let fieldsNoType = [];
|
|
|
|
for (let n in fields) {
|
|
|
|
if (!fields.hasOwnProperty(n)) continue;
|
|
|
|
fieldsNoType.push(n);
|
|
|
|
}
|
|
|
|
|
|
|
|
let fieldsWithType = [];
|
|
|
|
for (let n in fields) {
|
|
|
|
if (!fields.hasOwnProperty(n)) continue;
|
2018-03-09 17:49:35 +00:00
|
|
|
fieldsWithType.push(this.escapeField(n) + " " + fields[n]);
|
|
|
|
}
|
2017-07-26 17:49:01 +00:00
|
|
|
|
2017-07-25 21:55:26 +00:00
|
|
|
let sql = `
|
2017-07-26 17:49:01 +00:00
|
|
|
CREATE TEMPORARY TABLE _BACKUP_TABLE_NAME_(_FIELDS_TYPE_);
|
|
|
|
INSERT INTO _BACKUP_TABLE_NAME_ SELECT _FIELDS_NO_TYPE_ FROM _TABLE_NAME_;
|
2017-07-25 21:55:26 +00:00
|
|
|
DROP TABLE _TABLE_NAME_;
|
2017-07-26 17:49:01 +00:00
|
|
|
CREATE TABLE _TABLE_NAME_(_FIELDS_TYPE_);
|
|
|
|
INSERT INTO _TABLE_NAME_ SELECT _FIELDS_NO_TYPE_ FROM _BACKUP_TABLE_NAME_;
|
2017-07-25 21:55:26 +00:00
|
|
|
DROP TABLE _BACKUP_TABLE_NAME_;
|
|
|
|
`;
|
|
|
|
|
2018-03-09 17:49:35 +00:00
|
|
|
sql = sql.replace(/_BACKUP_TABLE_NAME_/g, this.escapeField(tableName + "_backup"));
|
2017-07-25 21:55:26 +00:00
|
|
|
sql = sql.replace(/_TABLE_NAME_/g, this.escapeField(tableName));
|
2018-03-09 17:49:35 +00:00
|
|
|
sql = sql.replace(/_FIELDS_NO_TYPE_/g, this.escapeFields(fieldsNoType).join(","));
|
|
|
|
sql = sql.replace(/_FIELDS_TYPE_/g, fieldsWithType.join(","));
|
2017-07-25 21:55:26 +00:00
|
|
|
|
|
|
|
return sql.trim().split("\n");
|
|
|
|
}
|
2018-03-09 17:49:35 +00:00
|
|
|
|
2017-06-11 21:11:14 +00:00
|
|
|
wrapQueries(queries) {
|
|
|
|
let output = [];
|
|
|
|
for (let i = 0; i < queries.length; i++) {
|
|
|
|
output.push(this.wrapQuery(queries[i]));
|
|
|
|
}
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
|
|
|
wrapQuery(sql, params = null) {
|
2018-03-09 17:49:35 +00:00
|
|
|
if (!sql) throw new Error("Cannot wrap empty string: " + sql);
|
2017-06-11 21:11:14 +00:00
|
|
|
|
|
|
|
if (sql.constructor === Array) {
|
|
|
|
let output = {};
|
|
|
|
output.sql = sql[0];
|
|
|
|
output.params = sql.length >= 2 ? sql[1] : null;
|
|
|
|
return output;
|
2018-03-09 17:49:35 +00:00
|
|
|
} else if (typeof sql === "string") {
|
2017-06-11 21:11:14 +00:00
|
|
|
return { sql: sql, params: params };
|
|
|
|
} else {
|
|
|
|
return sql; // Already wrapped
|
|
|
|
}
|
2017-05-12 20:17:23 +00:00
|
|
|
}
|
2017-05-07 21:02:17 +00:00
|
|
|
}
|
|
|
|
|
2017-12-04 22:58:42 +00:00
|
|
|
Database.TYPE_UNKNOWN = 0;
|
2017-06-06 20:58:19 +00:00
|
|
|
Database.TYPE_INT = 1;
|
|
|
|
Database.TYPE_TEXT = 2;
|
2017-06-15 18:18:48 +00:00
|
|
|
Database.TYPE_NUMERIC = 3;
|
2017-06-06 20:58:19 +00:00
|
|
|
|
2018-03-09 17:49:35 +00:00
|
|
|
module.exports = { Database };
|