Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'
This commit is contained in:
384
repos/effect/packages/sql/libsql/src/LibsqlClient.ts
Normal file
384
repos/effect/packages/sql/libsql/src/LibsqlClient.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
/**
|
||||
* libSQL adapter for Effect SQL, backed by `@libsql/client`.
|
||||
*
|
||||
* This module provides a {@link LibsqlClient} and the generic SQL client
|
||||
* service for `@libsql/client`. It uses Effect SQL's SQLite compiler, supports
|
||||
* managed SDK clients or caller-owned live clients, classifies libSQL 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 * as Libsql from "@libsql/client"
|
||||
import * as Config from "effect/Config"
|
||||
import * as Context from "effect/Context"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Layer from "effect/Layer"
|
||||
import * as Option from "effect/Option"
|
||||
import * as Redacted from "effect/Redacted"
|
||||
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 `LibsqlClient` values.
|
||||
*
|
||||
* @category type IDs
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const TypeId: TypeId = "~@effect/sql-libsql/LibsqlClient"
|
||||
|
||||
/**
|
||||
* Type-level identifier used to mark `LibsqlClient` values.
|
||||
*
|
||||
* @category type IDs
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export type TypeId = "~@effect/sql-libsql/LibsqlClient"
|
||||
|
||||
/**
|
||||
* libSQL-backed SQL client service, extending `SqlClient` with its runtime type marker and client configuration.
|
||||
*
|
||||
* @category models
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface LibsqlClient extends Client.SqlClient {
|
||||
readonly [TypeId]: TypeId
|
||||
readonly config: LibsqlClientConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Service tag for the libSQL client service.
|
||||
*
|
||||
* **When to use**
|
||||
*
|
||||
* Use to access or provide a libSQL client through the Effect context.
|
||||
*
|
||||
* @category services
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const LibsqlClient = Context.Service<LibsqlClient>("@effect/sql-libsql/LibsqlClient")
|
||||
|
||||
const LibsqlTransaction = Context.Service<readonly [LibsqlConnection, counter: number]>(
|
||||
"@effect/sql-libsql/LibsqlClient/LibsqlTransaction"
|
||||
)
|
||||
|
||||
/**
|
||||
* Configuration for a libSQL client, either by supplying connection options or an existing live libSQL client.
|
||||
*
|
||||
* @category models
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export type LibsqlClientConfig = LibsqlClientConfig.Full | LibsqlClientConfig.Live
|
||||
|
||||
/**
|
||||
* Namespace containing the configuration variants for `LibsqlClient`.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export declare namespace LibsqlClientConfig {
|
||||
/**
|
||||
* Shared libSQL client options for span attributes and query/result name transformations.
|
||||
*
|
||||
* @category models
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface Base {
|
||||
readonly spanAttributes?: Record<string, unknown> | undefined
|
||||
readonly transformResultNames?: ((str: string) => string) | undefined
|
||||
readonly transformQueryNames?: ((str: string) => string) | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection-based libSQL configuration used to create a managed client, including URL, credentials, sync, integer mode, TLS, and concurrency options.
|
||||
*
|
||||
* @category models
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface Full extends Base {
|
||||
/**
|
||||
* The database URL.
|
||||
*
|
||||
* **Details**
|
||||
*
|
||||
* The client supports `libsql:`, `http:`/`https:`, `ws:`/`wss:` and `file:` URL. For more infomation,
|
||||
* please refer to the project README:
|
||||
*
|
||||
* https://github.com/libsql/libsql-client-ts#supported-urls
|
||||
*/
|
||||
readonly url: string | URL
|
||||
/** Authentication token for the database. */
|
||||
readonly authToken?: Redacted.Redacted | undefined
|
||||
/** Encryption key for the database. */
|
||||
readonly encryptionKey?: Redacted.Redacted | undefined
|
||||
/** URL of a remote server to synchronize database with. */
|
||||
readonly syncUrl?: string | URL | undefined
|
||||
/** Sync interval in seconds. */
|
||||
readonly syncInterval?: number | undefined
|
||||
/**
|
||||
* Enables or disables TLS for `libsql:` URLs.
|
||||
*
|
||||
* **Details**
|
||||
*
|
||||
* By default, `libsql:` URLs use TLS. You can set this option to `false` to disable TLS.
|
||||
*/
|
||||
readonly tls?: boolean | undefined
|
||||
/**
|
||||
* How to convert SQLite integers to JavaScript values.
|
||||
*
|
||||
* **Details**
|
||||
*
|
||||
* - `"number"` (default): returns SQLite integers as JavaScript `number`-s (double precision floats).
|
||||
* `number` cannot precisely represent integers larger than 2^53-1 in absolute value, so attempting to read
|
||||
* larger integers will throw a `RangeError`.
|
||||
* - `"bigint"`: returns SQLite integers as JavaScript `bigint`-s (arbitrary precision integers). Bigints can
|
||||
* precisely represent all SQLite integers.
|
||||
* - `"string"`: returns SQLite integers as strings.
|
||||
*/
|
||||
readonly intMode?: "number" | "bigint" | "string" | undefined
|
||||
/**
|
||||
* Concurrency limit.
|
||||
*
|
||||
* **Details**
|
||||
*
|
||||
* By default, the client performs up to 20 concurrent requests. You can set this option to a higher
|
||||
* number to increase the concurrency limit or set it to 0 to disable concurrency limits completely.
|
||||
*/
|
||||
readonly concurrency?: number | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration that uses an existing libSQL client. 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: Libsql.Client
|
||||
}
|
||||
}
|
||||
|
||||
interface LibsqlConnection extends Connection {
|
||||
readonly beginTransaction: Effect.Effect<LibsqlConnection, SqlError>
|
||||
readonly commit: Effect.Effect<void, SqlError>
|
||||
readonly rollback: Effect.Effect<void, SqlError>
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a scoped libSQL SQL client with transaction support. When given connection options it creates and closes the SDK client; when given `liveClient`, the caller retains ownership.
|
||||
*
|
||||
* @category constructors
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const make = (
|
||||
options: LibsqlClientConfig
|
||||
): Effect.Effect<LibsqlClient, never, Scope.Scope | Reactivity.Reactivity> =>
|
||||
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"]
|
||||
]
|
||||
|
||||
class LibsqlConnectionImpl implements LibsqlConnection {
|
||||
readonly sdk: Libsql.Client | Libsql.Transaction
|
||||
constructor(sdk: Libsql.Client | Libsql.Transaction) {
|
||||
this.sdk = sdk
|
||||
}
|
||||
|
||||
run(
|
||||
sql: string,
|
||||
params: ReadonlyArray<unknown> = []
|
||||
) {
|
||||
return Effect.map(
|
||||
Effect.tryPromise({
|
||||
try: () => this.sdk.execute({ sql, args: params as Array<any> }),
|
||||
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
|
||||
}),
|
||||
(results) => results.rows
|
||||
)
|
||||
}
|
||||
|
||||
runRaw(
|
||||
sql: string,
|
||||
params: ReadonlyArray<unknown> = []
|
||||
) {
|
||||
return Effect.tryPromise({
|
||||
try: () => this.sdk.execute({ sql, args: params as Array<any> }),
|
||||
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
|
||||
})
|
||||
}
|
||||
|
||||
execute(
|
||||
sql: string,
|
||||
params: ReadonlyArray<unknown>,
|
||||
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
||||
) {
|
||||
return transformRows
|
||||
? Effect.map(this.run(sql, params), transformRows)
|
||||
: this.run(sql, params)
|
||||
}
|
||||
executeRaw(sql: string, params: ReadonlyArray<unknown>) {
|
||||
return this.runRaw(sql, params)
|
||||
}
|
||||
executeValues(sql: string, params: ReadonlyArray<unknown>) {
|
||||
return Effect.map(this.run(sql, params), (rows) => rows.map((row) => Array.from(row) as Array<any>))
|
||||
}
|
||||
executeValuesUnprepared(sql: string, params: ReadonlyArray<unknown>) {
|
||||
return this.executeValues(sql, params)
|
||||
}
|
||||
executeUnprepared(
|
||||
sql: string,
|
||||
params: ReadonlyArray<unknown>,
|
||||
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
||||
) {
|
||||
return this.execute(sql, params, transformRows)
|
||||
}
|
||||
executeStream() {
|
||||
return Stream.die("executeStream not implemented")
|
||||
}
|
||||
get beginTransaction() {
|
||||
return Effect.map(
|
||||
Effect.tryPromise({
|
||||
try: () => (this.sdk as Libsql.Client).transaction("write"),
|
||||
catch: (cause) =>
|
||||
new SqlError({ reason: classifyError(cause, "Failed to begin transaction", "beginTransaction") })
|
||||
}),
|
||||
(tx) => new LibsqlConnectionImpl(tx)
|
||||
)
|
||||
}
|
||||
get commit() {
|
||||
return Effect.tryPromise({
|
||||
try: () => (this.sdk as Libsql.Transaction).commit(),
|
||||
catch: (cause) =>
|
||||
new SqlError({ reason: classifyError(cause, "Failed to commit transaction", "commitTransaction") })
|
||||
})
|
||||
}
|
||||
get rollback() {
|
||||
return Effect.tryPromise({
|
||||
try: () => (this.sdk as Libsql.Transaction).rollback(),
|
||||
catch: (cause) =>
|
||||
new SqlError({ reason: classifyError(cause, "Failed to rollback transaction", "rollbackTransaction") })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const connection = "liveClient" in options
|
||||
? new LibsqlConnectionImpl(options.liveClient)
|
||||
: yield* Effect.map(
|
||||
Effect.acquireRelease(
|
||||
Effect.sync(() =>
|
||||
Libsql.createClient(
|
||||
{
|
||||
...options,
|
||||
authToken: Redacted.isRedacted(options.authToken)
|
||||
? Redacted.value(options.authToken)
|
||||
: options.authToken,
|
||||
encryptionKey: Redacted.isRedacted(options.encryptionKey)
|
||||
? Redacted.value(options.encryptionKey)
|
||||
: options.encryptionKey,
|
||||
url: options.url.toString(),
|
||||
syncUrl: options.syncUrl?.toString()
|
||||
} as Libsql.Config
|
||||
)
|
||||
),
|
||||
(sdk) => Effect.sync(() => sdk.close())
|
||||
),
|
||||
(sdk) => new LibsqlConnectionImpl(sdk)
|
||||
)
|
||||
const semaphore = yield* Semaphore.make(1)
|
||||
|
||||
const withTransaction = Client.makeWithTransaction({
|
||||
transactionService: LibsqlTransaction,
|
||||
spanAttributes,
|
||||
acquireConnection: Effect.uninterruptibleMask(Effect.fnUntraced(function*(restore) {
|
||||
const scope = Scope.makeUnsafe()
|
||||
yield* restore(semaphore.take(1))
|
||||
yield* Scope.addFinalizer(scope, semaphore.release(1))
|
||||
const conn = yield* connection.beginTransaction
|
||||
return [scope, conn] as const
|
||||
})),
|
||||
begin: () => Effect.void, // already begun in acquireConnection
|
||||
savepoint: (conn, id) => conn.executeRaw(`SAVEPOINT effect_sql_${id};`, []),
|
||||
commit: (conn) => conn.commit,
|
||||
rollback: (conn) => conn.rollback,
|
||||
rollbackSavepoint: (conn, id) => conn.executeRaw(`ROLLBACK TO SAVEPOINT effect_sql_${id};`, [])
|
||||
})
|
||||
|
||||
const acquirer = Effect.flatMap(
|
||||
Effect.serviceOption(LibsqlTransaction),
|
||||
Option.match({
|
||||
onNone: () => semaphore.withPermits(1)(Effect.succeed(connection as LibsqlConnection)),
|
||||
onSome: ([conn]) => Effect.succeed(conn)
|
||||
})
|
||||
)
|
||||
|
||||
return Object.assign(
|
||||
yield* Client.make({
|
||||
acquirer,
|
||||
compiler,
|
||||
spanAttributes,
|
||||
transformRows
|
||||
}),
|
||||
{
|
||||
[TypeId]: TypeId as TypeId,
|
||||
config: options,
|
||||
withTransaction,
|
||||
sdk: connection.sdk
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Creates a layer from a `Config`-wrapped libSQL client configuration, providing both `LibsqlClient` and `SqlClient`.
|
||||
*
|
||||
* @category layers
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const layerConfig: (
|
||||
config: Config.Wrap<LibsqlClientConfig>
|
||||
) => Layer.Layer<LibsqlClient | Client.SqlClient, Config.ConfigError> = (
|
||||
config: Config.Wrap<LibsqlClientConfig>
|
||||
): Layer.Layer<LibsqlClient | Client.SqlClient, Config.ConfigError> =>
|
||||
Layer.effectContext(
|
||||
Config.unwrap(config).pipe(
|
||||
Effect.flatMap(make),
|
||||
Effect.map((client) =>
|
||||
Context.make(LibsqlClient, client).pipe(
|
||||
Context.add(Client.SqlClient, client)
|
||||
)
|
||||
)
|
||||
)
|
||||
).pipe(Layer.provide(Reactivity.layer))
|
||||
|
||||
/**
|
||||
* Creates a layer from a concrete libSQL client configuration, providing both `LibsqlClient` and `SqlClient`.
|
||||
*
|
||||
* @category layers
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const layer = (
|
||||
config: LibsqlClientConfig
|
||||
): Layer.Layer<LibsqlClient | Client.SqlClient> =>
|
||||
Layer.effectContext(
|
||||
Effect.map(make(config), (client) =>
|
||||
Context.make(LibsqlClient, client).pipe(
|
||||
Context.add(Client.SqlClient, client)
|
||||
))
|
||||
).pipe(Layer.provide(Reactivity.layer))
|
||||
Reference in New Issue
Block a user