import { readdirSync, readFileSync } from 'node:fs'; import { DatabaseSync } from 'node:sqlite'; const MIGRATIONS = new URL('../migrations/', import.meta.url); // enough of d1 over node's sqlite to run the real migration and queries export class TestD1 { sqlite = new DatabaseSync(':memory:'); writes = 0; // `stopAt` leaves the later migrations out, to test one against older data constructor(stopAt?: string) { for (const name of readdirSync(MIGRATIONS).sort()) { if (name === stopAt) break; this.migrate(name); } } migrate(name: string): void { this.sqlite.exec(readFileSync(new URL(name, MIGRATIONS), 'utf8')); } prepare(sql: string): TestStatement { return new TestStatement(this, sql, []); } async batch(statements: TestStatement[]): Promise { this.sqlite.exec('BEGIN'); try { const out = statements.map((statement) => statement.execute()); this.sqlite.exec('COMMIT'); return out; } catch (err) { this.sqlite.exec('ROLLBACK'); throw err; } } query(sql: string, ...params: unknown[]): T[] { return this.sqlite.prepare(sql).all(...params) as T[]; } asD1(): D1Database { return this as unknown as D1Database; } } class TestStatement { constructor( private db: TestD1, private sql: string, private params: unknown[], ) {} bind(...params: unknown[]): TestStatement { return new TestStatement(this.db, this.sql, params); } execute(): { results: Record[] } { const statement = this.db.sqlite.prepare(this.sql); if (statement.columns().length > 0) return { results: statement.all(...this.params) }; this.db.writes += Number(statement.run(...this.params).changes); return { results: [] }; } async all(): Promise<{ results: Record[] }> { return this.execute(); } async first(): Promise | null> { return this.execute().results[0] ?? null; } async run(): Promise<{ results: Record[] }> { return this.execute(); } }