96 lines · 2.9 KB · TypeScript
RawDownload
import * as Effect from "effect/Effect";import * as Migrator from "effect/unstable/sql/Migrator";import * as SqlClient from "effect/unstable/sql/SqlClient";import { PersistenceInitializationError } from "./errors";import { MIGRATION_V1_SQL } from "./migrations";import type { EffectSqliteDatabaseAdapter, SqliteDatabase } from "./sqliteAdapter"; export const STATE_DB_SCHEMA_VERSION = 1; export function applyStateDbPragmas(db: SqliteDatabase): void {  db.exec("PRAGMA journal_mode=WAL;");  db.exec("PRAGMA synchronous=FULL;");  db.exec("PRAGMA busy_timeout=5000;");  db.exec("PRAGMA foreign_keys=ON;");} function normalizeStatementBatch(sql: string): string[] {  const normalized = sql.replace(/\r\n/g, "\n");  const statements: string[] = [];  let current = "";  let inSingleQuote = false;  let inDoubleQuote = false;  let inBacktick = false;   for (const char of normalized) {    if (char === "'" && !inDoubleQuote && !inBacktick) {      inSingleQuote = !inSingleQuote;      current += char;      continue;    }    if (char === '"' && !inSingleQuote && !inBacktick) {      inDoubleQuote = !inDoubleQuote;      current += char;      continue;    }    if (char === "`" && !inSingleQuote && !inDoubleQuote) {      inBacktick = !inBacktick;      current += char;      continue;    }    if (char === ";" && !inSingleQuote && !inDoubleQuote && !inBacktick) {      const statement = current.trim();      if (statement.length > 0) {        statements.push(statement);      }      current = "";      continue;    }    current += char;  }   const trailing = current.trim();  if (trailing.length > 0) {    statements.push(trailing);  }  return statements;} function isEffectSqliteDatabase(db: SqliteDatabase): db is EffectSqliteDatabaseAdapter {  return "runWithSqlClient" in db && typeof db.runWithSqlClient === "function";} function runEffectMigrations(db: EffectSqliteDatabaseAdapter): void {  const migrationV1Effect = Effect.gen(function*() {    const sql = yield* SqlClient.SqlClient;    for (const statement of normalizeStatementBatch(MIGRATION_V1_SQL)) {      yield* sql.unsafe(statement).raw;    }    yield* sql.unsafe(`PRAGMA user_version=${STATE_DB_SCHEMA_VERSION};`).raw;  });   const migrationLoader = Migrator.fromRecord({    "0001_initial_schema": migrationV1Effect,  });  const runMigrations = Migrator.make({});   db.runWithSqlClient(    runMigrations({ loader: migrationLoader }).pipe(      Effect.mapError((error) => {        const message =          error instanceof Error            ? error.message            : "Failed to run Effect SQL migrations for state database.";        return new Error(message, { cause: error });      }),    ),  );} export function runPersistenceMigrations(db: SqliteDatabase): void {  applyStateDbPragmas(db);  if (!isEffectSqliteDatabase(db)) {    throw new PersistenceInitializationError("Expected Effect-backed sqlite adapter for migrations");  }  runEffectMigrations(db);}