1import { assert, describe, it } from "@effect/vitest";
2import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
3import * as Effect from "effect/Effect";
4import * as Exit from "effect/Exit";
5import * as SqlClient from "effect/sql/SqlClient";
6
7import { migrationManifest, runMigrations } from "./Migrations.ts";
8
9const v2Migrations = migrationManifest.filter(([id]) => id >= 55);
10const newerMigrationId = Math.max(...migrationManifest.map(([id]) => id)) + 1;
11
12const history = Effect.gen(function* () {
13 const sql = yield* SqlClient.SqlClient;
14 const rows = yield* sql<{ readonly migration_id: number; readonly name: string }>`
15 SELECT migration_id, name FROM effect_sql_migrations ORDER BY migration_id
16 `;
17 return rows.map((row) => [row.migration_id, row.name] as const);
18});
19
20const v2TableCount = Effect.gen(function* () {
21 const sql = yield* SqlClient.SqlClient;
22 const rows = yield* sql`
23 SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'orchestration_v2_%'
24 `;
25 return rows.length;
26});
27
28describe("runMigrations", () => {
29 it.effect("migrates a fresh database through the whole manifest", () =>
30 Effect.gen(function* () {
31 yield* runMigrations();
32 assert.deepStrictEqual(yield* history, migrationManifest);
33 assert.isAbove(yield* v2TableCount, 0);
34 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: ":memory:" }))),
35 );
36
37 it.effect("refuses to start when another migration took a V2 id", () =>
38 Effect.gen(function* () {
39 const sql = yield* SqlClient.SqlClient;
40 yield* runMigrations({ toMigrationInclusive: 54 });
41 yield* sql`
42 INSERT INTO effect_sql_migrations (migration_id, name) VALUES (55, 'FutureMainMigration')
43 `;
44 const before = yield* history;
45
46 assert.deepStrictEqual(yield* runMigrations({ toMigrationInclusive: 54 }), []);
47 const exit = yield* Effect.exit(runMigrations());
48
49 assert.isTrue(Exit.isFailure(exit));
50 assert.include(String(exit), "55_OrchestrationV2 (recorded: FutureMainMigration)");
51 assert.deepStrictEqual(yield* history, before);
52 assert.strictEqual(yield* v2TableCount, 0);
53 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: ":memory:" }))),
54 );
55
56 it.effect("refuses to start when a later id would skip V2 migrations", () =>
57 Effect.gen(function* () {
58 const sql = yield* SqlClient.SqlClient;
59 yield* runMigrations({ toMigrationInclusive: 54 });
60 yield* sql`
61 INSERT INTO effect_sql_migrations (migration_id, name)
62 VALUES (${newerMigrationId}, 'FutureMainMigration')
63 `;
64 const before = yield* history;
65
66 const exit = yield* Effect.exit(runMigrations());
67
68 assert.isTrue(Exit.isFailure(exit));
69 for (const [id, name] of v2Migrations) {
70 assert.include(String(exit), `${id}_${name} (recorded: nothing)`);
71 }
72 assert.deepStrictEqual(yield* history, before);
73 assert.strictEqual(yield* v2TableCount, 0);
74 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: ":memory:" }))),
75 );
76
77 it.effect("upgrades a database at exactly 54", () =>
78 Effect.gen(function* () {
79 yield* runMigrations({ toMigrationInclusive: 54 });
80 assert.deepStrictEqual(yield* runMigrations(), v2Migrations);
81 assert.deepStrictEqual(yield* history, migrationManifest);
82 assert.isAbove(yield* v2TableCount, 0);
83 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: ":memory:" }))),
84 );
85
86 it.effect.each([30, 54])("still starts with a site-local migration at id %s", (id) =>
87 Effect.gen(function* () {
88 const sql = yield* SqlClient.SqlClient;
89 yield* runMigrations({ toMigrationInclusive: 54 });
90 yield* sql`
91 UPDATE effect_sql_migrations SET name = 'SiteLocalMigration' WHERE migration_id = ${id}
92 `;
93
94 assert.deepStrictEqual(yield* runMigrations(), v2Migrations);
95 assert.isAbove(yield* v2TableCount, 0);
96 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: ":memory:" }))),
97 );
98
99 it.effect("only checks V2 migrations within the requested limit", () =>
100 Effect.gen(function* () {
101 const sql = yield* SqlClient.SqlClient;
102 yield* runMigrations({ toMigrationInclusive: 55 });
103 yield* sql`
104 INSERT INTO effect_sql_migrations (migration_id, name)
105 VALUES (56, 'SiteLocalMigration'), (${newerMigrationId}, 'NewerV2Migration')
106 `;
107 const before = yield* history;
108
109 assert.deepStrictEqual(yield* runMigrations({ toMigrationInclusive: 55 }), []);
110 const exit = yield* Effect.exit(runMigrations({ toMigrationInclusive: 56 }));
111 assert.isTrue(Exit.isFailure(exit));
112 assert.include(
113 String(exit),
114 "56_RemoveRedundantProjectionIndexes (recorded: SiteLocalMigration)",
115 );
116 assert.notInclude(String(exit), "57_ScheduledTaskWebhooks");
117 assert.deepStrictEqual(yield* history, before);
118 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: ":memory:" }))),
119 );
120
121 it.effect("still starts after a newer build recorded a later V2 migration", () =>
122 Effect.gen(function* () {
123 const sql = yield* SqlClient.SqlClient;
124 yield* runMigrations();
125 yield* sql`
126 INSERT INTO effect_sql_migrations (migration_id, name)
127 VALUES (${newerMigrationId}, 'NewerV2Migration')
128 `;
129
130 assert.deepStrictEqual(yield* runMigrations(), []);
131 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: ":memory:" }))),
132 );
133});