mirror of
https://github.com/djhackersdev/minime.git
synced 2026-08-21 09:54:01 -05:00
Migrate to SQLite3
We'll keep the door open for side-by-side support of Postgres in the background, but due to SQLite's type system quirks we cannot use the same DDL for both databases, so we would have to maintain two sets of DDL (schema init and schema migration scripts) at once. Interested future contributors can shoulder this maintenance burden if they so choose.
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
export * from "./api";
|
||||
export * from "./pg";
|
||||
export * from "./sqlite";
|
||||
export * from "./util";
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Pool, PoolClient } from "pg";
|
||||
import * as sql from "sql-bricks-postgres";
|
||||
|
||||
import { DataSource, Row, Transaction } from "./api";
|
||||
|
||||
class PgTransaction implements Transaction {
|
||||
constructor(private readonly _conn: PoolClient) {}
|
||||
|
||||
async modify(stmt: sql.Statement): Promise<void> {
|
||||
await this._conn.query(stmt.toParams());
|
||||
}
|
||||
|
||||
async fetchRow(stmt: sql.SelectStatement): Promise<Row | undefined> {
|
||||
const { rows } = await this._conn.query(stmt.toParams());
|
||||
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async fetchRows(stmt: sql.SelectStatement): Promise<Row[]> {
|
||||
const { rows } = await this._conn.query(stmt.toParams());
|
||||
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
class PgDataSource implements DataSource {
|
||||
private readonly _pool: Pool;
|
||||
|
||||
constructor() {
|
||||
this._pool = new Pool();
|
||||
}
|
||||
|
||||
async transaction<T>(
|
||||
callback: (txn: Transaction) => Promise<T>
|
||||
): Promise<T> {
|
||||
const conn = await this._pool.connect();
|
||||
|
||||
await conn.query("begin");
|
||||
|
||||
try {
|
||||
const txn = new PgTransaction(conn);
|
||||
const result = await callback(txn);
|
||||
|
||||
await conn.query("commit");
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
await conn.query("rollback");
|
||||
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function openDataSource(): DataSource {
|
||||
return new PgDataSource();
|
||||
}
|
||||
104
src/sql/sqlite.ts
Normal file
104
src/sql/sqlite.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import Database from "better-sqlite3";
|
||||
import * as sql from "sql-bricks-postgres";
|
||||
|
||||
import { DataSource, Row, Transaction } from "./api";
|
||||
|
||||
// bless me father for i have sinned
|
||||
const fuFixup = new RegExp(" FOR UPDATE$");
|
||||
|
||||
function _preprocess(stmt: sql.Statement) {
|
||||
const params = stmt.toParams({ placeholder: "?" });
|
||||
const values = new Array<string | null>();
|
||||
|
||||
for (const value of params.values) {
|
||||
// Pass null through as-is
|
||||
// Pass dates as ISO strings in UTC
|
||||
// Pass everything else (numbers, booleans, BigInts) as their string rep.
|
||||
|
||||
if (value === null) {
|
||||
values.push(null);
|
||||
} else if (value instanceof Date) {
|
||||
values.push(value.toISOString());
|
||||
} else {
|
||||
values.push(value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Use string manipulation to cut off any trailing " FOR UPDATE" clause
|
||||
// in a SELECT statement, since SQLite doesn't support it. I really hope
|
||||
// that performing string manipulation on SQL code like this doesn't come
|
||||
// back to bite me.
|
||||
|
||||
return {
|
||||
text: params.text.replace(fuFixup, ""),
|
||||
values,
|
||||
};
|
||||
}
|
||||
|
||||
function _postprocess(obj: {}): Row {
|
||||
const result = {};
|
||||
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
result[k] = v.toString();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
class SqliteTransaction implements Transaction {
|
||||
constructor(private readonly _db: Database.Database) {}
|
||||
|
||||
modify(stmt: sql.Statement): Promise<void> {
|
||||
const params = _preprocess(stmt);
|
||||
|
||||
this._db.prepare(params.text).run(...params.values);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
fetchRow(stmt: sql.SelectStatement): Promise<Row | undefined> {
|
||||
const params = _preprocess(stmt);
|
||||
const raw = this._db.prepare(params.text).get(...params.values);
|
||||
const result = raw && _postprocess(raw);
|
||||
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
fetchRows(stmt: sql.SelectStatement): Promise<Row[]> {
|
||||
const params = _preprocess(stmt);
|
||||
const raw = this._db.prepare(params.text).all(...params.values);
|
||||
const result = raw.map(_postprocess);
|
||||
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
}
|
||||
|
||||
class SqliteDataSource implements DataSource {
|
||||
constructor(private readonly _path: string) {}
|
||||
|
||||
async transaction<T>(
|
||||
callback: (txn: Transaction) => Promise<T>
|
||||
): Promise<T> {
|
||||
const db = new Database(this._path);
|
||||
|
||||
db.defaultSafeIntegers();
|
||||
db.prepare("begin").run();
|
||||
|
||||
try {
|
||||
const txn = new SqliteTransaction(db);
|
||||
const result = await callback(txn);
|
||||
|
||||
db.prepare("commit").run();
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
db.prepare("rollback").run();
|
||||
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function openSqlite(path: string): DataSource {
|
||||
return new SqliteDataSource(path);
|
||||
}
|
||||
Reference in New Issue
Block a user