Repository navigation
fix(sql): keep trigger bodies in one statement, and terminate them - #597
Merged
Merged
Conversation
Migrating a trigger produced SQL that could not run. Two defects compounded;
the migration generated from the seeded SQLite demo databases hit both.
1. A `.` in a comment qualified the next identifier
Qualification was decided by scanning backwards over whitespace for a `.`:
function identIsQualified(sql, identStart) {
let k = identStart - 1;
while (k >= 0 && /\s/.test(sql[k])) k--;
return k >= 0 && sql[k] === '.';
}
A backward character scan cannot tell a `.` that is code from one that
merely ends a comment. The generator writes review notes above the
statements they concern, and one of them ends in a period:
-- review: skip FK … declare FOREIGN KEY in CREATE TABLE only (no ALTER ADD).
CREATE TRIGGER trg_decrement_stock …
That period made `CREATE` look like `schema.CREATE`, so `onWord` skipped it.
`stmtVerb` never became CREATE, `TRIGGER` was never matched against
ROUTINE_OBJECT, `inRoutineDdl` stayed false, and the body split at the `;`
inside BEGIN … END — leaving a fragment starting with `END` that glued
itself to the next statement:
[10] CREATE TRIGGER … BEGIN UPDATE products SET … WHERE id = NEW.product_id;
[11] END -- ALTER TABLE: ORDERS ALTER TABLE orders ADD COLUMN notes TEXT;
-> `incomplete input`, then `near "ALTER": syntax error`
Qualification now comes from the token stream, which already classifies
comments as trivia, so it is correct by construction: a `.` that ends a
comment never reaches it, while `a . b` and `a /* c */ . b` still read as
qualified. The reduction that found this was `--.` + a trigger.
2. Trigger definitions were emitted without a terminator
Three sites pushed `definition.trim()`. SQLite's sqlite_master.sql ends a
trigger at `END` with no `;`, so the CREATE TRIGGER never ended and
swallowed the following statement — which no splitter can fix, since the
text is genuinely ambiguous. They now go through ensureSemicolon. The
MySQL and Oracle `createTriggerStatement` hooks already end in `;`, so this
is a no-op for them and is idempotent besides.
Verified end to end against the seeded demo databases: the trigger is now one
statement, no fragment starts with `END`, and both triggers exist on the
target after the migration runs. Executed statements went from 14 to 16 of 20.
The 4 remaining failures are pre-existing and unrelated, left alone: the
generator emits SQLite's reserved internal `sqlite_autoindex_*` index names
(x2), and SQLite rejects non-constant `ADD COLUMN` defaults such as
`DEFAULT strftime(...)` (x2).
The frontend's shared/lib/sql-splitter.ts only re-exports this module, so the
SQL editor's execute-all path is covered by the same fix.
npx vitest run 6883 passed, 1 expected fail, 145 skipped (was 6874)
npx eslint . exit 0
npm run lint:security exit 0
npm run typecheck exit 0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ehcTb7vYMCawV1ajxwfxB
Contributor
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: 659bc208-f139-4d87-bb2e-33b5d1fe5879) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Migrating a trigger produced SQL that could not run. Two defects compounded; the migration generated from the seeded SQLite demo databases hit both.
1. A
.in a comment qualified the next identifierQualification was decided by scanning backwards over whitespace for a
.:A backward character scan cannot tell a
.that is code from one that merely ends a comment. The generator writes review notes above the statements they concern, and one of them ends in a period:That period made
CREATElook likeschema.CREATE, soonWordskipped it.stmtVerbnever becameCREATE,TRIGGERwas never matched againstROUTINE_OBJECT,inRoutineDdlstayed false, and the body split at the;insideBEGIN … END:Qualification now comes from the token stream, which already classifies comments as trivia, so it's correct by construction: a
.that ends a comment never reaches it, whilea . banda /* c */ . bstill read as qualified. Automated reduction took the trigger down to--.plus a trigger.2. Trigger definitions were emitted without a terminator
Three sites pushed
definition.trim(). SQLite'ssqlite_master.sqlends a trigger atENDwith no;, so theCREATE TRIGGERnever ended and swallowed the following statement — which no splitter can fix, because the text is genuinely ambiguous. They now go throughensureSemicolon. The MySQL and OraclecreateTriggerStatementhooks already end in;, so this is a no-op for them, and it's idempotent besides.Test plan
Verified end to end against the seeded demo databases, not only in unit tests:
Previously the trigger split in two and neither half ran. Executed statements went from 14 to 16 of 20.
npx vitest run— 6883 passed, 1 expected fail, 145 skipped (was 6874; +9 new tests)npx eslint .— exit 0npm run lint:security— exit 0npm run typecheck— exit 0bash scripts/seed/seed-all.sh sqlite, comparedemo_a→demo_b, execute the generated migration against a copy of the targetRegression coverage for the qualifier change
This touches a hot path in the splitter, so the new tests pin both directions — that a comment's
.no longer qualifies, and that genuine qualifiers still do:Notes for the reviewer
identIsQualifiedis deleted, not patched — the token-stream check replaces it entirely and it had no other caller.apps/web/src/frontend/shared/lib/sql-splitter.tsonly re-exports this module, so the SQL editor's execute-all path is covered by the same fix.Out of scope
The 4 remaining failures in that migration are pre-existing and unrelated — left alone:
sqlite_autoindex_*index names (×2)ADD COLUMNdefaults such asDEFAULT strftime(...)(×2)Also still open from the same review:
isQuotedIdentifieremits a quote-wrapped identifier verbatim, which allows DDL injection from a source schema's column names. That one needs a design decision and is not touched here.Generated by Claude Code
Note
Medium Risk
Changes a hot path in
splitSqlStatements(used by execute-all and migrations) and trigger DDL emission; behavior is well covered by new tests but touches statement-boundary logic.Overview
Fixes two bugs that broke SQLite trigger migrations when review comments sat above the DDL and when catalog definitions omitted a trailing semicolon.
SQL splitter: Identifier qualification no longer scans backward for a
.before the next word (which treated a period at the end of a-- review:comment asschema.CREATE). Qualification now follows the token stream viaprevTokenWasDot, soCREATE TRIGGER … BEGIN … ENDstays one executable cell while real cases likes.begin/s . beginstill count as qualified columns.Migration generator: Raw trigger bodies from introspection (e.g. SQLite
sqlite_masterending atENDwithout;) are emitted throughensureSemicolonat create-table, create-object, and alter-trigger paths so an unterminated trigger cannot absorb the following statement. Dialect hooks that already end in;are unchanged.New unit tests cover comment-period false qualification, genuine qualifiers, and trigger terminator idempotency.
Reviewed by Cursor Bugbot for commit 0476ac4. Bugbot is set up for automated code reviews on this repo. Configure here.