Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@effect/platform-node": "4.1.0",
"@effect/platform-node-shared": "4.1.0",
"@effect/sql-sqlite-bun": "4.1.0",
"@effect/sql-sqlite-node": "4.1.0",
"@npmcli/arborist": "9.4.0",
"@types/bun": "1.4.3",
"@types/cross-spawn": "6.0.6",
Expand Down
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@
"@ai-sdk/openai-compatible": "2.0.41",
"@anthropic-ai/claude-agent-sdk": "0.3.301",
"@effect/platform-node": "catalog:",
"@effect/sql-sqlite-bun": "catalog:",
"@opencode/http-recorder": "workspace:*",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.1",
Expand All @@ -98,6 +97,8 @@
"dependencies": {
"@ai-sdk/provider": "3.0.19",
"@ai-sdk/provider-utils": "4.0.62",
"@effect/sql-sqlite-bun": "catalog:",
"@effect/sql-sqlite-node": "catalog:",
"@ff-labs/fff-bun": "0.10.5",
"@ff-labs/fff-node": "0.10.5",
"@lydell/node-pty": "catalog:",
Expand Down
104 changes: 7 additions & 97 deletions packages/core/src/database/sqlite.bun.ts
Original file line number Diff line number Diff line change
@@ -1,103 +1,13 @@
import { Database, type SQLQueryBindings } from "bun:sqlite"
import { Context, Effect, Layer } from "effect"
import { Reactivity } from "effect/reactivity"
import { SqlClient } from "effect/sql"
import { classifySqliteError, SqlError } from "effect/sql/SqlError"
import { Sqlite } from "./sqlite.js"

const TypeId = "~@opencode/core/database/SqliteBun" as const
import { SqliteClient } from "@effect/sql-sqlite-bun"
import { Layer } from "effect"
import type { SqlClient } from "effect/sql"

export const supportsTuningPragmas = true

// Foreign keys default OFF and can be toggled per connection.
export const supportsForeignKeyToggle = true

interface Config extends Sqlite.ClientConfig {
readonly filename: string
readonly readonly?: boolean
readonly create?: boolean
readonly readwrite?: boolean
readonly disableWAL?: boolean
}

const make = (options: Config) =>
Effect.gen(function* () {
const native = (yield* Sqlite.Native) as Database

const run = (query: string, params: ReadonlyArray<unknown> = []) =>
Effect.withFiber<Array<Record<string, unknown>>, SqlError>((fiber) => {
const statement = native.query<Record<string, unknown>, SQLQueryBindings[]>(query)
// @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627
statement.safeIntegers(Context.get(fiber.context, SqlClient.SafeIntegers))
try {
return Effect.succeed(statement.all(...(params as SQLQueryBindings[])) ?? [])
} catch (cause) {
return Effect.fail(
new SqlError({
reason: classifySqliteError(cause, { message: "Failed to execute statement", operation: "execute" }),
}),
)
}
})

const runValues = (query: string, params: ReadonlyArray<unknown> = []) =>
Effect.withFiber<Array<unknown[]>, SqlError>((fiber) => {
const statement = native.query<unknown, SQLQueryBindings[]>(query)
// @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627
statement.safeIntegers(Context.get(fiber.context, SqlClient.SafeIntegers))
try {
return Effect.succeed(statement.values(...(params as SQLQueryBindings[])) ?? [])
} catch (cause) {
return Effect.fail(
new SqlError({
reason: classifySqliteError(cause, { message: "Failed to execute statement", operation: "execute" }),
}),
)
}
})

const connection = Sqlite.makeConnection(run, runValues, {
export: Effect.try({
try: () => native.serialize(),
catch: (cause) =>
new SqlError({
reason: classifySqliteError(cause, { message: "Failed to export database", operation: "export" }),
}),
}),
loadExtension: (path: string) =>
Effect.try({
try: () => native.loadExtension(path),
catch: (cause) =>
new SqlError({
reason: classifySqliteError(cause, { message: "Failed to load extension", operation: "loadExtension" }),
}),
}),
})

return yield* Sqlite.makeClient(options, connection, TypeId, (acquirer) => ({
export: Effect.flatMap(acquirer, (_) => _.export),
loadExtension: (path: string) => Effect.flatMap(acquirer, (_) => _.loadExtension(path)),
}))
})

const nativeLayer = (config: Config) =>
Layer.effect(
Sqlite.Native,
Effect.gen(function* () {
const native = new Database(config.filename, {
readonly: config.readonly,
readwrite: config.readwrite ?? true,
create: config.create ?? true,
})
yield* Effect.addFinalizer(() => Effect.sync(() => native.close()))
if (config.disableWAL !== true) native.run("PRAGMA journal_mode = WAL;")
return native
}),
)

const clientLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config))

