Repository navigation
fix(pretty_print): align expanded CASE from where it starts - #894
Merged
Merged
Conversation
This was referenced Oct 23, 2026
psteinroe
changed the base branch from
fix/pretty-print-values-layout
to
main
October 26, 2026 07:20
psteinroe
added a commit
that referenced
this pull request
Oct 26, 2026
Stacked on #894. Follow-up to #864 for the comparison case, plus a nested-subquery case from #857. **`= (subquery)` stays together** #864 kept `exists (`, `in (`, `= any (` and `> all (` on one line, but a scalar subquery compared with an operator still broke between the operator and `(`. With the config reported in #857 (`layout: expanded`, `clauseBodyStyle: compact`, `logicalOperatorPlacement: leading`): ```sql -- before using ( org_id = ( select org_id ... -- after using ( org_id = ( select org_id from members where user_id = auth.uid() ) ); ``` The default config did the same, `t.owner_id =` followed by `(select …)` on its own line. A binary operator now puts a plain space before a right-hand `(subquery)` or `array(subquery)`, and the subquery breaks inside its parentheses as it does after `in (`. Other right-hand operands still break after the operator when they don't fit. **Subqueries with clauses expand inside a select-list-only subquery** Since #857, a subquery made of a select list alone, such as `(select auth.uid())`, uses fit layout in expanded mode, because there are no clauses to break between. That fit layout applied to everything inside it, so a subquery with FROM/WHERE nested in it stayed on one line in expanded mode: ```sql -- layout: expanded, clauseBodyStyle: compact select * from t where a = (select coalesce((select max(x) from y where y.id = t.id), 0)); ``` The emitter now remembers the configured layout, and a nested subquery with clauses switches back to it, so the inner `select max(x)` / `from y` / `where ...` expands again. **Snapshot changes** All reviewed; one category: `x =\n(` / `x <>\n(` / `x <\n(` / `<>\narray(` → `x = (`, `x <> (`, `x < (`, `<> array(`. When the subquery is short enough to fit on the next line, it now sits indented inside the parentheses instead (`relnamespace = (\n select oid from pg_namespace where ...\n)`). Affected: `create_cast`, `create_index`, `create_view`, `namespace`, `opr_sanity`, `rangefuncs`, `reloptions`, `tablespace`, `type_sanity`, `with`. New fixtures `scalar_subquery.sql` and `scalar_subquery_expanded.sql` (the #857 reporter's config) cover a policy, `=` with a multi-clause subquery, `= array(subquery)`, the nested-coalesce case and a short `= (select 1)`. New `subquery_layout.rs` tests cover both findings. **Not covered** With `clauseBodyStyle: compact`, a WHERE condition keeps its indent for its own continuation lines, so a subquery that opens on the `where` line is indented two levels and closes one level in: ```sql where a = ( select ... ); ``` That is the same for `where exists (` today and is a separate change to the compact layout. **Follow-ups:** The compact WHERE indentation is fixed in #938.
psteinroe
added a commit
that referenced
this pull request
Oct 26, 2026
Stacked on #912. Fixes formatter options and `format` check output that were hard to use, found in the release review. **Option errors list the valid values** ```sh $ postgres-language-server format --clause-body-style=foo a.sql # before Error: couldn't parse `foo`: Value not supported for clause body style # after Error: couldn't parse `foo`: expected 'break' or 'compact' $ postgres-language-server format --values-row-layout=foo a.sql # before Error: couldn't parse `foo`: Value not supported for Layout. Use 'fit' or 'expanded'. # after Error: couldn't parse `foo`: expected 'fit' or 'expanded' ``` All formatter enums now say what they expect, without naming the Rust type. `Layout` is shared by `--layout` and `--values-row-layout`, and the user never sees that name. **A zero line width or indent size is rejected** `--line-width=0` and `--indent-size=0`, and `"lineWidth": 0` / `"indentSize": 0` in the config file, were accepted. `lineWidth` and `indentSize` are now `NonZeroU16` / `NonZeroU8`, as `files.maxSize` already is. The CLI says `couldn't parse '0': number would be zero for non-zero type`, the config file `invalid value: integer '0', expected a nonzero u16`. The JSON schema now has `minimum: 1` for both. **Check output names the line** Without `--write`, `format` printed one diagnostic per statement that would change. When a statement's diff fits on one line, it is printed without line numbers, and the header only names the file. In a large file there was no way to find the statement. The message now carries the line: ``` a.sql format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ × Statement at line 4 could be formatted using `--write` - SELECT··x·FROM·y·WHERE·z=1; + select·x·from·y·where·z·=·1; ``` I put the line in the message rather than giving the diagnostic a span. With a span, the printer adds a code frame of the statement above the diff. When only the blank lines between statements change, the whole-file diff now says "File could be formatted" instead of "Statement". **Help shows the values** Every format flag was shown as `=ARG` in `--help` and in the CLI reference. They now show `NUMBER`, `<spaces|tabs>`, `<fit|expanded>` and so on, like the other options. **Docs** The `layout` option was described as "always breaks between clauses". Expanded layout also puts every select list item and every AND/OR operand on its own line and breaks before every CASE branch. A select-list-only subquery and a CHECK condition follow the line width instead. The option's description (and so the schema, bindings and CLI reference) and `docs/features/formatting.md` now say this, and the docs have an example. Together with the CASE and tabs docs in #894, this covers the docs gaps from the review. **Tests** - CLI tests for the two option errors and for rejecting zero, with the two check-output snapshots updated for the new messages. - A configuration test for rejecting zero in the config file. - Schema, TypeScript bindings and CLI reference regenerated with `just gen-lint`. **Risks** A config file with `lineWidth` or `indentSize` set to 0 used to load and now fails to load with the error above. With either set to 0 the formatter output was unusable anyway: every group broke, or nothing was indented.
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.
Stacked on #893.
Follow-up to #884. Expanded CASE expressions lost their alignment when
CASEstarted left of the current indent, and a brokenWHENcondition continued at theWHENcolumn.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 caseonce the select list has opened its indent, oron casein a join, andWHENandENDthen drifted right:An alignment now keeps the column
CASEstarts 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 wheninstead of\t\t when.WHEN conditions are indented
A
WHENcondition that breaks continued at the column ofWHEN, soandread as a sibling ofwhen. In expanded layout every AND/OR breaks, so this affected every multi-operand condition: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.mdnow describes the expanded CASE layout and what tabs do to alignment: the columns only line up when the editor's tab width equalsindentSize. 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 afteronwithindentSize: 4,select casewithindentSize: 8in spaces and tabs, the exact tab and space output after a leading comma, and WHEN condition continuation in expanded and fit layout. Two tests incomment_layout.rsnow expect theANDof a WHEN condition, and the comment before it, one tab further in. The only snapshot that changes isexpanded_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.