Skip to content

Comment-escape fix, Workflow held back for 0.3.1, and column-rename references - #599

Merged
huyplb merged 5 commits into
mainfrom
claude/fervent-goodall-suen18
Nov 8, 2026
Merged

huyplb merged 5 commits into
mainfrom
claude/fervent-goodall-suen18

Conversation

@huyplb

@huyplb huyplb commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

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 comment

The 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:

-- CREATE VIEW: v
DROP TABLE victim; --
CREATE OR REPLACE VIEW "v DROP TABLE victim; --" AS
SELECT 1;

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:

Layer Covers
flattenIdentifier in ident every name the generator quotes (ident / bareName / qualify / columnIdent)
MigrationStep.objectName, flattened at all 4 assignment sites the per-step header, emitted separately from the statements — the route the first reproduction took
oneLineComment, plus keepCommentCommented as a backstop names ident never sees (index / constraint / trigger, type warnings), and notes a dialect hook builds from values the generator never quotes

That 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 generateObjectDdl combinations, 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.1

One flag, NAV_FLAGS.workflow, takes Workflow off the activity rail and out of the command palette. The view's code and /api/workflow are untouched and the engine still answers, so flipping it back to true is the whole of putting it back.

A view is reached only through the rail or the palette — there are no URLs for views — but activeView is persisted, so a session that was on Workflow before the flag went off reopened on it. redirectFor now also sends anyone home from a view whose nav item a flag hides, following filterNav's own rule. The E2E specs that drive the view skip on a mirrored WORKFLOW_ON_RAIL constant and run again unchanged with the flag.

3. feat(utilities): which lines name the column being renamed

"View v_open_orders may 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 columnReferenceLines produces 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:

  • The "after" line is presented as what the SQL needs to say, so it must parse. Renaming status to order status put the bare name in while the plan's own ALTER quoted 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.
  • A generated view can name a column on hundreds of lines, each a row React built. The list stops at 50 and says how many are left; the count above still reports them all, and the whole definition is one click away.

Checks

  • npx vitest run — 6948 passed, 1 expected fail, 145 skipped
  • npx eslint . · npm run lint:security · npm run typecheck — all exit 0
  • Each of the 5 review-fix tests verified to fail without its fix

Not in this PR

table.engine is still interpolated raw into the ClickHouse ENGINE clause, 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

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
@cursor

cursor Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot 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
@cursor

cursor Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot 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)

@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Test IDs

Added (7)

  • column-change-references
  • column-refs-{…}-{…}
  • column-refs-{…}-{…}-after-{…}
  • column-refs-{…}-{…}-definition
  • column-refs-{…}-{…}-lines
  • column-refs-{…}-{…}-more
  • column-refs-{…}-{…}-toggle

{…} is a part filled in at run time. The full tree: docs/testing/TEST_IDS.md.

@huyplb huyplb changed the title fix(sql): keep a name's line break from escaping a generated comment Comment-escape fix, Workflow held back for 0.3.1, and column-rename references Nov 8, 2026
@huyplb
huyplb merged commit ed91efd into main Nov 8, 2026
13 checks passed
@huyplb
huyplb deleted the claude/fervent-goodall-suen18 branch November 8, 2026 17:03
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.

2 participants