export const sqliteLayer = (config: Config) => {
const native = nativeLayer(config)
return Layer.merge(native, clientLayer(config).pipe(Layer.provide(native))).pipe(Layer.provide(Reactivity.layer))
}
// Opening the database can only fail on a broken file or a lock held past
// busyTimeout; Database.layer treats both as fatal.
export const sqliteLayer = (config: SqliteClient.SqliteClientConfig): Layer.Layer<SqlClient.SqlClient> =>
Layer.orDie(SqliteClient.layer(config))
103 changes: 8 additions & 95 deletions packages/core/src/database/sqlite.node.ts
Original file line number Diff line number Diff line change
@@ -1,100 +1,13 @@
import { DatabaseSync, type SQLInputValue } from "node:sqlite"
import { Context, Effect, Layer } from "effect"
import { Reactivity } from "effect/reactivity"
import { SqlClient } from "effect/sql"
import { classifySqliteError, SqlError } from "effect/sql/SqlError"
import { Sqlite } from "./sqlite.js"

const TypeId = "~@opencode/core/database/SqliteNode" as const
import { SqliteClient } from "@effect/sql-sqlite-node"
import { Layer } from "effect"
import type { SqlClient } from "effect/sql"

export const supportsTuningPragmas = true

// Foreign keys default OFF and can be toggled per connection.
// Foreign keys default ON in node:sqlite and can be toggled per connection.
export const supportsForeignKeyToggle = true

interface Config extends Sqlite.ClientConfig {
readonly filename: string
readonly readonly?: boolean
readonly create?: boolean
readonly readwrite?: boolean
readonly disableWAL?: boolean
readonly timeout?: number
readonly allowExtension?: boolean
}

const make = (options: Config) =>
Effect.gen(function* () {
const native = (yield* Sqlite.Native) as DatabaseSync

const run = (query: string, params: ReadonlyArray<unknown> = []) =>
Effect.withFiber<Array<Record<string, unknown>>, SqlError>((fiber) => {
const statement = native.prepare(query)
statement.setReadBigInts(Context.get(fiber.context, SqlClient.SafeIntegers))
try {
return Effect.succeed(statement.all(...(params as SQLInputValue[])) as Array<Record<string, unknown>>)
} catch (cause) {
return Effect.fail(
new SqlError({
reason: classifySqliteError(cause, { message: "Failed to execute statement", operation: "execute" }),
}),
)
}
})

const runValues = (query: string, params: ReadonlyArray<unknown> = []) =>
Effect.withFiber<ReadonlyArray<ReadonlyArray<unknown>>, SqlError>((fiber) => {
const statement = native.prepare(query)
statement.setReadBigInts(Context.get(fiber.context, SqlClient.SafeIntegers))
statement.setReturnArrays(true)
try {
return Effect.succeed(
statement.all(...(params as SQLInputValue[])) as unknown as ReadonlyArray<ReadonlyArray<unknown>>,
)
} catch (cause) {
return Effect.fail(
new SqlError({
reason: classifySqliteError(cause, { message: "Failed to execute statement", operation: "execute" }),
}),
)
}
})

const connection = Sqlite.makeConnection(run, runValues, {
loadExtension: (path: string) =>
Effect.try({
try: () => native.loadExtension(path),
catch: (cause) =>
new SqlError({
reason: classifySqliteError(cause, { message: "Failed to load extension", operation: "loadExtension" }),
}),
}),
})

return yield* Sqlite.makeClient(options, connection, TypeId, (acquirer) => ({
loadExtension: (path: string) => Effect.flatMap(acquirer, (_) => _.loadExtension(path)),
}))
})

const nativeLayer = (config: Config) =>
Layer.effect(
Sqlite.Native,
Effect.gen(function* () {
const native = new DatabaseSync(config.filename, {
readOnly: config.readonly,
timeout: config.timeout,
allowExtension: config.allowExtension,
enableForeignKeyConstraints: true,
open: true,
})
yield* Effect.addFinalizer(() => Effect.sync(() => native.close()))
if (config.disableWAL !== true && config.readonly !== true) native.exec("PRAGMA journal_mode = WAL;")
return native
}),
)

const clientLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config))

export const sqliteLayer = (config: Config) => {
const native = nativeLayer(config)
return Layer.merge(native, clientLayer(config).pipe(Layer.provide(native))).pipe(Layer.provide(Reactivity.layer))
}
// Opening the database can only fail on a broken file or a lock held past
// busyTimeout; Database.layer treats both as fatal.
export const sqliteLayer = (config: SqliteClient.SqliteClientConfig): Layer.Layer<SqlClient.SqlClient> =>
Layer.orDie(SqliteClient.layer(config))
93 changes: 0 additions & 93 deletions packages/core/src/database/sqlite.ts

This file was deleted.

Loading
Loading