Skip to content

Keep parentheses that a clause needs around a subquery or row - #97

Open
wleeaf wants to merge 1 commit into
nene:masterfrom
wleeaf:keep-required-subquery-parens
Open

wleeaf wants to merge 1 commit into
nene:masterfrom
wleeaf:keep-required-subquery-parens

Conversation

@wleeaf

@wleeaf wleeaf commented Oct 25, 2026

Copy link
Copy Markdown

The paren_expr printer collapses nested parentheses into a single pair. That's fine for (((1 + 2))). But when the inner pair belongs to a subquery or a row constructor and the outer pair is part of the enclosing clause, both are needed, and the output is no longer valid SQL:

CREATE POLICY admin_only ON permissions
USING ((SELECT is_admin FROM users WHERE name = current_user))

is formatted with USING (SELECT is_admin FROM users WHERE name = current_user), which PostgreSQL rejects with a syntax error at SELECT. The same thing happens with:

  • WITH CHECK ((SELECT ...));
  • GENERATED ALWAYS AS ((SELECT ...)) STORED;
  • a row in GENERATED ALWAYS AS ((a * 2, a * 3));
  • PARTITION BY RANGE (((a, b))), which comes out as RANGE ((a, b)).

Nested parentheses are still collapsed, except when the inner pair contains a subquery (select_stmt or compound_select_stmt) or a list. Then both pairs are kept. In plain expressions this can leave a redundant pair, as in SELECT ((SELECT 1)), but it never removes one that is needed.

Test: "preserves parenthesis around subquery used as the USING condition" in test/ddl/policy.test.ts. It fails without the change and passes with it. yarn test passes (1060 passed, 1 skipped), as do yarn ts:check and prettier --check on the changed files.

I found this by formatting the statements of PostgreSQL's regression tests with the postgresql parser and comparing the libpg_query parse tree of each output with that of the input. Together with the separate row-constructor fix (#96), the 8 statements where formatting changed the meaning through parentheses now keep it, and no other statement changed.

The paren_expr printer collapses nested parentheses into one pair. When
the inner pair belongs to a subquery or a row constructor and the outer
pair is part of the enclosing clause, both are required, and dropping
one gives invalid SQL:

    CREATE POLICY admin_only ON permissions
    USING ((SELECT is_admin FROM users WHERE name = current_user))

was formatted with USING (SELECT ...), which PostgreSQL rejects. The
same happened with GENERATED ALWAYS AS ((SELECT ...)) and with a row in
PARTITION BY RANGE (((a, b))).

Nested parentheses are now kept when the inner ones contain a subquery
or a list.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant