feat(core): add a database table prefix for shared SQLite databases by Brendonovich · Pull Request #53746 · anomalyco/opencode · GitHub
Skip to content

feat(core): add a database table prefix for shared SQLite databases - #53746

Open
Brendonovich wants to merge 3 commits into
devfrom
workerd-table-prefix
Open

Brendonovich wants to merge 3 commits into
devfrom
workerd-table-prefix

Conversation

@Brendonovich

@Brendonovich Brendonovich commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Issue for this PR

Closes #53577

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Adds a database.prefix option, so OpenCode can share a SQLite database with tables it doesn't own:

OpenCodeWorkerd.create({ storage, database: { prefix: "opencode_" } })
OpenCode.create({ database: { path, prefix: "opencode_" } })

With a prefix set, every table and index OpenCode creates, including the migration journal, is stored as <prefix><name>. Bootstrap only looks at tables under the prefix, so the host's tables (cf_agents_*, alchemy_*, or its own session/kv) no longer trip Database is not empty and has no session table, and names can't collide. With no prefix, behavior is unchanged, so existing deployments keep their data.

How it works:

  • Queries: PrefixedSQLiteDialect in our Drizzle fork (database/drizzle/sqlite-core/dialect.ts). Table definitions stay shared and unprefixed, because they're module-level and shared by every Durable Object in an isolate. The prefix is applied when a query is rendered. Drizzle renders nested SQL through the root statement's collectSQL, so wrapping the root lets the dialect rename every Table reference and table.column qualifier. That covers builders, subqueries, and sql templates that interpolate tables or columns. Joins are the one place Drizzle bakes a table name into a plain identifier while building the query, so the dialect passes join tables through as table references instead. Aliased from-tables aren't used, so they fail loudly when a prefix is set. The dialect is only used when a prefix is set.
  • DDL: schema.gen.ts references the table declarations. It writes CREATE TABLE ${KVTable}, REFERENCES ${ProjectTable}, and partial-index conditions like WHERE ${SessionPendingTable.type} = ... as Drizzle sql templates, so the dialect prefixes DDL as well. Index names and the migration journal have no declaration, so they use prefixedIdentifier("..."), a chunk the prefixed dialect renders under the prefix and any other dialect renders like sql.identifier. The migration generator emits these interpolations from drizzle-kit's output and imports the declarations.
  • Migrations name tables by string. New generated migrations use prefixedIdentifier("kv") rather than declarations, because migrations are snapshots and must stay correct if a declaration is later renamed or removed. Migration.up(tx) keeps its signature. Existing migrations are untouched. They never run on a prefixed database, because a fresh prefixed database bootstraps from the current schema and records them all as applied.
  • Bootstrap: DatabaseMigration.apply(db, prefix) discovers tables under the prefix and uses <prefix>migration as the journal.
  • Plumbing: Database.Options.prefix is validated as a plain identifier not starting with sqlite_. Database.layerFromClient and configuredClient take the prefix, and ServerWorkerd.Options.database.prefix passes it through.
  • Legacy v1 import: prefixed databases skip it, since an unprefixed session table there belongs to the host, not a v1 install.

The dialect is the only place the prefix is applied. Hand-written future migrations must name tables and indexes through table declarations or prefixedIdentifier rather than literal text. This is documented on Migration.up.

How did you verify your code works?

  • New packages/core/test/database-table-prefix.test.ts:
    • The rendered SQL for a select with a join, an update, an upsert, a delete, and a subquery with a sql template is fully prefixed, while columns named like tables (worktree) stay unprefixed.
    • Workerd storage with pre-existing foreign session, kv, and cf_agents_state tables plus an index boots twice under a prefix (bootstrap, then the migration path) and does a Drizzle write/read. Every OpenCode table and index ends up under the prefix, and the foreign tables and their rows are untouched.
    • The prefixed schema matches the unprefixed one name for name.
    • A shared bun database file boots under a prefix.
    • Invalid prefixes are rejected.
  • packages/server/test/workerd.test.ts: the workerd profile boots with database.prefix beside an object-owned session table.
  • As a one-off, I ran the full core suite (6524 tests) with a prefix forced on for every database. The only new failures were tests that deliberately use raw unprefixed SQL or compare against an unprefixed schema. The remaining failures are shell tests that fail the same way on v2. The server and SDK suites show no new failures.
  • Generated a throwaway migration that adds a column and a partial index, ran it against a prefixed database that also has a foreign kv table, and confirmed the result was opencode_kv plus "opencode_kv_scope_idx" ... WHERE "opencode_kv"."scope" is not null, with the foreign table untouched. The probe was then reverted.
  • bun script/migration.ts --check passes.
  • bun run check passes.

Screenshots / recordings

N/A

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

workerd: let embedders prefix OpenCode's SQLite tables

1 participant