This is an automated email from the ASF dual-hosted git repository.
morrySnow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 1271f00190d [fix](rbo) Guard union equal-set propagation (#67886)
1271f00190d is described below
commit 1271f00190d8ab7881adde1fdc78519fc5cb9ff3
Author: morrySnow <[email protected]>
AuthorDate: Sun Sep 20 10:22:16 2026 +0800
[fix](rbo) Guard union equal-set propagation (#67886)
## Problem
`UNION ALL` could publish output-column equality that does not hold for
every row. Downstream rules may then remove a required window ordering
key and change `RANK()` results.
Two forms reproduce the problem:
1. Regular children project columns in a different order from their
internal output. With child filters proving `a = b`, a union that
projects `(c, a, b)` can incorrectly map that equality to `(c, a)` and
remove `a` from `ORDER BY c, a`.
2. A regular child proves `a = b`, but constant rows such as `(1, 2)`
and `(2, 1)` do not. The union can still claim the output columns are
equal and remove `b` from `ORDER BY a, b`.
## Root cause
Logical and physical unions independently mapped child equality sets
through `child.getOutput()` positions. The authoritative union ordinal
mapping is `regularChildrenOutputs`, which can have a different order.
The derivation also considered only regular children and ignored every
constant row carried by the union.
## Reproduction
Create rows `(1, 1, 10)`, `(2, 2, 10)`, and `(1, 1, 20)`, duplicate a
filtered `(c, a, b)` child with `UNION ALL`, and compute `RANK() OVER
(ORDER BY c, a)`. The two rows with `(c, a) = (10, 2)` must have rank 3.
Separately, union filtered table rows satisfying `a = b` with constant
rows `(1, 2)` and `(2, 1)`, then compute `RANK() OVER (ORDER BY a, b)`.
The four ordered pairs must receive ranks 1, 2, 3, and 4.
## Fix
- Share one equal-set derivation between logical and physical unions.
- Validate and use each regular child's explicit union-output mapping.
- Intersect child equality classes by per-ordinal class signatures,
avoiding pairwise quadratic candidate generation.
- Refine candidates against every constant row after SQL comparison
coercion and constant folding.
- Accept only a folded `TRUE`; `NULL`, unsupported coercion,
non-foldable expressions, and malformed mappings conservatively provide
no equality proof.
- Build each surviving equivalence class with linear star edges.
---
.../nereids/properties/UnionDataTraitUtils.java | 576 +++++++++++++++++++++
.../nereids/trees/plans/logical/LogicalUnion.java | 74 +--
.../trees/plans/physical/PhysicalUnion.java | 75 +--
.../doris/nereids/properties/EqualSetTest.java | 139 +++++
.../rules/rewrite/EliminateOrderByKeyTest.java | 36 ++
.../union_equal_set/union_equal_set.out | 28 +
.../union_equal_set/union_equal_set.groovy | 121 +++++
7 files changed, 904 insertions(+), 145 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/UnionDataTraitUtils.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/UnionDataTraitUtils.java
new file mode 100644
index 00000000000..7cdd44feb42
--- /dev/null
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/UnionDataTraitUtils.java
@@ -0,0 +1,576 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.properties;
+
+import org.apache.doris.nereids.CascadesContext;
+import org.apache.doris.nereids.rules.expression.ExpressionRewriteContext;
+import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.NullSafeEqual;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.literal.BooleanLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.DateLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.Literal;
+import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.NumericLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.StringLikeLiteral;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.algebra.Union;
+import org.apache.doris.nereids.util.ExpressionUtils;
+import org.apache.doris.nereids.util.TypeCoercionUtils;
+import org.apache.doris.qe.ConnectContext;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Derives equal sets shared by logical and physical union plans.
+ *
+ * <p>Two union output slots are equal only when the corresponding values are
equal in every regular
+ * child and every constant row. Regular children contribute equality
information through their data
+ * traits, while constant rows contribute equality information through
constant folding and
+ * null-safe equality semantics.
+ */
+public final class UnionDataTraitUtils {
+
+ /** Utility class; it must not be instantiated. */
+ private UnionDataTraitUtils() {
+ }
+
+ /**
+ * Computes union output equalities that hold for every row source and
adds them to {@code builder}.
+ *
+ * <p>An output ordinal identifies the same union column across {@code
outputs}, every entry in
+ * {@code regularChildrenOutputs}, and every constant row. For regular
children, this method keeps
+ * only groups of ordinals whose mapped child slots belong to the same
equality class in every
+ * child. It then refines those groups with every constant row. A constant
row retains a pair only
+ * when folding their expressions and evaluating their null-safe equality
produces {@code TRUE}.
+ *
+ * <p>The union is assumed to satisfy the structural invariants
established during analysis: each
+ * regular child has one output mapping and every regular or constant
input has the union output
+ * width. This method does not validate those invariants. It leaves
existing entries in
+ * {@code builder} unchanged and only adds equal pairs proven by all union
inputs.
+ *
+ * @param union union metadata that supplies regular-child output mappings
and constant rows
+ * @param unionPlan concrete logical or physical union plan that supplies
children and output slots
+ * @param builder destination to which proven equal pairs between union
output slots are added
+ */
+ public static void computeEqualSet(Union union, Plan unionPlan,
DataTrait.Builder builder) {
+ List<Slot> outputs = unionPlan.getOutput();
+ List<Plan> children = unionPlan.children();
+ List<List<SlotReference>> childrenOutputs =
union.getRegularChildrenOutputs();
+ List<List<NamedExpression>> constantRows =
union.getConstantExprsList();
+ if (outputs.size() < 2 || (children.isEmpty() &&
constantRows.isEmpty())) {
+ return;
+ }
+
+ List<List<Integer>> equalGroups = children.isEmpty()
+ ? oneGroupForAllOutputs(outputs.size())
+ : intersectChildEqualGroups(children, childrenOutputs,
outputs.size());
+
+ if (!constantRows.isEmpty() && !equalGroups.isEmpty()) {
+ Optional<ExpressionRewriteContext> context =
createRewriteContext(unionPlan);
+ for (List<NamedExpression> row : constantRows) {
+ equalGroups = refineByConstantRow(equalGroups, row, context,
outputs.size());
+ if (equalGroups.isEmpty()) {
+ return;
+ }
+ }
+ }
+
+ for (List<Integer> equalGroup : equalGroups) {
+ int first = equalGroup.get(0);
+ for (int i = 1; i < equalGroup.size(); i++) {
+ builder.addEqualPair(outputs.get(first),
outputs.get(equalGroup.get(i)));
+ }
+ }
+ }
+
+ /**
+ * Intersects the equality partitions of all regular union children by
union output ordinal.
+ *
+ * <p>For each output ordinal, this method builds a signature containing
that ordinal's equality
+ * class ID in every child. Two output ordinals have the same signature
exactly when their mapped
+ * child slots are equal in every regular child. Singleton signature
groups are omitted because
+ * they do not describe an equality between different union outputs.
+ *
+ * @param children regular union children; child {@code i} corresponds to
mapping {@code i}
+ * @param childrenOutputs mapped child slots indexed first by child and
then by union output ordinal
+ * @param outputSize number of union output ordinals represented by every
child mapping
+ * @return groups of at least two output ordinals that are equal in every
regular child
+ */
+ private static List<List<Integer>> intersectChildEqualGroups(List<Plan>
children,
+ List<List<SlotReference>> childrenOutputs, int outputSize) {
+ List<List<Integer>> classIdsByChild = new ArrayList<>(children.size());
+ for (int childIndex = 0; childIndex < children.size(); childIndex++) {
+ classIdsByChild.add(equalClassIds(children.get(childIndex),
childrenOutputs.get(childIndex)));
+ }
+
+ Map<List<Integer>, List<Integer>> ordinalsBySignature = new
LinkedHashMap<>();
+ for (int outputIndex = 0; outputIndex < outputSize; outputIndex++) {
+ List<Integer> signature = new ArrayList<>(children.size());
+ for (List<Integer> childClassIds : classIdsByChild) {
+ signature.add(childClassIds.get(outputIndex));
+ }
+ ordinalsBySignature.computeIfAbsent(signature, key -> new
ArrayList<>()).add(outputIndex);
+ }
+ return onlyNonTrivialGroups(ordinalsBySignature.values());
+ }
+
+ /**
+ * Encodes one child's mapped output slots as equality-class IDs.
+ *
+ * <p>Slots in the same child data-trait equal set receive the same ID. A
mapped slot not present in
+ * any equal set receives its own ID, so it cannot accidentally compare
equal to a different slot.
+ * Repeated occurrences of the same mapped slot reuse the same ID.
+ *
+ * @param child child plan whose logical data trait defines slot equalities
+ * @param childOutputs child slots in union output-ordinal order
+ * @return class IDs in union output-ordinal order; equal IDs denote equal
mapped child slots
+ */
+ private static List<Integer> equalClassIds(Plan child, List<SlotReference>
childOutputs) {
+ DataTrait childTrait = child.getLogicalProperties().getTrait();
+ Map<Slot, Integer> classIdBySlot = new HashMap<>();
+ int nextClassId = 0;
+ for (Set<Slot> equalSet : childTrait.calAllEqualSet()) {
+ for (Slot slot : equalSet) {
+ classIdBySlot.put(slot, nextClassId);
+ }
+ nextClassId++;
+ }
+
+ List<Integer> classIds = new ArrayList<>(childOutputs.size());
+ for (Slot childOutput : childOutputs) {
+ Integer classId = classIdBySlot.get(childOutput);
+ if (classId == null) {
+ classId = nextClassId++;
+ classIdBySlot.put(childOutput, classId);
+ }
+ classIds.add(classId);
+ }
+ return classIds;
+ }
+
+ /**
+ * Refines candidate output equality groups using one constant row.
+ *
+ * <p>Each expression is first folded to a literal when possible.
Candidate ordinals are bucketed by
+ * a normalized {@link ConstantValueKey} to avoid comparing values that
clearly differ. Every pair
+ * in a multi-ordinal bucket is then checked independently, and proven
pairs are merged into
+ * equality components. All typed NULL literals use one shared key, so
compatible NULL expressions
+ * can reach that final proof without an incompatible pair discarding the
entire bucket. An ordinal
+ * whose expression cannot be folded, cannot be normalized, or cannot be
connected to another
+ * ordinal by a proven null-safe equality is omitted from the returned
groups.
+ *
+ * @param equalGroups candidate output-ordinal groups proven equal by
inputs processed so far
+ * @param row constant expressions in union output-ordinal order
+ * @param context optional rewrite context used while folding constants
and comparisons
+ * @param outputSize number of union outputs, used to size the per-ordinal
literal list
+ * @return non-singleton subgroups whose expressions are also proven equal
in this constant row
+ */
+ private static List<List<Integer>> refineByConstantRow(List<List<Integer>>
equalGroups,
+ List<NamedExpression> row, Optional<ExpressionRewriteContext>
context, int outputSize) {
+ List<Optional<Literal>> literals = new ArrayList<>(outputSize);
+ List<Optional<ConstantValueKey>> valueKeys = new
ArrayList<>(outputSize);
+ for (NamedExpression expression : row) {
+ Optional<Literal> literal = foldConstant(unwrapAlias(expression),
context);
+ literals.add(literal);
+
valueKeys.add(literal.flatMap(UnionDataTraitUtils::constantValueKey));
+ }
+
+ List<List<Integer>> refinedGroups = new ArrayList<>();
+ for (List<Integer> equalGroup : equalGroups) {
+ Map<ConstantValueKey, List<Integer>> ordinalsByValue = new
LinkedHashMap<>();
+ for (int outputIndex : equalGroup) {
+ Optional<ConstantValueKey> key = valueKeys.get(outputIndex);
+ key.ifPresent(valueKey -> ordinalsByValue
+ .computeIfAbsent(valueKey, ignored -> new
ArrayList<>()).add(outputIndex));
+ }
+ for (List<Integer> sameValueOrdinals : ordinalsByValue.values()) {
+ if (sameValueOrdinals.size() <= 1) {
+ continue;
+ }
+ refinedGroups.addAll(splitByProvenEquality(
+ sameValueOrdinals, row, literals, valueKeys, context));
+ }
+ }
+ return refinedGroups;
+ }
+
+ /**
+ * Splits one normalized-value bucket into independently proven equality
components.
+ *
+ * <p>Each bucket position starts in its own disjoint-set component. This
method evaluates every
+ * unordered pair of output ordinals and merges their components only when
null-safe comparison
+ * folds to {@code TRUE}. Pairs whose roots are already equal are skipped,
and proof results are
+ * cached by folded literal value and type so repeated compatible or
incompatible pairs do not
+ * repeat coercion and folding. Evaluating the remaining pairs makes the
result independent of
+ * ordinal order and preserves a compatible subgroup even when another
member, such as an
+ * ARRAY-typed NULL, cannot be coerced with it. Connected pairs may share
a component because
+ * proven value equality is transitive; singleton components are omitted
because they publish no
+ * output equality.
+ *
+ * @param sameValueOrdinals output ordinals that share one normalized
constant-value key
+ * @param row constant expressions in union output-ordinal order
+ * @param literals folded literals in union output-ordinal order
+ * @param valueKeys normalized keys for the folded literals in union
output-ordinal order
+ * @param context optional rewrite context used for coercion and constant
evaluation
+ * @return non-singleton ordinal components connected by proven null-safe
equality pairs
+ */
+ private static List<List<Integer>> splitByProvenEquality(List<Integer>
sameValueOrdinals,
+ List<NamedExpression> row, List<Optional<Literal>> literals,
+ List<Optional<ConstantValueKey>> valueKeys,
Optional<ExpressionRewriteContext> context) {
+ int[] parents = new int[sameValueOrdinals.size()];
+ for (int i = 0; i < parents.length; i++) {
+ parents[i] = i;
+ }
+ Map<ConstantComparisonKey, Boolean> proofCache = new HashMap<>();
+
+ for (int left = 0; left < sameValueOrdinals.size(); left++) {
+ for (int right = left + 1; right < sameValueOrdinals.size();
right++) {
+ int leftRoot = findRoot(parents, left);
+ int rightRoot = findRoot(parents, right);
+ if (leftRoot == rightRoot) {
+ continue;
+ }
+
+ int leftOrdinal = sameValueOrdinals.get(left);
+ int rightOrdinal = sameValueOrdinals.get(right);
+ ConstantComparisonKey comparisonKey = new
ConstantComparisonKey(
+ new LiteralSignature(literals.get(leftOrdinal).get(),
valueKeys.get(leftOrdinal).get()),
+ new LiteralSignature(literals.get(rightOrdinal).get(),
valueKeys.get(rightOrdinal).get()));
+ Boolean equal = proofCache.get(comparisonKey);
+ if (equal == null) {
+ equal = isNullSafeEqualInConstantRow(row, leftOrdinal,
rightOrdinal, context);
+ proofCache.put(comparisonKey, equal);
+ }
+ if (equal) {
+ parents[rightRoot] = leftRoot;
+ }
+ }
+ }
+
+ Map<Integer, List<Integer>> ordinalsByRoot = new LinkedHashMap<>();
+ for (int i = 0; i < sameValueOrdinals.size(); i++) {
+ int root = findRoot(parents, i);
+ ordinalsByRoot.computeIfAbsent(root, ignored -> new ArrayList<>())
+ .add(sameValueOrdinals.get(i));
+ }
+ return onlyNonTrivialGroups(ordinalsByRoot.values());
+ }
+
+ /**
+ * Finds the canonical root of one disjoint-set entry and compresses its
parent path.
+ *
+ * @param parents disjoint-set parent array indexed by positions in a
normalized-value bucket
+ * @param index bucket position whose component root is requested
+ * @return root position that identifies the equality component containing
{@code index}
+ */
+ private static int findRoot(int[] parents, int index) {
+ int root = index;
+ while (parents[root] != root) {
+ root = parents[root];
+ }
+ while (parents[index] != index) {
+ int parent = parents[index];
+ parents[index] = root;
+ index = parent;
+ }
+ return root;
+ }
+
+ /**
+ * Tests whether two ordinals in a constant row are provably equal under
null-safe semantics.
+ *
+ * <p>The expressions are unwrapped, coerced as operands of {@link
NullSafeEqual}, and
+ * constant-folded. Only the literal result {@code TRUE} proves equality.
This deliberately treats
+ * two NULL values as equal while treating a one-sided NULL as unequal.
{@code FALSE}, an
+ * unavailable fold result, and unsupported coercion or folding all return
{@code false}.
+ *
+ * @param row constant expressions in union output-ordinal order
+ * @param left ordinal of the left expression to compare
+ * @param right ordinal of the right expression to compare
+ * @param context optional rewrite context used for constant evaluation
+ * @return {@code true} only if the coerced null-safe equality folds to
+ * {@link BooleanLiteral#TRUE}
+ */
+ private static boolean isNullSafeEqualInConstantRow(List<NamedExpression>
row, int left, int right,
+ Optional<ExpressionRewriteContext> context) {
+ try {
+ Expression leftExpression = unwrapAlias(row.get(left));
+ Expression rightExpression = unwrapAlias(row.get(right));
+ Expression equality = TypeCoercionUtils.processComparisonPredicate(
+ new NullSafeEqual(leftExpression, rightExpression));
+ Optional<Literal> result =
ExpressionUtils.checkConstantExpr(equality, context);
+ // The trait is null-safe, so NULL <=> NULL is a valid proof while
NULL <=> value is not.
+ return result.isPresent() &&
BooleanLiteral.TRUE.equals(result.get());
+ } catch (RuntimeException e) {
+ // Unsupported coercion or an expression that cannot be folded is
not proof.
+ return false;
+ }
+ }
+
+ /**
+ * Attempts to fold an expression to a literal without allowing a fold
failure to publish a trait.
+ *
+ * @param expression expression to evaluate as a constant
+ * @param context optional rewrite context used by constant evaluation
+ * @return the folded literal, or an empty optional when the expression
cannot be folded safely
+ */
+ private static Optional<Literal> foldConstant(Expression expression,
+ Optional<ExpressionRewriteContext> context) {
+ try {
+ return ExpressionUtils.checkConstantExpr(expression, context);
+ } catch (RuntimeException e) {
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Builds a normalized key used to bucket literals that may be equal.
+ *
+ * <p>All numeric literals share a numeric family and use a
scale-insensitive decimal value.
+ * String-like and date literals are grouped within their respective
families by string value.
+ * Other literals retain their concrete class and literal object. Every
typed NULL literal maps to
+ * the same dedicated key because null-safe equality considers two NULL
values equal. A failure
+ * while extracting any other value yields an empty optional. Key equality
is only a prefilter;
+ * {@link #isNullSafeEqualInConstantRow(List, int, int, Optional)}
performs the final proof.
+ *
+ * @param literal folded literal to normalize
+ * @return a normalized value key, including the shared NULL key, or an
empty optional if no safe
+ * key can be produced
+ */
+ private static Optional<ConstantValueKey> constantValueKey(Literal
literal) {
+ if (literal.isNullLiteral()) {
+ return Optional.of(ConstantValueKey.NULL_VALUE_KEY);
+ }
+ try {
+ if (literal instanceof NumericLiteral) {
+ BigDecimal value = ((NumericLiteral)
literal).getBigDecimalValue().stripTrailingZeros();
+ return Optional.of(new ConstantValueKey(NumericLiteral.class,
value));
+ } else if (literal instanceof StringLikeLiteral) {
+ return Optional.of(new
ConstantValueKey(StringLikeLiteral.class, literal.getStringValue()));
+ } else if (literal instanceof DateLiteral) {
+ return Optional.of(new ConstantValueKey(DateLiteral.class,
literal.getStringValue()));
+ }
+ return Optional.of(new ConstantValueKey(literal.getClass(),
literal));
+ } catch (RuntimeException e) {
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Removes all outer alias layers from a named expression.
+ *
+ * @param expression named expression whose underlying value expression is
needed
+ * @return the first expression below all consecutive outer {@link Alias}
nodes
+ */
+ private static Expression unwrapAlias(NamedExpression expression) {
+ Expression unwrapped = expression;
+ while (unwrapped instanceof Alias) {
+ unwrapped = unwrapped.child(0);
+ }
+ return unwrapped;
+ }
+
+ /**
+ * Creates the rewrite context needed for context-dependent constant
folding when one is available.
+ *
+ * <p>Trait derivation can run without a thread-local connection or
statement context. In that case
+ * callers receive an empty optional and constant evaluation decides
whether it can proceed without
+ * the context.
+ *
+ * @param plan union plan used as the root of the temporary cascades and
expression rewrite contexts
+ * @return a rewrite context for the current statement, or an empty
optional when none is available
+ */
+ private static Optional<ExpressionRewriteContext>
createRewriteContext(Plan plan) {
+ ConnectContext connectContext = ConnectContext.get();
+ if (connectContext == null || connectContext.getStatementContext() ==
null) {
+ return Optional.empty();
+ }
+ return Optional.of(new ExpressionRewriteContext(plan,
CascadesContext.initContext(
+ connectContext.getStatementContext(), plan,
PhysicalProperties.ANY)));
+ }
+
+ /**
+ * Creates the initial candidate group used when a union has only constant
rows.
+ *
+ * <p>With no regular child, no child trait can rule out equality, so all
output ordinals begin in
+ * one candidate group. Each constant row subsequently splits or removes
members from this group.
+ *
+ * @param outputSize number of union output slots
+ * @return one group containing every ordinal from zero (inclusive) to
{@code outputSize} (exclusive)
+ */
+ private static List<List<Integer>> oneGroupForAllOutputs(int outputSize) {
+ List<Integer> allOutputs = new ArrayList<>(outputSize);
+ for (int outputIndex = 0; outputIndex < outputSize; outputIndex++) {
+ allOutputs.add(outputIndex);
+ }
+ List<List<Integer>> groups = new ArrayList<>(1);
+ groups.add(allOutputs);
+ return groups;
+ }
+
+ /**
+ * Removes singleton and empty ordinal groups that cannot express equality
between distinct outputs.
+ *
+ * @param groups candidate ordinal groups in the order they should be
considered
+ * @return a new outer list containing the original group objects whose
size is greater than one
+ */
+ private static List<List<Integer>>
onlyNonTrivialGroups(Iterable<List<Integer>> groups) {
+ List<List<Integer>> nonTrivialGroups = new ArrayList<>();
+ for (List<Integer> group : groups) {
+ if (group.size() > 1) {
+ nonTrivialGroups.add(group);
+ }
+ }
+ return nonTrivialGroups;
+ }
+
+ /**
+ * Normalized identity used to pre-group folded constants before
evaluating null-safe equality.
+ *
+ * <p>The key deliberately combines a literal family with a canonical
value. The family prevents
+ * unrelated literal categories from sharing a bucket, while allowing
representations within a
+ * supported category, such as different numeric literal classes, to meet
in the same bucket. A
+ * matching key is only a cheap candidate signal; it is never used by
itself as proof of equality.
+ */
+ private static final class ConstantValueKey {
+ /** Shared identity for SQL NULL, independent of the target type
carried by a {@link NullLiteral}. */
+ private static final ConstantValueKey NULL_VALUE_KEY
+ = new ConstantValueKey(NullLiteral.class, NullLiteral.class);
+
+ /** Literal category used to keep values from unrelated SQL type
families in separate buckets. */
+ private final Class<?> family;
+
+ /** Canonical value compared within {@link #family}, such as a
scale-normalized decimal. */
+ private final Object value;
+
+ /**
+ * Creates a key from a literal family and its canonical value.
+ *
+ * @param family normalized literal category used as the first part of
key identity
+ * @param value canonical value within {@code family}, used as the
second part of key identity
+ */
+ private ConstantValueKey(Class<?> family, Object value) {
+ this.family = family;
+ this.value = value;
+ }
+
+ /**
+ * Compares both normalized components of this key with another object.
+ *
+ * @param object object to compare with this key
+ * @return {@code true} when {@code object} is a key with the same
family and canonical value
+ */
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (!(object instanceof ConstantValueKey)) {
+ return false;
+ }
+ ConstantValueKey that = (ConstantValueKey) object;
+ return family.equals(that.family) && value.equals(that.value);
+ }
+
+ /**
+ * Computes a hash from the same family and canonical value used by
{@link #equals(Object)}.
+ *
+ * @return hash code for this normalized constant key
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(family, value);
+ }
+ }
+
+ /**
+ * Folded literal signature used to reuse a null-safe equality proof
within one constant row.
+ */
+ private static final class LiteralSignature {
+ private final Class<?> literalClass;
+ private final Object dataType;
+ private final ConstantValueKey valueKey;
+
+ private LiteralSignature(Literal literal, ConstantValueKey valueKey) {
+ this.literalClass = literal.getClass();
+ this.dataType = literal.getDataType();
+ this.valueKey = valueKey;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (!(object instanceof LiteralSignature)) {
+ return false;
+ }
+ LiteralSignature that = (LiteralSignature) object;
+ return literalClass.equals(that.literalClass)
+ && Objects.equals(dataType, that.dataType)
+ && valueKey.equals(that.valueKey);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(literalClass, dataType, valueKey);
+ }
+ }
+
+ /**
+ * Ordered pair of folded literal signatures used as a cache key for a
comparison proof.
+ */
+ private static final class ConstantComparisonKey {
+ private final LiteralSignature left;
+ private final LiteralSignature right;
+
+ private ConstantComparisonKey(LiteralSignature left, LiteralSignature
right) {
+ this.left = left;
+ this.right = right;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (!(object instanceof ConstantComparisonKey)) {
+ return false;
+ }
+ ConstantComparisonKey that = (ConstantComparisonKey) object;
+ return left.equals(that.left) && right.equals(that.right);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(left, right);
+ }
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalUnion.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalUnion.java
index 9e276035787..1fbecf16746 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalUnion.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalUnion.java
@@ -23,6 +23,7 @@ import org.apache.doris.nereids.memo.GroupExpression;
import org.apache.doris.nereids.properties.DataTrait;
import org.apache.doris.nereids.properties.LogicalProperties;
import org.apache.doris.nereids.properties.PhysicalProperties;
+import org.apache.doris.nereids.properties.UnionDataTraitUtils;
import org.apache.doris.nereids.rules.expression.ExpressionRewriteContext;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.NamedExpression;
@@ -45,14 +46,9 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
-import java.util.ArrayList;
-import java.util.BitSet;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.Optional;
-import java.util.Set;
/**
* Logical Union.
@@ -290,75 +286,9 @@ public class LogicalUnion extends LogicalSetOperation
implements Union, OutputPr
return super.hasUnboundExpression();
}
- private List<BitSet> mapSlotToIndex(Plan plan, List<Set<Slot>>
equalSlotsList) {
- Map<Slot, Integer> slotToIndex = new HashMap<>();
- for (int i = 0; i < plan.getOutput().size(); i++) {
- slotToIndex.put(plan.getOutput().get(i), i);
- }
- List<BitSet> equalSlotIndicesList = new ArrayList<>();
- for (Set<Slot> equalSlots : equalSlotsList) {
- BitSet equalSlotIndices = new BitSet();
- for (Slot slot : equalSlots) {
- if (slotToIndex.containsKey(slot)) {
- equalSlotIndices.set(slotToIndex.get(slot));
- }
- }
- if (equalSlotIndices.cardinality() > 1) {
- equalSlotIndicesList.add(equalSlotIndices);
- }
- }
- return equalSlotIndicesList;
- }
-
@Override
public void computeEqualSet(DataTrait.Builder builder) {
- if (children.isEmpty()) {
- return;
- }
-
- // Get the list of equal slot sets and their corresponding index
mappings for the first child
- List<Set<Slot>> childEqualSlotsList = child(0).getLogicalProperties()
- .getTrait().calAllEqualSet();
- List<BitSet> childEqualSlotsIndicesList = mapSlotToIndex(child(0),
childEqualSlotsList);
- List<BitSet> unionEqualSlotIndicesList = new
ArrayList<>(childEqualSlotsIndicesList);
-
- // Traverse all children and find the equal sets that exist in all
children
- for (int i = 1; i < children.size(); i++) {
- Plan child = children.get(i);
-
- // Get the equal slot sets for the current child
- childEqualSlotsList =
child.getLogicalProperties().getTrait().calAllEqualSet();
-
- // Map slots to indices for the current child
- childEqualSlotsIndicesList = mapSlotToIndex(child,
childEqualSlotsList);
-
- // Only keep the equal pairs that exist in all children of the
union
- // This is done by calculating the intersection of all children's
equal slot indices
- for (BitSet unionEqualSlotIndices : unionEqualSlotIndicesList) {
- BitSet intersect = new BitSet();
- for (BitSet childEqualSlotIndices :
childEqualSlotsIndicesList) {
- if
(unionEqualSlotIndices.intersects(childEqualSlotIndices)) {
- intersect = childEqualSlotIndices;
- break;
- }
- }
- unionEqualSlotIndices.and(intersect);
- }
- }
-
- // Build the functional dependencies for the output slots
- List<Slot> outputList = getOutput();
- for (BitSet equalSlotIndices : unionEqualSlotIndicesList) {
- if (equalSlotIndices.cardinality() <= 1) {
- continue;
- }
- int first = equalSlotIndices.nextSetBit(0);
- int next = equalSlotIndices.nextSetBit(first + 1);
- while (next > 0) {
- builder.addEqualPair(outputList.get(first),
outputList.get(next));
- next = equalSlotIndices.nextSetBit(next + 1);
- }
- }
+ UnionDataTraitUtils.computeEqualSet(this, this, builder);
}
@Override
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalUnion.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalUnion.java
index 24cad14f45a..ee6f5a4d024 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalUnion.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalUnion.java
@@ -21,9 +21,9 @@ import org.apache.doris.nereids.memo.GroupExpression;
import org.apache.doris.nereids.properties.DataTrait;
import org.apache.doris.nereids.properties.LogicalProperties;
import org.apache.doris.nereids.properties.PhysicalProperties;
+import org.apache.doris.nereids.properties.UnionDataTraitUtils;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.NamedExpression;
-import org.apache.doris.nereids.trees.expressions.Slot;
import org.apache.doris.nereids.trees.expressions.SlotReference;
import org.apache.doris.nereids.trees.plans.AbstractPlan;
import org.apache.doris.nereids.trees.plans.Plan;
@@ -38,14 +38,9 @@ import org.apache.doris.statistics.model.Statistics;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
-import java.util.ArrayList;
-import java.util.BitSet;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.Optional;
-import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -189,75 +184,9 @@ public class PhysicalUnion extends PhysicalSetOperation
implements Union {
// don't propagate uniform slots
}
- private List<BitSet> mapSlotToIndex(Plan plan, List<Set<Slot>>
equalSlotsList) {
- Map<Slot, Integer> slotToIndex = new HashMap<>();
- for (int i = 0; i < plan.getOutput().size(); i++) {
- slotToIndex.put(plan.getOutput().get(i), i);
- }
- List<BitSet> equalSlotIndicesList = new ArrayList<>();
- for (Set<Slot> equalSlots : equalSlotsList) {
- BitSet equalSlotIndices = new BitSet();
- for (Slot slot : equalSlots) {
- if (slotToIndex.containsKey(slot)) {
- equalSlotIndices.set(slotToIndex.get(slot));
- }
- }
- if (equalSlotIndices.cardinality() > 1) {
- equalSlotIndicesList.add(equalSlotIndices);
- }
- }
- return equalSlotIndicesList;
- }
-
@Override
public void computeEqualSet(DataTrait.Builder builder) {
- if (children.isEmpty()) {
- return;
- }
-
- // Get the list of equal slot sets and their corresponding index
mappings for the first child
- List<Set<Slot>> childEqualSlotsList = child(0).getLogicalProperties()
- .getTrait().calAllEqualSet();
- List<BitSet> childEqualSlotsIndicesList = mapSlotToIndex(child(0),
childEqualSlotsList);
- List<BitSet> unionEqualSlotIndicesList = new
ArrayList<>(childEqualSlotsIndicesList);
-
- // Traverse all children and find the equal sets that exist in all
children
- for (int i = 1; i < children.size(); i++) {
- Plan child = children.get(i);
-
- // Get the equal slot sets for the current child
- childEqualSlotsList =
child.getLogicalProperties().getTrait().calAllEqualSet();
-
- // Map slots to indices for the current child
- childEqualSlotsIndicesList = mapSlotToIndex(child,
childEqualSlotsList);
-
- // Only keep the equal pairs that exist in all children of the
union
- // This is done by calculating the intersection of all children's
equal slot indices
- for (BitSet unionEqualSlotIndices : unionEqualSlotIndicesList) {
- BitSet intersect = new BitSet();
- for (BitSet childEqualSlotIndices :
childEqualSlotsIndicesList) {
- if
(unionEqualSlotIndices.intersects(childEqualSlotIndices)) {
- intersect = childEqualSlotIndices;
- break;
- }
- }
- unionEqualSlotIndices.and(intersect);
- }
- }
-
- // Build the functional dependencies for the output slots
- List<Slot> outputList = getOutput();
- for (BitSet equalSlotIndices : unionEqualSlotIndicesList) {
- if (equalSlotIndices.cardinality() <= 1) {
- continue;
- }
- int first = equalSlotIndices.nextSetBit(0);
- int next = equalSlotIndices.nextSetBit(first + 1);
- while (next > 0) {
- builder.addEqualPair(outputList.get(first),
outputList.get(next));
- next = equalSlotIndices.nextSetBit(next + 1);
- }
- }
+ UnionDataTraitUtils.computeEqualSet(this, this, builder);
}
@Override
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/properties/EqualSetTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/properties/EqualSetTest.java
index d33dd556a8c..7a299e2854d 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/properties/EqualSetTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/properties/EqualSetTest.java
@@ -18,6 +18,8 @@
package org.apache.doris.nereids.properties;
import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalUnion;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalUnion;
import org.apache.doris.nereids.util.PlanChecker;
import org.apache.doris.utframe.TestWithFeService;
@@ -98,6 +100,143 @@ class EqualSetTest extends TestWithFeService {
.isEmpty());
}
+ @Test
+ void testUnionEqualSetUsesRegularChildOutputMapping() {
+ String sql = "select name, id, id2 from agg where id = id2 "
+ + "union all select name, id, id2 from agg where id = id2";
+ LogicalUnion union = analyzeLogicalUnion(sql);
+ Assertions.assertNotEquals(union.child(0).getOutput(),
union.getRegularChildOutput(0),
+ "the test must exercise an ordinal mapping that differs from
child.getOutput()");
+ assertUnionEqualPair(sql, union, 1, 2, true);
+ assertUnionEqualPair(sql, union, 0, 1, false);
+ }
+
+ @Test
+ void testUnionEqualSetChecksEveryConstantRow() {
+ assertUnionEqualPair(
+ "select id, id2 from agg where id = id2 "
+ + "union all select 1, 1 union all select 2, 2",
+ 0, 1, true);
+ assertUnionEqualPair(
+ "select id, id2 from agg where id = id2 "
+ + "union all select 1, 1 union all select 2, 3",
+ 0, 1, false);
+ assertUnionEqualPair(
+ "select id, id2 from agg where id = id2 "
+ + "union all select 1, 2 union all select 2, 1",
+ 0, 1, false);
+ }
+
+ @Test
+ void testConstantOnlyUnionEqualSetUsesNullSafeEquality() {
+ assertUnionEqualPair(
+ "select cast(1 as int), cast(1 as bigint) "
+ + "union all select cast(2 as int), cast(2 as bigint)",
+ 0, 1, true);
+ assertUnionEqualPair(
+ "select 1 + 1, cast(2 as bigint) "
+ + "union all select 2 * 2, cast(4 as bigint)",
+ 0, 1, true);
+ assertUnionEqualPair(
+ "select 1, 1 union all select 2, 3",
+ 0, 1, false);
+ assertUnionEqualPair(
+ "select cast(null as int), cast(null as bigint) union all
select 1, 1",
+ 0, 1, true);
+ assertUnionEqualPair(
+ "select cast(null as int), cast(1 as bigint) union all select
1, 1",
+ 0, 1, false);
+ }
+
+ @Test
+ void testConstantOnlyUnionKeepsComparableSubsetOfMixedNullBucket() {
+ String incompatibleNullFirst = "select cast(null as array<int>),
cast(null as int), "
+ + "cast(null as bigint) union all select cast(null as
array<int>), "
+ + "cast(1 as int), cast(1 as bigint)";
+ assertUnionEqualPair(incompatibleNullFirst, 1, 2, true);
+ assertUnionEqualPair(incompatibleNullFirst, 0, 1, false);
+
+ String compatibleValuesFirst = "select cast(null as array<int>),
cast(1 as int), "
+ + "cast(1 as bigint) union all select cast(null as
array<int>), "
+ + "cast(null as int), cast(null as bigint)";
+ assertUnionEqualPair(compatibleValuesFirst, 1, 2, true);
+ assertUnionEqualPair(compatibleValuesFirst, 0, 1, false);
+ }
+
+ @Test
+ void testWideConstantUnionReusesComparisonProofs() {
+ int columnCount = 256;
+ StringBuilder sql = new StringBuilder("select ");
+ appendNullIntColumns(sql, columnCount);
+ sql.append(" union all select ");
+ appendNullIntColumns(sql, columnCount);
+
+ LogicalUnion union = analyzeLogicalUnion(sql.toString());
+
Assertions.assertTrue(union.getLogicalProperties().getTrait().isNullSafeEqual(
+ union.getOutput().get(0), union.getOutput().get(columnCount -
1)));
+ }
+
+ private void appendNullIntColumns(StringBuilder sql, int columnCount) {
+ for (int column = 0; column < columnCount; column++) {
+ if (column > 0) {
+ sql.append(", ");
+ }
+ sql.append("cast(null as int)");
+ }
+ }
+
+ private void assertUnionEqualPair(String sql, int leftIndex, int
rightIndex, boolean expected) {
+ assertUnionEqualPair(sql, analyzeLogicalUnion(sql), leftIndex,
rightIndex, expected);
+ }
+
+ private void assertUnionEqualPair(String sql, LogicalUnion logicalUnion,
+ int leftIndex, int rightIndex, boolean expected) {
+ Assertions.assertEquals(expected,
logicalUnion.getLogicalProperties().getTrait().isNullSafeEqual(
+ logicalUnion.getOutput().get(leftIndex),
logicalUnion.getOutput().get(rightIndex)));
+
+ Plan physicalPlan =
PlanChecker.from(connectContext).analyze(sql).rewrite().implement().getPhysicalPlan();
+ PhysicalUnion physicalUnion = findPhysicalUnion(physicalPlan);
+ Assertions.assertNotNull(physicalUnion, "expected a PhysicalUnion in:
" + physicalPlan.treeString());
+ DataTrait.Builder builder = new DataTrait.Builder();
+ physicalUnion.computeEqualSet(builder);
+ DataTrait physicalTrait = builder.build();
+ Assertions.assertEquals(expected, physicalTrait.isNullSafeEqual(
+ physicalUnion.getOutput().get(leftIndex),
physicalUnion.getOutput().get(rightIndex)));
+ }
+
+ private LogicalUnion analyzeLogicalUnion(String sql) {
+ Plan rewritten =
PlanChecker.from(connectContext).analyze(sql).rewrite().getPlan();
+ LogicalUnion logicalUnion = findLogicalUnion(rewritten);
+ Assertions.assertNotNull(logicalUnion, "expected a LogicalUnion in: "
+ rewritten.treeString());
+ return logicalUnion;
+ }
+
+ private LogicalUnion findLogicalUnion(Plan plan) {
+ if (plan instanceof LogicalUnion) {
+ return (LogicalUnion) plan;
+ }
+ for (Plan child : plan.children()) {
+ LogicalUnion union = findLogicalUnion(child);
+ if (union != null) {
+ return union;
+ }
+ }
+ return null;
+ }
+
+ private PhysicalUnion findPhysicalUnion(Plan plan) {
+ if (plan instanceof PhysicalUnion) {
+ return (PhysicalUnion) plan;
+ }
+ for (Plan child : plan.children()) {
+ PhysicalUnion union = findPhysicalUnion(child);
+ if (union != null) {
+ return union;
+ }
+ }
+ return null;
+ }
+
@Test
void testFilterHaving() {
Plan plan = PlanChecker.from(connectContext)
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/EliminateOrderByKeyTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/EliminateOrderByKeyTest.java
index 937f62505a7..c5387c4cef7 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/EliminateOrderByKeyTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/EliminateOrderByKeyTest.java
@@ -213,6 +213,42 @@ public class EliminateOrderByKeyTest extends
TestWithFeService implements MemoPa
.getOrderKeys().isEmpty()));
}
+ @Test
+ void testWindowUnionNullSafeEquality() {
+ PlanChecker.from(connectContext)
+ .analyze("select rank() over(order by a, b) from ("
+ + "select cast(null as int) a, cast(null as bigint) b "
+ + "union all select cast(1 as int), cast(1 as bigint))
t")
+ .rewrite()
+ .printlnTree()
+ .matches(logicalWindow()
+ .when(window -> ((WindowExpression)
window.getWindowExpressions().get(0).child(0))
+ .getOrderKeys().size() == 1));
+ }
+
+ @Test
+ void testWindowUnionMixedNullTypesIndependentOfArmOrder() {
+ PlanChecker.from(connectContext)
+ .analyze("select a, rank() over(order by b, c) from ("
+ + "select cast(null as array<int>) a, cast(null as
int) b, cast(null as bigint) c "
+ + "union all select cast(null as array<int>), cast(1
as int), cast(1 as bigint)) t")
+ .rewrite()
+ .printlnTree()
+ .matches(logicalWindow()
+ .when(window -> ((WindowExpression)
window.getWindowExpressions().get(0).child(0))
+ .getOrderKeys().size() == 1));
+
+ PlanChecker.from(connectContext)
+ .analyze("select a, rank() over(order by b, c) from ("
+ + "select cast(null as array<int>) a, cast(1 as int)
b, cast(1 as bigint) c "
+ + "union all select cast(null as array<int>),
cast(null as int), cast(null as bigint)) t")
+ .rewrite()
+ .printlnTree()
+ .matches(logicalWindow()
+ .when(window -> ((WindowExpression)
window.getWindowExpressions().get(0).child(0))
+ .getOrderKeys().size() == 1));
+ }
+
@Test
void testWindowPartitionKey() {
// an order key that repeats the window's own partition key is
constant within each partition,
diff --git
a/regression-test/data/nereids_rules_p0/union_equal_set/union_equal_set.out
b/regression-test/data/nereids_rules_p0/union_equal_set/union_equal_set.out
new file mode 100644
index 00000000000..64ac6a00a6b
--- /dev/null
+++ b/regression-test/data/nereids_rules_p0/union_equal_set/union_equal_set.out
@@ -0,0 +1,28 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !reordered_regular_child_mapping --
+10 1 1 1
+10 1 1 1
+10 2 2 3
+10 2 2 3
+20 1 1 5
+20 1 1 5
+
+-- !mixed_regular_and_constant_rows --
+1 1 1
+1 2 2
+2 1 3
+2 2 4
+
+-- !one_constant_row_breaks_equality --
+1 1 1
+1 1 1
+2 2 3
+2 3 4
+
+-- !all_constant_rows_equal_after_coercion --
+1 1 1
+2 2 2
+
+-- !constant_null_breaks_equality --
+\N \N 1
+1 1 2
diff --git
a/regression-test/suites/nereids_rules_p0/union_equal_set/union_equal_set.groovy
b/regression-test/suites/nereids_rules_p0/union_equal_set/union_equal_set.groovy
new file mode 100644
index 00000000000..b26d64e674f
--- /dev/null
+++
b/regression-test/suites/nereids_rules_p0/union_equal_set/union_equal_set.groovy
@@ -0,0 +1,121 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("union_equal_set") {
+ sql "drop table if exists union_equal_reordered_t"
+ sql "drop table if exists union_equal_constant_t"
+
+ sql """
+ create table union_equal_reordered_t (
+ a int not null,
+ b int not null,
+ c int not null
+ ) duplicate key(a)
+ distributed by hash(a) buckets 1
+ properties("replication_num" = "1")
+ """
+ sql """
+ insert into union_equal_reordered_t values
+ (1, 1, 10),
+ (2, 2, 10),
+ (1, 1, 20)
+ """
+
+ explain {
+ sql """
+ select c, a, b, rank() over(order by c, a) rk
+ from (
+ select c, a, b from union_equal_reordered_t where a = b
+ union all
+ select c, a, b from union_equal_reordered_t where a = b
+ ) u
+ """
+ contains "functions: [rank()]"
+ contains "ASC NULLS FIRST, a[#"
+ }
+
+ qt_reordered_regular_child_mapping """
+ select c, a, b, rank() over(order by c, a) rk
+ from (
+ select c, a, b from union_equal_reordered_t where a = b
+ union all
+ select c, a, b from union_equal_reordered_t where a = b
+ ) u
+ order by c, a, b, rk
+ """
+
+ sql """
+ create table union_equal_constant_t (
+ a int not null,
+ b int not null
+ ) duplicate key(a)
+ distributed by hash(a) buckets 1
+ properties("replication_num" = "1")
+ """
+ sql "insert into union_equal_constant_t values (1, 1), (2, 2)"
+
+ explain {
+ sql """
+ select a, b, rank() over(order by a, b) rk
+ from (
+ select a, b from union_equal_constant_t where a = b
+ union all select 1, 2
+ union all select 2, 1
+ ) u
+ """
+ contains "functions: [rank()]"
+ contains "ASC NULLS FIRST, b[#"
+ }
+
+ qt_mixed_regular_and_constant_rows """
+ select a, b, rank() over(order by a, b) rk
+ from (
+ select a, b from union_equal_constant_t where a = b
+ union all select 1, 2
+ union all select 2, 1
+ ) u
+ order by a, b, rk
+ """
+
+ qt_one_constant_row_breaks_equality """
+ select a, b, rank() over(order by a, b) rk
+ from (
+ select a, b from union_equal_constant_t where a = b
+ union all select 1, 1
+ union all select 2, 3
+ ) u
+ order by a, b, rk
+ """
+
+ qt_all_constant_rows_equal_after_coercion """
+ select a, b, rank() over(order by a, b) rk
+ from (
+ select cast(1 as int) a, cast(1 as bigint) b
+ union all select cast(2 as int), cast(2 as bigint)
+ ) u
+ order by a, b, rk
+ """
+
+ qt_constant_null_breaks_equality """
+ select a, b, rank() over(order by a, b) rk
+ from (
+ select cast(null as int) a, cast(null as bigint) b
+ union all select cast(1 as int), cast(1 as bigint)
+ ) u
+ order by a, b, rk
+ """
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]