Repository navigation
Comment-escape fix, Workflow held back for 0.3.1, and column-rename references - #599
Merged
Merged
Conversation
The script writes object and column names into `--` notes — the per-step
header, review notes, manual-review blocks — and a comment ends at the first
line break. A name carrying one closed its note and left the rest of itself on
the next line, as a statement the migration then ran:
-- CREATE VIEW: v
DROP TABLE victim; --
CREATE OR REPLACE VIEW "v DROP TABLE victim; --" AS ...
The splitter read `DROP TABLE victim;` as its own statement. A line break
cannot be escaped inside a quoted identifier, so quoting does not help: to name
that column the SQL has to carry the break literally. Reachable the same way as
the other identifier injections — a source schema the user does not control —
and dialect-independent.
Fixed in three layers, because the names reach comments by three different
routes:
- `flattenIdentifier` in `ident`, where every name the generator quotes passes
through. Covers the review notes and manual-review blocks that take a name
from `ident` / `bareName` / `qualify` / `columnIdent`.
- `MigrationStep.objectName`, now documented as single-line and flattened at
all four assignment sites. The per-step header is emitted separately from the
statements, so it is not covered by the above, and it was the route the first
reproduction took.
- `oneLineComment` on the notes that quote a name `ident` never saw — an index,
constraint or trigger name, a type-translation warning — and
`keepCommentCommented` as a backstop at the one place every step's statements
pass through, for notes a dialect hook builds from values the generator never
quotes. ClickHouse's ENGINE / ORDER BY note repeats the sorting key and the
engine string; SQLite's column note repeats the type. All three were
exploitable, and the backstop covers hooks added later too.
The backstop only ever adds `-- ` to a line, and only inside a statement that
already opens with `--`, so real DDL cannot be commented out. Verified as a
no-op on valid output: across 1056 statements from every dialect, same- and
cross-dialect, nothing opening with `--` carried a non-comment line. A comment
and code never share a statement, because the step header is emitted separately.
Swept rather than spot-checked: 99 shape x dialect combinations (each also
cross-dialect) with the payload in every name field, 35 `generateObjectDdl`
combinations, and newlines in a sorting key, engine string, column type and
column default. All clean; before the fix, name fields, the two dialect notes
and the cross-dialect column note were not.
Trade-off: a name that really does contain a line break is emitted with spaces
instead, so that one object needs a hand-written migration. Such a name cannot
be escaped in SQL and is pathological; the alternative is running DDL that a
source schema chose.
npx vitest run 6925 passed, 1 expected fail, 145 skipped (was 6914)
npx eslint . exit 0
npm run lint:security exit 0
npm run typecheck exit 0
10 of the 11 new tests fail on the unfixed generator; the eleventh asserts that
ordinary output is unchanged and passes either way.
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: a84ab433-f497-4934-a210-6f4cb8bfb544) |
Workflow is a large feature and is not ready to be offered, so it comes off the activity rail and out of the command palette for the release. `NAV_FLAGS.workflow` in the web feature registry already existed for exactly this, so the change is that one flag; turning it back to `true` is the whole of putting the feature back. The view and `/api/workflow` are untouched, as are the engine and apps/workflow-server. Note what that does and does not mean: a view is reached only through the rail or the command palette — there is no URL for one — so with the flag off the Workflow view cannot be opened from the UI at all. The API stays mounted and still answers an API client, and the Home Services card still reports engine health. Tests follow the flag rather than being loosened: - The rail and command-palette assertions no longer list Workflow. The ActivityRail spec now asserts the button is absent, and its click-through check moved to the Access button so "a rail button switches view" is still covered. - The two Workflow e2e specs drive the view through the rail, which no longer exists, so they are `describe.skipIf(!WORKFLOW_ON_RAIL)`. That constant mirrors the flag, so flipping the flag re-enables them unchanged rather than leaving a silent skip behind. npx vitest run 6925 passed, 1 expected fail, 145 skipped npx eslint . exit 0 npm run typecheck exit 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ehcTb7vYMCawV1ajxwfxB
The column change screen listed the objects a rename touches — "View
v_open_orders may use it", "Procedure bump_order_status may use it" — and left
it there. The engine renames the column; it does not rewrite the SQL inside a
view or a routine that reads it, so somebody then had to open each object and
hunt for the name themselves.
The impact now carries the lines. `columnReferenceLines` returns, for each
dependent object, the 1-based line number, the line as written, where on the
line the name sits, and — when the change renames — the line as it will have to
read. A new read-only panel shows them, with the whole definition behind a
toggle.
On the demo schema, renaming customers.tier:
VIEW v_customer_orders — 2 line(s)
1 | SELECT c.id AS customer_id, c.name, c.email, c.tier,
→ SELECT c.id AS customer_id, c.name, c.email, c.customer_tier,
6 | GROUP BY c.id, c.name, c.email, c.tier
→ GROUP BY c.id, c.name, c.email, c.customer_tier
Read-only on purpose: Fox does not edit somebody else's routine, and the panel
says so where a rename is planned. Covers views, functions, procedures and
triggers.
Notes on the implementation:
- One filter feeds both the existing name lists and `dependents`, so the two
cannot drift. The name lists are untouched: the warnings and the summary read
them.
- Matching keeps the semantics the impact already had — whole word, case
insensitive, plain text — so a name inside a string literal or a comment
counts. That errs towards showing a line somebody can dismiss over hiding one
they needed, and the panel calls these references, not edits.
- The matcher uses lookarounds rather than the consuming boundary groups
`wordIn` uses: a consuming group swallows the character between two adjacent
occurrences and reports one of them, and these offsets are what the UI
highlights. `SET status = status` marks both.
- The UI highlights from the offsets the plan reported rather than searching
again, so the highlight cannot disagree with what the plan found.
- Types reach the frontend through @foxschema/ui-shared, not @foxschema/sql
directly — architecture.test.ts enforces that, and caught it.
- `npm run test-ids` regenerated for the new data-testids.
npx vitest run 6942 passed, 1 expected fail, 145 skipped (was 6925)
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
The nav flag takes Workflow off the rail and out of the command palette, which is every way into a view — except one. `activeView` is persisted, so a session that was on Workflow before the flag went off reopens on it, and `redirectFor` only ever consulted `allowed`. `redirectFor` now also sends anyone back home from a view whose nav item a flag hides, by `filterNav`'s own rule (a flag nobody turned on hides its item). The comment above the flag claimed a direct URL still reached the view; there are no URLs for views at all, so it now says what is true. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ehcTb7vYMCawV1ajxwfxB
…p them Two things the column-references panel got wrong. The "after" line is shown as what a view or routine needs to say, so it has to be valid SQL. Renaming `status` to `order status` put the bare name in — `SELECT order status FROM orders` — while the plan's own ALTER quoted it. The new name is now quoted for the dialect when it needs it, and where the definition already quotes the old name those quotes stay and only the word inside changes, so nothing is quoted twice. A plain name is inserted untouched, so every ordinary rename reads as before. A generated view can name a column on hundreds of lines, and every one was a row React built. The list stops at fifty and says how many are left; the count above still reports them all, and the whole definition is a click away. 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: 87ce9aee-e5e9-4644-8b31-5ec91fc1461f) |
Contributor
Test IDsAdded (7)
|
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.
Three separate pieces of work, each its own commit. The first is the security fix this PR opened with; the other two are the 0.3.1 release flag and the new Utilities panel, folded in because they share the branch.
1.
fix(sql): a name's line break escaping a generated commentThe script writes names into
--notes — the per-step header, review notes, manual-review blocks — and a comment ends at the first line break. A name carrying one closed its note and left the rest of itself behind as a statement the migration then ran:Quoting doesn't help: a line break can't be escaped inside a quoted identifier. Dialect-independent, and reachable the same way as the other identifier injections — a source schema the user doesn't control.
Names reach comments by three routes, so the fix has three layers:
flattenIdentifierinidentident/bareName/qualify/columnIdent)MigrationStep.objectName, flattened at all 4 assignment sitesoneLineComment, pluskeepCommentCommentedas a backstopidentnever sees (index / constraint / trigger, type warnings), and notes a dialect hook builds from values the generator never quotesThat third layer matters: ClickHouse's ENGINE / ORDER BY note repeats the sorting key and engine string, SQLite's column note repeats the type. All three were exploitable, and the backstop covers hooks added later. It can't break DDL: it only adds
--to a line, and only inside a statement that already opens with--. Verified a no-op across 1056 statements from every dialect, same- and cross-dialect.Swept rather than spot-checked: 99 shape × dialect combinations with the payload in every name field, 35
generateObjectDdlcombinations, and line breaks in a sorting key, engine string, column type and column default. 10 of the 11 new tests fail on the unfixed generator; the eleventh asserts ordinary output is unchanged and passes either way.Trade-off: a name that really does contain a line break is emitted with spaces, so that object needs a hand-written migration. Such a name can't be escaped in SQL and is pathological — the alternative is running DDL a source schema chose. The name still reaches the DDL flattened, so the object stays identifiable.
2.
chore(web): Workflow off the rail for 0.3.1One flag,
NAV_FLAGS.workflow, takes Workflow off the activity rail and out of the command palette. The view's code and/api/workfloware untouched and the engine still answers, so flipping it back totrueis the whole of putting it back.A view is reached only through the rail or the palette — there are no URLs for views — but
activeViewis persisted, so a session that was on Workflow before the flag went off reopened on it.redirectFornow also sends anyone home from a view whose nav item a flag hides, followingfilterNav's own rule. The E2E specs that drive the view skip on a mirroredWORKFLOW_ON_RAILconstant and run again unchanged with the flag.3.
feat(utilities): which lines name the column being renamed"View
v_open_ordersmay use it" says there is work without saying where. The plan now carries, for each dependent view, function, procedure and trigger, the lines that name the column — with the match offsets — and the panel shows each line as written, highlighted, and (when renaming) as it will have to read. Read-only on purpose: Fox renames the column, it does not rewrite somebody else's routine.One matcher in
columnReferenceLinesproduces both the highlight offsets and the rewritten line, so the two cannot disagree. The existing name lists are untouched — the warnings and summary still read them — and one shared filter feeds both, so they cannot drift.Two defects found reviewing it afterwards, fixed in the last commit:
statustoorder statusput the bare name in while the plan's ownALTERquoted it. The new name is now quoted for the dialect when it needs it; where the definition already quotes the old name those quotes stay and only the word inside changes, so nothing is quoted twice. A plain name is inserted untouched, so every ordinary rename reads exactly as before.Checks
npx vitest run— 6948 passed, 1 expected fail, 145 skippednpx eslint .·npm run lint:security·npm run typecheck— all exit 0Not in this PR
table.engineis still interpolated raw into the ClickHouseENGINEclause, so a hostile engine string yields two statements. Engine names come from a fixed set, but engine parameters (ReplicatedMergeTree('/path','{replica}')) are chosen at CREATE TABLE time, so validating it needs care not to break parameterised engines — left for its own decision. The comment backstop here does cover the engine string where it appears inside a-- review:note, just not in the executable clause.🤖 Generated with Claude Code
https://claude.ai/code/session_019ehcTb7vYMCawV1ajxwfxB