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);}
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);
}