mirror of
https://github.com/zadam/trilium.git
synced 2026-02-27 17:13:38 +01:00
refactor(server): decouple bettersqlite3 from sql service
This commit is contained in:
parent
00e7482968
commit
a67464b4a0
@ -1,44 +1,65 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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";
|
import fs from "fs";
|
||||||
import Database from "better-sqlite3";
|
|
||||||
import ws from "./ws.js";
|
|
||||||
import becca_loader from "../becca/becca_loader.js";
|
import becca_loader from "../becca/becca_loader.js";
|
||||||
import entity_changes from "./entity_changes.js";
|
import cls from "./cls.js";
|
||||||
import config from "./config.js";
|
import config from "./config.js";
|
||||||
|
import dataDir from "./data_dir.js";
|
||||||
|
import entity_changes from "./entity_changes.js";
|
||||||
|
import log from "./log.js";
|
||||||
|
import BetterSqlite3Provider from "./sql_nodejs.js";
|
||||||
|
import ws from "./ws.js";
|
||||||
|
|
||||||
const dbOpts: Database.Options = {
|
type Params = any;
|
||||||
nativeBinding: process.env.BETTERSQLITE3_NATIVE_PATH || undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
let dbConnection: DatabaseType = buildDatabase();
|
export interface Statement {
|
||||||
|
run(...params: Params): RunResult;
|
||||||
|
get(params: Params): unknown;
|
||||||
|
all(...params: Params): unknown[];
|
||||||
|
iterate(...params: Params): IterableIterator<unknown>;
|
||||||
|
raw(toggleState?: boolean): this;
|
||||||
|
pluck(toggleState?: boolean): this;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Transaction {
|
||||||
|
deferred(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunResult {
|
||||||
|
changes: number;
|
||||||
|
lastInsertRowid: number | bigint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DatabaseProvider {
|
||||||
|
loadFromFile(path: string, isReadOnly: boolean): void;
|
||||||
|
loadFromMemory(): void;
|
||||||
|
loadFromBuffer(buffer: NonSharedBuffer): void;
|
||||||
|
backup(destinationFile: string): void;
|
||||||
|
prepare(query: string): Statement;
|
||||||
|
transaction<T>(func: (statement: Statement) => T): Transaction;
|
||||||
|
get inTransaction(): boolean;
|
||||||
|
exec(query: string): void;
|
||||||
|
close(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbConnection: DatabaseProvider = new BetterSqlite3Provider();
|
||||||
let statementCache: Record<string, Statement> = {};
|
let statementCache: Record<string, Statement> = {};
|
||||||
|
|
||||||
function buildDatabase() {
|
function buildDatabase() {
|
||||||
// for integration tests, ignore the config's readOnly setting
|
// for integration tests, ignore the config's readOnly setting
|
||||||
if (process.env.TRILIUM_INTEGRATION_TEST === "memory") {
|
if (process.env.TRILIUM_INTEGRATION_TEST === "memory") {
|
||||||
return buildIntegrationTestDatabase();
|
buildIntegrationTestDatabase();
|
||||||
} else if (process.env.TRILIUM_INTEGRATION_TEST === "memory-no-store") {
|
} else if (process.env.TRILIUM_INTEGRATION_TEST === "memory-no-store") {
|
||||||
return new Database(":memory:", dbOpts);
|
dbConnection.loadFromMemory();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Database(dataDir.DOCUMENT_PATH, {
|
dbConnection.loadFromFile(dataDir.DOCUMENT_PATH, config.General.readOnly);
|
||||||
...dbOpts,
|
|
||||||
readonly: config.General.readOnly
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildDatabase();
|
||||||
|
|
||||||
function buildIntegrationTestDatabase(dbPath?: string) {
|
function buildIntegrationTestDatabase(dbPath?: string) {
|
||||||
const dbBuffer = fs.readFileSync(dbPath ?? dataDir.DOCUMENT_PATH);
|
const buffer = fs.readFileSync(dbPath ?? dataDir.DOCUMENT_PATH);
|
||||||
return new Database(dbBuffer, dbOpts);
|
dbConnection.loadFromBuffer(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
function rebuildIntegrationTestDatabase(dbPath?: string) {
|
function rebuildIntegrationTestDatabase(dbPath?: string) {
|
||||||
@ -47,28 +68,12 @@ function rebuildIntegrationTestDatabase(dbPath?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This allows a database that is read normally but is kept in memory and discards all modifications.
|
// This allows a database that is read normally but is kept in memory and discards all modifications.
|
||||||
dbConnection = buildIntegrationTestDatabase(dbPath);
|
buildIntegrationTestDatabase(dbPath);
|
||||||
statementCache = {};
|
statementCache = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!process.env.TRILIUM_INTEGRATION_TEST) {
|
|
||||||
dbConnection.pragma("journal_mode = WAL");
|
|
||||||
}
|
|
||||||
|
|
||||||
const LOG_ALL_QUERIES = false;
|
const LOG_ALL_QUERIES = false;
|
||||||
|
|
||||||
type Params = any;
|
|
||||||
|
|
||||||
[`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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function insert<T extends {}>(tableName: string, rec: T, replace = false) {
|
function insert<T extends {}>(tableName: string, rec: T, replace = false) {
|
||||||
const keys = Object.keys(rec || {});
|
const keys = Object.keys(rec || {});
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
@ -129,7 +134,7 @@ function upsert<T extends {}>(tableName: string, primaryKey: string, rec: T) {
|
|||||||
* @returns the corresponding {@link Statement}.
|
* @returns the corresponding {@link Statement}.
|
||||||
*/
|
*/
|
||||||
function stmt(sql: string, isRaw?: boolean) {
|
function stmt(sql: string, isRaw?: boolean) {
|
||||||
const key = (isRaw ? "raw/" + sql : sql);
|
const key = (isRaw ? `raw/${sql}` : sql);
|
||||||
|
|
||||||
if (!(key in statementCache)) {
|
if (!(key in statementCache)) {
|
||||||
statementCache[key] = dbConnection.prepare(sql);
|
statementCache[key] = dbConnection.prepare(sql);
|
||||||
@ -169,11 +174,11 @@ function getManyRows<T>(query: string, params: Params): T[] {
|
|||||||
|
|
||||||
let j = 1;
|
let j = 1;
|
||||||
for (const param of curParams) {
|
for (const param of curParams) {
|
||||||
curParamsObj["param" + j++] = param;
|
curParamsObj[`param${j++}`] = param;
|
||||||
}
|
}
|
||||||
|
|
||||||
let i = 1;
|
let i = 1;
|
||||||
const questionMarks = curParams.map(() => ":param" + i++).join(",");
|
const questionMarks = curParams.map(() => `:param${i++}`).join(",");
|
||||||
const curQuery = query.replace(/\?\?\?/g, questionMarks);
|
const curQuery = query.replace(/\?\?\?/g, questionMarks);
|
||||||
|
|
||||||
const statement = curParams.length === PARAM_LIMIT ? stmt(curQuery) : dbConnection.prepare(curQuery);
|
const statement = curParams.length === PARAM_LIMIT ? stmt(curQuery) : dbConnection.prepare(curQuery);
|
||||||
@ -240,23 +245,23 @@ function executeMany(query: string, params: Params) {
|
|||||||
|
|
||||||
let j = 1;
|
let j = 1;
|
||||||
for (const param of curParams) {
|
for (const param of curParams) {
|
||||||
curParamsObj["param" + j++] = param;
|
curParamsObj[`param${j++}`] = param;
|
||||||
}
|
}
|
||||||
|
|
||||||
let i = 1;
|
let i = 1;
|
||||||
const questionMarks = curParams.map(() => ":param" + i++).join(",");
|
const questionMarks = curParams.map(() => `:param${i++}`).join(",");
|
||||||
const curQuery = query.replace(/\?\?\?/g, questionMarks);
|
const curQuery = query.replace(/\?\?\?/g, questionMarks);
|
||||||
|
|
||||||
dbConnection.prepare(curQuery).run(curParamsObj);
|
dbConnection.prepare(curQuery).run(curParamsObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeScript(query: string): DatabaseType {
|
function executeScript(query: string) {
|
||||||
if (LOG_ALL_QUERIES) {
|
if (LOG_ALL_QUERIES) {
|
||||||
console.log(query);
|
console.log(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
return dbConnection.exec(query);
|
dbConnection.exec(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -350,7 +355,7 @@ function fillParamList(paramIds: string[] | Set<string>, truncate = true) {
|
|||||||
async function copyDatabase(targetFilePath: string) {
|
async function copyDatabase(targetFilePath: string) {
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(targetFilePath);
|
fs.unlinkSync(targetFilePath);
|
||||||
} catch (e) {} // unlink throws exception if the file did not exist
|
} catch (e) { } // unlink throws exception if the file did not exist
|
||||||
|
|
||||||
await dbConnection.backup(targetFilePath);
|
await dbConnection.backup(targetFilePath);
|
||||||
}
|
}
|
||||||
|
|||||||
71
apps/server/src/services/sql_nodejs.ts
Normal file
71
apps/server/src/services/sql_nodejs.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import Database, { type Database as DatabaseType } from "better-sqlite3";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
|
||||||
|
import dataDirs from "./data_dir";
|
||||||
|
import type { DatabaseProvider, Statement, Transaction } from "./sql";
|
||||||
|
|
||||||
|
const dbOpts: Database.Options = {
|
||||||
|
nativeBinding: process.env.BETTERSQLITE3_NATIVE_PATH || undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class BetterSqlite3Provider implements DatabaseProvider {
|
||||||
|
|
||||||
|
private dbConnection?: DatabaseType;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach((eventType) => {
|
||||||
|
// closing connection is especially important to fold -wal file into the main DB file
|
||||||
|
// (see https://sqlite.org/tempfiles.html for details)
|
||||||
|
process.on(eventType, () => this.close());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFromFile(path: string, isReadOnly: boolean) {
|
||||||
|
this.dbConnection = new Database(path, {
|
||||||
|
...dbOpts,
|
||||||
|
readonly: isReadOnly
|
||||||
|
});
|
||||||
|
this.dbConnection.pragma("journal_mode = WAL");
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFromMemory() {
|
||||||
|
this.dbConnection = new Database(":memory:", dbOpts);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFromBuffer(buffer: NonSharedBuffer) {
|
||||||
|
this.dbConnection = new Database(buffer, dbOpts);
|
||||||
|
}
|
||||||
|
|
||||||
|
backup(destinationFile: string) {
|
||||||
|
this.dbConnection?.backup(destinationFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare(query: string): Statement {
|
||||||
|
if (!this.dbConnection) throw new Error("DB not open.");
|
||||||
|
return this.dbConnection.prepare(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction<T>(func: (statement: Statement) => T): Transaction {
|
||||||
|
if (!this.dbConnection) throw new Error("DB not open.");
|
||||||
|
return this.dbConnection.transaction(func);
|
||||||
|
}
|
||||||
|
|
||||||
|
get inTransaction() {
|
||||||
|
if (!this.dbConnection) throw new Error("DB not open.");
|
||||||
|
return this.dbConnection.inTransaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
exec(query: string): void {
|
||||||
|
this.dbConnection?.exec(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.dbConnection?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildIntegrationTestDatabase(dbPath?: string) {
|
||||||
|
const dbBuffer = readFileSync(dbPath ?? dataDirs.DOCUMENT_PATH);
|
||||||
|
return new Database(dbBuffer, dbOpts);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user