| 1 | import type { Database } from "bun:sqlite"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import { 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 | */ |
| 11 | export 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). */ |
| 42 | export 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); |