github-actions[bot] commented on code in PR #66482:
URL: https://github.com/apache/doris/pull/66482#discussion_r3773449843
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/SubqueryToApply.java:
##########
@@ -503,6 +540,199 @@ private Pair<LogicalPlan, Optional<Expression>>
addApply(SubqueryExpr subquery,
return Pair.of(logicalProject, newCorrelatedOuterExpr);
}
+ /**
+ * simplify the conjunct that contains mark join slots and infer the
behavior of each
+ * mark join slot, return the rewritten conjunct together with the mark
slots info.
+ *
+ * for each mark slot, the pair in the returned map has:
+ * Pair.first: whether the null and false values of the mark slot are
indistinguishable,
+ * i.e. the mark slot can be treated as a non-nullable
boolean. it only affects
+ * how the mark value is computed (treating null as false) and
never changes the
+ * number of output rows, so it's safe for every join type and
the filter.
+ * Pair.second: whether the original mark join can be directly eliminated
and turned into a
+ * plain semi join. a plain semi join only outputs the
matched rows, while a
+ * mark join keeps all original rows and adds a mark column,
so eliminating the
+ * mark join is only safe when discarding the unmatched rows
is already part of
+ * the containing join's semantics (inner, cross and semi
joins).
+ *
+ * when Pair.second is true, the mark slot is replaced by the true literal
in the returned
+ * conjunct, and the caller can drop the mark join slot to turn the mark
join into a plain
+ * semi join.
+ *
+ * extraEvaluationDomain extends the evaluation domain with expressions
that are not yet
+ * part of the plan but will be evaluated on the same rows later, e.g. the
generated
+ * assert_true(count(*) <= 1) that addApply synthesizes for a later
correlated scalar
+ * subquery; see collectGeneratedAssertionsOfLaterConjuncts.
+ *
+ * currentConjunctSubqueries are the subqueries of the conjunct being
processed: their own
+ * query plans are evaluated identically whether the mark join is kept or
eliminated (both
+ * the apply and the resulting semi/anti join evaluate the inner plan,
only the output row
+ * set differs), so a sensitive expression inside them cannot be affected
by the
+ * elimination and must be excluded from the evaluation domain; see
collectEvaluationDomain.
+ */
+ private Pair<Expression, Map<MarkJoinSlotReference, Pair<Boolean,
Boolean>>> simplifyConjunctWithMarkJoinSlot(
+ Expression conjunct, Plan plan, CascadesContext cascadesContext,
+ List<Expression> extraEvaluationDomain, Set<SubqueryExpr>
currentConjunctSubqueries) {
+ ExpressionRewriteContext rewriteContext = new
ExpressionRewriteContext(plan, cascadesContext);
+ Map<MarkJoinSlotReference, Pair<Boolean, Boolean>> markSlotsInfo;
+ if (conjunct.containsType(MarkJoinSlotReference.class)) {
+ List<Expression> evaluationDomain = collectEvaluationDomain(plan,
currentConjunctSubqueries);
+ evaluationDomain.addAll(extraEvaluationDomain);
+ markSlotsInfo = ExpressionUtils.inferMarkSlotNotNullMap(conjunct,
rewriteContext, evaluationDomain);
+ } else {
+ markSlotsInfo = Maps.newHashMap();
+ }
+ Map<MarkJoinSlotReference, BooleanLiteral> replaceMap =
Maps.newHashMap();
+ for (Map.Entry<MarkJoinSlotReference, Pair<Boolean, Boolean>> entry :
markSlotsInfo.entrySet()) {
+ if (entry.getValue().second) {
+ replaceMap.put(entry.getKey(), BooleanLiteral.TRUE);
+ }
+ }
+ if (!replaceMap.isEmpty()) {
+ conjunct = ExpressionUtils.replace(conjunct, replaceMap);
+ }
+ return Pair.of(conjunct, markSlotsInfo);
+ }
+
+ /*
+ * collect the complete evaluation domain of the mark slot inference: the
containing
+ * conjunct set of the filter/join, plus all the expressions inside the
correlated
+ * subquery plans. a sensitive expression (e.g. assert_true) does not need
to be inside
+ * the current conjunct: it may be a sibling conjunct of the same
filter/join, or live in
+ * a later subquery plan whose input rows are pruned together with the
outer rows when an
+ * earlier mark join is eliminated. pair.second is only safe when every
such expression
+ * is still evaluated on the same rows after the elimination, so they all
belong to the
+ * evaluation domain that the pair.second proof must be validated against.
a generated
+ * assert_true(count(*) <= 1) for a later correlated scalar subquery is
not visible here
+ * (it is synthesized by addApply after the collection), so the callers
add it separately
+ * via collectGeneratedAssertionsOfLaterConjuncts.
+ *
+ * the current conjunct's OWN subquery plans are excluded: they are
evaluated identically
+ * whether the mark join is kept or eliminated (the apply and the
resulting semi/anti
+ * join both evaluate the inner plan per outer row, or once for
uncorrelated; only the
+ * output row set differs), so a NoneMovableFunction/volatile inside them
cannot be
+ * affected by the elimination and fencing pair.second on it would only
lose valid
+ * eliminations.
+ * e.g. `ifnull(k in (select ... where assert_true(inner)), false)`: the
assert_true is
+ * inside the current conjunct's own subquery plan, which is evaluated
identically by the
+ * apply and by the resulting semi join, so eliminating the mark join
cannot suppress it;
+ * before the exclusion the mark join was kept (isMarkJoin=true) purely
because of that
+ * unreachable assert_true.
+ */
+ private List<Expression> collectEvaluationDomain(Plan plan,
Set<SubqueryExpr> currentConjunctSubqueries) {
+ List<Expression> evaluationDomain = new ArrayList<>();
+ if (plan instanceof LogicalFilter) {
+ evaluationDomain.addAll(((LogicalFilter<? extends Plan>)
plan).getConjuncts());
+ } else if (plan instanceof LogicalJoin) {
+ evaluationDomain.addAll(((LogicalJoin<?, ?>)
plan).getExpressions());
+ }
+ List<Expression> subqueryPlanExpressions = new ArrayList<>();
+ for (Expression expression : evaluationDomain) {
+ Set<SubqueryExpr> subqueries =
expression.collect(SubqueryExpr.class::isInstance);
Review Comment:
[P2] Filter plan expressions by downstream Apply reachability
Conversely, this scans every non-current subquery plan without considering
Apply order or join child. An earlier filter Apply is already below a later
target, and an opposite-side join Apply is in an independent subtree;
eliminating the target cannot skip either one's `AssertTrue`/volatile
expression. Collecting those expressions still clears Pair.second and retains
the marker-producing Apply plus its join-reordering, runtime-filter,
selectivity, and exploration barriers. Please include only plans that are
actually downstream of the target: later Applies on the filter chain, or later
Applies on the same physical join child. This is distinct from the existing
generated-assertion over-fence because these are expressions already present in
the original subquery plans.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/SubqueryToApply.java:
##########
@@ -503,6 +540,199 @@ private Pair<LogicalPlan, Optional<Expression>>
addApply(SubqueryExpr subquery,
return Pair.of(logicalProject, newCorrelatedOuterExpr);
}
+ /**
+ * simplify the conjunct that contains mark join slots and infer the
behavior of each
+ * mark join slot, return the rewritten conjunct together with the mark
slots info.
+ *
+ * for each mark slot, the pair in the returned map has:
+ * Pair.first: whether the null and false values of the mark slot are
indistinguishable,
+ * i.e. the mark slot can be treated as a non-nullable
boolean. it only affects
+ * how the mark value is computed (treating null as false) and
never changes the
+ * number of output rows, so it's safe for every join type and
the filter.
+ * Pair.second: whether the original mark join can be directly eliminated
and turned into a
+ * plain semi join. a plain semi join only outputs the
matched rows, while a
+ * mark join keeps all original rows and adds a mark column,
so eliminating the
+ * mark join is only safe when discarding the unmatched rows
is already part of
+ * the containing join's semantics (inner, cross and semi
joins).
+ *
+ * when Pair.second is true, the mark slot is replaced by the true literal
in the returned
+ * conjunct, and the caller can drop the mark join slot to turn the mark
join into a plain
+ * semi join.
+ *
+ * extraEvaluationDomain extends the evaluation domain with expressions
that are not yet
+ * part of the plan but will be evaluated on the same rows later, e.g. the
generated
+ * assert_true(count(*) <= 1) that addApply synthesizes for a later
correlated scalar
+ * subquery; see collectGeneratedAssertionsOfLaterConjuncts.
+ *
+ * currentConjunctSubqueries are the subqueries of the conjunct being
processed: their own
+ * query plans are evaluated identically whether the mark join is kept or
eliminated (both
+ * the apply and the resulting semi/anti join evaluate the inner plan,
only the output row
+ * set differs), so a sensitive expression inside them cannot be affected
by the
+ * elimination and must be excluded from the evaluation domain; see
collectEvaluationDomain.
+ */
+ private Pair<Expression, Map<MarkJoinSlotReference, Pair<Boolean,
Boolean>>> simplifyConjunctWithMarkJoinSlot(
+ Expression conjunct, Plan plan, CascadesContext cascadesContext,
+ List<Expression> extraEvaluationDomain, Set<SubqueryExpr>
currentConjunctSubqueries) {
+ ExpressionRewriteContext rewriteContext = new
ExpressionRewriteContext(plan, cascadesContext);
+ Map<MarkJoinSlotReference, Pair<Boolean, Boolean>> markSlotsInfo;
+ if (conjunct.containsType(MarkJoinSlotReference.class)) {
+ List<Expression> evaluationDomain = collectEvaluationDomain(plan,
currentConjunctSubqueries);
+ evaluationDomain.addAll(extraEvaluationDomain);
+ markSlotsInfo = ExpressionUtils.inferMarkSlotNotNullMap(conjunct,
rewriteContext, evaluationDomain);
+ } else {
+ markSlotsInfo = Maps.newHashMap();
+ }
+ Map<MarkJoinSlotReference, BooleanLiteral> replaceMap =
Maps.newHashMap();
+ for (Map.Entry<MarkJoinSlotReference, Pair<Boolean, Boolean>> entry :
markSlotsInfo.entrySet()) {
+ if (entry.getValue().second) {
+ replaceMap.put(entry.getKey(), BooleanLiteral.TRUE);
+ }
+ }
+ if (!replaceMap.isEmpty()) {
+ conjunct = ExpressionUtils.replace(conjunct, replaceMap);
+ }
+ return Pair.of(conjunct, markSlotsInfo);
+ }
+
+ /*
+ * collect the complete evaluation domain of the mark slot inference: the
containing
+ * conjunct set of the filter/join, plus all the expressions inside the
correlated
+ * subquery plans. a sensitive expression (e.g. assert_true) does not need
to be inside
+ * the current conjunct: it may be a sibling conjunct of the same
filter/join, or live in
+ * a later subquery plan whose input rows are pruned together with the
outer rows when an
+ * earlier mark join is eliminated. pair.second is only safe when every
such expression
+ * is still evaluated on the same rows after the elimination, so they all
belong to the
+ * evaluation domain that the pair.second proof must be validated against.
a generated
+ * assert_true(count(*) <= 1) for a later correlated scalar subquery is
not visible here
+ * (it is synthesized by addApply after the collection), so the callers
add it separately
+ * via collectGeneratedAssertionsOfLaterConjuncts.
+ *
+ * the current conjunct's OWN subquery plans are excluded: they are
evaluated identically
+ * whether the mark join is kept or eliminated (the apply and the
resulting semi/anti
+ * join both evaluate the inner plan per outer row, or once for
uncorrelated; only the
+ * output row set differs), so a NoneMovableFunction/volatile inside them
cannot be
+ * affected by the elimination and fencing pair.second on it would only
lose valid
+ * eliminations.
+ * e.g. `ifnull(k in (select ... where assert_true(inner)), false)`: the
assert_true is
+ * inside the current conjunct's own subquery plan, which is evaluated
identically by the
+ * apply and by the resulting semi join, so eliminating the mark join
cannot suppress it;
+ * before the exclusion the mark join was kept (isMarkJoin=true) purely
because of that
+ * unreachable assert_true.
+ */
+ private List<Expression> collectEvaluationDomain(Plan plan,
Set<SubqueryExpr> currentConjunctSubqueries) {
+ List<Expression> evaluationDomain = new ArrayList<>();
+ if (plan instanceof LogicalFilter) {
+ evaluationDomain.addAll(((LogicalFilter<? extends Plan>)
plan).getConjuncts());
+ } else if (plan instanceof LogicalJoin) {
+ evaluationDomain.addAll(((LogicalJoin<?, ?>)
plan).getExpressions());
+ }
+ List<Expression> subqueryPlanExpressions = new ArrayList<>();
+ for (Expression expression : evaluationDomain) {
+ Set<SubqueryExpr> subqueries =
expression.collect(SubqueryExpr.class::isInstance);
+ for (SubqueryExpr subquery : subqueries) {
+ // skip the current conjunct's own subquery plans: see the
domain doc above
+ if (currentConjunctSubqueries.contains(subquery)) {
+ continue;
+ }
+ collectPlanExpressions(subquery.getQueryPlan(),
subqueryPlanExpressions);
+ }
+ }
+ evaluationDomain.addAll(subqueryPlanExpressions);
+ return evaluationDomain;
+ }
+
+ private void collectPlanExpressions(Plan plan, List<Expression>
expressions) {
+ expressions.addAll(plan.getExpressions());
+ for (Plan child : plan.children()) {
+ collectPlanExpressions(child, expressions);
+ }
+ }
+
+ /*
+ * whether addApply will synthesize the runtime assert_true(count(*) <= 1)
for the
+ * subquery: a correlated scalar subquery without a top-level scalar agg
that is not
+ * limit-one-eliminated. a top-level scalar agg returns at most one row
and a
+ * limit-one-eliminated subquery is guaranteed to produce at most one row,
so no check
+ * is generated for them. the check references a count slot that only
exists after
+ * addApply, so it is invisible to collectEvaluationDomain and a preceding
mark join
+ * whose elimination prunes the rows reaching the check must be fenced.
+ */
+ private static boolean isCorrelatedScalarNeedingRuntimeCheck(SubqueryExpr
subquery) {
+ if (!(subquery instanceof ScalarSubquery)) {
+ return false;
+ }
+ ScalarSubquery scalar = (ScalarSubquery) subquery;
+ return !scalar.getCorrelateSlots().isEmpty()
+ && !scalar.hasTopLevelScalarAgg()
+ && !scalar.limitOneIsEliminated();
+ }
+
+ /*
+ * collect a representative of the runtime assert_true(count(*) <= 1) that
addApply
+ * will synthesize for every correlated scalar subquery in the conjuncts
after
+ * currentIndex: those applies are built above the current conjunct's
apply, so
+ * eliminating the current conjunct's mark join would prune the rows that
reach the
+ * generated assertion and suppress its error. only the sensitive-function
type
+ * matters for the inference fence, so a representative assertion with a
fresh count
+ * slot is enough to fence the elimination.
+ *
+ * whether a later conjunct's generated assertion is affected by the
current mark join
+ * elimination depends on the plan shape, which differs between the two
paths:
+ *
+ * filter path (FILTER_SUBQUERY_TO_APPLY, relatedInfoList == null): every
conjunct's
+ * apply is stacked on ONE chain above the filter's child (each new apply
wraps the
+ * accumulated plan), so for
+ * `t1 join t2 on xx where t1.k in (sub1) and t2.b = (scalar2)`
+ * the plan becomes
+ * project(assert_true(count<=1)) <- scalar2's generated
assertion
+ * +-- Apply1(t2.b = scalar2) <- wraps Apply0
+ * +-- Apply0(t1.k in sub1) <- mark join, wraps the
whole join
+ * +-- Join(t1, t2)
+ * even though sub1 correlates to t1 and scalar2 to t2, both applies sit
ABOVE the whole
+ * join on the same chain: the joined rows already carry both sides'
columns, and
+ * eliminating Apply0's mark join prunes joined rows BEFORE Apply1
evaluates, so the
+ * assertion of every later conjunct is downstream of every earlier mark
join. hence all
+ * later conjuncts must be fenced here.
+ *
+ * join path (JOIN_SUBQUERY_TO_APPLY, relatedInfoList != null): each
conjunct's apply is
+ * attached to the LEFT or RIGHT child of the join (collectRelatedInfo
decides the side),
+ * so for
+ * `t1 join t2 on t1.k in (sub1) and t2.b = (scalar2)`
+ * the plan becomes
+ * Join(other=[M1, t2.b = scalar2_out])
+ * +-- Apply0(t1.k in sub1) <- left subtree, mark join
+ * | +-- t1
+ * +-- project(assert_true(count<=1)) <- right subtree, scalar2's
assertion
+ * +-- Apply1(t2.b = scalar2)
+ * +-- t2
+ * the left and right subtrees are evaluated independently BEFORE the join
combines them,
+ * so eliminating the current mark join only prunes rows on its OWN side:
an OPPOSITE-side
+ * later conjunct is unaffected and must NOT be fenced (the pre-fix code
fenced it, losing
+ * a valid elimination), while a SAME-side later conjunct is stacked above
the current
+ * apply and IS fenced.
+ */
+ private List<Expression> collectGeneratedAssertionsOfLaterConjuncts(
+ int currentIndex, List<Set<SubqueryExpr>> subqueryExprsList,
+ List<RelatedInfo> relatedInfoList) {
+ List<Expression> generatedAssertions = new ArrayList<>();
+ for (int j = currentIndex + 1; j < subqueryExprsList.size(); ++j) {
+ if (relatedInfoList != null && relatedInfoList.get(j) !=
relatedInfoList.get(currentIndex)) {
Review Comment:
[P1] Compare the selected Apply child rather than `RelatedInfo` identity
`Unrelated` and `RelatedToRight` compare unequal here, but the construction
above sends both through the right-child branch. For inner-join ON conjuncts
ordered as an uncorrelated `EXISTS (SELECT 1 FROM empty_e)` followed by an
output-used right-correlated scalar, the first marker is removable and this
check skips the later scalar assertion. The marker-free EXISTS becomes a cross
join with `Limit(1, empty_e)`, removes every right row below the scalar Apply,
and suppresses a duplicate-group cardinality error; the retained mark form
preserves those rows with a false marker and raises. Please compare effective
physical sides (only `RelatedToLeft` maps left; `Unrelated` and
`RelatedToRight` map right) and add this expected-error regression.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/SubqueryToApply.java:
##########
@@ -503,6 +540,199 @@ private Pair<LogicalPlan, Optional<Expression>>
addApply(SubqueryExpr subquery,
return Pair.of(logicalProject, newCorrelatedOuterExpr);
}
+ /**
+ * simplify the conjunct that contains mark join slots and infer the
behavior of each
+ * mark join slot, return the rewritten conjunct together with the mark
slots info.
+ *
+ * for each mark slot, the pair in the returned map has:
+ * Pair.first: whether the null and false values of the mark slot are
indistinguishable,
+ * i.e. the mark slot can be treated as a non-nullable
boolean. it only affects
+ * how the mark value is computed (treating null as false) and
never changes the
+ * number of output rows, so it's safe for every join type and
the filter.
+ * Pair.second: whether the original mark join can be directly eliminated
and turned into a
+ * plain semi join. a plain semi join only outputs the
matched rows, while a
+ * mark join keeps all original rows and adds a mark column,
so eliminating the
+ * mark join is only safe when discarding the unmatched rows
is already part of
+ * the containing join's semantics (inner, cross and semi
joins).
+ *
+ * when Pair.second is true, the mark slot is replaced by the true literal
in the returned
+ * conjunct, and the caller can drop the mark join slot to turn the mark
join into a plain
+ * semi join.
+ *
+ * extraEvaluationDomain extends the evaluation domain with expressions
that are not yet
+ * part of the plan but will be evaluated on the same rows later, e.g. the
generated
+ * assert_true(count(*) <= 1) that addApply synthesizes for a later
correlated scalar
+ * subquery; see collectGeneratedAssertionsOfLaterConjuncts.
+ *
+ * currentConjunctSubqueries are the subqueries of the conjunct being
processed: their own
+ * query plans are evaluated identically whether the mark join is kept or
eliminated (both
+ * the apply and the resulting semi/anti join evaluate the inner plan,
only the output row
+ * set differs), so a sensitive expression inside them cannot be affected
by the
+ * elimination and must be excluded from the evaluation domain; see
collectEvaluationDomain.
+ */
+ private Pair<Expression, Map<MarkJoinSlotReference, Pair<Boolean,
Boolean>>> simplifyConjunctWithMarkJoinSlot(
+ Expression conjunct, Plan plan, CascadesContext cascadesContext,
+ List<Expression> extraEvaluationDomain, Set<SubqueryExpr>
currentConjunctSubqueries) {
+ ExpressionRewriteContext rewriteContext = new
ExpressionRewriteContext(plan, cascadesContext);
+ Map<MarkJoinSlotReference, Pair<Boolean, Boolean>> markSlotsInfo;
+ if (conjunct.containsType(MarkJoinSlotReference.class)) {
+ List<Expression> evaluationDomain = collectEvaluationDomain(plan,
currentConjunctSubqueries);
+ evaluationDomain.addAll(extraEvaluationDomain);
+ markSlotsInfo = ExpressionUtils.inferMarkSlotNotNullMap(conjunct,
rewriteContext, evaluationDomain);
+ } else {
+ markSlotsInfo = Maps.newHashMap();
+ }
+ Map<MarkJoinSlotReference, BooleanLiteral> replaceMap =
Maps.newHashMap();
+ for (Map.Entry<MarkJoinSlotReference, Pair<Boolean, Boolean>> entry :
markSlotsInfo.entrySet()) {
+ if (entry.getValue().second) {
+ replaceMap.put(entry.getKey(), BooleanLiteral.TRUE);
+ }
+ }
+ if (!replaceMap.isEmpty()) {
+ conjunct = ExpressionUtils.replace(conjunct, replaceMap);
+ }
+ return Pair.of(conjunct, markSlotsInfo);
+ }
+
+ /*
+ * collect the complete evaluation domain of the mark slot inference: the
containing
+ * conjunct set of the filter/join, plus all the expressions inside the
correlated
+ * subquery plans. a sensitive expression (e.g. assert_true) does not need
to be inside
+ * the current conjunct: it may be a sibling conjunct of the same
filter/join, or live in
+ * a later subquery plan whose input rows are pruned together with the
outer rows when an
+ * earlier mark join is eliminated. pair.second is only safe when every
such expression
+ * is still evaluated on the same rows after the elimination, so they all
belong to the
+ * evaluation domain that the pair.second proof must be validated against.
a generated
+ * assert_true(count(*) <= 1) for a later correlated scalar subquery is
not visible here
+ * (it is synthesized by addApply after the collection), so the callers
add it separately
+ * via collectGeneratedAssertionsOfLaterConjuncts.
+ *
+ * the current conjunct's OWN subquery plans are excluded: they are
evaluated identically
+ * whether the mark join is kept or eliminated (the apply and the
resulting semi/anti
+ * join both evaluate the inner plan per outer row, or once for
uncorrelated; only the
+ * output row set differs), so a NoneMovableFunction/volatile inside them
cannot be
+ * affected by the elimination and fencing pair.second on it would only
lose valid
+ * eliminations.
+ * e.g. `ifnull(k in (select ... where assert_true(inner)), false)`: the
assert_true is
+ * inside the current conjunct's own subquery plan, which is evaluated
identically by the
+ * apply and by the resulting semi join, so eliminating the mark join
cannot suppress it;
+ * before the exclusion the mark join was kept (isMarkJoin=true) purely
because of that
+ * unreachable assert_true.
+ */
+ private List<Expression> collectEvaluationDomain(Plan plan,
Set<SubqueryExpr> currentConjunctSubqueries) {
+ List<Expression> evaluationDomain = new ArrayList<>();
+ if (plan instanceof LogicalFilter) {
+ evaluationDomain.addAll(((LogicalFilter<? extends Plan>)
plan).getConjuncts());
+ } else if (plan instanceof LogicalJoin) {
+ evaluationDomain.addAll(((LogicalJoin<?, ?>)
plan).getExpressions());
+ }
+ List<Expression> subqueryPlanExpressions = new ArrayList<>();
+ for (Expression expression : evaluationDomain) {
+ Set<SubqueryExpr> subqueries =
expression.collect(SubqueryExpr.class::isInstance);
+ for (SubqueryExpr subquery : subqueries) {
+ // skip the current conjunct's own subquery plans: see the
domain doc above
+ if (currentConjunctSubqueries.contains(subquery)) {
Review Comment:
[P1] Include later same-conjunct Applies in this target's safety domain
This excludes every subquery plan in the current conjunct, while
`collectGeneratedAssertionsOfLaterConjuncts` starts only at the next conjunct
index. In a single filter conjunct such as `nvl(nvl(o.k IN (...), false) AND
o.x = (SELECT s.v ... WHERE s.g = o.g), false)`, preorder builds the IN Apply
below the scalar Apply and Pair.second is true for the IN marker. Removing it
creates a left-semi join that can discard the only outer row whose scalar group
has multiple results before the later `Count`/`AssertTrue` runs, so the query
returns instead of raising `correlate scalar subquery must return only 1 row`.
The existing later-conjunct fence does not cover this same-index shape. Please
make the domain target/order-specific: exclude the target and already-lower
Applies, but include subsequent same-conjunct sensitive plans and output-used
generated scalar assertions, with an expected-error regression.
##########
regression-test/suites/query_p0/subquery/subquery_unnesting.groovy:
##########
@@ -145,4 +146,294 @@ suite ("subquery_unnesting") {
FROM (SELECT 1 AS x) t
WHERE 1 NOT IN (SELECT CAST(NULL AS INT));
"""
+
+ // =====================================================================
+ // mark join elimination in the join ON condition.
+ //
+ // inferMarkSlotNotNullMap returns a pair for each mark join slot:
+ // Pair.first : the null and false values of the mark slot are
+ // indistinguishable, so the mark slot can be treated as a
+ // non-nullable boolean (null is computed as false when
+ // producing the mark value). this never changes the number
+ // of output rows, so it's safe for all join types.
+ // Pair.second: the original mark join can be directly eliminated and
+ // turned into a plain semi join. a plain semi join only
+ // outputs the matched rows, while a mark join keeps all
+ // original rows plus the mark column, so eliminating the
+ // mark join is only safe for inner, cross and semi joins
+ // where dropping the unmatched rows is already part of the
+ // join semantics.
+ //
+ // take the query below as an example:
+ // select t1.* from t1 left join t2 on t1.k2 = t2.k3
+ // and t1.k1 in (select t3.k1 from t3 where t1.k2 = t3.k2)
+ // for the outer join the mark join must be kept: the unmatched left rows
+ // (mark = false/null) must be preserved with null columns of t2, while a
+ // plain semi join would drop them. so the analyzed plan must keep
+ // isMarkJoin=true and only infer the non-nullable mark
+ // (isMarkJoinSlotNotNull=true). for inner/cross/semi join the unmatched
+ // rows are dropped anyway, so the mark join can be safely eliminated
+ // (isMarkJoin=false and the mark slot is replaced by the true literal).
+ //
+ // note: anti join also keeps the mark join for the null-aware semantics
+ // of NOT IN, but executing an anti join with a subquery in its ON
+ // condition is a pre-existing unsupported path in physical planning, so
+ // only the analyzed plan is checked here. asof join's ON clause only
+ // allows equal conjuncts, so a subquery can never appear in it and the
+ // asof branch in the code is defensive only.
+ // =====================================================================
+
+ // inner join: the mark join is eliminated (isMarkJoin=false,
MarkJoinSlotReference=empty)
+ explain {
+ sql("""analyzed plan select t1.* from t1 inner join t2 on t1.k2 = t2.k3
+ and t1.k1 in (select t3.k1 from t3 where t1.k2 = t3.k2) order
by t1.k1, t1.k2;""")
+ contains("isMarkJoin=false")
+ contains("MarkJoinSlotReference=empty")
+ }
+ // semi join: the mark join is eliminated too
+ explain {
+ sql("""analyzed plan select t1.* from t1 left semi join t2 on t1.k2 =
t2.k3
+ and t1.k1 in (select t3.k1 from t3 where t1.k2 = t3.k2) order
by t1.k1, t1.k2;""")
+ contains("isMarkJoin=false")
+ contains("MarkJoinSlotReference=empty")
+ }
+ // outer join: the mark join must be kept, and the mark slot's null/false
+ // equivalence (isMarkJoinSlotNotNull=true) is still inferred
+ explain {
+ sql("""analyzed plan select t1.* from t1 left join t2 on t1.k2 = t2.k3
+ and t1.k1 in (select t3.k1 from t3 where t1.k2 = t3.k2) order
by t1.k1, t1.k2;""")
+ contains("isMarkJoin=true")
+ contains("isMarkJoinSlotNotNull=true")
+ }
+ // anti join: the mark join must be kept for the null-aware semantics of
NOT IN
+ explain {
+ sql("""analyzed plan select t1.* from t1 left anti join t2 on t1.k2 =
t2.k3
+ and t1.k1 not in (select t3.k1 from t3 where t1.k2 = t3.k2)
order by t1.k1, t1.k2;""")
+ contains("isMarkJoin=true")
+ contains("isMarkJoinSlotNotNull=true")
+ }
+
+ // result checks: the mark join elimination must not change the query
results.
+ // (the outer join results with subquery in the ON condition are already
+ // covered by qt_select37 / qt_select43, the mark join is kept there)
+ // inner join with IN subquery in the ON condition (mark join eliminated)
+ qt_select66 """select t1.* from t1 inner join t2 on t1.k2 = t2.k3 and
t1.k1 in (select t3.k1 from t3 where t1.k2 = t3.k2) order by t1.k1, t1.k2;"""
+ // inner join with NOT IN subquery in the ON condition (mark join
eliminated)
+ qt_select67 """select t1.* from t1 inner join t2 on t1.k2 = t2.k3 and
t1.k1 not in (select t3.k1 from t3 where t1.k2 = t3.k2) order by t1.k1,
t1.k2;"""
+ // left semi join with IN subquery in the ON condition (mark join
eliminated)
+ qt_select68 """select t1.* from t1 left semi join t2 on t1.k2 = t2.k3 and
t1.k1 in (select t3.k1 from t3 where t1.k2 = t3.k2) order by t1.k1, t1.k2;"""
+
+ // error-behavior regression: the mark join must NOT be eliminated when
the filter
+ // contains assert_true (a NoneMovableFunction). although M = false and M
= null both
+ // fold the predicate to false (the row-truth proof), eliminating the mark
join changes
+ // which rows reach assert_true: the semi join prunes the unmatched rows
before the
+ // filter, so assert_true is no longer evaluated on them and its error is
suppressed.
+ // with the mark join kept, all rows reach the filter and assert_true
throws on the
+ // unmatched guard = false rows.
+ // data: M = assert_t.k1 in (assert_s.k1 where assert_s.k2 = assert_t.k2),
so only
+ // row (2,2) matches; guard = assert_t.k2 = 2 is false exactly on the
unmatched rows
+ // (1,1) and (3,3)
+ sql "drop table if exists assert_t"
+ sql "drop table if exists assert_s"
+ sql """create table assert_t (k1 bigint, k2 bigint) DUPLICATE KEY(k1)
+ DISTRIBUTED BY HASH(k2) BUCKETS 1
PROPERTIES('replication_num'='1');"""
+ sql """create table assert_s (k1 bigint, k2 bigint) DUPLICATE KEY(k1)
+ DISTRIBUTED BY HASH(k2) BUCKETS 1
PROPERTIES('replication_num'='1');"""
+ sql """insert into assert_t values (1,1),(2,2),(3,3);"""
+ sql """insert into assert_s values (2,2);"""
+ test {
+ sql """select assert_t.k1 from assert_t
+ where ifnull(
+ ifnull(assert_t.k1 in (select assert_s.k1 from assert_s
+ where assert_s.k2 = assert_t.k2), false)
+ and assert_true(assert_t.k2 = 2, 'assert failed'),
+ false);"""
+ exception "assert failed"
+ }
+
+ // error-behavior regressions for the "complete evaluation domain" fence:
the mark join
+ // must not be eliminated when a NoneMovableFunction (assert_true) exists
anywhere in the
+ // affected evaluation domain, even if it is NOT inside the mark conjunct
itself.
+ sql "drop table if exists assert_u"
+ sql """create table assert_u (k1 bigint, k2 bigint) DUPLICATE KEY(k1)
+ DISTRIBUTED BY HASH(k2) BUCKETS 1
PROPERTIES('replication_num'='1');"""
+ sql """insert into assert_u values (1,1),(2,2),(3,3);"""
+
+ // sibling conjunct: assert_true is a SIBLING conjunct of the eliminable
mark conjunct.
+ // the mark conjunct must be a MARKER-REQUIRING form (ifnull(k1 in (...),
false)): a bare
+ // `k1 not in (...)` is itself a top-level SubqueryExpr, so
shouldOutputMarkJoinSlot returns
+ // false and no mark slot is created (it becomes a plain left anti join
that emits the
+ // unmatched rows anyway, so the test would stay green even without the
fence). with the
+ // mark form the analyzed plan must keep isMarkJoin=true, which is the
sensitive signal:
+ // removing the complete evaluation domain fence turns it into
isMarkJoin=false (the mark
+ // join is eliminated into a semi join). note that the error behavior is
NOT the sensitive
+ // signal here — assert_true(k2 = 2) only references the outer columns, so
the optimizer
+ // pushes it below the join (into the outer scan) and it raises the error
on every outer
+ // row regardless of the elimination.
+ explain {
+ sql("""analyzed plan select assert_t.k1 from assert_t
+ where ifnull(assert_t.k1 in (select assert_s.k1 from assert_s
+ where assert_s.k2 = assert_t.k2), false)
+ and assert_true(assert_t.k2 = 2, 'assert failed');""")
+ contains("isMarkJoin=true")
+ }
+
+ // sensitive expression inside a later subquery plan: assert_true lives in
the filter of
+ // a LATER subquery (the EXISTS one). marker replacement erases that plan
from the
+ // earlier IN conjunct, so the complete evaluation domain (including all
subquery plans)
+ // must fence the earlier IN apply from being eliminated into a semi join.
the 'assert
+ // failed' error must still be raised.
+ test {
+ sql """select assert_t.k1 from assert_t
+ where ifnull(assert_t.k1 in (select assert_s.k1 from assert_s
+ where assert_s.k2 = assert_t.k2), false)
+ and exists (select 1 from assert_u
+ where assert_u.k2 = assert_t.k2
+ and assert_true(assert_u.k1 = 2, 'assert
failed'));"""
+ exception "assert failed"
+ }
+
+ // error-behavior regression for the retained-mark non-nullable inference
(pair.first):
+ // ((M and assert_true(guard, 'bad')) or flag) keeps the mark join
(pair.second = false)
+ // and pair.first alone would mark M non-nullable. M = k1 in (select null)
is NULL for
+ // every row (null in the build side), and treating that null as false
changes how
+ // assert_true is evaluated: the vectorized AND evaluates its right
operand for a
+ // nullable null input but can return early for an all-false non-null
column, so the
+ // required 'assert failed' error would be suppressed. pair.first must be
fenced to
+ // false so M stays null and assert_true is evaluated on every row.
+ sql "drop table if exists null_src"
+ sql """create table null_src (v bigint null) DUPLICATE KEY(v)
+ DISTRIBUTED BY HASH(v) BUCKETS 1
PROPERTIES('replication_num'='1');"""
+ sql """insert into null_src values (null);"""
+ sql "drop table if exists guard_t"
+ sql """create table guard_t (k1 bigint, guard bigint) DUPLICATE KEY(k1)
+ DISTRIBUTED BY HASH(k1) BUCKETS 1
PROPERTIES('replication_num'='1');"""
+ sql """insert into guard_t values (1,0),(2,0),(3,0);"""
+ test {
+ sql """select guard_t.k1 from guard_t
+ where (guard_t.k1 in (select null from null_src)
+ and assert_true(guard_t.guard = 1, 'assert failed'))
+ or guard_t.guard = 2;"""
+ exception "assert failed"
+ }
+
+ // =====================================================================
+ // split-fence regression: for an uncorrelated NULLABLE positive IN in a
join ON
+ // condition with a sensitive SIBLING expression, pair.first
(isMarkJoinSlotNotNull)
+ // must be kept while pair.second (isMarkJoin=true) is fenced.
+ //
+ // the mark predicate `uncor_in_t1.k in (select c from uncor_in_t3)` is
itself CLEAN
+ // (no assert_true inside it), so the current-predicate fence does not
apply to it.
+ // the sibling assert_true only lives in the evaluation domain, so it
fences pair.second
+ // only: the mark join is kept (isMarkJoin=true) because eliminating it
would prune the
+ // unmatched rows before assert_true, but pair.first stays true because
the sibling
+ // cannot observe this generated marker's null-vs-false mapping
(pair.first keeps the
+ // apply and only maps the marker's null to false).
+ //
+ // this matters beyond the analyzed plan: with isMarkJoinSlotNotNull=true,
InApplyToJoin
+ // moves the (nullable) IN equality into the hash conjuncts, so
JoinUtils.couldShuffle
+ // stays true and the physical planner keeps the shuffle alternative. if
pair.first were
+ // wrongly fenced (isMarkJoinSlotNotNull=false), the equality would stay
in the
+ // markConjuncts only, producing a standalone mark join with no hash
conjuncts, which
+ // couldShuffle forces to broadcast. the uncorrelated IN (no correlation
hash conjunct)
+ // makes this mark join the only join deciding the distribution, so the
regression pins
+ // the isMarkJoinSlotNotNull signal directly.
+ sql "drop table if exists uncor_in_t1"
+ sql "drop table if exists uncor_in_t2"
+ sql "drop table if exists uncor_in_t3"
+ sql """create table uncor_in_t1 (k bigint, a bigint) DUPLICATE KEY(k)
+ DISTRIBUTED BY HASH(k) BUCKETS 1
PROPERTIES('replication_num'='1');"""
+ sql """create table uncor_in_t2 (b bigint) DUPLICATE KEY(b)
+ DISTRIBUTED BY HASH(b) BUCKETS 1
PROPERTIES('replication_num'='1');"""
+ sql """create table uncor_in_t3 (c bigint null) DUPLICATE KEY(c)
+ DISTRIBUTED BY HASH(c) BUCKETS 1
PROPERTIES('replication_num'='1');"""
+ sql """insert into uncor_in_t1 values (1,1),(2,2),(3,3);"""
+ sql """insert into uncor_in_t2 values (1),(2);"""
+ sql """insert into uncor_in_t3 values (1),(null);"""
+ explain {
+ sql("""analyzed plan select uncor_in_t1.* from uncor_in_t1
+ join uncor_in_t2 on uncor_in_t1.a = uncor_in_t2.b
+ and uncor_in_t1.k in (select c from uncor_in_t3)
+ and assert_true(uncor_in_t1.k > 0, 'assert failed')
+ order by uncor_in_t1.k;""")
+ contains("isMarkJoin=true")
+ contains("isMarkJoinSlotNotNull=true")
+ }
+
+ // =====================================================================
+ // current-conjunct-own-subquery exclusion regression: a
NoneMovableFunction inside
+ // the CURRENT conjunct's OWN subquery plan must NOT fence the mark join
elimination.
+ //
+ // the mark predicate `ifnull(k1 in (select ...), false)` is clean, and
the assert_true
+ // lives inside the subquery's own plan (its filter). the inner plan is
evaluated
+ // identically whether the mark join is kept or eliminated: both the apply
and the
+ // resulting semi join evaluate the subquery (per outer row for a
correlated subquery),
+ // only the output row set differs, so assert_true inside it cannot be
affected by the
+ // elimination. the evaluation domain must therefore exclude the current
conjunct's own
+ // subquery plans; otherwise the mark join is kept (isMarkJoin=true)
purely because of a
+ // sensitive expression the elimination cannot reach. with the exclusion
the mark join is
+ // eliminated (isMarkJoin=false) and the subquery is still evaluated, so
assert_true
+ // still raises its error on the inner row with k2 = 0.
+ sql "drop table if exists inner_assert_t"
+ sql "drop table if exists inner_assert_s"
+ sql """create table inner_assert_t (k1 bigint, k2 bigint) DUPLICATE KEY(k1)
+ DISTRIBUTED BY HASH(k2) BUCKETS 1
PROPERTIES('replication_num'='1');"""
+ sql """create table inner_assert_s (k1 bigint, k2 bigint) DUPLICATE KEY(k1)
+ DISTRIBUTED BY HASH(k2) BUCKETS 1
PROPERTIES('replication_num'='1');"""
+ sql """insert into inner_assert_t values (1,1),(2,2),(3,3),(4,4);"""
+ sql """insert into inner_assert_s values (2,2),(4,0);"""
+ explain {
+ sql("""analyzed plan select inner_assert_t.k1 from inner_assert_t
+ where ifnull(inner_assert_t.k1 in (select inner_assert_s.k1
from inner_assert_s
+ where inner_assert_s.k1 = inner_assert_t.k1
+ and assert_true(inner_assert_s.k2 > 0, 'assert
failed')), false)
+ order by inner_assert_t.k1;""")
+ contains("isMarkJoin=false")
+ }
+ test {
+ sql """select inner_assert_t.k1 from inner_assert_t
+ where ifnull(inner_assert_t.k1 in (select inner_assert_s.k1
from inner_assert_s
+ where inner_assert_s.k1 = inner_assert_t.k1
+ and assert_true(inner_assert_s.k2 > 0, 'assert
failed')), false)
+ order by inner_assert_t.k1;"""
+ exception "assert failed"
+ }
+
+ // =====================================================================
+ // opposite-side exclusion regression: in the JOIN path, a later
correlated scalar on
+ // the OPPOSITE side of the join must NOT fence the current mark join
elimination.
+ //
+ // the join has two subquery conjuncts on opposite sides: a mark IN
correlated to the
+ // left (side_join_t.k in (select ... where side_join_s.g =
side_join_t.g)) and a later
+ // correlated scalar correlated to the right (side_join_u.b = (select
side_join_v.c ...
+ // where side_join_v.h = side_join_u.h)), which generates the runtime
+ // assert_true(count(*) <= 1) in the right subtree. the right subtree is
an independent
+ // branch: eliminating the left mark join (semi join pruning the left
rows) does not
+ // change which right rows reach the generated assertion, so the fence on
it is
+ // unnecessary and the left mark join can be eliminated (isMarkJoin=false).
+ sql "drop table if exists side_join_t"
+ sql "drop table if exists side_join_s"
+ sql "drop table if exists side_join_u"
+ sql "drop table if exists side_join_v"
+ sql """create table side_join_t (k bigint, g bigint) DUPLICATE KEY(k)
+ DISTRIBUTED BY HASH(k) BUCKETS 1
PROPERTIES('replication_num'='1');"""
+ sql """create table side_join_s (k bigint, g bigint) DUPLICATE KEY(k)
+ DISTRIBUTED BY HASH(k) BUCKETS 1
PROPERTIES('replication_num'='1');"""
+ sql """create table side_join_u (b bigint, h bigint) DUPLICATE KEY(b)
+ DISTRIBUTED BY HASH(b) BUCKETS 1
PROPERTIES('replication_num'='1');"""
+ sql """create table side_join_v (c bigint, h bigint) DUPLICATE KEY(c)
+ DISTRIBUTED BY HASH(c) BUCKETS 1
PROPERTIES('replication_num'='1');"""
+ sql """insert into side_join_t values (1,1),(2,2);"""
+ sql """insert into side_join_s values (1,1),(2,2),(3,3);"""
+ sql """insert into side_join_u values (10,10),(20,20);"""
+ sql """insert into side_join_v values (100,10),(200,20);"""
+ explain {
+ sql("""analyzed plan select side_join_t.k, side_join_u.b from
side_join_t
+ join side_join_u on side_join_t.k in (select side_join_s.k
from side_join_s
+ where side_join_s.g = side_join_t.g)
+ and side_join_u.b = (select side_join_v.c from side_join_v
+ where side_join_v.h = side_join_u.h)
+ order by side_join_t.k;""")
+ contains("isMarkJoin=false")
Review Comment:
[P2] Make this assertion target the IN Apply
This query has both the target left-side IN Apply and a right-side scalar
Apply. The scalar Apply never has a marker, so it independently prints
`isMarkJoin=false`. A regression that incorrectly retains the IN marker
therefore contains both `isMarkJoin=true` and `isMarkJoin=false`, and this
positive substring check still passes. Please also assert
`notContains("isMarkJoin=true")` or match the exact target Apply shape so the
test fails when the intended elimination is lost.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]