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


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/SessionVarGuardRewriter.java:
##########
@@ -136,14 +322,175 @@ public Plan visit(Plan plan, Void ctx) {
 
     /**
      * Check if current query session variables match MV creation session 
variables.
-     * Only compares variables that affect query results.
+     * Only compares variables that affect query results. The time zone is 
compared with its canonical
+     * identity (UTC / Etc/UTC / GMT / +00:00 are the same zone) so equivalent 
spellings do not cause a
+     * mismatch and a resulting unnecessary rewrite loss.
      */
     public static boolean checkSessionVariablesMatch(Map<String, String> 
currentSessionVars,
             Map<String, String> persistSessionVars) {
         if (persistSessionVars == null || persistSessionVars.isEmpty()) {
             // If no session variables saved, consider them matched
             return true;
         }
-        return currentSessionVars.equals(persistSessionVars);
+        for (Map.Entry<String, String> entry : persistSessionVars.entrySet()) {
+            String key = entry.getKey();
+            String persistedValue = entry.getValue();
+            String currentValue = currentSessionVars.get(key);
+            if (SessionVariable.TIME_ZONE.equals(key)) {
+                if (!timeZonesEquivalent(persistedValue, currentValue)) {
+                    return false;
+                }
+            } else if (!Objects.equals(persistedValue, currentValue)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Whether the guard rewriter must be applied to the object owning {@code 
persistSessionVars}: either
+     * some persisted affectQueryResult variable no longer matches the current 
session, or the persisted
+     * map does not carry {@code time_zone} at all. The latter covers 
pre-change metadata whose creation
+     * time zone is unknown, so time-zone sensitive expressions of such 
objects must always be guarded
+     * (a conservative compatibility fence) to avoid cross-zone rewrite of 
stale materialized values.
+     */
+    public static boolean needsSessionVarGuard(Map<String, String> 
currentSessionVars,
+            Map<String, String> persistSessionVars) {
+        return computeGuardMask(currentSessionVars, persistSessionVars) != 
GUARD_NONE;
+    }
+
+    /**
+     * Computes which session-variable dependency families differ between the 
query session and the
+     * persisted (creation) session, as a bitmask of {@link #GUARD_TIME_ZONE} 
and {@link #GUARD_OTHER}.
+     * Per-family scoping lets a time-zone-only difference keep rewriting 
expressions that depend on other
+     * variables (and vice versa).
+     */
+    public static int computeGuardMask(Map<String, String> currentSessionVars,
+            Map<String, String> persistSessionVars) {
+        if (persistSessionVars == null || persistSessionVars.isEmpty()) {
+            return GUARD_NONE;
+        }
+        int mask = GUARD_NONE;
+        // The creation zone is unknown (pre-change metadata) or differs from 
the current zone.
+        if (!persistSessionVars.containsKey(SessionVariable.TIME_ZONE)
+                || 
!timeZonesEquivalent(persistSessionVars.get(SessionVariable.TIME_ZONE),
+                        currentSessionVars.get(SessionVariable.TIME_ZONE))) {
+            mask |= GUARD_TIME_ZONE;
+        }
+        if (differsInNonTimeZoneVars(currentSessionVars, persistSessionVars)) {
+            mask |= GUARD_OTHER;
+        }
+        return mask;
+    }
+
+    /**
+     * True if a non-time_zone affectQueryResult variable differs between the 
two maps.
+     */
+    private static boolean differsInNonTimeZoneVars(Map<String, String> 
currentVars, Map<String, String> persistVars) {
+        if (currentVars == null || persistVars == null) {
+            return false;
+        }
+        for (Map.Entry<String, String> entry : persistVars.entrySet()) {

Review Comment:
   [P1] Compare both key sets when deriving GUARD_OTHER
   
   This loop ignores keys that exist only in the current map. For example, 
`enable_decimal256` existed long before it was added to the persisted 
plan-variable set, so an older nonempty view/MV map lacks that key; a new query 
with it enabled still gets no `GUARD_OTHER`, leaving decimal `Multiply`/`Sum` 
structurally eligible under incompatible result-type semantics. The old 
whole-map comparison caught this one-sided key set. Please compare the union of 
non-time-zone keys (treating a missing value as a mismatch or applying an 
explicit compatibility fence) and add a historical-map test with a current-only 
plan variable.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/SessionVarGuardRewriter.java:
##########
@@ -136,14 +322,175 @@ public Plan visit(Plan plan, Void ctx) {
 
     /**
      * Check if current query session variables match MV creation session 
variables.
-     * Only compares variables that affect query results.
+     * Only compares variables that affect query results. The time zone is 
compared with its canonical
+     * identity (UTC / Etc/UTC / GMT / +00:00 are the same zone) so equivalent 
spellings do not cause a
+     * mismatch and a resulting unnecessary rewrite loss.
      */
     public static boolean checkSessionVariablesMatch(Map<String, String> 
currentSessionVars,
             Map<String, String> persistSessionVars) {
         if (persistSessionVars == null || persistSessionVars.isEmpty()) {
             // If no session variables saved, consider them matched
             return true;
         }
-        return currentSessionVars.equals(persistSessionVars);
+        for (Map.Entry<String, String> entry : persistSessionVars.entrySet()) {
+            String key = entry.getKey();
+            String persistedValue = entry.getValue();
+            String currentValue = currentSessionVars.get(key);
+            if (SessionVariable.TIME_ZONE.equals(key)) {
+                if (!timeZonesEquivalent(persistedValue, currentValue)) {
+                    return false;
+                }
+            } else if (!Objects.equals(persistedValue, currentValue)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Whether the guard rewriter must be applied to the object owning {@code 
persistSessionVars}: either
+     * some persisted affectQueryResult variable no longer matches the current 
session, or the persisted
+     * map does not carry {@code time_zone} at all. The latter covers 
pre-change metadata whose creation
+     * time zone is unknown, so time-zone sensitive expressions of such 
objects must always be guarded
+     * (a conservative compatibility fence) to avoid cross-zone rewrite of 
stale materialized values.
+     */
+    public static boolean needsSessionVarGuard(Map<String, String> 
currentSessionVars,
+            Map<String, String> persistSessionVars) {
+        return computeGuardMask(currentSessionVars, persistSessionVars) != 
GUARD_NONE;
+    }
+
+    /**
+     * Computes which session-variable dependency families differ between the 
query session and the
+     * persisted (creation) session, as a bitmask of {@link #GUARD_TIME_ZONE} 
and {@link #GUARD_OTHER}.
+     * Per-family scoping lets a time-zone-only difference keep rewriting 
expressions that depend on other
+     * variables (and vice versa).
+     */
+    public static int computeGuardMask(Map<String, String> currentSessionVars,
+            Map<String, String> persistSessionVars) {
+        if (persistSessionVars == null || persistSessionVars.isEmpty()) {
+            return GUARD_NONE;
+        }
+        int mask = GUARD_NONE;
+        // The creation zone is unknown (pre-change metadata) or differs from 
the current zone.
+        if (!persistSessionVars.containsKey(SessionVariable.TIME_ZONE)
+                || 
!timeZonesEquivalent(persistSessionVars.get(SessionVariable.TIME_ZONE),
+                        currentSessionVars.get(SessionVariable.TIME_ZONE))) {
+            mask |= GUARD_TIME_ZONE;
+        }
+        if (differsInNonTimeZoneVars(currentSessionVars, persistSessionVars)) {
+            mask |= GUARD_OTHER;
+        }
+        return mask;
+    }
+
+    /**
+     * True if a non-time_zone affectQueryResult variable differs between the 
two maps.
+     */
+    private static boolean differsInNonTimeZoneVars(Map<String, String> 
currentVars, Map<String, String> persistVars) {
+        if (currentVars == null || persistVars == null) {
+            return false;
+        }
+        for (Map.Entry<String, String> entry : persistVars.entrySet()) {
+            String key = entry.getKey();
+            if (SessionVariable.TIME_ZONE.equals(key)) {
+                continue;
+            }
+            if (!Objects.equals(entry.getValue(), currentVars.get(key))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Whether two time-zone spellings denote the same zone. UTC / Etc/UTC / 
GMT / +00:00 are all the
+     * same instant-zone and must compare equal even though their persisted 
strings differ.
+     */
+    public static boolean timeZonesEquivalent(String tzA, String tzB) {
+        if (Objects.equals(tzA, tzB)) {
+            return true;
+        }
+        if (tzA == null || tzB == null) {
+            return false;
+        }
+        try {
+            return 
ZoneId.of(tzA).normalized().equals(ZoneId.of(tzB).normalized());
+        } catch (DateTimeException e) {
+            return false;
+        }
+    }
+
+    private static Map<String, String> 
currentAffectQueryResultInPlanVariables() {
+        ConnectContext ctx = ConnectContext.get();
+        if (ctx == null || ctx.getSessionVariable() == null) {
+            return ImmutableMap.of();
+        }
+        return ctx.getSessionVariable().getAffectQueryResultInPlanVariables();
+    }
+
+    /**
+     * Whether a stored column expression (a generated column or a synchronous 
materialized view column
+     * definition) is time-zone sensitive: its materialized value depends on 
the session time zone because
+     * a TIMESTAMPTZ operand (possibly nested in a complex type) is converted 
into a zone-dependent
+     * representation. Such expressions cannot be kept consistent across 
sessions: BE evaluates them in the
+     * write/load session time zone, so a value created in one zone and 
written in another would silently
+     * materialize the wrong value. They are therefore rejected at creation 
time.
+     */
+    public static boolean isTimeZoneSensitiveStoredExpr(Expr expr) {
+        if (expr == null) {
+            return false;
+        }
+        return isTimeZoneSensitiveLegacy(expr);
+    }
+
+    private static boolean isTimeZoneSensitiveLegacy(Expr expr) {
+        if (expr instanceof SlotRef || expr instanceof LiteralExpr) {
+            return false;
+        }
+        if (expr instanceof IsNullPredicate) {
+            // the boolean result of IS (NOT) NULL does not depend on the time 
zone
+            return false;
+        }
+        if (expr instanceof FunctionCallExpr && ((FunctionCallExpr) 
expr).isAggregateFunction()) {
+            // aggregates preserve the UTC instant; only a time-zone 
conversion inside their argument
+            // (e.g. MAX(date_trunc(ts, 'day'))) makes the value zone dependent
+            for (Expr child : expr.getChildren()) {
+                if (isTimeZoneSensitiveLegacy(child)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        for (Expr child : expr.getChildren()) {

Review Comment:
   [P1] Classify the stored operation in both directions
   
   This new legacy-Expr fence only scans child types. It therefore accepts 
`CAST(varchar_col AS TIMESTAMPTZ)`: the sole child is VARCHAR, while the 
zone-sensitive target/result type is never inspected, so generated/sync-MV 
values can still interpret the same offset-free string differently in each 
write session. In the other direction it rejects zone-invariant predicates such 
as `ts1 = ts2` merely because their children are TIMESTAMPTZ, breaking 
otherwise valid generated-column and sync-MV DDL. Please classify the actual 
operation plus source and result types, and add cross-zone tests for casts into 
TIMESTAMPTZ together with positive comparison tests.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateTableInfo.java:
##########
@@ -1141,6 +1142,11 @@ private void generatedColumnCheck(ConnectContext ctx) {
             TypeCoercionUtils.checkCanCastTo(expr.getDataType(), 
column.getType());
             ExpressionToExpr translator = new ExpressionToExpr(i, 
translateMap);
             Expr e = expr.accept(translator, planTranslatorContext);
+            if (SessionVarGuardRewriter.isTimeZoneSensitiveStoredExpr(e)) {

Review Comment:
   [P1] Validate the generated column after applying its declared type
   
   This check sees only the user expression, not the implicit cast to 
`column.getType()`. Thus `rendered STRING GENERATED ALWAYS AS (ts)` for a 
TIMESTAMPTZ slot passes as a safe `SlotRef`; `BindSink` later adds the actual 
TIMESTAMPTZ-to-STRING cast in `getOutputProjectByCoercion`, after leaving the 
persisted-session scope, so UTC and +08:00 loads store different renderings. 
The reverse implicit cast from an offset-free string slot into a TIMESTAMPTZ 
generated column has the same gap. Please validate the expression after 
applying the declared target coercion (or pass that target into the classifier) 
and add a cross-zone test for this direct-slot shape.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/RefreshMTMVInfo.java:
##########
@@ -97,17 +97,36 @@ private void checkPartitionExist(MTMV mtmv) throws 
org.apache.doris.common.Analy
                         "The partition method of this asynchronous 
materialized view "
                                 + "does not support refreshing by partition");
             }
-            List<AllPartitionDesc> partitionDescs = 
MTMVPartitionUtil.getPartitionDescsByRelatedTable(
-                    mtmv.getTableProperty().getProperties(), 
mtmv.getMvPartitionInfo(), mtmv.getMvProperties(),
-                    mtmv.getPartitionColumns());
-            Set<String> shouldExistPartitionNames = 
Sets.newHashSetWithExpectedSize(partitionDescs.size());
-            partitionDescs.stream().forEach(desc -> {
-                shouldExistPartitionNames.add(((SinglePartitionDesc) 
desc).getPartitionName());
-            });
+            // First validate against the real physical partition names 
already stored in the MTMV metadata.
+            // SHOW PARTITIONS returns these names, and MVs created before 
partition name generation was made
+            // deterministic may carry a historical time suffix, so 
regenerating names here could produce a
+            // different string than the stored one and wrongly reject a valid 
refresh request.
+            Set<String> existPartitionNames = mtmv.getPartitionNames();
+            // Secondly validate against the partition names that would be 
generated (and aligned) from the
+            // related base table partition descs, so that refreshing a 
not-yet-created partition is allowed.
+            Set<PartitionKeyDesc> relatedPartitionDescs = 
MTMVPartitionUtil.generateRelatedPartitionDescs(
+                    mtmv.getMvPartitionInfo(), mtmv.getMvProperties(), 
mtmv.getPartitionColumns(),
+                    Maps.newHashMap()).keySet();
+            Set<String> shouldExistPartitionNames = 
relatedPartitionDescs.stream()
+                    .map(MTMVPartitionUtil::generatePartitionName)
+                    .collect(Collectors.toSet());
             for (String partition : partitions) {
-                if (!shouldExistPartitionNames.contains(partition)) {
+                if (shouldExistPartitionNames.contains(partition)) {

Review Comment:
   [P1] Resolve regenerated names to an existing legacy physical partition
   
   For an MTMV created before this change, a long descriptor `D` is stored 
under the old time-suffixed physical name, while `generatePartitionName(D)` now 
returns the SHA-based name. This branch accepts that new name, but 
`alignMvPartition` sees that `D` is already represented and adds nothing; 
`calculateNeedRefreshPartitions` then drops the nonphysical name and the task 
completes as `NOT_REFRESH`/success without refreshing anything. Please remap 
the generated alias to the stored physical name (or accept it only when 
alignment will actually create it), fail if an explicit target still has no 
physical partition after alignment, and cover a legacy time-suffixed partition 
in the regression.



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