History

[codex] Preserve native SQLite failure context (#3428)

Julius Marminge committed 4 months ago

Commit abc253d1 parent ce11e6f3 files · +104 −19Browse files

Unified · Split

3 files
  1. apps/server/src/persistence/Layers/Sqlite.ts +2 −1
  2. apps/server/src/persistence/NodeSqliteClient.test.ts +22 −0
  3. apps/server/src/persistence/NodeSqliteClient.ts +80 −18
apps/server/src/persistence/Layers/Sqlite.ts+2−1
@@ -3,6 +3,7 @@ import * as Layer from "effect/Layer"; · 2 unmodified lines
33 import * as FileSystem from "effect/FileSystem";
44 import * as Path from "effect/Path";
55 import * as SqlClient from "effect/unstable/sql/SqlClient";
6+import type { SqlError } from "effect/unstable/sql/SqlError";
67
78 import { runMigrations } from "../Migrations.ts";
89 import { ServerConfig } from "../../config.ts";
@@ -13,7 +14,7 @@ type RuntimeSqliteLayerConfig = { · 4 unmodified lines
1314 };
1415
1516 type Loader = {
16− layer: (config: RuntimeSqliteLayerConfig) => Layer.Layer<SqlClient.SqlClient>;
17+ layer: (config: RuntimeSqliteLayerConfig) => Layer.Layer<SqlClient.SqlClient, SqlError>;
1718 };
1819 const defaultSqliteClientLoaders = {
1920 bun: () => import("@effect/sql-sqlite-bun/SqliteClient"),
apps/server/src/persistence/NodeSqliteClient.test.ts+22−0
@@ -1,5 +1,6 @@
11 import { assert, it } from "@effect/vitest";
22 import * as Effect from "effect/Effect";
3+import * as Layer from "effect/Layer";
34 import * as SqlClient from "effect/unstable/sql/SqlClient";
45
56 import * as SqliteClient from "./NodeSqliteClient.ts";
@@ -27,4 +28,25 @@ layer("NodeSqliteClient", (it) => { · 21 unmodified lines
2728 assert.equal(values[1]?.[1], "beta");
2829 }),
2930 );
31+
32+ it.effect("returns a typed failure when an unprepared statement cannot be prepared", () =>
33+ Effect.gen(function* () {
34+ const sql = yield* SqlClient.SqlClient;
35+ const error = yield* Effect.flip(sql.unsafe("SELECT FROM").unprepared);
36+
37+ assert.equal(error._tag, "SqlError");
38+ assert.equal(error.reason.operation, "prepare");
39+ }),
40+ );
3041 });
42+
43+it.effect("returns a typed failure when the database cannot be opened", () =>
44+ Effect.gen(function* () {
45+ const error = yield* Effect.flip(
46+ Layer.build(SqliteClient.layer({ filename: "\0" })).pipe(Effect.scoped),
47+ );
48+
49+ assert.equal(error._tag, "SqlError");
50+ assert.equal(error.reason.operation, "open");
51+ }),
52+);
apps/server/src/persistence/NodeSqliteClient.ts+80−18
@@ -13,6 +13,7 @@ import * as Effect from "effect/Effect"; · 12 unmodified lines
1313 import * as Fiber from "effect/Fiber";
1414 import { identity } from "effect/Function";
1515 import * as Layer from "effect/Layer";
16+import * as Schema from "effect/Schema";
1617 import * as Scope from "effect/Scope";
1718 import * as Semaphore from "effect/Semaphore";
1819 import * as Context from "effect/Context";
@@ -45,6 +46,27 @@ export interface SqliteMemoryClientConfig extends Omit< · 26 unmodified lines
4546 "filename" | "readonly"
4647 > {}
4748
49+export class UnsupportedNodeSqliteVersionError extends Schema.TaggedErrorClass<UnsupportedNodeSqliteVersionError>()(
50+ "UnsupportedNodeSqliteVersionError",
51+ {
52+ nodeVersion: Schema.String,
53+ requirement: Schema.String,
54+ },
55+) {
56+ override get message(): string {
57+ return `Node.js ${this.nodeVersion} is missing required node:sqlite APIs. Upgrade to ${this.requirement}.`;
58+ }
59+}
60+
61+export class UnsupportedNodeSqliteOperationError extends Schema.TaggedErrorClass<UnsupportedNodeSqliteOperationError>()(
62+ "UnsupportedNodeSqliteOperationError",
63+ {},
64+) {
65+ override get message(): string {
66+ return "Node SQLite does not support executeStream.";
67+ }
68+}
69+
4870 /**
4971 * Verify that the current Node.js version includes the `node:sqlite` APIs
5072 * used by `NodeSqliteClient` — specifically `StatementSync.columns()` (added
@@ -60,8 +82,10 @@ const checkNodeSqliteCompat = () => { · 9 unmodified lines
6082
6183 if (!supported) {
6284 return Effect.die(
63− `Node.js ${process.versions.node} is missing required node:sqlite APIs ` +
64− `(StatementSync.columns). Upgrade to Node.js >=22.16, >=23.11, or >=24.`,
85+ new UnsupportedNodeSqliteVersionError({
86+ nodeVersion: process.versions.node,
87+ requirement: "Node.js >=22.16, >=23.11, or >=24",
88+ }),
6589 );
6690 }
6791 return Effect.void;
@@ -70,7 +94,7 @@ const checkNodeSqliteCompat = () => { · 2 unmodified lines
7094 const makeWithDatabase = Effect.fn("makeWithDatabase")(function* (
7195 options: SqliteClientConfig,
7296 openDatabase: () => NodeSqlite.DatabaseSync,
73−): Effect.fn.Return<Client.SqlClient, never, Scope.Scope | Reactivity.Reactivity> {
97+): Effect.fn.Return<Client.SqlClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
7498 yield* checkNodeSqliteCompat();
7599
76100 const compiler = Statement.makeCompilerSqlite(options.transformQueryNames);
@@ -80,10 +104,28 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( · 3 unmodified lines
80104
81105 const makeConnection = Effect.gen(function* () {
82106 const scope = yield* Effect.scope;
83− const db = openDatabase();
107+ const db = yield* Effect.try({
108+ try: openDatabase,
109+ catch: (cause) =>
110+ new SqlError({
111+ reason: classifySqliteError(cause, {
112+ message: "Failed to open database",
113+ operation: "open",
114+ }),
115+ }),
116+ });
84117 yield* Scope.addFinalizer(
85118 scope,
86− Effect.sync(() => db.close()),
119+ Effect.try({
120+ try: () => db.close(),
121+ catch: (cause) =>
122+ new SqlError({
123+ reason: classifySqliteError(cause, {
124+ message: "Failed to close database",
125+ operation: "close",
126+ }),
127+ }),
128+ }).pipe(Effect.orDie),
87129 );
88130
89131 const statementReaderCache = new WeakMap<NodeSqlite.StatementSync, boolean>();
@@ -119,8 +161,8 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( · 29 unmodified lines
119161 raw: boolean,
120162 ) =>
121163 Effect.withFiber<ReadonlyArray<any>, SqlError>((fiber) => {
122− statement.setReadBigInts(Boolean(Context.get(fiber.context, Client.SafeIntegers)));
123164 try {
165+ statement.setReadBigInts(Boolean(Context.get(fiber.context, Client.SafeIntegers)));
124166 if (hasRows(statement)) {
125167 return Effect.succeed(statement.all(...(params as any)));
126168 }
@@ -166,11 +208,20 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( · 39 unmodified lines
166208 }),
167209 }),
168210 (statement) =>
169− Effect.sync(() => {
170− if (hasRows(statement)) {
171− statement.setReturnArrays(false);
172− }
173− }),
211+ Effect.try({
212+ try: () => {
213+ if (hasRows(statement)) {
214+ statement.setReturnArrays(false);
215+ }
216+ },
217+ catch: (cause) =>
218+ new SqlError({
219+ reason: classifySqliteError(cause, {
220+ message: "Failed to reset statement result mode",
221+ operation: "resetResultMode",
222+ }),
223+ }),
224+ }).pipe(Effect.orDie),
174225 );
175226
176227 return identity<Connection>({
@@ -184,11 +235,20 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( · 7 unmodified lines
184235 return runValues(sql, params);
185236 },
186237 executeUnprepared(sql, params, rowTransform) {
187− const effect = runStatement(db.prepare(sql), params ?? [], false);
238+ const effect = Effect.try({
239+ try: () => db.prepare(sql),
240+ catch: (cause) =>
241+ new SqlError({
242+ reason: classifySqliteError(cause, {
243+ message: "Failed to prepare statement",
244+ operation: "prepare",
245+ }),
246+ }),
247+ }).pipe(Effect.flatMap((statement) => runStatement(statement, params ?? [], false)));
188248 return rowTransform ? Effect.map(effect, rowTransform) : effect;
189249 },
190250 executeStream(_sql, _params) {
191− return Stream.die("executeStream not implemented");
251+ return Stream.die(new UnsupportedNodeSqliteOperationError());
192252 },
193253 });
194254 });
@@ -220,7 +280,7 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( · 25 unmodified lines
220280
221281 const make = (
222282 options: SqliteClientConfig,
223−): Effect.Effect<Client.SqlClient, never, Scope.Scope | Reactivity.Reactivity> =>
283+): Effect.Effect<Client.SqlClient, SqlError, Scope.Scope | Reactivity.Reactivity> =>
224284 makeWithDatabase(
225285 options,
226286 () =>
@@ -232,7 +292,7 @@ const make = ( · 5 unmodified lines
232292
233293 const makeMemory = (
234294 config: SqliteMemoryClientConfig = {},
235−): Effect.Effect<Client.SqlClient, never, Scope.Scope | Reactivity.Reactivity> =>
295+): Effect.Effect<Client.SqlClient, SqlError, Scope.Scope | Reactivity.Reactivity> =>
236296 makeWithDatabase(
237297 {
238298 ...config,
@@ -249,13 +309,15 @@ const makeMemory = ( · 10 unmodified lines
249309
250310 export const layerConfig = (
251311 config: Config.Wrap<SqliteClientConfig>,
252−): Layer.Layer<Client.SqlClient, Config.ConfigError> =>
312+): Layer.Layer<Client.SqlClient, Config.ConfigError | SqlError> =>
253313 Layer.effect(Client.SqlClient, Config.unwrap(config).pipe(Effect.flatMap(make))).pipe(
254314 Layer.provide(Reactivity.layer),
255315 );
256316
257−export const layer = (config: SqliteClientConfig): Layer.Layer<Client.SqlClient> =>
317+export const layer = (config: SqliteClientConfig): Layer.Layer<Client.SqlClient, SqlError> =>
258318 Layer.effect(Client.SqlClient, make(config)).pipe(Layer.provide(Reactivity.layer));
259319
260−export const layerMemory = (config: SqliteMemoryClientConfig = {}): Layer.Layer<Client.SqlClient> =>
320+export const layerMemory = (
321+ config: SqliteMemoryClientConfig = {},
322+): Layer.Layer<Client.SqlClient, SqlError> =>
261323 Layer.effect(Client.SqlClient, makeMemory(config)).pipe(Layer.provide(Reactivity.layer));

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t