1import type { Database } from "bun:sqlite";
2import * as Effect from "effect/Effect";
3import { MigrationError, type SqlExecutor } from "@/SQL/Migrations/index.ts";
4
5/**
6 * A {@link SqlExecutor} over an in-memory `bun:sqlite` database — the unit
7 * test stand-in for the D1 HTTP executor / local workerd tunnel. Batches run
8 * in a real transaction, so mid-batch failures roll back like D1's batched
9 * query does.
10 */
11export const makeSqliteExecutor = (db: Database): SqlExecutor => ({
12 dialect: "sqlite",
13 query: (sql, params) =>
14 Effect.try({
15 try: () =>
16 db.query(sql).all(...((params ?? []) as never[])) as Array<
17 Record<string, unknown>
18 >,
19 catch: (cause) => new MigrationError({ message: String(cause), cause }),
20 }),
21 batch: (statements) =>
22 Effect.try({
23 try: () => {
24 db.run("BEGIN");
25 try {
26 for (const statement of statements) db.run(statement);
27 db.run("COMMIT");
28 } catch (error) {
29 try {
30 db.run("ROLLBACK");
31 } catch {
32 // already rolled back
33 }
34 throw error;
35 }
36 },
37 catch: (cause) => new MigrationError({ message: String(cause), cause }),
38 }),
39});
40
41/** List user table names (excluding sqlite internals). */
42export const tableNames = (db: Database): string[] =>
43 (
44 db
45 .query(
46 "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name;",
47 )
48 .all() as Array<{ name: string }>
49 ).map((row) => row.name);