github-actions[bot] commented on code in PR #68787:
URL: https://github.com/apache/doris/pull/68787#discussion_r4225599960


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/IvmNormalizeMTMV.java:
##########
@@ -1069,6 +1071,202 @@ private Map<String, Slot> collectIvmHiddenSlots(Plan 
normalizedChild) {
                 .collect(Collectors.toMap(Slot::getName, slot -> slot, (left, 
right) -> left, LinkedHashMap::new));
     }
 
+    /**
+     * Materializes the aggregate state columns this layer drops, so an 
incremental refresh can still
+     * read the old aggregate state from the MV.
+     *
+     * <p>Every state slot the apply stage reads must be a persisted MV 
column, because apply resolves
+     * the old state by column name from the MV physical table. Hidden state 
columns are hidden-named
+     * and therefore propagate through every layer, but the aggregate 
functions whose own value is
+     * their mergeable state (SUM, COUNT(expr), MIN/MAX, 
COLLECT_LIST/ARRAY_AGG, BITMAP_UNION) keep
+     * that state in their visible column, which disappears as soon as an 
upper layer consumes it
+     * inside an expression without projecting it, as in {@code SELECT SUM(v) 
* 100}. Such a slot is
+     * materialized here as a bare pass-through hidden alias, and every target 
reading it is rebound
+     * to that alias.
+     *
+     * <p>Rebinding matters as much as materializing: the column pool lets a 
target reuse a visible
+     * aggregate column as its own hidden state (AVG reusing a visible SUM 
column), so a reusing target
+     * must follow the column's owner onto the materialized carrier instead of 
reading a column that no
+     * longer reaches the MV.
+     *
+     * <p>The materialized name is the name the delta sub-plan already 
generates for that state
+     * ({@link IvmUtil#ivmAggHiddenColumnName}, keyed by the owning target's 
ordinal and kind), so delta
+     * aggregate outputs and delta slot lookups are unaffected and both sides 
of the merge agree on the
+     * column name. A slot an earlier layer already materialized, or that 
another target already
+     * materialized for the same aggregate state, is reused instead of 
materializing a duplicate.
+     */
+    private List<NamedExpression> 
materializeDroppedAggState(List<NamedExpression> outputs) {
+        IvmAggMeta aggMeta = rewriteResult.getAggMeta();
+        if (aggMeta == null) {
+            // Below the aggregate no target is known yet, so no aggregate 
state can be dropped here.
+            return outputs;
+        }
+        List<NamedExpression> extendedOutputs = new ArrayList<>(outputs);
+        List<IvmAggTarget> reboundTargets = new 
ArrayList<>(aggMeta.getAggTargets().size());
+        boolean rebound = false;
+        // While the layout is being created there is no MV yet (CREATE 
MATERIALIZED VIEW keeps only its
+        // name), so normalize decides the layout itself; a refresh is bound 
by the MV's own schema.
+        MTMV mtmv = statementContext.getIvmRewriteContext().get().getMtmv();
+        for (IvmAggTarget target : aggMeta.getAggTargets()) {
+            Slot valueStateSlot = valueStateSlotFor(target, mtmv, 
extendedOutputs, aggMeta);
+            ImmutableMap.Builder<IvmAggStateKey, Slot> hiddenStateSlots = 
ImmutableMap.builder();
+            for (Map.Entry<IvmAggStateKey, Slot> hiddenStateSlot : 
target.getHiddenStateSlots().entrySet()) {
+                hiddenStateSlots.put(hiddenStateSlot.getKey(),
+                        materializeAggStateSlot(extendedOutputs, 
hiddenStateSlot.getValue(), aggMeta));
+            }
+            IvmAggTarget reboundTarget = target.withStateSlots(valueStateSlot, 
hiddenStateSlots.build());
+            rebound |= reboundTarget != target;
+            reboundTargets.add(reboundTarget);
+        }
+        if (rebound) {
+            // Keep the rebinding visible to the layers above: a layer that 
passes a state column through
+            // under a different slot (the refresh sink rebinds the normalized 
hidden columns to the MV's
+            // own slots) changes which slot carries the state without adding 
any column.
+            rewriteResult.setAggMeta(aggMeta.withAggTargets(reboundTargets));
+        }
+        return extendedOutputs;
+    }
+
+    /**
+     * Returns the slot this target's own value state is read from, 
materializing a carrier column when the
+     * state needs a column the layout does not have yet.
+     *
+     * <p>When {@code mtmv} is null the layout is being created (CREATE 
MATERIALIZED VIEW knows only its
+     * name): the state is the visible aggregate column when that column 
survives into the MV, and a
+     * materialized carrier otherwise. On a refresh {@code mtmv} exists and 
its schema owns the layout: the
+     * state is the carrier only when the MV really has that column, and 
otherwise the MV's visible column
+     * carries it. That keeps the refresh path independent of the binder's 
insert projects, which rename a
+     * state column locally and rebuild it as the MV column under its own 
name, so a user column that
+     * happens to be named like the aggregate's generated alias never becomes 
the state.
+     */
+    private Slot valueStateSlotFor(IvmAggTarget target, MTMV mtmv, 
List<NamedExpression> outputs,
+            IvmAggMeta aggMeta) {
+        if (!aggFunctionRegistry.visibleColumnHoldsValueState(target)) {
+            // AVG, BITMAP_UNION_COUNT and COUNT(*) merge hidden state or the 
group count instead.
+            return target.getValueStateSlot();
+        }
+        // The column currently carrying this target's value: the carrier 
materialized by a lower layer if
+        // there is one, otherwise the visible column the aggregate still 
produces.
+        Slot stateSlot = target.getValueStateSlot() != null
+                ? target.getValueStateSlot() : target.getVisibleSlot();
+        if (mtmv == null) {
+            // Otherwise the projection decides: the state keeps the column 
the plan projects it as (the
+            // aggregate output itself or a rename of it), and a state the 
projection drops gets a carrier.
+            return materializeAggStateSlot(outputs, stateSlot, aggMeta);
+        }
+        String carrierName = 
IvmUtil.ivmAggHiddenColumnName(target.getOrdinal(),
+                target.getFunctionKind().name());
+        if (mtmv.getColumn(carrierName) != null) {
+            return materializeAggStateSlot(outputs, stateSlot, aggMeta);
+        }
+        // Without a carrier the MV keeps the state in the column the plan 
projected it as. That name is
+        // only usable when the MV really has it: the binder renames a state 
column to the MV's own column
+        // name for an unnamed aggregate (COUNT(*) becomes __count_0), while 
for a clamped key column it
+        // renames the state to a project-local name and rebuilds the MV 
column by coercion, in which case
+        // the MV's visible column carries the state.
+        NamedExpression projected = findProjectedKey(outputs, stateSlot);
+        return projected != null && mtmv.getColumn(projected.getName()) != null
+                ? projected.toSlot() : null;
+    }
+
+    /**
+     * Returns the slot that carries {@code stateSlot} above this layer, 
appending a hidden pass-through
+     * alias when this layer drops it.
+     *
+     * <p>A layer keeps the state alive when it projects the state slot itself 
or a pure rename of it, which
+     * is how the refresh sink maps the normalized hidden columns onto the 
MV's own slots. Only when the
+     * state slot is really gone from the layer's output does it need a 
carrier.
+     */
+    private Slot materializeAggStateSlot(List<NamedExpression> outputs, Slot 
stateSlot, IvmAggMeta aggMeta) {
+        NamedExpression projected = findProjectedKey(outputs, stateSlot);
+        if (projected != null) {
+            // The projecting output's own slot is what carries the value: the 
column itself, a column it
+            // projects through, or a carrier another target needed for this 
same state.
+            return projected.toSlot();
+        }
+        return materializeStateCarrier(outputs, stateSlot, aggMeta);
+    }
+
+    /**
+     * Appends a hidden carrier column for {@code stateSlot} and returns its 
slot.
+     *
+     * <p>The carrier is named after the owning target's ordinal and kind, 
which is the name the delta
+     * sub-plan already generates for the same state, so no delta-side lookup 
changes.
+     */
+    private Slot materializeStateCarrier(List<NamedExpression> outputs, Slot 
stateSlot, IvmAggMeta aggMeta) {
+        IvmAggTarget owner = aggTargetOwningVisibleSlot(stateSlot, aggMeta);
+        Alias carrier = new Alias(stateSlot,
+                IvmUtil.ivmAggHiddenColumnName(owner.getOrdinal(), 
owner.getFunctionKind().name()));

Review Comment:
   [P1] Preserve physical type conversion for new aggregate carriers. For a 
legacy DATE column `d`, `SELECT k, YEAR(MIN(d)) AS y ... GROUP BY k` makes this 
carrier DATE, but `MTMVPlanUtil.generateColumns` makes its physical hidden 
column DATEV2. On refresh, `rewriteIvmHiddenOutput` replaces the binder's typed 
hidden placeholder with the bare DATE carrier while the sink still targets 
DATEV2; the BE DATEV2 writer requires `ColumnDateV2`. The same gap affects 
wrapped ARRAY_AGG/COLLECT_LIST over legacy DATE elements (ARRAY<DATE> versus 
ARRAY<DATEV2>). COMPLETE and incremental refresh can fail writing these new 
carriers. Preserve sink coercion for hidden state and cover both direct and 
nested converted types.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to