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


##########
fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVPlanUtil.java:
##########
@@ -123,13 +124,33 @@ public static ConnectContext createMTMVContext(MTMV mtmv, 
List<RuleType> disable
         if (workloadGroup.isPresent()) {
             ctx.getSessionVariable().setWorkloadGroup(workloadGroup.get());
         }
+        // For a TIMESTAMPTZ-partitioned MTMV the partition boundaries are 
derived in UTC
+        // (see MTMVPartitionExprDateTrunc: TIMESTAMPTZ bounds are truncated 
on their UTC wall-clock).
+        // The partition expression must therefore be evaluated in UTC during 
refresh, regardless of the
+        // creation / DDL session zone, otherwise the generated partition key 
falls outside every MV
+        // partition ("no partition for this tuple") and the refresh fails 
with an empty MV.
+        if (mtmvUsesTimeStampTzPartition(mtmv)) {
+            
ctx.getSessionVariable().setTimeZone(TimeUtils.getUTCTimeZone().getID());

Review Comment:
   [P1] Preserve the MTMV definition's captured time zone here. This context is 
used for both refresh execution and rewrite-cache construction, so changing the 
whole context to UTC also changes unrelated select/filter/group expressions.
   
   For example, an MTMV created in +08:00 can partition by the raw TIMESTAMPTZ 
slot and project `CAST(date_trunc(ts, 'day') AS STRING)`; a 2024-01-01 20:30Z 
row is stored as January 1 here instead of the January 2 defined by the 
creation session. The persisted session-variable map still contains +08:00, so 
a +08:00 query gets guard mask 0 and may rewrite against this UTC-built cache. 
Normalize only the partition-routing value/boundary that requires UTC (or 
persist and compare the effective semantics consistently), and cover direct 
contents plus same-zone/cross-zone rewrite behavior.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/SessionVarGuardRewriter.java:
##########
@@ -71,40 +127,85 @@ private Plan rewritePlanNode(Plan plan) {
         return plan;
     }
 
-    private static class ReplaceRule implements ExpressionPatternRuleFactory {
+    /**
+     * Applies {@link AddSessionVarGuardRewriter} to the whole expression 
tree, so that non-Alias
+     * expressions (e.g. filter predicates, join conjuncts) are guarded as 
well as alias children.
+     */
+    private static class AddGuardExpressionRewriteRule implements 
ExpressionRewriteRule<ExpressionRewriteContext> {
         private final AddSessionVarGuardRewriter addGuardRewriter;
 
-        private ReplaceRule(AddSessionVarGuardRewriter guard) {
-            this.addGuardRewriter = guard;
+        private AddGuardExpressionRewriteRule(AddSessionVarGuardRewriter 
addGuardRewriter) {
+            this.addGuardRewriter = addGuardRewriter;
         }
 
         @Override
-        public List<ExpressionPatternMatcher<? extends Expression>> 
buildRules() {
-            return ImmutableList.of(
-                    matchesType(Alias.class).thenApply(ctx -> {
-                        Alias alias = ctx.expr;
-                        Expression aliasChild = 
alias.child().accept(addGuardRewriter, Boolean.FALSE);
-                        return 
alias.withChildren(ImmutableList.of(aliasChild));
-                    }).toRule(ExpressionRuleType.ADD_SESSION_VAR_GUARD)
-            );
+        public Expression rewrite(Expression expr, ExpressionRewriteContext 
ctx) {
+            return expr.accept(addGuardRewriter, Boolean.FALSE);
         }
     }
 
-    /** This ensures that all expressions implementing NeedSessionVarGuard are
-     * wrapped in a SessionVarGuardExpr layer.
-     * e.g. (a+b)*c -> guard(guard(a+b)*c)
-     * */
+    /**
+     * Wraps expressions whose value depends on session variables (or on the 
session time zone) in a
+     * {@link SessionVarGuardExpr} when the relevant session variables differ 
from the ones persisted on
+     * the object (view / materialized view / generated column) being 
processed.
+     */
     public static class AddSessionVarGuardRewriter extends 
DefaultExpressionRewriter<Boolean> {
         private final Map<String, String> sessionVar;
+        // Whether the time-zone family (time-zone sensitive expressions) must 
be guarded: the creation
+        // time zone differs from the current one, or the persisted map does 
not carry time_zone at all
+        // (pre-time_zone metadata), so the creation zone is unknown and must 
be treated as different.
+        private final boolean timeZoneDiffersOrUnknown;
+        // Whether the "other" guard family (NeedSessionVarGuard expressions, 
e.g. decimal256 dependent)
+        // must be guarded: some affectQueryResult session variable other than 
time_zone differs.
+        private final boolean otherSessionVarsDiffer;
 
         public AddSessionVarGuardRewriter(Map<String, String> var) {
+            this(var, currentAffectQueryResultInPlanVariables());

Review Comment:
   [P1] Do not derive this constructor's guard decision from the current 
thread-local session.
   
   Its only production caller constructs it inside 
`AutoCloseSessionVariable(indexMeta.getSessionVariables())`, so the current map 
already equals `var` and no `NeedSessionVarGuard` node is wrapped. The sync-MV 
WHERE expression is translated only after that scope restores the load session: 
`DECIMAL(30,0) * DECIMAL(30,0)` can be analyzed as `DECIMAL(60,0)` with 
persisted `enable_decimal256=true` but recomputed as `DECIMAL(38,0)` while 
translating a load with the setting false, changing overflow and predicate 
membership. Preserve this overload's unconditional behavior or pass the 
pre-scope load variables explicitly.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/SessionVarGuardRewriter.java:
##########
@@ -121,6 +222,59 @@ public Expression 
visitSessionVarGuardExpr(SessionVarGuardExpr expr, Boolean con
             }
             return expr;
         }
+
+        private boolean needsSessionVarGuard(Expression expr) {
+            if (expr instanceof NeedSessionVarGuard) {
+                return otherSessionVarsDiffer;
+            }
+            return timeZoneDiffersOrUnknown && isTimeZoneSensitive(expr);
+        }
+
+        /**
+         * An expression is time-zone sensitive when its value is a 
session-time-zone dependent rendering of
+         * a TIMESTAMPTZ value (stored as UTC), e.g. date_trunc/cast/floor on 
a timestamptz column, or a
+         * string conversion of a TIMESTAMPTZ nested in a complex type 
(ARRAY&lt;timestamptz&gt;, MAP, STRUCT).
+         * Zone-invariant operations - plain slot/literal passthroughs, named 
expressions (their children are
+         * guarded individually), aggregate functions such as COUNT/MIN/MAX 
(they preserve the UTC instant)
+         * and IS (NOT) NULL checks - must NOT be guarded so that safe 
rewrites keep working.
+         */
+        private static boolean isTimeZoneSensitive(Expression expr) {
+            if (expr instanceof Slot || expr instanceof Literal || expr 
instanceof NamedExpression
+                    || expr instanceof AggregateFunction || expr instanceof 
IsNull
+                    || (expr instanceof Not && expr.child(0) instanceof 
IsNull)) {
+                return false;
+            }
+            try {
+                return containsTimeStampTz(expr);

Review Comment:
   [P1] Do not classify subtype-constrained structural expressions as wrappable 
values.
   
   With a cross-zone view containing `row_number() OVER (ORDER BY ts)`, this 
returns true for `OrderExpression(ts)`, the visitor replaces it with 
`SessionVarGuardExpr`, and `WindowExpression.withChildren` then casts that 
child back to `OrderExpression` and throws `ClassCastException`. The same 
problem occurs for `explode(ARRAY<TIMESTAMPTZ>)` because 
`GenerateExpressionRewrite` casts the rewritten root to `Function`. Guard the 
value-producing children without replacing structural roots, and add whole-plan 
tests for both shapes.



##########
fe/fe-core/src/main/java/org/apache/doris/alter/MaterializedViewHandler.java:
##########
@@ -555,6 +556,21 @@ private List<Column> 
checkAndPrepareMaterializedView(CreateMaterializedViewComma
         List<MVColumnItem> mvColumnItemList = 
createMvCommand.getMVColumnItemList();
         List<Column> newMVColumns = Lists.newArrayList();
 
+        // A synchronous materialized view column that converts a TIMESTAMPTZ 
value into a time-zone
+        // dependent representation (date_trunc/cast/floor on a timestamptz 
column, ...) cannot be kept
+        // consistent: BE computes such columns in the write/load session time 
zone, so data loaded in a
+        // different zone than the one used to build the MV would silently 
materialize different values.
+        // Reject them at creation instead of allowing silently wrong 
materialization.
+        for (MVColumnItem mvColumnItem : mvColumnItemList) {

Review Comment:
   [P1] Apply this rejection to the separately stored sync-MV WHERE expression 
too.
   
   `PlanValidator` records the filter as `whereClauseItem`, so it is not in 
`getMVColumnItemList()`, and `BindSink` later rebuilds that predicate for 
writes. A UTC-created MV whose selected expressions are safe but whose WHERE 
compares `date_trunc(ts, 'day')` can therefore accept a boundary row under UTC 
and reject the same row under +08:00, corrupting index membership. Validate 
`getWhereClauseItem().getDefineExpr()` here and add a WHERE-only negative 
creation test.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/MTMV.java:
##########
@@ -259,10 +262,12 @@ public boolean addTaskResult(MTMVTask task, MTMVRelation 
relation,
                 this.relation = relation;
                 if (needUpdateCache) {
                     if (cacheGeneration == rewriteCacheGeneration) {
-                        // Initialize cacheWithGuard, cacheWithoutGuard will 
be lazily generated when needed
-                        this.cacheWithGuard = mtmvCacheWithGuard;
-                        // Clear the other cache to ensure consistency
-                        this.cacheWithoutGuard = mtmvCacheWithoutGuard;
+                        // Replace all caches with the freshly generated 
unguarded one; the guarded caches
+                        // will be lazily generated when needed. On replay no 
cache is generated (null).
+                        rewriteCaches.clear();

Review Comment:
   [P1] Advance `rewriteCacheGeneration` when a successful refresh replaces 
this cache set.
   
   A mask-1 builder can expand an old base-view definition (for example, `t 
WHERE a > 0`) and pause before publication. `ALTER VIEW` can change it to `a > 
10`; `processBaseViewChange` does not bump this generation; the new-version 
refresh materializes `a > 10`, clears the map, and inserts only mask 0. The old 
builder then sees the same generation and publishes its `a > 0` plan as mask 1, 
so a cross-zone query matching the old predicate can rewrite to newly refreshed 
data with different semantics. Add a latch test spanning the view change and 
`addTaskResult`.



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