1import * as NodeSqlite from "node:sqlite";
2
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Path from "effect/Path";
6import * as Schema from "effect/Schema";
7
8export class V2DatabaseImportError extends Schema.TaggedError<V2DatabaseImportError>()(
9 "V2DatabaseImportError",
10 { sourcePath: Schema.String, destinationPath: Schema.String, cause: Schema.Defect() },
11) {
12 override get message() {
13 return `Could not copy the V1 database at ${this.sourcePath} to ${this.destinationPath}. The V1 database has not been migrated.`;
14 }
15}
16
17/** Seed V2 once. Its copied legacy tables remain the source for lazy transcript import. */
18export const initializeV2Database = Effect.fn("initializeV2Database")(function* (
19 destinationPath: string,
20) {
21 const fs = yield* FileSystem.FileSystem;
22 const path = yield* Path.Path;
23 const directory = path.dirname(destinationPath);
24 const sourcePath = path.join(directory, "state.sqlite");
25 yield* Effect.gen(function* () {
26 if (yield* fs.exists(destinationPath)) return;
27 if (!(yield* fs.exists(sourcePath))) return;
28 const temporaryDirectory = yield* fs.makeTempDirectoryScoped({
29 directory,
30 prefix: ".v2-import-",
31 });
32 const snapshotPath = path.join(temporaryDirectory, "snapshot.sqlite");
33 yield* Effect.tryPromise(async () => {
34 const database = new NodeSqlite.DatabaseSync(sourcePath, { readOnly: true });
35 try {
36 await NodeSqlite.backup(database, snapshotPath);
37 } finally {
38 database.close();
39 }
40 });
41 // Publish only a complete snapshot, without replacing an existing V2 database.
42 yield* fs
43 .link(snapshotPath, destinationPath)
44 .pipe(
45 Effect.catch((error) =>
46 error.reason._tag === "AlreadyExists" ? Effect.void : Effect.fail(error),
47 ),
48 );
49 }).pipe(
50 Effect.scoped,
51 Effect.mapError((cause) => new V2DatabaseImportError({ sourcePath, destinationPath, cause })),
52 );
53});