Skip to content

Commit 35d1efe

Browse files
Canonicalize inner joins to comma form once, replacing per-pass flatteners
1 parent dca34cb commit 35d1efe

7 files changed

Lines changed: 895 additions & 391 deletions

File tree

doc/source/dev/coding-conventions.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,22 @@ Naming
5252
:cfunc:`gate_type`).
5353

5454

55+
Rewrite Passes and Join Shapes
56+
------------------------------
57+
58+
A new rewrite pass in the planner hook must **not** pattern-match the
59+
FROM clause for the ``JoinExpr`` / comma-join distinction (e.g. by
60+
requiring every fromlist item to be a ``RangeTblRef``): the prologue of
61+
:cfunc:`process_query` (:cfunc:`normalize_inner_joins`) canonicalises
62+
every tracked query level -- recursively, including sublink bodies,
63+
subquery RTEs, and CTE bodies -- to the flat comma-join form, with the
64+
dissolved ``RTE_JOIN`` entries compacted away, so a pass may simply
65+
assume it. Only outer joins still appear as ``JoinExpr``\ s. When
66+
adding a feature that inspects FROM shapes, extend the
67+
``join_syntax_equivalence`` regression test with a case pair -- it
68+
exists precisely because features have repeatedly worked for one join
69+
syntax and not the other.
70+
5571
Error Reporting
5672
---------------
5773

doc/source/dev/query-rewriting.rst

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,33 @@ The function proceeds in the following order:
183183
Step 0: Prologue and FROM-less Queries
184184
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
185185

186-
A short prologue runs before anything else: inert scope-local
186+
A short prologue runs before anything else. First,
187+
:cfunc:`normalize_inner_joins` canonicalises the FROM clause: every
188+
all-inner ``JoinExpr`` fromlist item (``a JOIN b ON c``, including
189+
``USING`` / ``NATURAL`` forms) is dissolved into its leaf
190+
``RangeTblRef``\ s, with the ON conditions splayed into a flat WHERE
191+
conjunction and every reference to a dissolved join's alias columns
192+
resolved to base expressions through ``joinaliasvars``. PostgreSQL
193+
hands the hook two different Query shapes for the same inner-join
194+
semantics -- the flat comma-join form and the ``JoinExpr`` tree -- and
195+
rewrite passes that pattern-matched one shape have historically missed
196+
the other; after this pass, **every downstream pass (and any new one)
197+
may assume the flat comma-join form**. Only outer joins still appear
198+
as ``JoinExpr``\ s, for the outer-join lowering. The dissolved
199+
``RTE_JOIN`` entries are dropped from the range table, with every
200+
surviving rtindex renumbered, so shape gates that require an
201+
all-``RTE_RELATION`` range table (the safe-query candidate gate, the
202+
sublink decorrelation) match both syntaxes identically. The pass
203+
recurses -- via ``normalize_inner_joins_walker`` -- into every nested
204+
Query: sublink bodies, subquery RTEs, and CTE bodies, so e.g. a
205+
``NOT IN`` body written with ``JOIN`` syntax is already canonical when
206+
the sublink pre-passes inspect it; a whole-row reference to a
207+
dissolved join (``j.*``) declines the pass for that level. The
208+
``join_syntax_equivalence`` regression test pins the resulting
209+
guarantee: one query per feature area, run in both syntaxes, must
210+
produce identical results.
211+
212+
The rest of the prologue: inert scope-local
187213
:sqlfunc:`provenance` fetches are resolved by
188214
:cfunc:`process_inert_fetches`, the conditioning surface is rewritten
189215
by :cfunc:`rewrite_cond_predicates` (see Step 11 below), projected
@@ -313,17 +339,16 @@ Before proceeding, the function checks for:
313339

