Skip to content

fix(pretty_print): close a subquery opened on the WHERE or ON line under the keyword - #938

Merged
psteinroe merged 16 commits into
mainfrom
fix/pretty-print-compact-subquery-indent
Oct 26, 2026
Merged

psteinroe merged 16 commits into
mainfrom
fix/pretty-print-compact-subquery-indent

Conversation

@psteinroe

Copy link
Copy Markdown
Collaborator

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:

-- 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.

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
psteinroe changed the base branch from fix/pretty-print-nested-case to main October 26, 2026 11:18
@psteinroe
psteinroe merged commit 47bdcdd into main Oct 26, 2026
12 checks passed
@psteinroe
psteinroe deleted the fix/pretty-print-compact-subquery-indent branch October 26, 2026 11:38
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.
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