2017-10-21 21:10:33 -04:00
|
|
|
"use strict";
|
|
|
|
|
2021-04-07 22:01:52 +02:00
|
|
|
/**
|
|
|
|
* @module sql
|
|
|
|
*/
|
|
|
|
|
2024-02-16 20:27:00 +02:00
|
|
|
const log = require('./log.ts');
|
2020-06-20 21:42:41 +02:00
|
|
|
const Database = require('better-sqlite3');
|
2024-02-16 21:03:37 +02:00
|
|
|
const dataDir = require('./data_dir.ts');
|
2023-11-22 19:34:48 +01:00
|
|
|
const cls = require('./cls.js');
|
2022-01-17 23:47:26 +01:00
|
|
|
const fs = require("fs-extra");
|
2017-10-14 23:31:44 -04:00
|
|
|
|
2020-06-20 21:42:41 +02:00
|
|
|
const dbConnection = new Database(dataDir.DOCUMENT_PATH);
|
|
|
|
dbConnection.pragma('journal_mode = WAL');
|
2017-12-03 22:29:23 -05:00
|
|
|
|
2021-12-29 23:19:05 +01:00
|
|
|
const LOG_ALL_QUERIES = false;
|
|
|
|
|
2020-06-15 17:56:53 +02:00
|
|
|
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach(eventType => {
|
2020-04-14 22:15:55 +02:00
|
|
|
process.on(eventType, () => {
|
|
|
|
if (dbConnection) {
|
|
|
|
// closing connection is especially important to fold -wal file into the main DB file
|
|
|
|
// (see https://sqlite.org/tempfiles.html for details)
|
|
|
|
dbConnection.close();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
function insert(tableName, rec, replace = false) {
|
2023-09-19 23:48:55 +02:00
|
|
|
const keys = Object.keys(rec || {});
|
2017-10-25 22:39:21 -04:00
|
|
|
if (keys.length === 0) {
|
2022-12-21 15:19:05 +01:00
|
|
|
log.error(`Can't insert empty object into table ${tableName}`);
|
2017-10-25 22:39:21 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const columns = keys.join(", ");
|
|
|
|
const questionMarks = keys.map(p => "?").join(", ");
|
|
|
|
|
2022-12-21 15:19:05 +01:00
|
|
|
const query = `INSERT
|
|
|
|
${replace ? "OR REPLACE" : ""} INTO
|
|
|
|
${tableName}
|
|
|
|
(
|
|
|
|
${columns}
|
|
|
|
)
|
|
|
|
VALUES (${questionMarks})`;
|
2017-10-14 23:31:44 -04:00
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
const res = execute(query, Object.values(rec));
|
2017-10-14 23:31:44 -04:00
|
|
|
|
2021-02-27 21:09:13 +01:00
|
|
|
return res ? res.lastInsertRowid : null;
|
2017-10-14 23:31:44 -04:00
|
|
|
}
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
function replace(tableName, rec) {
|
|
|
|
return insert(tableName, rec, true);
|
2019-03-25 22:06:17 +01:00
|
|
|
}
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
function upsert(tableName, primaryKey, rec) {
|
2023-09-19 23:48:55 +02:00
|
|
|
const keys = Object.keys(rec || {});
|
2019-03-25 22:06:17 +01:00
|
|
|
if (keys.length === 0) {
|
2022-12-21 15:19:05 +01:00
|
|
|
log.error(`Can't upsert empty object into table ${tableName}`);
|
2019-03-25 22:06:17 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const columns = keys.join(", ");
|
|
|
|
|
2022-12-21 15:19:05 +01:00
|
|
|
const questionMarks = keys.map(colName => `@${colName}`).join(", ");
|
2019-03-25 22:06:17 +01:00
|
|
|
|
2020-06-17 23:03:46 +02:00
|
|
|
const updateMarks = keys.map(colName => `${colName} = @${colName}`).join(", ");
|
2019-03-25 22:06:17 +01:00
|
|
|
|
|
|
|
const query = `INSERT INTO ${tableName} (${columns}) VALUES (${questionMarks})
|
|
|
|
ON CONFLICT (${primaryKey}) DO UPDATE SET ${updateMarks}`;
|
|
|
|
|
2020-06-17 23:03:46 +02:00
|
|
|
for (const idx in rec) {
|
|
|
|
if (rec[idx] === true || rec[idx] === false) {
|
|
|
|
rec[idx] = rec[idx] ? 1 : 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
execute(query, rec);
|
2017-10-29 22:22:30 -04:00
|
|
|
}
|
|
|
|
|
2020-06-17 23:03:46 +02:00
|
|
|
const statementCache = {};
|
|
|
|
|
|
|
|
function stmt(sql) {
|
|
|
|
if (!(sql in statementCache)) {
|
|
|
|
statementCache[sql] = dbConnection.prepare(sql);
|
|
|
|
}
|
|
|
|
|
|
|
|
return statementCache[sql];
|
|
|
|
}
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
function getRow(query, params = []) {
|
2020-06-20 21:42:41 +02:00
|
|
|
return wrap(query, s => s.get(params));
|
2017-10-14 23:31:44 -04:00
|
|
|
}
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
function getRowOrNull(query, params = []) {
|
|
|
|
const all = getRows(query, params);
|
2017-10-28 19:55:55 -04:00
|
|
|
|
2017-10-29 22:22:30 -04:00
|
|
|
return all.length > 0 ? all[0] : null;
|
|
|
|
}
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
function getValue(query, params = []) {
|
2021-12-23 12:54:21 +01:00
|
|
|
return wrap(query, s => s.pluck().get(params));
|
2017-10-28 19:55:55 -04:00
|
|
|
}
|
|
|
|
|
2020-08-16 22:57:48 +02:00
|
|
|
// smaller values can result in better performance due to better usage of statement cache
|
|
|
|
const PARAM_LIMIT = 100;
|
2018-05-30 20:28:10 -04:00
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
function getManyRows(query, params) {
|
2018-05-30 20:28:10 -04:00
|
|
|
let results = [];
|
|
|
|
|
|
|
|
while (params.length > 0) {
|
2019-05-13 20:40:00 +02:00
|
|
|
const curParams = params.slice(0, Math.min(params.length, PARAM_LIMIT));
|
2018-05-30 20:28:10 -04:00
|
|
|
params = params.slice(curParams.length);
|
|
|
|
|
2020-06-17 23:03:46 +02:00
|
|
|
const curParamsObj = {};
|
|
|
|
|
|
|
|
let j = 1;
|
|
|
|
for (const param of curParams) {
|
|
|
|
curParamsObj['param' + j++] = param;
|
|
|
|
}
|
|
|
|
|
2018-05-30 20:28:10 -04:00
|
|
|
let i = 1;
|
2020-06-17 23:03:46 +02:00
|
|
|
const questionMarks = curParams.map(() => ":param" + i++).join(",");
|
2018-05-30 20:28:10 -04:00
|
|
|
const curQuery = query.replace(/\?\?\?/g, questionMarks);
|
|
|
|
|
2020-08-16 22:57:48 +02:00
|
|
|
const statement = curParams.length === PARAM_LIMIT
|
|
|
|
? stmt(curQuery)
|
|
|
|
: dbConnection.prepare(curQuery);
|
|
|
|
|
|
|
|
const subResults = statement.all(curParamsObj);
|
2020-06-20 23:24:34 +02:00
|
|
|
results = results.concat(subResults);
|
2018-05-30 20:28:10 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
function getRows(query, params = []) {
|
2020-06-20 21:42:41 +02:00
|
|
|
return wrap(query, s => s.all(params));
|
|
|
|
}
|
|
|
|
|
2021-07-24 12:04:48 +02:00
|
|
|
function getRawRows(query, params = []) {
|
|
|
|
return wrap(query, s => s.raw().all(params));
|
|
|
|
}
|
|
|
|
|
2020-06-20 21:42:41 +02:00
|
|
|
function iterateRows(query, params = []) {
|
2021-12-29 23:19:05 +01:00
|
|
|
if (LOG_ALL_QUERIES) {
|
|
|
|
console.log(query);
|
|
|
|
}
|
|
|
|
|
2020-06-20 21:42:41 +02:00
|
|
|
return stmt(query).iterate(params);
|
2017-10-14 23:31:44 -04:00
|
|
|
}
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
function getMap(query, params = []) {
|
2017-11-02 22:55:22 -04:00
|
|
|
const map = {};
|
2021-12-23 12:54:21 +01:00
|
|
|
const results = getRawRows(query, params);
|
2017-11-02 22:55:22 -04:00
|
|
|
|
|
|
|
for (const row of results) {
|
2021-12-23 12:54:21 +01:00
|
|
|
map[row[0]] = row[1];
|
2017-11-02 22:55:22 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return map;
|
|
|
|
}
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
function getColumn(query, params = []) {
|
2021-12-23 12:54:21 +01:00
|
|
|
return wrap(query, s => s.pluck().all(params));
|
2017-10-24 23:14:26 -04:00
|
|
|
}
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
function execute(query, params = []) {
|
2020-06-20 21:42:41 +02:00
|
|
|
return wrap(query, s => s.run(params));
|
2017-10-14 23:31:44 -04:00
|
|
|
}
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
function executeMany(query, params) {
|
2021-12-29 23:19:05 +01:00
|
|
|
if (LOG_ALL_QUERIES) {
|
|
|
|
console.log(query);
|
|
|
|
}
|
|
|
|
|
2020-07-09 23:59:27 +02:00
|
|
|
while (params.length > 0) {
|
|
|
|
const curParams = params.slice(0, Math.min(params.length, PARAM_LIMIT));
|
|
|
|
params = params.slice(curParams.length);
|
|
|
|
|
|
|
|
const curParamsObj = {};
|
|
|
|
|
|
|
|
let j = 1;
|
|
|
|
for (const param of curParams) {
|
|
|
|
curParamsObj['param' + j++] = param;
|
|
|
|
}
|
|
|
|
|
|
|
|
let i = 1;
|
|
|
|
const questionMarks = curParams.map(() => ":param" + i++).join(",");
|
|
|
|
const curQuery = query.replace(/\?\?\?/g, questionMarks);
|
|
|
|
|
|
|
|
dbConnection.prepare(curQuery).run(curParamsObj);
|
|
|
|
}
|
2019-11-01 22:09:51 +01:00
|
|
|
}
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
function executeScript(query) {
|
2021-12-29 23:19:05 +01:00
|
|
|
if (LOG_ALL_QUERIES) {
|
|
|
|
console.log(query);
|
|
|
|
}
|
|
|
|
|
2020-06-20 21:42:41 +02:00
|
|
|
return dbConnection.exec(query);
|
2017-10-15 17:31:49 -04:00
|
|
|
}
|
|
|
|
|
2020-06-20 21:42:41 +02:00
|
|
|
function wrap(query, func) {
|
|
|
|
const startTimestamp = Date.now();
|
2020-08-30 22:29:38 +02:00
|
|
|
let result;
|
2020-05-03 21:27:24 +02:00
|
|
|
|
2021-12-29 23:19:05 +01:00
|
|
|
if (LOG_ALL_QUERIES) {
|
|
|
|
console.log(query);
|
|
|
|
}
|
|
|
|
|
2020-08-30 22:29:38 +02:00
|
|
|
try {
|
|
|
|
result = func(stmt(query));
|
|
|
|
}
|
|
|
|
catch (e) {
|
|
|
|
if (e.message.includes("The database connection is not open")) {
|
|
|
|
// this often happens on killing the app which puts these alerts in front of user
|
|
|
|
// in these cases error should be simply ignored.
|
|
|
|
console.log(e.message);
|
|
|
|
|
2023-04-08 19:51:39 +08:00
|
|
|
return null;
|
2020-08-30 22:29:38 +02:00
|
|
|
}
|
2020-12-16 20:58:43 +01:00
|
|
|
|
|
|
|
throw e;
|
2020-08-30 22:29:38 +02:00
|
|
|
}
|
2017-11-21 22:11:27 -05:00
|
|
|
|
2020-06-20 21:42:41 +02:00
|
|
|
const milliseconds = Date.now() - startTimestamp;
|
2019-12-01 12:51:47 +01:00
|
|
|
|
2023-10-20 09:36:57 +02:00
|
|
|
if (milliseconds >= 20 && !cls.isSlowQueryLoggingDisabled()) {
|
2020-06-20 21:42:41 +02:00
|
|
|
if (query.includes("WITH RECURSIVE")) {
|
|
|
|
log.info(`Slow recursive query took ${milliseconds}ms.`);
|
|
|
|
}
|
|
|
|
else {
|
2020-12-11 15:24:44 +01:00
|
|
|
log.info(`Slow query took ${milliseconds}ms: ${query.trim().replace(/\s+/g, " ")}`);
|
2020-06-20 21:42:41 +02:00
|
|
|
}
|
2017-11-01 20:31:44 -04:00
|
|
|
}
|
2017-11-18 18:57:50 -05:00
|
|
|
|
2020-06-20 21:42:41 +02:00
|
|
|
return result;
|
2017-11-01 20:31:44 -04:00
|
|
|
}
|
|
|
|
|
2020-06-20 12:31:38 +02:00
|
|
|
function transactional(func) {
|
2021-03-12 23:48:14 +01:00
|
|
|
try {
|
|
|
|
const ret = dbConnection.transaction(func).deferred();
|
|
|
|
|
|
|
|
if (!dbConnection.inTransaction) { // i.e. transaction was really committed (and not just savepoint released)
|
2023-11-22 19:34:48 +01:00
|
|
|
require('./ws.js').sendTransactionEntityChangesToAllClients();
|
2021-03-12 23:48:14 +01:00
|
|
|
}
|
2017-10-29 18:50:28 -04:00
|
|
|
|
2021-03-12 23:48:14 +01:00
|
|
|
return ret;
|
2020-06-21 13:44:47 +02:00
|
|
|
}
|
2021-03-12 23:48:14 +01:00
|
|
|
catch (e) {
|
2022-06-08 22:25:00 +02:00
|
|
|
const entityChangeIds = cls.getAndClearEntityChangeIds();
|
2021-04-14 22:57:45 +02:00
|
|
|
|
2022-06-08 22:25:00 +02:00
|
|
|
if (entityChangeIds.length > 0) {
|
2021-04-16 23:00:08 +02:00
|
|
|
log.info("Transaction rollback dirtied the becca, forcing reload.");
|
2021-04-14 22:57:45 +02:00
|
|
|
|
2023-11-22 19:34:48 +01:00
|
|
|
require('../becca/becca_loader.js').load();
|
2021-04-14 22:57:45 +02:00
|
|
|
}
|
2017-10-31 00:15:49 -04:00
|
|
|
|
2023-01-24 09:35:00 +01:00
|
|
|
// the maxEntityChangeId has been incremented during failed transaction, need to recalculate
|
2023-11-22 19:34:48 +01:00
|
|
|
require('./entity_changes.js').recalculateMaxEntityChangeId();
|
2023-01-24 09:35:00 +01:00
|
|
|
|
2021-03-12 23:48:14 +01:00
|
|
|
throw e;
|
|
|
|
}
|
2017-10-29 18:50:28 -04:00
|
|
|
}
|
|
|
|
|
2021-03-14 22:54:39 +01:00
|
|
|
function fillParamList(paramIds, truncate = true) {
|
|
|
|
if (paramIds.length === 0) {
|
2020-12-10 21:27:21 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (truncate) {
|
|
|
|
execute("DELETE FROM param_list");
|
|
|
|
}
|
|
|
|
|
2021-03-14 22:54:39 +01:00
|
|
|
paramIds = Array.from(new Set(paramIds));
|
2020-12-10 21:27:21 +01:00
|
|
|
|
2021-03-14 22:54:39 +01:00
|
|
|
if (paramIds.length > 30000) {
|
|
|
|
fillParamList(paramIds.slice(30000), false);
|
2020-12-10 21:27:21 +01:00
|
|
|
|
2021-03-14 22:54:39 +01:00
|
|
|
paramIds = paramIds.slice(0, 30000);
|
2020-12-10 21:27:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// doing it manually to avoid this showing up on the sloq query list
|
2023-04-08 19:51:39 +08:00
|
|
|
const s = stmt(`INSERT INTO param_list VALUES ${paramIds.map(paramId => `(?)`).join(',')}`);
|
2020-12-10 21:27:21 +01:00
|
|
|
|
2021-03-14 22:54:39 +01:00
|
|
|
s.run(paramIds);
|
2020-12-10 21:27:21 +01:00
|
|
|
}
|
|
|
|
|
2022-01-17 23:47:26 +01:00
|
|
|
async function copyDatabase(targetFilePath) {
|
|
|
|
try {
|
|
|
|
fs.unlinkSync(targetFilePath);
|
|
|
|
} catch (e) {
|
|
|
|
} // unlink throws exception if the file did not exist
|
|
|
|
|
|
|
|
await dbConnection.backup(targetFilePath);
|
|
|
|
}
|
|
|
|
|
2023-10-20 09:36:57 +02:00
|
|
|
function disableSlowQueryLogging(cb) {
|
|
|
|
const orig = cls.isSlowQueryLoggingDisabled();
|
|
|
|
|
|
|
|
try {
|
|
|
|
cls.disableSlowQueryLogging(true);
|
|
|
|
|
|
|
|
return cb();
|
|
|
|
}
|
|
|
|
finally {
|
|
|
|
cls.disableSlowQueryLogging(orig);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-14 23:31:44 -04:00
|
|
|
module.exports = {
|
2020-06-20 23:09:34 +02:00
|
|
|
dbConnection,
|
2017-10-14 23:31:44 -04:00
|
|
|
insert,
|
2017-10-29 22:22:30 -04:00
|
|
|
replace,
|
2021-04-07 22:01:52 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get single value from the given query - first column from first returned row.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {string} query - SQL query with ? used as parameter placeholder
|
|
|
|
* @param {object[]} [params] - array of params if needed
|
2023-01-05 23:38:41 +01:00
|
|
|
* @returns [object] - single value
|
2021-04-07 22:01:52 +02:00
|
|
|
*/
|
2018-01-29 17:41:59 -05:00
|
|
|
getValue,
|
2021-04-07 22:01:52 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get first returned row.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {string} query - SQL query with ? used as parameter placeholder
|
|
|
|
* @param {object[]} [params] - array of params if needed
|
2023-01-05 23:38:41 +01:00
|
|
|
* @returns {object} - map of column name to column value
|
2021-04-07 22:01:52 +02:00
|
|
|
*/
|
2018-01-29 17:41:59 -05:00
|
|
|
getRow,
|
|
|
|
getRowOrNull,
|
2021-04-07 22:01:52 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get all returned rows.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {string} query - SQL query with ? used as parameter placeholder
|
|
|
|
* @param {object[]} [params] - array of params if needed
|
2023-01-05 23:38:41 +01:00
|
|
|
* @returns {object[]} - array of all rows, each row is a map of column name to column value
|
2021-04-07 22:01:52 +02:00
|
|
|
*/
|
2018-01-29 17:41:59 -05:00
|
|
|
getRows,
|
2021-07-24 12:04:48 +02:00
|
|
|
getRawRows,
|
2020-06-20 21:42:41 +02:00
|
|
|
iterateRows,
|
2018-05-30 20:28:10 -04:00
|
|
|
getManyRows,
|
2021-04-07 22:01:52 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a map of first column mapping to second column.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {string} query - SQL query with ? used as parameter placeholder
|
|
|
|
* @param {object[]} [params] - array of params if needed
|
2023-01-05 23:38:41 +01:00
|
|
|
* @returns {object} - map of first column to second column
|
2021-04-07 22:01:52 +02:00
|
|
|
*/
|
2017-11-02 22:55:22 -04:00
|
|
|
getMap,
|
2021-04-07 22:01:52 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a first column in an array.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {string} query - SQL query with ? used as parameter placeholder
|
|
|
|
* @param {object[]} [params] - array of params if needed
|
2023-01-05 23:38:41 +01:00
|
|
|
* @returns {object[]} - array of first column of all returned rows
|
2021-04-07 22:01:52 +02:00
|
|
|
*/
|
2018-01-29 17:41:59 -05:00
|
|
|
getColumn,
|
2021-04-07 22:01:52 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Execute SQL
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {string} query - SQL query with ? used as parameter placeholder
|
|
|
|
* @param {object[]} [params] - array of params if needed
|
|
|
|
*/
|
2017-10-14 23:31:44 -04:00
|
|
|
execute,
|
2019-11-01 22:09:51 +01:00
|
|
|
executeMany,
|
2017-10-15 17:31:49 -04:00
|
|
|
executeScript,
|
2019-03-25 22:06:17 +01:00
|
|
|
transactional,
|
2020-12-10 21:27:21 +01:00
|
|
|
upsert,
|
2022-01-17 23:47:26 +01:00
|
|
|
fillParamList,
|
2023-10-20 09:36:57 +02:00
|
|
|
copyDatabase,
|
|
|
|
disableSlowQueryLogging
|
2020-05-12 13:40:42 +02:00
|
|
|
};
|