This is an automated email from the ASF dual-hosted git repository.
yujun777 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 4f3abce2c25 [fix](ivm) Resolve IVM identity keys by slot identity and
materialize unprojected keys (#67669)
4f3abce2c25 is described below
commit 4f3abce2c25e7b1e3d24eeaa97cffcfd76abe531
Author: yujun <[email protected]>
AuthorDate: Thu Sep 10 11:13:13 2026 +0800
[fix](ivm) Resolve IVM identity keys by slot identity and materialize
unprojected keys (#67669)
Two follow-ups from the #62606 review round, both around same-named
identity keys of aggregate/full-keys MVs:
1. **Resolve IVM aggregate group keys by slot identity instead of name**
— an aggregate delta over a join of two tables that both expose the same
column name (`GROUP BY l.id, r.id`) resolved its group keys through
name-keyed lookups. A name index collapses same-named slots onto the
last match, so both keys were bound to one delta slot and a `(10, 20)`
group was written as `(20, 20)`, diverging from COMPLETE. The delta-side
group keys are now resolved and carried by slot identity (ordered with
the aggregate metadata), the apply project emits each key output from
its own delta slot, and the `ivm_use_full_keys` identity conjuncts
resolve their delta side by identity first. The full-keys regression
with a non-full-keys counterpart covers plain INSERT, full-keys, and MOW
UPDATE refresh, each cross-checked against COMPLETE.
2. **Materialize unprojected full-keys identity keys at the project
layer** — with `ivm_use_full_keys` every identity key must be
materialized in the stored layout, either as a visible output or under a
hidden key column. The result sink judged "already projected" by column
name, so a same-named key from another table (selecting only `l.id`
while grouping by `l.id, r.id`) was silently dropped and the key set
lost one dimension. The sink now checks projection by slot identity, and
materialization moves down to the first project that drops a key, so
CREATE (result sink) and refresh (olap-table sink) plans produce the
same hidden-key layout. A new regression asserts the hidden key column
exists (DESC with hidden columns) and that INCREMENTAL matches COMPLETE.
Tests: `IvmAggDeltaHandlerTest` (33) and `IvmNormalizeMTMVJoinTest` new
cases pass; the new regression cases pass and the full ivm regression
set (85 suites) is green on the materialization change.
Trace issue: https://github.com/apache/doris/issues/65418
---
.../apache/doris/mtmv/ivm/IvmAggDeltaHandler.java | 90 +++++--
.../doris/mtmv/ivm/IvmDeltaRewriteHelper.java | 14 ++
.../nereids/rules/analysis/IvmNormalizeMTMV.java | 44 +++-
.../doris/mtmv/ivm/IvmAggDeltaHandlerTest.java | 104 ++++++++
.../doris/mtmv/ivm/IvmNormalizeMTMVJoinTest.java | 67 ++++++
.../rules/analysis/IvmNormalizeMTMVTest.java | 15 +-
.../ivm/test_ivm_agg_join_same_name_group_key.out | 50 ++++
...est_ivm_full_keys_same_name_unprojected_key.out | 30 +++
.../test_ivm_agg_join_same_name_group_key.groovy | 263 +++++++++++++++++++++
..._ivm_full_keys_same_name_unprojected_key.groovy | 106 +++++++++
10 files changed, 750 insertions(+), 33 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/IvmAggDeltaHandler.java
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/IvmAggDeltaHandler.java
index 86b2d011f28..a72e073413f 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/IvmAggDeltaHandler.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/IvmAggDeltaHandler.java
@@ -110,15 +110,26 @@ class IvmAggDeltaHandler {
private final Slot deltaGroupCountSlot;
/** Per-target delta slots consumed by aggregate function processors
during apply. */
private final Map<IvmAggDeltaSlotRef, Slot> applyDeltaSlots;
- /** Group key slots resolved from topDeltaProject output, keyed by
column name. */
+ /**
+ * Group key slots resolved from the topDeltaProject output, ordered
like
+ * {@link IvmAggMeta#getGroupKeySlots()}. Same-named keys (e.g. l.id
and r.id after
+ * a join) must be addressed by position/identity through this list,
never by name.
+ */
+ private final List<Slot> deltaGroupKeySlots;
+ /**
+ * Name-keyed view of {@link #deltaGroupKeySlots}; with same-named
group keys the map
+ * keeps only the last slot, so it is only usable when all key names
are distinct.
+ */
private final Map<String, Slot> groupKeySlotsByName;
private DeltaPlanParts(LogicalProject<?> topDeltaProject, Slot
rowIdSlot, Slot deltaGroupCountSlot,
- Map<IvmAggDeltaSlotRef, Slot> applyDeltaSlots, Map<String,
Slot> groupKeySlotsByName) {
+ Map<IvmAggDeltaSlotRef, Slot> applyDeltaSlots, List<Slot>
deltaGroupKeySlots,
+ Map<String, Slot> groupKeySlotsByName) {
this.topDeltaProject = topDeltaProject;
this.rowIdSlot = rowIdSlot;
this.deltaGroupCountSlot = deltaGroupCountSlot;
this.applyDeltaSlots = applyDeltaSlots;
+ this.deltaGroupKeySlots = deltaGroupKeySlots;
this.groupKeySlotsByName = groupKeySlotsByName;
}
}
@@ -237,19 +248,27 @@ class IvmAggDeltaHandler {
aggFunctionRegistry.mapApplyDeltaSlots(
target, outputByName, applyDeltaSlots,
deltaGroupCountSlot, aggExpressionBuilder);
}
- Map<String, Slot> groupKeySlotsByName = new LinkedHashMap<>();
+ // Group keys are resolved by slot identity, not by name: same-named
keys (e.g.
+ // l.id / r.id after a join) collapse to the last matching slot under
a name index.
+ List<Slot> deltaOutputSlots = topDeltaProject.getOutput();
+ List<Slot> deltaGroupKeySlots = new ArrayList<>(groupKeySize);
for (Slot groupKey : aggMeta.getGroupKeySlots()) {
- Slot resolved = outputByName.get(groupKey.getName());
+ Slot resolved = helper.findSlotByExprId(deltaOutputSlots,
groupKey);
if (resolved == null) {
throw new IvmException(IvmFailureReason.PLAN_REWRITE_FAILED,
"IVM agg delta rewrite failed to resolve delta group
key slot: "
- + groupKey.getName());
+ + groupKey);
}
- groupKeySlotsByName.put(groupKey.getName(), resolved);
+ deltaGroupKeySlots.add(resolved);
+ }
+ // Name-keyed view, kept only for lookups on distinct key names (see
field comment).
+ Map<String, Slot> groupKeySlotsByName = new LinkedHashMap<>();
+ for (Slot resolved : deltaGroupKeySlots) {
+ groupKeySlotsByName.put(resolved.getName(), resolved);
}
return new DeltaPlanParts(topDeltaProject,
outputByName.get(Column.IVM_ROW_ID_COL),
- deltaGroupCountSlot, applyDeltaSlots, groupKeySlotsByName);
+ deltaGroupCountSlot, applyDeltaSlots, deltaGroupKeySlots,
groupKeySlotsByName);
}
/**
@@ -304,9 +323,8 @@ class IvmAggDeltaHandler {
"negative group count");
finalByColumnName.put(Column.IVM_ROW_ID_COL, delta.rowIdSlot);
finalByColumnName.put(aggMeta.getGroupCountSlot().getName(),
newGroupCount);
- for (Slot groupKey : aggMeta.getGroupKeySlots()) {
- finalByColumnName.put(groupKey.getName(), deltaGroupKey(delta,
groupKey.getName()));
- }
+ // Group keys are emitted from the delta side below, resolved by slot
identity, and
+ // must not enter the name-keyed map: same-named keys would overwrite
each other.
Set<String> visibleColumnNames = new HashSet<>();
for (IvmAggTarget target : aggMeta.getAggTargets()) {
@@ -330,7 +348,13 @@ class IvmAggDeltaHandler {
// Keep the normalized aggregate schema here. The normalize-added top
project computes row-id above this
// project, and the final sink project reorders columns by MV schema.
for (Slot target : normalizedAgg.getOutput()) {
- Expression expr = finalByColumnName.get(target.getName());
+ // A group key output takes the delta-side key slot: a group that
is new in the
+ // delta has no MV row to copy from. Same-named keys are
disambiguated by slot
+ // identity against aggMeta's ordered group keys.
+ int groupKeyIndex = groupKeyIndexIn(target, aggMeta);
+ Expression expr = groupKeyIndex >= 0
+ ? delta.deltaGroupKeySlots.get(groupKeyIndex)
+ : finalByColumnName.get(target.getName());
if (expr == null) {
throw new IvmException(IvmFailureReason.PLAN_REWRITE_FAILED,
"IVM agg delta rewrite missing output expression for
column: "
@@ -350,11 +374,18 @@ class IvmAggDeltaHandler {
List<Slot> identityKeys = rewriteResult == null ? null :
rewriteResult.getIdentityKeySlots();
if (identityKeys != null) {
for (Slot identityKey : identityKeys) {
+ // The MV side is matched by column name (the MV key columns
are the
+ // normalized outputs). When an MV selects same-named group
keys under
+ // output aliases, the alias differs from the key slot name
and this lookup
+ // misses: the identity conjunct is skipped and the row-id
conjunct still
+ // guards the join, so no wrong value is produced. The delta
side below is
+ // always resolved by slot identity (findDeltaKeyByIdentity),
so a matched
+ // identity key can never be paired with the wrong delta slot.
Slot mvKey =
helper.findSlotByNameOrNull(rawMvScan.getOutput(), identityKey.getName());
if (mvKey == null) {
continue;
}
- Slot deltaKey = findDeltaKeyByName(delta,
identityKey.getName());
+ Slot deltaKey = findDeltaKeyByIdentity(delta, identityKey);
if (deltaKey == null) {
continue;
}
@@ -367,6 +398,21 @@ class IvmAggDeltaHandler {
return conjuncts.build();
}
+ /**
+ * Resolves the delta-side key slot for an identity key by slot identity
first (both
+ * live in the same normalized plan, so same-named keys such as l.id /
r.id match their
+ * own delta slot), falling back to the name-keyed lookup for legacy
callers whose keys
+ * have distinct names.
+ */
+ private Slot findDeltaKeyByIdentity(DeltaPlanParts delta, Slot
identityKey) {
+ for (Slot keySlot : delta.deltaGroupKeySlots) {
+ if (keySlot.getExprId().equals(identityKey.getExprId())) {
+ return keySlot;
+ }
+ }
+ return findDeltaKeyByName(delta, identityKey.getName());
+ }
+
private Slot findDeltaKeyByName(DeltaPlanParts delta, String name) {
Slot direct = delta.groupKeySlotsByName.get(name);
if (direct != null) {
@@ -449,15 +495,6 @@ class IvmAggDeltaHandler {
return delta.deltaGroupCountSlot;
}
- private Expression deltaGroupKey(DeltaPlanParts delta, String name) {
- Slot slot = delta.groupKeySlotsByName.get(name);
- if (slot == null) {
- throw new IvmException(IvmFailureReason.PLAN_REWRITE_FAILED,
- "IVM agg delta rewrite failed to resolve delta group key:
" + name);
- }
- return slot;
- }
-
/**
* Collects delta output names where NULL should be normalized to zero
before apply.
*
@@ -487,4 +524,15 @@ class IvmAggDeltaHandler {
return slotByName;
}
+ /** Index of {@code slot} among the aggregate group keys, or -1 when it is
not a group key. */
+ private int groupKeyIndexIn(Slot slot, IvmAggMeta aggMeta) {
+ List<Slot> groupKeys = aggMeta.getGroupKeySlots();
+ for (int i = 0; i < groupKeys.size(); i++) {
+ if (groupKeys.get(i).getExprId().equals(slot.getExprId())) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/IvmDeltaRewriteHelper.java
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/IvmDeltaRewriteHelper.java
index 2ed7d14931e..7a6bc0f7558 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/IvmDeltaRewriteHelper.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/IvmDeltaRewriteHelper.java
@@ -81,6 +81,20 @@ public class IvmDeltaRewriteHelper {
return null;
}
+ /**
+ * Finds the slot in {@code slots} whose ExprId equals {@code target}'s,
or null. Slots
+ * that share a name (e.g. l.id / r.id after a join) must be resolved by
slot identity
+ * instead of a name-keyed lookup, which collapses same-named slots onto
one of them.
+ */
+ public Slot findSlotByExprId(List<Slot> slots, Slot target) {
+ for (Slot slot : slots) {
+ if (slot.getExprId().equals(target.getExprId())) {
+ return slot;
+ }
+ }
+ return null;
+ }
+
/**
* Adds the root-level fallback guard for a non-deterministic MV row-id.
Inserts remain valid;
* any delete delta fails so the caller can fall back to a full refresh.
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/IvmNormalizeMTMV.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/IvmNormalizeMTMV.java
index 1c92e8017d2..9ce790d31e5 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/IvmNormalizeMTMV.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/IvmNormalizeMTMV.java
@@ -383,9 +383,19 @@ public class IvmNormalizeMTMV extends
DefaultPlanRewriter<IvmNormalizeMTMV.Norma
NamedExpression projected = findProjectedKey(baseOutputs,
keySlot);
if (projected != null) {
survivingKeys.add(projected.toSlot());
- } else {
- extendedOutputs.add(keySlot);
+ } else if (IvmUtil.isIvmHiddenColumn(keySlot.getName())) {
+ // Already materialized by a lower layer: keep the hidden
key slot as is.
survivingKeys.add(keySlot);
+ } else {
+ // An unprojected business key is materialized here as a
hidden key column
+ // instead of being passed through: the result sink
(CREATE) and the
+ // olap-table sink (refresh) then see the same hidden
layout and agree on
+ // the hidden column names. Keys are matched by slot
identity, so a
+ // same-named key from another table (e.g. r.id next to an
output l.id)
+ // still gets its own hidden column.
+ Alias hiddenAlias = materializeHiddenKey(keySlot);
+ extendedOutputs.add(hiddenAlias);
+ survivingKeys.add(hiddenAlias.toSlot());
}
}
finalOutputs = ImmutableList.copyOf(extendedOutputs);
@@ -416,6 +426,21 @@ public class IvmNormalizeMTMV extends
DefaultPlanRewriter<IvmNormalizeMTMV.Norma
return null;
}
+ /**
+ * Materializes an unprojected identity key under a unique hidden key
column name. The
+ * same helper is used by the project layer (where refresh and CREATE
plans first drop a
+ * key) and by the sink (direct-child fallback), so both paths agree on
hidden names.
+ *
+ * <p>Note: the returned alias owns a fresh ExprId, so a later delta
rewrite cannot match
+ * the hidden column to the original key slot by identity; it resolves it
through the
+ * hidden column name (see the agg handler's sanitized-name fallback).
+ */
+ private Alias materializeHiddenKey(Slot keySlot) {
+ String hiddenName = Column.IVM_KEY_COL_PREFIX + (++sinkKeyCounter) +
"_"
+ + IvmUtil.sanitizeIvmKeyName(keySlot.getName()) + "_COL__";
+ return new Alias(keySlot, hiddenName);
+ }
+
@Override
public Plan visitLogicalFilter(LogicalFilter<? extends Plan> filter,
NormalizeContext context) {
Plan result = filter.withChildren(child -> child.accept(this,
context.afterNonSink()));
@@ -927,19 +952,20 @@ public class IvmNormalizeMTMV extends
DefaultPlanRewriter<IvmNormalizeMTMV.Norma
List<Slot> sinkKeys = new ArrayList<>();
List<NamedExpression> finalOutputs = new ArrayList<>(baseOutputs);
if (childKeys != null && !childKeys.isEmpty()) {
- Set<String> outputNames = finalOutputs.stream()
- .map(NamedExpression::getName)
- .collect(Collectors.toSet());
for (Slot keySlot : childKeys) {
String keyName = keySlot.getName();
if (IvmUtil.isIvmHiddenColumn(keyName)) {
sinkKeys.add(keySlot);
- } else if (outputNames.contains(keyName)) {
+ } else if (findProjectedKey(finalOutputs, keySlot) != null) {
+ // The key is already materialized by an output that
directly emits the
+ // key slot (bare slot or alias over it). Matching by slot
identity -
+ // as the project layer does - instead of by name is
required: a
+ // same-named key from another table (e.g. r.id next to an
output l.id)
+ // must still be materialized under its own hidden column,
or the
+ // full-keys identity set silently loses one dimension.
sinkKeys.add(keySlot);
} else {
- String hiddenName = Column.IVM_KEY_COL_PREFIX +
(++sinkKeyCounter) + "_"
- + IvmUtil.sanitizeIvmKeyName(keyName) + "_COL__";
- Alias hiddenAlias = new Alias(keySlot, hiddenName);
+ Alias hiddenAlias = materializeHiddenKey(keySlot);
finalOutputs.add(hiddenAlias);
sinkKeys.add(hiddenAlias.toSlot());
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/IvmAggDeltaHandlerTest.java
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/IvmAggDeltaHandlerTest.java
index 9f6fb5c5b89..44b2597e3c3 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/IvmAggDeltaHandlerTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/IvmAggDeltaHandlerTest.java
@@ -22,7 +22,10 @@ import org.apache.doris.catalog.KeysType;
import org.apache.doris.catalog.MTMV;
import org.apache.doris.catalog.OlapTable;
import org.apache.doris.nereids.analyzer.UnboundTableSink;
+import org.apache.doris.nereids.rules.exploration.join.JoinReorderContext;
import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.EqualTo;
+import org.apache.doris.nereids.trees.expressions.ExprId;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.NamedExpression;
import org.apache.doris.nereids.trees.expressions.Not;
@@ -59,6 +62,7 @@ import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import java.util.stream.Collectors;
class IvmAggDeltaHandlerTest extends IvmDeltaTestBase {
@@ -734,6 +738,106 @@ class IvmAggDeltaHandlerTest extends IvmDeltaTestBase {
"MIN/MAX with expression args should still have assert_true
guards");
}
+ @Test
+ void testGroupedAggWithSameNamedGroupKeysKeepsDistinctDeltaSlots() {
+ // GROUP BY l.id, r.id over a join of two tables that both expose
"id": the
+ // same-named group keys must keep their identity through the delta
rewrite,
+ // otherwise both keys collapse onto one slot and a (10, 20) group
becomes
+ // (20, 20).
+ LogicalOlapScan leftScan = buildScanForTable(1L, "l");
+ LogicalOlapScan rightScan = buildScanForTable(2L, "r");
+ Slot leftId = leftScan.getOutput().get(0);
+ Slot rightId = rightScan.getOutput().get(0);
+ Assertions.assertEquals(leftId.getName(), rightId.getName(),
+ "test setup expects same-named key columns on both base
tables");
+ Assertions.assertNotEquals(leftId.getExprId(), rightId.getExprId());
+
+ LogicalJoin<LogicalOlapScan, LogicalOlapScan> join = new LogicalJoin<>(
+ JoinType.INNER_JOIN, ImmutableList.of(new EqualTo(leftId,
rightId)),
+ leftScan, rightScan, JoinReorderContext.EMPTY);
+ Alias countAlias = new Alias(new Count(), "cnt");
+ LogicalAggregate<LogicalJoin<LogicalOlapScan, LogicalOlapScan>> agg =
new LogicalAggregate<>(
+ ImmutableList.of(leftId, rightId),
+ ImmutableList.of(leftId, rightId, countAlias),
+ true, Optional.empty(), join);
+
+ AggRewriteResult result = rewriteAgg(agg);
+ List<Slot> aggGroupKeys =
result.bundle.rewriteResult.getAggMeta().getGroupKeySlots();
+ Assertions.assertEquals(2, aggGroupKeys.size());
+ Assertions.assertEquals(leftId.getName(),
aggGroupKeys.get(0).getName());
+ Assertions.assertEquals(leftId.getName(),
aggGroupKeys.get(1).getName());
+
+ // Apply project must emit each group key from its own delta-side slot
(the apply
+ // project output order follows the aggregate outputs, keys first).
+ List<Expression> applyKeyExprs =
getApplyProject(result).getProjects().stream()
+ .filter(projection ->
leftId.getName().equals(projection.getName()))
+ .map(projection -> projection instanceof Alias ? ((Alias)
projection).child() : projection)
+ .collect(Collectors.toList());
+ Assertions.assertEquals(2, applyKeyExprs.size(), "expected two
same-named group key outputs");
+ Assertions.assertInstanceOf(Slot.class, applyKeyExprs.get(0));
+ Assertions.assertInstanceOf(Slot.class, applyKeyExprs.get(1));
+ Assertions.assertNotEquals(((Slot) applyKeyExprs.get(0)).getExprId(),
+ ((Slot) applyKeyExprs.get(1)).getExprId(),
+ "same-named group keys must resolve to distinct delta slots");
+
+ // The apply keys must be exactly the delta group key slots in aggMeta
order. The
+ // delta top project output is laid out as [row_id, group keys...,
other outputs]
+ // (the row-id hash relies on the same layout), so group key i sits at
index 1 + i.
+ LogicalProject<?> deltaTop = getDeltaTopProject(result);
+ Assertions.assertEquals(((Slot) applyKeyExprs.get(0)).getExprId(),
+ deltaTop.getOutput().get(1).getExprId());
+ Assertions.assertEquals(((Slot) applyKeyExprs.get(1)).getExprId(),
+ deltaTop.getOutput().get(2).getExprId());
+ }
+
+ @Test
+ void testGroupedAggFullKeysWithSameNamedGroupKeysKeepsDistinctDeltaSlots()
{
+ // With ivm_use_full_keys the MV key is row_id + group by keys; the
same-named
+ // l.id / r.id identity keys must each resolve to their own delta slot
in the
+ // apply-join identity conjuncts, not both to the last same-named slot.
+ LogicalOlapScan leftScan = buildScanForTable(1L, "l");
+ LogicalOlapScan rightScan = buildScanForTable(2L, "r");
+ Slot leftId = leftScan.getOutput().get(0);
+ Slot rightId = rightScan.getOutput().get(0);
+ LogicalJoin<LogicalOlapScan, LogicalOlapScan> joinInput = new
LogicalJoin<>(
+ JoinType.INNER_JOIN, ImmutableList.of(new EqualTo(leftId,
rightId)),
+ leftScan, rightScan, JoinReorderContext.EMPTY);
+ Alias countAlias = new Alias(new Count(), "cnt");
+ LogicalAggregate<LogicalJoin<LogicalOlapScan, LogicalOlapScan>> agg =
new LogicalAggregate<>(
+ ImmutableList.of(leftId, rightId),
+ ImmutableList.of(leftId, rightId, countAlias),
+ true, Optional.empty(), joinInput);
+
+ AggRewriteResult result = rewriteAggWithIdentityKeys(agg,
+ ImmutableList.of(leftId, rightId));
+ LogicalJoin<?, ?> join = getJoin(result);
+
+ LogicalProject<?> deltaTop = getDeltaTopProject(result);
+ List<Slot> deltaKeySlots =
ImmutableList.of(deltaTop.getOutput().get(1),
+ deltaTop.getOutput().get(2));
+
+ // Identity conjuncts are the NullSafeEquals that are not the row-id
conjunct;
+ // each same-named identity key must be joined against its own delta
key slot.
+ List<Expression> identityConjuncts =
join.getHashJoinConjuncts().stream()
+ .filter(condition -> condition instanceof NullSafeEqual)
+ .filter(condition ->
!condition.toSql().contains(Column.IVM_ROW_ID_COL))
+ .collect(Collectors.toList());
+ Assertions.assertEquals(2, identityConjuncts.size(),
+ "expected one identity conjunct per group key, but got: " +
identityConjuncts);
+ Set<ExprId> deltaSides = identityConjuncts.stream()
+ .map(conjunct -> {
+ NullSafeEqual equal = (NullSafeEqual) conjunct;
+ Expression right = equal.right();
+ return right instanceof Slot ? ((Slot) right).getExprId()
+ : ((Slot) equal.left()).getExprId();
+ })
+ .collect(Collectors.toSet());
+ Assertions.assertEquals(2, deltaSides.size(),
+ "same-named identity keys must map to distinct delta key
slots");
+
Assertions.assertTrue(deltaSides.contains(deltaKeySlots.get(0).getExprId()));
+
Assertions.assertTrue(deltaSides.contains(deltaKeySlots.get(1).getExprId()));
+ }
+
private static final class AggRewriteResult {
private final PlanBundle bundle;
private final MTMV mtmv;
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/IvmNormalizeMTMVJoinTest.java
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/IvmNormalizeMTMVJoinTest.java
index 6f4e26b820c..0062b1f99ae 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/IvmNormalizeMTMVJoinTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/ivm/IvmNormalizeMTMVJoinTest.java
@@ -19,6 +19,7 @@ package org.apache.doris.mtmv.ivm;
import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.KeysType;
+import org.apache.doris.catalog.MTMV;
import org.apache.doris.catalog.OlapTable;
import org.apache.doris.catalog.info.TableNameInfo;
import org.apache.doris.nereids.hint.DistributeHint;
@@ -59,6 +60,7 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -1024,4 +1026,69 @@ class IvmNormalizeMTMVJoinTest extends IvmDeltaTestBase {
Assertions.assertEquals(JOIN_RIGHT_MATCH_COL,
slotOfIsNullEncoding(hashChildren.get(5)).getName(),
"last outer compose key is the outer right match flag");
}
+
+ @Test
+ void testFullKeysSinkMaterializesSameNamedUnprojectedKey() {
+ // SELECT l.id FROM l JOIN r ON l.id = r.id with ivm_use_full_keys:
the right id is
+ // an identity key that is not projected by the user output. It shares
its name with
+ // the projected left id, so a name-keyed "already projected" test at
the sink would
+ // drop it; the sink must materialize it under its own hidden key
column instead.
+ LogicalOlapScan left = buildMowScan(1, "l");
+ LogicalOlapScan right = buildMowScan(2, "r");
+ Slot leftId = left.getOutput().get(0);
+ Slot rightId = right.getOutput().get(0);
+ Assertions.assertEquals(leftId.getName(), rightId.getName(),
+ "test setup expects same-named key columns on both base
tables");
+
+ LogicalJoin<LogicalOlapScan, LogicalOlapScan> join = new LogicalJoin<>(
+ JoinType.INNER_JOIN, ImmutableList.of(new EqualTo(leftId,
rightId)),
+ left, right, JoinReorderContext.EMPTY);
+ LogicalProject<Plan> project = new
LogicalProject<>(ImmutableList.of(leftId), join);
+ LogicalResultSink<Plan> sink = new
LogicalResultSink<>(ImmutableList.of(leftId), project);
+
+ MTMV mtmv = buildMtmvFromPlan(ImmutableList.of(leftId));
+ mtmv.getIvmInfo().setUseFullKeys(true);
+ ConnectContext ctx = newConnectContext();
+ ctx.getStatementContext().setIvmRewriteContext(Optional.of(
+ new IvmRewriteContext(IvmRewriteContext.Mode.CREATE, mtmv,
false)));
+ JobContext jobContext = newJobContextForRoot(sink, ctx);
+ Plan normalized = new IvmNormalizeMTMV().rewriteRoot(sink, jobContext);
+ IvmRewriteResult rewriteResult =
jobContext.getCascadesContext().getIvmRewriteResult().get();
+
+ List<Slot> identityKeys = rewriteResult.getIdentityKeySlots();
+ Assertions.assertNotNull(identityKeys);
+ Assertions.assertTrue(identityKeys.stream()
+ .anyMatch(key ->
key.getExprId().equals(leftId.getExprId())),
+ "the projected left id must stay an identity key, but got: " +
identityKeys);
+
+ String embeddedId = IvmUtil.sanitizeIvmKeyName(rightId.getName());
+ Slot rightHiddenKey = identityKeys.stream()
+ .filter(key -> IvmUtil.isIvmHiddenColumn(key.getName())
+ && key.getName().endsWith("_" + embeddedId + "_COL__"))
+ .findFirst().orElse(null);
+ Assertions.assertNotNull(rightHiddenKey,
+ "same-named right id key must be materialized as a hidden key
column, got: "
+ + identityKeys);
+
+ // The hidden key column is materialized at the first project that
does not project
+ // the key: find that alias in the normalized tree and verify it wraps
the right id
+ // slot rather than the same-named left id.
+ List<Alias> hiddenAliases = new ArrayList<>();
+ normalized.foreach(node -> {
+ if (node instanceof LogicalProject) {
+ for (NamedExpression projection : ((LogicalProject<?>)
node).getProjects()) {
+ if (projection instanceof Alias &&
projection.getName().equals(rightHiddenKey.getName())) {
+ hiddenAliases.add((Alias) projection);
+ }
+ }
+ }
+ });
+ Assertions.assertFalse(hiddenAliases.isEmpty(),
+ "expected a hidden key projection named " +
rightHiddenKey.getName());
+ for (Alias hiddenAlias : hiddenAliases) {
+ Assertions.assertInstanceOf(Slot.class, hiddenAlias.child());
+ Assertions.assertEquals(rightId.getExprId(), ((Slot)
hiddenAlias.child()).getExprId(),
+ "the hidden key column must wrap the right id slot, not
the same-named left id");
+ }
+ }
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/IvmNormalizeMTMVTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/IvmNormalizeMTMVTest.java
index 34898b049b8..a64a223960c 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/IvmNormalizeMTMVTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/IvmNormalizeMTMVTest.java
@@ -1271,12 +1271,21 @@ class IvmNormalizeMTMVTest {
ImmutableList.of(join.getOutput().get(0),
join.getOutput().get(1)), join);
JobContext jobContext = newJobContextWithFullKeys(sink);
- new IvmNormalizeMTMV().rewriteRoot(sink, jobContext);
+ Plan result = new IvmNormalizeMTMV().rewriteRoot(sink, jobContext);
IvmRewriteResult rewriteResult =
jobContext.getCascadesContext().getIvmRewriteResult().orElseThrow();
List<String> keyNames = rewriteResult.getIdentityKeySlots().stream()
.map(Slot::getName).collect(Collectors.toList());
- // Left table keys (id, name) followed by right table keys (id, name),
deduped by output presence.
- Assertions.assertEquals(ImmutableList.of("id", "name", "id", "name"),
keyNames);
+ // Left keys (id, name) are the projected outputs; the same-named
right keys are not
+ // projected, so each must be materialized under its own hidden key
column instead of
+ // being folded onto the same-named left outputs (which would silently
drop the right
+ // dimensions from the full-keys layout).
+ String rightIdKey = Column.IVM_KEY_COL_PREFIX + "1_id_COL__";
+ String rightNameKey = Column.IVM_KEY_COL_PREFIX + "2_name_COL__";
+ Assertions.assertEquals(ImmutableList.of("id", "name", rightIdKey,
rightNameKey), keyNames);
+ List<String> outputNames = result.getOutput().stream()
+ .map(Slot::getName).collect(Collectors.toList());
+ Assertions.assertTrue(outputNames.contains(rightIdKey));
+ Assertions.assertTrue(outputNames.contains(rightNameKey));
}
@Test
diff --git
a/regression-test/data/mtmv_p0/ivm/test_ivm_agg_join_same_name_group_key.out
b/regression-test/data/mtmv_p0/ivm/test_ivm_agg_join_same_name_group_key.out
new file mode 100644
index 00000000000..23626b2cec3
--- /dev/null
+++ b/regression-test/data/mtmv_p0/ivm/test_ivm_agg_join_same_name_group_key.out
@@ -0,0 +1,50 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !same_key_complete --
+1 10 100 1
+2 20 200 1
+3 30 300 1
+
+-- !same_key_incremental --
+1 10 100 1
+2 20 200 1
+3 30 300 1
+4 40 400 1
+
+-- !same_key_complete2 --
+1 10 100 1
+2 20 200 1
+3 30 300 1
+4 40 400 1
+
+-- !same_key_fk_complete --
+1 10 100 1
+2 20 200 1
+3 30 300 1
+
+-- !same_key_fk_incremental --
+1 10 100 1
+2 20 200 1
+3 30 300 1
+4 40 400 1
+
+-- !same_key_fk_complete2 --
+1 10 100 1
+2 20 200 1
+3 30 300 1
+4 40 400 1
+
+-- !same_key_up_complete --
+1 10 100 1
+2 20 200 1
+3 30 300 1
+
+-- !same_key_up_incremental --
+1 50 50 1
+2 20 200 1
+3 30 300 1
+
+-- !same_key_up_complete2 --
+1 50 50 1
+2 20 200 1
+3 30 300 1
+
diff --git
a/regression-test/data/mtmv_p0/ivm/test_ivm_full_keys_same_name_unprojected_key.out
b/regression-test/data/mtmv_p0/ivm/test_ivm_full_keys_same_name_unprojected_key.out
new file mode 100644
index 00000000000..bb339f239c6
--- /dev/null
+++
b/regression-test/data/mtmv_p0/ivm/test_ivm_full_keys_same_name_unprojected_key.out
@@ -0,0 +1,30 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !fk_hidden_desc --
+lid int Yes true \N
+__DORIS_IVM_KEY_1_id_COL__ int Yes true \N
+__DORIS_IVM_ROW_ID_COL__ largeint No true \N
+total bigint Yes false \N NONE
+cnt bigint No false \N NONE
+__DORIS_IVM_AGG_COUNT_COL__ bigint No false \N NONE
+__DORIS_IVM_AGG_0_COUNT_COL__ bigint No false \N NONE
+__DORIS_DELETE_SIGN__ tinyint No false 0 NONE
+__DORIS_VERSION_COL__ bigint No false 0 NONE
+__DORIS_SEQUENCE_COL__ bigint Yes false \N NONE
+
+-- !fk_hidden_complete --
+1 100 1
+2 200 1
+3 300 1
+
+-- !fk_hidden_incremental --
+1 100 1
+2 200 1
+3 300 1
+4 400 1
+
+-- !fk_hidden_complete2 --
+1 100 1
+2 200 1
+3 300 1
+4 400 1
+
diff --git
a/regression-test/suites/mtmv_p0/ivm/test_ivm_agg_join_same_name_group_key.groovy
b/regression-test/suites/mtmv_p0/ivm/test_ivm_agg_join_same_name_group_key.groovy
new file mode 100644
index 00000000000..a67617d31a8
--- /dev/null
+++
b/regression-test/suites/mtmv_p0/ivm/test_ivm_agg_join_same_name_group_key.groovy
@@ -0,0 +1,263 @@
+// 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("test_ivm_agg_join_same_name_group_key") {
+
+ // GROUP BY l.id, r.id over a join of two tables that both expose a column
"id": the
+ // same-named group keys must keep their identity through the aggregate
delta rewrite.
+ // A name-keyed lookup collapses both keys onto one slot and turns group
(1,20) into
+ // (20,20), diverging from COMPLETE.
+
+ def mvName = "ivm_same_key_mv"
+ def lTable = "ivm_same_key_l"
+ def rTable = "ivm_same_key_r"
+
+ sql """drop materialized view if exists ${mvName}"""
+ sql """drop table if exists ${lTable}"""
+ sql """drop table if exists ${rTable}"""
+
+ // Both tables carry a column literally named "id"; the join key is k.
+ sql """
+ CREATE TABLE ${lTable} (
+ id INT,
+ k INT,
+ v INT
+ )
+ UNIQUE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES (
+ "replication_num" = "1",
+ "binlog.enable" = "true",
+ "binlog.format" = "ROW", "binlog.need_historical_value" = "true",
+ "enable_unique_key_merge_on_write" = "true"
+ )
+ """
+ sql """
+ CREATE TABLE ${rTable} (
+ id INT,
+ k INT
+ )
+ UNIQUE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES (
+ "replication_num" = "1",
+ "binlog.enable" = "true",
+ "binlog.format" = "ROW", "binlog.need_historical_value" = "true",
+ "enable_unique_key_merge_on_write" = "true"
+ )
+ """
+ sql """INSERT INTO ${lTable} VALUES (1,1,100),(2,2,200),(3,3,300)"""
+ sql """INSERT INTO ${rTable} VALUES (10,1),(20,2),(30,3)"""
+
+ sql """
+ CREATE MATERIALIZED VIEW ${mvName}
+ BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL
+ DISTRIBUTED BY RANDOM BUCKETS 2
+ PROPERTIES ('replication_num' = '1')
+ AS
+ SELECT ${lTable}.id AS lid,
+ ${rTable}.id AS rid,
+ SUM(${lTable}.v) AS total,
+ COUNT(*) AS cnt
+ FROM ${lTable}
+ INNER JOIN ${rTable}
+ ON ${lTable}.k = ${rTable}.k
+ GROUP BY ${lTable}.id, ${rTable}.id
+ """
+
+ sql """REFRESH MATERIALIZED VIEW ${mvName} COMPLETE"""
+ waitingMTMVTaskFinishedByMvName(mvName)
+ order_qt_same_key_complete """SELECT lid, rid, total, cnt FROM ${mvName}"""
+
+ // Incremental delta: a brand-new group (lid=4, rid=40). Under the
same-name collapse
+ // the lid value would be taken from the rid slot and the group would be
written as
+ // (40,40), so this assert is the regression guard.
+ sql """INSERT INTO ${lTable} VALUES (4,4,400)"""
+ sql """INSERT INTO ${rTable} VALUES (40,4)"""
+ sql """REFRESH MATERIALIZED VIEW ${mvName} INCREMENTAL"""
+ waitingMTMVTaskFinishedByMvName(mvName)
+ order_qt_same_key_incremental """SELECT lid, rid, total, cnt FROM
${mvName}"""
+
+ // Cross-check against a fresh COMPLETE rebuild.
+ sql """REFRESH MATERIALIZED VIEW ${mvName} COMPLETE"""
+ waitingMTMVTaskFinishedByMvName(mvName)
+ order_qt_same_key_complete2 """SELECT lid, rid, total, cnt FROM
${mvName}"""
+
+ sql """drop materialized view if exists ${mvName}"""
+ sql """drop table if exists ${lTable}"""
+ sql """drop table if exists ${rTable}"""
+
+ // =========================================================
+ // Part 2: same-named group keys with ivm_use_full_keys=true
+ // (MV key = row_id + group by keys)
+ // =========================================================
+ def fkMv = "ivm_same_key_fk_mv"
+ def fkL = "ivm_same_key_fk_l"
+ def fkR = "ivm_same_key_fk_r"
+
+ sql """drop materialized view if exists ${fkMv}"""
+ sql """drop table if exists ${fkL}"""
+ sql """drop table if exists ${fkR}"""
+
+ sql """
+ CREATE TABLE ${fkL} (
+ id INT,
+ k INT,
+ v INT
+ )
+ UNIQUE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES (
+ "replication_num" = "1",
+ "binlog.enable" = "true",
+ "binlog.format" = "ROW", "binlog.need_historical_value" = "true",
+ "enable_unique_key_merge_on_write" = "true"
+ )
+ """
+ sql """
+ CREATE TABLE ${fkR} (
+ id INT,
+ k INT
+ )
+ UNIQUE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES (
+ "replication_num" = "1",
+ "binlog.enable" = "true",
+ "binlog.format" = "ROW", "binlog.need_historical_value" = "true",
+ "enable_unique_key_merge_on_write" = "true"
+ )
+ """
+ sql """INSERT INTO ${fkL} VALUES (1,1,100),(2,2,200),(3,3,300)"""
+ sql """INSERT INTO ${fkR} VALUES (10,1),(20,2),(30,3)"""
+
+ sql """
+ CREATE MATERIALIZED VIEW ${fkMv}
+ BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL
+ DISTRIBUTED BY RANDOM BUCKETS 2
+ PROPERTIES ('replication_num' = '1', 'ivm_use_full_keys' = 'true')
+ AS
+ SELECT ${fkL}.id AS lid,
+ ${fkR}.id AS rid,
+ SUM(${fkL}.v) AS total,
+ COUNT(*) AS cnt
+ FROM ${fkL}
+ INNER JOIN ${fkR}
+ ON ${fkL}.k = ${fkR}.k
+ GROUP BY ${fkL}.id, ${fkR}.id
+ """
+
+ sql """REFRESH MATERIALIZED VIEW ${fkMv} COMPLETE"""
+ waitingMTMVTaskFinishedByMvName(fkMv)
+ order_qt_same_key_fk_complete """SELECT lid, rid, total, cnt FROM
${fkMv}"""
+
+ sql """INSERT INTO ${fkL} VALUES (4,4,400)"""
+ sql """INSERT INTO ${fkR} VALUES (40,4)"""
+ sql """REFRESH MATERIALIZED VIEW ${fkMv} INCREMENTAL"""
+ waitingMTMVTaskFinishedByMvName(fkMv)
+ order_qt_same_key_fk_incremental """SELECT lid, rid, total, cnt FROM
${fkMv}"""
+
+ // Cross-check the full-keys MV against a fresh COMPLETE rebuild.
+ sql """REFRESH MATERIALIZED VIEW ${fkMv} COMPLETE"""
+ waitingMTMVTaskFinishedByMvName(fkMv)
+ order_qt_same_key_fk_complete2 """SELECT lid, rid, total, cnt FROM
${fkMv}"""
+
+ sql """drop materialized view if exists ${fkMv}"""
+ sql """drop table if exists ${fkL}"""
+ sql """drop table if exists ${fkR}"""
+
+ // =========================================================
+ // Part 3: same-named group keys under UPDATE (delete + insert delta)
+ // =========================================================
+ def upMv = "ivm_same_key_up_mv"
+ def upL = "ivm_same_key_up_l"
+ def upR = "ivm_same_key_up_r"
+
+ sql """drop materialized view if exists ${upMv}"""
+ sql """drop table if exists ${upL}"""
+ sql """drop table if exists ${upR}"""
+
+ sql """
+ CREATE TABLE ${upL} (
+ id INT,
+ k INT,
+ v INT
+ )
+ UNIQUE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES (
+ "replication_num" = "1",
+ "binlog.enable" = "true",
+ "binlog.format" = "ROW", "binlog.need_historical_value" = "true",
+ "enable_unique_key_merge_on_write" = "true"
+ )
+ """
+ sql """
+ CREATE TABLE ${upR} (
+ id INT,
+ k INT
+ )
+ UNIQUE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES (
+ "replication_num" = "1",
+ "binlog.enable" = "true",
+ "binlog.format" = "ROW", "binlog.need_historical_value" = "true",
+ "enable_unique_key_merge_on_write" = "true"
+ )
+ """
+ sql """INSERT INTO ${upL} VALUES (1,1,100),(2,2,200),(3,3,300)"""
+ sql """INSERT INTO ${upR} VALUES (10,1),(20,2),(30,3)"""
+
+ sql """
+ CREATE MATERIALIZED VIEW ${upMv}
+ BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL
+ DISTRIBUTED BY RANDOM BUCKETS 2
+ PROPERTIES ('replication_num' = '1')
+ AS
+ SELECT ${upL}.id AS lid,
+ ${upR}.id AS rid,
+ SUM(${upL}.v) AS total,
+ COUNT(*) AS cnt
+ FROM ${upL}
+ INNER JOIN ${upR}
+ ON ${upL}.k = ${upR}.k
+ GROUP BY ${upL}.id, ${upR}.id
+ """
+
+ sql """REFRESH MATERIALIZED VIEW ${upMv} COMPLETE"""
+ waitingMTMVTaskFinishedByMvName(upMv)
+ order_qt_same_key_up_complete """SELECT lid, rid, total, cnt FROM
${upMv}"""
+
+ // MOW update on l: (1,1,100) -> (1,5,50) drops the join with r(10,1) and
forms a new
+ // group (1,50). The incremental delta deletes group (1,10) and inserts
group (1,50);
+ // a same-name collapse would keep (1,10) and/or write the new key wrongly.
+ sql """INSERT INTO ${upL} VALUES (1,5,50)"""
+ sql """INSERT INTO ${upR} VALUES (50,5)"""
+ sql """REFRESH MATERIALIZED VIEW ${upMv} INCREMENTAL"""
+ waitingMTMVTaskFinishedByMvName(upMv)
+ order_qt_same_key_up_incremental """SELECT lid, rid, total, cnt FROM
${upMv}"""
+
+ // Cross-check the UPDATE result against a fresh COMPLETE rebuild.
+ sql """REFRESH MATERIALIZED VIEW ${upMv} COMPLETE"""
+ waitingMTMVTaskFinishedByMvName(upMv)
+ order_qt_same_key_up_complete2 """SELECT lid, rid, total, cnt FROM
${upMv}"""
+
+ sql """drop materialized view if exists ${upMv}"""
+ sql """drop table if exists ${upL}"""
+ sql """drop table if exists ${upR}"""
+}
diff --git
a/regression-test/suites/mtmv_p0/ivm/test_ivm_full_keys_same_name_unprojected_key.groovy
b/regression-test/suites/mtmv_p0/ivm/test_ivm_full_keys_same_name_unprojected_key.groovy
new file mode 100644
index 00000000000..900ec27cbfc
--- /dev/null
+++
b/regression-test/suites/mtmv_p0/ivm/test_ivm_full_keys_same_name_unprojected_key.groovy
@@ -0,0 +1,106 @@
+// 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("test_ivm_full_keys_same_name_unprojected_key") {
+
+ // GROUP BY l.id, r.id over a join where both tables expose "id", while
the user output
+ // selects only the left id (right id is an identity key that must be
materialized as a
+ // hidden key column). With ivm_use_full_keys the MV key is row_id + group
by keys;
+ // treating the same-named right id as already projected drops it from the
key set.
+
+ def mvName = "ivm_fk_hidden_mv"
+ def lTable = "ivm_fk_hidden_l"
+ def rTable = "ivm_fk_hidden_r"
+
+ sql """drop materialized view if exists ${mvName}"""
+ sql """drop table if exists ${lTable}"""
+ sql """drop table if exists ${rTable}"""
+
+ sql """
+ CREATE TABLE ${lTable} (
+ id INT,
+ k INT,
+ v INT
+ )
+ UNIQUE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES (
+ "replication_num" = "1",
+ "binlog.enable" = "true",
+ "binlog.format" = "ROW", "binlog.need_historical_value" = "true",
+ "enable_unique_key_merge_on_write" = "true"
+ )
+ """
+ sql """
+ CREATE TABLE ${rTable} (
+ id INT,
+ k INT
+ )
+ UNIQUE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 2
+ PROPERTIES (
+ "replication_num" = "1",
+ "binlog.enable" = "true",
+ "binlog.format" = "ROW", "binlog.need_historical_value" = "true",
+ "enable_unique_key_merge_on_write" = "true"
+ )
+ """
+ sql """INSERT INTO ${lTable} VALUES (1,1,100),(2,2,200),(3,3,300)"""
+ sql """INSERT INTO ${rTable} VALUES (10,1),(20,2),(30,3)"""
+
+ // Only l.id is selected; r.id participates in GROUP BY but is not in the
output, so it
+ // must be carried by a hidden key column under ivm_use_full_keys.
+ sql """
+ CREATE MATERIALIZED VIEW ${mvName}
+ BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL
+ DISTRIBUTED BY RANDOM BUCKETS 2
+ PROPERTIES ('replication_num' = '1', 'ivm_use_full_keys' = 'true')
+ AS
+ SELECT ${lTable}.id AS lid,
+ SUM(${lTable}.v) AS total,
+ COUNT(*) AS cnt
+ FROM ${lTable}
+ INNER JOIN ${rTable}
+ ON ${lTable}.k = ${rTable}.k
+ GROUP BY ${lTable}.id, ${rTable}.id
+ """
+
+ // The hidden key column for the unprojected r.id group key must exist in
the MV schema
+ // (hidden columns are hidden from DESC unless show_hidden_columns is on).
+ sql """set show_hidden_columns = true"""
+ qt_fk_hidden_desc """DESC ${mvName}"""
+ sql """set show_hidden_columns = false"""
+
+ sql """REFRESH MATERIALIZED VIEW ${mvName} COMPLETE"""
+ waitingMTMVTaskFinishedByMvName(mvName)
+ order_qt_fk_hidden_complete """SELECT lid, total, cnt FROM ${mvName}"""
+
+ sql """INSERT INTO ${lTable} VALUES (4,4,400)"""
+ sql """INSERT INTO ${rTable} VALUES (40,4)"""
+ sql """REFRESH MATERIALIZED VIEW ${mvName} INCREMENTAL"""
+ waitingMTMVTaskFinishedByMvName(mvName)
+ order_qt_fk_hidden_incremental """SELECT lid, total, cnt FROM ${mvName}"""
+
+ // Cross-check against a fresh COMPLETE rebuild.
+ sql """REFRESH MATERIALIZED VIEW ${mvName} COMPLETE"""
+ waitingMTMVTaskFinishedByMvName(mvName)
+ order_qt_fk_hidden_complete2 """SELECT lid, total, cnt FROM ${mvName}"""
+
+ sql """drop materialized view if exists ${mvName}"""
+ sql """drop table if exists ${lTable}"""
+ sql """drop table if exists ${rTable}"""
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]