@@ -183,7 +183,33 @@ The function proceeds in the following order:
183183Step 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
189215by :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`,
683708which replaces the fallback to tree decomposition or external
684709knowledge 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
688713fromlist 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
932949The 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 /
936953metadata / ancestry disjointness) → ``qc_split_quals `` →
937954constant-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
957975gate).
958976
0 commit comments