Repository navigation
fix(pretty_print): break the remaining calls and statement headers within the line width - #946
Merged
Merged
Conversation
…functions break sensibly
…print-grant-admin-option
…int-long-identifiers
…thin the line width
This was referenced Oct 24, 2026
Merged
psteinroe
added a commit
that referenced
this pull request
Oct 26, 2026
…functions break sensibly (#898) Stacked on #896. Three places where a call-like expression broke in the wrong place or not at all. **`count(*) filter (...)` keeps `count(*)` together** A function call put its name and arguments in the same group as a following FILTER or WITHIN GROUP. When that clause didn't fit, the arguments broke too: ```sql -- before count( dl.id ) filter (where dl.created_at > now() - cast('180 days' as interval)) -- after count(dl.id) filter (where dl.created_at > now() - cast('180 days' as interval)) ``` The name and arguments now have their own group whenever FILTER, WITHIN GROUP or OVER follows. OVER already had one. The line breaks before the clause first. A clause that moves to its own line is indented one level, so it reads as part of the call: `percentile_cont(0.5)\n within group (order by x)`. **A FILTER clause that fits stays on one line** `filter (where` was followed by a soft line in the call's group, so a FILTER that fit still broke as `filter (where\n cond)` once the call broke, with `)` attached to the condition. FILTER is now a group of its own: `filter (where cond)` when it fits, and otherwise `filter (`, `where cond` indented on its own lines, and `)` on its own line, like a policy's `using (...)`. **Long `array[...]` literals fill indented lines** `array[...]` had no indent and no break after `[`. Once it didn't fit, every element after the first went on its own line at the enclosing indent, and `]` stuck to the last element: ```sql -- before oid <> all (cast(array['regproc', 'regprocedure', 'regoper', ... 'regcollation'] as regtype[])) -- after oid <> all (cast(array[ 'regproc', 'regprocedure', 'regoper', 'regoperator', 'regconfig', 'regdictionary', 'regnamespace', 'regcollation' ] as regtype[])) ``` Arrays now use the same layout as an `IN (...)` list. On one line when they fit; otherwise a break after `[`, the elements filling indented lines, and `]` back on the line the array started on. **`json_value`, `json_query` and `json_exists` respect the line width** These had no indent and no break after `(`, so a long context item ran past the width, and the clauses that wrapped did so at the enclosing indent with `)` attached to the last one. #882 left this out of scope. They now use the function-call layout that `json_table` already uses: on one line when the call fits, otherwise the context item, the path and each clause (`passing`, `returning`, wrapper, quotes, `on empty`, `on error`) on their own lines, one level in, and `)` on its own line: ```sql -- before (lineWidth 60) select json_value(jsonb_column_with_long_name, '$.some.very.long.path.expression.here' returning int default 0 on empty error on error) from t; -- after select json_value( jsonb_column_with_long_name, '$.some.very.long.path.expression.here' returning int default 0 on empty error on error ) from t; ``` The `passing` arguments are a group of their own, so `passing 0 as x, 2 as y` stays on one line. The three functions shared most of their emitter code, which is now written once. **Snapshot changes** All reviewed: - `fn(\n arg\n)\nfilter (where ...` / `within group (...)` → `fn(arg)` with the clause indented on the next line (`aggregates`, `ordered_set_filter`, `tuplesort`, `func_call_within_group_filter_0`, `20230405163940_download_metrics`, `window`) - array elements one per line at the enclosing indent → filled, indented lines between `array[` and `]` (`amutils`, `arrays`, `type_sanity`, `stats_ext`, `rowtypes`, `rangefuncs`, `multirangetypes`, `jsonpath`) - broken `json_value`/`json_query`/`json_exists` calls → function-call layout (`sqljson_queryfuncs`, most of the diff, and `json_returning`) New `function_call_layout.rs` tests cover `count(*) filter`, `within group`, long and short arrays, and the three JSON functions, broken and flat. **Not covered** - `json_arrayagg`/`json_objectagg`, `xmlserialize`/`xmlexists`, `position(... in ...)`, `extract(... from ...)` and a few statement headers listed in the release review can still go past the width. **Follow-ups:** The remaining width offenders are fixed in #946.
psteinroe
changed the base branch from
fix/pretty-print-compact-subquery-indent
to
main
October 26, 2026 11:38
psteinroe
added a commit
that referenced
this pull request
Oct 26, 2026
Stacked on #946. #912 listed this as not covered. The snapshot width check (`assert_line_lengths` in `tests.rs`) didn't catch lines past the width in two common cases: - It only counted the characters between string literals. A call like `json_value(cast('{"a": ...}' as jsonb), '$.a.b'` of 95 columns passed at width 80, because most of it was inside quotes. - It excused a line whose longest identifier plus 20 columns didn't fit. That is how the `json_value` layout fixed in #898 got through. **The new check** `common::width_problems` replaces it. It uses the Postgres lexer and measures the whole line, with tabs counted as `indentSize` columns. A line is only excused when its indentation, its longest token (string literals included) and 12 more columns don't fit. That covers a long literal, a long quoted identifier, a function body, or code nested so deeply that there is no room left. A `--` comment ending the line is written as in the source and is not counted. The snapshot tests and the option matrix from #912 both use it. The matrix checks width only at 80 and 120 with `castStyle: cast`: at 40 some statements can't fit, and a `::` cast can't break either (`select '...'::int8` with a compact select list). **Formatter changes it needed** The stricter check found these, and they are fixed here: - **Text after a multi-line literal.** A group that contained a multi-line literal or function body stayed on one line as long as the literal's first line fit. Whatever followed its last line was never measured, e.g. a chain of `#-` operators running to 206 columns in `explain`. The renderer now checks that the text after the last line of such a token fits too. - **`is [not] distinct from`, `like`, `ilike`, `similar to`.** These now wrap their right-hand side like a comparison operator does, instead of always printing a space. - **SQL-syntax calls** (`extract`, `position`, `overlay`, `substring`, `xmlexists`). Their arguments are a group inside the call's parentheses, so `second from cast(...)` stays together on the indented line when it fits there, and `from` only breaks when it doesn't. **Snapshot changes** All reviewed: - text after a multi-line literal moves to the next line, or the call around the literal breaks (`explain`, `jsonb_jsonpath`, `misc_functions`, `tsearch`); - `is [not] distinct from` / `like` wrap before the right-hand side (`xid`, `txid`, `opr_sanity`, `strings`); - `extract(field from source)` breaks after `from` only when the arguments don't fit the indented line (`timetz`, `interval`). New tests in `width_layout.rs` cover the check itself, `is not distinct from`, `similar to`, and text after a multi-line literal. All fixtures pass the stricter check, and the option matrix passes with it.
psteinroe
added a commit
that referenced
this pull request
Oct 26, 2026
Stacked on #950. `cast(x as type)` broke before `as` with no indent, so the type sat at the column of the enclosing expression, and `)` stuck to it. #946 listed this as not covered: ```sql -- before select cast('4 millenniums 5 centuries 4 decades 1 year 4 months 4 days 17 minutes 31 seconds' as interval); -- after select cast( '4 millenniums 5 centuries 4 decades 1 year 4 months 4 days 17 minutes 31 seconds' as interval ); ``` **What changed** CAST now uses the function-call layout from #946: on one line when it fits; otherwise `(`, then `expr as type` one level in, and `)` back on the cast's line. `expr as type` is a group of its own, so it stays together on the indented line when it fits there, and breaks before `as` only when it doesn't: ```sql -- before -cast('-2147483647 months -2147483647 days -9223372036854775807 us' as interval); -- after -cast( '-2147483647 months -2147483647 days -9223372036854775807 us' as interval ); ``` `castStyle: operator` (`x::type`) is unchanged. **Snapshot changes** 51 snapshot files change, all reviewed. The categories: - **A cast that doesn't fit:** `cast(x\nas type)` becomes `cast(\n x as type\n)` when `x as type` fits on the indented line, or `cast(\n x\n as type\n)` when it doesn't. This is most of the diff, mostly long literals: `jsonb`, `json`, `interval`, `tstypes`, `horology`, `xml`, `sqljson_queryfuncs`, `int8`, `md5`, `name`, `timestamptz`, `xid`, `txid`. - **A cast around a call that breaks:** `cast(format(` / `cast(row(` used to hug the call, with `as type)` on the last line at the enclosing indent. Now the call sits one level inside `cast(`, followed by `as type` and `)` on their own lines (`brin`, `brin_bloom`, `brin_multi`, `amutils`, `type_sanity`, `stats_import`, `fast_default`, `jsonb`). - **Deep nesting:** a broken cast around a deep chain of calls moves the whole chain one level right. The deep `replace(...)` chain in `complex_select_0` / `complex_select_part_2` shows this, and it is the most visible change. - A few casts inside `extract(`, `tuplesort`, `window`, `arrays` and `plpgsql` move the same way. New `cast_breaks_like_a_function_call` test in `width_layout.rs`. All `pgls_pretty_print` tests and the option matrix (fixtures and regression suite) pass, and clippy is clean.
psteinroe
added a commit
that referenced
this pull request
Oct 26, 2026
Stacked on #959. With `clauseBodyStyle: compact`, a lone select item stays on the `select` line inside an indent kept for its continuation lines. #938 listed this as not covered. When the item only breaks inside its own parentheses, a function call for instance, it has no continuation lines, and the indent pushed its arguments two levels in and its `)` one level in, under nothing. This is the same pattern #938 fixed for WHERE and ON: ```sql -- clauseBodyStyle: compact -- before select json_build_object( 'identifier', users.id, 'display_name', users.name ) from users; -- after select json_build_object( 'identifier', users.id, 'display_name', users.name ) from users; ``` **What changed** The compact select list leaves out the indent for a lone item without an alias that is one of these: - a function call without FILTER, WITHIN GROUP or OVER; - `coalesce(...)`, `greatest`/`least(...)` or `xmlserialize(...)`; - `cast(...)` with `castStyle: cast`; - a subquery link, using the rule from #938. Anything else keeps the indent, because it can continue on lines of its own: an alias (`... as total` can wrap), a FILTER clause, an operator chain such as `a + b +\n c`. **The FILTER layout** The other item, `filter (where` keeping `where` on the `filter (` line with `)` attached to the condition, was already fixed later in the stack: - #898 got a follow-up commit, and its description now describes it: `filter (where cond)` on one line when it fits, otherwise `filter (`, `where cond` indented, and `)` on its own line. - #946 uses the same layout for the JSON aggregates. `count_star_stays_together_when_the_filter_breaks` in `function_call_layout.rs` asserts it, so there is no separate change here. **Snapshot changes** All reviewed; one category: a lone call or subquery in a compact select list moves its arguments one level left and closes `)` under `select` (`compact_select_list*`, `scalar_subquery_expanded`). New test `a_lone_compact_select_item_closes_under_select` in `subquery_layout.rs`, covering a call and an operator chain that keeps its indent. The option matrix passes.
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 #938.
#898 put
json_value/json_query/json_existson the function-call layout and listed the other constructs the release review found past the line width. This PR handles those. Each had no break opportunity, or broke without an indent so that the continuation sat at the enclosing level.Calls whose arguments the formatter writes itself
Two new helpers,
open_call_arguments/close_call_arguments, give these the function-call layout:(, a break and one more indent when the call doesn't fit,)back on the call's line. The keyword parts that join the arguments (in,from,placing,for,passing,as, the commas) become break opportunities:json_arrayagg/json_objectagg. The call has its own group when FILTER or OVER follows, so the line breaks before the clause first, as for other aggregates since fix(pretty_print): group function calls so count(*), arrays and JSON functions break sensibly #898. Their FILTER now uses the samefilter (where ...)layout as other aggregates.xmlserialize,xmlroot,xmlelement,xmlparse,xmlpi,xmlexistsposition(... in ...),extract(... from ...),overlay(...),substring(...),trim(...),normalize(...)Statement parts
connection,publicationandwith (...)are indented continuation clauses, as in ALTER SUBSCRIPTION and GRANT. Before, a long statement printed the header on one line and then broke inside the publication list or the options.comment on ... is '...': breaks beforeis, one level in.when tag in (...): the tags fill indented lines inside the parentheses.explain (...): the options are a parenthesized list, one per line when they don't fit.json_tablecolumns: a column that doesn't fit putspath, the wrapper, quotes and theon empty/on errorclauses on their own lines, one level in.Snapshot changes
All reviewed; every change is one of the constructs above going from a too-long line, or a break without an indent, to the layout described:
subscription,object_address,create_subscription_stmt_0: subscription clausesxml: XML functionssqljson: JSON aggregates and their FILTERsqljson_jsontable: json_table columnsstrings: overlaytimetz,interval: extractplpgsql,create_table_like,comment_stmt_2: comment on ... isevent_trigger: when tag inpartition_prune: explain optionsA few calls that were broken before now fit on one line.
New
width_layout.rstests format each construct at width 60 and check every line fits, with exact output for the main shapes. The option matrix passes.Not covered
cast(x as type)still breaks beforeaswithout an indent (visible insideextractintimetz). Changing that touches most snapshots, so it is left for its own PR.