Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'
This commit is contained in:
268
repos/effect/packages/sql/sqlite-bun/src/SqliteClient.ts
Normal file
268
repos/effect/packages/sql/sqlite-bun/src/SqliteClient.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* Connects Effect SQL to SQLite when running on Bun, using `bun:sqlite`.
|
||||
*
|
||||
* This module opens a SQLite database and exposes it as both `SqliteClient` and
|
||||
* the generic Effect SQL client. It serializes access to the database, enables
|
||||
* WAL mode unless disabled, and supports database export and extension loading.
|
||||
* Streaming queries and `updateValues` are not supported by this driver.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
import { Database } from "bun:sqlite"
|
||||
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 Bun `SqliteClient` values.
|
||||
*
|
||||
* @category type IDs
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const TypeId: TypeId = "~@effect/sql-sqlite-bun/SqliteClient"
|
||||
|
||||
/**
|
||||
* Type-level identifier used to mark Bun `SqliteClient` values.
|
||||
*
|
||||
* @category type IDs
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export type TypeId = "~@effect/sql-sqlite-bun/SqliteClient"
|
||||
|
||||
/**
|
||||
* Bun SQLite client service, extending `SqlClient` with database export and extension loading helpers. `updateValues` is not supported.
|
||||
*
|
||||
* @category models
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface SqliteClient extends Client.SqlClient {
|
||||
readonly [TypeId]: TypeId
|
||||
readonly config: SqliteClientConfig
|
||||
readonly export: Effect.Effect<Uint8Array, SqlError>
|
||||
readonly loadExtension: (path: string) => Effect.Effect<void, SqlError>
|
||||
|
||||
/** Not supported in sqlite */
|
||||
readonly updateValues: never
|
||||
}
|
||||
|
||||
/**
|
||||
* Service tag for the Bun SQLite client service.
|
||||
*
|
||||
* **When to use**
|
||||
*
|
||||
* Use to access or provide a Bun SQLite client through the Effect context.
|
||||
*
|
||||
* @category services
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const SqliteClient = Context.Service<SqliteClient>("@effect/sql-sqlite-bun/Client")
|
||||
|
||||
/**
|
||||
* Configuration for a Bun SQLite client, including filename, open mode flags, WAL behavior, span attributes, and query/result name transforms.
|
||||
*
|
||||
* @category models
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface SqliteClientConfig {
|
||||
readonly filename: string
|
||||
readonly readonly?: boolean | undefined
|
||||
readonly create?: boolean | undefined
|
||||
readonly readwrite?: boolean | undefined
|
||||
readonly disableWAL?: boolean | undefined
|
||||
|
||||
readonly spanAttributes?: Record<string, unknown> | undefined
|
||||
|
||||
readonly transformResultNames?: ((str: string) => string) | undefined
|
||||
readonly transformQueryNames?: ((str: string) => string) | undefined
|
||||
}
|
||||
|
||||
interface SqliteConnection extends Connection {
|
||||
readonly export: Effect.Effect<Uint8Array, SqlError>
|
||||
readonly loadExtension: (path: string) => Effect.Effect<void, SqlError>
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a scoped Bun SQLite client for a database file, enabling WAL by default and serializing access. Streaming queries are not implemented.
|
||||
*
|
||||
* @category constructors
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const make = (
|
||||
options: SqliteClientConfig
|
||||
): Effect.Effect<SqliteClient, 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 makeConnection = Effect.gen(function*() {
|
||||
const db = new Database(options.filename, {
|
||||
readonly: options.readonly,
|
||||
readwrite: options.readwrite ?? true,
|
||||
create: options.create ?? true
|
||||
} as any)
|
||||
yield* Effect.addFinalizer(() => Effect.sync(() => db.close()))
|
||||
|
||||
if (options.disableWAL !== true) {
|
||||
db.run("PRAGMA journal_mode = WAL;")
|
||||
}
|
||||
|
||||
const prepare = (sql: string, useSafeIntegers: boolean) => {
|
||||
const statement = db.query(sql)
|
||||
// @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627
|
||||
statement.safeIntegers(useSafeIntegers)
|
||||
return statement
|
||||
}
|
||||
|
||||
const run = (
|
||||
sql: string,
|
||||
params: ReadonlyArray<unknown> = []
|
||||
) =>
|
||||
Effect.withFiber<Array<any>, SqlError>((fiber) => {
|
||||
const useSafeIntegers = Context.get(fiber.context, Client.SafeIntegers)
|
||||
try {
|
||||
return Effect.succeed((prepare(sql, useSafeIntegers).all(...(params as any)) ?? []) as Array<any>)
|
||||
} catch (cause) {
|
||||
return Effect.fail(new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") }))
|
||||
}
|
||||
})
|
||||
|
||||
const runValues = (
|
||||
sql: string,
|
||||
params: ReadonlyArray<unknown> = []
|
||||
) =>
|
||||
Effect.withFiber<Array<any>, SqlError>((fiber) => {
|
||||
const useSafeIntegers = Context.get(fiber.context, Client.SafeIntegers)
|
||||
try {
|
||||
return Effect.succeed((prepare(sql, useSafeIntegers).values(...(params as any)) ?? []) as Array<any>)
|
||||
} catch (cause) {
|
||||
return Effect.fail(
|
||||
new SqlError({ reason: classifyError(cause, "Failed to execute statement", "executeValues") })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return identity<SqliteConnection>({
|
||||
execute(sql, params, transformRows) {
|
||||
return transformRows
|
||||
? Effect.map(run(sql, params), transformRows)
|
||||
: run(sql, params)
|
||||
},
|
||||
executeRaw(sql, params) {
|
||||
return run(sql, params)
|
||||
},
|
||||
executeValues(sql, params) {
|
||||
return runValues(sql, params)
|
||||
},
|
||||
executeValuesUnprepared(sql, params) {
|
||||
return runValues(sql, params)
|
||||
},
|
||||
executeUnprepared(sql, params, transformRows) {
|
||||
return this.execute(sql, params, transformRows)
|
||||
},
|
||||
executeStream(_sql, _params) {
|
||||
return Stream.die("executeStream not implemented")
|
||||
},
|
||||
export: Effect.try({
|
||||
try: () => db.serialize(),
|
||||
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to export database", "export") })
|
||||
}),
|
||||
loadExtension: (path) =>
|
||||
Effect.try({
|
||||
try: () => db.loadExtension(path),
|
||||
catch: (cause) =>
|
||||
new SqlError({ reason: classifyError(cause, "Failed to load extension", "loadExtension") })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const semaphore = yield* Semaphore.make(1)
|
||||
const connection = yield* makeConnection
|
||||
|
||||
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: [
|
||||
...(options.spanAttributes ? Object.entries(options.spanAttributes) : []),
|
||||
[ATTR_DB_SYSTEM_NAME, "sqlite"]
|
||||
],
|
||||
transformRows
|
||||
})) as SqliteClient,
|
||||
{
|
||||
[TypeId]: TypeId as TypeId,
|
||||
config: options,
|
||||
export: Effect.flatMap(acquirer, (_) => _.export),
|
||||
loadExtension: (path: string) => Effect.flatMap(acquirer, (_) => _.loadExtension(path))
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Creates a layer from a `Config`-wrapped Bun SQLite client configuration, providing both `SqliteClient` and `SqlClient`.
|
||||
*
|
||||
* @category layers
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const layerConfig = (
|
||||
config: Config.Wrap<SqliteClientConfig>
|
||||
): Layer.Layer<SqliteClient | Client.SqlClient, Config.ConfigError> =>
|
||||
Layer.effectContext(
|
||||
Config.unwrap(config).pipe(
|
||||
Effect.flatMap(make),
|
||||
Effect.map((client) =>
|
||||
Context.make(SqliteClient, client).pipe(
|
||||
Context.add(Client.SqlClient, client)
|
||||
)
|
||||
)
|
||||
)
|
||||
).pipe(Layer.provide(Reactivity.layer))
|
||||
|
||||
/**
|
||||
* Creates a layer from a concrete Bun SQLite client configuration, providing both `SqliteClient` and `SqlClient`.
|
||||
*
|
||||
* @category layers
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const layer = (
|
||||
config: SqliteClientConfig
|
||||
): Layer.Layer<SqliteClient | Client.SqlClient> =>
|
||||
Layer.effectContext(
|
||||
Effect.map(make(config), (client) =>
|
||||
Context.make(SqliteClient, client).pipe(
|
||||
Context.add(Client.SqlClient, client)
|
||||
))
|
||||
).pipe(Layer.provide(Reactivity.layer))
|
||||
90
repos/effect/packages/sql/sqlite-bun/src/SqliteMigrator.ts
Normal file
90
repos/effect/packages/sql/sqlite-bun/src/SqliteMigrator.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Runs database migrations for Bun SQLite 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`. It does not add Bun-specific schema dump support; 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 using the configured `SqlClient`, returning the migrations that were applied.
|
||||
*
|
||||
* @category constructors
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const run: <R2 = never>(
|
||||
options: Migrator.MigratorOptions<R2>
|
||||
) => Effect.Effect<
|
||||
ReadonlyArray<readonly [id: number, name: string]>,
|
||||
Migrator.MigrationError | SqlError,
|
||||
Client.SqlClient | R2
|
||||
> = Migrator.make({
|
||||
// dumpSchema(path, table) {
|
||||
// const dump = (args: Array<string>) =>
|
||||
// Effect.gen(function*() {
|
||||
// const sql = yield* SqliteClient
|
||||
// const dump = yield* pipe(
|
||||
// Command.make("sqlite3", (sql as SqliteClient).config.filename, ...args),
|
||||
// Command.string
|
||||
// )
|
||||
// return dump.replace(/^create table sqlite_sequence\(.*$/im, "")
|
||||
// .replace(/\n{2,}/gm, "\n\n")
|
||||
// .trim()
|
||||
// }).pipe(
|
||||
// Effect.mapError((error) => new Migrator.MigrationError({ kind: "Failed", message: error.message }))
|
||||
// )
|
||||
//
|
||||
// const dumpSchema = dump([".schema"])
|
||||
//
|
||||
// const dumpMigrations = dump([
|
||||
// "--cmd",
|
||||
// `.mode insert ${table}`,
|
||||
// `select * from ${table}`
|
||||
// ])
|
||||
//
|
||||
// const dumpAll = Effect.map(
|
||||
// Effect.all([dumpSchema, dumpMigrations], { concurrency: 2 }),
|
||||
// ([schema, migrations]) => schema + "\n\n" + migrations
|
||||
// )
|
||||
//
|
||||
// const dumpFile = (file: string) =>
|
||||
// Effect.gen(function*() {
|
||||
// const fs = yield* FileSystem
|
||||
// const path = yield* Path
|
||||
// const dump = yield* dumpAll
|
||||
// yield* fs.makeDirectory(path.dirname(file), { recursive: true })
|
||||
// yield* fs.writeFileString(file, dump)
|
||||
// }).pipe(
|
||||
// Effect.mapError((error) => new Migrator.MigrationError({ kind: "Failed", message: error.message }))
|
||||
// )
|
||||
//
|
||||
// return dumpFile(path)
|
||||
// }
|
||||
})
|
||||
|
||||
/**
|
||||
* Creates a layer that runs the configured SQL migrations during layer construction.
|
||||
*
|
||||
* @category constructors
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const layer = <R>(
|
||||
options: Migrator.MigratorOptions<R>
|
||||
): Layer.Layer<
|
||||
never,
|
||||
SqlError | Migrator.MigrationError,
|
||||
Client.SqlClient | R
|
||||
> => Layer.effectDiscard(run(options))
|
||||
15
repos/effect/packages/sql/sqlite-bun/src/index.ts
Normal file
15
repos/effect/packages/sql/sqlite-bun/src/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
|
||||
// @barrel: Auto-generated exports. Do not edit manually.
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export * as SqliteClient from "./SqliteClient.ts"
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export * as SqliteMigrator from "./SqliteMigrator.ts"
|
||||
Reference in New Issue
Block a user