github-actions[bot] commented on code in PR #66795:
URL: https://github.com/apache/doris/pull/66795#discussion_r4422345484
##########
fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVCacheManager.java:
##########
@@ -46,21 +46,21 @@ public MTMVCacheManager() {
caches = build(Config.mtmv_cache_manage_num,
Config.expire_mtmv_cache_in_fe_second);
}
- public MTMVCache getIfPresent(long mtmvId, boolean guarded) {
- return caches.getIfPresent(new Key(mtmvId, guarded));
+ public MTMVCache getIfPresent(long mtmvId, int guardMask) {
+ return caches.getIfPresent(new Key(mtmvId, guardMask));
}
- public void put(long mtmvId, boolean guarded, MTMVCache cache) {
+ public void put(long mtmvId, int guardMask, MTMVCache cache) {
Objects.requireNonNull(cache, "mtmv cache to publish must not be
null");
synchronized (swapLock) {
- caches.put(new Key(mtmvId, guarded), cache);
+ caches.put(new Key(mtmvId, guardMask), cache);
}
}
public void invalidate(long mtmvId) {
synchronized (swapLock) {
- caches.invalidate(new Key(mtmvId, true));
- caches.invalidate(new Key(mtmvId, false));
+ // Every guard mask is an independent entry for the same MV, so
drop them all.
+ caches.asMap().keySet().removeIf(key -> key.mtmvId == mtmvId);
Review Comment:
[P2] Invalidate the four mask keys without scanning the shared cache
Every successful MTMV refresh calls this through `invalidateRewriteCache()`
while holding the MV write lock, and `swapLock` also serializes all cache puts
and config swaps. The new `removeIf` traverses every cached MV to remove at
most four entries. With N cached one-entry MVs, refreshing the fleet now
performs O(N²) key visits while unrelated publications wait; the default
capacity is 3000 and is configurable without an upper bound. Both production
writers only create masks 0, 1, 2 or 3, so directly invalidating those four
keys preserves the new semantics with constant per-MV work. This is separate
from the already-reported unreachable mask-0 prebuild cost.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/SessionVarGuardRewriter.java:
##########
@@ -139,15 +348,268 @@ 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) {
+ 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;
+ }
+
+ /**
+ * 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.
+ */
+ 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)) {
+ 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();
+ }
+
+ /**
+ * 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) {
+ // the boolean result of IS (NOT) NULL does not depend on the time
zone
+ return false;
+ }
+ 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 both its source and its result type 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);
+ boolean sourceTz = containsTimeStampTz(source.getType());
+ boolean targetTz = containsTimeStampTz(castExpr.getType());
+ if (sourceTz != targetTz) {
Review Comment:
[P1] Compare corresponding leaf types when classifying complex casts
Consider a non-key source `s STRUCT<a:TIMESTAMPTZ(6), b:TIMESTAMPTZ(6)>` and
`g STRUCT<a:TIMESTAMPTZ(6), b:STRING> GENERATED ALWAYS AS (s)`. This supported
declared-target cast has `sourceTz=true` and `targetTz=true` because field `a`
stays TIMESTAMPTZ, so the fence recurses into the safe SlotRef and accepts it.
BindSink later inserts the actual struct cast; BE preserves `a` but renders `b`
with RuntimeState's write zone. Identical input instants consequently store
different strings in UTC and +08:00 loads. The earlier scalar source/target and
missing implicit-cast issues are fixed; this mixed-field cast is a distinct
instance that defeats the new presence-only check. Compare corresponding
array/map/struct leaf types recursively and reject any sensitive leaf
conversion, with a mixed-field generated-column test.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/SessionVarGuardRewriter.java:
##########
@@ -139,15 +348,268 @@ 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) {
+ 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;
+ }
+
+ /**
+ * 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.
+ */
+ 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)) {
+ 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();
+ }
+
+ /**
+ * 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) {
+ // the boolean result of IS (NOT) NULL does not depend on the time
zone
+ return false;
Review Comment:
[P1] Inspect the operand before exempting IS NULL
A generated column such as `g BOOLEAN GENERATED ALWAYS AS ((CASE WHEN
date_trunc(ts, 'day') = ts2 THEN NULL ELSE 1 END) IS NULL)` passes the new
fence: DDL analysis preserves the CASE, translation creates
`IsNullPredicate(CaseExpr(...))`, and this branch returns false without
visiting it. For `ts = 2024-01-01 00:30Z` and `ts2 = 2024-01-01 00:00Z`, a UTC
write stores `g=true`, while a +08:00 write stores `g=false` for the same
instants. BindSink's captured-variable scope does not preserve an
expression-specific BE zone; translation unwraps the guards and DateTrunc uses
the write session. This is a different bypass from the previously reported
direct DateTrunc/cast/WHERE cases, which are now rejected. Recurse into the IS
NULL operand and add a negative generated-column test for this nullable CASE
shape.
--
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]