Skip to content

Commit d95f70d

Browse files
authored
branch-4.0:[fix](analyze) Preserve variant subfields in view definitions to fix select view result wrong when view select has variant field (#62907) (#63390)
pr: #62907 commitId: 446a342
1 parent 9610c7e commit d95f70d

2 files changed

Lines changed: 191 additions & 18 deletions

File tree

fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,83 +1030,82 @@ public List<Expression> bindSlotByScope(UnboundSlot unboundSlot, Scope scope) {
10301030

10311031
private List<? extends Expression> bindExpressionByCatalogDbTableColumn(
10321032
UnboundSlot unboundSlot, List<String> nameParts, Optional<Pair<Integer, Integer>> idxInSql, Scope scope) {
1033-
List<Slot> slots = addSqlIndexInfo(bindSingleSlotByCatalog(
1034-
nameParts.get(0), nameParts.get(1), nameParts.get(2), nameParts.get(3), scope), idxInSql);
1033+
List<Slot> slots = bindSingleSlotByCatalog(
1034+
nameParts.get(0), nameParts.get(1), nameParts.get(2), nameParts.get(3), scope);
10351035
if (slots.isEmpty()) {
10361036
return bindExpressionByDbTableColumn(unboundSlot, nameParts, idxInSql, scope);
10371037
} else if (slots.size() > 1) {
1038-
return slots;
1038+
return addSqlIndexInfo(slots, idxInSql);
10391039
}
10401040
if (nameParts.size() == 4) {
1041-
return slots;
1041+
return addSqlIndexInfo(slots, idxInSql);
10421042
}
10431043

10441044
Optional<Expression> expression = bindNestedFields(
10451045
unboundSlot, slots.get(0), nameParts.subList(4, nameParts.size())
10461046
);
10471047
if (!expression.isPresent()) {
1048-
return slots;
1048+
return addSqlIndexInfo(slots, idxInSql);
10491049
}
10501050
return ImmutableList.of(expression.get());
10511051
}
10521052

10531053
private List<? extends Expression> bindExpressionByDbTableColumn(
10541054
UnboundSlot unboundSlot, List<String> nameParts, Optional<Pair<Integer, Integer>> idxInSql, Scope scope) {
1055-
List<Slot> slots = addSqlIndexInfo(
1056-
bindSingleSlotByDb(nameParts.get(0), nameParts.get(1), nameParts.get(2), scope), idxInSql);
1055+
List<Slot> slots = bindSingleSlotByDb(nameParts.get(0), nameParts.get(1), nameParts.get(2), scope);
10571056
if (slots.isEmpty()) {
10581057
return bindExpressionByTableColumn(unboundSlot, nameParts, idxInSql, scope);
10591058
} else if (slots.size() > 1) {
1060-
return slots;
1059+
return addSqlIndexInfo(slots, idxInSql);
10611060
}
10621061
if (nameParts.size() == 3) {
1063-
return slots;
1062+
return addSqlIndexInfo(slots, idxInSql);
10641063
}
10651064

10661065
Optional<Expression> expression = bindNestedFields(
10671066
unboundSlot, slots.get(0), nameParts.subList(3, nameParts.size())
10681067
);
10691068
if (!expression.isPresent()) {
1070-
return slots;
1069+
return addSqlIndexInfo(slots, idxInSql);
10711070
}
10721071
return ImmutableList.of(expression.get());
10731072
}
10741073

10751074
private List<? extends Expression> bindExpressionByTableColumn(
10761075
UnboundSlot unboundSlot, List<String> nameParts, Optional<Pair<Integer, Integer>> idxInSql, Scope scope) {
1077-
List<Slot> slots = addSqlIndexInfo(bindSingleSlotByTable(nameParts.get(0), nameParts.get(1), scope), idxInSql);
1076+
List<Slot> slots = bindSingleSlotByTable(nameParts.get(0), nameParts.get(1), scope);
10781077
if (slots.isEmpty()) {
10791078
return bindExpressionByColumn(unboundSlot, nameParts, idxInSql, scope);
10801079
} else if (slots.size() > 1) {
1081-
return slots;
1080+
return addSqlIndexInfo(slots, idxInSql);
10821081
}
10831082
if (nameParts.size() == 2) {
1084-
return slots;
1083+
return addSqlIndexInfo(slots, idxInSql);
10851084
}
10861085

10871086
Optional<Expression> expression = bindNestedFields(
10881087
unboundSlot, slots.get(0), nameParts.subList(2, nameParts.size())
10891088
);
10901089
if (!expression.isPresent()) {
1091-
return slots;
1090+
return addSqlIndexInfo(slots, idxInSql);
10921091
}
10931092
return ImmutableList.of(expression.get());
10941093
}
10951094

10961095
private List<? extends Expression> bindExpressionByColumn(
10971096
UnboundSlot unboundSlot, List<String> nameParts, Optional<Pair<Integer, Integer>> idxInSql, Scope scope) {
1098-
List<Slot> slots = addSqlIndexInfo(bindSingleSlotByName(nameParts.get(0), scope), idxInSql);
1097+
List<Slot> slots = bindSingleSlotByName(nameParts.get(0), scope);
10991098
if (slots.size() != 1) {
1100-
return slots;
1099+
return addSqlIndexInfo(slots, idxInSql);
11011100
}
11021101
if (nameParts.size() == 1) {
1103-
return slots;
1102+
return addSqlIndexInfo(slots, idxInSql);
11041103
}
11051104
Optional<Expression> expression = bindNestedFields(
11061105
unboundSlot, slots.get(0), nameParts.subList(1, nameParts.size())
11071106
);
11081107
if (!expression.isPresent()) {
1109-
return slots;
1108+
return addSqlIndexInfo(slots, idxInSql);
11101109
}
11111110
return ImmutableList.of(expression.get());
11121111
}
@@ -1134,9 +1133,26 @@ private Optional<Expression> bindNestedFields(UnboundSlot unboundSlot, Slot slot
11341133
}
11351134
throw new AnalysisException("No such field '" + fieldName + "' in '" + lastFieldName + "'");
11361135
}
1136+
addNestedFieldsSqlIndexInfo(unboundSlot, slot, fieldNames);
11371137
return Optional.of(new Alias(expression, unboundSlot.getName(), slot.getQualifier()));
11381138
}
11391139

