Skip to content

fix(selfhost): translate SQLite AUTOINCREMENT so migrations apply on Postgres - #10143

Merged
JSONbored merged 1 commit into
mainfrom
fix/pg-autoincrement-dialect
Jul 31, 2026
Merged

fix(selfhost): translate SQLite AUTOINCREMENT so migrations apply on Postgres#10143
JSONbored merged 1 commit into
mainfrom
fix/pg-autoincrement-dialect

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Closes #10138

P0 — this blocked every Orb upgrade past 0208. Found by deploying orb-v3.7.0-beta.6 to edge-nl-01: the container crash-looped, rolled back to orb-v3.7.0-beta.5, healthy. ~90s of downtime.

error: syntax error at or near "AUTOINCREMENT"
  code: '42601'
  at async runSelfHostMigrations (server.mjs:210857)

migrations/0209_service_status_samples.sql:21 declares id INTEGER PRIMARY KEY AUTOINCREMENT. Postgres cannot parse it, runSelfHostMigrations throws, the process exits.

No data damage. Migrations apply inside a transaction — service_status_samples was never created and _selfhost_migrations still ends at 0208. It failed closed, which is the one good thing about it.

Why the dialect layer missed it

Migrations are authored in SQLite dialect and translated by translateDdl (pg-dialect.ts), applied at pg-adapter.ts:138/150. It handles SQLite date/json functions and INSERT OR IGNORE. Its own doc comment states the assumption that left the hole:

Column types (TEXT/INTEGER/REAL) are PG-native

True of types — but AUTOINCREMENT is a constraint. 0209 is the only migration in the repo that uses it; every other table uses plain INTEGER PRIMARY KEY, which parses on both dialects, so the gap stayed latent until someone wrote one.

Three fixes considered, two rejected

Strip the keyword — rejected, strictly worse. Bare INTEGER PRIMARY KEY auto-assigns in SQLite (it aliases the rowid) but is an ordinary NOT NULL integer in Postgres. The DDL would parse, then every id-omitting insert would fail at runtime — and recordServiceStatusSamples is best-effort and swallows errors, so the status board would silently record nothing. Trading a loud boot failure for silent data loss is not a fix. The mutation test for this variant fails 8 assertions.

Edit the migration — rejected, would cause a second outage. contentSha256 hashes the raw source file (migrate.ts:152), so changing 0209's bytes throws selfhost_migration_content_drift on every Orb that already applied it. On D1 the SQLite syntax is also perfectly valid. Shipped migrations are immutable for exactly this reason.

Translate it — taken.

INTEGER PRIMARY KEY AUTOINCREMENT  ->  BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY

BY DEFAULT not ALWAYS: SQLite permits an explicit id and ALWAYS would reject one, turning a legal insert into a Postgres-only runtime error — precisely the dialect-dependent divergence this layer exists to prevent. BIGINT matches SQLite's 64-bit rowid rather than narrowing to int4.

The real gap, and why this is not a new CI job

