Skip to content

fix(pretty_print): keep comments before CASE and GRANT keywords in place - #934

Merged
psteinroe merged 14 commits into
mainfrom
fix/pretty-print-comment-anchors
Oct 26, 2026
Merged

psteinroe merged 14 commits into
mainfrom
fix/pretty-print-comment-anchors

Conversation

@psteinroe

Copy link
Copy Markdown
Collaborator

Stacked on #917.

Comments written before a keyword that has no AST node of its own (ELSE, THEN, END, GRANT's ON/TO/FROM) moved past the keyword, sometimes into another branch or clause. The output was stable on reformat, so nothing flagged it, but the comment no longer sat where it was written. Finding P17 of the release review.

select case -- why
when a then 'x' /* inline */ else 'y' end from t;
-- before
select
  case
    when a then 'x'
    else /* inline */ 'y' -- why
  end
from
  t;
-- after
select
  case
    -- why
    when a then 'x' /* inline */
    else 'y'
  end
from
  t;

select case when a /* cond */ then 1 else 2 /* last */ end from t;
-- before
select case when a then /* cond */ 1 else 2 end from /* last */ t;
-- after
select case when a /* cond */ then 1 else 2 /* last */ end from t;

grant select -- c
on t to u;
-- before
grant select
  on table -- c
  t
  to u;
-- after
grant select -- c
  on table t
  to u;

Why they moved

Comments are attached to AST nodes by source position, and a comment that leads goes to the next node: a block comment, an own-line comment, or a comment after a clause keyword. For 'x' /* inline */ else 'y' the next node is 'y', so the comment came out after else. A comment right after case didn't count as following a clause keyword, so it trailed the whole CASE and came out after the last branch. A comment before end led the next node after the CASE, in the FROM clause.

What changed

  • When the token after a comment is a keyword that no node starts at, the comment remembers that keyword (before_keyword). A parent that writes such a keyword before a child can take the comments written before it, with emit_comments_before_keyword, and emit them before the keyword. They are emitted like the trailing comments of the previous node, so an own-line comment keeps its own line. Only comments recorded for that exact keyword are taken. A parent that doesn't call it keeps today's placement, so nothing else moves.
  • CASE takes them before ELSE and THEN. GRANT/REVOKE takes them before ON and TO/FROM, and GRANT role before TO/FROM.
  • A comment before END stays with the last branch: it trails the previous node instead of leading the next one, because nothing inside the CASE follows END.
  • CASE counts as a clause keyword, so a line comment right after it goes before the first WHEN.
  • The renderer collapses two spaces in a row inside a group that stays on one line, as it already did for broken groups. The comment emission otherwise gave a /* cond */ then.

Tests

  • New tests in comment_layout.rs cover a comment before ELSE, THEN and END, after CASE, and before GRANT's ON and TO, in fit and expanded layout, each formatted twice to check idempotence.
  • Two unit tests in comments.rs cover the attachment.
  • No snapshot changes: the snapshot harness renders without comments.
  • The option matrix from test(pretty_print): fuzz the option matrix in CI #912 passes over the fixtures and the regression suite.

Not covered

Other keyword-only positions (ON in a join, AS before an alias, FROM in a SELECT, ...) keep the old placement. They can opt in the same way when needed.

@psteinroe
psteinroe changed the base branch from fix/cli-format-options to main October 26, 2026 10:13
@psteinroe
psteinroe merged commit 1defb62 into main Oct 26, 2026
12 checks passed
@psteinroe
psteinroe deleted the fix/pretty-print-comment-anchors branch October 26, 2026 10:40
psteinroe added a commit that referenced this pull request Oct 26, 2026
Stacked on #934.

In expanded layout, a CASE nested in the THEN of another CASE moved right by `then ` plus two indent levels per level of nesting. #884 aligns each CASE's branches with the column it starts at, and a nested CASE starts after `then `. With the default indent, twenty levels reached 184 columns at width 100. Finding P16 of the release review.

**What changed**

A CASE written as the result of a `THEN` or `ELSE` no longer aligns with its own column. It is indented from the line of its keyword: its branches one level in, and `END` under `THEN` or `ELSE`. Each level now costs two indents, the minimum for a WHEN and its THEN:

```sql
-- layout: expanded, indentSize: 4
-- before
case status
    when 'A'
        then case
                 when enabled
                     then 'active'
                 else 'paused'
             end
    else 'closed'
end
-- after
case status
    when 'A'
        then case
            when enabled
                then 'active'
            else 'paused'
        end
    else 'closed'
end
```

`else case when ... else case ...` chains read the same way. A CASE anywhere else, after `select` or a leading comma for instance, still aligns with the column it starts at, which is what #884 was about. Fit layout doesn't align, so it is unchanged.

This changes the nested layout that #884's `expanded_nested_case_keeps_each_branch_at_its_own_indent` test asserted, and the test now expects the new layout. I preferred this over breaking after `then` for every nested CASE: that would add a line and a level for every nested CASE, also the shallow ones that fit today. The docs section on expanded CASE describes the nested layout.

**Tests and snapshot changes**

- `case_layout.rs`: the updated nested test, a CASE nested in ELSE, and twenty levels nested in THEN staying within width 100.
- The only snapshot change is the nested CASE in `expanded_case_alignment` (added in #912), whose inner branches move left to the new indent.
- The option matrix passes over the fixtures and the regression suite.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant