github-actions[bot] commented on code in PR #66795:
URL: https://github.com/apache/doris/pull/66795#discussion_r4423973001
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateMTMVInfo.java:
##########
@@ -177,8 +180,33 @@ public void analyze(ConnectContext ctx) throws Exception {
enableIvm = isExplicitIncremental();
analyzeQuery(ctx);
}
+ // For a TIMESTAMPTZ-partitioned MTMV (date_trunc on a timestamptz
column) the derived partition
+ // boundaries are UTC-aligned (see MTMVPartitionExprDateTrunc).
Persist time_zone=UTC as the effective
+ // creation zone so the background refresh evaluates the partition key
in UTC (matching the UTC
+ // boundaries) and the rewrite guard compares queries against the same
UTC semantics. Without this,
+ // content expressions would be materialized in a zone different from
the persisted one, and queries
+ // in the persisted (non-UTC) zone could rewrite against a cache built
with different semantics.
+ // This must run after EITHER analysis arm has finalized
mvPartitionInfo: for REFRESH AUTO the IVM
+ // probe may fall back to the regular path (an aliased date_trunc
partition is an EXPR partition,
+ // which the IVM probe rejects), and the persisted session map would
otherwise keep the non-UTC DDL
+ // zone while the physical boundaries stay UTC-aligned, so the refresh
computes a day boundary that
+ // falls into no MV partition (or into the preceding UTC one). The
detection must use the analyzed
+ // base partition column, not the MTMV output columns: an aliased
definition projects only
+ // date_trunc(ts, 'day') AS day_ts, so neither the source column name
nor its type is visible there.
+ if (MTMVPlanUtil.usesUtcTimeStampTzPartition(mvPartitionInfo)) {
+ // Persisting UTC as the effective creation zone only works if
every FE can interpret it: an older
+ // FE does not know time_zone as a plan variable and would refresh
this MV in its own default zone,
+ // routing rows outside the UTC-aligned partitions. Refuse the DDL
instead of writing metadata that
+ // a still-running old FE would silently misinterpret.
+ SessionVarGuardRewriter.checkClusterSupportsPlanTimeZoneFence();
+ Map<String, String> effectiveSessionVariables =
Maps.newHashMap(sessionVariables);
+ effectiveSessionVariables.put(SessionVariable.TIME_ZONE,
TimeUtils.getUTCTimeZone().getID());
+ this.sessionVariables = effectiveSessionVariables;
Review Comment:
[P1] Derive the stored schema under the effective UTC session
`analyzeQuery(ctx)` has already derived columns from the optimized physical
plan before this map replacement. In a +08:00 creation session, an MTMV with
`PARTITION BY(d)` can have this reduced definition:
```text
Project(date_trunc(ts,'day') AS d,
IF(CAST('2024-01-01 00:00:00' AS TIMESTAMPTZ(6))
= CAST('2023-12-31 16:00:00+00:00' AS TIMESTAMPTZ(6)), 1, NULL)
AS c)
Scan(t)
```
Creation folds `c` to 1, and `generateColumns` persists `c` as NOT NULL.
This block changes only the session map; refresh reparses the original SQL in
UTC and folds `c` to NULL. The ordinary-MV schema check compares types only, so
it does not repair the nullable mismatch, and the sink rejects/filters those
rows. Reanalyze the definition and derive its schema in the effective UTC
session before persisting it, restoring the caller's session afterward. Add a
non-UTC creation/refresh regression with a zone-dependent folded nullable
output.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateMTMVInfo.java:
##########
@@ -177,8 +180,33 @@ public void analyze(ConnectContext ctx) throws Exception {
enableIvm = isExplicitIncremental();
analyzeQuery(ctx);
}
+ // For a TIMESTAMPTZ-partitioned MTMV (date_trunc on a timestamptz
column) the derived partition
+ // boundaries are UTC-aligned (see MTMVPartitionExprDateTrunc).
Persist time_zone=UTC as the effective
+ // creation zone so the background refresh evaluates the partition key
in UTC (matching the UTC
+ // boundaries) and the rewrite guard compares queries against the same
UTC semantics. Without this,
+ // content expressions would be materialized in a zone different from
the persisted one, and queries
+ // in the persisted (non-UTC) zone could rewrite against a cache built
with different semantics.
+ // This must run after EITHER analysis arm has finalized
mvPartitionInfo: for REFRESH AUTO the IVM
+ // probe may fall back to the regular path (an aliased date_trunc
partition is an EXPR partition,
+ // which the IVM probe rejects), and the persisted session map would
otherwise keep the non-UTC DDL
+ // zone while the physical boundaries stay UTC-aligned, so the refresh
computes a day boundary that
+ // falls into no MV partition (or into the preceding UTC one). The
detection must use the analyzed
+ // base partition column, not the MTMV output columns: an aliased
definition projects only
+ // date_trunc(ts, 'day') AS day_ts, so neither the source column name
nor its type is visible there.
+ if (MTMVPlanUtil.usesUtcTimeStampTzPartition(mvPartitionInfo)) {
+ // Persisting UTC as the effective creation zone only works if
every FE can interpret it: an older
+ // FE does not know time_zone as a plan variable and would refresh
this MV in its own default zone,
+ // routing rows outside the UTC-aligned partitions. Refuse the DDL
instead of writing metadata that
+ // a still-running old FE would silently misinterpret.
+ SessionVarGuardRewriter.checkClusterSupportsPlanTimeZoneFence();
+ Map<String, String> effectiveSessionVariables =
Maps.newHashMap(sessionVariables);
+ effectiveSessionVariables.put(SessionVariable.TIME_ZONE,
TimeUtils.getUTCTimeZone().getID());
Review Comment:
[P1] Reconcile stored time-zone hints before persisting UTC
An EXPR-partition definition such as `PARTITION BY(d) AS SELECT /*+
SET_VAR(time_zone='+08:00') */ date_trunc(ts,'day') AS d, COUNT(*) AS c FROM t
GROUP BY date_trunc(ts,'day')` retains its hint in `querySql`. Both
`UpdateMvByPartitionCommand.from` and `MTMVCache.from` parse that SQL after the
persisted UTC context is initialized, so the hint sets the actual refresh/cache
session back to +08:00. For a UTC Jan-1 partition containing `ts=2024-01-01
20:30Z`, refresh stores `d=2024-01-01 16:00Z`, which is inside that partition
and can commit successfully. An unhinted UTC query then selects mask 0 from
this persisted UTC map; the hint node is gone and the unguarded DateTrunc/COUNT
aggregate structurally matches, so the rewrite can return 16:00Z instead of the
direct UTC boundary 00:00Z. Reject or reconcile conflicting time_zone hints
across refresh and cache construction before claiming UTC semantics, and cover
this successful late-day refresh/rewrite case. This does not depend on the
separate schema-nullability issue.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/SessionVarGuardRewriter.java:
##########
@@ -139,15 +358,429 @@ 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;
+ }
+
+ /**
+ * Whether the persisted session-variable map identifies the creation time
zone.
+ *
+ * <p>A missing, empty or time_zone-less map means the creation zone is
unknown: the object was created
+ * before time_zone was persisted (old FE), or its metadata was replayed
by a new FE without the key.
+ * Such an object is the old-metadata -> new-FE upgrade direction of the
metadata compatibility fence:
+ * its zone-sensitive expressions are guarded forever and it must be
recreated on the new FE before it
+ * can participate in a cross-zone rewrite. The reverse direction (new
metadata read by an old FE)
+ * cannot be repaired from the new code: an old read-serving FE neither
registers this key nor
+ * classifies TIMESTAMPTZ functions, so it keeps building its
nominal-mismatch cache without the time
+ * zone guard. New-metadata objects therefore have to be rebuilt/recreated
before an old FE serves
+ * their reads, or the old FE must not be used for reads until it is
upgraded.
+ */
+ public static boolean isCreationSessionKnown(Map<String, String>
persistSessionVars) {
+ return persistSessionVars != null && !persistSessionVars.isEmpty()
+ && persistSessionVars.containsKey(SessionVariable.TIME_ZONE);
+ }
+
+ /**
+ * 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).
+ *
+ * <p>An unknown creation session (see {@link #isCreationSessionKnown})
always selects the time-zone
+ * family, never an unconditional match.
+ */
+ public static int computeGuardMask(Map<String, String> currentSessionVars,
+ Map<String, String> persistSessionVars) {
+ if (planSessionVarFenceVersion(persistSessionVars)
+ > SessionVariable.CURRENT_PLAN_SESSION_VAR_FENCE_VERSION) {
+ // The persisted plan-variable set was written by a newer FE
(metadata produced before a
+ // downgrade). This FE cannot interpret it, so it must not build a
rewrite cache that any
+ // session could consume: select both guard families.
+ return GUARD_TIME_ZONE | GUARD_OTHER;
+ }
+ int mask = GUARD_NONE;
+ // The creation zone is unknown (pre-change metadata, including an
empty map) or differs from the
+ // current zone.
+ if (!isCreationSessionKnown(persistSessionVars)
+ ||
!timeZonesEquivalent(persistSessionVars.get(SessionVariable.TIME_ZONE),
+ currentSessionVars == null ? null :
currentSessionVars.get(SessionVariable.TIME_ZONE))) {
+ mask |= GUARD_TIME_ZONE;
+ }
+ if (persistSessionVars != null && !persistSessionVars.isEmpty()
+ && differsInNonTimeZoneVars(currentSessionVars,
persistSessionVars)) {
+ mask |= GUARD_OTHER;
+ }
+ return mask;
+ }
+
+ /**
+ * The plan-variable fence version stamped into a persisted
session-variable map, 0 when the map predates
+ * the stamp. A value this FE cannot parse is reported as newer than
anything it knows, so the read fence
+ * fails closed instead of trusting unknown metadata.
+ */
+ public static int planSessionVarFenceVersion(Map<String, String>
persistSessionVars) {
+ if (persistSessionVars == null) {
+ return 0;
+ }
+ String version =
persistSessionVars.get(SessionVariable.PLAN_SESSION_VAR_FENCE_VERSION);
+ if (version == null) {
+ return 0;
+ }
+ try {
+ return Integer.parseInt(version.trim());
+ } catch (NumberFormatException e) {
+ return Integer.MAX_VALUE;
+ }
+ }
+
+ /**
+ * Guard mask for the rewrite cache of a synchronous materialized view.
+ *
+ * <p>Sync MVs keep one deliberate exception: a difference confined to the
strict-table sql_mode bits
+ * does not change the semantics of the rewritten expressions, so it must
not disable rewrite (see
+ * {@link #checkSessionVariablesMatchForSyncMv}). The exception is only
consulted when the mask selects
+ * the non-time-zone family on its own, and it can therefore never
suppress the time-zone fence: a
+ * legacy sync MV whose persisted map is empty or time_zone-less is still
guarded for the time zone
+ * (the full mask is returned), so an empty map is never an unconditional
match and the cache cannot be
+ * rewritten in a different zone.
+ */
+ public static int computeSyncMvGuardMask(Map<String, String>
currentSessionVars,
+ Map<String, String> persistSessionVars) {
+ int guardMask = computeGuardMask(currentSessionVars,
persistSessionVars);
+ if (guardMask == GUARD_OTHER
+ && checkSessionVariablesMatchForSyncMv(currentSessionVars,
persistSessionVars)) {
+ return GUARD_NONE;
+ }
+ return guardMask;
+ }
+
+ /**
+ * True if a non-time_zone affectQueryResult variable differs between the
two maps.
+ * The union of both key sets is compared: a variable that exists in only
one map is a mismatch.
+ * A current-only variable (e.g. enable_decimal256, added to the persisted
plan-variable set after this
+ * view/MV was created) means the object was materialized with the other
(default) value, so the current
+ * non-default setting must disable rewrite; a persist-only variable
likewise differs because its value
+ * in the other map is unknown. This restores the conservative whole-map
comparison semantics.
+ * time_zone and the fence version stamp are excluded: the zone difference
is scoped to its own family,
+ * and the stamp only identifies the persisted plan-variable set (it is
validated by
+ * {@link #planSessionVarFenceVersion}), it is not a setting that changes
an expression's value.
+ */
+ private static boolean differsInNonTimeZoneVars(Map<String, String>
currentVars, Map<String, String> persistVars) {
+ if (currentVars == null || persistVars == null) {
+ return false;
+ }
+ for (String key : Sets.union(currentVars.keySet(),
persistVars.keySet())) {
+ if (SessionVariable.TIME_ZONE.equals(key)
+ ||
SessionVariable.PLAN_SESSION_VAR_FENCE_VERSION.equals(key)) {
+ continue;
+ }
+ if (!Objects.equals(persistVars.get(key), 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();
+ }
+
+ /** The keys that describe a result-affecting setting, i.e. the map
without the fence version stamp. */
+ private static Set<String> planVariableKeySet(Map<String, String>
sessionVars) {
+ Set<String> keys = Sets.newHashSet(sessionVars.keySet());
+ keys.remove(SessionVariable.PLAN_SESSION_VAR_FENCE_VERSION);
+ return keys;
+ }
+
+ /**
+ * Fails unless every alive frontend can interpret the persisted
plan-variable set of this FE.
+ *
+ * <p>The {@link SessionVariable#PLAN_SESSION_VAR_FENCE_VERSION} stamp
lets a new FE refuse metadata
+ * written by a newer one, but the opposite direction - an <em>old</em> FE
serving new metadata - can only
+ * be prevented where the metadata is created: an old FE neither registers
{@code time_zone} as a plan
+ * variable nor classifies TIMESTAMPTZ expressions, so it would refresh
and rewrite such an object without
+ * the time-zone guard. Any DDL that writes semantics only a fenced FE can
honour therefore has to check
+ * that no older FE is still alive in the cluster (a rolling upgrade must
finish first).
+ *
+ * <p>Development builds report {@code 0.0.0}, which carries no release
ordering, so the check is a no-op
+ * there; an unparsable version string is also treated as "cannot prove it
is older" instead of blocking
+ * DDL, because vendor version schemes are not comparable with this FE's
release.
+ */
+ public static void checkClusterSupportsPlanTimeZoneFence() throws
AnalysisException {
+ Env env = Env.getCurrentEnv();
+ if (env == null) {
+ return;
+ }
+ List<Frontend> frontends = env.getFrontends(null);
+ String selfHost = env.getSelfNode() == null ? null :
env.getSelfNode().getHost();
+ DigitalVersion selfVersion = null;
+ for (Frontend frontend : frontends) {
+ if (selfHost != null && selfHost.equals(frontend.getHost())) {
+ selfVersion = parseReleaseVersion(frontend.getVersion());
+ }
+ }
+ if (selfVersion == null) {
+ return;
+ }
+ for (Frontend frontend : frontends) {
+ if (!frontend.isAlive()) {
+ continue;
+ }
+ DigitalVersion version =
parseReleaseVersion(frontend.getVersion());
+ if (version != null && version.before(selfVersion)) {
+ throw new AnalysisException("frontend " + frontend.getHost() +
" runs " + frontend.getVersion()
+ + ", which predates this frontend's " +
selfVersion.major + "." + selfVersion.minor
+ + "." + selfVersion.revision + ". It cannot interpret
the persisted plan variables of an "
+ + "object whose semantics depend on the session time
zone, so the object cannot be "
+ + "created safely now. Upgrade all frontends first and
retry.");
+ }
+ }
+ }
+
+ private static DigitalVersion parseReleaseVersion(String version) {
+ if (version == null) {
+ return null;
+ }
+ Matcher matcher = DORIS_RELEASE_VERSION.matcher(version);
+ if (!matcher.find()) {
+ return null;
+ }
+ try {
+ DigitalVersion parsed =
DigitalVersion.fromString(matcher.group(0));
+ // 0.0.0 is the development build marker, which carries no release
ordering.
+ return parsed.id == 0 ? null : parsed;
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Whether a stored column expression (a generated column or a synchronous
materialized view column
+ * definition, including the separately stored WHERE clause) is time-zone
sensitive: its materialized
+ * value depends on the session time zone because a TIMESTAMPTZ operand
(possibly nested in a complex
+ * type) is rendered or interpreted in that zone.
+ *
+ * <p>Such expressions cannot be kept consistent: they are evaluated by BE
in the write/load session
+ * time zone, and the persisted session variables only drive FE
binding/cache decisions, so an
+ * expression created in one zone and evaluated in another materializes a
different value (the
+ * {@code SessionVarGuardExpr} wrapper is dropped by {@code
MergeGuardExpr} and by expression to
+ * thrift translation before execution). They are therefore rejected at
creation time instead.
+ *
+ * <p>Every rendering of an instant into another representation is
rejected, including STRING: the
+ * string embeds the write-session offset, so the same instant is stored
as different strings by
+ * different write sessions and any later grouping/comparison observes
that difference. Comparing two
+ * TIMESTAMPTZ instants and copying an instant verbatim are zone invariant
and stay allowed; the
+ * comparison operands are still classified individually because they may
contain a sensitive cast.
+ */
+ public static boolean isTimeZoneSensitiveStoredExpr(Expr expr) {
+ if (expr == null) {
+ return false;
+ }
+ if (expr instanceof SlotRef || expr instanceof LiteralExpr) {
+ // A bare slot/literal only carries a value; whether that value is
zone dependent is decided by
+ // the operation that renders or interprets it (a TIMESTAMPTZ slot
holds a zone-independent
+ // instant, an offset-free string slot holds whatever the storage
session wrote).
+ return false;
+ }
+ if (expr instanceof IsNullPredicate) {
+ // IS (NOT) NULL itself is zone invariant, but it only reads the
null-ness of its operand: a
+ // nullable CASE built from a zone-sensitive expression (e.g.
+ // (CASE WHEN date_trunc(ts, 'day') = ts2 THEN NULL ELSE 1 END) IS
NULL) therefore materializes
+ // different booleans in different write sessions, so the operand
must be classified as well.
+ return isTimeZoneSensitiveStoredExpr(expr.getChild(0));
+ }
+ if (expr instanceof CastExpr) {
+ return isTimeZoneSensitiveCast((CastExpr) expr);
+ }
+ if (expr instanceof BinaryPredicate &&
isInstantComparison((BinaryPredicate) expr)) {
+ // Comparing two TIMESTAMPTZ instants is zone invariant regardless
of the operator, but the
+ // operands may themselves be zone-sensitive expressions (e.g.
date_trunc(ts, 'day') = ts2), so
+ // recurse. A comparison against an offset-free literal is NOT
exempt: the literal is
+ // interpreted in the write/load session zone and falls through to
the check below.
+ return isTimeZoneSensitiveStoredExpr(expr.getChild(0))
+ || isTimeZoneSensitiveStoredExpr(expr.getChild(1));
+ }
+ 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 (isTimeZoneSensitiveStoredExpr(child)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ for (Expr child : expr.getChildren()) {
+ if (containsTimeStampTz(child.getType())) {
+ return true;
+ }
+ if (isTimeZoneSensitiveStoredExpr(child)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Classify a cast by comparing its source and result types instead of
only its children: casting INTO
+ * TIMESTAMPTZ interprets an offset-free source representation
(string/datetime) in the session zone,
+ * and casting a TIMESTAMPTZ instant OUT of TIMESTAMPTZ renders it in the
session zone - including into
+ * STRING, whose rendering embeds the write-session offset and is
therefore not canonical.
+ * TIMESTAMPTZ <-> TIMESTAMPTZ is instant preserving and only
recurses into the source.
+ */
+ private static boolean isTimeZoneSensitiveCast(CastExpr castExpr) {
+ Expr source = castExpr.getChild(0);
+ if (isTimeZoneSensitiveTypeConversion(source.getType(),
castExpr.getType())) {
+ return true;
+ }
+ return isTimeZoneSensitiveStoredExpr(source);
+ }
+
+ /**
+ * Whether converting {@code from} into {@code to} renders or interprets a
TIMESTAMPTZ instant in the
+ * session zone.
+ *
+ * <p>Array, map and struct conversions are compared leaf by leaf. A
presence-only check is not enough:
+ * casting {@code STRUCT<a:TIMESTAMPTZ, b:TIMESTAMPTZ>} to
+ * {@code STRUCT<a:TIMESTAMPTZ, b:STRING>} keeps a TIMESTAMPTZ field
in both types while the
+ * {@code b} leaf is rendered in the write session zone, so the same
instant would be stored with
+ * different strings by different write sessions.
+ */
+ private static boolean
isTimeZoneSensitiveTypeConversion(org.apache.doris.catalog.Type from,
+ org.apache.doris.catalog.Type to) {
+ if (from.isArrayType() && to.isArrayType()) {
+ return
isTimeZoneSensitiveTypeConversion(((org.apache.doris.catalog.ArrayType)
from).getItemType(),
+ ((org.apache.doris.catalog.ArrayType) to).getItemType());
+ }
+ if (from.isMapType() && to.isMapType()) {
+ org.apache.doris.catalog.MapType fromMap =
(org.apache.doris.catalog.MapType) from;
+ org.apache.doris.catalog.MapType toMap =
(org.apache.doris.catalog.MapType) to;
+ return isTimeZoneSensitiveTypeConversion(fromMap.getKeyType(),
toMap.getKeyType())
+ ||
isTimeZoneSensitiveTypeConversion(fromMap.getValueType(), toMap.getValueType());
+ }
+ if (from.isStructType() && to.isStructType()) {
+ List<org.apache.doris.catalog.StructField> fromFields =
+ ((org.apache.doris.catalog.StructType) from).getFields();
+ List<org.apache.doris.catalog.StructField> toFields =
+ ((org.apache.doris.catalog.StructType) to).getFields();
+ // A struct cast requires the same fields in the same order, so
any other shape is not a plain
+ // struct conversion and falls through to the presence-based check
below.
+ if (fromFields.size() == toFields.size() &&
sameStructFieldNames(fromFields, toFields)) {
Review Comment:
[P2] Compare renamed struct fields by position
Equal-arity STRUCT casts are positional and permit field renames:
`CheckCast` compares corresponding field types, and the existing
struct_cast_rename_pruning regression covers this behavior. For non-key columns
`s STRUCT<a:TIMESTAMPTZ(6)>` and `g STRUCT<b:TIMESTAMPTZ(6)> GENERATED ALWAYS
AS (s)`, the new declared-target validation reaches this check. The names
differ, so recursion is skipped and the fallback returns sensitive merely
because both types contain TIMESTAMPTZ. CREATE TABLE is therefore rejected even
though the cast only relabels an unchanged instant and never uses the session
zone. Compare corresponding leaf conversions by position regardless of field
names, retaining rejection of actual TIMESTAMPTZ-to-STRING conversions, and add
a positive renamed-struct generated-column case.
--
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]