Skip to content

docs: Postgres portability — Kysely spike (compile-only) - #1070

Draft
lane711 wants to merge 2 commits into
mainfrom
lane711/postgres-migration-effort
Draft

docs: Postgres portability — Kysely spike (compile-only)#1070
lane711 wants to merge 2 commits into
mainfrom
lane711/postgres-migration-effort

Conversation

@lane711

@lane711 lane711 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

What

Re-measures the SonicJS → Postgres port effort with a compile-only Kysely spike. Ports the two data-access chokepoints to Kysely and proves one TypeScript builder compiles to correct SQLite (D1) and Postgres SQL:

  • ReadDocumentRepository.list() (the R4 read chokepoint)
  • Writedocuments.saveDraft() (the hardest write: .batch(), COALESCE(MAX)+1 version subquery, the R5 27-bind INSERT, prune DELETE … NOT IN (… LIMIT ?))

Compile-only via Kysely DummyDriver + per-dialect QueryCompiler. No DB, no production code touched. Docs + a runnable spike only.

Findings

# Result
1 ?$N placeholder rewrite is free (dialect compiler)
2 0/1-int vs real boolean collapses to one per-dialect mapper, not 300 edits
3 The 'd.' alias string-prefix hack in list() is deleted (Kysely resolves aliases)
4 The R5 27-bind INSERT bind-counting bug class disappears with named values({col}); COALESCE(MAX)+1 (R6) stays in SQL
5 .batch().transaction(); kysely-d1 maps it back to D1 batch on Workers
6 json_extract is absent from both chokepoints; the 38 dialect-divergent sites are a bounded ~20-file set
7 Kysely compiles to D1 / better-sqlite3 / libSQL / Postgres from one query layer

Revised estimate

~5–9 weeks to full Postgres parity + a dual-dialect test matrix, at ~half the defect risk of a raw hand-written SQL port. Kysely doesn't cut the week-count much — it changes the risk profile (typed, compiler-checked, dual-dialect).

Files

  • docs/ai/plans/postgres-portability-poc/ — runnable spike (npm i && node list-read.mjs && node saveDraft-write.mjs) + README with full measurements
  • docs/ai/plans/portability-analysis.md — status header + link to the spike

Not in scope

No production code, no packages/core/migrations/* changes, no dependency added to the app (spike has its own isolated package.json). This is an RFC/measurement to decide whether to pursue Tier 2 (Postgres).

🤖 Generated with Claude Code

Re-measures the SonicJS -> Postgres effort. Ports the two chokepoint paths
(read DocumentRepository.list, write documents.saveDraft) to Kysely and
proves one TypeScript builder compiles to correct SQLite (D1) AND Postgres
SQL. Compile-only via DummyDriver + per-dialect QueryCompiler — no DB, no
production code touched.

Findings:
- `?` -> `$N` placeholder rewrite is free (dialect compiler).
- 0/1-int vs real boolean collapses to one per-dialect mapper.
- The R5 27-bind INSERT bind-counting class of bug disappears with named
  values({col}); COALESCE(MAX)+1 version subquery (R6) stays in SQL.
- json_extract is absent from both chokepoints; the 38 dialect-divergent
  sites are a bounded ~20-file set.
- Kysely compiles to D1 / better-sqlite3 / libSQL / Postgres from one layer.

Revised estimate: ~5-9 weeks to full Postgres parity + dual-dialect test
matrix, at roughly half the defect risk of a raw-SQL port.

Adds docs/ai/plans/postgres-portability-poc/ (runnable spike + README) and
links it from portability-analysis.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ordered go/no-go plan to add Postgres as a runtime DB backend alongside
D1/SQLite. Core strategy: keep D1Database as the universal seam (a
D1-compatible sqlite driver already exists) and add one postgres-driver +
the bounded residual dialect SQL.

Three simplifying decisions shrink the surface:
- PG keeps 0/1 integer boolean columns -> the hundreds of `= 1`
  comparisons need no change.
- q_* stay real STORED generated columns -> DocumentRepository.list()
  (read chokepoint) needs no change.
- postgres-driver rewrites ?->$N internally -> the 743 .prepare sites run
  unchanged; only the ~20 json_extract files + 1 DDL generator + 21
  INSERT OR IGNORE sites are touched.

6 phases + optional Kysely quality track, ~7-10 eng-weeks to GA with a
dual-dialect (pglite) test matrix. Risk register + per-file targets
enumerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lane711

lane711 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

📋 Added postgres-implementation-plan.md — the phased Tier-2 runbook.

Core strategy: keep D1Database as the universal seam (a D1-compatible SQLite driver already exists) → the port is one postgres-driver.ts + bounded residual dialect SQL, not a rewrite.

Three decisions shrink the surface to near-nothing:

  • PG keeps 0/1 integer boolean columns → hundreds of = 1 comparisons unchanged
  • q_* stay real STORED generated columnsDocumentRepository.list() (read chokepoint) unchanged
  • driver rewrites ?$N → 743 .prepare() sites unchanged; only ~20 json_extract files + 1 DDL generator + 21 INSERT OR IGNORE sites touched

6 phases (driver → schema/migrations → residual SQL → auth → dual-dialect pglite test matrix → rollout), gated behind DB_DRIVER (default d1, PG opt-in). Kysely is an optional post-GA quality track. ~7–10 eng-weeks.

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