feat(db): Bun.SQL connection and portable migrations
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f71a31a5f1
commit
738d813fb8
8 files changed
+193
-2
No files matched your search
@@ -0,0 +1,21 @@
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
export type Db = { sql: Bun.SQL; dialect: "sqlite" | "postgres"; close(): Promise<void> };
|
||||
|
||||
export async function connect(url: string): Promise<Db> {
|
||||
if (url.startsWith("sqlite:")) {
|
||||
const path = url.replace(/^sqlite:(\/\/)?/, "");
|
||||
if (path !== ":memory:" && path !== "") mkdirSync(dirname(path), { recursive: true });
|
||||
// A single connection keeps in-memory databases coherent and serialises writers.
|
||||
const sql = new Bun.SQL(url, { max: 1 });
|
||||
await sql.unsafe("PRAGMA foreign_keys = ON");
|
||||
if (path !== ":memory:") await sql.unsafe("PRAGMA journal_mode = WAL");
|
||||
return { sql, dialect: "sqlite", close: () => sql.close() };
|
||||
}
|
||||
if (url.startsWith("postgres:") || url.startsWith("postgresql:")) {
|
||||
const sql = new Bun.SQL(url);
|
||||
return { sql, dialect: "postgres", close: () => sql.close() };
|
||||
}
|
||||
throw new Error(`Unsupported database URL (expected sqlite:// or postgres://): ${url}`);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { connect, type Db } from "./connect";
|
||||
export { migrate } from "./migrate";
|
||||
@@ -0,0 +1,27 @@
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { Db } from "./connect";
|
||||
|
||||
const MIGRATIONS_DIR = join(import.meta.dir, "..", "migrations");
|
||||
|
||||
function statements(text: string): string[] {
|
||||
const noComments = text.split("\n").filter((l) => !l.trim().startsWith("--")).join("\n");
|
||||
return noComments.split(";").map((s) => s.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
export async function migrate(db: Db): Promise<string[]> {
|
||||
await db.sql.unsafe("CREATE TABLE IF NOT EXISTS schema_migrations (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL)");
|
||||
const done = new Set((await db.sql`SELECT name FROM schema_migrations`).map((r: { name: string }) => r.name));
|
||||
const files = readdirSync(MIGRATIONS_DIR).filter((f) => f.endsWith(".sql")).sort();
|
||||
const applied: string[] = [];
|
||||
for (const name of files) {
|
||||
if (done.has(name)) continue;
|
||||
const stmts = statements(readFileSync(join(MIGRATIONS_DIR, name), "utf8"));
|
||||
await db.sql.begin(async (tx) => {
|
||||
for (const s of stmts) await tx.unsafe(s);
|
||||
await tx`INSERT INTO schema_migrations (name, applied_at) VALUES (${name}, ${new Date().toISOString()})`;
|
||||
});
|
||||
applied.push(name);
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
Reference in new issue
Block a user