be/src/exec/scan/access_path_parser.cpp
Line | Count | Source |
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 | | #include "exec/scan/access_path_parser.h" |
19 | | |
20 | | #include <fmt/format.h> |
21 | | |
22 | | #include <algorithm> |
23 | | #include <charconv> |
24 | | #include <map> |
25 | | #include <string> |
26 | | #include <string_view> |
27 | | #include <utility> |
28 | | |
29 | | #include "common/cast_set.h" |
30 | | #include "common/consts.h" |
31 | | #include "core/assert_cast.h" |
32 | | #include "core/data_type/data_type.h" |
33 | | #include "core/data_type/data_type_array.h" |
34 | | #include "core/data_type/data_type_map.h" |
35 | | #include "core/data_type/data_type_nullable.h" |
36 | | #include "core/data_type/data_type_struct.h" |
37 | | #include "runtime/descriptors.h" |
38 | | #include "util/string_util.h" |
39 | | |
40 | | namespace doris { |
41 | | namespace { |
42 | | |
43 | 297k | bool is_scanner_materialized_virtual_column(const std::string& column_name) { |
44 | 297k | return column_name == BeConsts::ICEBERG_ROWID_COL; |
45 | 297k | } |
46 | | |
47 | 55.7k | bool parse_non_negative_int(std::string_view value, int32_t* result) { |
48 | 55.7k | DORIS_CHECK(result != nullptr); |
49 | 55.7k | int32_t parsed = -1; |
50 | 55.7k | const auto* begin = value.data(); |
51 | 55.7k | const auto* end = begin + value.size(); |
52 | 55.7k | const auto [ptr, ec] = std::from_chars(begin, end, parsed); |
53 | 55.7k | if (ec != std::errc() || ptr != end || parsed < 0) { |
54 | 26.4k | return false; |
55 | 26.4k | } |
56 | 29.2k | *result = parsed; |
57 | 29.2k | return true; |
58 | 55.7k | } |
59 | | |
60 | 2 | std::string access_path_to_string(const std::vector<std::string>& path) { |
61 | 2 | return fmt::format("{}", fmt::join(path, ".")); |
62 | 2 | } |
63 | | |
64 | | format::ColumnDefinition* find_or_add_child(format::ColumnDefinition* parent, int32_t id, |
65 | 204k | std::string name, DataTypePtr type) { |
66 | 204k | DORIS_CHECK(parent != nullptr); |
67 | 204k | for (auto& child : parent->children) { |
68 | 111k | if ((child.has_identifier_field_id() && child.get_identifier_field_id() == id) || |
69 | 111k | child.name == name) { |
70 | 0 | return &child; |
71 | 0 | } |
72 | 111k | } |
73 | 204k | parent->children.push_back({ |
74 | 204k | .identifier = Field::create_field<TYPE_INT>(id), |
75 | 204k | .name = std::move(name), |
76 | 204k | .type = std::move(type), |
77 | 204k | .children = {}, |
78 | 204k | .default_expr = nullptr, |
79 | 204k | .is_partition_key = false, |
80 | 204k | }); |
81 | 204k | return &parent->children.back(); |
82 | 204k | } |
83 | | |
84 | | void inherit_schema_metadata(format::ColumnDefinition* column, |
85 | 204k | const format::ColumnDefinition* schema_column) { |
86 | 204k | if (column == nullptr || schema_column == nullptr) { |
87 | 132k | return; |
88 | 132k | } |
89 | 72.4k | column->name_mapping = schema_column->name_mapping; |
90 | | // The presence bit is part of the mapping contract: an explicit empty mapping must remain |
91 | | // authoritative after access-path pruning instead of enabling current-name fallback. |
92 | 72.4k | column->has_name_mapping = schema_column->has_name_mapping; |
93 | 72.4k | column->initial_default_value = schema_column->initial_default_value; |
94 | 72.4k | column->initial_default_value_is_base64 = schema_column->initial_default_value_is_base64; |
95 | 72.4k | column->is_optional = schema_column->is_optional; |
96 | 72.4k | column->default_expr = schema_column->default_expr; |
97 | 72.4k | } |
98 | | |
99 | | const format::ColumnDefinition* find_schema_child_by_path( |
100 | | const format::ColumnDefinition* schema_column, const std::string& child_path, |
101 | 61.9k | bool prefer_exact_name_match) { |
102 | 61.9k | if (schema_column == nullptr) { |
103 | 31.2k | return nullptr; |
104 | 31.2k | } |
105 | 30.7k | int32_t parsed_field_id = -1; |
106 | 30.7k | if (parse_non_negative_int(child_path, &parsed_field_id)) { |
107 | 4.32k | const auto child_it = std::ranges::find_if( |
108 | 8.38k | schema_column->children, [&](const format::ColumnDefinition& child) { |
109 | 8.38k | return child.has_identifier_field_id() && |
110 | 8.38k | child.get_identifier_field_id() == parsed_field_id; |
111 | 8.38k | }); |
112 | 4.32k | return child_it == schema_column->children.end() ? nullptr : &*child_it; |
113 | 4.32k | } |
114 | 26.4k | if (!prefer_exact_name_match) { |
115 | 14.9k | const auto child_it = std::ranges::find_if(schema_column->children, [&](const auto& child) { |
116 | 14.9k | if (to_lower(child.name) == to_lower(child_path)) { |
117 | 7.32k | return true; |
118 | 7.32k | } |
119 | 7.62k | return std::ranges::any_of(child.name_mapping, [&](const std::string& alias) { |
120 | 1 | return to_lower(alias) == to_lower(child_path); |
121 | 1 | }); |
122 | 14.9k | }); |
123 | 7.32k | return child_it == schema_column->children.end() ? nullptr : &*child_it; |
124 | 7.32k | } |
125 | | // Iceberg can reuse a historical name for a newly added sibling. Current names therefore |
126 | | // have precedence across the entire struct; an earlier alias must not steal that access path. |
127 | 36.6k | const auto exact_it = std::ranges::find_if(schema_column->children, [&](const auto& child) { |
128 | 36.6k | return to_lower(child.name) == to_lower(child_path); |
129 | 36.6k | }); |
130 | 19.1k | if (exact_it != schema_column->children.end()) { |
131 | 19.1k | return &*exact_it; |
132 | 19.1k | } |
133 | 18.4E | const auto alias_it = std::ranges::find_if(schema_column->children, [&](const auto& child) { |
134 | 11 | return std::ranges::any_of(child.name_mapping, [&](const std::string& alias) { |
135 | 6 | return to_lower(alias) == to_lower(child_path); |
136 | 6 | }); |
137 | 11 | }); |
138 | 18.4E | return alias_it == schema_column->children.end() ? nullptr : &*alias_it; |
139 | 19.1k | } |
140 | | |
141 | 204k | int32_t schema_field_id(const format::ColumnDefinition* schema_column) { |
142 | 204k | if (schema_column == nullptr || !schema_column->has_identifier_field_id()) { |
143 | 146k | return -1; |
144 | 146k | } |
145 | 58.5k | return schema_column->get_identifier_field_id(); |
146 | 204k | } |
147 | | |
148 | 200k | int32_t schema_field_id_or(const format::ColumnDefinition* schema_column, int32_t fallback) { |
149 | 200k | const auto field_id = schema_field_id(schema_column); |
150 | 200k | return field_id >= 0 ? field_id : fallback; |
151 | 200k | } |
152 | | |
153 | | std::string schema_field_name_or(const format::ColumnDefinition* schema_column, |
154 | 57.6k | std::string fallback) { |
155 | 57.6k | return schema_column == nullptr || schema_column->name.empty() ? fallback : schema_column->name; |
156 | 57.6k | } |
157 | | |
158 | | struct AccessPathNode { |
159 | | bool project_all = false; |
160 | | std::map<std::string, AccessPathNode> children; |
161 | | }; |
162 | | |
163 | 1.99k | void merge_access_path_node(AccessPathNode* dst, const AccessPathNode& src) { |
164 | 1.99k | DORIS_CHECK(dst != nullptr); |
165 | 1.99k | if (dst->project_all) { |
166 | 0 | return; |
167 | 0 | } |
168 | 1.99k | if (src.project_all) { |
169 | 1.20k | dst->project_all = true; |
170 | 1.20k | dst->children.clear(); |
171 | 1.20k | return; |
172 | 1.20k | } |
173 | 915 | for (const auto& [path, child] : src.children) { |
174 | 915 | merge_access_path_node(&dst->children[path], child); |
175 | 915 | } |
176 | 787 | } |
177 | | |
178 | | void insert_access_path(AccessPathNode* root, const std::vector<std::string>& path, |
179 | 32.8k | size_t path_idx) { |
180 | 32.8k | DORIS_CHECK(root != nullptr); |
181 | 32.8k | if (root->project_all) { |
182 | 0 | return; |
183 | 0 | } |
184 | 32.8k | if (path_idx >= path.size()) { |
185 | 26.4k | root->project_all = true; |
186 | 26.4k | root->children.clear(); |
187 | 26.4k | return; |
188 | 26.4k | } |
189 | 6.39k | insert_access_path(&root->children[path[path_idx]], path, path_idx + 1); |
190 | 6.39k | } |
191 | | |
192 | | void collect_variant_access_paths(const AccessPathNode& node, std::vector<std::string>* path, |
193 | 3 | std::vector<std::vector<std::string>>* result) { |
194 | 3 | DORIS_CHECK(path != nullptr && result != nullptr); |
195 | 3 | for (const auto& [segment, child] : node.children) { |
196 | 3 | path->push_back(segment); |
197 | 3 | if (child.project_all || child.children.empty()) { |
198 | 3 | result->push_back(*path); |
199 | 3 | } else { |
200 | 0 | collect_variant_access_paths(child, path, result); |
201 | 0 | } |
202 | 3 | path->pop_back(); |
203 | 3 | } |
204 | 3 | } |
205 | | |
206 | | Status build_nested_children_from_access_node(format::ColumnDefinition* column, |
207 | | const DataTypePtr& type, const AccessPathNode& node, |
208 | | const std::string& path, |
209 | | const format::ColumnDefinition* schema_column, |
210 | | bool prefer_exact_name_match); |
211 | | |
212 | | // Expand a full complex-column projection into table-schema children when the table format provides |
213 | | // an external/current schema. Without this, `SELECT complex_col` or `SELECT *` leaves |
214 | | // ColumnDefinition::children empty, so ColumnMapper treats the root complex column as a scalar |
215 | | // mapping and later tries to cast the old file shape to the current table shape directly. |
216 | | // |
217 | | // Examples: |
218 | | // - STRUCT country/city projected from an old file STRUCT country/population/location should |
219 | | // create children country and city, so city can be materialized as missing/default. |
220 | | // - ARRAY<STRUCT<item, quantity>> should create the array element wrapper and then the element |
221 | | // struct children item and quantity. |
222 | | // - MAP<STRING, STRUCT<full_name, age>> should create semantic children key/value directly, then |
223 | | // expand the value struct children full_name and age. Do not introduce a physical entries |
224 | | // wrapper here: ColumnMapper and TableReader treat MAP children as [key, value]. |
225 | | Status build_all_nested_children_from_schema(format::ColumnDefinition* column, |
226 | | const DataTypePtr& type, const std::string& path, |
227 | | const format::ColumnDefinition* schema_column, |
228 | 304k | bool prefer_exact_name_match) { |
229 | 304k | DORIS_CHECK(column != nullptr); |
230 | | |
231 | 304k | const auto nested_type = remove_nullable(type); |
232 | 304k | AccessPathNode project_all; |
233 | 304k | project_all.project_all = true; |
234 | 304k | switch (nested_type->get_primitive_type()) { |
235 | 26.2k | case TYPE_STRUCT: { |
236 | 26.2k | const auto& struct_type = assert_cast<const DataTypeStruct&>(*nested_type); |
237 | 83.9k | for (size_t field_idx = 0; field_idx < struct_type.get_elements().size(); ++field_idx) { |
238 | 57.6k | const auto field_name = struct_type.get_element_name(field_idx); |
239 | 57.6k | const auto* schema_child = |
240 | 57.6k | find_schema_child_by_path(schema_column, field_name, prefer_exact_name_match); |
241 | 57.6k | auto* child = find_or_add_child( |
242 | 57.6k | column, schema_field_id_or(schema_child, cast_set<int32_t>(field_idx)), |
243 | 57.6k | schema_field_name_or(schema_child, field_name), |
244 | 57.6k | struct_type.get_element(field_idx)); |
245 | 57.6k | inherit_schema_metadata(child, schema_child); |
246 | 57.6k | RETURN_IF_ERROR(build_nested_children_from_access_node( |
247 | 57.6k | child, child->type, project_all, path + "." + child->name, schema_child, |
248 | 57.6k | prefer_exact_name_match)); |
249 | 57.6k | } |
250 | 26.2k | return Status::OK(); |
251 | 26.2k | } |
252 | 49.6k | case TYPE_ARRAY: { |
253 | 49.6k | const auto& array_type = assert_cast<const DataTypeArray&>(*nested_type); |
254 | 49.6k | const auto* element_schema = schema_column != nullptr && !schema_column->children.empty() |
255 | 49.6k | ? schema_column->children.data() |
256 | 49.6k | : nullptr; |
257 | 49.6k | auto* child = find_or_add_child(column, schema_field_id_or(element_schema, 0), "element", |
258 | 49.6k | array_type.get_nested_type()); |
259 | 49.6k | inherit_schema_metadata(child, element_schema); |
260 | 49.6k | return build_nested_children_from_access_node(child, child->type, project_all, path + ".*", |
261 | 49.6k | element_schema, prefer_exact_name_match); |
262 | 26.2k | } |
263 | 45.1k | case TYPE_MAP: { |
264 | 45.1k | const auto& map_type = assert_cast<const DataTypeMap&>(*nested_type); |
265 | 45.1k | const auto* key_schema = schema_column != nullptr && !schema_column->children.empty() |
266 | 45.1k | ? schema_column->children.data() |
267 | 45.1k | : nullptr; |
268 | 45.1k | const auto* value_schema = schema_column != nullptr && schema_column->children.size() > 1 |
269 | 45.1k | ? &schema_column->children[1] |
270 | 45.1k | : nullptr; |
271 | 45.1k | auto* key_child = find_or_add_child(column, schema_field_id_or(key_schema, 0), "key", |
272 | 45.1k | map_type.get_key_type()); |
273 | 45.1k | inherit_schema_metadata(key_child, key_schema); |
274 | 45.1k | RETURN_IF_ERROR(build_nested_children_from_access_node( |
275 | 45.1k | key_child, key_child->type, project_all, path + ".KEYS", key_schema, |
276 | 45.1k | prefer_exact_name_match)); |
277 | 45.1k | auto* value_child = find_or_add_child(column, schema_field_id_or(value_schema, 1), "value", |
278 | 45.1k | map_type.get_value_type()); |
279 | 45.1k | inherit_schema_metadata(value_child, value_schema); |
280 | 45.1k | RETURN_IF_ERROR(build_nested_children_from_access_node( |
281 | 45.1k | value_child, value_child->type, project_all, path + ".VALUES", value_schema, |
282 | 45.1k | prefer_exact_name_match)); |
283 | 45.1k | return Status::OK(); |
284 | 45.1k | } |
285 | 183k | default: |
286 | 183k | return Status::OK(); |
287 | 304k | } |
288 | 304k | } |
289 | | |
290 | | Status build_struct_children_from_access_node(format::ColumnDefinition* column, |
291 | | const DataTypeStruct& struct_type, |
292 | | const AccessPathNode& node, const std::string& path, |
293 | | const format::ColumnDefinition* schema_column, |
294 | 3.32k | bool prefer_exact_name_match) { |
295 | 3.32k | DORIS_CHECK(column != nullptr); |
296 | 4.36k | for (const auto& [child_path, child_node] : node.children) { |
297 | | // Struct children are resolved by name or schema field id. We do not treat a numeric |
298 | | // child token as a struct ordinal, because `col.0` becomes ambiguous once the struct |
299 | | // evolves. Position-based access needs a separate design if it is required later. |
300 | 4.36k | if (child_path == "OFFSET" || child_path == "*" || child_path == "KEYS" || |
301 | 4.36k | child_path == "VALUES") { |
302 | 4 | return Status::NotSupported( |
303 | 4 | "AccessPathParser does not support access path {} for slot {}", |
304 | 4 | path + "." + child_path, column->name); |
305 | 4 | } |
306 | | |
307 | | // Prefer the table/schema ColumnDefinition because it carries field ids and aliases. |
308 | | // Fallback to the struct type name only for formats without external schema metadata. |
309 | 4.36k | const auto* schema_child = |
310 | 4.36k | find_schema_child_by_path(schema_column, child_path, prefer_exact_name_match); |
311 | 4.36k | int32_t field_id = schema_field_id(schema_child); |
312 | 4.36k | std::string field_name = schema_child == nullptr ? child_path : schema_child->name; |
313 | 4.36k | DataTypePtr field_type = schema_child == nullptr ? nullptr : schema_child->type; |
314 | 4.36k | if (field_id < 0 || field_type == nullptr) { |
315 | 50 | for (size_t field_idx = 0; field_idx < struct_type.get_elements().size(); ++field_idx) { |
316 | 47 | if (to_lower(struct_type.get_element_name(field_idx)) == to_lower(field_name)) { |
317 | 31 | field_id = cast_set<int32_t>(field_idx); |
318 | 31 | field_name = struct_type.get_element_name(field_idx); |
319 | 31 | field_type = struct_type.get_element(field_idx); |
320 | 31 | break; |
321 | 31 | } |
322 | 47 | } |
323 | 34 | } |
324 | | |
325 | 4.36k | if (field_id < 0 || field_type == nullptr) { |
326 | 3 | return Status::NotSupported( |
327 | 3 | "AccessPathParser does not support access path {} for slot {}", |
328 | 3 | path + "." + child_path, column->name); |
329 | 3 | } |
330 | | // TODO: For TVF Parquet files without field ids, this fallback uses the struct ordinal as |
331 | | // the table child identifier. BY_NAME mapping should instead keep a string identifier and |
332 | | // let TableColumnMapper resolve the file-local child id from the Parquet schema. |
333 | 4.36k | auto* child = find_or_add_child(column, field_id, field_name, field_type); |
334 | 4.36k | inherit_schema_metadata(child, schema_child); |
335 | 4.36k | RETURN_IF_ERROR(build_nested_children_from_access_node( |
336 | 4.36k | child, child->type, child_node, path + "." + child_path, schema_child, |
337 | 4.36k | prefer_exact_name_match)); |
338 | 4.36k | } |
339 | 3.31k | return Status::OK(); |
340 | 3.32k | } |
341 | | |
342 | | Status build_map_children_from_access_node(format::ColumnDefinition* column, |
343 | | const DataTypeMap& map_type, const AccessPathNode& node, |
344 | | const std::string& path, |
345 | | const format::ColumnDefinition* schema_column, |
346 | 1.01k | bool prefer_exact_name_match) { |
347 | 1.01k | DORIS_CHECK(column != nullptr); |
348 | 1.01k | AccessPathNode key_node; |
349 | 1.01k | AccessPathNode value_node; |
350 | 1.01k | bool need_key = false; |
351 | 1.01k | bool need_value = false; |
352 | | |
353 | 1.08k | for (const auto& [child_path, child_node] : node.children) { |
354 | 1.08k | if (child_path == "OFFSET") { |
355 | 1 | return Status::NotSupported( |
356 | 1 | "AccessPathParser does not support access path {} for slot {}", |
357 | 1 | path + "." + child_path, column->name); |
358 | 1 | } |
359 | 1.07k | if (child_path == "KEYS") { |
360 | 209 | need_key = true; |
361 | 209 | merge_access_path_node(&key_node, child_node); |
362 | 209 | continue; |
363 | 209 | } |
364 | 870 | if (child_path == "VALUES" || child_path == "*") { |
365 | 869 | need_key = true; |
366 | 869 | key_node.project_all = true; |
367 | 869 | key_node.children.clear(); |
368 | 869 | need_value = true; |
369 | 869 | merge_access_path_node(&value_node, child_node); |
370 | 869 | continue; |
371 | 869 | } |
372 | 1 | return Status::NotSupported("AccessPathParser does not support access path {} for slot {}", |
373 | 1 | path + "." + child_path, column->name); |
374 | 870 | } |
375 | 1.01k | if (need_key && !need_value) { |
376 | | // A key-only MAP projection is not independently materializable yet. FileScannerV2 can |
377 | | // describe a projection such as `m.KEYS`, but the downstream file block -> table block path |
378 | | // still builds a ColumnMap from key column + value column + offsets. If the value child is |
379 | | // omitted here, TableReader/ColumnMapper cannot reconstruct a valid table MAP column even |
380 | | // though the query only needs keys. |
381 | | // |
382 | | // Example: |
383 | | // SELECT map_keys(m) FROM t; |
384 | | // or |
385 | | // SELECT * FROM t WHERE array_contains(map_keys(m), 'k1'); |
386 | | // |
387 | | // The access path only asks for `m.KEYS`, but the scan still has to read `m.VALUES` as a |
388 | | // temporary full projection until map materialization supports constructing a table MAP |
389 | | // from keys only. |
390 | 145 | need_value = true; |
391 | 145 | value_node.project_all = true; |
392 | 145 | value_node.children.clear(); |
393 | 145 | } |
394 | | |
395 | 1.01k | if (!need_key && !need_value) { |
396 | 0 | return Status::OK(); |
397 | 0 | } |
398 | | |
399 | 1.01k | const auto* key_schema = schema_column != nullptr && !schema_column->children.empty() |
400 | 1.01k | ? schema_column->children.data() |
401 | 1.01k | : nullptr; |
402 | 1.01k | const auto* value_schema = schema_column != nullptr && schema_column->children.size() > 1 |
403 | 1.01k | ? &schema_column->children[1] |
404 | 1.01k | : nullptr; |
405 | 1.01k | if (need_key) { |
406 | 1.01k | auto* key_child = find_or_add_child(column, schema_field_id_or(key_schema, 0), "key", |
407 | 1.01k | map_type.get_key_type()); |
408 | 1.01k | inherit_schema_metadata(key_child, key_schema); |
409 | 1.01k | RETURN_IF_ERROR(build_nested_children_from_access_node(key_child, key_child->type, key_node, |
410 | 1.01k | path + ".KEYS", key_schema, |
411 | 1.01k | prefer_exact_name_match)); |
412 | 1.01k | } |
413 | 1.01k | if (need_value) { |
414 | 1.01k | auto* value_child = find_or_add_child(column, schema_field_id_or(value_schema, 1), "value", |
415 | 1.01k | map_type.get_value_type()); |
416 | 1.01k | inherit_schema_metadata(value_child, value_schema); |
417 | 1.01k | RETURN_IF_ERROR(build_nested_children_from_access_node( |
418 | 1.01k | value_child, value_child->type, value_node, path + ".VALUES", value_schema, |
419 | 1.01k | prefer_exact_name_match)); |
420 | 1.01k | } |
421 | 1.01k | return Status::OK(); |
422 | 1.01k | } |
423 | | |
424 | | Status build_nested_children_from_access_node(format::ColumnDefinition* column, |
425 | | const DataTypePtr& type, const AccessPathNode& node, |
426 | | const std::string& path, |
427 | | const format::ColumnDefinition* schema_column, |
428 | 309k | bool prefer_exact_name_match) { |
429 | 309k | DORIS_CHECK(column != nullptr); |
430 | 309k | if (node.project_all || node.children.empty()) { |
431 | 304k | return build_all_nested_children_from_schema(column, type, path, schema_column, |
432 | 304k | prefer_exact_name_match); |
433 | 304k | } |
434 | | |
435 | 5.00k | const auto nested_type = remove_nullable(type); |
436 | 5.00k | switch (nested_type->get_primitive_type()) { |
437 | 3.32k | case TYPE_STRUCT: |
438 | 3.32k | return build_struct_children_from_access_node( |
439 | 3.32k | column, assert_cast<const DataTypeStruct&>(*nested_type), node, path, schema_column, |
440 | 3.32k | prefer_exact_name_match); |
441 | 676 | case TYPE_ARRAY: { |
442 | 676 | if (node.children.size() != 1 || !node.children.contains("*")) { |
443 | 2 | return Status::NotSupported( |
444 | 2 | "AccessPathParser does not support access path {} for slot {}", path, |
445 | 2 | column->name); |
446 | 2 | } |
447 | 674 | const auto& array_type = assert_cast<const DataTypeArray&>(*nested_type); |
448 | 674 | const auto* element_schema = schema_column != nullptr && !schema_column->children.empty() |
449 | 674 | ? schema_column->children.data() |
450 | 674 | : nullptr; |
451 | 674 | auto* child = find_or_add_child(column, schema_field_id_or(element_schema, 0), "element", |
452 | 674 | array_type.get_nested_type()); |
453 | 674 | inherit_schema_metadata(child, element_schema); |
454 | 674 | return build_nested_children_from_access_node(child, child->type, node.children.at("*"), |
455 | 674 | path + ".*", element_schema, |
456 | 674 | prefer_exact_name_match); |
457 | 676 | } |
458 | 1.01k | case TYPE_MAP: |
459 | 1.01k | return build_map_children_from_access_node( |
460 | 1.01k | column, assert_cast<const DataTypeMap&>(*nested_type), node, path, schema_column, |
461 | 1.01k | prefer_exact_name_match); |
462 | 3 | case TYPE_VARIANT: { |
463 | | // A Variant nested below STRUCT/ARRAY/MAP owns paths relative to this terminal. Keeping |
464 | | // them on the nested ColumnDefinition lets ColumnMapper select the same physical leaves |
465 | | // as a root Variant without flattening away the surrounding container. |
466 | 3 | column->variant_access_paths.clear(); |
467 | 3 | std::vector<std::string> variant_path; |
468 | 3 | collect_variant_access_paths(node, &variant_path, &column->variant_access_paths); |
469 | 3 | std::ranges::sort(column->variant_access_paths); |
470 | 3 | column->variant_access_paths.erase(std::unique(column->variant_access_paths.begin(), |
471 | 3 | column->variant_access_paths.end()), |
472 | 3 | column->variant_access_paths.end()); |
473 | 3 | return Status::OK(); |
474 | 676 | } |
475 | 0 | default: |
476 | 0 | return Status::NotSupported("AccessPathParser does not support access path {} for slot {}", |
477 | 0 | path, column->name); |
478 | 5.00k | } |
479 | 5.00k | } |
480 | | |
481 | | } // namespace |
482 | | |
483 | | Status AccessPathParser::build_nested_children(format::ColumnDefinition* column, |
484 | | const std::vector<TColumnAccessPath>& access_paths, |
485 | | const format::ColumnDefinition* schema_column, |
486 | 297k | bool prefer_exact_name_match) { |
487 | 297k | DORIS_CHECK(column != nullptr); |
488 | 297k | if (is_scanner_materialized_virtual_column(column->name)) { |
489 | 547 | return Status::OK(); |
490 | 547 | } |
491 | 296k | if (remove_nullable(column->type)->get_primitive_type() == TYPE_VARIANT) { |
492 | 271 | column->variant_access_paths.clear(); |
493 | 395 | for (const auto& access_path : access_paths) { |
494 | 395 | if (access_path.type != TAccessPathType::DATA || |
495 | 395 | !access_path.__isset.data_access_path) { |
496 | 0 | return Status::NotSupported( |
497 | 0 | "AccessPathParser only supports DATA access paths for Variant slot {}", |
498 | 0 | column->name); |
499 | 0 | } |
500 | 395 | const auto& path = access_path.data_access_path.path; |
501 | 395 | if (path.empty()) { |
502 | | // Match the generic access-path tree: an empty DATA path denotes the whole slot |
503 | | // and dominates every narrower Variant path in the same request. |
504 | 1 | column->variant_access_paths.clear(); |
505 | 1 | return Status::OK(); |
506 | 1 | } |
507 | 394 | int32_t top_level_id = -1; |
508 | 394 | if (to_lower(path.front()) != to_lower(column->name) && |
509 | 394 | (!parse_non_negative_int(path.front(), &top_level_id) || |
510 | 392 | !column->has_identifier_field_id() || |
511 | 392 | top_level_id != column->get_identifier_field_id())) { |
512 | 0 | return Status::NotSupported( |
513 | 0 | "AccessPathParser access path {} does not match Variant slot {}", |
514 | 0 | access_path_to_string(path), column->name); |
515 | 0 | } |
516 | 394 | if (path.size() == 1) { |
517 | | // A whole-root access covers every subpath and must disable physical leaf pruning. |
518 | 66 | column->variant_access_paths.clear(); |
519 | 66 | return Status::OK(); |
520 | 66 | } |
521 | 328 | column->variant_access_paths.emplace_back(path.begin() + 1, path.end()); |
522 | 328 | } |
523 | 204 | std::ranges::sort(column->variant_access_paths); |
524 | 204 | column->variant_access_paths.erase(std::unique(column->variant_access_paths.begin(), |
525 | 204 | column->variant_access_paths.end()), |
526 | 204 | column->variant_access_paths.end()); |
527 | 204 | return Status::OK(); |
528 | 271 | } |
529 | 296k | if (!is_complex_type(remove_nullable(column->type)->get_primitive_type())) { |
530 | 191k | return Status::OK(); |
531 | 191k | } |
532 | | |
533 | 104k | AccessPathNode root; |
534 | | // Build tree for AccessPathNode. |
535 | | // For example, for access paths ["a.b", "a.c", "d"], the tree will be: |
536 | | // root |
537 | | // ├── a |
538 | | // │ ├── b |
539 | | // │ └── c |
540 | | // └── d |
541 | 104k | for (const auto& access_path : access_paths) { |
542 | | // TODO: Support META access paths if needed. Currently AccessPathParser only supports |
543 | | // DATA access paths. |
544 | 26.4k | if (access_path.type != TAccessPathType::DATA || !access_path.__isset.data_access_path) { |
545 | 2 | return Status::NotSupported( |
546 | 2 | "AccessPathParser only supports DATA access paths for slot {}", column->name); |
547 | 2 | } |
548 | 26.4k | const auto& path = access_path.data_access_path.path; |
549 | 26.4k | if (path.empty()) { |
550 | 0 | insert_access_path(&root, path, 0); |
551 | 0 | continue; |
552 | 0 | } |
553 | 26.4k | int32_t top_level_id = -1; |
554 | 26.4k | if (to_lower(path.front()) != to_lower(column->name) && |
555 | 26.4k | (!parse_non_negative_int(path.front(), &top_level_id) || |
556 | 24.6k | !column->has_identifier_field_id() || |
557 | 24.5k | top_level_id != column->get_identifier_field_id())) { |
558 | 2 | return Status::NotSupported("AccessPathParser access path {} does not match slot {}", |
559 | 2 | access_path_to_string(path), column->name); |
560 | 2 | } |
561 | 26.4k | insert_access_path(&root, path, 1); |
562 | 26.4k | } |
563 | | // Recursively build nested children for the column based on the AccessPathNode tree. |
564 | 104k | return build_nested_children_from_access_node(column, column->type, root, column->name, |
565 | 104k | schema_column, prefer_exact_name_match); |
566 | 104k | } |
567 | | |
568 | | Status AccessPathParser::build_nested_children(format::ColumnDefinition* column, |
569 | | const SlotDescriptor* slot_desc, |
570 | | const format::ColumnDefinition* schema_column, |
571 | 295k | bool prefer_exact_name_match) { |
572 | 295k | DORIS_CHECK(column != nullptr); |
573 | 295k | DORIS_CHECK(slot_desc != nullptr); |
574 | 295k | return build_nested_children(column, slot_desc->all_access_paths(), |
575 | 295k | slot_desc->predicate_access_paths(), schema_column, |
576 | 295k | prefer_exact_name_match); |
577 | 295k | } |
578 | | |
579 | | Status AccessPathParser::build_nested_children( |
580 | | format::ColumnDefinition* column, const std::vector<TColumnAccessPath>& all_access_paths, |
581 | | const std::vector<TColumnAccessPath>& predicate_access_paths, |
582 | 295k | const format::ColumnDefinition* schema_column, bool prefer_exact_name_match) { |
583 | 295k | DORIS_CHECK(column != nullptr); |
584 | 295k | auto predicate_column = *column; |
585 | 295k | RETURN_IF_ERROR(build_nested_children(column, all_access_paths, schema_column, |
586 | 295k | prefer_exact_name_match)); |
587 | 295k | column->has_predicate_access_paths = !predicate_access_paths.empty(); |
588 | 295k | column->predicate_children.clear(); |
589 | 295k | column->predicate_variant_access_paths.clear(); |
590 | 295k | if (predicate_access_paths.empty()) { |
591 | 293k | return Status::OK(); |
592 | 293k | } |
593 | | |
594 | 1.75k | predicate_column.children.clear(); |
595 | 1.75k | predicate_column.variant_access_paths.clear(); |
596 | 1.75k | predicate_column.has_predicate_access_paths = false; |
597 | 1.75k | predicate_column.predicate_children.clear(); |
598 | 1.75k | predicate_column.predicate_variant_access_paths.clear(); |
599 | 1.75k | RETURN_IF_ERROR(build_nested_children(&predicate_column, predicate_access_paths, schema_column, |
600 | 1.75k | prefer_exact_name_match)); |
601 | 1.75k | column->predicate_children = std::move(predicate_column.children); |
602 | 1.75k | column->predicate_variant_access_paths = std::move(predicate_column.variant_access_paths); |
603 | 1.75k | return Status::OK(); |
604 | 1.75k | } |
605 | | |
606 | | } // namespace doris |