The real-Postgres suite already catches thistest/integration/selfhost-pg.test.ts, "applies every migration". It ran on the very commit that introduced 0209 (a7673e24, #9990) and failed correctly. But it only runs in selfhost.yml, which is push-to-main only, so the signal landed after the merge. It then stayed red across five consecutive runs while PRs kept merging, because a post-merge failure blocks nothing and pages no one.

The obvious fix — a Postgres service job in ci.yml — is the wrong tool here: CI runtime is already the binding constraint (26k+ tests per PR), and runtime cost is exactly what moved selfhost.yml post-merge in the first place. Adding back what was deliberately removed would be trading one problem for the one it was created to solve.

So the pre-merge guard is test/unit/migration-dialect-portability.test.ts: applies translateDdl to every migration, fails if a SQLite-only construct survives. No Postgres, no service container, no new job — 13 tests in ~11ms inside the unit run that already happens.

Two design points that make it hold up:

  • Derived from the translator, not restated. It checks the output of translateDdl, so adding a translation automatically satisfies it and removing one automatically fails it. A hand-maintained banned-string list would be a second source of truth that drifts on the first change.
  • Comment-stripped. My first version produced three false positives — 0054 and 0176 document INSERT OR REPLACE in header comments, and 0180 annotates a column "NOT autoincrement". All three run fine on Postgres today. A checker that cries wolf gets muted.

It proves parseable, not correct; selfhost.yml remains the only real-Postgres coverage and its red is worth acting on. That is now said in its header, which previously claimed ci.yml validated the pg suites pre-merge — it does not and cannot, since there is no service container and they skip without PG_TEST_URL.

Verification

  • Full suite + typecheck / dead-exports / dead-source / actionlint green.
  • Only 0209–0211 are unapplied on the Orb; all three now translate cleanly.
  • Mutation-tested:
mutation tests failed
unwire the translation from translateDdl (the outage) 3
strip-only, no identity column (the tempting wrong fix) 8
GENERATED ALWAYS instead of BY DEFAULT 7

Plus a self-test asserting the check still fires on real DDL and is exempt only inside comments — so comment-stripping cannot silently neuter it.

…Postgres

Migration 0209 declares `id INTEGER PRIMARY KEY AUTOINCREMENT`. Postgres
cannot parse AUTOINCREMENT (SQLSTATE 42601), so runSelfHostMigrations aborted
and the process exited: every Orb upgrading past 0208 crash-looped on boot.
Reproduced deploying orb-v3.7.0-beta.6 to edge-nl-01; rolled back to beta.5.
No data damage -- migrations apply in a transaction, so the table was never
created and the ledger still ends at 0208.

Migrations are authored in SQLite dialect and translated by translateDdl,
which covers SQLite date/json functions and INSERT OR IGNORE. Its doc comment
records the assumption behind the gap -- "column types are PG-native" -- which
is true of types, but AUTOINCREMENT is a CONSTRAINT. 0209 is the only
migration that uses it; every other table uses plain INTEGER PRIMARY KEY,
which parses on both dialects, so the gap stayed latent.

Stripping the keyword would be worse than leaving it. Bare INTEGER PRIMARY KEY
auto-assigns in SQLite but is an ordinary NOT NULL integer in Postgres, so the
DDL would parse and every id-omitting insert would fail at runtime instead --
silently, since the writer is best effort and swallows errors. It is rendered
as a Postgres identity column instead: BY DEFAULT rather than ALWAYS, because
SQLite permits an explicit id, and BIGINT to match the 64-bit rowid.

The migration file is deliberately untouched. contentSha256 hashes the raw
source, so editing it would throw selfhost_migration_content_drift on every
Orb that already applied it -- and on D1 the SQLite syntax is valid.

The real-Postgres suite already catches this ("applies every migration") but
only runs in selfhost.yml, which is push-to-main only: it went red on the very
commit that introduced 0209 and stayed red across five runs while PRs kept
merging. The pre-merge counterpart added here applies translateDdl to every
migration and fails if a SQLite-only construct survives -- no Postgres, no
service container, ~11ms inside the unit run that already happens, because CI
runtime is the constraint that moved that workflow post-merge to begin with.
Checked post-translation rather than against a banned-string list so it cannot
drift from the translator, and comment-stripped so the many migrations that
merely DISCUSS these constructs are not flagged.

selfhost.yml's header claimed ci.yml validated the pg integration suites
pre-merge. It does not and cannot -- there is no service container, so without
PG_TEST_URL they skip. Corrected in place.

Closes #10138
@loopover-orb

loopover-orb Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Important

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏳ LoopOver is waiting…

LoopOver has seen this pull request and is waiting on CI checks to finish before reviewing it. This comment will update once the review runs.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed · 🟨 Waiting

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@JSONbored JSONbored self-assigned this Jul 31, 2026
@JSONbored
JSONbored merged commit 07df5ca into main Jul 31, 2026
3 checks passed
@JSONbored
JSONbored deleted the fix/pg-autoincrement-dialect branch July 31, 2026 09:05
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.99%. Comparing base (291fcd9) to head (1562b11).
⚠️ Report is 9 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #10143   +/-   ##
=======================================
  Coverage   91.99%   91.99%           
=======================================
  Files         931      931           
  Lines      114000   114001    +1     
  Branches    27523    27523           
=======================================
+ Hits       104877   104878    +1     
  Misses       7823     7823           
  Partials     1300     1300           
Flag Coverage Δ
backend 95.67% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/selfhost/pg-dialect.ts 100.00% <100.00%> (ø)

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.

P0: migration 0209 uses SQLite AUTOINCREMENT — every Orb upgrade past 0208 crash-loops

1 participant