be/src/format/table/iceberg_reader.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 "format/table/iceberg_reader.h" |
19 | | |
20 | | #include <gen_cpp/Descriptors_types.h> |
21 | | #include <gen_cpp/Metrics_types.h> |
22 | | #include <gen_cpp/PlanNodes_types.h> |
23 | | #include <gen_cpp/parquet_types.h> |
24 | | #include <glog/logging.h> |
25 | | #include <parallel_hashmap/phmap.h> |
26 | | #include <rapidjson/document.h> |
27 | | |
28 | | #include <algorithm> |
29 | | #include <cstring> |
30 | | #include <functional> |
31 | | #include <memory> |
32 | | |
33 | | #include "common/compiler_util.h" // IWYU pragma: keep |
34 | | #include "common/consts.h" |
35 | | #include "common/status.h" |
36 | | #include "core/assert_cast.h" |
37 | | #include "core/block/block.h" |
38 | | #include "core/block/column_with_type_and_name.h" |
39 | | #include "core/column/column.h" |
40 | | #include "core/column/column_nullable.h" |
41 | | #include "core/column/column_string.h" |
42 | | #include "core/column/column_vector.h" |
43 | | #include "core/data_type/data_type_factory.hpp" |
44 | | #include "core/data_type/data_type_nullable.h" |
45 | | #include "core/data_type/define_primitive_type.h" |
46 | | #include "core/data_type/primitive_type.h" |
47 | | #include "core/string_ref.h" |
48 | | #include "exprs/aggregate/aggregate_function.h" |
49 | | #include "format/format_common.h" |
50 | | #include "format/generic_reader.h" |
51 | | #include "format/orc/vorc_reader.h" |
52 | | #include "format/parquet/schema_desc.h" |
53 | | #include "format/parquet/vparquet_column_chunk_reader.h" |
54 | | #include "format/table/deletion_vector_reader.h" |
55 | | #include "format/table/iceberg/iceberg_orc_nested_column_utils.h" |
56 | | #include "format/table/iceberg/iceberg_parquet_nested_column_utils.h" |
57 | | #include "format/table/iceberg_scan_semantics.h" |
58 | | #include "format/table/nested_column_access_helper.h" |
59 | | #include "format/table/table_schema_change_helper.h" |
60 | | #include "runtime/runtime_state.h" |
61 | | #include "util/coding.h" |
62 | | |
63 | | namespace cctz { |
64 | | class time_zone; |
65 | | } // namespace cctz |
66 | | namespace doris { |
67 | | class RowDescriptor; |
68 | | class SlotDescriptor; |
69 | | class TupleDescriptor; |
70 | | |
71 | | namespace io { |
72 | | struct IOContext; |
73 | | } // namespace io |
74 | | class VExprContext; |
75 | | } // namespace doris |
76 | | |
77 | | namespace doris { |
78 | | const std::string IcebergOrcReader::ICEBERG_ORC_ATTRIBUTE = "iceberg.id"; |
79 | | |
80 | | namespace { |
81 | | |
82 | 186 | bool orc_subtree_has_iceberg_id(const orc::Type* type, const std::string& attribute) { |
83 | 186 | if (type->hasAttributeKey(attribute)) { |
84 | 93 | return true; |
85 | 93 | } |
86 | 93 | for (uint64_t idx = 0; idx < type->getSubtypeCount(); ++idx) { |
87 | 93 | if (orc_subtree_has_iceberg_id(type->getSubtype(idx), attribute)) { |
88 | 93 | return true; |
89 | 93 | } |
90 | 93 | } |
91 | 0 | return false; |
92 | 93 | } |
93 | | |
94 | | } // namespace |
95 | | |
96 | | bool IcebergTableReader::_is_fully_dictionary_encoded( |
97 | 10 | const tparquet::ColumnMetaData& column_metadata) { |
98 | 16 | const auto is_dictionary_encoding = [](tparquet::Encoding::type encoding) { |
99 | 16 | return encoding == tparquet::Encoding::PLAIN_DICTIONARY || |
100 | 16 | encoding == tparquet::Encoding::RLE_DICTIONARY; |
101 | 16 | }; |
102 | 14 | const auto is_data_page = [](tparquet::PageType::type page_type) { |
103 | 14 | return page_type == tparquet::PageType::DATA_PAGE || |
104 | 14 | page_type == tparquet::PageType::DATA_PAGE_V2; |
105 | 14 | }; |
106 | 10 | const auto is_level_encoding = [](tparquet::Encoding::type encoding) { |
107 | 2 | return encoding == tparquet::Encoding::RLE || encoding == tparquet::Encoding::BIT_PACKED; |
108 | 2 | }; |
109 | | |
110 | | // A column chunk may have a dictionary page but still contain plain-encoded data pages. |
111 | | // Only treat it as dictionary-coded when all data pages are dictionary encoded. |
112 | 10 | if (column_metadata.__isset.encoding_stats) { |
113 | 9 | bool has_data_page_stats = false; |
114 | 14 | for (const tparquet::PageEncodingStats& enc_stat : column_metadata.encoding_stats) { |
115 | 14 | if (is_data_page(enc_stat.page_type) && enc_stat.count > 0) { |
116 | 10 | has_data_page_stats = true; |
117 | 10 | if (!is_dictionary_encoding(enc_stat.encoding)) { |
118 | 4 | return false; |
119 | 4 | } |
120 | 10 | } |
121 | 14 | } |
122 | 5 | if (has_data_page_stats) { |
123 | 4 | return true; |
124 | 4 | } |
125 | 5 | } |
126 | | |
127 | 2 | bool has_dict_encoding = false; |
128 | 2 | bool has_nondict_encoding = false; |
129 | 3 | for (const tparquet::Encoding::type& encoding : column_metadata.encodings) { |
130 | 3 | if (is_dictionary_encoding(encoding)) { |
131 | 1 | has_dict_encoding = true; |
132 | 1 | } |
133 | | |
134 | 3 | if (!is_dictionary_encoding(encoding) && !is_level_encoding(encoding)) { |
135 | 2 | has_nondict_encoding = true; |
136 | 2 | break; |
137 | 2 | } |
138 | 3 | } |
139 | 2 | if (!has_dict_encoding || has_nondict_encoding) { |
140 | 2 | return false; |
141 | 2 | } |
142 | | |
143 | 0 | return true; |
144 | 2 | } |
145 | | |
146 | | // ============================================================================ |
147 | | // IcebergParquetReader: on_before_init_reader (Parquet-specific schema matching) |
148 | | // ============================================================================ |
149 | 129 | Status IcebergParquetReader::on_before_init_reader(ReaderInitContext* ctx) { |
150 | 129 | _column_descs = ctx->column_descs; |
151 | 129 | _fill_col_name_to_block_idx = ctx->col_name_to_block_idx; |
152 | 129 | _file_format = Fileformat::PARQUET; |
153 | | |
154 | | // Get file metadata schema first (available because _open_file() already ran) |
155 | 129 | const FieldDescriptor* field_desc = nullptr; |
156 | 129 | RETURN_IF_ERROR(this->get_file_metadata_schema(&field_desc)); |
157 | 129 | DCHECK(field_desc != nullptr); |
158 | | |
159 | | // Build table_info_node by field_id or name matching. |
160 | | // This must happen BEFORE column classification so we can use children_column_exists |
161 | | // to check if a column exists in the file (by field ID, not name). |
162 | 127 | if (!get_scan_params().__isset.history_schema_info || |
163 | 127 | get_scan_params().history_schema_info.empty()) [[unlikely]] { |
164 | 1 | RETURN_IF_ERROR(BuildTableInfoUtil::by_parquet_name(ctx->tuple_descriptor, *field_desc, |
165 | 1 | ctx->table_info_node)); |
166 | 126 | } else { |
167 | 126 | RETURN_IF_ERROR(BuildTableInfoUtil::by_parquet_field_id_with_name_mapping( |
168 | 126 | get_scan_params().history_schema_info.front().root_field, *field_desc, |
169 | 126 | ctx->table_info_node, supports_iceberg_scan_semantics_v1(&get_scan_params()))); |
170 | 126 | } |
171 | | |
172 | 127 | std::unordered_set<std::string> partition_col_names; |
173 | 127 | if (ctx->range->__isset.columns_from_path_keys) { |
174 | 38 | partition_col_names.insert(ctx->range->columns_from_path_keys.begin(), |
175 | 38 | ctx->range->columns_from_path_keys.end()); |
176 | 38 | } |
177 | | |
178 | | // Single pass: classify columns, detect $row_id, handle partition fallback. |
179 | 127 | bool has_partition_from_path = false; |
180 | 774 | for (const auto& desc : *ctx->column_descs) { |
181 | 774 | if (desc.category == ColumnCategory::SYNTHESIZED) { |
182 | 0 | if (desc.name == BeConsts::ICEBERG_ROWID_COL) { |
183 | 0 | this->register_synthesized_column_handler( |
184 | 0 | BeConsts::ICEBERG_ROWID_COL, [this](Block* block, size_t rows) -> Status { |
185 | 0 | return _fill_iceberg_row_id(block, rows); |
186 | 0 | }); |
187 | 0 | continue; |
188 | 0 | } else if (desc.name.starts_with(BeConsts::GLOBAL_ROWID_COL)) { |
189 | 0 | auto topn_row_id_column_iter = _create_topn_row_id_column_iterator(); |
190 | 0 | this->register_synthesized_column_handler( |
191 | 0 | desc.name, |
192 | 0 | [iter = std::move(topn_row_id_column_iter), this, &desc]( |
193 | 0 | Block* block, size_t rows) -> Status { |
194 | 0 | return fill_topn_row_id(iter, desc.name, block, rows); |
195 | 0 | }); |
196 | 0 | continue; |
197 | 0 | } |
198 | 774 | } else if (desc.category == ColumnCategory::PARTITION_KEY) { |
199 | 38 | bool has_partition_value = partition_col_names.contains(desc.name); |
200 | 38 | bool exists_in_file = ctx->table_info_node->children_column_exists(desc.name); |
201 | 38 | if (!has_partition_value || exists_in_file) { |
202 | | // Keep PARTITION_KEY category stable for scan planning, but still read |
203 | | // from file when the column exists there. |
204 | 38 | ctx->column_names.push_back(desc.name); |
205 | 38 | continue; |
206 | 38 | } |
207 | 0 | has_partition_from_path = true; |
208 | 736 | } else if (desc.category == ColumnCategory::REGULAR) { |
209 | 736 | ctx->column_names.push_back(desc.name); |
210 | 736 | } else if (desc.category == ColumnCategory::GENERATED) { |
211 | 0 | _init_row_lineage_columns(); |
212 | 0 | if (desc.name == ROW_LINEAGE_ROW_ID) { |
213 | 0 | ctx->column_names.push_back(desc.name); |
214 | 0 | this->register_generated_column_handler( |
215 | 0 | ROW_LINEAGE_ROW_ID, [this](Block* block, size_t rows) -> Status { |
216 | 0 | return _fill_row_lineage_row_id(block, rows); |
217 | 0 | }); |
218 | 0 | continue; |
219 | 0 | } else if (desc.name == ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER) { |
220 | 0 | ctx->column_names.push_back(desc.name); |
221 | 0 | this->register_generated_column_handler( |
222 | 0 | ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER, |
223 | 0 | [this](Block* block, size_t rows) -> Status { |
224 | 0 | return _fill_row_lineage_last_updated_sequence_number(block, rows); |
225 | 0 | }); |
226 | 0 | continue; |
227 | 0 | } |
228 | 0 | } |
229 | 774 | } |
230 | | |
231 | | // Set up partition value extraction if any partition columns need filling from path |
232 | 127 | if (has_partition_from_path) { |
233 | 0 | RETURN_IF_ERROR(_extract_partition_values(*ctx->range, ctx->tuple_descriptor, |
234 | 0 | _fill_partition_values, |
235 | 0 | &_fill_partition_value_is_null)); |
236 | 0 | } |
237 | | |
238 | 127 | _all_required_col_names = ctx->column_names; |
239 | | |
240 | | // Create column IDs from field descriptor |
241 | 127 | auto column_id_result = |
242 | 127 | _create_column_ids(field_desc, ctx->tuple_descriptor, ctx->table_info_node); |
243 | 127 | ctx->column_ids = std::move(column_id_result.column_ids); |
244 | 127 | ctx->filter_column_ids = std::move(column_id_result.filter_column_ids); |
245 | | |
246 | | // Build field_id -> block_column_name mapping for equality delete filtering. |
247 | | // This was previously done in init_reader() column matching (pre-CRTP refactoring). |
248 | 774 | for (const auto* slot : ctx->tuple_descriptor->slots()) { |
249 | 774 | _id_to_block_column_name.emplace(slot->col_unique_id(), slot->col_name()); |
250 | 774 | } |
251 | | |
252 | | // Process delete files (must happen before _do_init_reader so expand col IDs are included) |
253 | 127 | RETURN_IF_ERROR(_init_row_filters()); |
254 | | |
255 | | // Add expand column IDs for equality delete and remap expand column names |
256 | | // to match master's behavior: |
257 | | // - Use field_id to find the actual file column name in Parquet schema |
258 | | // - Prefix with __equality_delete_column__ to avoid name conflicts |
259 | | // - Correctly map table_col_name → file_col_name in table_info_node |
260 | 119 | const static std::string EQ_DELETE_PRE = "__equality_delete_column__"; |
261 | 119 | std::unordered_map<int, const FieldSchema*> field_id_to_file_column; |
262 | 119 | bool all_file_columns_have_field_ids = true; |
263 | 119 | bool any_file_column_has_field_id = false; |
264 | 1.01k | for (int i = 0; i < field_desc->size(); ++i) { |
265 | 897 | const auto* field_schema = field_desc->get_column(i); |
266 | 897 | if (field_schema) { |
267 | 897 | if (field_schema->field_id < 0) { |
268 | 3 | all_file_columns_have_field_ids = false; |
269 | 894 | } else { |
270 | 894 | any_file_column_has_field_id = true; |
271 | 894 | field_id_to_file_column[field_schema->field_id] = field_schema; |
272 | 894 | } |
273 | 897 | } |
274 | 897 | } |
275 | 119 | const bool use_field_ids_for_hidden_keys = |
276 | 119 | supports_iceberg_scan_semantics_v1(&get_scan_params()) |
277 | 119 | ? any_file_column_has_field_id |
278 | 119 | : all_file_columns_have_field_ids; |
279 | 119 | const auto struct_node = |
280 | 119 | std::dynamic_pointer_cast<TableSchemaChangeHelper::StructNode>(ctx->table_info_node); |
281 | 119 | DORIS_CHECK(struct_node != nullptr); |
282 | | |
283 | | // Rebuild _expand_col_names with proper file-column-based names |
284 | 119 | std::vector<std::string> new_expand_col_names; |
285 | 121 | for (size_t i = 0; i < _expand_col_names.size(); ++i) { |
286 | 2 | const auto& old_name = _expand_col_names[i]; |
287 | | // Find the field_id for this expand column |
288 | 2 | int field_id = -1; |
289 | 2 | for (auto& [fid, name] : _id_to_block_column_name) { |
290 | 2 | if (name == old_name) { |
291 | 2 | field_id = fid; |
292 | 2 | break; |
293 | 2 | } |
294 | 2 | } |
295 | | |
296 | 2 | const FieldSchema* file_column = nullptr; |
297 | 2 | if (use_field_ids_for_hidden_keys) { |
298 | 1 | auto id_it = field_id_to_file_column.find(field_id); |
299 | 1 | if (id_it != field_id_to_file_column.end()) { |
300 | 1 | file_column = id_it->second; |
301 | 1 | } |
302 | 1 | } else { |
303 | 1 | std::string table_column_name = old_name; |
304 | 1 | if (const auto* table_field = _find_current_schema_field(field_id); |
305 | 1 | table_field != nullptr && table_field->__isset.name) { |
306 | 1 | table_column_name = table_field->name; |
307 | 1 | } |
308 | | // Delete files may retain a pre-rename key name, while StructNode is keyed by the |
309 | | // current table name. Resolve that name by stable field ID before following BY_NAME. |
310 | 1 | if (struct_node->get_children().contains(table_column_name) && |
311 | 1 | struct_node->children_column_exists(table_column_name)) { |
312 | 1 | const auto& mapped_name = struct_node->children_file_column_name(table_column_name); |
313 | 3 | for (int j = 0; j < field_desc->size(); ++j) { |
314 | 3 | const auto* candidate = field_desc->get_column(j); |
315 | 3 | if (candidate != nullptr && candidate->name == mapped_name) { |
316 | 1 | file_column = candidate; |
317 | 1 | break; |
318 | 1 | } |
319 | 3 | } |
320 | 1 | DORIS_CHECK(file_column != nullptr); |
321 | 1 | } |
322 | 1 | } |
323 | | |
324 | 2 | const std::string file_col_name = file_column == nullptr ? old_name : file_column->name; |
325 | 2 | std::string table_col_name = EQ_DELETE_PRE + file_col_name; |
326 | | |
327 | | // Update _id_to_block_column_name |
328 | 2 | if (field_id >= 0) { |
329 | 2 | _id_to_block_column_name[field_id] = table_col_name; |
330 | 2 | } |
331 | | |
332 | | // Update _expand_columns name |
333 | 2 | if (i < _expand_columns.size()) { |
334 | 2 | _expand_columns[i].name = table_col_name; |
335 | 2 | } |
336 | | |
337 | 2 | if (file_column == nullptr) { |
338 | 0 | DORIS_CHECK(i < _expand_columns.size()); |
339 | 0 | RETURN_IF_ERROR(_register_missing_equality_delete_column(field_id, table_col_name, |
340 | 0 | _expand_columns[i].type)); |
341 | | // The old data file predates this equality key. Keep it in the expand block so the |
342 | | // synthesized-column hook can materialize its logical initial default before reader |
343 | | // filtering, but do not advertise it to Parquet as a physical child. |
344 | 0 | new_expand_col_names.push_back(table_col_name); |
345 | 0 | continue; |
346 | 0 | } |
347 | | |
348 | 2 | new_expand_col_names.push_back(table_col_name); |
349 | | |
350 | | // Add column IDs |
351 | 2 | ctx->column_ids.insert(file_column->get_column_id()); |
352 | | |
353 | | // Register in table_info_node: table_col_name → file_col_name |
354 | 2 | ctx->column_names.push_back(table_col_name); |
355 | 2 | ctx->table_info_node->add_children(table_col_name, file_col_name, |
356 | 2 | TableSchemaChangeHelper::ConstNode::get_instance()); |
357 | 2 | } |
358 | 119 | _expand_col_names = std::move(new_expand_col_names); |
359 | | |
360 | | // Enable group filtering for Iceberg |
361 | 119 | _filter_groups = true; |
362 | | |
363 | 119 | return Status::OK(); |
364 | 119 | } |
365 | | |
366 | | // ============================================================================ |
367 | | // IcebergParquetReader: _create_column_ids |
368 | | // ============================================================================ |
369 | | ColumnIdResult IcebergParquetReader::_create_column_ids( |
370 | | const FieldDescriptor* field_desc, const TupleDescriptor* tuple_descriptor, |
371 | 134 | const std::shared_ptr<TableSchemaChangeHelper::Node>& table_info_node) { |
372 | 134 | auto* mutable_field_desc = const_cast<FieldDescriptor*>(field_desc); |
373 | 134 | mutable_field_desc->assign_ids(); |
374 | | |
375 | 134 | std::unordered_map<int, const FieldSchema*> iceberg_id_to_field_schema_map; |
376 | 1.10k | for (int i = 0; i < field_desc->size(); ++i) { |
377 | 969 | auto field_schema = field_desc->get_column(i); |
378 | 969 | if (!field_schema) continue; |
379 | 969 | int iceberg_id = field_schema->field_id; |
380 | 969 | iceberg_id_to_field_schema_map[iceberg_id] = field_schema; |
381 | 969 | } |
382 | | |
383 | 134 | std::set<uint64_t> column_ids; |
384 | 134 | std::set<uint64_t> filter_column_ids; |
385 | | |
386 | 134 | auto process_access_paths = [](const FieldSchema* parquet_field, |
387 | 134 | const std::vector<TColumnAccessPath>& access_paths, |
388 | 134 | std::set<uint64_t>& out_ids) { |
389 | 16 | process_nested_access_paths( |
390 | 16 | parquet_field, access_paths, out_ids, |
391 | 16 | [](const FieldSchema* field) { return field->get_column_id(); }, |
392 | 16 | [](const FieldSchema* field) { return field->get_max_column_id(); }, |
393 | 16 | IcebergParquetNestedColumnUtils::extract_nested_column_ids); |
394 | 16 | }; |
395 | | |
396 | | // The Iceberg schema-mapping root is a StructNode whose registered children are the real |
397 | | // table columns. When present, resolve each column by name through it so the column-id set |
398 | | // stays consistent with the schema-mapping decision (BY_ID or BY_NAME/name-mapping); |
399 | | // otherwise fall back to matching by Iceberg field id. |
400 | 134 | const auto* struct_node = |
401 | 134 | dynamic_cast<const TableSchemaChangeHelper::StructNode*>(table_info_node.get()); |
402 | | |
403 | 789 | for (const auto* slot : tuple_descriptor->slots()) { |
404 | 789 | const FieldSchema* field_schema = nullptr; |
405 | 789 | if (struct_node != nullptr) { |
406 | | // Synthesized/metadata slots (e.g. the TopN global row-id or the $row_id column) are |
407 | | // never registered as children, so check membership before querying: calling |
408 | | // children_column_exists() on an unregistered name DCHECK-aborts in debug builds and |
409 | | // throws std::out_of_range from .at() in release builds. |
410 | 774 | if (struct_node->get_children().contains(slot->col_name()) && |
411 | 775 | struct_node->children_column_exists(slot->col_name())) { |
412 | | // Use the physical child selected by the schema-mapping pass. This keeps partial-id |
413 | | // files in BY_NAME mode from binding a projected column through an unrelated stale |
414 | | // field id. |
415 | 757 | const auto& file_column_name = |
416 | 757 | struct_node->children_file_column_name(slot->col_name()); |
417 | 6.09k | for (int i = 0; i < field_desc->size(); ++i) { |
418 | 6.09k | const auto* candidate = field_desc->get_column(i); |
419 | 6.09k | if (candidate != nullptr && candidate->name == file_column_name) { |
420 | 755 | field_schema = candidate; |
421 | 755 | break; |
422 | 755 | } |
423 | 6.09k | } |
424 | 757 | DORIS_CHECK(field_schema != nullptr); |
425 | 757 | } |
426 | 774 | } else { |
427 | 15 | auto it = iceberg_id_to_field_schema_map.find(slot->col_unique_id()); |
428 | 15 | if (it != iceberg_id_to_field_schema_map.end()) { |
429 | 13 | field_schema = it->second; |
430 | 13 | } |
431 | 15 | } |
432 | 789 | if (field_schema == nullptr) { |
433 | 19 | continue; |
434 | 19 | } |
435 | | |
436 | 770 | if ((slot->col_type() != TYPE_STRUCT && slot->col_type() != TYPE_ARRAY && |
437 | 770 | slot->col_type() != TYPE_MAP)) { |
438 | 756 | column_ids.insert(field_schema->column_id); |
439 | 756 | if (slot->is_predicate()) { |
440 | 0 | filter_column_ids.insert(field_schema->column_id); |
441 | 0 | } |
442 | 756 | continue; |
443 | 756 | } |
444 | | |
445 | 14 | const auto& all_access_paths = slot->all_access_paths(); |
446 | 14 | process_access_paths(field_schema, all_access_paths, column_ids); |
447 | | |
448 | 14 | const auto& predicate_access_paths = slot->predicate_access_paths(); |
449 | 14 | if (!predicate_access_paths.empty()) { |
450 | 6 | process_access_paths(field_schema, predicate_access_paths, filter_column_ids); |
451 | 6 | } |
452 | 14 | } |
453 | 134 | return {std::move(column_ids), std::move(filter_column_ids)}; |
454 | 134 | } |
455 | | |
456 | | // ============================================================================ |
457 | | // IcebergParquetReader: _read_position_delete_file |
458 | | // ============================================================================ |
459 | | Status IcebergParquetReader::_read_position_delete_file(const TFileRangeDesc* delete_range, |
460 | 1 | DeleteFile* position_delete) { |
461 | 1 | ParquetReader parquet_delete_reader(get_profile(), get_scan_params(), *delete_range, |
462 | 1 | READ_DELETE_FILE_BATCH_SIZE, &get_state()->timezone_obj(), |
463 | 1 | get_io_ctx(), get_state(), _meta_cache); |
464 | | // The delete file range has size=-1 (read whole file). We must disable |
465 | | // row group filtering before init; otherwise _do_init_reader returns EndOfFile |
466 | | // when _filter_groups && _range_size < 0. |
467 | 1 | ParquetInitContext delete_ctx; |
468 | 1 | delete_ctx.filter_groups = false; |
469 | 1 | delete_ctx.column_names = delete_file_col_names; |
470 | 1 | delete_ctx.col_name_to_block_idx = |
471 | 1 | const_cast<std::unordered_map<std::string, uint32_t>*>(&DELETE_COL_NAME_TO_BLOCK_IDX); |
472 | 1 | RETURN_IF_ERROR(parquet_delete_reader.init_reader(&delete_ctx)); |
473 | | |
474 | 0 | const tparquet::FileMetaData* meta_data = parquet_delete_reader.get_meta_data(); |
475 | 0 | bool dictionary_coded = true; |
476 | 0 | for (const auto& row_group : meta_data->row_groups) { |
477 | 0 | const auto& column_chunk = row_group.columns[ICEBERG_FILE_PATH_INDEX]; |
478 | 0 | if (!(column_chunk.__isset.meta_data && has_dict_page(column_chunk.meta_data))) { |
479 | 0 | dictionary_coded = false; |
480 | 0 | break; |
481 | 0 | } |
482 | 0 | } |
483 | 0 | DataTypePtr data_type_file_path = make_nullable(std::make_shared<DataTypeString>()); |
484 | 0 | DataTypePtr data_type_pos = make_nullable(std::make_shared<DataTypeInt64>()); |
485 | 0 | bool eof = false; |
486 | 0 | while (!eof) { |
487 | 0 | Block block = { |
488 | 0 | dictionary_coded |
489 | 0 | ? ColumnWithTypeAndName {ColumnNullable::create(ColumnDictI32::create(), |
490 | 0 | ColumnUInt8::create()), |
491 | 0 | data_type_file_path, ICEBERG_FILE_PATH} |
492 | 0 | : ColumnWithTypeAndName {data_type_file_path, ICEBERG_FILE_PATH}, |
493 | |
|
494 | 0 | {data_type_pos, ICEBERG_ROW_POS}}; |
495 | 0 | size_t read_rows = 0; |
496 | 0 | RETURN_IF_ERROR(parquet_delete_reader.get_next_block(&block, &read_rows, &eof)); |
497 | | |
498 | 0 | if (read_rows <= 0) { |
499 | 0 | break; |
500 | 0 | } |
501 | 0 | RETURN_IF_ERROR(_gen_position_delete_file_range(block, position_delete, read_rows, |
502 | 0 | dictionary_coded)); |
503 | 0 | } |
504 | 0 | return Status::OK(); |
505 | 0 | }; |
506 | | |
507 | | // ============================================================================ |
508 | | // IcebergOrcReader: on_before_init_reader (ORC-specific schema matching) |
509 | | // ============================================================================ |
510 | 94 | Status IcebergOrcReader::on_before_init_reader(ReaderInitContext* ctx) { |
511 | 94 | _column_descs = ctx->column_descs; |
512 | 94 | _fill_col_name_to_block_idx = ctx->col_name_to_block_idx; |
513 | 94 | _file_format = Fileformat::ORC; |
514 | | |
515 | | // Get ORC file type first (available because _create_file_reader() already ran) |
516 | 94 | const orc::Type* orc_type_ptr = nullptr; |
517 | 94 | RETURN_IF_ERROR(this->get_file_type(&orc_type_ptr)); |
518 | | |
519 | | // Build table_info_node by field_id or name matching. |
520 | | // This must happen BEFORE column classification so we can use children_column_exists |
521 | | // to check if a column exists in the file (by field ID, not name). |
522 | 94 | if (!get_scan_params().__isset.history_schema_info || |
523 | 94 | get_scan_params().history_schema_info.empty()) [[unlikely]] { |
524 | 1 | RETURN_IF_ERROR(BuildTableInfoUtil::by_orc_name(ctx->tuple_descriptor, orc_type_ptr, |
525 | 1 | ctx->table_info_node)); |
526 | 93 | } else { |
527 | 93 | RETURN_IF_ERROR(BuildTableInfoUtil::by_orc_field_id_with_name_mapping( |
528 | 93 | get_scan_params().history_schema_info.front().root_field, orc_type_ptr, |
529 | 93 | ICEBERG_ORC_ATTRIBUTE, ctx->table_info_node, |
530 | 93 | supports_iceberg_scan_semantics_v1(&get_scan_params()))); |
531 | 93 | } |
532 | | |
533 | 94 | std::unordered_set<std::string> partition_col_names; |
534 | 94 | if (ctx->range->__isset.columns_from_path_keys) { |
535 | 0 | partition_col_names.insert(ctx->range->columns_from_path_keys.begin(), |
536 | 0 | ctx->range->columns_from_path_keys.end()); |
537 | 0 | } |
538 | | |
539 | | // Single pass: classify columns, detect $row_id, handle partition fallback. |
540 | 94 | bool has_partition_from_path = false; |
541 | 158 | for (const auto& desc : *ctx->column_descs) { |
542 | 158 | if (desc.category == ColumnCategory::SYNTHESIZED) { |
543 | 0 | if (desc.name == BeConsts::ICEBERG_ROWID_COL) { |
544 | 0 | this->register_synthesized_column_handler( |
545 | 0 | BeConsts::ICEBERG_ROWID_COL, [this](Block* block, size_t rows) -> Status { |
546 | 0 | return _fill_iceberg_row_id(block, rows); |
547 | 0 | }); |
548 | 0 | continue; |
549 | 0 | } else if (desc.name.starts_with(BeConsts::GLOBAL_ROWID_COL)) { |
550 | 0 | auto topn_row_id_column_iter = _create_topn_row_id_column_iterator(); |
551 | 0 | this->register_synthesized_column_handler( |
552 | 0 | desc.name, |
553 | 0 | [iter = std::move(topn_row_id_column_iter), this, &desc]( |
554 | 0 | Block* block, size_t rows) -> Status { |
555 | 0 | return fill_topn_row_id(iter, desc.name, block, rows); |
556 | 0 | }); |
557 | 0 | continue; |
558 | 0 | } |
559 | 158 | } else if (desc.category == ColumnCategory::PARTITION_KEY) { |
560 | 0 | bool has_partition_value = partition_col_names.contains(desc.name); |
561 | 0 | bool exists_in_file = ctx->table_info_node->children_column_exists(desc.name); |
562 | 0 | if (!has_partition_value || exists_in_file) { |
563 | 0 | ctx->column_names.push_back(desc.name); |
564 | 0 | continue; |
565 | 0 | } |
566 | 0 | has_partition_from_path = true; |
567 | 158 | } else if (desc.category == ColumnCategory::REGULAR) { |
568 | 158 | ctx->column_names.push_back(desc.name); |
569 | 158 | } else if (desc.category == ColumnCategory::GENERATED) { |
570 | 0 | _init_row_lineage_columns(); |
571 | 0 | if (desc.name == ROW_LINEAGE_ROW_ID) { |
572 | 0 | ctx->column_names.push_back(desc.name); |
573 | 0 | this->register_generated_column_handler( |
574 | 0 | ROW_LINEAGE_ROW_ID, [this](Block* block, size_t rows) -> Status { |
575 | 0 | return _fill_row_lineage_row_id(block, rows); |
576 | 0 | }); |
577 | 0 | continue; |
578 | 0 | } else if (desc.name == ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER) { |
579 | 0 | ctx->column_names.push_back(desc.name); |
580 | 0 | this->register_generated_column_handler( |
581 | 0 | ROW_LINEAGE_LAST_UPDATED_SEQ_NUMBER, |
582 | 0 | [this](Block* block, size_t rows) -> Status { |
583 | 0 | return _fill_row_lineage_last_updated_sequence_number(block, rows); |
584 | 0 | }); |
585 | 0 | continue; |
586 | 0 | } |
587 | 0 | } |
588 | 158 | } |
589 | | |
590 | 94 | if (has_partition_from_path) { |
591 | 0 | RETURN_IF_ERROR(_extract_partition_values(*ctx->range, ctx->tuple_descriptor, |
592 | 0 | _fill_partition_values, |
593 | 0 | &_fill_partition_value_is_null)); |
594 | 0 | } |
595 | | |
596 | 94 | _all_required_col_names = ctx->column_names; |
597 | | |
598 | | // Create column IDs from ORC type |
599 | 94 | auto column_id_result = |
600 | 94 | _create_column_ids(orc_type_ptr, ctx->tuple_descriptor, ctx->table_info_node); |
601 | 94 | ctx->column_ids = std::move(column_id_result.column_ids); |
602 | 94 | ctx->filter_column_ids = std::move(column_id_result.filter_column_ids); |
603 | | |
604 | | // Build field_id -> block_column_name mapping for equality delete filtering. |
605 | 158 | for (const auto* slot : ctx->tuple_descriptor->slots()) { |
606 | 158 | _id_to_block_column_name.emplace(slot->col_unique_id(), slot->col_name()); |
607 | 158 | } |
608 | | |
609 | | // Process delete files (must happen before _do_init_reader so expand col IDs are included) |
610 | 94 | RETURN_IF_ERROR(_init_row_filters()); |
611 | | |
612 | | // Add expand column IDs for equality delete and remap expand column names |
613 | | // (matching master's behavior with __equality_delete_column__ prefix) |
614 | 94 | const static std::string EQ_DELETE_PRE = "__equality_delete_column__"; |
615 | 94 | std::unordered_map<int, const orc::Type*> field_id_to_file_column; |
616 | 94 | bool all_file_columns_have_field_ids = true; |
617 | 339 | for (uint64_t i = 0; i < orc_type_ptr->getSubtypeCount(); ++i) { |
618 | 245 | const orc::Type* sub_type = orc_type_ptr->getSubtype(i); |
619 | 245 | if (sub_type->hasAttributeKey(ICEBERG_ORC_ATTRIBUTE)) { |
620 | 244 | int fid = std::stoi(sub_type->getAttributeValue(ICEBERG_ORC_ATTRIBUTE)); |
621 | 244 | field_id_to_file_column[fid] = sub_type; |
622 | 244 | } else { |
623 | 1 | all_file_columns_have_field_ids = false; |
624 | 1 | } |
625 | 245 | } |
626 | 94 | const bool use_field_ids_for_hidden_keys = |
627 | 94 | supports_iceberg_scan_semantics_v1(&get_scan_params()) |
628 | 94 | ? orc_subtree_has_iceberg_id(orc_type_ptr, ICEBERG_ORC_ATTRIBUTE) |
629 | 94 | : all_file_columns_have_field_ids; |
630 | 94 | const auto struct_node = |
631 | 94 | std::dynamic_pointer_cast<TableSchemaChangeHelper::StructNode>(ctx->table_info_node); |
632 | 94 | DORIS_CHECK(struct_node != nullptr); |
633 | | |
634 | 94 | std::vector<std::string> new_expand_col_names; |
635 | 96 | for (size_t i = 0; i < _expand_col_names.size(); ++i) { |
636 | 2 | const auto& old_name = _expand_col_names[i]; |
637 | 2 | int field_id = -1; |
638 | 2 | for (auto& [fid, name] : _id_to_block_column_name) { |
639 | 2 | if (name == old_name) { |
640 | 2 | field_id = fid; |
641 | 2 | break; |
642 | 2 | } |
643 | 2 | } |
644 | | |
645 | 2 | const orc::Type* file_column = nullptr; |
646 | 2 | if (use_field_ids_for_hidden_keys) { |
647 | 2 | auto id_it = field_id_to_file_column.find(field_id); |
648 | 2 | if (id_it != field_id_to_file_column.end()) { |
649 | 1 | file_column = id_it->second; |
650 | 1 | } |
651 | 2 | } else { |
652 | 0 | std::string table_column_name = old_name; |
653 | 0 | if (const auto* table_field = _find_current_schema_field(field_id); |
654 | 0 | table_field != nullptr && table_field->__isset.name) { |
655 | 0 | table_column_name = table_field->name; |
656 | 0 | } |
657 | | // The mapping node uses current names even when an older delete file does not. |
658 | 0 | if (struct_node->get_children().contains(table_column_name) && |
659 | 0 | struct_node->children_column_exists(table_column_name)) { |
660 | 0 | const auto& mapped_name = struct_node->children_file_column_name(table_column_name); |
661 | 0 | for (uint64_t j = 0; j < orc_type_ptr->getSubtypeCount(); ++j) { |
662 | 0 | if (orc_type_ptr->getFieldName(j) == mapped_name) { |
663 | 0 | file_column = orc_type_ptr->getSubtype(j); |
664 | 0 | break; |
665 | 0 | } |
666 | 0 | } |
667 | 0 | DORIS_CHECK(file_column != nullptr); |
668 | 0 | } |
669 | 0 | } |
670 | | |
671 | 2 | std::string file_col_name = old_name; |
672 | 2 | if (file_column != nullptr) { |
673 | 3 | for (uint64_t j = 0; j < orc_type_ptr->getSubtypeCount(); ++j) { |
674 | 3 | if (orc_type_ptr->getSubtype(j) == file_column) { |
675 | 1 | file_col_name = orc_type_ptr->getFieldName(j); |
676 | 1 | break; |
677 | 1 | } |
678 | 3 | } |
679 | 1 | } |
680 | 2 | std::string table_col_name = EQ_DELETE_PRE + file_col_name; |
681 | | |
682 | 2 | if (field_id >= 0) { |
683 | 2 | _id_to_block_column_name[field_id] = table_col_name; |
684 | 2 | } |
685 | 2 | if (i < _expand_columns.size()) { |
686 | 2 | _expand_columns[i].name = table_col_name; |
687 | 2 | } |
688 | 2 | if (file_column == nullptr) { |
689 | 1 | DORIS_CHECK(i < _expand_columns.size()); |
690 | 1 | RETURN_IF_ERROR(_register_missing_equality_delete_column(field_id, table_col_name, |
691 | 1 | _expand_columns[i].type)); |
692 | | // The old data file predates this equality key. Keep it in the expand block so the |
693 | | // synthesized-column hook can materialize its logical initial default before ORC's |
694 | | // block-size checks. Adding it to column_names/table_info_node would mark it as an |
695 | | // existing ORC child and make OrcReader read a column that is not present in the file. |
696 | 1 | new_expand_col_names.push_back(table_col_name); |
697 | 1 | continue; |
698 | 1 | } |
699 | 1 | new_expand_col_names.push_back(table_col_name); |
700 | | |
701 | | // Add column IDs |
702 | 1 | ctx->column_ids.insert(file_column->getColumnId()); |
703 | | |
704 | 1 | ctx->column_names.push_back(table_col_name); |
705 | 1 | ctx->table_info_node->add_children(table_col_name, file_col_name, |
706 | 1 | TableSchemaChangeHelper::ConstNode::get_instance()); |
707 | 1 | } |
708 | 94 | _expand_col_names = std::move(new_expand_col_names); |
709 | | |
710 | 94 | return Status::OK(); |
711 | 94 | } |
712 | | |
713 | | // ============================================================================ |
714 | | // IcebergOrcReader: _create_column_ids |
715 | | // ============================================================================ |
716 | | ColumnIdResult IcebergOrcReader::_create_column_ids( |
717 | | const orc::Type* orc_type, const TupleDescriptor* tuple_descriptor, |
718 | 101 | const std::shared_ptr<TableSchemaChangeHelper::Node>& table_info_node) { |
719 | 101 | std::unordered_map<int, const orc::Type*> iceberg_id_to_orc_type_map; |
720 | 396 | for (uint64_t i = 0; i < orc_type->getSubtypeCount(); ++i) { |
721 | 295 | const auto* orc_sub_type = orc_type->getSubtype(i); |
722 | 295 | if (!orc_sub_type) { |
723 | 0 | continue; |
724 | 0 | } |
725 | 295 | if (!orc_sub_type->hasAttributeKey(ICEBERG_ORC_ATTRIBUTE)) { |
726 | 2 | continue; |
727 | 2 | } |
728 | 293 | int iceberg_id = std::stoi(orc_sub_type->getAttributeValue(ICEBERG_ORC_ATTRIBUTE)); |
729 | 293 | iceberg_id_to_orc_type_map[iceberg_id] = orc_sub_type; |
730 | 293 | } |
731 | | |
732 | 101 | std::set<uint64_t> column_ids; |
733 | 101 | std::set<uint64_t> filter_column_ids; |
734 | | |
735 | 101 | auto process_access_paths = [](const orc::Type* orc_field, |
736 | 101 | const std::vector<TColumnAccessPath>& access_paths, |
737 | 101 | std::set<uint64_t>& out_ids) { |
738 | 16 | process_nested_access_paths( |
739 | 16 | orc_field, access_paths, out_ids, |
740 | 16 | [](const orc::Type* type) { return type->getColumnId(); }, |
741 | 16 | [](const orc::Type* type) { return type->getMaximumColumnId(); }, |
742 | 16 | IcebergOrcNestedColumnUtils::extract_nested_column_ids); |
743 | 16 | }; |
744 | | |
745 | | // The Iceberg schema-mapping root is a StructNode whose registered children are the real |
746 | | // table columns. When present, resolve each column by name through it so the column-id set |
747 | | // stays consistent with the schema-mapping decision (BY_ID or BY_NAME/name-mapping); |
748 | | // otherwise fall back to matching by Iceberg field id. |
749 | 101 | const auto* struct_node = |
750 | 101 | dynamic_cast<const TableSchemaChangeHelper::StructNode*>(table_info_node.get()); |
751 | | |
752 | 173 | for (const auto* slot : tuple_descriptor->slots()) { |
753 | 173 | const orc::Type* orc_field = nullptr; |
754 | 173 | if (struct_node != nullptr) { |
755 | | // Synthesized/metadata slots (e.g. the TopN global row-id or the $row_id column) are |
756 | | // never registered as children, so check membership before querying: calling |
757 | | // children_column_exists() on an unregistered name DCHECK-aborts in debug builds and |
758 | | // throws std::out_of_range from .at() in release builds. |
759 | 160 | if (struct_node->get_children().contains(slot->col_name()) && |
760 | 160 | struct_node->children_column_exists(slot->col_name())) { |
761 | | // Select the physical child resolved by the shared schema-mapping pass. Hidden |
762 | | // equality keys and projected columns must obey the same BY_NAME decision for |
763 | | // partial-id ORC files. |
764 | 141 | const auto& file_column_name = |
765 | 141 | struct_node->children_file_column_name(slot->col_name()); |
766 | 331 | for (uint64_t i = 0; i < orc_type->getSubtypeCount(); ++i) { |
767 | 331 | if (orc_type->getFieldName(i) == file_column_name) { |
768 | 141 | orc_field = orc_type->getSubtype(i); |
769 | 141 | break; |
770 | 141 | } |
771 | 331 | } |
772 | 141 | DORIS_CHECK(orc_field != nullptr); |
773 | 141 | } |
774 | 160 | } else { |
775 | 13 | auto it = iceberg_id_to_orc_type_map.find(slot->col_unique_id()); |
776 | 13 | if (it != iceberg_id_to_orc_type_map.end()) { |
777 | 13 | orc_field = it->second; |
778 | 13 | } |
779 | 13 | } |
780 | 173 | if (orc_field == nullptr) { |
781 | 19 | continue; |
782 | 19 | } |
783 | | |
784 | 154 | if ((slot->col_type() != TYPE_STRUCT && slot->col_type() != TYPE_ARRAY && |
785 | 154 | slot->col_type() != TYPE_MAP)) { |
786 | 144 | column_ids.insert(orc_field->getColumnId()); |
787 | 144 | if (slot->is_predicate()) { |
788 | 0 | filter_column_ids.insert(orc_field->getColumnId()); |
789 | 0 | } |
790 | 144 | continue; |
791 | 144 | } |
792 | | |
793 | 10 | const auto& all_access_paths = slot->all_access_paths(); |
794 | 10 | process_access_paths(orc_field, all_access_paths, column_ids); |
795 | | |
796 | 10 | const auto& predicate_access_paths = slot->predicate_access_paths(); |
797 | 10 | if (!predicate_access_paths.empty()) { |
798 | 6 | process_access_paths(orc_field, predicate_access_paths, filter_column_ids); |
799 | 6 | } |
800 | 10 | } |
801 | | |
802 | 101 | return {std::move(column_ids), std::move(filter_column_ids)}; |
803 | 101 | } |
804 | | |
805 | | // ============================================================================ |
806 | | // IcebergOrcReader: _read_position_delete_file |
807 | | // ============================================================================ |
808 | | Status IcebergOrcReader::_read_position_delete_file(const TFileRangeDesc* delete_range, |
809 | 0 | DeleteFile* position_delete) { |
810 | 0 | OrcReader orc_delete_reader(get_profile(), get_state(), get_scan_params(), *delete_range, |
811 | 0 | READ_DELETE_FILE_BATCH_SIZE, get_state()->timezone(), get_io_ctx(), |
812 | 0 | _meta_cache); |
813 | 0 | OrcInitContext delete_ctx; |
814 | 0 | delete_ctx.column_names = delete_file_col_names; |
815 | 0 | delete_ctx.col_name_to_block_idx = |
816 | 0 | const_cast<std::unordered_map<std::string, uint32_t>*>(&DELETE_COL_NAME_TO_BLOCK_IDX); |
817 | 0 | RETURN_IF_ERROR(orc_delete_reader.init_reader(&delete_ctx)); |
818 | | |
819 | 0 | bool eof = false; |
820 | 0 | DataTypePtr data_type_file_path {new DataTypeString}; |
821 | 0 | DataTypePtr data_type_pos {new DataTypeInt64}; |
822 | 0 | while (!eof) { |
823 | 0 | Block block = {{data_type_file_path, ICEBERG_FILE_PATH}, {data_type_pos, ICEBERG_ROW_POS}}; |
824 | |
|
825 | 0 | size_t read_rows = 0; |
826 | 0 | RETURN_IF_ERROR(orc_delete_reader.get_next_block(&block, &read_rows, &eof)); |
827 | | |
828 | 0 | RETURN_IF_ERROR(_gen_position_delete_file_range(block, position_delete, read_rows, false)); |
829 | 0 | } |
830 | 0 | return Status::OK(); |
831 | 0 | } |
832 | | |
833 | | } // namespace doris |