| author | |
| committer | |
| log | ca63d42d670837b918081d1fc1ebada553814b4c |
| tree | e3722bbd2e014c2379504e2af273473a9bba964a |
| parent | 134d51096ea0d00a53a499e8f0c87e31fafb0006 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>22 files changed, 408 insertions(+), 404 deletions(-)
apps/server/scripts/migrate-dev-db.test.ts+1-1| ... | @@ -6,7 +6,7 @@ import * as Path from "effect/Path"; | ... | @@ -6,7 +6,7 @@ import * as Path from "effect/Path"; |
| 6 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 6 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 7 | 7 | ||
| 8 | import { runMigrations } from "../src/persistence/Migrations.ts"; | 8 | import { runMigrations } from "../src/persistence/Migrations.ts"; |
| 9 | import * as NodeSqliteClient from "../src/persistence/NodeSqliteClient.ts"; | 9 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 10 | import { runMigrateDevDb } from "./migrate-dev-db.ts"; | 10 | import { runMigrateDevDb } from "./migrate-dev-db.ts"; |
| 11 | 11 | ||
| 12 | const withDatabase = <A, E>( | 12 | const withDatabase = <A, E>( |
apps/server/scripts/migrate-dev-db.ts+1-1| ... | @@ -39,7 +39,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; | ... | @@ -39,7 +39,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 39 | import { Command, Flag } from "effect/unstable/cli"; | 39 | import { Command, Flag } from "effect/unstable/cli"; |
| 40 | 40 | ||
| 41 | import { migrationManifest, runMigrations } from "../src/persistence/Migrations.ts"; | 41 | import { migrationManifest, runMigrations } from "../src/persistence/Migrations.ts"; |
| 42 | import * as NodeSqliteClient from "../src/persistence/NodeSqliteClient.ts"; | 42 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 43 | 43 | ||
| 44 | export class MigrateDevDbNotInWorktreeError extends Schema.TaggedErrorClass<MigrateDevDbNotInWorktreeError>()( | 44 | export class MigrateDevDbNotInWorktreeError extends Schema.TaggedErrorClass<MigrateDevDbNotInWorktreeError>()( |
| 45 | "MigrateDevDbNotInWorktreeError", | 45 | "MigrateDevDbNotInWorktreeError", |
apps/server/scripts/t3-sqlite-state.test.ts+1-1| ... | @@ -5,7 +5,7 @@ import * as FileSystem from "effect/FileSystem"; | ... | @@ -5,7 +5,7 @@ import * as FileSystem from "effect/FileSystem"; |
| 5 | import * as Path from "effect/Path"; | 5 | import * as Path from "effect/Path"; |
| 6 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 6 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 7 | 7 | ||
| 8 | import * as NodeSqliteClient from "../src/persistence/NodeSqliteClient.ts"; | 8 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 9 | import { runSqliteState } from "./t3-sqlite-state.ts"; | 9 | import { runSqliteState } from "./t3-sqlite-state.ts"; |
| 10 | 10 | ||
| 11 | const createFixtureDatabase = Effect.fn("createSqliteStateFixtureDatabase")(function* ( | 11 | const createFixtureDatabase = Effect.fn("createSqliteStateFixtureDatabase")(function* ( |
apps/server/scripts/t3-sqlite-state.ts+1-1| ... | @@ -15,7 +15,7 @@ import * as Schema from "effect/Schema"; | ... | @@ -15,7 +15,7 @@ import * as Schema from "effect/Schema"; |
| 15 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 15 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 16 | import { Argument, Command, Flag } from "effect/unstable/cli"; | 16 | import { Argument, Command, Flag } from "effect/unstable/cli"; |
| 17 | 17 | ||
| 18 | import * as NodeSqliteClient from "../src/persistence/NodeSqliteClient.ts"; | 18 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 19 | 19 | ||
| 20 | export const SqliteStateOperation = Schema.Literals(["query", "exec"]); | 20 | export const SqliteStateOperation = Schema.Literals(["query", "exec"]); |
| 21 | export type SqliteStateOperation = typeof SqliteStateOperation.Type; | 21 | export type SqliteStateOperation = typeof SqliteStateOperation.Type; |
apps/server/src/persistence/Layers/Sqlite.ts+1-1| ... | @@ -18,7 +18,7 @@ type Loader = { | ... | @@ -18,7 +18,7 @@ type Loader = { |
| 18 | }; | 18 | }; |
| 19 | const defaultSqliteClientLoaders = { | 19 | const defaultSqliteClientLoaders = { |
| 20 | bun: () => import("@effect/sql-sqlite-bun/SqliteClient"), | 20 | bun: () => import("@effect/sql-sqlite-bun/SqliteClient"), |
| 21 | node: () => import("../NodeSqliteClient.ts"), | 21 | node: () => import("@t3tools/shared/nodeSqliteClient"), |
| 22 | } satisfies Record<string, () => Promise<Loader>>; | 22 | } satisfies Record<string, () => Promise<Loader>>; |
| 23 | 23 | ||
| 24 | const makeRuntimeSqliteLayer = Effect.fn("makeRuntimeSqliteLayer")(function* ( | 24 | const makeRuntimeSqliteLayer = Effect.fn("makeRuntimeSqliteLayer")(function* ( |
apps/server/src/persistence/Migrations/016_CanonicalizeModelSelections.test.ts+1-1| ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; | ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; |
| 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | ||
| 6 | import { runMigrations } from "../Migrations.ts"; | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 8 | 8 | ||
| 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); | 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); |
| 10 | 10 |
apps/server/src/persistence/Migrations/019_ProjectionSnapshotLookupIndexes.test.ts+1-1| ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; | ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; |
| 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | ||
| 6 | import { runMigrations } from "../Migrations.ts"; | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 8 | 8 | ||
| 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); | 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); |
| 10 | 10 |
apps/server/src/persistence/Migrations/024_BackfillProjectionThreadShellSummary.test.ts+1-1| ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; | ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; |
| 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | ||
| 6 | import { runMigrations } from "../Migrations.ts"; | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 8 | 8 | ||
| 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); | 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); |
| 10 | 10 |
apps/server/src/persistence/Migrations/025_CleanupInvalidProjectionPendingApprovals.test.ts+1-1| ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; | ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; |
| 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | ||
| 6 | import { runMigrations } from "../Migrations.ts"; | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 8 | 8 | ||
| 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); | 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); |
| 10 | 10 |
apps/server/src/persistence/Migrations/026_CanonicalizeModelSelectionOptions.test.ts+1-1| ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; | ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; |
| 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | ||
| 6 | import { runMigrations } from "../Migrations.ts"; | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 8 | 8 | ||
| 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); | 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); |
| 10 | 10 |
apps/server/src/persistence/Migrations/027_028_ProviderInstanceIdColumns.test.ts+1-1| ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; | ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; |
| 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | ||
| 6 | import { runMigrations } from "../Migrations.ts"; | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 8 | 8 | ||
| 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); | 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); |
| 10 | 10 |
apps/server/src/persistence/Migrations/029_ProjectionThreadDetailOrderingIndexes.test.ts+1-1| ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; | ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; |
| 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | ||
| 6 | import { runMigrations } from "../Migrations.ts"; | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 8 | 8 | ||
| 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); | 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); |
| 10 | 10 |
apps/server/src/persistence/Migrations/031_AuthAuthorizationScopes.test.ts+1-1| ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; | ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; |
| 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | ||
| 6 | import { runMigrations } from "../Migrations.ts"; | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 8 | 8 | ||
| 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); | 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); |
| 10 | 10 |
apps/server/src/persistence/Migrations/035_ProjectionThreadTitleRegeneration.test.ts+1-1| ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; | ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; |
| 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | ||
| 6 | import { runMigrations } from "../Migrations.ts"; | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 8 | 8 | ||
| 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); | 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); |
| 10 | 10 |
apps/server/src/persistence/Migrations/040_ProjectionProjectFaviconPath.test.ts+1-1| ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; | ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; |
| 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | ||
| 6 | import { runMigrations } from "../Migrations.ts"; | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 8 | 8 | ||
| 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); | 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); |
| 10 | 10 |
apps/server/src/persistence/Migrations/041_AuthSessionClientConnection.test.ts+1-1| ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; | ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; |
| 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | ||
| 6 | import { runMigrations } from "../Migrations.ts"; | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 8 | 8 | ||
| 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); | 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); |
| 10 | 10 |
apps/server/src/persistence/Migrations/042_ProjectionThreadLinkedPullRequest.test.ts+1-1| ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; | ... | @@ -4,7 +4,7 @@ import * as Layer from "effect/Layer"; |
| 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | ||
| 6 | import { runMigrations } from "../Migrations.ts"; | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 8 | 8 | ||
| 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); | 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); |
| 10 | 10 |
apps/server/src/persistence/NodeSqliteClient.test.ts deleted-56| ... | @@ -1,56 +0,0 @@ | ||
| 1 | import { assert, it } from "@effect/vitest"; | ||
| 2 | import * as Effect from "effect/Effect"; | ||
| 3 | import * as Layer from "effect/Layer"; | ||
| 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | ||
| 5 | |||
| 6 | import * as SqliteClient from "./NodeSqliteClient.ts"; | ||
| 7 | |||
| 8 | const layer = it.layer(SqliteClient.layerMemory()); | ||
| 9 | |||
| 10 | layer("NodeSqliteClient", (it) => { | ||
| 11 | it.effect("runs prepared queries and returns positional values", () => | ||
| 12 | Effect.gen(function* () { | ||
| 13 | const sql = yield* SqlClient.SqlClient; | ||
| 14 | |||
| 15 | yield* sql`CREATE TABLE entries(id INTEGER PRIMARY KEY, name TEXT NOT NULL)`; | ||
| 16 | yield* sql`INSERT INTO entries(name) VALUES (${"alpha"}), (${"beta"})`; | ||
| 17 | |||
| 18 | const rows = yield* sql<{ readonly id: number; readonly name: string }>` | ||
| 19 | SELECT id, name FROM entries ORDER BY id | ||
| 20 | `; | ||
| 21 | assert.equal(rows.length, 2); | ||
| 22 | assert.equal(rows[0]?.name, "alpha"); | ||
| 23 | assert.equal(rows[1]?.name, "beta"); | ||
| 24 | |||
| 25 | const values = yield* sql`SELECT id, name FROM entries ORDER BY id`.values; | ||
| 26 | assert.equal(values.length, 2); | ||
| 27 | assert.equal(values[0]?.[1], "alpha"); | ||
| 28 | assert.equal(values[1]?.[1], "beta"); | ||
| 29 | |||
| 30 | const unpreparedValues = yield* sql`SELECT id, name FROM entries ORDER BY id` | ||
| 31 | .valuesUnprepared; | ||
| 32 | assert.deepEqual(unpreparedValues, values); | ||
| 33 | }), | ||
| 34 | ); | ||
| 35 | |||
| 36 | it.effect("returns a typed failure when an unprepared statement cannot be prepared", () => | ||
| 37 | Effect.gen(function* () { | ||
| 38 | const sql = yield* SqlClient.SqlClient; | ||
| 39 | const error = yield* Effect.flip(sql.unsafe("SELECT FROM").unprepared); | ||
| 40 | |||
| 41 | assert.equal(error._tag, "SqlError"); | ||
| 42 | assert.equal(error.reason.operation, "prepare"); | ||
| 43 | }), | ||
| 44 | ); | ||
| 45 | }); | ||
| 46 | |||
| 47 | it.effect("returns a typed failure when the database cannot be opened", () => | ||
| 48 | Effect.gen(function* () { | ||
| 49 | const error = yield* Effect.flip( | ||
| 50 | Layer.build(SqliteClient.layer({ filename: "\0" })).pipe(Effect.scoped), | ||
| 51 | ); | ||
| 52 | |||
| 53 | assert.equal(error._tag, "SqlError"); | ||
| 54 | assert.equal(error.reason.operation, "open"); | ||
| 55 | }), | ||
| 56 | ); | ||
apps/server/src/persistence/NodeSqliteClient.ts deleted-331| ... | @@ -1,331 +0,0 @@ | ||
| 1 | /** | ||
| 2 | * Port of `@effect/sql-sqlite-node` that uses the native `node:sqlite` | ||
| 3 | * bindings instead of `better-sqlite3`. | ||
| 4 | * | ||
| 5 | * @module SqliteClient | ||
| 6 | */ | ||
| 7 | import * as NodeSqlite from "node:sqlite"; | ||
| 8 | |||
| 9 | import * as Cache from "effect/Cache"; | ||
| 10 | import * as Config from "effect/Config"; | ||
| 11 | import * as Duration from "effect/Duration"; | ||
| 12 | import * as Effect from "effect/Effect"; | ||
| 13 | import * as Fiber from "effect/Fiber"; | ||
| 14 | import { identity } from "effect/Function"; | ||
| 15 | import * as Layer from "effect/Layer"; | ||
| 16 | import * as Schema from "effect/Schema"; | ||
| 17 | import * as Scope from "effect/Scope"; | ||
| 18 | import * as Semaphore from "effect/Semaphore"; | ||
| 19 | import * as Context from "effect/Context"; | ||
| 20 | import * as Stream from "effect/Stream"; | ||
| 21 | import * as Reactivity from "effect/unstable/reactivity/Reactivity"; | ||
| 22 | import * as Client from "effect/unstable/sql/SqlClient"; | ||
| 23 | import type { Connection } from "effect/unstable/sql/SqlConnection"; | ||
| 24 | import { SqlError, classifySqliteError } from "effect/unstable/sql/SqlError"; | ||
| 25 | import * as Statement from "effect/unstable/sql/Statement"; | ||
| 26 | |||
| 27 | const ATTR_DB_SYSTEM_NAME = "db.system.name"; | ||
| 28 | |||
| 29 | export const TypeId: TypeId = "~local/sqlite-node/SqliteClient"; | ||
| 30 | |||
| 31 | export type TypeId = "~local/sqlite-node/SqliteClient"; | ||
| 32 | |||
| 33 | export interface SqliteClientConfig { | ||
| 34 | readonly filename: string; | ||
| 35 | readonly readonly?: boolean | undefined; | ||
| 36 | readonly allowExtension?: boolean | undefined; | ||
| 37 | readonly prepareCacheSize?: number | undefined; | ||
| 38 | readonly prepareCacheTTL?: Duration.Input | undefined; | ||
| 39 | readonly spanAttributes?: Record<string, unknown> | undefined; | ||
| 40 | readonly transformResultNames?: ((str: string) => string) | undefined; | ||
| 41 | readonly transformQueryNames?: ((str: string) => string) | undefined; | ||
| 42 | } | ||
| 43 | |||
| 44 | export interface SqliteMemoryClientConfig extends Omit< | ||
| 45 | SqliteClientConfig, | ||
| 46 | "filename" | "readonly" | ||
| 47 | > {} | ||
| 48 | |||
| 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 | |||
| 70 | /** | ||
| 71 | * Verify that the current Node.js version includes the `node:sqlite` APIs | ||
| 72 | * used by `NodeSqliteClient` — specifically `StatementSync.columns()` (added | ||
| 73 | * in Node 22.16.0 / 23.11.0). | ||
| 74 | * | ||
| 75 | * @see https://github.com/nodejs/node/pull/57490 | ||
| 76 | */ | ||
| 77 | const checkNodeSqliteCompat = () => { | ||
| 78 | const parts = process.versions.node.split(".").map(Number); | ||
| 79 | const major = parts[0] ?? 0; | ||
| 80 | const minor = parts[1] ?? 0; | ||
| 81 | const supported = (major === 22 && minor >= 16) || (major === 23 && minor >= 11) || major >= 24; | ||
| 82 | |||
| 83 | if (!supported) { | ||
| 84 | return Effect.die( | ||
| 85 | new UnsupportedNodeSqliteVersionError({ | ||
| 86 | nodeVersion: process.versions.node, | ||
| 87 | requirement: "Node.js >=22.16, >=23.11, or >=24", | ||
| 88 | }), | ||
| 89 | ); | ||
| 90 | } | ||
| 91 | return Effect.void; | ||
| 92 | }; | ||
| 93 | |||
| 94 | const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( | ||
| 95 | options: SqliteClientConfig, | ||
| 96 | openDatabase: () => NodeSqlite.DatabaseSync, | ||
| 97 | ): Effect.fn.Return<Client.SqlClient, SqlError, Scope.Scope | Reactivity.Reactivity> { | ||
| 98 | yield* checkNodeSqliteCompat(); | ||
| 99 | |||
| 100 | const compiler = Statement.makeCompilerSqlite(options.transformQueryNames); | ||
| 101 | const transformRows = options.transformResultNames | ||
| 102 | ? Statement.defaultTransforms(options.transformResultNames).array | ||
| 103 | : undefined; | ||
| 104 | |||
| 105 | const makeConnection = Effect.gen(function* () { | ||
| 106 | const scope = yield* Effect.scope; | ||
| 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 | }); | ||
| 117 | yield* Scope.addFinalizer( | ||
| 118 | scope, | ||
| 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), | ||
| 129 | ); | ||
| 130 | |||
| 131 | const statementReaderCache = new WeakMap<NodeSqlite.StatementSync, boolean>(); | ||
| 132 | const hasRows = (statement: NodeSqlite.StatementSync): boolean => { | ||
| 133 | const cached = statementReaderCache.get(statement); | ||
| 134 | if (cached !== undefined) { | ||
| 135 | return cached; | ||
| 136 | } | ||
| 137 | const value = statement.columns().length > 0; | ||
| 138 | statementReaderCache.set(statement, value); | ||
| 139 | return value; | ||
| 140 | }; | ||
| 141 | |||
| 142 | const prepare = (sql: string) => | ||
| 143 | Effect.try({ | ||
| 144 | try: () => db.prepare(sql), | ||
| 145 | catch: (cause) => | ||
| 146 | new SqlError({ | ||
| 147 | reason: classifySqliteError(cause, { | ||
| 148 | message: "Failed to prepare statement", | ||
| 149 | operation: "prepare", | ||
| 150 | }), | ||
| 151 | }), | ||
| 152 | }); | ||
| 153 | |||
| 154 | const prepareCache = yield* Cache.make({ | ||
| 155 | capacity: options.prepareCacheSize ?? 200, | ||
| 156 | timeToLive: options.prepareCacheTTL ?? Duration.minutes(10), | ||
| 157 | lookup: prepare, | ||
| 158 | }); | ||
| 159 | |||
| 160 | const runStatement = ( | ||
| 161 | statement: NodeSqlite.StatementSync, | ||
| 162 | params: ReadonlyArray<unknown>, | ||
| 163 | raw: boolean, | ||
| 164 | ) => | ||
| 165 | Effect.withFiber<ReadonlyArray<any>, SqlError>((fiber) => { | ||
| 166 | try { | ||
| 167 | statement.setReadBigInts(Boolean(Context.get(fiber.context, Client.SafeIntegers))); | ||
| 168 | if (hasRows(statement)) { | ||
| 169 | return Effect.succeed(statement.all(...(params as any))); | ||
| 170 | } | ||
| 171 | const result = statement.run(...(params as any)); | ||
| 172 | return Effect.succeed(raw ? (result as unknown as ReadonlyArray<any>) : []); | ||
| 173 | } catch (cause) { | ||
| 174 | return Effect.fail( | ||
| 175 | new SqlError({ | ||
| 176 | reason: classifySqliteError(cause, { | ||
| 177 | message: "Failed to execute statement", | ||
| 178 | operation: "execute", | ||
| 179 | }), | ||
| 180 | }), | ||
| 181 | ); | ||
| 182 | } | ||
| 183 | }); | ||
| 184 | |||
| 185 | const run = (sql: string, params: ReadonlyArray<unknown>, raw = false) => | ||
| 186 | Effect.flatMap(Cache.get(prepareCache, sql), (s) => runStatement(s, params, raw)); | ||
| 187 | |||
| 188 | const runStatementValues = ( | ||
| 189 | statement: NodeSqlite.StatementSync, | ||
| 190 | params: ReadonlyArray<unknown>, | ||
| 191 | ) => | ||
| 192 | Effect.acquireUseRelease( | ||
| 193 | Effect.succeed(statement), | ||
| 194 | (statement) => | ||
| 195 | Effect.try({ | ||
| 196 | try: () => { | ||
| 197 | if (hasRows(statement)) { | ||
| 198 | statement.setReturnArrays(true); | ||
| 199 | // Safe to cast to array after we've setReturnArrays(true) | ||
| 200 | return statement.all(...(params as any)) as unknown as ReadonlyArray< | ||
| 201 | ReadonlyArray<unknown> | ||
| 202 | >; | ||
| 203 | } | ||
| 204 | statement.run(...(params as any)); | ||
| 205 | return []; | ||
| 206 | }, | ||
| 207 | catch: (cause) => | ||
| 208 | new SqlError({ | ||
| 209 | reason: classifySqliteError(cause, { | ||
| 210 | message: "Failed to execute statement", | ||
| 211 | operation: "execute", | ||
| 212 | }), | ||
| 213 | }), | ||
| 214 | }), | ||
| 215 | (statement) => | ||
| 216 | Effect.try({ | ||
| 217 | try: () => { | ||
| 218 | if (hasRows(statement)) { | ||
| 219 | statement.setReturnArrays(false); | ||
| 220 | } | ||
| 221 | }, | ||
| 222 | catch: (cause) => | ||
| 223 | new SqlError({ | ||
| 224 | reason: classifySqliteError(cause, { | ||
| 225 | message: "Failed to reset statement result mode", | ||
| 226 | operation: "resetResultMode", | ||
| 227 | }), | ||
| 228 | }), | ||
| 229 | }).pipe(Effect.orDie), | ||
| 230 | ); | ||
| 231 | |||
| 232 | const runValues = (sql: string, params: ReadonlyArray<unknown>) => | ||
| 233 | Effect.flatMap(Cache.get(prepareCache, sql), (statement) => | ||
| 234 | runStatementValues(statement, params), | ||
| 235 | ); | ||
| 236 | |||
| 237 | return identity<Connection>({ | ||
| 238 | execute(sql, params, rowTransform) { | ||
| 239 | return rowTransform ? Effect.map(run(sql, params), rowTransform) : run(sql, params); | ||
| 240 | }, | ||
| 241 | executeRaw(sql, params) { | ||
| 242 | return run(sql, params, true); | ||
| 243 | }, | ||
| 244 | executeValues(sql, params) { | ||
| 245 | return runValues(sql, params); | ||
| 246 | }, | ||
| 247 | executeValuesUnprepared(sql, params) { | ||
| 248 | return Effect.flatMap(prepare(sql), (statement) => | ||
| 249 | runStatementValues(statement, params ?? []), | ||
| 250 | ); | ||
| 251 | }, | ||
| 252 | executeUnprepared(sql, params, rowTransform) { | ||
| 253 | const effect = prepare(sql).pipe( | ||
| 254 | Effect.flatMap((statement) => runStatement(statement, params ?? [], false)), | ||
| 255 | ); | ||
| 256 | return rowTransform ? Effect.map(effect, rowTransform) : effect; | ||
| 257 | }, | ||
| 258 | executeStream(_sql, _params) { | ||
| 259 | return Stream.die(new UnsupportedNodeSqliteOperationError()); | ||
| 260 | }, | ||
| 261 | }); | ||
| 262 | }); | ||
| 263 | |||
| 264 | const semaphore = yield* Semaphore.make(1); | ||
| 265 | const connection = yield* makeConnection; | ||
| 266 | |||
| 267 | const acquirer = semaphore.withPermits(1)(Effect.succeed(connection)); | ||
| 268 | const transactionAcquirer = Effect.uninterruptibleMask((restore) => { | ||
| 269 | const fiber = Fiber.getCurrent()!; | ||
| 270 | const scope = Context.getUnsafe(fiber.context, Scope.Scope); | ||
| 271 | return Effect.as( | ||
| 272 | Effect.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))), | ||
| 273 | connection, | ||
| 274 | ); | ||
| 275 | }); | ||
| 276 | |||
| 277 | return yield* Client.make({ | ||
| 278 | acquirer, | ||
| 279 | compiler, | ||
| 280 | transactionAcquirer, | ||
| 281 | spanAttributes: [ | ||
| 282 | ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), | ||
| 283 | [ATTR_DB_SYSTEM_NAME, "sqlite"], | ||
| 284 | ], | ||
| 285 | transformRows, | ||
| 286 | }); | ||
| 287 | }); | ||
| 288 | |||
| 289 | const make = ( | ||
| 290 | options: SqliteClientConfig, | ||
| 291 | ): Effect.Effect<Client.SqlClient, SqlError, Scope.Scope | Reactivity.Reactivity> => | ||
| 292 | makeWithDatabase( | ||
| 293 | options, | ||
| 294 | () => | ||
| 295 | new NodeSqlite.DatabaseSync(options.filename, { | ||
| 296 | readOnly: options.readonly ?? false, | ||
| 297 | allowExtension: options.allowExtension ?? false, | ||
| 298 | }), | ||
| 299 | ); | ||
| 300 | |||
| 301 | const makeMemory = ( | ||
| 302 | config: SqliteMemoryClientConfig = {}, | ||
| 303 | ): Effect.Effect<Client.SqlClient, SqlError, Scope.Scope | Reactivity.Reactivity> => | ||
| 304 | makeWithDatabase( | ||
| 305 | { | ||
| 306 | ...config, | ||
| 307 | filename: ":memory:", | ||
| 308 | readonly: false, | ||
| 309 | }, | ||
| 310 | () => { | ||
| 311 | const database = new NodeSqlite.DatabaseSync(":memory:", { | ||
| 312 | allowExtension: config.allowExtension ?? false, | ||
| 313 | }); | ||
| 314 | return database; | ||
| 315 | }, | ||
| 316 | ); | ||
| 317 | |||
| 318 | export const layerConfig = ( | ||
| 319 | config: Config.Wrap<SqliteClientConfig>, | ||
| 320 | ): Layer.Layer<Client.SqlClient, Config.ConfigError | SqlError> => | ||
| 321 | Layer.effect(Client.SqlClient, Config.unwrap(config).pipe(Effect.flatMap(make))).pipe( | ||
| 322 | Layer.provide(Reactivity.layer), | ||
| 323 | ); | ||
| 324 | |||
| 325 | export const layer = (config: SqliteClientConfig): Layer.Layer<Client.SqlClient, SqlError> => | ||
| 326 | Layer.effect(Client.SqlClient, make(config)).pipe(Layer.provide(Reactivity.layer)); | ||
| 327 | |||
| 328 | export const layerMemory = ( | ||
| 329 | config: SqliteMemoryClientConfig = {}, | ||
| 330 | ): Layer.Layer<Client.SqlClient, SqlError> => | ||
| 331 | Layer.effect(Client.SqlClient, makeMemory(config)).pipe(Layer.provide(Reactivity.layer)); | ||
packages/shared/package.json+4| ... | @@ -246,6 +246,10 @@ | ... | @@ -246,6 +246,10 @@ |
| 246 | "./claudeCompaction": { | 246 | "./claudeCompaction": { |
| 247 | "types": "./src/claudeCompaction.ts", | 247 | "types": "./src/claudeCompaction.ts", |
| 248 | "import": "./src/claudeCompaction.ts" | 248 | "import": "./src/claudeCompaction.ts" |
| 249 | }, | ||
| 250 | "./nodeSqliteClient": { | ||
| 251 | "types": "./src/nodeSqliteClient.ts", | ||
| 252 | "import": "./src/nodeSqliteClient.ts" | ||
| 249 | } | 253 | } |
| 250 | }, | 254 | }, |
| 251 | "scripts": { | 255 | "scripts": { |
packages/shared/src/nodeSqliteClient.test.ts created+56| ... | @@ -0,0 +1,56 @@ | ||
| 1 | import { assert, it } from "@effect/vitest"; | ||
| 2 | import * as Effect from "effect/Effect"; | ||
| 3 | import * as Layer from "effect/Layer"; | ||
| 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | ||
| 5 | |||
| 6 | import * as SqliteClient from "./nodeSqliteClient.ts"; | ||
| 7 | |||
| 8 | const layer = it.layer(SqliteClient.layerMemory()); | ||
| 9 | |||
| 10 | layer("NodeSqliteClient", (it) => { | ||
| 11 | it.effect("runs prepared queries and returns positional values", () => | ||
| 12 | Effect.gen(function* () { | ||
| 13 | const sql = yield* SqlClient.SqlClient; | ||
| 14 | |||
| 15 | yield* sql`CREATE TABLE entries(id INTEGER PRIMARY KEY, name TEXT NOT NULL)`; | ||
| 16 | yield* sql`INSERT INTO entries(name) VALUES (${"alpha"}), (${"beta"})`; | ||
| 17 | |||
| 18 | const rows = yield* sql<{ readonly id: number; readonly name: string }>` | ||
| 19 | SELECT id, name FROM entries ORDER BY id | ||
| 20 | `; | ||
| 21 | assert.equal(rows.length, 2); | ||
| 22 | assert.equal(rows[0]?.name, "alpha"); | ||
| 23 | assert.equal(rows[1]?.name, "beta"); | ||
| 24 | |||
| 25 | const values = yield* sql`SELECT id, name FROM entries ORDER BY id`.values; | ||
| 26 | assert.equal(values.length, 2); | ||
| 27 | assert.equal(values[0]?.[1], "alpha"); | ||
| 28 | assert.equal(values[1]?.[1], "beta"); | ||
| 29 | |||
| 30 | const unpreparedValues = yield* sql`SELECT id, name FROM entries ORDER BY id` | ||
| 31 | .valuesUnprepared; | ||
| 32 | assert.deepEqual(unpreparedValues, values); | ||
| 33 | }), | ||
| 34 | ); | ||
| 35 | |||
| 36 | it.effect("returns a typed failure when an unprepared statement cannot be prepared", () => | ||
| 37 | Effect.gen(function* () { | ||
| 38 | const sql = yield* SqlClient.SqlClient; | ||
| 39 | const error = yield* Effect.flip(sql.unsafe("SELECT FROM").unprepared); | ||
| 40 | |||
| 41 | assert.equal(error._tag, "SqlError"); | ||
| 42 | assert.equal(error.reason.operation, "prepare"); | ||
| 43 | }), | ||
| 44 | ); | ||
| 45 | }); | ||
| 46 | |||
| 47 | it.effect("returns a typed failure when the database cannot be opened", () => | ||
| 48 | Effect.gen(function* () { | ||
| 49 | const error = yield* Effect.flip( | ||
| 50 | Layer.build(SqliteClient.layer({ filename: "\0" })).pipe(Effect.scoped), | ||
| 51 | ); | ||
| 52 | |||
| 53 | assert.equal(error._tag, "SqlError"); | ||
| 54 | assert.equal(error.reason.operation, "open"); | ||
| 55 | }), | ||
| 56 | ); | ||
packages/shared/src/nodeSqliteClient.ts created+331| ... | @@ -0,0 +1,331 @@ | ||
| 1 | /** | ||
| 2 | * Port of `@effect/sql-sqlite-node` that uses the native `node:sqlite` | ||
| 3 | * bindings instead of `better-sqlite3`. | ||
| 4 | * | ||
| 5 | * @module SqliteClient | ||
| 6 | */ | ||
| 7 | import * as NodeSqlite from "node:sqlite"; | ||
| 8 | |||
| 9 | import * as Cache from "effect/Cache"; | ||
| 10 | import * as Config from "effect/Config"; | ||
| 11 | import * as Duration from "effect/Duration"; | ||
| 12 | import * as Effect from "effect/Effect"; | ||
| 13 | import * as Fiber from "effect/Fiber"; | ||
| 14 | import { identity } from "effect/Function"; | ||
| 15 | import * as Layer from "effect/Layer"; | ||
| 16 | import * as Schema from "effect/Schema"; | ||
| 17 | import * as Scope from "effect/Scope"; | ||
| 18 | import * as Semaphore from "effect/Semaphore"; | ||
| 19 | import * as Context from "effect/Context"; | ||
| 20 | import * as Stream from "effect/Stream"; | ||
| 21 | import * as Reactivity from "effect/unstable/reactivity/Reactivity"; | ||
| 22 | import * as Client from "effect/unstable/sql/SqlClient"; | ||
| 23 | import type { Connection } from "effect/unstable/sql/SqlConnection"; | ||
| 24 | import { SqlError, classifySqliteError } from "effect/unstable/sql/SqlError"; | ||
| 25 | import * as Statement from "effect/unstable/sql/Statement"; | ||
| 26 | |||
| 27 | const ATTR_DB_SYSTEM_NAME = "db.system.name"; | ||
| 28 | |||
| 29 | export const TypeId: TypeId = "~local/sqlite-node/SqliteClient"; | ||
| 30 | |||
| 31 | export type TypeId = "~local/sqlite-node/SqliteClient"; | ||
| 32 | |||
| 33 | export interface SqliteClientConfig { | ||
| 34 | readonly filename: string; | ||
| 35 | readonly readonly?: boolean | undefined; | ||
| 36 | readonly allowExtension?: boolean | undefined; | ||
| 37 | readonly prepareCacheSize?: number | undefined; | ||
| 38 | readonly prepareCacheTTL?: Duration.Input | undefined; | ||
| 39 | readonly spanAttributes?: Record<string, unknown> | undefined; | ||
| 40 | readonly transformResultNames?: ((str: string) => string) | undefined; | ||
| 41 | readonly transformQueryNames?: ((str: string) => string) | undefined; | ||
| 42 | } | ||
| 43 | |||
| 44 | export interface SqliteMemoryClientConfig extends Omit< | ||
| 45 | SqliteClientConfig, | ||
| 46 | "filename" | "readonly" | ||
| 47 | > {} | ||
| 48 | |||
| 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 | |||
| 70 | /** | ||
| 71 | * Verify that the current Node.js version includes the `node:sqlite` APIs | ||
| 72 | * used by `NodeSqliteClient` — specifically `StatementSync.columns()` (added | ||
| 73 | * in Node 22.16.0 / 23.11.0). | ||
| 74 | * | ||
| 75 | * @see https://github.com/nodejs/node/pull/57490 | ||
| 76 | */ | ||
| 77 | const checkNodeSqliteCompat = () => { | ||
| 78 | const parts = process.versions.node.split(".").map(Number); | ||
| 79 | const major = parts[0] ?? 0; | ||
| 80 | const minor = parts[1] ?? 0; | ||
| 81 | const supported = (major === 22 && minor >= 16) || (major === 23 && minor >= 11) || major >= 24; | ||
| 82 | |||
| 83 | if (!supported) { | ||
| 84 | return Effect.die( | ||
| 85 | new UnsupportedNodeSqliteVersionError({ | ||
| 86 | nodeVersion: process.versions.node, | ||
| 87 | requirement: "Node.js >=22.16, >=23.11, or >=24", | ||
| 88 | }), | ||
| 89 | ); | ||
| 90 | } | ||
| 91 | return Effect.void; | ||
| 92 | }; | ||
| 93 | |||
| 94 | const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( | ||
| 95 | options: SqliteClientConfig, | ||
| 96 | openDatabase: () => NodeSqlite.DatabaseSync, | ||
| 97 | ): Effect.fn.Return<Client.SqlClient, SqlError, Scope.Scope | Reactivity.Reactivity> { | ||
| 98 | yield* checkNodeSqliteCompat(); | ||
| 99 | |||
| 100 | const compiler = Statement.makeCompilerSqlite(options.transformQueryNames); | ||
| 101 | const transformRows = options.transformResultNames | ||
| 102 | ? Statement.defaultTransforms(options.transformResultNames).array | ||
| 103 | : undefined; | ||
| 104 | |||
| 105 | const makeConnection = Effect.gen(function* () { | ||
| 106 | const scope = yield* Effect.scope; | ||
| 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 | }); | ||
| 117 | yield* Scope.addFinalizer( | ||
| 118 | scope, | ||
| 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), | ||
| 129 | ); | ||
| 130 | |||
| 131 | const statementReaderCache = new WeakMap<NodeSqlite.StatementSync, boolean>(); | ||
| 132 | const hasRows = (statement: NodeSqlite.StatementSync): boolean => { | ||
| 133 | const cached = statementReaderCache.get(statement); | ||
| 134 | if (cached !== undefined) { | ||
| 135 | return cached; | ||
| 136 | } | ||
| 137 | const value = statement.columns().length > 0; | ||
| 138 | statementReaderCache.set(statement, value); | ||
| 139 | return value; | ||
| 140 | }; | ||
| 141 | |||
| 142 | const prepare = (sql: string) => | ||
| 143 | Effect.try({ | ||
| 144 | try: () => db.prepare(sql), | ||
| 145 | catch: (cause) => | ||
| 146 | new SqlError({ | ||
| 147 | reason: classifySqliteError(cause, { | ||
| 148 | message: "Failed to prepare statement", | ||
| 149 | operation: "prepare", | ||
| 150 | }), | ||
| 151 | }), | ||
| 152 | }); | ||
| 153 | |||
| 154 | const prepareCache = yield* Cache.make({ | ||
| 155 | capacity: options.prepareCacheSize ?? 200, | ||
| 156 | timeToLive: options.prepareCacheTTL ?? Duration.minutes(10), | ||
| 157 | lookup: prepare, | ||
| 158 | }); | ||
| 159 | |||
| 160 | const runStatement = ( | ||
| 161 | statement: NodeSqlite.StatementSync, | ||
| 162 | params: ReadonlyArray<unknown>, | ||
| 163 | raw: boolean, | ||
| 164 | ) => | ||
| 165 | Effect.withFiber<ReadonlyArray<any>, SqlError>((fiber) => { | ||
| 166 | try { | ||
| 167 | statement.setReadBigInts(Boolean(Context.get(fiber.context, Client.SafeIntegers))); | ||
| 168 | if (hasRows(statement)) { | ||
| 169 | return Effect.succeed(statement.all(...(params as any))); | ||
| 170 | } | ||
| 171 | const result = statement.run(...(params as any)); | ||
| 172 | return Effect.succeed(raw ? (result as unknown as ReadonlyArray<any>) : []); | ||
| 173 | } catch (cause) { | ||
| 174 | return Effect.fail( | ||
| 175 | new SqlError({ | ||
| 176 | reason: classifySqliteError(cause, { | ||
| 177 | message: "Failed to execute statement", | ||
| 178 | operation: "execute", | ||
| 179 | }), | ||
| 180 | }), | ||
| 181 | ); | ||
| 182 | } | ||
| 183 | }); | ||
| 184 | |||
| 185 | const run = (sql: string, params: ReadonlyArray<unknown>, raw = false) => | ||
| 186 | Effect.flatMap(Cache.get(prepareCache, sql), (s) => runStatement(s, params, raw)); | ||
| 187 | |||
| 188 | const runStatementValues = ( | ||
| 189 | statement: NodeSqlite.StatementSync, | ||
| 190 | params: ReadonlyArray<unknown>, | ||
| 191 | ) => | ||
| 192 | Effect.acquireUseRelease( | ||
| 193 | Effect.succeed(statement), | ||
| 194 | (statement) => | ||
| 195 | Effect.try({ | ||
| 196 | try: () => { | ||
| 197 | if (hasRows(statement)) { | ||
| 198 | statement.setReturnArrays(true); | ||
| 199 | // Safe to cast to array after we've setReturnArrays(true) | ||
| 200 | return statement.all(...(params as any)) as unknown as ReadonlyArray< | ||
| 201 | ReadonlyArray<unknown> | ||
| 202 | >; | ||
| 203 | } | ||
| 204 | statement.run(...(params as any)); | ||
| 205 | return []; | ||
| 206 | }, | ||
| 207 | catch: (cause) => | ||
| 208 | new SqlError({ | ||
| 209 | reason: classifySqliteError(cause, { | ||
| 210 | message: "Failed to execute statement", | ||
| 211 | operation: "execute", | ||
| 212 | }), | ||
| 213 | }), | ||
| 214 | }), | ||
| 215 | (statement) => | ||
| 216 | Effect.try({ | ||
| 217 | try: () => { | ||
| 218 | if (hasRows(statement)) { | ||
| 219 | statement.setReturnArrays(false); | ||
| 220 | } | ||
| 221 | }, | ||
| 222 | catch: (cause) => | ||
| 223 | new SqlError({ | ||
| 224 | reason: classifySqliteError(cause, { | ||
| 225 | message: "Failed to reset statement result mode", | ||
| 226 | operation: "resetResultMode", | ||
| 227 | }), | ||
| 228 | }), | ||
| 229 | }).pipe(Effect.orDie), | ||
| 230 | ); | ||
| 231 | |||
| 232 | const runValues = (sql: string, params: ReadonlyArray<unknown>) => | ||
| 233 | Effect.flatMap(Cache.get(prepareCache, sql), (statement) => | ||
| 234 | runStatementValues(statement, params), | ||
| 235 | ); | ||
| 236 | |||
| 237 | return identity<Connection>({ | ||
| 238 | execute(sql, params, rowTransform) { | ||
| 239 | return rowTransform ? Effect.map(run(sql, params), rowTransform) : run(sql, params); | ||
| 240 | }, | ||
| 241 | executeRaw(sql, params) { | ||
| 242 | return run(sql, params, true); | ||
| 243 | }, | ||
| 244 | executeValues(sql, params) { | ||
| 245 | return runValues(sql, params); | ||
| 246 | }, | ||
| 247 | executeValuesUnprepared(sql, params) { | ||
| 248 | return Effect.flatMap(prepare(sql), (statement) => | ||
| 249 | runStatementValues(statement, params ?? []), | ||
| 250 | ); | ||
| 251 | }, | ||
| 252 | executeUnprepared(sql, params, rowTransform) { | ||
| 253 | const effect = prepare(sql).pipe( | ||
| 254 | Effect.flatMap((statement) => runStatement(statement, params ?? [], false)), | ||
| 255 | ); | ||
| 256 | return rowTransform ? Effect.map(effect, rowTransform) : effect; | ||
| 257 | }, | ||
| 258 | executeStream(_sql, _params) { | ||
| 259 | return Stream.die(new UnsupportedNodeSqliteOperationError()); | ||
| 260 | }, | ||
| 261 | }); | ||
| 262 | }); | ||
| 263 | |||
| 264 | const semaphore = yield* Semaphore.make(1); | ||
| 265 | const connection = yield* makeConnection; | ||
| 266 | |||
| 267 | const acquirer = semaphore.withPermits(1)(Effect.succeed(connection)); | ||
| 268 | const transactionAcquirer = Effect.uninterruptibleMask((restore) => { | ||
| 269 | const fiber = Fiber.getCurrent()!; | ||
| 270 | const scope = Context.getUnsafe(fiber.context, Scope.Scope); | ||
| 271 | return Effect.as( | ||
| 272 | Effect.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))), | ||
| 273 | connection, | ||
| 274 | ); | ||
| 275 | }); | ||
| 276 | |||
| 277 | return yield* Client.make({ | ||
| 278 | acquirer, | ||
| 279 | compiler, | ||
| 280 | transactionAcquirer, | ||
| 281 | spanAttributes: [ | ||
| 282 | ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), | ||
| 283 | [ATTR_DB_SYSTEM_NAME, "sqlite"], | ||
| 284 | ], | ||
| 285 | transformRows, | ||
| 286 | }); | ||
| 287 | }); | ||
| 288 | |||
| 289 | const make = ( | ||
| 290 | options: SqliteClientConfig, | ||
| 291 | ): Effect.Effect<Client.SqlClient, SqlError, Scope.Scope | Reactivity.Reactivity> => | ||
| 292 | makeWithDatabase( | ||
| 293 | options, | ||
| 294 | () => | ||
| 295 | new NodeSqlite.DatabaseSync(options.filename, { | ||
| 296 | readOnly: options.readonly ?? false, | ||
| 297 | allowExtension: options.allowExtension ?? false, | ||
| 298 | }), | ||
| 299 | ); | ||
| 300 | |||
| 301 | const makeMemory = ( | ||
| 302 | config: SqliteMemoryClientConfig = {}, | ||
| 303 | ): Effect.Effect<Client.SqlClient, SqlError, Scope.Scope | Reactivity.Reactivity> => | ||
| 304 | makeWithDatabase( | ||
| 305 | { | ||
| 306 | ...config, | ||
| 307 | filename: ":memory:", | ||
| 308 | readonly: false, | ||
| 309 | }, | ||
| 310 | () => { | ||
| 311 | const database = new NodeSqlite.DatabaseSync(":memory:", { | ||
| 312 | allowExtension: config.allowExtension ?? false, | ||
| 313 | }); | ||
| 314 | return database; | ||
| 315 | }, | ||
| 316 | ); | ||
| 317 | |||
| 318 | export const layerConfig = ( | ||
| 319 | config: Config.Wrap<SqliteClientConfig>, | ||
| 320 | ): Layer.Layer<Client.SqlClient, Config.ConfigError | SqlError> => | ||
| 321 | Layer.effect(Client.SqlClient, Config.unwrap(config).pipe(Effect.flatMap(make))).pipe( | ||
| 322 | Layer.provide(Reactivity.layer), | ||
| 323 | ); | ||
| 324 | |||
| 325 | export const layer = (config: SqliteClientConfig): Layer.Layer<Client.SqlClient, SqlError> => | ||
| 326 | Layer.effect(Client.SqlClient, make(config)).pipe(Layer.provide(Reactivity.layer)); | ||
| 327 | |||
| 328 | export const layerMemory = ( | ||
| 329 | config: SqliteMemoryClientConfig = {}, | ||
| 330 | ): Layer.Layer<Client.SqlClient, SqlError> => | ||
| 331 | Layer.effect(Client.SqlClient, makeMemory(config)).pipe(Layer.provide(Reactivity.layer)); | ||