1140+
private void addNestedFieldsSqlIndexInfo(UnboundSlot unboundSlot, Slot slot, List<String> fieldNames) {
1141+
Optional<Pair<Integer, Integer>> indexInSql = unboundSlot.getIndexInSqlString();
1142+
if (!indexInSql.isPresent()) {
1143+
return;
1144+
}
1145+
ConnectContext connectContext = ConnectContext.get();
1146+
if (connectContext == null || connectContext.getStatementContext() == null) {
1147+
return;
1148+
}
1149+
List<String> fullName = new ArrayList<>(slot.getQualifier());
1150+
fullName.add(slot.getName());
1151+
fullName.addAll(fieldNames);
1152+
connectContext.getStatementContext().addIndexInSqlToString(indexInSql.get(),
1153+
Utils.qualifiedNameWithBackquote(fullName));
1154+
}
1155+
11401156
public static boolean sameTableName(String boundSlot, String unboundSlot) {
11411157
if (GlobalVariable.lowerCaseTableNames != 1) {
11421158
return boundSlot.equals(unboundSlot);
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
suite("test_create_view_variant_nested_field") {
19+
sql "SET enable_nereids_planner=true;"
20+
sql "SET enable_fallback_to_original_planner=false;"
21+
22+
String tableName = "test_create_view_variant_nested_field_events"
23+
String videoMetaTableName = "test_create_view_variant_nested_field_video_meta"
24+
String viewName = "test_create_view_variant_nested_field_view"
25+
String cteViewName = "test_create_view_variant_nested_field_cte_view"
26+
String bracketViewName = "test_create_view_variant_nested_field_bracket_view"
27+
28+
sql "DROP VIEW IF EXISTS ${viewName}"
29+
sql "DROP VIEW IF EXISTS ${cteViewName}"
30+
sql "DROP VIEW IF EXISTS ${bracketViewName}"
31+
sql "DROP TABLE IF EXISTS ${videoMetaTableName}"
32+
sql "DROP TABLE IF EXISTS ${tableName}"
33+
34+
sql """
35+
CREATE TABLE ${tableName} (
36+
event_key LARGEINT NOT NULL,
37+
event_at DATETIME NOT NULL,
38+
event_type VARCHAR(20) NOT NULL,
39+
event_value VARIANT<'video_id':largeint, 'duration':bigint>,
40+
user_connect_info VARIANT<'user_client':text>
41+
)
42+
DUPLICATE KEY(event_key)
43+
DISTRIBUTED BY HASH(event_key) BUCKETS 1
44+
PROPERTIES (
45+
"replication_num" = "1",
46+
"storage_format" = "V3"
47+
)
48+
"""
49+
50+
sql """
51+
CREATE TABLE ${videoMetaTableName} (
52+
fid LARGEINT NOT NULL,
53+
effective_duration_s BIGINT NULL
54+
)
55+
DUPLICATE KEY(fid)
56+
DISTRIBUTED BY HASH(fid) BUCKETS 1
57+
PROPERTIES (
58+
"replication_num" = "1"
59+
)
60+
"""
61+
62+
sql """
63+
INSERT INTO ${tableName} VALUES
64+
(1, '2026-04-21 00:00:00', 'watch_time', '{"video_id":100,"duration":15000}', '{"user_client":"ios"}')
65+
"""
66+
sql "INSERT INTO ${videoMetaTableName} VALUES (100, 60)"
67+
68+
sql """
69+
CREATE VIEW ${viewName} AS
70+
SELECT
71+
CAST(e.event_value.video_id AS LARGEINT) AS video_id,
72+
TRY_CAST(e.event_value.duration AS INT) AS duration_ms,
73+
CAST(e.user_connect_info.user_client AS VARCHAR) AS client
74+
FROM ${tableName} e
75+
"""
76+
77+
def viewCount = sql """
78+
SELECT COUNT(*) FROM ${viewName}
79+
WHERE video_id = 100 AND duration_ms > 0 AND client = 'ios'
80+
"""
81+
assertEquals("1", viewCount[0][0].toString())
82+
83+
def createViewSql = sql "SHOW CREATE VIEW ${viewName}"
84+
assertTrue(createViewSql[0][1].contains("`event_value`.`video_id`"))
85+
assertTrue(createViewSql[0][1].contains("`event_value`.`duration`"))
86+
assertTrue(createViewSql[0][1].contains("`user_connect_info`.`user_client`"))
87+
assertFalse(createViewSql[0][1].contains("`event_value` AS LARGEINT"))
88+
assertFalse(createViewSql[0][1].contains("`event_value` AS INT"))
89+
assertFalse(createViewSql[0][1].contains("`user_connect_info` AS VARCHAR"))
90+
91+
sql """
92+
CREATE VIEW ${cteViewName} AS
93+
WITH extracted AS (
94+
SELECT
95+
DATE_TRUNC(event_at, 'DAY') AS event_day,
96+
CAST(event_value.video_id AS LARGEINT) AS video_id,
97+
TRY_CAST(event_value.duration AS INT) AS duration_ms,
98+
CAST(user_connect_info.user_client AS VARCHAR) AS client
99+
FROM ${tableName}
100+
WHERE event_type = 'watch_time'
101+
), final AS (
102+
SELECT
103+
event_day,
104+
video_id,
105+
SUM(duration_ms) AS total_duration,
106+
MIN_BY(client, event_day) AS client
107+
FROM extracted e
108+
JOIN ${videoMetaTableName} v ON e.video_id = v.fid
109+
WHERE duration_ms > 0 AND client IS NOT NULL
110+
GROUP BY 1, 2
111+
)
112+
SELECT * FROM final
113+
"""
114+
115+
def cteViewCount = sql "SELECT COUNT(*) FROM ${cteViewName}"
116+
assertEquals("1", cteViewCount[0][0].toString())
117+
118+
def cteFilteredViewCount = sql """
119+
SELECT COUNT(*) FROM ${cteViewName}
120+
WHERE event_day >= '2026-04-20'
121+
"""
122+
assertEquals("1", cteFilteredViewCount[0][0].toString())
123+
124+
sql """
125+
ALTER VIEW ${viewName} AS
126+
SELECT
127+
CAST(e.event_value.video_id AS LARGEINT) AS video_id,
128+
TRY_CAST(e.event_value.duration AS INT) AS duration_ms,
129+
CAST(e.user_connect_info.user_client AS VARCHAR) AS client
130+
FROM ${tableName} e
131+
"""
132+
133+
def alterViewCount = sql """
134+
SELECT COUNT(*) FROM ${viewName}
135+
WHERE video_id = 100 AND duration_ms > 0 AND client = 'ios'
136+
"""
137+
assertEquals("1", alterViewCount[0][0].toString())
138+
139+
sql """
140+
CREATE VIEW ${bracketViewName} AS
141+
SELECT CAST(event_value['video_id'] AS LARGEINT) AS video_id
142+
FROM ${tableName}
143+
"""
144+
145+
def bracketViewCount = sql "SELECT COUNT(*) FROM ${bracketViewName} WHERE video_id = 100"
146+
assertEquals("1", bracketViewCount[0][0].toString())
147+
148+
def bracketCreateViewSql = sql "SHOW CREATE VIEW ${bracketViewName}"
149+
assertTrue(bracketCreateViewSql[0][1].contains("`event_value`['video_id']"))
150+
assertFalse(bracketCreateViewSql[0][1].contains("['video_id']['video_id']"))
151+
152+
sql "DROP VIEW IF EXISTS ${viewName}"
153+
sql "DROP VIEW IF EXISTS ${cteViewName}"
154+
sql "DROP VIEW IF EXISTS ${bracketViewName}"
155+
sql "DROP TABLE IF EXISTS ${videoMetaTableName}"
156+
sql "DROP TABLE IF EXISTS ${tableName}"
157+
}

0 commit comments

Comments
 (0)