Skip to content

Commit 53740f4

Browse files
authored
EFF-744 Fix sqlite migrator lock errors being silently swallowed (#1829)
1 parent f7a0b71 commit 53740f4

File tree

3 files changed

+60
-5
lines changed

3 files changed

+60
-5
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"effect": patch
3+
---
4+
5+
Fix sql migrator lock handling to only treat duplicate migration-row inserts as a concurrent migration lock.

packages/effect/src/unstable/sql/Migrator.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,11 +236,13 @@ export const make = <RD = never>({
236236
if (required.length > 0) {
237237
yield* pipe(
238238
insertMigrations(required.map(([id, name]) => [id, name])),
239-
Effect.mapError((_) =>
240-
new MigrationError({
241-
kind: "Locked",
242-
message: "Migrations already running"
243-
})
239+
Effect.mapError((error): MigrationError | SqlError =>
240+
error.reason._tag === "ConstraintError"
241+
? new MigrationError({
242+
kind: "Locked",
243+
message: "Migrations already running"
244+
})
245+
: error
244246
)
245247
)
246248
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { NodeFileSystem } from "@effect/platform-node"
2+
import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-node"
3+
import { assert, describe, it } from "@effect/vitest"
4+
import { Effect, FileSystem } from "effect"
5+
import { Reactivity } from "effect/unstable/reactivity"
6+
import * as SqlClient from "effect/unstable/sql/SqlClient"
7+
import * as SqlError from "effect/unstable/sql/SqlError"
8+
9+
const makeClients = Effect.gen(function*() {
10+
const fs = yield* FileSystem.FileSystem
11+
const dir = yield* fs.makeTempDirectoryScoped()
12+
const filename = dir + "/test.db"
13+
14+
return {
15+
lockClient: yield* SqliteClient.make({ filename }),
16+
migratorClient: yield* SqliteClient.make({ filename })
17+
}
18+
}).pipe(Effect.provide([NodeFileSystem.layer, Reactivity.layer]))
19+
20+
describe("SqliteMigrator", () => {
21+
it.effect("fails on lock errors", () =>
22+
Effect.gen(function*() {
23+
const { lockClient, migratorClient } = yield* makeClients
24+
25+
yield* migratorClient`PRAGMA busy_timeout = 1`
26+
27+
yield* SqliteMigrator.run({
28+
loader: SqliteMigrator.fromRecord({})
29+
}).pipe(Effect.provideService(SqlClient.SqlClient, migratorClient))
30+
31+
yield* Effect.acquireRelease(
32+
lockClient`BEGIN IMMEDIATE`,
33+
() => lockClient`ROLLBACK`.pipe(Effect.ignore)
34+
)
35+
36+
const error = yield* Effect.flip(
37+
SqliteMigrator.run({
38+
loader: SqliteMigrator.fromRecord({
39+
"1_test": Effect.void
40+
})
41+
}).pipe(Effect.provideService(SqlClient.SqlClient, migratorClient))
42+
)
43+
44+
assert.strictEqual(error._tag, "SqlError")
45+
assert(SqlError.isSqlError(error))
46+
assert.strictEqual(error.reason._tag, "LockTimeoutError")
47+
}))
48+
})

0 commit comments

Comments
 (0)