| 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 | 6 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 7 | 7 | |
| 8 | 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 | 10 | import { runMigrateDevDb } from "./migrate-dev-db.ts"; |
| 11 | 11 | |
| 12 | 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 | 39 | import { Command, Flag } from "effect/unstable/cli"; |
| 40 | 40 | |
| 41 | 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 | 44 | export class MigrateDevDbNotInWorktreeError extends Schema.TaggedErrorClass<MigrateDevDbNotInWorktreeError>()( |
| 45 | 45 | "MigrateDevDbNotInWorktreeError", |
apps/server/scripts/t3-sqlite-state.test.ts+1-1| ... | ... | @@ -5,7 +5,7 @@ import * as FileSystem from "effect/FileSystem"; |
| 5 | 5 | import * as Path from "effect/Path"; |
| 6 | 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 | 9 | import { runSqliteState } from "./t3-sqlite-state.ts"; |
| 10 | 10 | |
| 11 | 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 | 15 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 16 | 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 | 20 | export const SqliteStateOperation = Schema.Literals(["query", "exec"]); |
| 21 | 21 | export type SqliteStateOperation = typeof SqliteStateOperation.Type; |
apps/server/src/persistence/Layers/Sqlite.ts+1-1| ... | ... | @@ -18,7 +18,7 @@ type Loader = { |
| 18 | 18 | }; |
| 19 | 19 | const defaultSqliteClientLoaders = { |
| 20 | 20 | bun: () => import("@effect/sql-sqlite-bun/SqliteClient"), |
| 21 | node: () => import("../NodeSqliteClient.ts"), | |
| 21 | node: () => import("@t3tools/shared/nodeSqliteClient"), | |
| 22 | 22 | } satisfies Record<string, () => Promise<Loader>>; |
| 23 | 23 | |
| 24 | 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 | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | |
| 6 | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | |
| 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; | |
| 8 | 8 | |
| 9 | 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 | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | |
| 6 | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | |
| 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; | |
| 8 | 8 | |
| 9 | 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 | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | |
| 6 | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | |
| 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; | |
| 8 | 8 | |
| 9 | 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 | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | |
| 6 | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | |
| 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; | |
| 8 | 8 | |
| 9 | 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 | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | |
| 6 | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | |
| 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; | |
| 8 | 8 | |
| 9 | 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 | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | |
| 6 | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | |
| 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; | |
| 8 | 8 | |
| 9 | 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 | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | |
| 6 | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | |
| 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; | |
| 8 | 8 | |
| 9 | 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 | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | |
| 6 | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | |
| 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; | |
| 8 | 8 | |
| 9 | 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 | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | |
| 6 | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | |
| 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; | |
| 8 | 8 | |
| 9 | 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 | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | |
| 6 | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | |
| 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; | |
| 8 | 8 | |
| 9 | 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 | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | |
| 6 | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | |
| 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; | |
| 8 | 8 | |
| 9 | 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 | 4 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 5 | 5 | |
| 6 | 6 | import { runMigrations } from "../Migrations.ts"; |
| 7 | import * as NodeSqliteClient from "../NodeSqliteClient.ts"; | |
| 7 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; | |
| 8 | 8 | |
| 9 | 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 | 246 | "./claudeCompaction": { |
| 247 | 247 | "types": "./src/claudeCompaction.ts", |
| 248 | 248 | "import": "./src/claudeCompaction.ts" |
| 249 | }, | |
| 250 | "./nodeSqliteClient": { | |
| 251 | "types": "./src/nodeSqliteClient.ts", | |
| 252 | "import": "./src/nodeSqliteClient.ts" | |
| 249 | 253 | } |
| 250 | 254 | }, |
| 251 | 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)); |