Every style drops the middle branch of a UNION / INTERSECT / EXCEPT chain with three or more branches. Confirmed on the published v1.4.0 and on main.
SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3;
-- formats to
SELECT 1 UNION ALL SELECT 3;
SELECT 1 UNION SELECT 2 EXCEPT SELECT 3 INTERSECT SELECT 4;
-- formats to
SELECT 1 EXCEPT SELECT 3 INTERSECT SELECT 4;
The output parses and runs, and returns different rows.
Cause
A UNION B UNION C parses left-nested: ((A UNION B) UNION C). collect_clauses_recursive() recurses into the left select_clause, sets clauses.set_op to UNION B, then returns to the outer level and sets clauses.set_op to UNION C, replacing it. SelectClauses holds one set operation, so B is lost.
Right-nested chains (A UNION (B INTERSECT C), which is how INTERSECT's higher precedence parses) are not affected, because the right side is collected recursively with its own set_op.
Fix
Append the outer operation to the end of the existing chain instead of replacing it. The formatter emits the branches as a flat sequence in source order, and PostgreSQL applies the same precedence when it parses that sequence, so the chain's grouping does not need to be kept.
Found by the token-loss guard added in #59; part of #58.
Every style drops the middle branch of a UNION / INTERSECT / EXCEPT chain with three or more branches. Confirmed on the published v1.4.0 and on
main.The output parses and runs, and returns different rows.
Cause
A UNION B UNION Cparses left-nested:((A UNION B) UNION C).collect_clauses_recursive()recurses into the leftselect_clause, setsclauses.set_optoUNION B, then returns to the outer level and setsclauses.set_optoUNION C, replacing it.SelectClausesholds one set operation, soBis lost.Right-nested chains (
A UNION (B INTERSECT C), which is how INTERSECT's higher precedence parses) are not affected, because the right side is collected recursively with its ownset_op.Fix
Append the outer operation to the end of the existing chain instead of replacing it. The formatter emits the branches as a flat sequence in source order, and PostgreSQL applies the same precedence when it parses that sequence, so the chain's grouping does not need to be kept.
Found by the token-loss guard added in #59; part of #58.