|
1 | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | +import { createClient } from "@libsql/client"; |
2 | 3 | import { Effect, Predicate } from "effect"; |
3 | 4 |
|
4 | 5 | import { |
@@ -78,57 +79,42 @@ describe("runSqliteDataMigrations", () => { |
78 | 79 |
|
79 | 80 | it.effect("converges when concurrent runners stamp the same migration", () => |
80 | 81 | Effect.gen(function* () { |
81 | | - const stamps = new Set<string>(); |
82 | | - let initialReads = 0; |
83 | | - let releaseInitialReads: (() => void) | undefined; |
84 | | - const initialReadsComplete = new Promise<void>((resolve) => { |
85 | | - releaseInitialReads = resolve; |
| 82 | + const client = createClient({ url: ":memory:" }); |
| 83 | + let bodiesStarted = 0; |
| 84 | + let releaseBodies: (() => void) | undefined; |
| 85 | + // Hold both bodies until both runners have read the ledger as empty. |
| 86 | + const bothBodiesStarted = new Promise<void>((resolve) => { |
| 87 | + releaseBodies = resolve; |
86 | 88 | }); |
87 | | - const client: SqliteDataMigrationClient = { |
88 | | - execute: (stmt) => { |
89 | | - const sql = typeof stmt === "string" ? stmt : stmt.sql; |
90 | | - if (sql === "SELECT name FROM data_migration") { |
91 | | - const rows = [...stamps].map((name) => ({ name })); |
92 | | - initialReads++; |
93 | | - if (initialReads === 2) releaseInitialReads?.(); |
94 | | - return initialReadsComplete.then(() => ({ rows })); |
95 | | - } |
96 | | - if (typeof stmt === "object" && sql.startsWith("INSERT INTO data_migration")) { |
97 | | - const name = String(stmt.args[0]); |
98 | | - if (stamps.has(name)) { |
99 | | - // oxlint-disable-next-line executor/no-promise-reject -- simulates a storage-driver rejection at the adapter boundary under test |
100 | | - return Promise.reject("UNIQUE constraint failed: data_migration.name"); |
101 | | - } |
102 | | - stamps.add(name); |
103 | | - return Promise.resolve({ rows: [] }); |
104 | | - } |
105 | | - if ( |
106 | | - typeof stmt === "object" && |
107 | | - sql === "SELECT name FROM data_migration WHERE name = ?" |
108 | | - ) { |
109 | | - const name = String(stmt.args[0]); |
110 | | - return Promise.resolve({ rows: stamps.has(name) ? [{ name }] : [] }); |
111 | | - } |
112 | | - return Promise.resolve({ rows: [] }); |
113 | | - }, |
| 89 | + const migration: SqliteDataMigration = { |
| 90 | + name: "2026-06-05-concurrent", |
| 91 | + run: () => |
| 92 | + Effect.promise(() => { |
| 93 | + bodiesStarted++; |
| 94 | + if (bodiesStarted === 2) releaseBodies?.(); |
| 95 | + return bothBodiesStarted; |
| 96 | + }), |
114 | 97 | }; |
115 | | - const migration = migrationSpy("2026-06-05-concurrent"); |
116 | 98 |
|
117 | 99 | const results = yield* Effect.all( |
118 | 100 | [ |
119 | | - runSqliteDataMigrations(client, [migration.migration]), |
120 | | - runSqliteDataMigrations(client, [migration.migration]), |
| 101 | + runSqliteDataMigrations(client, [migration]), |
| 102 | + runSqliteDataMigrations(client, [migration]), |
121 | 103 | ], |
122 | 104 | { concurrency: "unbounded" }, |
123 | 105 | ); |
124 | 106 |
|
125 | 107 | expect(results).toEqual([["2026-06-05-concurrent"], ["2026-06-05-concurrent"]]); |
126 | | - expect(migration.calls.length).toBe(2); |
127 | | - expect(stamps).toEqual(new Set(["2026-06-05-concurrent"])); |
| 108 | + expect(bodiesStarted).toBe(2); |
| 109 | + const stamps = yield* Effect.promise(() => |
| 110 | + client.execute("SELECT name FROM data_migration ORDER BY name"), |
| 111 | + ); |
| 112 | + expect(stamps.rows.map((row) => row.name)).toEqual(["2026-06-05-concurrent"]); |
| 113 | + client.close(); |
128 | 114 | }), |
129 | 115 | ); |
130 | 116 |
|
131 | | - it.effect("surfaces a stamp failure when no concurrent runner committed it", () => |
| 117 | + it.effect("surfaces non-conflict stamp failures", () => |
132 | 118 | Effect.gen(function* () { |
133 | 119 | const client: SqliteDataMigrationClient = { |
134 | 120 | execute: (stmt) => { |
|
0 commit comments