Repository navigation
fix(pretty_print): close a subquery opened on the WHERE or ON line under the keyword - #938
Merged
Merged
Conversation
…functions break sensibly
…print-grant-admin-option
…int-long-identifiers
This was referenced Oct 24, 2026
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
…thin the line width (#946) Stacked on #938. #898 put `json_value`/`json_query`/`json_exists` on 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 #898. Their FILTER now uses the same `filter (where ...)` layout as other aggregates. - `xmlserialize`, `xmlroot`, `xmlelement`, `xmlparse`, `xmlpi`, `xmlexists` - `position(... in ...)`, `extract(... from ...)`, `overlay(...)`, `substring(...)`, `trim(...)`, `normalize(...)` ```sql -- before (lineWidth 60) select position(some_long_substring_column_name in some_other_long_string_column_name) from t; -- after select position( some_long_substring_column_name in some_other_long_string_column_name ) from t; ``` **Statement parts** - **CREATE SUBSCRIPTION:** `connection`, `publication` and `with (...)` 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 before `is`, one level in. - **Event trigger `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_table` columns:** a column that doesn't fit puts `path`, the wrapper, quotes and the `on empty` / `on error` clauses 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 clauses - `xml`: XML functions - `sqljson`: JSON aggregates and their FILTER - `sqljson_jsontable`: json_table columns - `strings`: overlay - `timetz`, `interval`: extract - `plpgsql`, `create_table_like`, `comment_stmt_2`: comment on ... is - `event_trigger`: when tag in - `partition_prune`: explain options A few calls that were broken before now fit on one line. New `width_layout.rs` tests 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 before `as` without an indent (visible inside `extract` in `timetz`). Changing that touches most snapshots, so it is left for its own PR.
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 #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: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:exists (...),x in (...),x = any (...),(...),array(...);x <op> (subquery)orx <op> array(subquery), wherexis a column, parameter or constant;notin 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 compactwhere/havingline, or on anonline, move one level left, so)closes under the keyword. This is incompact_select_list*,scalar_subquery_expanded,sublink_bool_groups_expandedand, for ON with the default config,predicate.New tests in
subquery_layout.rscoverwhere exists (,where x = (, a chain with anexistsoperand, andon 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.