314340
- **Sublinks** (``EXISTS``, ``IN``, scalar subqueries) over a tracked
315341
relation that the decorrelation pre-passes could not rewrite: not
316-
supported. A body written with explicit inner ``JOIN`` syntax is
317-
first normalized to the comma-join form by
318-
``oj_flatten_body_inner_joins`` (ON quals move into the body WHERE;
319-
the dead ``RTE_JOIN`` entries stay in the range table), and the body
320-
may mix tracked and untracked relations -- the untracked ones ride
321-
along inside the derived cross-product subquery built by
322-
``oj_wrap_body_from``, contributing neutral provenance. What still
323-
trips the error: an outer join in the body, a reference to a join
324-
alias (``USING`` / ``NATURAL`` merged columns, ``j.*``), a nested
325-
sublink, ``LIMIT``/``OFFSET``, or a bare uncorrelated value /
326-
``count(*)`` body compared against an outer column.
342+
supported. Bodies arrive here already canonicalised to the
343+
comma-join form by :cfunc:`normalize_inner_joins` (Step 0 recurses
344+
into sublink subselects), and may mix tracked and untracked
345+
relations -- the untracked ones ride along inside the derived
346+
cross-product subquery built by ``oj_wrap_body_from``, contributing
347+
neutral provenance. What still trips the error: an outer join or a
348+
whole-row join reference in the body (both decline the
349+
canonicalisation), a nested sublink, ``LIMIT``/``OFFSET``, or a bare
350+
uncorrelated value / ``count(*)`` body compared against an outer
351+
column.
327352
- ``DISTINCT ON``: not supported.
328353
- ``DISTINCT`` (plain): converted to ``GROUP BY`` via
329354
:cfunc:`transform_distinct_into_group_by`.
@@ -683,22 +708,14 @@ linear time by :cfunc:`BooleanCircuit::independentEvaluation`,
683708
which replaces the fallback to tree decomposition or external
684709
knowledge compilation that the unrewritten circuit would require.
685710

686-
Two normalisation pre-passes run at the head of
711+
One normalisation pre-pass runs at the head of
687712
``try_safe_query_rewrite`` so the detector and rewriter see a flat
688713
fromlist of base ``RTE_RELATION`` entries regardless of how the
689-
user wrote the query:
690-
691-
- ``try_flatten_inner_joins(q)`` dissolves every ``INNER`` /
692-
``CROSS`` ``JoinExpr`` in any fromlist into flat
693-
``RangeTblRef``\ s plus AND-merged ``ON``-clauses, recursing
694-
through ``RTE_SUBQUERY`` bodies so a wrapped INNER JOIN gets
695-
flattened in the inner body before the inlining pre-pass picks
696-
it up. Refuses outer joins (NULL-padding rows break per-row
697-
independence), aliased joins (``JOIN ... AS j`` would require
698-
resolving columns through ``joinaliasvars``), and ``USING``
699-
clauses. Orphaned synthetic ``RTE_JOIN`` entries are dropped
700-
via the shared :cfunc:`compact_orphan_rtes` helper.
701-
- ``try_inline_simple_subqueries(q)`` then inlines every simple
714+
user wrote the query (explicit inner joins need no handling here:
715+
:cfunc:`normalize_inner_joins` in the ``process_query`` prologue
716+
has already dissolved them at every nesting level):
717+
718+
- ``try_inline_simple_subqueries(q)`` inlines every simple
702719
``RTE_SUBQUERY`` fromlist entry (PG-rewritten view bodies,
703720
inline ``FROM (SELECT …)`` subqueries) into the outer rtable.
704721
A subquery is "simple" when it is a flat conjunctive ``SELECT``
@@ -930,8 +947,8 @@ The extensions and their interaction with the base rewriter:
930947
read-once.
931948

932949
The interaction between these extensions and the base rewriter is
933-
ordered as: PG-18 group-RTE strip → INNER / CROSS JoinExpr
934-
flattening → simple-subquery inlining → PK self-join unification
950+
ordered as: PG-18 group-RTE strip → simple-subquery inlining →
951+
PK self-join unification
935952
→ disjoint-constant certification → candidate gate (shape /
936953
metadata / ancestry disjointness) → ``qc_split_quals`` →
937954
constant-selection pre-pass → multi-component dispatch →
@@ -952,7 +969,8 @@ regression tests live in ``test/sql/safe_query_const_sel.sql``,
952969
(cumulative regression checks for the FD closure),
953970
``safe_query_self_join_disjoint.sql``,
954971
``safe_query_view_descent.sql`` (subquery-inlining pre-pass),
955-
``safe_query_inner_join.sql`` (JoinExpr flattening), and
972+
``safe_query_inner_join.sql`` (JOIN-syntax queries reaching the
973+
gate through the prologue's inner-join canonicalisation), and
956974
``safe_query_ancestry_disjoint.sql`` (ancestry-based disjointness
957975
gate).
958976

0 commit comments

Comments
 (0)