refactor(core): move database schema ownership (#29068)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
parent
6bcb9cb9bb
commit
7f571d36ea
390 changed files with 11106 additions and 9143 deletions
60
packages/core/src/database/database.ts
Normal file
60
packages/core/src/database/database.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
export * as Database from "./database"
|
||||
|
||||
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
||||
import { layer as sqliteLayer } from "#sqlite"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { Global } from "../global"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { isAbsolute, join } from "path"
|
||||
import { DatabaseMigration } from "./migration"
|
||||
import { InstallationChannel } from "../installation/version"
|
||||
|
||||
const makeDatabase = EffectDrizzleSqlite.makeWithDefaults()
|
||||
type DatabaseShape = Effect.Success<typeof makeDatabase>
|
||||
|
||||
export interface Interface {
|
||||
db: DatabaseShape
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/storage/Database") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDatabase
|
||||
|
||||
yield* db.run("PRAGMA journal_mode = WAL")
|
||||
yield* db.run("PRAGMA synchronous = NORMAL")
|
||||
yield* db.run("PRAGMA busy_timeout = 5000")
|
||||
yield* db.run("PRAGMA cache_size = -64000")
|
||||
yield* db.run("PRAGMA foreign_keys = ON")
|
||||
yield* db.run("PRAGMA wal_checkpoint(PASSIVE)")
|
||||
yield* DatabaseMigration.apply(db)
|
||||
|
||||
return { db }
|
||||
}).pipe(Effect.orDie),
|
||||
)
|
||||
|
||||
export function layerFromPath(filename: string) {
|
||||
return layer.pipe(Layer.provide(sqliteLayer({ filename })))
|
||||
}
|
||||
|
||||
export function path() {
|
||||
if (Flag.OPENCODE_DB) {
|
||||
if (Flag.OPENCODE_DB === ":memory:" || isAbsolute(Flag.OPENCODE_DB)) return Flag.OPENCODE_DB
|
||||
return join(Global.Path.data, Flag.OPENCODE_DB)
|
||||
}
|
||||
if (
|
||||
["latest", "beta", "prod"].includes(InstallationChannel) ||
|
||||
process.env.OPENCODE_DISABLE_CHANNEL_DB === "1" ||
|
||||
process.env.OPENCODE_DISABLE_CHANNEL_DB === "true"
|
||||
)
|
||||
return join(Global.Path.data, "opencode.db")
|
||||
return join(Global.Path.data, `opencode-${InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`)
|
||||
}
|
||||
|
||||
export const defaultLayer = Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
return layerFromPath(path())
|
||||
}),
|
||||
).pipe(Layer.provide(Global.defaultLayer))
|
||||
25
packages/core/src/database/migration.gen.ts
Normal file
25
packages/core/src/database/migration.gen.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import type { DatabaseMigration } from "./migration"
|
||||
|
||||
export const migrations = (await Promise.all([
|
||||
import("./migration/20260127222353_familiar_lady_ursula"),
|
||||
import("./migration/20260211171708_add_project_commands"),
|
||||
import("./migration/20260213144116_wakeful_the_professor"),
|
||||
import("./migration/20260225215848_workspace"),
|
||||
import("./migration/20260227213759_add_session_workspace_id"),
|
||||
import("./migration/20260228203230_blue_harpoon"),
|
||||
import("./migration/20260303231226_add_workspace_fields"),
|
||||
import("./migration/20260309230000_move_org_to_state"),
|
||||
import("./migration/20260312043431_session_message_cursor"),
|
||||
import("./migration/20260323234822_events"),
|
||||
import("./migration/20260410174513_workspace-name"),
|
||||
import("./migration/20260413175956_chief_energizer"),
|
||||
import("./migration/20260423070820_add_icon_url_override"),
|
||||
import("./migration/20260427172553_slow_nightmare"),
|
||||
import("./migration/20260428004200_add_session_path"),
|
||||
import("./migration/20260501142318_next_venus"),
|
||||
import("./migration/20260504145000_add_sync_owner"),
|
||||
import("./migration/20260507164347_add_workspace_time"),
|
||||
import("./migration/20260510033149_session_usage"),
|
||||
import("./migration/20260511000411_data_migration_state"),
|
||||
import("./migration/20260530232709_lovely_romulus"),
|
||||
])).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
58
packages/core/src/database/migration.ts
Normal file
58
packages/core/src/database/migration.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
export * as DatabaseMigration from "./migration"
|
||||
|
||||
import { sql } from "drizzle-orm"
|
||||
import { Effect } from "effect"
|
||||
import type { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
||||
import { migrations } from "./migration.gen"
|
||||
|
||||
type Database = EffectDrizzleSqlite.EffectSQLiteDatabase
|
||||
type Transaction = Parameters<Parameters<Database["transaction"]>[0]>[0]
|
||||
|
||||
export type Migration = {
|
||||
id: string
|
||||
up: (tx: Transaction) => Effect.Effect<void, unknown>
|
||||
}
|
||||
|
||||
export function apply(db: Database) {
|
||||
return applyOnly(db, migrations)
|
||||
}
|
||||
|
||||
export function applyOnly(db: Database, input: Migration[]) {
|
||||
return Effect.gen(function* () {
|
||||
yield* db.run(
|
||||
sql`CREATE TABLE IF NOT EXISTS ${sql.identifier("migration")} (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`,
|
||||
)
|
||||
let completed = new Set(
|
||||
(yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
|
||||
)
|
||||
if (completed.size === 0) {
|
||||
// Existing installs used Drizzle's migration journal. Seed the new
|
||||
// journal once so TypeScript migrations don't replay old SQL.
|
||||
if (
|
||||
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${"__drizzle_migrations"}`)
|
||||
) {
|
||||
yield* db.run(sql`
|
||||
INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
|
||||
SELECT name, ${Date.now()}
|
||||
FROM ${sql.identifier("__drizzle_migrations")}
|
||||
WHERE name IS NOT NULL
|
||||
`)
|
||||
completed = new Set(
|
||||
(yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const migration of input) {
|
||||
if (completed.has(migration.id)) continue
|
||||
yield* db.transaction((tx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* migration.up(tx)
|
||||
yield* tx.run(
|
||||
sql`INSERT INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${migration.id}, ${Date.now()})`,
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260127222353_familiar_lady_ursula",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`project\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`worktree\` text NOT NULL,
|
||||
\`vcs\` text,
|
||||
\`name\` text,
|
||||
\`icon_url\` text,
|
||||
\`icon_color\` text,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
\`time_initialized\` integer,
|
||||
\`sandboxes\` text NOT NULL
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`message\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`session_id\` text NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
\`data\` text NOT NULL,
|
||||
CONSTRAINT \`fk_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`part\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`message_id\` text NOT NULL,
|
||||
\`session_id\` text NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
\`data\` text NOT NULL,
|
||||
CONSTRAINT \`fk_part_message_id_message_id_fk\` FOREIGN KEY (\`message_id\`) REFERENCES \`message\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`permission\` (
|
||||
\`project_id\` text PRIMARY KEY,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
\`data\` text NOT NULL,
|
||||
CONSTRAINT \`fk_permission_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`project_id\` text NOT NULL,
|
||||
\`parent_id\` text,
|
||||
\`slug\` text NOT NULL,
|
||||
\`directory\` text NOT NULL,
|
||||
\`title\` text NOT NULL,
|
||||
\`version\` text NOT NULL,
|
||||
\`share_url\` text,
|
||||
\`summary_additions\` integer,
|
||||
\`summary_deletions\` integer,
|
||||
\`summary_files\` integer,
|
||||
\`summary_diffs\` text,
|
||||
\`revert\` text,
|
||||
\`permission\` text,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
\`time_compacting\` integer,
|
||||
\`time_archived\` integer,
|
||||
CONSTRAINT \`fk_session_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`todo\` (
|
||||
\`session_id\` text NOT NULL,
|
||||
\`content\` text NOT NULL,
|
||||
\`status\` text NOT NULL,
|
||||
\`priority\` text NOT NULL,
|
||||
\`position\` integer NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
CONSTRAINT \`todo_pk\` PRIMARY KEY(\`session_id\`, \`position\`),
|
||||
CONSTRAINT \`fk_todo_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_share\` (
|
||||
\`session_id\` text PRIMARY KEY,
|
||||
\`id\` text NOT NULL,
|
||||
\`secret\` text NOT NULL,
|
||||
\`url\` text NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
CONSTRAINT \`fk_session_share_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`CREATE INDEX \`message_session_idx\` ON \`message\` (\`session_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`part_message_idx\` ON \`part\` (\`message_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_parent_idx\` ON \`session\` (\`parent_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`todo_session_idx\` ON \`todo\` (\`session_id\`);`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260211171708_add_project_commands",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`project\` ADD \`commands\` text;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260213144116_wakeful_the_professor",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`control_account\` (
|
||||
\`email\` text NOT NULL,
|
||||
\`url\` text NOT NULL,
|
||||
\`access_token\` text NOT NULL,
|
||||
\`refresh_token\` text NOT NULL,
|
||||
\`token_expiry\` integer,
|
||||
\`active\` integer NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
CONSTRAINT \`control_account_pk\` PRIMARY KEY(\`email\`, \`url\`)
|
||||
);
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260225215848_workspace",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`workspace\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`branch\` text,
|
||||
\`project_id\` text NOT NULL,
|
||||
\`config\` text NOT NULL,
|
||||
CONSTRAINT \`fk_workspace_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260227213759_add_session_workspace_id",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`workspace_id\` text;`)
|
||||
yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260228203230_blue_harpoon",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`account\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`email\` text NOT NULL,
|
||||
\`url\` text NOT NULL,
|
||||
\`access_token\` text NOT NULL,
|
||||
\`refresh_token\` text NOT NULL,
|
||||
\`token_expiry\` integer,
|
||||
\`selected_org_id\` text,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`account_state\` (
|
||||
\`id\` integer PRIMARY KEY NOT NULL,
|
||||
\`active_account_id\` text,
|
||||
FOREIGN KEY (\`active_account_id\`) REFERENCES \`account\`(\`id\`) ON UPDATE no action ON DELETE set null
|
||||
);
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260303231226_add_workspace_fields",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`workspace\` ADD \`type\` text NOT NULL;`)
|
||||
yield* tx.run(`ALTER TABLE \`workspace\` ADD \`name\` text;`)
|
||||
yield* tx.run(`ALTER TABLE \`workspace\` ADD \`directory\` text;`)
|
||||
yield* tx.run(`ALTER TABLE \`workspace\` ADD \`extra\` text;`)
|
||||
yield* tx.run(`ALTER TABLE \`workspace\` DROP COLUMN \`config\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260309230000_move_org_to_state",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`account_state\` ADD \`active_org_id\` text;`)
|
||||
yield* tx.run(`UPDATE \`account_state\` SET \`active_org_id\` = (SELECT \`selected_org_id\` FROM \`account\` WHERE \`account\`.\`id\` = \`account_state\`.\`active_account_id\`);`)
|
||||
yield* tx.run(`ALTER TABLE \`account\` DROP COLUMN \`selected_org_id\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260312043431_session_message_cursor",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`DROP INDEX IF EXISTS \`message_session_idx\`;`)
|
||||
yield* tx.run(`DROP INDEX IF EXISTS \`part_message_idx\`;`)
|
||||
yield* tx.run(`CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260323234822_events",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`event_sequence\` (
|
||||
\`aggregate_id\` text PRIMARY KEY,
|
||||
\`seq\` integer NOT NULL
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`event\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`aggregate_id\` text NOT NULL,
|
||||
\`seq\` integer NOT NULL,
|
||||
\`type\` text NOT NULL,
|
||||
\`data\` text NOT NULL,
|
||||
CONSTRAINT \`fk_event_aggregate_id_event_sequence_aggregate_id_fk\` FOREIGN KEY (\`aggregate_id\`) REFERENCES \`event_sequence\`(\`aggregate_id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260410174513_workspace-name",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`PRAGMA foreign_keys=OFF;`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`__new_workspace\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`type\` text NOT NULL,
|
||||
\`name\` text DEFAULT '' NOT NULL,
|
||||
\`branch\` text,
|
||||
\`directory\` text,
|
||||
\`extra\` text,
|
||||
\`project_id\` text NOT NULL,
|
||||
CONSTRAINT \`fk_workspace_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`INSERT INTO \`__new_workspace\`(\`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\`) SELECT \`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\` FROM \`workspace\`;`)
|
||||
yield* tx.run(`DROP TABLE \`workspace\`;`)
|
||||
yield* tx.run(`ALTER TABLE \`__new_workspace\` RENAME TO \`workspace\`;`)
|
||||
yield* tx.run(`PRAGMA foreign_keys=ON;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260413175956_chief_energizer",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_entry\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`session_id\` text NOT NULL,
|
||||
\`type\` text NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
\`data\` text NOT NULL,
|
||||
CONSTRAINT \`fk_session_entry_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`CREATE INDEX \`session_entry_session_idx\` ON \`session_entry\` (\`session_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_entry_session_type_idx\` ON \`session_entry\` (\`session_id\`,\`type\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_entry_time_created_idx\` ON \`session_entry\` (\`time_created\`);`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260423070820_add_icon_url_override",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
ALTER TABLE \`project\` ADD \`icon_url_override\` text;
|
||||
UPDATE \`project\` SET \`icon_url_override\` = \`icon_url\` WHERE \`icon_url\` IS NOT NULL;
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260427172553_slow_nightmare",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_message\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`session_id\` text NOT NULL,
|
||||
\`type\` text NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
\`data\` text NOT NULL,
|
||||
CONSTRAINT \`fk_session_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`DROP INDEX IF EXISTS \`session_entry_session_idx\`;`)
|
||||
yield* tx.run(`DROP INDEX IF EXISTS \`session_entry_session_type_idx\`;`)
|
||||
yield* tx.run(`DROP INDEX IF EXISTS \`session_entry_time_created_idx\`;`)
|
||||
yield* tx.run(`CREATE INDEX \`session_message_session_idx\` ON \`session_message\` (\`session_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_message_session_type_idx\` ON \`session_message\` (\`session_id\`,\`type\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`)
|
||||
yield* tx.run(`DROP TABLE \`session_entry\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260428004200_add_session_path",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`path\` text;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260501142318_next_venus",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`agent\` text;`)
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`model\` text;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260504145000_add_sync_owner",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`event_sequence\` ADD \`owner_id\` text;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260507164347_add_workspace_time",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`workspace\` ADD \`time_used\` integer NOT NULL DEFAULT 0;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260510033149_session_usage",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`cost\` real DEFAULT 0 NOT NULL;`)
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`tokens_input\` integer DEFAULT 0 NOT NULL;`)
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`tokens_output\` integer DEFAULT 0 NOT NULL;`)
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`tokens_reasoning\` integer DEFAULT 0 NOT NULL;`)
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`tokens_cache_read\` integer DEFAULT 0 NOT NULL;`)
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`tokens_cache_write\` integer DEFAULT 0 NOT NULL;`)
|
||||
yield* tx.run(`
|
||||
UPDATE session
|
||||
SET
|
||||
cost = coalesce((
|
||||
SELECT sum(coalesce(json_extract(message.data, '$.cost'), 0))
|
||||
FROM message
|
||||
WHERE message.session_id = session.id
|
||||
AND json_extract(message.data, '$.role') = 'assistant'
|
||||
), 0),
|
||||
tokens_input = coalesce((
|
||||
SELECT sum(coalesce(json_extract(message.data, '$.tokens.input'), 0))
|
||||
FROM message
|
||||
WHERE message.session_id = session.id
|
||||
AND json_extract(message.data, '$.role') = 'assistant'
|
||||
), 0),
|
||||
tokens_output = coalesce((
|
||||
SELECT sum(coalesce(json_extract(message.data, '$.tokens.output'), 0))
|
||||
FROM message
|
||||
WHERE message.session_id = session.id
|
||||
AND json_extract(message.data, '$.role') = 'assistant'
|
||||
), 0),
|
||||
tokens_reasoning = coalesce((
|
||||
SELECT sum(coalesce(json_extract(message.data, '$.tokens.reasoning'), 0))
|
||||
FROM message
|
||||
WHERE message.session_id = session.id
|
||||
AND json_extract(message.data, '$.role') = 'assistant'
|
||||
), 0),
|
||||
tokens_cache_read = coalesce((
|
||||
SELECT sum(coalesce(json_extract(message.data, '$.tokens.cache.read'), 0))
|
||||
FROM message
|
||||
WHERE message.session_id = session.id
|
||||
AND json_extract(message.data, '$.role') = 'assistant'
|
||||
), 0),
|
||||
tokens_cache_write = coalesce((
|
||||
SELECT sum(coalesce(json_extract(message.data, '$.tokens.cache.write'), 0))
|
||||
FROM message
|
||||
WHERE message.session_id = session.id
|
||||
AND json_extract(message.data, '$.role') = 'assistant'
|
||||
), 0)
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260511000411_data_migration_state",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`data_migration\` (
|
||||
\`name\` text PRIMARY KEY,
|
||||
\`time_completed\` integer NOT NULL
|
||||
);
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260530232709_lovely_romulus",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`metadata\` text;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
10
packages/core/src/database/schema.sql.ts
Normal file
10
packages/core/src/database/schema.sql.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { integer } from "drizzle-orm/sqlite-core"
|
||||
|
||||
export const Timestamps = {
|
||||
time_created: integer()
|
||||
.notNull()
|
||||
.$default(() => Date.now()),
|
||||
time_updated: integer()
|
||||
.notNull()
|
||||
.$onUpdate(() => Date.now()),
|
||||
}
|
||||
177
packages/core/src/database/sqlite.bun.ts
Normal file
177
packages/core/src/database/sqlite.bun.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import { Database } from "bun:sqlite"
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite"
|
||||
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"
|
||||
import { Sqlite } from "./sqlite"
|
||||
|
||||
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
||||
|
||||
const TypeId = "~@opencode-ai/core/database/SqliteBun" as const
|
||||
type TypeId = typeof TypeId
|
||||
|
||||
interface SqliteClient extends Client.SqlClient {
|
||||
readonly [TypeId]: TypeId
|
||||
readonly config: Config
|
||||
readonly export: Effect.Effect<Uint8Array, SqlError>
|
||||
readonly loadExtension: (path: string) => Effect.Effect<void, SqlError>
|
||||
readonly updateValues: never
|
||||
}
|
||||
|
||||
interface Config {
|
||||
readonly filename: string
|
||||
readonly readonly?: boolean
|
||||
readonly create?: boolean
|
||||
readonly readwrite?: boolean
|
||||
readonly disableWAL?: boolean
|
||||
readonly spanAttributes?: Record<string, unknown>
|
||||
readonly transformResultNames?: (str: string) => string
|
||||
readonly transformQueryNames?: (str: string) => string
|
||||
}
|
||||
|
||||
interface SqliteConnection extends Connection {
|
||||
readonly export: Effect.Effect<Uint8Array, SqlError>
|
||||
readonly loadExtension: (path: string) => Effect.Effect<void, SqlError>
|
||||
}
|
||||
|
||||
const make = (options: Config) =>
|
||||
Effect.gen(function* () {
|
||||
const native = (yield* Sqlite.Native) as Database
|
||||
|
||||
const compiler = Statement.makeCompilerSqlite(options.transformQueryNames)
|
||||
const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames).array : undefined
|
||||
|
||||
const run = (query: string, params: ReadonlyArray<unknown> = []) =>
|
||||
Effect.withFiber<Array<Record<string, unknown>>, SqlError>((fiber) => {
|
||||
const statement = native.query(query)
|
||||
// @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627
|
||||
statement.safeIntegers(Context.get(fiber.context, Client.SafeIntegers))
|
||||
try {
|
||||
return Effect.succeed((statement.all(...(params as any)) ?? []) 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<Array<unknown[]>, SqlError>((fiber) => {
|
||||
const statement = native.query(query)
|
||||
// @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627
|
||||
statement.safeIntegers(Context.get(fiber.context, Client.SafeIntegers))
|
||||
try {
|
||||
return Effect.succeed((statement.values(...(params as any)) ?? []) as Array<unknown[]>)
|
||||
} catch (cause) {
|
||||
return Effect.fail(
|
||||
new SqlError({
|
||||
reason: classifySqliteError(cause, { message: "Failed to execute statement", operation: "execute" }),
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const connection = identity<SqliteConnection>({
|
||||
execute(query, params, transformRows) {
|
||||
return transformRows ? Effect.map(run(query, params), transformRows) : run(query, params)
|
||||
},
|
||||
executeRaw(query, params) {
|
||||
return run(query, params)
|
||||
},
|
||||
executeValues(query, params) {
|
||||
return runValues(query, params)
|
||||
},
|
||||
executeUnprepared(query, params, transformRows) {
|
||||
return this.execute(query, params, transformRows)
|
||||
},
|
||||
executeStream() {
|
||||
return Stream.die("executeStream not implemented")
|
||||
},
|
||||
export: Effect.try({
|
||||
try: () => native.serialize(),
|
||||
catch: (cause) =>
|
||||
new SqlError({ reason: classifySqliteError(cause, { message: "Failed to export database", operation: "export" }) }),
|
||||
}),
|
||||
loadExtension: (path) =>
|
||||
Effect.try({
|
||||
try: () => native.loadExtension(path),
|
||||
catch: (cause) =>
|
||||
new SqlError({
|
||||
reason: classifySqliteError(cause, { message: "Failed to load extension", operation: "loadExtension" }),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
const client = 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,
|
||||
config: options,
|
||||
export: Effect.flatMap(acquirer, (_) => _.export),
|
||||
loadExtension: (path: string) => Effect.flatMap(acquirer, (_) => _.loadExtension(path)),
|
||||
},
|
||||
)
|
||||
|
||||
return client
|
||||
})
|
||||
|
||||
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 sqliteLayer = (config: Config) => Layer.effect(Client.SqlClient, make(config))
|
||||
|
||||
const drizzleLayer = Layer.effect(
|
||||
Sqlite.Drizzle,
|
||||
Effect.gen(function* () {
|
||||
return drizzle({ client: (yield* Sqlite.Native) as Database })
|
||||
}),
|
||||
)
|
||||
|
||||
export const layer = (config: Config) =>
|
||||
Layer.merge(
|
||||
nativeLayer(config),
|
||||
Layer.merge(sqliteLayer(config), drizzleLayer).pipe(Layer.provide(nativeLayer(config))),
|
||||
).pipe(
|
||||
Layer.provide(Reactivity.layer),
|
||||
)
|
||||
172
packages/core/src/database/sqlite.node.ts
Normal file
172
packages/core/src/database/sqlite.node.ts
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import { DatabaseSync, type SQLInputValue } from "node:sqlite"
|
||||
import { drizzle } from "drizzle-orm/node-sqlite"
|
||||
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"
|
||||
import { Sqlite } from "./sqlite"
|
||||
|
||||
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
||||
|
||||
const TypeId = "~@opencode-ai/core/database/SqliteNode" as const
|
||||
type TypeId = typeof TypeId
|
||||
|
||||
interface SqliteClient extends Client.SqlClient {
|
||||
readonly [TypeId]: TypeId
|
||||
readonly config: Config
|
||||
readonly loadExtension: (path: string) => Effect.Effect<void, SqlError>
|
||||
readonly updateValues: never
|
||||
}
|
||||
|
||||
interface Config {
|
||||
readonly filename: string
|
||||
readonly readonly?: boolean
|
||||
readonly create?: boolean
|
||||
readonly readwrite?: boolean
|
||||
readonly disableWAL?: boolean
|
||||
readonly timeout?: number
|
||||
readonly allowExtension?: boolean
|
||||
readonly spanAttributes?: Record<string, unknown>
|
||||
readonly transformResultNames?: (str: string) => string
|
||||
readonly transformQueryNames?: (str: string) => string
|
||||
}
|
||||
|
||||
interface SqliteConnection extends Connection {
|
||||
readonly loadExtension: (path: string) => Effect.Effect<void, SqlError>
|
||||
}
|
||||
|
||||
const make = (options: Config) =>
|
||||
Effect.gen(function* () {
|
||||
const native = (yield* Sqlite.Native) as DatabaseSync
|
||||
|
||||
const compiler = Statement.makeCompilerSqlite(options.transformQueryNames)
|
||||
const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames).array : undefined
|
||||
|
||||
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, Client.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, Client.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 = identity<SqliteConnection>({
|
||||
execute(query, params, transformRows) {
|
||||
return transformRows ? Effect.map(run(query, params), transformRows) : run(query, params)
|
||||
},
|
||||
executeRaw(query, params) {
|
||||
return run(query, params)
|
||||
},
|
||||
executeValues(query, params) {
|
||||
return runValues(query, params)
|
||||
},
|
||||
executeUnprepared(query, params, transformRows) {
|
||||
return this.execute(query, params, transformRows)
|
||||
},
|
||||
executeStream() {
|
||||
return Stream.die("executeStream not implemented")
|
||||
},
|
||||
loadExtension: (path) =>
|
||||
Effect.try({
|
||||
try: () => native.loadExtension(path),
|
||||
catch: (cause) =>
|
||||
new SqlError({
|
||||
reason: classifySqliteError(cause, { message: "Failed to load extension", operation: "loadExtension" }),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
const client = 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,
|
||||
config: options,
|
||||
loadExtension: (path: string) => Effect.flatMap(acquirer, (_) => _.loadExtension(path)),
|
||||
},
|
||||
)
|
||||
|
||||
return client
|
||||
})
|
||||
|
||||
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 sqliteLayer = (config: Config) => Layer.effect(Client.SqlClient, make(config))
|
||||
|
||||
const drizzleLayer = Layer.effect(
|
||||
Sqlite.Drizzle,
|
||||
Effect.gen(function* () {
|
||||
return drizzle({ client: (yield* Sqlite.Native) as DatabaseSync }) as unknown as Sqlite.DrizzleClient
|
||||
}),
|
||||
)
|
||||
|
||||
export const layer = (config: Config) =>
|
||||
Layer.merge(
|
||||
nativeLayer(config),
|
||||
Layer.merge(sqliteLayer(config), drizzleLayer).pipe(Layer.provide(nativeLayer(config))),
|
||||
).pipe(
|
||||
Layer.provide(Reactivity.layer),
|
||||
)
|
||||
8
packages/core/src/database/sqlite.ts
Normal file
8
packages/core/src/database/sqlite.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export * as Sqlite from "./sqlite"
|
||||
|
||||
import { Context } from "effect"
|
||||
import type { drizzle } from "drizzle-orm/bun-sqlite"
|
||||
|
||||
export type DrizzleClient = ReturnType<typeof drizzle>
|
||||
export class Native extends Context.Service<Native, unknown>()("@opencode-ai/core/database/SqliteNative") {}
|
||||
export class Drizzle extends Context.Service<Drizzle, DrizzleClient>()("@opencode-ai/core/database/SqliteDrizzle") {}
|
||||
Loading…
Add table
Add a link
Reference in a new issue