From 4f70e21c3ec3a4db0dead6713b921c37a2308e46 Mon Sep 17 00:00:00 2001 From: Taras Kornichuk Date: Wed, 22 Jul 2026 17:16:25 +0300 Subject: [PATCH 1/2] feat(sql-tursodatabase): add @effect/sql-tursodatabase driver for the Rust Turso engine --- .changeset/add-sql-tursodatabase.md | 7 + packages/sql/tursodatabase/LICENSE | 21 ++ packages/sql/tursodatabase/README.md | 7 + packages/sql/tursodatabase/docgen.json | 23 ++ packages/sql/tursodatabase/package.json | 72 +++++ packages/sql/tursodatabase/src/TursoClient.ts | 300 ++++++++++++++++++ .../sql/tursodatabase/src/TursoMigrator.ts | 47 +++ packages/sql/tursodatabase/src/index.ts | 15 + .../sql/tursodatabase/test/Client.test.ts | 95 ++++++ packages/sql/tursodatabase/tsconfig.json | 8 + packages/sql/tursodatabase/vitest.config.ts | 6 + pnpm-lock.yaml | 76 ++++- tsconfig.json | 2 + tsconfig.packages.json | 1 + 14 files changed, 679 insertions(+), 1 deletion(-) create mode 100644 .changeset/add-sql-tursodatabase.md create mode 100644 packages/sql/tursodatabase/LICENSE create mode 100644 packages/sql/tursodatabase/README.md create mode 100644 packages/sql/tursodatabase/docgen.json create mode 100644 packages/sql/tursodatabase/package.json create mode 100644 packages/sql/tursodatabase/src/TursoClient.ts create mode 100644 packages/sql/tursodatabase/src/TursoMigrator.ts create mode 100644 packages/sql/tursodatabase/src/index.ts create mode 100644 packages/sql/tursodatabase/test/Client.test.ts create mode 100644 packages/sql/tursodatabase/tsconfig.json create mode 100644 packages/sql/tursodatabase/vitest.config.ts diff --git a/.changeset/add-sql-tursodatabase.md b/.changeset/add-sql-tursodatabase.md new file mode 100644 index 00000000000..0610ccbfae3 --- /dev/null +++ b/.changeset/add-sql-tursodatabase.md @@ -0,0 +1,7 @@ +--- +"@effect/sql-tursodatabase": minor +--- + +Add `@effect/sql-tursodatabase`, an `@effect/sql` driver for the in-process Rust Turso engine (`@tursodatabase/database`). + +Provides `TursoClient` (a `SqlClient` exposing queries as `Effect`, the connection as a composable `Layer`, and typed `SqlError`s via Effect SQL's SQLite error classification) alongside `TursoMigrator`. Mirrors the existing `@effect/sql-libsql` and `@effect/sql-sqlite-node` drivers, including transaction support with savepoints and `SafeIntegers`. Streaming queries are not implemented. diff --git a/packages/sql/tursodatabase/LICENSE b/packages/sql/tursodatabase/LICENSE new file mode 100644 index 00000000000..8967b4b1768 --- /dev/null +++ b/packages/sql/tursodatabase/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024-present The Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/sql/tursodatabase/README.md b/packages/sql/tursodatabase/README.md new file mode 100644 index 00000000000..13a1e9c52ef --- /dev/null +++ b/packages/sql/tursodatabase/README.md @@ -0,0 +1,7 @@ +# `@effect/sql-tursodatabase` + +An `@effect/sql` implementation using the in-process Rust Turso engine (`@tursodatabase/database`). + +## Documentation + +- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/sql-tursodatabase). diff --git a/packages/sql/tursodatabase/docgen.json b/packages/sql/tursodatabase/docgen.json new file mode 100644 index 00000000000..06ce70035d8 --- /dev/null +++ b/packages/sql/tursodatabase/docgen.json @@ -0,0 +1,23 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/sql/tursodatabase/src/", + "exclude": ["src/internal/**/*.ts"], + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "rewriteRelativeImportExtensions": true, + "allowImportingTsExtensions": true, + "paths": { + "effect": ["../../../effect/src/index.js"], + "effect/*": ["../../../effect/src/*.js"] + }, + "plugins": [ + { "name": "@effect/language-service", "includeSuggestionsInTsc": false } + ] + } +} diff --git a/packages/sql/tursodatabase/package.json b/packages/sql/tursodatabase/package.json new file mode 100644 index 00000000000..718f51fbc5d --- /dev/null +++ b/packages/sql/tursodatabase/package.json @@ -0,0 +1,72 @@ +{ + "name": "@effect/sql-tursodatabase", + "version": "4.0.0-beta.100", + "type": "module", + "license": "MIT", + "description": "A Turso toolkit for Effect", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/sql/tursodatabase" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "sql", + "turso", + "database" + ], + "keywords": [ + "typescript", + "sql", + "turso", + "database" + ], + "sideEffects": [], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null, + "./*/index": null + }, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + "./package.json": "./package.json", + ".": "./dist/index.js", + "./*": "./dist/*.js", + "./internal/*": null, + "./*/index": null + } + }, + "scripts": { + "codegen": "effect-utils codegen", + "build": "tsc -b tsconfig.json && pnpm babel", + "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "devDependencies": { + "effect": "workspace:^" + }, + "peerDependencies": { + "effect": "workspace:^" + }, + "dependencies": { + "@tursodatabase/database": "^0.6.1", + "@tursodatabase/database-common": "^0.6.1" + } +} diff --git a/packages/sql/tursodatabase/src/TursoClient.ts b/packages/sql/tursodatabase/src/TursoClient.ts new file mode 100644 index 00000000000..90a5d63020b --- /dev/null +++ b/packages/sql/tursodatabase/src/TursoClient.ts @@ -0,0 +1,300 @@ +/** + * Turso adapter for Effect SQL, backed by `@tursodatabase/database`. + * + * This module provides a {@link TursoClient} and the generic SQL client service + * for the in-process Rust Turso engine. It uses Effect SQL's SQLite compiler, + * supports a managed engine connection or a caller-owned live connection, + * classifies Turso and SQLite failures as `SqlError`s, and provides transaction + * support with savepoints. Streaming queries are not implemented by this driver. + * + * @since 4.0.0 + */ +import { connect } from "@tursodatabase/database" +import type { DatabaseOpts, DatabasePromise, StatementPromise } from "@tursodatabase/database-common" +import * as Config from "effect/Config" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Scope from "effect/Scope" +import * as Semaphore from "effect/Semaphore" +import * as Stream from "effect/Stream" +import * as Reactivity from "effect/unstable/reactivity/Reactivity" +import * as Client from "effect/unstable/sql/SqlClient" +import type { Connection } from "effect/unstable/sql/SqlConnection" +import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError" +import * as Statement from "effect/unstable/sql/Statement" + +const ATTR_DB_SYSTEM_NAME = "db.system.name" + +const classifyError = (cause: unknown, message: string, operation: string) => + classifySqliteError(cause, { message, operation }) + +/** + * Runtime type identifier used to mark `TursoClient` values. + * + * @category type IDs + * @since 4.0.0 + */ +export const TypeId: TypeId = "~@effect/sql-tursodatabase/TursoClient" + +/** + * Type-level identifier used to mark `TursoClient` values. + * + * @category type IDs + * @since 4.0.0 + */ +export type TypeId = "~@effect/sql-tursodatabase/TursoClient" + +/** + * Turso-backed SQL client service, extending `SqlClient` with its runtime type marker and client configuration. + * + * @category models + * @since 4.0.0 + */ +export interface TursoClient extends Client.SqlClient { + readonly [TypeId]: TypeId + readonly config: TursoClientConfig + readonly sdk: DatabasePromise +} + +/** + * Service tag for the Turso client service. + * + * **When to use** + * + * Use to access or provide a Turso client through the Effect context. + * + * @category services + * @since 4.0.0 + */ +export const TursoClient = Context.Service("@effect/sql-tursodatabase/TursoClient") + +/** + * Configuration for a Turso client, either by supplying connection options or an existing live connection. + * + * @category models + * @since 4.0.0 + */ +export type TursoClientConfig = TursoClientConfig.Full | TursoClientConfig.Live + +/** + * Namespace containing the configuration variants for `TursoClient`. + * + * @since 4.0.0 + */ +export declare namespace TursoClientConfig { + /** + * Shared Turso client options for span attributes and query/result name transformations. + * + * @category models + * @since 4.0.0 + */ + export interface Base { + readonly spanAttributes?: Record | undefined + readonly transformResultNames?: ((str: string) => string) | undefined + readonly transformQueryNames?: ((str: string) => string) | undefined + } + + /** + * Connection-based Turso configuration used to open a managed engine connection. + * + * @category models + * @since 4.0.0 + */ + export interface Full extends Base, DatabaseOpts { + /** + * The database path. + * + * **Details** + * + * The in-process Turso engine opens a local database file by path, or an + * in-memory database with `":memory:"`. Unlike libSQL, the engine does not + * accept `file:`/`libsql:`/`http:` URLs — pass a filesystem path. + */ + readonly url: string | URL + } + + /** + * Configuration that uses an existing Turso connection. The supplied `liveClient` is caller-owned and is not closed by the Effect client. + * + * @category models + * @since 4.0.0 + */ + export interface Live extends Base { + readonly liveClient: DatabasePromise + } +} + +/** + * Creates a scoped Turso SQL client with transaction support. When given connection options it opens and closes the engine connection; when given `liveClient`, the caller retains ownership. + * + * @category constructors + * @since 4.0.0 + */ +export const make = ( + options: TursoClientConfig +): Effect.Effect => + Effect.gen(function*() { + const compiler = Statement.makeCompilerSqlite(options.transformQueryNames) + const transformRows = options.transformResultNames ? + Statement.defaultTransforms( + options.transformResultNames + ).array : + undefined + + const spanAttributes: Array<[string, unknown]> = [ + ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), + [ATTR_DB_SYSTEM_NAME, "sqlite"] + ] + + const db = "liveClient" in options + ? options.liveClient + : yield* Effect.acquireRelease( + Effect.promise(() => { + const { spanAttributes: _s, transformQueryNames: _q, transformResultNames: _r, url, ...opts } = options + return connect(url.toString(), opts as DatabaseOpts) + }), + (db) => Effect.promise(() => db.close()) + ) + + // `@tursodatabase/database` types `prepare` as async, but some engine + // versions return the statement synchronously. Wrapping in an async thunk + // normalizes both shapes for `Effect.tryPromise`. + const prepare = (sql: string) => + Effect.tryPromise({ + try: async () => db.prepare(sql), + catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to prepare statement", "prepare") }) + }) + + const runStatement = ( + statement: StatementPromise, + params: ReadonlyArray, + raw: boolean + ) => + Effect.withFiber, SqlError>((fiber) => { + if (Context.get(fiber.context, Client.SafeIntegers)) { + statement.safeIntegers(true) + } + return Effect.tryPromise({ + try: async () => { + if (statement.reader) { + return await statement.all(...params) + } + const result = await statement.run(...params) + return raw ? result as unknown as ReadonlyArray : [] + }, + catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") }) + }) + }) + + const runValues = ( + statement: StatementPromise, + params: ReadonlyArray + ) => + Effect.tryPromise({ + try: async () => { + if (statement.reader) { + return await statement.raw(true).all(...params) as ReadonlyArray> + } + await statement.run(...params) + return [] + }, + catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") }) + }) + + const run = (sql: string, params: ReadonlyArray, raw = false) => + Effect.flatMap(prepare(sql), (statement) => runStatement(statement, params, raw)) + + const connection = identity({ + execute(sql, params, transformRows) { + return transformRows + ? Effect.map(run(sql, params), transformRows) + : run(sql, params) + }, + executeRaw(sql, params) { + return run(sql, params, true) + }, + executeValues(sql, params) { + return Effect.flatMap(prepare(sql), (statement) => runValues(statement, params)) + }, + executeValuesUnprepared(sql, params) { + return Effect.flatMap(prepare(sql), (statement) => runValues(statement, params)) + }, + executeUnprepared(sql, params, transformRows) { + return transformRows + ? Effect.map(run(sql, params), transformRows) + : run(sql, params) + }, + executeStream(_sql, _params) { + return Stream.die("executeStream not implemented") + } + }) + + const semaphore = yield* Semaphore.make(1) + + const acquirer = semaphore.withPermits(1)(Effect.succeed(connection)) + const transactionAcquirer = Effect.uninterruptibleMask((restore) => { + const fiber = Fiber.getCurrent()! + const scope = Context.getUnsafe(fiber.context, Scope.Scope) + return Effect.as( + Effect.tap( + restore(semaphore.take(1)), + () => Scope.addFinalizer(scope, semaphore.release(1)) + ), + connection + ) + }) + + return Object.assign( + yield* Client.make({ + acquirer, + compiler, + transactionAcquirer, + spanAttributes, + transformRows + }), + { + [TypeId]: TypeId as TypeId, + config: options, + sdk: db + } + ) + }) + +/** + * Creates a layer from a `Config`-wrapped Turso client configuration, providing both `TursoClient` and `SqlClient`. + * + * @category layers + * @since 4.0.0 + */ +export const layerConfig = ( + config: Config.Wrap +): Layer.Layer => + Layer.effectContext( + Config.unwrap(config).pipe( + Effect.flatMap(make), + Effect.map((client) => + Context.make(TursoClient, client).pipe( + Context.add(Client.SqlClient, client) + ) + ) + ) + ).pipe(Layer.provide(Reactivity.layer)) + +/** + * Creates a layer from a concrete Turso client configuration, providing both `TursoClient` and `SqlClient`. + * + * @category layers + * @since 4.0.0 + */ +export const layer = ( + config: TursoClientConfig +): Layer.Layer => + Layer.effectContext( + Effect.map(make(config), (client) => + Context.make(TursoClient, client).pipe( + Context.add(Client.SqlClient, client) + )) + ).pipe(Layer.provide(Reactivity.layer)) diff --git a/packages/sql/tursodatabase/src/TursoMigrator.ts b/packages/sql/tursodatabase/src/TursoMigrator.ts new file mode 100644 index 00000000000..5e8aba9967f --- /dev/null +++ b/packages/sql/tursodatabase/src/TursoMigrator.ts @@ -0,0 +1,47 @@ +/** + * Runs database migrations for Turso projects that use Effect SQL. + * + * This module re-exports the shared migration loaders and errors, then provides + * `run` and `layer` helpers that apply pending migration files with the current + * `SqlClient`. Migration execution is handled by the shared SQL migrator. + * + * @since 4.0.0 + */ +import type * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Migrator from "effect/unstable/sql/Migrator" +import type * as Client from "effect/unstable/sql/SqlClient" +import type { SqlError } from "effect/unstable/sql/SqlError" + +/** + * @since 4.0.0 + */ +export * from "effect/unstable/sql/Migrator" + +/** + * Runs SQL migrations for a Turso database using the shared `Migrator` implementation and the current `SqlClient`. + * + * @category constructors + * @since 4.0.0 + */ +export const run: ( + options: Migrator.MigratorOptions +) => Effect.Effect< + ReadonlyArray, + Migrator.MigrationError | SqlError, + Client.SqlClient | R2 +> = Migrator.make({}) + +/** + * Creates a layer that runs the configured Turso migrations during layer construction and provides no services. + * + * @category constructors + * @since 4.0.0 + */ +export const layer = ( + options: Migrator.MigratorOptions +): Layer.Layer< + never, + Migrator.MigrationError | SqlError, + Client.SqlClient | R +> => Layer.effectDiscard(run(options)) diff --git a/packages/sql/tursodatabase/src/index.ts b/packages/sql/tursodatabase/src/index.ts new file mode 100644 index 00000000000..6339639fc4c --- /dev/null +++ b/packages/sql/tursodatabase/src/index.ts @@ -0,0 +1,15 @@ +/** + * @since 4.0.0 + */ + +// @barrel: Auto-generated exports. Do not edit manually. + +/** + * @since 4.0.0 + */ +export * as TursoClient from "./TursoClient.ts" + +/** + * @since 4.0.0 + */ +export * as TursoMigrator from "./TursoMigrator.ts" diff --git a/packages/sql/tursodatabase/test/Client.test.ts b/packages/sql/tursodatabase/test/Client.test.ts new file mode 100644 index 00000000000..0e4f0c97bcb --- /dev/null +++ b/packages/sql/tursodatabase/test/Client.test.ts @@ -0,0 +1,95 @@ +import { TursoClient } from "@effect/sql-tursodatabase" +import { assert, describe, it } from "@effect/vitest" +import { Effect } from "effect" +import { Reactivity } from "effect/unstable/reactivity" + +const makeClient = TursoClient.make({ + url: ":memory:" +}).pipe(Effect.provide(Reactivity.layer)) + +describe("Client", () => { + it.effect("should work", () => + Effect.gen(function*() { + const sql = yield* makeClient + let response + response = yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` + assert.deepStrictEqual(response, []) + response = yield* sql`INSERT INTO test (name) VALUES ('hello')` + assert.deepStrictEqual(response, []) + response = yield* sql`SELECT * FROM test` + assert.deepStrictEqual(response, [{ id: 1, name: "hello" }]) + response = yield* sql`SELECT * FROM test`.values + assert.deepStrictEqual(response, [[1, "hello"]]) + response = yield* sql`INSERT INTO test (name) VALUES ('world')`.pipe(sql.withTransaction) + assert.deepStrictEqual(response, []) + response = yield* sql`SELECT * FROM test` + assert.deepStrictEqual(response, [ + { id: 1, name: "hello" }, + { id: 2, name: "world" } + ]) + })) + + it.effect("should work with raw", () => + Effect.gen(function*() { + const sql = yield* makeClient + let response + response = yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`.raw + assert.deepStrictEqual(response, { changes: 0, lastInsertRowid: 0 }) + response = yield* sql`INSERT INTO test (name) VALUES ('hello')`.raw + assert.deepStrictEqual(response, { changes: 1, lastInsertRowid: 1 }) + response = yield* sql`SELECT * FROM test`.raw + assert.deepStrictEqual(response, [{ id: 1, name: "hello" }]) + response = yield* sql`INSERT INTO test (name) VALUES ('world')`.raw.pipe(sql.withTransaction) + assert.deepStrictEqual(response, { changes: 1, lastInsertRowid: 2 }) + response = yield* sql`SELECT * FROM test` + assert.deepStrictEqual(response, [ + { id: 1, name: "hello" }, + { id: 2, name: "world" } + ]) + })) + + it.effect("withTransaction", () => + Effect.gen(function*() { + const sql = yield* makeClient + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` + yield* sql.withTransaction(sql`INSERT INTO test (name) VALUES ('hello')`) + const rows = yield* sql`SELECT * FROM test` + assert.deepStrictEqual(rows, [{ id: 1, name: "hello" }]) + })) + + it.effect("withTransaction rollback", () => + Effect.gen(function*() { + const sql = yield* makeClient + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` + yield* sql`INSERT INTO test (name) VALUES ('hello')`.pipe( + Effect.andThen(Effect.fail("boom")), + sql.withTransaction, + Effect.ignore + ) + const rows = yield* sql`SELECT * FROM test` + assert.deepStrictEqual(rows, []) + })) + + it.effect("withTransaction nested", () => + Effect.gen(function*() { + const sql = yield* makeClient + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` + const stmt = sql`INSERT INTO test (name) VALUES ('hello')` + yield* stmt.pipe(Effect.andThen(() => stmt.pipe(sql.withTransaction)), sql.withTransaction) + const rows = yield* sql<{ total_rows: number }>`select count(*) as total_rows FROM test` + assert.deepStrictEqual(rows.at(0)?.total_rows, 2) + })) + + it.effect("withTransaction nested rollback", () => + Effect.gen(function*() { + const sql = yield* makeClient + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` + const stmt = sql`INSERT INTO test (name) VALUES ('hello')` + yield* stmt.pipe( + Effect.andThen(() => stmt.pipe(Effect.andThen(Effect.fail("boom")), sql.withTransaction, Effect.ignore)), + sql.withTransaction + ) + const rows = yield* sql<{ total_rows: number }>`select count(*) as total_rows FROM test` + assert.deepStrictEqual(rows.at(0)?.total_rows, 1) + })) +}) diff --git a/packages/sql/tursodatabase/tsconfig.json b/packages/sql/tursodatabase/tsconfig.json new file mode 100644 index 00000000000..19a2f5dbca4 --- /dev/null +++ b/packages/sql/tursodatabase/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../../effect" } + ] +} diff --git a/packages/sql/tursodatabase/vitest.config.ts b/packages/sql/tursodatabase/vitest.config.ts new file mode 100644 index 00000000000..c8a52c1826e --- /dev/null +++ b/packages/sql/tursodatabase/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../../vitest.shared.ts" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7df085fdc7f..741e57f7c44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -670,6 +670,19 @@ importers: specifier: workspace:^ version: link:../../effect + packages/sql/tursodatabase: + dependencies: + '@tursodatabase/database': + specifier: ^0.6.1 + version: 0.6.1 + '@tursodatabase/database-common': + specifier: ^0.6.1 + version: 0.6.1 + devDependencies: + effect: + specifier: workspace:^ + version: link:../../effect + packages/tools/ai-codegen: dependencies: '@effect/openapi-generator': @@ -2833,6 +2846,32 @@ packages: resolution: {integrity: sha512-w071DSzP94YfN6XiWhOxnLpYT3uqtxJBDYdh6Jdjzt+Ce6DNspJsPQgpC7rbts/B8tEkq0LHoYuIF/O5Jh5rPg==} engines: {node: '>=18'} + '@tursodatabase/database-common@0.6.1': + resolution: {integrity: sha512-fkmkhgSUDg3MXkcziTG9Ime3AY5+YvvDN7VvUDEMNV7MP+XgwE/i5tU6eQP1EKa/zDkdSYPq8gPcmROmKmKL8Q==} + + '@tursodatabase/database-darwin-arm64@0.6.1': + resolution: {integrity: sha512-2/nlqulI/kWlrh7vHMwF5fkuavGRWB83SIhzHZmoHSQovq0a28Z4+WAHerjh9yNs7DTj70U9PIsvAtRdh77l7g==} + cpu: [arm64] + os: [darwin] + + '@tursodatabase/database-linux-arm64-gnu@0.6.1': + resolution: {integrity: sha512-ixooF2xyxV84GYIYPLI5D9VEp+cX3WPBmXRyoCGw7+l9G66eOp8QppGF0dKFYhtp/AJ8YmOp+lemqBoCue9N8Q==} + cpu: [arm64] + os: [linux] + + '@tursodatabase/database-linux-x64-gnu@0.6.1': + resolution: {integrity: sha512-cNZ3FS8XBn4X8zHSzd31nvEeBbR4hCDBGs2P3ViND1Egu5oDZ822a+hWxNxMgMPgUJf/aGBEAk5p+j11fAC/QQ==} + cpu: [x64] + os: [linux] + + '@tursodatabase/database-win32-x64-msvc@0.6.1': + resolution: {integrity: sha512-3CASiW3BbeNT4qryxPKKZECGxaAtfUeM/Bjx1NWIxShCLLtRpnaEUt7WLOT2hz+sqU322H6fmMuaN9sYrcwBxw==} + cpu: [x64] + os: [win32] + + '@tursodatabase/database@0.6.1': + resolution: {integrity: sha512-qg6LO1XHA9kc2s97crg0/nHiKFk3xTzylRuZPYggO0vA4hx2rp8fk0Je2vFGOkfVv+ZmzsXOIb+DBHmdrG+72A==} + '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -6046,6 +6085,11 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} + terser@5.48.0: + resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} + engines: {node: '>=10'} + hasBin: true + terser@5.49.0: resolution: {integrity: sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==} engines: {node: '>=10'} @@ -8485,6 +8529,29 @@ snapshots: '@ts-graphviz/ast': 2.0.7 '@ts-graphviz/common': 2.1.5 + '@tursodatabase/database-common@0.6.1': {} + + '@tursodatabase/database-darwin-arm64@0.6.1': + optional: true + + '@tursodatabase/database-linux-arm64-gnu@0.6.1': + optional: true + + '@tursodatabase/database-linux-x64-gnu@0.6.1': + optional: true + + '@tursodatabase/database-win32-x64-msvc@0.6.1': + optional: true + + '@tursodatabase/database@0.6.1': + dependencies: + '@tursodatabase/database-common': 0.6.1 + optionalDependencies: + '@tursodatabase/database-darwin-arm64': 0.6.1 + '@tursodatabase/database-linux-arm64-gnu': 0.6.1 + '@tursodatabase/database-linux-x64-gnu': 0.6.1 + '@tursodatabase/database-win32-x64-msvc': 0.6.1 + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 @@ -10663,7 +10730,7 @@ snapshots: metro-minify-terser@0.83.7: dependencies: flow-enums-runtime: 0.0.6 - terser: 5.49.0 + terser: 5.48.0 metro-resolver@0.83.7: dependencies: @@ -12055,6 +12122,13 @@ snapshots: term-size@2.2.1: {} + terser@5.48.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.17.0 + commander: 2.20.3 + source-map-support: 0.5.21 + terser@5.49.0: dependencies: '@jridgewell/source-map': 0.3.11 diff --git a/tsconfig.json b/tsconfig.json index b2a9c688e53..cde93484196 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -85,6 +85,8 @@ "@effect/sql-sqlite-react-native/*": ["./packages/sql/sqlite-react-native/src/*.ts"], "@effect/sql-sqlite-wasm": ["./packages/sql/sqlite-wasm/src/index.ts"], "@effect/sql-sqlite-wasm/*": ["./packages/sql/sqlite-wasm/src/*.ts"], + "@effect/sql-tursodatabase": ["./packages/sql/tursodatabase/src/index.ts"], + "@effect/sql-tursodatabase/*": ["./packages/sql/tursodatabase/src/*.ts"], "@effect/ai-codegen": ["./packages/tools/ai-codegen/src/index.ts"], "@effect/ai-codegen/*": ["./packages/tools/ai-codegen/src/*.ts"], "@effect/ai-docgen": ["./packages/tools/ai-docgen/src/index.ts"], diff --git a/tsconfig.packages.json b/tsconfig.packages.json index 229e31251eb..a66f0bb44c3 100644 --- a/tsconfig.packages.json +++ b/tsconfig.packages.json @@ -29,6 +29,7 @@ { "path": "packages/sql/sqlite-node" }, { "path": "packages/sql/sqlite-react-native" }, { "path": "packages/sql/sqlite-wasm" }, + { "path": "packages/sql/tursodatabase" }, { "path": "packages/tools/ai-codegen" }, { "path": "packages/tools/bundle" }, { "path": "packages/tools/openapi-generator" }, From 0eacaa779e13d2ccbbe8c26e5f98ffc1082720a0 Mon Sep 17 00:00:00 2001 From: Taras Kornichuk Date: Wed, 22 Jul 2026 18:43:22 +0300 Subject: [PATCH 2/2] fix(sql-tursodatabase): address review feedback - runValues now honors SafeIntegers like runStatement (.values queries no longer silently lose integer precision); add a regression test - classify connect() failures as SqlError (parity with sqlite-wasm's async in-process open), widening make/layer/layerConfig error channels - @category layers for TursoMigrator.layer, per .patterns/jsdoc.md --- packages/sql/tursodatabase/src/TursoClient.ts | 38 +++++++++++-------- .../sql/tursodatabase/src/TursoMigrator.ts | 2 +- .../sql/tursodatabase/test/Client.test.ts | 10 +++++ 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/packages/sql/tursodatabase/src/TursoClient.ts b/packages/sql/tursodatabase/src/TursoClient.ts index 90a5d63020b..d744210b23a 100644 --- a/packages/sql/tursodatabase/src/TursoClient.ts +++ b/packages/sql/tursodatabase/src/TursoClient.ts @@ -135,7 +135,7 @@ export declare namespace TursoClientConfig { */ export const make = ( options: TursoClientConfig -): Effect.Effect => +): Effect.Effect => Effect.gen(function*() { const compiler = Statement.makeCompilerSqlite(options.transformQueryNames) const transformRows = options.transformResultNames ? @@ -152,9 +152,12 @@ export const make = ( const db = "liveClient" in options ? options.liveClient : yield* Effect.acquireRelease( - Effect.promise(() => { - const { spanAttributes: _s, transformQueryNames: _q, transformResultNames: _r, url, ...opts } = options - return connect(url.toString(), opts as DatabaseOpts) + Effect.tryPromise({ + try: () => { + const { spanAttributes: _s, transformQueryNames: _q, transformResultNames: _r, url, ...opts } = options + return connect(url.toString(), opts as DatabaseOpts) + }, + catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to open database", "openDatabase") }) }), (db) => Effect.promise(() => db.close()) ) @@ -193,15 +196,20 @@ export const make = ( statement: StatementPromise, params: ReadonlyArray ) => - Effect.tryPromise({ - try: async () => { - if (statement.reader) { - return await statement.raw(true).all(...params) as ReadonlyArray> - } - await statement.run(...params) - return [] - }, - catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") }) + Effect.withFiber>, SqlError>((fiber) => { + if (Context.get(fiber.context, Client.SafeIntegers)) { + statement.safeIntegers(true) + } + return Effect.tryPromise({ + try: async () => { + if (statement.reader) { + return await statement.raw(true).all(...params) as ReadonlyArray> + } + await statement.run(...params) + return [] + }, + catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") }) + }) }) const run = (sql: string, params: ReadonlyArray, raw = false) => @@ -271,7 +279,7 @@ export const make = ( */ export const layerConfig = ( config: Config.Wrap -): Layer.Layer => +): Layer.Layer => Layer.effectContext( Config.unwrap(config).pipe( Effect.flatMap(make), @@ -291,7 +299,7 @@ export const layerConfig = ( */ export const layer = ( config: TursoClientConfig -): Layer.Layer => +): Layer.Layer => Layer.effectContext( Effect.map(make(config), (client) => Context.make(TursoClient, client).pipe( diff --git a/packages/sql/tursodatabase/src/TursoMigrator.ts b/packages/sql/tursodatabase/src/TursoMigrator.ts index 5e8aba9967f..b2bbc3d0e9e 100644 --- a/packages/sql/tursodatabase/src/TursoMigrator.ts +++ b/packages/sql/tursodatabase/src/TursoMigrator.ts @@ -35,7 +35,7 @@ export const run: ( /** * Creates a layer that runs the configured Turso migrations during layer construction and provides no services. * - * @category constructors + * @category layers * @since 4.0.0 */ export const layer = ( diff --git a/packages/sql/tursodatabase/test/Client.test.ts b/packages/sql/tursodatabase/test/Client.test.ts index 0e4f0c97bcb..6c1c159205c 100644 --- a/packages/sql/tursodatabase/test/Client.test.ts +++ b/packages/sql/tursodatabase/test/Client.test.ts @@ -2,6 +2,7 @@ import { TursoClient } from "@effect/sql-tursodatabase" import { assert, describe, it } from "@effect/vitest" import { Effect } from "effect" import { Reactivity } from "effect/unstable/reactivity" +import * as SqlClient from "effect/unstable/sql/SqlClient" const makeClient = TursoClient.make({ url: ":memory:" @@ -92,4 +93,13 @@ describe("Client", () => { const rows = yield* sql<{ total_rows: number }>`select count(*) as total_rows FROM test` assert.deepStrictEqual(rows.at(0)?.total_rows, 1) })) + + it.effect("honors SafeIntegers for .values queries", () => + Effect.gen(function*() { + const sql = yield* makeClient + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, big INTEGER)` + yield* sql`INSERT INTO test (big) VALUES (9007199254740993)` + const rows = yield* sql`SELECT big FROM test`.values + assert.deepStrictEqual(rows, [[9007199254740993n]]) + }).pipe(Effect.provideService(SqlClient.SafeIntegers, true))) })