| ... | ... | @@ -1,6 +1,10 @@ |
| 1 | import * as NodeSqlite from "node:sqlite"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 1 | 3 | import { assert, it } from "@effect/vitest"; |
| 2 | 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as FileSystem from "effect/FileSystem"; |
| 3 | 6 | import * as Layer from "effect/Layer"; |
| 7 | import * as Path from "effect/Path"; |
| 4 | 8 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 9 | |
| 6 | 10 | import * as SqliteClient from "./nodeSqliteClient.ts"; |
| ... | ... | @@ -8,6 +12,21 @@ import * as SqliteClient from "./nodeSqliteClient.ts"; |
| 8 | 12 | const layer = it.layer(SqliteClient.layer({ filename: ":memory:" })); |
| 9 | 13 | |
| 10 | 14 | layer("NodeSqliteClient", (it) => { |
| 15 | it.effect("retries preparing a query after the missing schema becomes available", () => |
| 16 | Effect.gen(function* () { |
| 17 | const sql = yield* SqlClient.SqlClient; |
| 18 | const select = sql<{ name: string }>`SELECT name FROM created_after_prepare_failure`; |
| 19 | const error = yield* select.pipe(Effect.flip); |
| 20 | assert.equal(error._tag, "SqlError"); |
| 21 | assert.equal(error.reason.operation, "prepare"); |
| 22 | |
| 23 | yield* sql`CREATE TABLE created_after_prepare_failure(name TEXT NOT NULL)`; |
| 24 | yield* sql`INSERT INTO created_after_prepare_failure VALUES ('recovered')`; |
| 25 | assert.deepEqual(yield* select, [{ name: "recovered" }]); |
| 26 | assert.deepEqual(yield* select.values, [["recovered"]]); |
| 27 | }), |
| 28 | ); |
| 29 | |
| 11 | 30 | it.effect("runs prepared queries and returns positional values", () => |
| 12 | 31 | Effect.gen(function* () { |
| 13 | 32 | const sql = yield* SqlClient.SqlClient; |
| ... | ... | @@ -54,3 +73,32 @@ it.effect("returns a typed failure when the database cannot be opened", () => |
| 54 | 73 | assert.equal(error.reason.operation, "open"); |
| 55 | 74 | }), |
| 56 | 75 | ); |
| 76 | |
| 77 | it.effect( |
| 78 | "recovers a prepared query immediately after an exclusive database lock is released", |
| 79 | () => |
| 80 | Effect.gen(function* () { |
| 81 | const fs = yield* FileSystem.FileSystem; |
| 82 | const path = yield* Path.Path; |
| 83 | const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-sqlite-prepare-" }); |
| 84 | const filename = path.join(directory, "state.sqlite"); |
| 85 | const blocker = yield* Effect.acquireRelease( |
| 86 | Effect.sync(() => new NodeSqlite.DatabaseSync(filename)), |
| 87 | (database) => Effect.sync(() => database.close()), |
| 88 | ); |
| 89 | yield* Effect.sync(() => { |
| 90 | blocker.exec("CREATE TABLE entries(value TEXT); INSERT INTO entries VALUES ('retained')"); |
| 91 | }); |
| 92 | yield* Effect.gen(function* () { |
| 93 | const sql = yield* SqlClient.SqlClient; |
| 94 | yield* Effect.sync(() => blocker.exec("BEGIN EXCLUSIVE")); |
| 95 | const select = sql`SELECT value FROM entries`; |
| 96 | const error = yield* select.values.pipe(Effect.flip); |
| 97 | assert.equal(error._tag, "SqlError"); |
| 98 | assert.equal(error.reason.operation, "prepare"); |
| 99 | yield* Effect.sync(() => blocker.exec("ROLLBACK")); |
| 100 | assert.deepEqual(yield* select.values, [["retained"]]); |
| 101 | assert.deepEqual(yield* select, [{ value: "retained" }]); |
| 102 | }).pipe(Effect.provide(SqliteClient.layer({ filename }))); |
| 103 | }).pipe(Effect.provide(NodeServices.layer)), |
| 104 | ); |