Skip to content

fix(pretty_print): indent a nested CASE from its THEN or ELSE - #935

Merged
psteinroe merged 15 commits into
mainfrom
fix/pretty-print-nested-case
Oct 26, 2026
Merged

psteinroe merged 15 commits into
mainfrom
fix/pretty-print-nested-case

Conversation

@psteinroe

Copy link
Copy Markdown
Collaborator

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:

-- 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 test(pretty_print): fuzz the option matrix in CI #912), whose inner branches move left to the new indent.
  • The option matrix passes over the fixtures and the regression suite.

psteinroe added a commit that referenced this pull request Oct 26, 2026
Stacked on #893.

Follow-up to #884. Expanded CASE expressions lost their alignment when `CASE` started left of the current indent, and a broken `WHEN` condition continued at the `WHEN` column.

**Alignment from where CASE starts**

The renderer stored an alignment as an offset from the indent of the level it started at, and clamped a negative offset to zero. When the text on a line starts left of that indent, the offset is negative. That happens for `select case` once the select list has opened its indent, or `on case` in a join, and `WHEN` and `END` then drifted right:

```sql
-- layout: expanded, clauseBodyStyle: compact, indentSize: 8
-- before
select case
                when a
                        then 1
                else 2
        end
from t;
-- after
select case
               when a
                       then 1
               else 2
       end
from t;
```

An alignment now keeps the column `CASE` starts at together with the indent level at that point. A continuation line starts at that column plus one indent per level opened since. When the offset is not negative this gives the same columns as before, so the existing expanded CASE output is unchanged.

With `indentStyle: "tabs"` an aligned line is written as one tab per indent level and spaces for the rest, as before. When the column is left of the indent, it uses only as many tabs as fit before the column: `\t       when` instead of `\t\t when`.

**WHEN conditions are indented**

A `WHEN` condition that breaks continued at the column of `WHEN`, so `and` read as a sibling of `when`. In expanded layout every AND/OR breaks, so this affected every multi-operand condition:

```sql
-- before
case
  when aaaa = 1
  and bbbb = 2
    then 'x'
-- after
case
  when aaaa = 1
    and bbbb = 2
    then 'x'
```

The condition is now one indent level in from `WHEN`, the same way a WHERE condition is. Fit layout does the same once the condition breaks. Expanded layout still breaks at every AND/OR in a WHEN condition. I left that alone because it is what expanded layout does everywhere else, and the reporter of #884 uses it that way.

**Docs**

`docs/features/formatting.md` now describes the expanded CASE layout and what tabs do to alignment: the columns only line up when the editor's tab width equals `indentSize`. A test checks that the example in the docs is what the formatter prints.

**Tests and snapshot changes**

New tests in `case_layout.rs`: CASE after `on` with `indentSize: 4`, `select case` with `indentSize: 8` in spaces and tabs, the exact tab and space output after a leading comma, and WHEN condition continuation in expanded and fit layout. Two tests in `comment_layout.rs` now expect the `AND` of a WHEN condition, and the comment before it, one tab further in. The only snapshot that changes is `expanded_case_expr_200`, where the AND lines of two WHEN conditions move one level in.

**Not covered**

A CASE nested in the THEN of another CASE still moves right by about 9 columns per level in expanded layout. Twenty levels reach 184 columns at width 100. Three or four levels stay within the width, so I didn't change the layout for this.

**Follow-ups:** The nested CASE drift in expanded layout is fixed in #935.
@psteinroe
psteinroe changed the base branch from fix/pretty-print-comment-anchors to main October 26, 2026 10:40
@psteinroe
psteinroe merged commit 810c082 into main Oct 26, 2026
12 checks passed
@psteinroe
psteinroe deleted the fix/pretty-print-nested-case branch October 26, 2026 11:18
psteinroe added a commit that referenced this pull request Oct 26, 2026
…der the keyword (#938)

Stacked on #935.

Follow-up to #896, which listed this as not covered. A condition that stays on its keyword line (WHERE and HAVING with `clauseBodyStyle: compact`, and ON in every config) was indented for its continuation lines. When the condition only breaks inside the parentheses of a subquery, it has no continuation lines of its own. The indent then put the subquery two levels in and the closing parenthesis one level in, under nothing:

```sql
-- layout: expanded, clauseBodyStyle: compact
-- before
select *
from public.permissions
where exists (
    select 1
    from public.permission_set_permissions as rp
    where rp.permission_name = permissions.name
  );
-- after
select *
from public.permissions
where exists (
  select 1
  from public.permission_set_permissions as rp
  where rp.permission_name = permissions.name
);
```

The same happened to `on exists (` in a join with the default config.

**What changed**

`emit_clause_condition` (compact) and the ON of a join leave out the continuation indent when the condition breaks only inside a subquery. That covers:

- a subquery link: `exists (...)`, `x in (...)`, `x = any (...)`, `(...)`, `array(...)`;
- `x <op> (subquery)` or `x <op> array(subquery)`, where `x` is a column, parameter or constant;
- `not` in front of either.

Everything else keeps the indent, including AND/OR chains, whose operands still continue one level in, with a subquery in them closing at that level.

**Snapshot changes**

All reviewed; one category: the body and `)` of a subquery opened on a compact `where`/`having` line, or on an `on` line, move one level left, so `)` closes under the keyword. This is in `compact_select_list*`, `scalar_subquery_expanded`, `sublink_bool_groups_expanded` and, for ON with the default config, `predicate`.

New tests in `subquery_layout.rs` cover `where exists (`, `where x = (`, a chain with an `exists` operand, and `on x = (`.

**Not covered**

A lone select item in compact layout has the same pattern: `select coalesce(` indents its arguments two levels and closes one level in. That is the select-list indent from #865 and would need the same rule there.
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