Skip to content

chore(gastown): migrate DO SQLite to Drizzle ORM#704

Closed
iscekic wants to merge 8 commits intomainfrom
chore/gastown-drizzle
Closed

chore(gastown): migrate DO SQLite to Drizzle ORM#704
iscekic wants to merge 8 commits intomainfrom
chore/gastown-drizzle

Conversation

@iscekic
Copy link
Contributor

@iscekic iscekic commented Mar 1, 2026

Summary

  • Replace raw SQL query() calls and Zod-based table definitions with Drizzle ORM query builder across all 3 DOs (TownDO, GastownUserDO, AgentDO) and 5 sub-modules (~110 query sites)
  • Add drizzle-orm/drizzle-kit dependencies, create sqlite-schema.ts with all 11 active tables, and wire drizzle() + migrate() in each DO constructor
  • Delete 19 legacy table files, query.util.ts, table.ts, and update AGENTS.md SQL conventions

Details

Schema & Config

  • src/db/sqlite-schema.ts — 11 tables defined with sqliteTable, matching existing DDL exactly (columns, types, defaults, CHECK constraints, indexes). Exports $inferSelect/$inferInsert types.
  • drizzle.config.ts — sqlite dialect, durable-sqlite driver
  • drizzle/0000_mushy_elektra.sql — Generated migration with IF NOT EXISTS for backward compatibility with existing DO instances
  • wrangler.jsonc — Added .sql import rules for migration bundle
  • pnpm-workspace.yaml — Added drizzle-kit: ^0.31.9 to catalog

Query Rewrites

File Queries converted Key patterns
Town.do.ts ~30 JOIN columns, COUNT aggregates, conditional WHERE, sql template for arithmetic
beads.ts ~18 Dynamic conditions array, LIMIT/OFFSET, recursive delete cascade
review-queue.ts ~16 Review JOIN, json_set() via sql tag, molecule step chain
agents.ts ~15 Agent JOIN (status alias), getTableColumns spread, name allocation
GastownUser.do.ts ~9 Simple CRUD, ORDER BY DESC
Agent.do.ts ~4 .returning() replaces last_insert_rowid(), NOT IN subquery prune
rigs.ts ~4 .onConflictDoUpdate() for upsert
mail.ts ~4 JOIN for pending mail to working agents

Cleanup

  • Deleted src/db/tables/ (19 files), src/util/table.ts, src/util/query.util.ts
  • Updated AGENTS.md SQL section for Drizzle patterns

Verification

  • Typecheck: 0 new errors (4 pre-existing errors in handler files confirmed on main)
  • Tests: 0 new failures (9 pre-existing failures in client.test.ts confirmed on main)

Backward Compatibility

Migration SQL uses CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS, so existing DO instances (which already have tables but lack __drizzle_migrations) will not fail when drizzle runs the initial migration.

Replace raw SQL query() calls and Zod-based table definitions with
Drizzle ORM query builder across all 3 DOs (TownDO, GastownUserDO,
AgentDO) and 5 sub-modules (~110 query sites total).

- Add drizzle-orm/drizzle-kit deps and sqlite-schema.ts with 11 tables
- Wire drizzle() + migrate() in each DO constructor
- Convert all queries to type-safe query builder (select/insert/update/delete)
- Delete 19 legacy table files, query.util.ts, and table.ts
- Update AGENTS.md SQL conventions for Drizzle patterns
- Migration SQL uses IF NOT EXISTS for backward compatibility
@iscekic iscekic self-assigned this Mar 1, 2026
@kilo-code-bot
Copy link
Contributor

kilo-code-bot bot commented Mar 1, 2026

Code Review Summary

Status: No New Issues Found | Recommendation: Merge

This is a thorough, well-structured migration from raw SQL (query() helper + Zod schemas) to Drizzle ORM across the entire cloudflare-gastown package. The migration is clean and consistent:

  • All SqlStorage parameters replaced with DrizzleSqliteDODatabase
  • Raw SQL strings replaced with Drizzle query builder (db.select(), db.insert(), db.update(), db.delete())
  • Zod-based row parsing replaced with $inferSelect types + JSON.parse() for JSON text columns
  • Join types derived from query builders (NonNullable<ReturnType<...>>) instead of hand-written types
  • Dead code stubs (initAgentTables, initMailTables, initReviewQueueTables) removed
  • Centralized schema in db/sqlite-schema.ts with proper CHECK constraints and indexes
  • Migration file generated via drizzle-kit with durable-sqlite driver
  • wrangler.jsonc updated with rules for .sql file bundling

All previously flagged issues from earlier review rounds have been addressed in follow-up commits (c5591ff7, 1048b97d, 03e44579, 2b1fbc6f).

Remaining Minor Items (already flagged in prior review)

Severity File Issue
SUGGESTION agents.ts as AgentJoinRow casts remain — the type is now correctly derived from the query builder, but the as casts could be removed if drizzle's .get() / .all() return types are narrowed (low priority)
SUGGESTION mail.ts:24 Local parseBead is still a duplicate of the exported one from beads.ts — could import instead
SUGGESTION Town.do.ts:1181 pendingAgentColumns duplicates agentJoinColumns from agents.ts — could be shared

