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


##########
fe/fe-core/src/main/java/org/apache/doris/mtmv/ivm/agg/IvmAggTarget.java:
##########
@@ -42,16 +42,23 @@ public class IvmAggTarget {
     private final Slot visibleSlot;
     // Persisted hidden state column slots. For example, an AVG target has 
hidden SUM and COUNT states.
     private final Map<IvmAggStateKey, Slot> hiddenStateSlots;
+    // Column carrying this target's own aggregate value state when the 
visible column does not
+    // survive to the MV (see 
IvmAggFunctionProcessor#visibleColumnHoldsValueState). Null means the
+    // visible column carries it: either because it reaches the MV as a 
visible column, or because
+    // this target's old value is derived from hidden state instead (AVG, 
BITMAP_UNION_COUNT,
+    // COUNT(*)). Apply reads the old value through getValueStateColumnName().
+    private final Slot valueStateSlot;
     // the expression(s) from the base scan that feed this aggregate
     // (empty for COUNT(*); may be Slot or compound Expression like v1+v2)
     private final List<Expression> exprArgs;
 
     public IvmAggTarget(int ordinal, IvmAggFunctionKind functionKind, Slot 
visibleSlot,
-            Map<IvmAggStateKey, Slot> hiddenStateSlots, List<Expression> 
exprArgs) {
+            Map<IvmAggStateKey, Slot> hiddenStateSlots, Slot valueStateSlot, 
List<Expression> exprArgs) {

Review Comment:
   [P1] Update the remaining FE test callers for the new constructor. 
IvmAggProcessorTestBase.target() and IvmAggColumnSharingTest.targetWithHidden() 
still call new IvmAggTarget(...) with five arguments, while this is now the 
only constructor and requires six. Test source compilation therefore fails 
before the new IVM tests can run. Pass the intended valueStateSlot (null where 
appropriate) at both sites, or keep a delegating overload.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/IvmNormalizeMTMV.java:
##########
@@ -1069,6 +1070,143 @@ 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;
+        for (IvmAggTarget target : aggMeta.getAggTargets()) {
+            Slot valueStateSlot = target.getValueStateSlot();
+            if (aggFunctionRegistry.visibleColumnHoldsValueState(target)) {
+                // 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 carried = materializeAggStateSlot(extendedOutputs,
+                        valueStateSlot != null ? valueStateSlot : 
target.getVisibleSlot(), aggMeta);
+                // The visible column reaching the MV is not a separate 
carrier.
+                valueStateSlot = 
carried.getExprId().equals(target.getVisibleSlot().getExprId())
+                        ? null : carried;
+            }
+            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 that carries {@code stateSlot} above this layer, 
appending a hidden pass-through
+     * alias when this layer drops it.
+     */
+    private Slot materializeAggStateSlot(List<NamedExpression> outputs, Slot 
stateSlot, IvmAggMeta aggMeta) {
+        NamedExpression projected = findProjectedKey(outputs, stateSlot);
+        if (projected != null) {
+            // Already carried above this layer: a visible column projecting 
through, a hidden state
+            // column (which always propagates), or a carrier another target 
needed for this same state.
+            // The projecting output's own slot is what holds the value, so an 
alias that passes the
+            // state through under another name rebinds the target to that 
name.
+            return projected.toSlot();
+        }
+        IvmAggTarget owner = aggTargetOwningVisibleSlot(stateSlot, aggMeta);

Review Comment:
   [P1] Preserve aggregate-state ownership across the sink's coercion projects. 
For an IVM view defined as SELECT MIN(s) AS m FROM t with s STRING, the MV 
stores its first visible column as VARCHAR(65533). On refresh, BindSink adds 
Project(SUBSTRING(m1, 1, 65533)) -> Project(m AS m1) -> Aggregate(MIN(s) AS m). 
The lower project rebinds the value state to m1; the upper project drops it, 
but aggTargetOwningVisibleSlot only recognizes the original aggregate ExprId 
and throws PLAN_REWRITE_FAILED. The view can be created but cannot COMPLETE or 
INCREMENTAL refresh. Handle the rebinding while keeping CREATE and refresh 
state layouts aligned, and cover this refresh shape.



##########
regression-test/suites/mtmv_p0/ivm/test_ivm_agg_expr_over_agg.groovy:
##########
@@ -0,0 +1,626 @@
+// 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_expr_over_agg") {
+
+    // =========================================================
+    // A scalar expression wrapped around an aggregate result, as
+    // in SELECT k, SUM(v) * 100 FROM t GROUP BY k, must stay
+    // incrementally maintainable.
+    //
+    // Apply merges the old MV state in the state domain and then
+    // re-applies the outer expression:
+    //     new.s100 = f(apply(old_mv.sum_v, delta.sum_v))
+    // so the MV must persist a column carrying SUM(v) itself. That
+    // column is the visible aggregate output when the select list
+    // projects it (SELECT SUM(v) AS s, SUM(v) * 100) and a
+    // materialized hidden column when an upper expression consumes
+    // it without projecting it (SELECT SUM(v) * 100).
+    //
+    // These cases verify both halves of the invariant:
+    //   * the hidden layout, via DESC (the dropped state column is
+    //     materialized, once per aggregate state, reusing existing
+    //     columns when they already carry it);
+    //   * the merged values, through INSERT/UPDATE/DELETE and
+    //     incremental refreshes.
+    //
+    // NOTE: set show_hidden_columns=true right before a DESC only —
+    // enabling it earlier puts the session in debug mode and blocks
+    // CREATE MATERIALIZED VIEW.
+    // =========================================================
+
+    def refreshIncremental = { mv ->
+        sql """REFRESH MATERIALIZED VIEW ${mv} INCREMENTAL"""
+        waitingMTMVTaskFinishedByMvName(mv)
+    }
+
+    sql """drop materialized view if exists test_ivm_expr_over_agg_sum;"""
+    sql """drop materialized view if exists test_ivm_expr_over_agg_cnt;"""
+    sql """drop materialized view if exists test_ivm_expr_over_agg_min;"""
+    sql """drop materialized view if exists test_ivm_expr_over_agg_max;"""
+    sql """drop materialized view if exists test_ivm_expr_over_agg_list;"""
+    sql """drop materialized view if exists test_ivm_expr_over_agg_div;"""
+    sql """drop materialized view if exists test_ivm_expr_over_agg_cast;"""
+    sql """drop materialized view if exists test_ivm_expr_over_agg_scalar;"""
+    sql """drop materialized view if exists test_ivm_expr_over_agg_sum_avg;"""
+    sql """drop materialized view if exists test_ivm_expr_over_agg_plain;"""
+    sql """drop materialized view if exists test_ivm_expr_over_agg_cnt_star;"""
+    sql """drop materialized view if exists 
test_ivm_expr_over_agg_avg_round;"""
+    sql """drop materialized view if exists test_ivm_expr_over_agg_bitmap;"""
+    sql """drop materialized view if exists test_ivm_expr_over_agg_agg_arg;"""
+    sql """drop materialized view if exists test_ivm_expr_over_agg_key_expr;"""
+    sql """drop materialized view if exists 
test_ivm_expr_over_agg_full_keys;"""
+    sql """drop table if exists test_ivm_expr_over_agg_base;"""
+
+    sql """
+        CREATE TABLE test_ivm_expr_over_agg_base (
+            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"
+        );
+    """
+
+    // =========================================================
+    // Part 1: hidden layout of the wrapped-aggregate shapes
+    // =========================================================
+
+    // SUM(v) * 100: SUM's own value is its mergeable state and the visible 
column is
+    // consumed by the outer expression, so the state is materialized as 
_0_SUM_COL__
+    // next to the hidden non-NULL count.
+    sql """
+        CREATE MATERIALIZED VIEW test_ivm_expr_over_agg_sum
+        BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL
+        DISTRIBUTED BY RANDOM BUCKETS 2
+        PROPERTIES ('replication_num' = '1')
+        AS SELECT k, SUM(v) * 100 AS s100 FROM test_ivm_expr_over_agg_base 
GROUP BY k;
+    """
+    sql """set show_hidden_columns=true"""
+    qt_sum_desc """DESC test_ivm_expr_over_agg_sum"""
+    sql """set show_hidden_columns=false"""
+
+    // COUNT(v) + 1: same for COUNT(expr), whose visible column is the count 
state.
+    sql """
+        CREATE MATERIALIZED VIEW test_ivm_expr_over_agg_cnt
+        BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL
+        DISTRIBUTED BY RANDOM BUCKETS 2
+        PROPERTIES ('replication_num' = '1')
+        AS SELECT k, COUNT(v) + 1 AS c1 FROM test_ivm_expr_over_agg_base GROUP 
BY k;
+    """
+    sql """set show_hidden_columns=true"""
+    qt_cnt_desc """DESC test_ivm_expr_over_agg_cnt"""
+    sql """set show_hidden_columns=false"""
+
+    // MIN(v) * 2 and MAX(v) + 1.
+    sql """
+        CREATE MATERIALIZED VIEW test_ivm_expr_over_agg_min
+        BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL
+        DISTRIBUTED BY RANDOM BUCKETS 2
+        PROPERTIES ('replication_num' = '1')
+        AS SELECT k, MIN(v) * 2 AS m2 FROM test_ivm_expr_over_agg_base GROUP 
BY k;
+    """
+    sql """set show_hidden_columns=true"""
+    qt_min_desc """DESC test_ivm_expr_over_agg_min"""
+    sql """set show_hidden_columns=false"""
+
+    sql """
+        CREATE MATERIALIZED VIEW test_ivm_expr_over_agg_max
+        BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL
+        DISTRIBUTED BY RANDOM BUCKETS 2
+        PROPERTIES ('replication_num' = '1')
+        AS SELECT k, MAX(v) + 1 AS m1 FROM test_ivm_expr_over_agg_base GROUP 
BY k;
+    """
+    sql """set show_hidden_columns=true"""
+    qt_max_desc """DESC test_ivm_expr_over_agg_max"""
+    sql """set show_hidden_columns=false"""
+
+    // ARRAY_SIZE(COLLECT_LIST(v)): the visible array is the whole aggregate 
state.
+    sql """
+        CREATE MATERIALIZED VIEW test_ivm_expr_over_agg_list
+        BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL
+        DISTRIBUTED BY RANDOM BUCKETS 2
+        PROPERTIES ('replication_num' = '1')
+        AS SELECT k, ARRAY_SIZE(COLLECT_LIST(v)) AS n FROM 
test_ivm_expr_over_agg_base GROUP BY k;
+    """
+    sql """set show_hidden_columns=true"""
+    qt_list_desc """DESC test_ivm_expr_over_agg_list"""
+    sql """set show_hidden_columns=false"""
+
+    // SUM(v) / COUNT(v): two states, both consumed by one expression. The 
visible COUNT
+    // column of the COUNT target is also SUM's hidden non-NULL count (column 
pool), so
+    // the shared count state is materialized exactly once.
+    sql """
+        CREATE MATERIALIZED VIEW test_ivm_expr_over_agg_div
+        BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL
+        DISTRIBUTED BY RANDOM BUCKETS 2
+        PROPERTIES ('replication_num' = '1')
+        AS SELECT k, SUM(v) / COUNT(v) AS d FROM test_ivm_expr_over_agg_base 
GROUP BY k;
+    """
+    sql """set show_hidden_columns=true"""
+    qt_div_desc """DESC test_ivm_expr_over_agg_div"""
+    sql """set show_hidden_columns=false"""
+
+    // CAST(SUM(v) AS DOUBLE) and a scalar (no GROUP BY) variant.
+    sql """
+        CREATE MATERIALIZED VIEW test_ivm_expr_over_agg_cast
+        BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL
+        DISTRIBUTED BY RANDOM BUCKETS 2
+        PROPERTIES ('replication_num' = '1')
+        AS SELECT k, CAST(SUM(v) AS DOUBLE) AS d FROM 
test_ivm_expr_over_agg_base GROUP BY k;
+    """
+    sql """set show_hidden_columns=true"""
+    qt_cast_desc """DESC test_ivm_expr_over_agg_cast"""
+    sql """set show_hidden_columns=false"""
+
+    sql """
+        CREATE MATERIALIZED VIEW test_ivm_expr_over_agg_scalar
+        BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL
+        DISTRIBUTED BY RANDOM BUCKETS 2
+        PROPERTIES ('replication_num' = '1')
+        AS SELECT SUM(v) * 100 AS s100 FROM test_ivm_expr_over_agg_base;
+    """
+    sql """set show_hidden_columns=true"""
+    qt_scalar_desc """DESC test_ivm_expr_over_agg_scalar"""
+    sql """set show_hidden_columns=false"""
+
+    // SUM(v) * 100 next to AVG(v) * 200: AVG's hidden SUM state reuses the 
visible SUM
+    // column, so it must follow that column onto the single materialized 
carrier instead
+    // of adding a second SUM column.
+    sql """
+        CREATE MATERIALIZED VIEW test_ivm_expr_over_agg_sum_avg
+        BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL
+        DISTRIBUTED BY RANDOM BUCKETS 2
+        PROPERTIES ('replication_num' = '1')
+        AS SELECT k, SUM(v) * 100 AS s100, AVG(v) * 200 AS a200 FROM 
test_ivm_expr_over_agg_base GROUP BY k;
+    """
+    sql """set show_hidden_columns=true"""
+    qt_sum_avg_desc """DESC test_ivm_expr_over_agg_sum_avg"""
+    sql """set show_hidden_columns=false"""
+
+    // =========================================================
+    // Part 2: shapes that already worked must keep their exact
+    // layout — no extra column is materialized when the visible
+    // column itself carries the state.
+    // =========================================================
+
+    // The visible SUM column is projected, so it carries the state itself.
+    sql """
+        CREATE MATERIALIZED VIEW test_ivm_expr_over_agg_plain
+        BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL
+        DISTRIBUTED BY RANDOM BUCKETS 2
+        PROPERTIES ('replication_num' = '1')
+        AS SELECT k, SUM(v) AS s, SUM(v) * 100 AS s100 FROM 
test_ivm_expr_over_agg_base GROUP BY k;
+    """
+    sql """set show_hidden_columns=true"""
+    qt_plain_desc """DESC test_ivm_expr_over_agg_plain"""
+    sql """set show_hidden_columns=false"""
+
+    // COUNT(*) reads the group count, AVG and BITMAP_UNION_COUNT derive their 
visible value
+    // from hidden state: wrapping them needs no state materialization.
+    sql """
+        CREATE MATERIALIZED VIEW test_ivm_expr_over_agg_cnt_star
+        BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL
+        DISTRIBUTED BY RANDOM BUCKETS 2
+        PROPERTIES ('replication_num' = '1')
+        AS SELECT k, COUNT(*) * 2 AS c2 FROM test_ivm_expr_over_agg_base GROUP 
BY k;
+    """
+    sql """set show_hidden_columns=true"""
+    qt_cnt_star_desc """DESC test_ivm_expr_over_agg_cnt_star"""
+    sql """set show_hidden_columns=false"""
+
+    sql """
+        CREATE MATERIALIZED VIEW test_ivm_expr_over_agg_avg_round
+        BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL
+        DISTRIBUTED BY RANDOM BUCKETS 2
+        PROPERTIES ('replication_num' = '1')
+        AS SELECT k, ROUND(AVG(v), 2) AS a FROM test_ivm_expr_over_agg_base 
GROUP BY k;
+    """
+    sql """set show_hidden_columns=true"""
+    qt_avg_round_desc """DESC test_ivm_expr_over_agg_avg_round"""
+    sql """set show_hidden_columns=false"""
+
+    sql """
+        CREATE MATERIALIZED VIEW test_ivm_expr_over_agg_bitmap
+        BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL
+        DISTRIBUTED BY RANDOM BUCKETS 2
+        PROPERTIES ('replication_num' = '1')
+        AS SELECT k, BITMAP_UNION_COUNT(TO_BITMAP(v)) + 0 AS b FROM 
test_ivm_expr_over_agg_base GROUP BY k;

Review Comment:
   [P2] Exercise a wrapped BITMAP_UNION through refresh. This bitmap case uses 
BITMAP_UNION_COUNT, whose processor reads its existing hidden bitmap state; it 
never tests the newly changed IvmAggBitmapUnionProcessor lookup through a 
materialized value carrier. Add a view such as 
BITMAP_COUNT(BITMAP_UNION(TO_BITMAP(v))), then check initial and subsequent 
incremental refreshes and COMPLETE refresh against the base query. The current 
unit test checks only the carrier's layout.



-- 
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