Notes/src/services/sql.ts

409 lines
11 KiB
TypeScript
Raw Normal View History

2017-10-21 21:10:33 -04:00
"use strict";
2021-04-07 22:01:52 +02:00
/**
* @module sql
*/
import log from "./log.js";
import type { Statement, Database as DatabaseType, RunResult } from "better-sqlite3";
import dataDir from "./data_dir.js";
import cls from "./cls.js";
import fs from "fs-extra";
import Database from "better-sqlite3";
import ws from "./ws.js";
import becca_loader from "../becca/becca_loader.js";
import entity_changes from "./entity_changes.js";
let dbConnection: DatabaseType = buildDatabase();
let statementCache: Record<string, Statement> = {};
function buildDatabase() {
if (process.env.TRILIUM_INTEGRATION_TEST === "memory") {
return buildIntegrationTestDatabase();
}
return new Database(dataDir.DOCUMENT_PATH);
}
function buildIntegrationTestDatabase(dbPath?: string) {
const dbBuffer = fs.readFileSync(dbPath ?? dataDir.DOCUMENT_PATH);
return new Database(dbBuffer);
}
function rebuildIntegrationTestDatabase(dbPath?: string) {
if (dbConnection) {
dbConnection.close();
}
// This allows a database that is read normally but is kept in memory and discards all modifications.
dbConnection = buildIntegrationTestDatabase(dbPath);
statementCache = {};
}
if (!process.env.TRILIUM_INTEGRATION_TEST) {
2025-01-09 18:07:02 +02:00
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;
2024-02-16 22:44:12 +02:00
type Params = any;
2025-01-09 18:07:02 +02:00
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach((eventType) => {
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();
}
});
});
2024-02-16 22:44:12 +02:00
function insert<T extends {}>(tableName: string, rec: T, 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) {
log.error(`Can't insert empty object into table ${tableName}`);
2017-10-25 22:39:21 -04:00
return;
}
const columns = keys.join(", ");
2025-01-09 18:07:02 +02:00
const questionMarks = keys.map((p) => "?").join(", ");
2017-10-25 22:39:21 -04:00
const query = `INSERT
${replace ? "OR REPLACE" : ""} INTO
${tableName}
(
${columns}
)
VALUES (${questionMarks})`;
2020-06-20 12:31:38 +02:00
const res = execute(query, Object.values(rec));
return res ? res.lastInsertRowid : null;
}
function replace<T extends {}>(tableName: string, rec: T): number | null {
return insert(tableName, rec, true) as number | null;
}
2024-02-16 22:44:12 +02:00
function upsert<T extends {}>(tableName: string, primaryKey: string, rec: T) {
2023-09-19 23:48:55 +02:00
const keys = Object.keys(rec || {});
if (keys.length === 0) {
log.error(`Can't upsert empty object into table ${tableName}`);
return;
}
const columns = keys.join(", ");
2025-01-09 18:07:02 +02:00
const questionMarks = keys.map((colName) => `@${colName}`).join(", ");
2025-01-09 18:07:02 +02:00
const updateMarks = keys.map((colName) => `${colName} = @${colName}`).join(", ");
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) {
2024-02-16 22:44:12 +02:00
(rec as any)[idx] = rec[idx] ? 1 : 0;
2020-06-17 23:03:46 +02:00
}
}
2020-06-20 12:31:38 +02:00
execute(query, rec);
2017-10-29 22:22:30 -04:00
}
2024-02-16 22:44:12 +02:00
function stmt(sql: string) {
2020-06-17 23:03:46 +02:00
if (!(sql in statementCache)) {
statementCache[sql] = dbConnection.prepare(sql);
}
return statementCache[sql];
}
2024-02-16 22:44:12 +02:00
function getRow<T>(query: string, params: Params = []): T {
2025-01-09 18:07:02 +02:00
return wrap(query, (s) => s.get(params)) as T;
}
2024-02-16 22:44:12 +02:00
function getRowOrNull<T>(query: string, params: Params = []): T | null {
2020-06-20 12:31:38 +02:00
const all = getRows(query, params);
2024-02-16 22:44:12 +02:00
if (!all) {
return null;
}
2025-01-09 18:07:02 +02:00
return (all.length > 0 ? all[0] : null) as T | null;
2017-10-29 22:22:30 -04:00
}
function getValue<T>(query: string, params: Params = []): T {
2025-01-09 18:07:02 +02:00
return wrap(query, (s) => s.pluck().get(params)) as T;
}
// smaller values can result in better performance due to better usage of statement cache
const PARAM_LIMIT = 100;
function getManyRows<T>(query: string, params: Params): T[] {
2024-02-16 22:44:12 +02:00
let results: unknown[] = [];
while (params.length > 0) {
const curParams = params.slice(0, Math.min(params.length, PARAM_LIMIT));
params = params.slice(curParams.length);
2024-02-16 22:44:12 +02:00
const curParamsObj: Record<string, any> = {};
2020-06-17 23:03:46 +02:00
let j = 1;
for (const param of curParams) {
2025-01-09 18:07:02 +02:00
curParamsObj["param" + j++] = param;
2020-06-17 23:03:46 +02:00
}
let i = 1;
2020-06-17 23:03:46 +02:00
const questionMarks = curParams.map(() => ":param" + i++).join(",");
const curQuery = query.replace(/\?\?\?/g, questionMarks);
2025-01-09 18:07:02 +02:00
const statement = curParams.length === PARAM_LIMIT ? stmt(curQuery) : dbConnection.prepare(curQuery);
const subResults = statement.all(curParamsObj);
results = results.concat(subResults);
}
2025-01-09 18:07:02 +02:00
return (results as T[] | null) || [];
}
function getRows<T>(query: string, params: Params = []): T[] {
2025-01-09 18:07:02 +02:00
return wrap(query, (s) => s.all(params)) as T[];
2020-06-20 21:42:41 +02:00
}
2024-02-17 20:45:31 +02:00
function getRawRows<T extends {} | unknown[]>(query: string, params: Params = []): T[] {
2025-01-09 18:07:02 +02:00
return (wrap(query, (s) => s.raw().all(params)) as T[]) || [];
}
function iterateRows<T>(query: string, params: Params = []): IterableIterator<T> {
2021-12-29 23:19:05 +01:00
if (LOG_ALL_QUERIES) {
console.log(query);
}
return stmt(query).iterate(params) as IterableIterator<T>;
}
2024-02-16 22:44:12 +02:00
function getMap<K extends string | number | symbol, V>(query: string, params: Params = []) {
const map: Record<K, V> = {} as Record<K, V>;
2024-02-17 18:55:41 +02:00
const results = getRawRows<[K, V]>(query, params);
2017-11-02 22:55:22 -04:00
2024-02-16 22:44:12 +02:00
for (const row of results || []) {
2024-02-17 18:55:41 +02:00
map[row[0]] = row[1];
2017-11-02 22:55:22 -04:00
}
return map;
}
function getColumn<T>(query: string, params: Params = []): T[] {
2025-01-09 18:07:02 +02:00
return wrap(query, (s) => s.pluck().all(params)) as T[];
2017-10-24 23:14:26 -04:00
}
function execute(query: string, params: Params = []): RunResult {
2025-01-09 18:07:02 +02:00
return wrap(query, (s) => s.run(params)) as RunResult;
}
2024-02-16 22:44:12 +02:00
function executeMany(query: string, params: 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);
2024-02-16 22:44:12 +02:00
const curParamsObj: Record<string, any> = {};
2020-07-09 23:59:27 +02:00
let j = 1;
for (const param of curParams) {
2025-01-09 18:07:02 +02:00
curParamsObj["param" + j++] = param;
2020-07-09 23:59:27 +02:00
}
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
}
function executeScript(query: string): DatabaseType {
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
}
function wrap(query: string, func: (statement: Statement) => unknown): unknown {
2020-06-20 21:42:41 +02:00
const startTimestamp = Date.now();
let result;
2021-12-29 23:19:05 +01:00
if (LOG_ALL_QUERIES) {
console.log(query);
}
try {
result = func(stmt(query));
2025-01-09 18:07:02 +02:00
} catch (e: any) {
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-12-16 20:58:43 +01:00
throw e;
}
2017-11-21 22:11:27 -05:00
2020-06-20 21:42:41 +02:00
const milliseconds = Date.now() - startTimestamp;
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.`);
2025-01-09 18:07:02 +02:00
} 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
}
}
2020-06-20 21:42:41 +02:00
return result;
}
function transactional<T>(func: (statement: Statement) => T) {
try {
2024-02-16 22:44:12 +02:00
const ret = (dbConnection.transaction(func) as any).deferred();
2025-01-09 18:07:02 +02:00
if (!dbConnection.inTransaction) {
// i.e. transaction was really committed (and not just savepoint released)
ws.sendTransactionEntityChangesToAllClients();
}
return ret;
2025-01-09 18:07:02 +02:00
} catch (e) {
2022-06-08 22:25:00 +02:00
const entityChangeIds = cls.getAndClearEntityChangeIds();
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.");
becca_loader.load();
}
// the maxEntityChangeId has been incremented during failed transaction, need to recalculate
entity_changes.recalculateMaxEntityChangeId();
throw e;
}
}
2024-04-06 21:55:27 +03:00
function fillParamList(paramIds: string[] | Set<string>, truncate = true) {
if ("length" in paramIds && paramIds.length === 0) {
return;
}
if (truncate) {
execute("DELETE FROM param_list");
}
2021-03-14 22:54:39 +01:00
paramIds = Array.from(new Set(paramIds));
2021-03-14 22:54:39 +01:00
if (paramIds.length > 30000) {
fillParamList(paramIds.slice(30000), false);
2021-03-14 22:54:39 +01:00
paramIds = paramIds.slice(0, 30000);
}
// doing it manually to avoid this showing up on the slow query list
2025-01-09 18:07:02 +02:00
const s = stmt(`INSERT INTO param_list VALUES ${paramIds.map((paramId) => `(?)`).join(",")}`);
2021-03-14 22:54:39 +01:00
s.run(paramIds);
}
2024-02-16 22:44:12 +02:00
async function copyDatabase(targetFilePath: string) {
try {
fs.unlinkSync(targetFilePath);
2025-01-09 18:07:02 +02:00
} catch (e) {} // unlink throws exception if the file did not exist
await dbConnection.backup(targetFilePath);
}
2024-02-16 22:44:12 +02:00
function disableSlowQueryLogging<T>(cb: () => T) {
2023-10-20 09:36:57 +02:00
const orig = cls.isSlowQueryLoggingDisabled();
try {
cls.disableSlowQueryLogging(true);
return cb();
2025-01-09 18:07:02 +02:00
} finally {
2023-10-20 09:36:57 +02:00
cls.disableSlowQueryLogging(orig);
}
}
export default {
dbConnection,
insert,
2017-10-29 22:22:30 -04:00
replace,
2021-04-07 22:01:52 +02:00
/**
2025-01-09 18:07:02 +02:00
* Get single value from the given query - first column from first returned row.
*
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
* @returns single value
*/
getValue,
2021-04-07 22:01:52 +02:00
/**
2025-01-09 18:07:02 +02:00
* Get first returned row.
*
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
* @returns - map of column name to column value
*/
getRow,
getRowOrNull,
2021-04-07 22:01:52 +02:00
/**
2025-01-09 18:07:02 +02:00
* Get all returned rows.
*
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
* @returns - array of all rows, each row is a map of column name to column value
*/
getRows,
getRawRows,
2020-06-20 21:42:41 +02:00
iterateRows,
getManyRows,
2021-04-07 22:01:52 +02:00
/**
2025-01-09 18:07:02 +02:00
* Get a map of first column mapping to second column.
*
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
* @returns - map of first column to second column
*/
2017-11-02 22:55:22 -04:00
getMap,
2021-04-07 22:01:52 +02:00
/**
2025-01-09 18:07:02 +02:00
* Get a first column in an array.
*
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
* @returns array of first column of all returned rows
*/
getColumn,
2021-04-07 22:01:52 +02:00
/**
2025-01-09 18:07:02 +02:00
* Execute SQL
*
* @param query - SQL query with ? used as parameter placeholder
* @param params - array of params if needed
*/
execute,
2019-11-01 22:09:51 +01:00
executeMany,
2017-10-15 17:31:49 -04:00
executeScript,
transactional,
upsert,
fillParamList,
2023-10-20 09:36:57 +02:00
copyDatabase,
disableSlowQueryLogging,
rebuildIntegrationTestDatabase
};