These are all pre-existing suggestions from earlier review rounds and don't block merge.

Files Reviewed (18 files)
  • cloudflare-gastown/AGENTS.md — Updated SQL query conventions for Drizzle
  • cloudflare-gastown/drizzle.config.ts — New drizzle-kit config
  • cloudflare-gastown/drizzle/0000_mushy_elektra.sql — Generated migration
  • cloudflare-gastown/drizzle/migrations.js — Migration registry
  • cloudflare-gastown/drizzle/meta/_journal.json — Migration journal
  • cloudflare-gastown/drizzle/meta/0000_snapshot.json — Schema snapshot
  • cloudflare-gastown/package.json — Added drizzle-orm + drizzle-kit deps
  • cloudflare-gastown/src/db/sqlite-schema.ts — New centralized Drizzle schema
  • cloudflare-gastown/src/dos/Agent.do.ts — Migrated to Drizzle
  • cloudflare-gastown/src/dos/GastownUser.do.ts — Migrated to Drizzle
  • cloudflare-gastown/src/dos/Town.do.ts — Migrated to Drizzle
  • cloudflare-gastown/src/dos/town/agents.ts — Migrated to Drizzle
  • cloudflare-gastown/src/dos/town/beads.ts — Migrated to Drizzle
  • cloudflare-gastown/src/dos/town/mail.ts — Migrated to Drizzle
  • cloudflare-gastown/src/dos/town/review-queue.ts — Migrated to Drizzle
  • cloudflare-gastown/src/dos/town/rigs.ts — Migrated to Drizzle
  • cloudflare-gastown/src/types.ts — Updated type imports to use Drizzle inferred types
  • cloudflare-gastown/wrangler.jsonc — Added .sql bundling rule
  • cloudflare-gastown/src/util/query.util.ts — Deleted (replaced by Drizzle)
  • cloudflare-gastown/src/util/table.ts — Deleted (replaced by Drizzle)
  • pnpm-lock.yaml — Updated lockfile

@iscekic iscekic changed the base branch from main to chore/drizzle-base March 1, 2026 21:53
Base automatically changed from chore/drizzle-base to main March 1, 2026 22:08
- Derive AgentJoinRow, ReviewJoinRow, EscalationJoinRow, ConvoyJoinRow
  from query builder ReturnType instead of hand-writing them; eliminates
  all 'as' casts (AgentJoinRow x4, ReviewJoinRow x2, Agent['role'],
  Agent['status'], EscalationEntry['severity'], status as 'idle')
- Narrow updateAgentStatus param to AgentMetadataSelect['status'];
  add AgentStatus.parse() at the TownDO public boundary (matching the
  BeadStatus.parse() pattern used by updateBeadStatus)
- Export parseBead from beads.ts and import it in agents.ts, mail.ts,
  review-queue.ts, Town.do.ts instead of duplicating the function
- Delete dead init stubs: initAgentTables, initMailTables,
  initReviewQueueTables (never called, stale comments)
- Add comment on self-ref FK any in sqlite-schema.ts (drizzle limitation)
- Clarify docs/do-sqlite-drizzle.md reference as repo-root path in AGENTS.md
- Add comment on db.run(sql`...`) prune in Agent.do.ts explaining why
  the sql template tag is used (NOT IN subquery)
# Conflicts:
#	cloudflare-gastown/package.json
#	cloudflare-gastown/src/db/tables/agent-metadata.table.ts
#	cloudflare-gastown/src/db/tables/beads.table.ts
#	pnpm-lock.yaml
@iscekic iscekic requested review from jeanduplessis and jrf0110 March 1, 2026 23:36
…C types

Cloudflare's Rpc.Serializable<T> resolves DO RPC return types to never
when they contain `unknown`. This was fixed on main (1da0cb3) for the
old table-based types, but the drizzle migration reintroduced `unknown`
in the new Omit-based type overrides.

Changed to `any` with eslint-disable comments:
- Bead.metadata, CreateBeadInput.metadata, BeadEventRecord.metadata
- Molecule.formula
- AgentMetadataRecord.checkpoint
toAgent() in agents.ts correctly does JSON.parse(row.checkpoint), but the
inline agent construction in schedulePendingWork() was passing the raw
SQLite text string through. Matches the toAgent() pattern.
// Exclude beads.status from the join columns since agent_metadata.status
// shadows it and we need the agent status, not the bead status.
const { status: _beadStatus, ...beadCols } = getTableColumns(beads);
const pendingAgentColumns = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: Duplicated agent join columns + manual toAgent mapping

pendingAgentColumns (lines 1181-1191) and the rows.map(row => ({...})) block (lines 1209-1221) duplicate agentJoinColumns and toAgent() from agents.ts. This duplication was the root cause of the checkpoint JSON.parse regression caught in the previous review round.

Consider exporting agentJoinColumns, agentJoinQuery, and toAgent from agents.ts and reusing them here. The additional .where() clause for cooldown filtering can be appended via .$dynamic(), the same pattern already used in listAgents().

@iscekic
Copy link
Contributor Author

iscekic commented Mar 3, 2026

Closing per agreement with @jrf0110

@iscekic iscekic closed this Mar 3, 2026
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.

1 participant