Skip to content

fix(hub): dollar-quote-aware SQL migration splitter#63

Merged
finedesignz merged 1 commit into
mainfrom
fix/migrate-dollar-quotes
May 26, 2026
Merged

fix(hub): dollar-quote-aware SQL migration splitter#63
finedesignz merged 1 commit into
mainfrom
fix/migrate-dollar-quotes

Conversation

@finedesignz
Copy link
Copy Markdown
Owner

Bug

hub/src/db/migrate.ts split schema.sql on ; without respecting PostgreSQL's \$\$...\$\$ dollar-quoted string syntax. Every DO \$\$ BEGIN ... EXCEPTION WHEN ... END \$\$; block got split mid-string. Production boot logs:

[migrate] failed: DO \$\$ BEGIN ALTER TABLE users ADD CONSTRAINT users_claude_session_threshold_pct_range ... — unterminated dollar-quoted string at or near "\$\$ BEGIN ..."
[migrate] failed: EXCEPTION WHEN duplicate_object THEN NULL; END \$\$ — syntax error at or near "EXCEPTION"
[migrate] applied 135/142 statements

Net effect: 7/142 statements failed. Two CHECK constraints (users_claude_session_threshold_pct_range, users_claude_week_threshold_pct_range) and the re-added scheduled_task_runs_status_check never landed.

Fix

Replace naive split(';') with a state-machine splitter splitSqlStatements() that respects:

  • \$\$ ... \$\$ and tagged \$tag\$ ... \$tag\$ dollar-quoted strings
  • single-quoted strings ('...') with '' escape
  • double-quoted identifiers ("...") with "" escape
  • line comments (-- ... \n)
  • block comments (/* ... */), nested per Postgres semantics
  • bare positional params like \$1 (NOT a dollar-quote tag)

schema.sql is not modified — the DO blocks are valid SQL; only the parser was wrong.

Test coverage

13 new unit tests in hub/test/migrate.test.ts:

  • DO block with EXCEPTION + inner semicolons → 1 statement
  • CREATE TABLE → DO block → CREATE TABLE → 3 statements
  • Tagged \$body\$ ... \$body\$ with embedded ; in a string literal
  • Single-quoted string with embedded ;
  • Escaped '' inside string literal
  • Double-quoted identifier with embedded ;
  • Block comment containing ;
  • Line comment containing ;
  • Bare \$1 positional param not treated as a dollar quote
  • Nested block comments
  • Real-file validation: every DO block in the live schema.sql now parses to a single statement with balanced \$\$ token counts

All 13 pass. No regressions in the rest of the hub test suite (5 pre-existing failures in insert-run-started-at.test.ts are unrelated and exist on main HEAD prior to this branch).

Follow-up flagged (NOT in this PR)

runMigrations() currently catches per-statement errors and continues, only logging them. That silent-failure mode is what let this bug live in production. Worth a separate PR to abort boot on any migration error — but out of scope here per the task brief.

Verification

  • bun test test/migrate.test.ts → 13/13 pass
  • bun test (full hub suite) → 295 pass, 63 skip, 5 pre-existing unrelated fails

🤖 Generated with Claude Code

The previous splitter in hub/src/db/migrate.ts naively split schema.sql on
`;`, ignoring PostgreSQL dollar-quoted string syntax. Every `DO $$ BEGIN ...
EXCEPTION WHEN ... END $$;` block got split mid-string and rejected by
Postgres. Production logs showed 7/142 statements failing on boot, leaving
two CHECK constraints (`users_claude_session_threshold_pct_range`,
`users_claude_week_threshold_pct_range`) and a re-added
`scheduled_task_runs_status_check` constraint un-applied.

Fix: replace `split(';')` with a small state-machine splitter
`splitSqlStatements()` that respects:
  - $$...$$ and tagged $tag$...$tag$ dollar-quoted strings
  - single-quoted strings with '' escape
  - double-quoted identifiers with "" escape
  - line comments (-- to newline)
  - block comments (nested per Postgres semantics)
  - bare positional params like $1 (not a dollar-quote tag)

schema.sql is unchanged — the DO blocks are valid SQL; only the parser was
wrong.

Tests: 13 new unit tests in hub/test/migrate.test.ts covering each shape,
plus a real-file assertion that every DO block in the live schema.sql now
parses to a single statement with balanced $$ tokens.

Note (separate concern, NOT fixed here): runMigrations() still logs failed
statements and continues rather than aborting boot. That latent
silent-failure is what allowed this bug to live in production for weeks
without an obvious crash signal. Worth a follow-up PR to fail loudly on
migration error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@finedesignz finedesignz merged commit 23eda5f into main May 26, 2026
1 check passed
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