This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new f2e81cf3deb branch-4.1: [fix](analyze) Preserve variant subfields in
view definitions to fix select view result wrong when view select has variant
field #62907 (#63151)
f2e81cf3deb is described below
commit f2e81cf3deb7bb275e3fc088c067ddbcb8a57e74
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu May 21 10:21:54 2026 +0800
branch-4.1: [fix](analyze) Preserve variant subfields in view definitions
to fix select view result wrong when view select has variant field #62907
(#63151)
Cherry-picked from #62907
Co-authored-by: seawinde <[email protected]>
---
.../nereids/rules/analysis/ExpressionAnalyzer.java | 52 ++++---
.../test_create_view_variant_nested_field.groovy | 157 +++++++++++++++++++++
2 files changed, 191 insertions(+), 18 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
index d11a5c89bb9..e92c4b62546 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
@@ -1079,83 +1079,82 @@ public class ExpressionAnalyzer extends
SubExprAnalyzer<ExpressionRewriteContext
private List<? extends Expression> bindExpressionByCatalogDbTableColumn(
UnboundSlot unboundSlot, List<String> nameParts,
Optional<Pair<Integer, Integer>> idxInSql, Scope scope) {
- List<Slot> slots = addSqlIndexInfo(bindSingleSlotByCatalog(
- nameParts.get(0), nameParts.get(1), nameParts.get(2),
nameParts.get(3), scope), idxInSql);
+ List<Slot> slots = bindSingleSlotByCatalog(
+ nameParts.get(0), nameParts.get(1), nameParts.get(2),
nameParts.get(3), scope);
if (slots.isEmpty()) {
return bindExpressionByDbTableColumn(unboundSlot, nameParts,
idxInSql, scope);
} else if (slots.size() > 1) {
- return slots;
+ return addSqlIndexInfo(slots, idxInSql);
}
if (nameParts.size() == 4) {
- return slots;
+ return addSqlIndexInfo(slots, idxInSql);
}
Optional<Expression> expression = bindNestedFields(
unboundSlot, slots.get(0), nameParts.subList(4,
nameParts.size())
);
if (!expression.isPresent()) {
- return slots;
+ return addSqlIndexInfo(slots, idxInSql);
}
return ImmutableList.of(expression.get());
}
private List<? extends Expression> bindExpressionByDbTableColumn(
UnboundSlot unboundSlot, List<String> nameParts,
Optional<Pair<Integer, Integer>> idxInSql, Scope scope) {
- List<Slot> slots = addSqlIndexInfo(
- bindSingleSlotByDb(nameParts.get(0), nameParts.get(1),
nameParts.get(2), scope), idxInSql);
+ List<Slot> slots = bindSingleSlotByDb(nameParts.get(0),
nameParts.get(1), nameParts.get(2), scope);
if (slots.isEmpty()) {
return bindExpressionByTableColumn(unboundSlot, nameParts,
idxInSql, scope);
} else if (slots.size() > 1) {
- return slots;
+ return addSqlIndexInfo(slots, idxInSql);
}
if (nameParts.size() == 3) {
- return slots;
+ return addSqlIndexInfo(slots, idxInSql);
}
Optional<Expression> expression = bindNestedFields(
unboundSlot, slots.get(0), nameParts.subList(3,
nameParts.size())
);
if (!expression.isPresent()) {
- return slots;
+ return addSqlIndexInfo(slots, idxInSql);
}
return ImmutableList.of(expression.get());
}
private List<? extends Expression> bindExpressionByTableColumn(
UnboundSlot unboundSlot, List<String> nameParts,
Optional<Pair<Integer, Integer>> idxInSql, Scope scope) {
- List<Slot> slots =
addSqlIndexInfo(bindSingleSlotByTable(nameParts.get(0), nameParts.get(1),
scope), idxInSql);
+ List<Slot> slots = bindSingleSlotByTable(nameParts.get(0),
nameParts.get(1), scope);
if (slots.isEmpty()) {
return bindExpressionByColumn(unboundSlot, nameParts, idxInSql,
scope);
} else if (slots.size() > 1) {
- return slots;
+ return addSqlIndexInfo(slots, idxInSql);
}
if (nameParts.size() == 2) {
- return slots;
+ return addSqlIndexInfo(slots, idxInSql);
}
Optional<Expression> expression = bindNestedFields(
unboundSlot, slots.get(0), nameParts.subList(2,
nameParts.size())
);
if (!expression.isPresent()) {
- return slots;
+ return addSqlIndexInfo(slots, idxInSql);
}
return ImmutableList.of(expression.get());
}
private List<? extends Expression> bindExpressionByColumn(
UnboundSlot unboundSlot, List<String> nameParts,
Optional<Pair<Integer, Integer>> idxInSql, Scope scope) {
- List<Slot> slots =
addSqlIndexInfo(bindSingleSlotByName(nameParts.get(0), scope), idxInSql);
+ List<Slot> slots = bindSingleSlotByName(nameParts.get(0), scope);
if (slots.size() != 1) {
- return slots;
+ return addSqlIndexInfo(slots, idxInSql);
}
if (nameParts.size() == 1) {
- return slots;
+ return addSqlIndexInfo(slots, idxInSql);
}
Optional<Expression> expression = bindNestedFields(
unboundSlot, slots.get(0), nameParts.subList(1,
nameParts.size())
);
if (!expression.isPresent()) {
- return slots;
+ return addSqlIndexInfo(slots, idxInSql);
}
return ImmutableList.of(expression.get());
}
@@ -1183,9 +1182,26 @@ public class ExpressionAnalyzer extends
SubExprAnalyzer<ExpressionRewriteContext
}
throw new AnalysisException("No such field '" + fieldName + "' in
'" + lastFieldName + "'");
}
+ addNestedFieldsSqlIndexInfo(unboundSlot, slot, fieldNames);
return Optional.of(new Alias(expression, unboundSlot.getName(),
slot.getQualifier()));
}
+ private void addNestedFieldsSqlIndexInfo(UnboundSlot unboundSlot, Slot
slot, List<String> fieldNames) {
+ Optional<Pair<Integer, Integer>> indexInSql =
unboundSlot.getIndexInSqlString();
+ if (!indexInSql.isPresent()) {
+ return;
+ }
+ ConnectContext connectContext = ConnectContext.get();
+ if (connectContext == null || connectContext.getStatementContext() ==
null) {
+ return;
+ }
+ List<String> fullName = new ArrayList<>(slot.getQualifier());
+ fullName.add(slot.getName());
+ fullName.addAll(fieldNames);
+
connectContext.getStatementContext().addIndexInSqlToString(indexInSql.get(),
+ Utils.qualifiedNameWithBackquote(fullName));
+ }
+
public static boolean sameTableName(String boundSlot, String unboundSlot,
int lowerCaseTableNames) {
if (lowerCaseTableNames == 0) {
return boundSlot.equals(unboundSlot);
diff --git
a/regression-test/suites/ddl_p0/create_view_nereids/test_create_view_variant_nested_field.groovy
b/regression-test/suites/ddl_p0/create_view_nereids/test_create_view_variant_nested_field.groovy
new file mode 100644
index 00000000000..75cf62cb349
--- /dev/null
+++
b/regression-test/suites/ddl_p0/create_view_nereids/test_create_view_variant_nested_field.groovy
@@ -0,0 +1,157 @@
+// 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_create_view_variant_nested_field") {
+ sql "SET enable_nereids_planner=true;"
+ sql "SET enable_fallback_to_original_planner=false;"
+
+ String tableName = "test_create_view_variant_nested_field_events"
+ String videoMetaTableName =
"test_create_view_variant_nested_field_video_meta"
+ String viewName = "test_create_view_variant_nested_field_view"
+ String cteViewName = "test_create_view_variant_nested_field_cte_view"
+ String bracketViewName =
"test_create_view_variant_nested_field_bracket_view"
+
+ sql "DROP VIEW IF EXISTS ${viewName}"
+ sql "DROP VIEW IF EXISTS ${cteViewName}"
+ sql "DROP VIEW IF EXISTS ${bracketViewName}"
+ sql "DROP TABLE IF EXISTS ${videoMetaTableName}"
+ sql "DROP TABLE IF EXISTS ${tableName}"
+
+ sql """
+ CREATE TABLE ${tableName} (
+ event_key LARGEINT NOT NULL,
+ event_at DATETIME NOT NULL,
+ event_type VARCHAR(20) NOT NULL,
+ event_value VARIANT<'video_id':largeint, 'duration':bigint>,
+ user_connect_info VARIANT<'user_client':text>
+ )
+ DUPLICATE KEY(event_key)
+ DISTRIBUTED BY HASH(event_key) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "storage_format" = "V3"
+ )
+ """
+
+ sql """
+ CREATE TABLE ${videoMetaTableName} (
+ fid LARGEINT NOT NULL,
+ effective_duration_s BIGINT NULL
+ )
+ DUPLICATE KEY(fid)
+ DISTRIBUTED BY HASH(fid) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1"
+ )
+ """
+
+ sql """
+ INSERT INTO ${tableName} VALUES
+ (1, '2026-04-21 00:00:00', 'watch_time',
'{"video_id":100,"duration":15000}', '{"user_client":"ios"}')
+ """
+ sql "INSERT INTO ${videoMetaTableName} VALUES (100, 60)"
+
+ sql """
+ CREATE VIEW ${viewName} AS
+ SELECT
+ CAST(e.event_value.video_id AS LARGEINT) AS video_id,
+ TRY_CAST(e.event_value.duration AS INT) AS duration_ms,
+ CAST(e.user_connect_info.user_client AS VARCHAR) AS client
+ FROM ${tableName} e
+ """
+
+ def viewCount = sql """
+ SELECT COUNT(*) FROM ${viewName}
+ WHERE video_id = 100 AND duration_ms > 0 AND client = 'ios'
+ """
+ assertEquals("1", viewCount[0][0].toString())
+
+ def createViewSql = sql "SHOW CREATE VIEW ${viewName}"
+ assertTrue(createViewSql[0][1].contains("`event_value`.`video_id`"))
+ assertTrue(createViewSql[0][1].contains("`event_value`.`duration`"))
+
assertTrue(createViewSql[0][1].contains("`user_connect_info`.`user_client`"))
+ assertFalse(createViewSql[0][1].contains("`event_value` AS LARGEINT"))
+ assertFalse(createViewSql[0][1].contains("`event_value` AS INT"))
+ assertFalse(createViewSql[0][1].contains("`user_connect_info` AS VARCHAR"))
+
+ sql """
+ CREATE VIEW ${cteViewName} AS
+ WITH extracted AS (
+ SELECT
+ DATE_TRUNC(event_at, 'DAY') AS event_day,
+ CAST(event_value.video_id AS LARGEINT) AS video_id,
+ TRY_CAST(event_value.duration AS INT) AS duration_ms,
+ CAST(user_connect_info.user_client AS VARCHAR) AS client
+ FROM ${tableName}
+ WHERE event_type = 'watch_time'
+ ), final AS (
+ SELECT
+ event_day,
+ video_id,
+ SUM(duration_ms) AS total_duration,
+ MIN_BY(client, event_day) AS client
+ FROM extracted e
+ JOIN ${videoMetaTableName} v ON e.video_id = v.fid
+ WHERE duration_ms > 0 AND client IS NOT NULL
+ GROUP BY 1, 2
+ )
+ SELECT * FROM final
+ """
+
+ def cteViewCount = sql "SELECT COUNT(*) FROM ${cteViewName}"
+ assertEquals("1", cteViewCount[0][0].toString())
+
+ def cteFilteredViewCount = sql """
+ SELECT COUNT(*) FROM ${cteViewName}
+ WHERE event_day >= '2026-04-20'
+ """
+ assertEquals("1", cteFilteredViewCount[0][0].toString())
+
+ sql """
+ ALTER VIEW ${viewName} AS
+ SELECT
+ CAST(e.event_value.video_id AS LARGEINT) AS video_id,
+ TRY_CAST(e.event_value.duration AS INT) AS duration_ms,
+ CAST(e.user_connect_info.user_client AS VARCHAR) AS client
+ FROM ${tableName} e
+ """
+
+ def alterViewCount = sql """
+ SELECT COUNT(*) FROM ${viewName}
+ WHERE video_id = 100 AND duration_ms > 0 AND client = 'ios'
+ """
+ assertEquals("1", alterViewCount[0][0].toString())
+
+ sql """
+ CREATE VIEW ${bracketViewName} AS
+ SELECT CAST(event_value['video_id'] AS LARGEINT) AS video_id
+ FROM ${tableName}
+ """
+
+ def bracketViewCount = sql "SELECT COUNT(*) FROM ${bracketViewName} WHERE
video_id = 100"
+ assertEquals("1", bracketViewCount[0][0].toString())
+
+ def bracketCreateViewSql = sql "SHOW CREATE VIEW ${bracketViewName}"
+
assertTrue(bracketCreateViewSql[0][1].contains("`event_value`['video_id']"))
+
assertFalse(bracketCreateViewSql[0][1].contains("['video_id']['video_id']"))
+
+ sql "DROP VIEW IF EXISTS ${viewName}"
+ sql "DROP VIEW IF EXISTS ${cteViewName}"
+ sql "DROP VIEW IF EXISTS ${bracketViewName}"
+ sql "DROP TABLE IF EXISTS ${videoMetaTableName}"
+ sql "DROP TABLE IF EXISTS ${tableName}"
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]