be/src/format_v2/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_v2/table/iceberg_reader.h" |
19 | | |
20 | | #include <algorithm> |
21 | | #include <memory> |
22 | | #include <sstream> |
23 | | #include <utility> |
24 | | |
25 | | #include "common/cast_set.h" |
26 | | #include "common/consts.h" |
27 | | #include "core/assert_cast.h" |
28 | | #include "core/block/block.h" |
29 | | #include "core/column/column_const.h" |
30 | | #include "core/column/column_nullable.h" |
31 | | #include "core/column/column_string.h" |
32 | | #include "core/column/column_struct.h" |
33 | | #include "core/column/column_vector.h" |
34 | | #include "core/data_type/data_type_array.h" |
35 | | #include "core/data_type/data_type_map.h" |
36 | | #include "core/data_type/data_type_number.h" |
37 | | #include "core/data_type/data_type_struct.h" |
38 | | #include "core/data_type/define_primitive_type.h" |
39 | | #include "core/field.h" |
40 | | #include "exprs/vliteral.h" |
41 | | #include "exprs/vslot_ref.h" |
42 | | #include "format/table/deletion_vector_reader.h" |
43 | | #include "format_v2/expr/cast.h" |
44 | | #include "format_v2/expr/equality_delete_predicate.h" |
45 | | #include "format_v2/orc/orc_reader.h" |
46 | | #include "format_v2/parquet/parquet_reader.h" |
47 | | #include "format_v2/parquet/reader/column_reader.h" |
48 | | #include "format_v2/table_reader.h" |
49 | | #include "io/file_factory.h" |
50 | | #include "util/debug_points.h" |
51 | | #include "util/url_coding.h" |
52 | | |
53 | | namespace doris::format::iceberg { |
54 | | |
55 | | static constexpr const char* ROW_LINEAGE_ROW_ID = "_row_id"; |
56 | | static constexpr int32_t ROW_LINEAGE_ROW_ID_FIELD_ID = 2147483540; |
57 | | |
58 | | namespace { |
59 | | |
60 | 6 | bool contains_variant_type(const DataTypePtr& input) { |
61 | 6 | if (input == nullptr) { |
62 | 6 | return false; |
63 | 6 | } |
64 | 0 | const auto type = remove_nullable(input); |
65 | 0 | switch (type->get_primitive_type()) { |
66 | 0 | case TYPE_VARIANT: |
67 | 0 | return true; |
68 | 0 | case TYPE_ARRAY: |
69 | 0 | return contains_variant_type(assert_cast<const DataTypeArray&>(*type).get_nested_type()); |
70 | 0 | case TYPE_MAP: { |
71 | 0 | const auto& map = assert_cast<const DataTypeMap&>(*type); |
72 | 0 | return contains_variant_type(map.get_key_type()) || |
73 | 0 | contains_variant_type(map.get_value_type()); |
74 | 0 | } |
75 | 0 | case TYPE_STRUCT: |
76 | 0 | return std::ranges::any_of(assert_cast<const DataTypeStruct&>(*type).get_elements(), |
77 | 0 | contains_variant_type); |
78 | 0 | default: |
79 | 0 | return false; |
80 | 0 | } |
81 | 0 | } |
82 | | |
83 | 8 | bool mapping_reads_variant(const format::ColumnMapping& mapping) { |
84 | 8 | if (!mapping.file_local_id.has_value()) { |
85 | 2 | return false; |
86 | 2 | } |
87 | 6 | if (contains_variant_type(mapping.original_file_type)) { |
88 | 0 | return true; |
89 | 0 | } |
90 | 6 | if (mapping.table_type != nullptr && |
91 | 6 | remove_nullable(mapping.table_type)->get_primitive_type() == TYPE_VARIANT) { |
92 | 2 | return true; |
93 | 2 | } |
94 | 4 | return std::ranges::any_of(mapping.child_mappings, mapping_reads_variant); |
95 | 6 | } |
96 | | |
97 | 2 | const char* file_format_name(FileFormat format) { |
98 | 2 | switch (format) { |
99 | 0 | case FileFormat::PARQUET: |
100 | 0 | return "PARQUET"; |
101 | 2 | case FileFormat::ORC: |
102 | 2 | return "ORC"; |
103 | 0 | case FileFormat::CSV: |
104 | 0 | return "CSV"; |
105 | 0 | case FileFormat::JSON: |
106 | 0 | return "JSON"; |
107 | 0 | case FileFormat::TEXT: |
108 | 0 | return "TEXT"; |
109 | 0 | case FileFormat::JNI: |
110 | 0 | return "JNI"; |
111 | 0 | case FileFormat::NATIVE: |
112 | 0 | return "NATIVE"; |
113 | 0 | case FileFormat::ARROW: |
114 | 0 | return "ARROW"; |
115 | 0 | case FileFormat::WAL: |
116 | 0 | return "WAL"; |
117 | 2 | } |
118 | 0 | return "UNKNOWN"; |
119 | 2 | } |
120 | | |
121 | | } // namespace |
122 | | |
123 | | Status IcebergTableReader::validate_variant_file_mappings( |
124 | 52 | FileFormat format, const std::vector<format::ColumnMapping>& mappings) { |
125 | 52 | if (format == FileFormat::PARQUET || !std::ranges::any_of(mappings, mapping_reads_variant)) { |
126 | 50 | return Status::OK(); |
127 | 50 | } |
128 | | // Gate on a physical mapping, not the table schema: an older ORC/Avro file may legitimately |
129 | | // omit a Variant field added by schema evolution, in which case the mapper synthesizes NULL. |
130 | 2 | return Status::NotSupported( |
131 | 2 | "Iceberg Variant is supported only for Parquet files in FileScannerV2; file format {} " |
132 | 2 | "(including ORC/Avro readers) is not supported", |
133 | 2 | file_format_name(format)); |
134 | 52 | } |
135 | | |
136 | 47 | Status IcebergTableReader::validate_file_mapping(const format::TableColumnMapper& mapper) const { |
137 | 47 | if (_push_down_agg_type == TPushAggOp::type::COUNT && _push_down_count_columns.has_value() && |
138 | 47 | _push_down_count_columns->empty()) { |
139 | | // COUNT(*) may retain an arbitrary minimum-width slot, but that carrier is never a |
140 | | // semantic physical read and must not trigger the Variant file-format capability gate. |
141 | 0 | return Status::OK(); |
142 | 0 | } |
143 | 47 | return validate_variant_file_mappings(_format, mapper.mappings()); |
144 | 47 | } |
145 | | |
146 | | template <typename T> |
147 | 0 | static std::string join_values_for_debug(const std::vector<T>& values) { |
148 | 0 | std::ostringstream out; |
149 | 0 | out << "["; |
150 | 0 | for (size_t idx = 0; idx < values.size(); ++idx) { |
151 | 0 | if (idx > 0) { |
152 | 0 | out << ", "; |
153 | 0 | } |
154 | 0 | out << values[idx]; |
155 | 0 | } |
156 | 0 | out << "]"; |
157 | 0 | return out.str(); |
158 | 0 | } |
159 | | |
160 | 1 | static bool is_projected_row_lineage_row_id(const format::ColumnDefinition& column) { |
161 | | // Iceberg row lineage columns can be bound by field id when a mapper has already been built, |
162 | | // but customize_file_scan_request() is also exercised directly by scan-request tests before the |
163 | | // mapper exists. In that path, inspect the projected table schema so row-position dependencies |
164 | | // are still added for `_row_id`. |
165 | 1 | return column.name == ROW_LINEAGE_ROW_ID || |
166 | 1 | (column.has_identifier_field_id() && |
167 | 0 | column.get_identifier_field_id() == ROW_LINEAGE_ROW_ID_FIELD_ID); |
168 | 1 | } |
169 | | |
170 | 43 | static bool is_projected_iceberg_rowid(const format::ColumnDefinition& column) { |
171 | 43 | return column.name == BeConsts::ICEBERG_ROWID_COL; |
172 | 43 | } |
173 | | |
174 | | static Status build_missing_equality_delete_key_expr(const format::ColumnDefinition& table_field, |
175 | | const DataTypePtr& delete_key_type, |
176 | 7 | VExprSPtr* key_expr) { |
177 | 7 | DORIS_CHECK(delete_key_type != nullptr); |
178 | 7 | DORIS_CHECK(key_expr != nullptr); |
179 | 7 | if (!table_field.initial_default_value.has_value()) { |
180 | | // A newly added optional field without an initial default is logically NULL in older |
181 | | // files. EqualityDeletePredicate treats NULL == NULL as a match. |
182 | 2 | *key_expr = VLiteral::create_shared(make_nullable(delete_key_type), Field()); |
183 | 2 | return Status::OK(); |
184 | 2 | } |
185 | | |
186 | 5 | VExprSPtr literal; |
187 | 5 | if (table_field.initial_default_value_is_base64 || |
188 | 5 | table_field.type->get_primitive_type() == TYPE_VARBINARY) { |
189 | | // New FE versions mark every Iceberg UUID/BINARY/FIXED default as Base64 regardless of its |
190 | | // Doris mapping. Keep the VARBINARY fallback for scan descriptors produced before that |
191 | | // marker existed. Decode before parsing so STRING/CHAR and VARBINARY all compare against |
192 | | // the raw bytes stored in equality-delete files. |
193 | 3 | std::string decoded_default; |
194 | 3 | if (!base64_decode(*table_field.initial_default_value, &decoded_default)) { |
195 | 0 | return Status::InvalidArgument("Invalid Base64 Iceberg initial default for field {}", |
196 | 0 | table_field.name); |
197 | 0 | } |
198 | 3 | if (table_field.type->get_primitive_type() == TYPE_VARBINARY) { |
199 | 1 | const auto initial_default = |
200 | 1 | Field::create_field<TYPE_VARBINARY>(StringView(decoded_default)); |
201 | | // VLiteral must copy the borrowed StringView while decoded_default is alive; UUID and |
202 | | // long FIXED defaults otherwise retain a pointer into freed decode storage. |
203 | 1 | literal = VLiteral::create_shared(table_field.type, initial_default); |
204 | 2 | } else { |
205 | 2 | DORIS_CHECK(is_string_type(table_field.type->get_primitive_type())); |
206 | 2 | literal = VLiteral::create_shared(table_field.type, |
207 | 2 | Field::create_field<TYPE_STRING>(decoded_default)); |
208 | 2 | } |
209 | 3 | } else { |
210 | | // An added field's initial default is its logical value in every older data file that lacks |
211 | | // the physical column. FE normalizes the string for the current Doris table type. |
212 | 2 | Field initial_default; |
213 | 2 | RETURN_IF_ERROR(table_field.type->get_serde()->from_fe_string( |
214 | 2 | *table_field.initial_default_value, initial_default)); |
215 | 2 | literal = VLiteral::create_shared(table_field.type, initial_default); |
216 | 2 | } |
217 | | |
218 | 5 | DORIS_CHECK(literal != nullptr); |
219 | 5 | if (table_field.type->equals(*delete_key_type)) { |
220 | 1 | *key_expr = std::move(literal); |
221 | 1 | return Status::OK(); |
222 | 1 | } |
223 | 4 | auto cast_expr = Cast::create_shared(delete_key_type); |
224 | 4 | cast_expr->add_child(std::move(literal)); |
225 | 4 | *key_expr = std::move(cast_expr); |
226 | 4 | return Status::OK(); |
227 | 5 | } |
228 | | |
229 | 0 | static std::string iceberg_delete_file_debug_string(const TIcebergDeleteFileDesc& delete_file) { |
230 | 0 | std::ostringstream out; |
231 | 0 | out << "TIcebergDeleteFileDesc{path=" << (delete_file.__isset.path ? delete_file.path : "null") |
232 | 0 | << ", content=" << (delete_file.__isset.content ? delete_file.content : -1) |
233 | 0 | << ", file_format=" |
234 | 0 | << (delete_file.__isset.file_format ? static_cast<int>(delete_file.file_format) : -1) |
235 | 0 | << ", position_lower_bound=" |
236 | 0 | << (delete_file.__isset.position_lower_bound ? delete_file.position_lower_bound : -1) |
237 | 0 | << ", position_upper_bound=" |
238 | 0 | << (delete_file.__isset.position_upper_bound ? delete_file.position_upper_bound : -1) |
239 | 0 | << ", field_ids=" |
240 | 0 | << (delete_file.__isset.field_ids ? join_values_for_debug(delete_file.field_ids) : "[]") |
241 | 0 | << ", content_offset=" |
242 | 0 | << (delete_file.__isset.content_offset ? delete_file.content_offset : -1) |
243 | 0 | << ", content_size_in_bytes=" |
244 | 0 | << (delete_file.__isset.content_size_in_bytes ? delete_file.content_size_in_bytes : -1) |
245 | 0 | << "}"; |
246 | 0 | return out.str(); |
247 | 0 | } |
248 | | |
249 | | static std::string iceberg_delete_files_debug_string( |
250 | 0 | const std::vector<TIcebergDeleteFileDesc>& delete_files) { |
251 | 0 | std::ostringstream out; |
252 | 0 | out << "["; |
253 | 0 | for (size_t idx = 0; idx < delete_files.size(); ++idx) { |
254 | 0 | if (idx > 0) { |
255 | 0 | out << ", "; |
256 | 0 | } |
257 | 0 | out << iceberg_delete_file_debug_string(delete_files[idx]); |
258 | 0 | } |
259 | 0 | out << "]"; |
260 | 0 | return out.str(); |
261 | 0 | } |
262 | | |
263 | 0 | static std::string iceberg_params_debug_string(const std::optional<TIcebergFileDesc>& params) { |
264 | 0 | if (!params.has_value()) { |
265 | 0 | return "null"; |
266 | 0 | } |
267 | 0 | const auto& iceberg_params = *params; |
268 | 0 | std::ostringstream out; |
269 | 0 | out << "TIcebergFileDesc{format_version=" |
270 | 0 | << (iceberg_params.__isset.format_version ? iceberg_params.format_version : -1) |
271 | 0 | << ", content=" << (iceberg_params.__isset.content ? iceberg_params.content : -1) |
272 | 0 | << ", original_file_path=" |
273 | 0 | << (iceberg_params.__isset.original_file_path ? iceberg_params.original_file_path : "null") |
274 | 0 | << ", row_count=" << (iceberg_params.__isset.row_count ? iceberg_params.row_count : -1) |
275 | 0 | << ", partition_spec_id=" |
276 | 0 | << (iceberg_params.__isset.partition_spec_id ? iceberg_params.partition_spec_id : 0) |
277 | 0 | << ", has_partition_data_json=" << iceberg_params.__isset.partition_data_json |
278 | 0 | << ", first_row_id=" |
279 | 0 | << (iceberg_params.__isset.first_row_id ? iceberg_params.first_row_id : -1) |
280 | 0 | << ", last_updated_sequence_number=" |
281 | 0 | << (iceberg_params.__isset.last_updated_sequence_number |
282 | 0 | ? iceberg_params.last_updated_sequence_number |
283 | 0 | : -1) |
284 | 0 | << ", delete_file_count=" |
285 | 0 | << (iceberg_params.__isset.delete_files ? iceberg_params.delete_files.size() : 0) |
286 | 0 | << ", delete_files=" |
287 | 0 | << (iceberg_params.__isset.delete_files |
288 | 0 | ? iceberg_delete_files_debug_string(iceberg_params.delete_files) |
289 | 0 | : "[]") |
290 | 0 | << ", has_serialized_split=" << iceberg_params.__isset.serialized_split << "}"; |
291 | 0 | return out.str(); |
292 | 0 | } |
293 | | |
294 | | IcebergTableReader::PositionDeleteRowsCollector::PositionDeleteRowsCollector( |
295 | | PositionDeleteFile* rows_by_data_file) |
296 | 12 | : _rows_by_data_file(rows_by_data_file) { |
297 | 12 | DORIS_CHECK(_rows_by_data_file != nullptr); |
298 | 12 | } |
299 | | |
300 | | Status IcebergTableReader::PositionDeleteRowsCollector::collect(const Block& block, |
301 | 22 | size_t read_rows) { |
302 | 22 | if (read_rows == 0) { |
303 | 10 | return Status::OK(); |
304 | 10 | } |
305 | 12 | const auto& file_path_column_ptr = |
306 | 12 | block.get_by_position(ICEBERG_FILE_PATH_BLOCK_POSITION).column; |
307 | 12 | const auto& pos_column_ptr = block.get_by_position(ICEBERG_ROW_POS_BLOCK_POSITION).column; |
308 | 12 | if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*file_path_column_ptr); |
309 | 12 | nullable_column != nullptr && nullable_column->has_null(0, read_rows)) { |
310 | 1 | return Status::Corruption("Iceberg position delete column file_path contains null values"); |
311 | 1 | } |
312 | 11 | if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*pos_column_ptr); |
313 | 11 | nullable_column != nullptr && nullable_column->has_null(0, read_rows)) { |
314 | 1 | return Status::Corruption("Iceberg position delete column pos contains null values"); |
315 | 1 | } |
316 | 10 | const auto& file_path_column = |
317 | 10 | assert_cast<const ColumnString&>(*remove_nullable(file_path_column_ptr)); |
318 | 10 | const auto& pos_column = assert_cast<const ColumnInt64&>(*remove_nullable(pos_column_ptr)); |
319 | 26 | for (size_t row = 0; row < read_rows; ++row) { |
320 | 16 | const auto file_path = file_path_column.get_data_at(row).to_string(); |
321 | 16 | (*_rows_by_data_file)[file_path].push_back(pos_column.get_element(row)); |
322 | 16 | } |
323 | 10 | return Status::OK(); |
324 | 11 | } |
325 | | |
326 | 55 | Status IcebergTableReader::prepare_split(const format::SplitReadOptions& options) { |
327 | 55 | { |
328 | 55 | SCOPED_TIMER(_profile.total_timer); |
329 | 55 | SCOPED_TIMER(_profile.prepare_split_timer); |
330 | 55 | _row_lineage_columns = {}; |
331 | 55 | _iceberg_params.reset(); |
332 | 55 | _delete_predicates_initialized = false; |
333 | 55 | _position_delete_rows_storage.clear(); |
334 | 55 | _equality_delete_filters.clear(); |
335 | 55 | _split_cache = options.cache; |
336 | 55 | if (options.current_range.__isset.table_format_params && |
337 | 55 | options.current_range.table_format_params.__isset.iceberg_params) { |
338 | 53 | const auto& iceberg_params = options.current_range.table_format_params.iceberg_params; |
339 | 53 | _iceberg_params = iceberg_params; |
340 | 53 | if (iceberg_params.__isset.first_row_id) { |
341 | 9 | _row_lineage_columns.first_row_id = iceberg_params.first_row_id; |
342 | 9 | } |
343 | 53 | if (iceberg_params.__isset.last_updated_sequence_number) { |
344 | 6 | _row_lineage_columns.last_updated_sequence_number = |
345 | 6 | iceberg_params.last_updated_sequence_number; |
346 | 6 | } |
347 | 53 | } |
348 | 55 | } |
349 | 55 | RETURN_IF_ERROR(TableReader::prepare_split(options)); |
350 | 52 | SCOPED_TIMER(_profile.total_timer); |
351 | 52 | SCOPED_TIMER(_profile.prepare_split_timer); |
352 | 52 | if (current_split_pruned()) { |
353 | 0 | return Status::OK(); |
354 | 0 | } |
355 | | // Iceberg data files are immutable once referenced by a snapshot; updates create new data files |
356 | | // at new paths instead of overwriting existing files. This lets the Parquet V2 reader use page |
357 | | // cache when the scan range does not carry an mtime, without extending V1's path::0 behavior to |
358 | | // mutable Hive/local files. |
359 | 52 | mark_current_data_file_immutable(); |
360 | 52 | if (_is_table_level_count_active()) { |
361 | 1 | return Status::OK(); |
362 | 1 | } |
363 | 51 | DBUG_EXECUTE_IF("IcebergTableReader.prepare_split.before_delete_file_scan", |
364 | 51 | DBUG_RUN_CALLBACK()); |
365 | 51 | RETURN_IF_ERROR(_init_delete_predicates(options.current_range.table_format_params)); |
366 | 49 | return Status::OK(); |
367 | 51 | } |
368 | | |
369 | 0 | std::string IcebergTableReader::debug_string() const { |
370 | 0 | size_t position_delete_file_count = 0; |
371 | 0 | size_t equality_delete_file_count = 0; |
372 | 0 | size_t deletion_vector_file_count = 0; |
373 | 0 | if (_iceberg_params.has_value() && _iceberg_params->__isset.delete_files) { |
374 | 0 | for (const auto& delete_file : _iceberg_params->delete_files) { |
375 | 0 | if (!delete_file.__isset.content) { |
376 | 0 | continue; |
377 | 0 | } |
378 | 0 | if (delete_file.content == POSITION_DELETE) { |
379 | 0 | ++position_delete_file_count; |
380 | 0 | } else if (delete_file.content == EQUALITY_DELETE) { |
381 | 0 | ++equality_delete_file_count; |
382 | 0 | } else if (delete_file.content == DELETION_VECTOR) { |
383 | 0 | ++deletion_vector_file_count; |
384 | 0 | } |
385 | 0 | } |
386 | 0 | } |
387 | |
|
388 | 0 | std::ostringstream equality_filters; |
389 | 0 | equality_filters << "["; |
390 | 0 | for (size_t idx = 0; idx < _equality_delete_filters.size(); ++idx) { |
391 | 0 | if (idx > 0) { |
392 | 0 | equality_filters << ", "; |
393 | 0 | } |
394 | 0 | const auto& filter = _equality_delete_filters[idx]; |
395 | 0 | equality_filters << "EqualityDeleteFilter{field_ids=" |
396 | 0 | << join_values_for_debug(filter.field_ids) << ", key_types=["; |
397 | 0 | for (size_t type_idx = 0; type_idx < filter.key_types.size(); ++type_idx) { |
398 | 0 | if (type_idx > 0) { |
399 | 0 | equality_filters << ", "; |
400 | 0 | } |
401 | 0 | equality_filters << (filter.key_types[type_idx] == nullptr |
402 | 0 | ? "null" |
403 | 0 | : filter.key_types[type_idx]->get_name()); |
404 | 0 | } |
405 | 0 | equality_filters << "], delete_block_rows=" << filter.delete_block.rows() |
406 | 0 | << ", delete_block_columns=" << filter.delete_block.columns() << "}"; |
407 | 0 | } |
408 | 0 | equality_filters << "]"; |
409 | |
|
410 | 0 | std::ostringstream out; |
411 | 0 | out << "IcebergTableReader{base=" << format::TableReader::debug_string() |
412 | 0 | << ", iceberg_params=" << iceberg_params_debug_string(_iceberg_params) |
413 | 0 | << ", row_lineage_first_row_id=" << _row_lineage_columns.first_row_id |
414 | 0 | << ", row_lineage_last_updated_sequence_number=" |
415 | 0 | << _row_lineage_columns.last_updated_sequence_number |
416 | 0 | << ", need_row_lineage_row_id=" << _need_row_lineage_row_id() |
417 | 0 | << ", need_iceberg_rowid=" << _need_iceberg_rowid() |
418 | 0 | << ", row_position_block_position=" << _row_position_block_position |
419 | 0 | << ", delete_predicates_initialized=" << _delete_predicates_initialized |
420 | 0 | << ", position_delete_file_count=" << position_delete_file_count |
421 | 0 | << ", equality_delete_file_count=" << equality_delete_file_count |
422 | 0 | << ", deletion_vector_file_count=" << deletion_vector_file_count |
423 | 0 | << ", position_delete_rows_storage_count=" << _position_delete_rows_storage.size() |
424 | 0 | << ", equality_delete_filter_count=" << _equality_delete_filters.size() |
425 | 0 | << ", equality_delete_filters=" << equality_filters.str() << "}"; |
426 | 0 | return out.str(); |
427 | 0 | } |
428 | | |
429 | 41 | Status IcebergTableReader::materialize_virtual_columns(Block* table_block) { |
430 | 100 | for (size_t column_idx = 0; column_idx < _data_reader.column_mapper->mappings().size(); |
431 | 59 | ++column_idx) { |
432 | 59 | const auto& mapping = _data_reader.column_mapper->mappings()[column_idx]; |
433 | 59 | switch (mapping.virtual_column_type) { |
434 | 9 | case format::TableVirtualColumnType::ROW_ID: |
435 | 9 | RETURN_IF_ERROR(_materialize_row_lineage_row_id(table_block, column_idx)); |
436 | 9 | break; |
437 | 9 | case format::TableVirtualColumnType::LAST_UPDATED_SEQUENCE_NUMBER: |
438 | 8 | RETURN_IF_ERROR( |
439 | 8 | _materialize_row_lineage_last_updated_sequence_number(table_block, column_idx)); |
440 | 8 | break; |
441 | 8 | case format::TableVirtualColumnType::ICEBERG_ROWID: |
442 | 1 | RETURN_IF_ERROR(_materialize_iceberg_rowid(table_block, column_idx)); |
443 | 1 | break; |
444 | 41 | case format::TableVirtualColumnType::INVALID: |
445 | 41 | break; |
446 | 59 | } |
447 | 59 | } |
448 | 41 | return Status::OK(); |
449 | 41 | } |
450 | | |
451 | 48 | Status IcebergTableReader::customize_file_scan_request(format::FileScanRequest* file_request) { |
452 | 48 | RETURN_IF_ERROR(TableReader::customize_file_scan_request(file_request)); |
453 | 48 | if ((_row_lineage_columns.first_row_id >= 0 && _need_row_lineage_row_id()) || |
454 | 48 | _need_iceberg_rowid()) { |
455 | 9 | RETURN_IF_ERROR(_append_row_position_output_column(file_request)); |
456 | 9 | } |
457 | 48 | RETURN_IF_ERROR(_append_equality_delete_predicates(file_request)); |
458 | 48 | return Status::OK(); |
459 | 48 | } |
460 | | |
461 | 47 | bool IcebergTableReader::_supports_aggregate_pushdown(TPushAggOp::type agg_type) const { |
462 | 47 | if (!TableReader::_supports_aggregate_pushdown(agg_type)) { |
463 | 47 | return false; |
464 | 47 | } |
465 | 0 | return _equality_delete_filters.empty(); |
466 | 47 | } |
467 | | |
468 | | Status IcebergTableReader::_parse_deletion_vector_file(const TTableFormatFileDesc& t_desc, |
469 | | DeleteFileDesc* desc, |
470 | 62 | bool* has_delete_file) { |
471 | 62 | DORIS_CHECK(desc != nullptr); |
472 | 62 | DORIS_CHECK(has_delete_file != nullptr); |
473 | 62 | *has_delete_file = false; |
474 | 62 | if (!t_desc.__isset.iceberg_params) { |
475 | 2 | return Status::OK(); |
476 | 2 | } |
477 | 60 | const auto& iceberg_params = t_desc.iceberg_params; |
478 | 60 | if (!iceberg_params.__isset.format_version || |
479 | 60 | iceberg_params.format_version < MIN_SUPPORT_DELETE_FILES_VERSION || |
480 | 60 | !iceberg_params.__isset.delete_files || iceberg_params.delete_files.empty()) { |
481 | 16 | return Status::OK(); |
482 | 16 | } |
483 | | |
484 | 44 | const TIcebergDeleteFileDesc* deletion_vector = nullptr; |
485 | 46 | for (const auto& delete_file : iceberg_params.delete_files) { |
486 | 46 | if (!delete_file.__isset.content || delete_file.content != DELETION_VECTOR) { |
487 | 31 | continue; |
488 | 31 | } |
489 | 15 | if (deletion_vector != nullptr) { |
490 | 1 | return Status::DataQualityError("This iceberg data file has multiple DVs."); |
491 | 1 | } |
492 | 14 | deletion_vector = &delete_file; |
493 | 14 | } |
494 | 43 | if (deletion_vector == nullptr) { |
495 | 30 | return Status::OK(); |
496 | 30 | } |
497 | 13 | size_t bytes_read = 0; |
498 | 13 | RETURN_IF_ERROR(validate_iceberg_deletion_vector_descriptor(*deletion_vector, bytes_read)); |
499 | | |
500 | 11 | const std::string data_file_path = iceberg_params.__isset.original_file_path |
501 | 11 | ? iceberg_params.original_file_path |
502 | 11 | : _data_file_path(); |
503 | 11 | desc->key = build_iceberg_deletion_vector_cache_key(data_file_path, *deletion_vector); |
504 | 11 | desc->path = deletion_vector->path; |
505 | 11 | desc->start_offset = deletion_vector->content_offset; |
506 | 11 | desc->size = static_cast<int64_t>(bytes_read); |
507 | 11 | desc->file_size = -1; |
508 | 11 | desc->format = DeleteFileDesc::Format::ICEBERG; |
509 | 11 | *has_delete_file = true; |
510 | 11 | return Status::OK(); |
511 | 13 | } |
512 | | |
513 | 51 | Status IcebergTableReader::_init_delete_predicates(const TTableFormatFileDesc& t_desc) { |
514 | 51 | if (!t_desc.__isset.iceberg_params || _delete_predicates_initialized) { |
515 | 2 | _delete_predicates_initialized = true; |
516 | 2 | return Status::OK(); |
517 | 2 | } |
518 | 49 | const auto& iceberg_params = t_desc.iceberg_params; |
519 | 49 | if (!iceberg_params.__isset.format_version || |
520 | 49 | iceberg_params.format_version < MIN_SUPPORT_DELETE_FILES_VERSION || |
521 | 49 | !iceberg_params.__isset.delete_files || iceberg_params.delete_files.empty()) { |
522 | 16 | _delete_predicates_initialized = true; |
523 | 16 | return Status::OK(); |
524 | 16 | } |
525 | | |
526 | 33 | std::vector<TIcebergDeleteFileDesc> position_delete_files; |
527 | 33 | std::vector<TIcebergDeleteFileDesc> equality_delete_files; |
528 | 34 | for (const auto& delete_file : iceberg_params.delete_files) { |
529 | 34 | if (!delete_file.__isset.content) { |
530 | 0 | continue; |
531 | 0 | } |
532 | 34 | if (delete_file.content == POSITION_DELETE) { |
533 | 14 | position_delete_files.push_back(delete_file); |
534 | 20 | } else if (delete_file.content == EQUALITY_DELETE) { |
535 | 17 | equality_delete_files.push_back(delete_file); |
536 | 17 | } |
537 | 34 | } |
538 | | // Per Iceberg scan planning, position delete files apply only when there is no deletion vector |
539 | | // for the data file. DVs and position deletes now intentionally use different in-memory |
540 | | // representations, so use the Roaring pointer as the DV sentinel. |
541 | 33 | if (_deletion_vector != nullptr) { |
542 | 3 | position_delete_files.clear(); |
543 | 3 | } |
544 | | // Initialize position and equality delete predicates. Position delete files contain row |
545 | | // positions of deleted rows, which can be directly added to `_delete_rows`. Equality delete |
546 | | // files contain values of deleted rows, which require reading the files and building |
547 | | // predicates for later filtering. |
548 | 33 | if (!position_delete_files.empty()) { |
549 | 13 | RETURN_IF_ERROR(_init_position_delete_rows(position_delete_files)); |
550 | 13 | } |
551 | 31 | if (!equality_delete_files.empty()) { |
552 | 17 | RETURN_IF_ERROR(_init_equality_delete_predicates(equality_delete_files)); |
553 | 17 | } |
554 | | |
555 | 31 | _delete_predicates_initialized = true; |
556 | 31 | return Status::OK(); |
557 | 31 | } |
558 | | |
559 | | std::shared_ptr<io::FileSystemProperties> IcebergTableReader::_delete_file_system_properties( |
560 | 28 | const TFileScanRangeParams& scan_params) { |
561 | 28 | auto system_properties = std::make_shared<io::FileSystemProperties>(); |
562 | 28 | system_properties->system_type = |
563 | 28 | scan_params.__isset.file_type ? scan_params.file_type : TFileType::FILE_LOCAL; |
564 | 28 | system_properties->properties = scan_params.properties; |
565 | 28 | system_properties->hdfs_params = scan_params.hdfs_params; |
566 | 28 | if (scan_params.__isset.broker_addresses) { |
567 | 0 | system_properties->broker_addresses.assign(scan_params.broker_addresses.begin(), |
568 | 0 | scan_params.broker_addresses.end()); |
569 | 0 | } |
570 | 28 | return system_properties; |
571 | 28 | } |
572 | | |
573 | | std::unique_ptr<io::FileDescription> IcebergTableReader::_delete_file_description( |
574 | 28 | const TFileRangeDesc& range) { |
575 | 28 | auto file_description = std::make_unique<io::FileDescription>(); |
576 | 28 | file_description->path = range.path; |
577 | 28 | file_description->file_size = range.__isset.file_size ? range.file_size : -1; |
578 | 28 | file_description->range_start_offset = range.__isset.start_offset ? range.start_offset : 0; |
579 | 28 | file_description->range_size = range.__isset.size ? range.size : -1; |
580 | | // Iceberg delete files follow the same immutable-file contract as data files: a snapshot |
581 | | // references a fixed object and later changes publish a new file rather than replacing it. |
582 | 28 | file_description->is_immutable = true; |
583 | 28 | if (range.__isset.fs_name) { |
584 | 4 | file_description->fs_name = range.fs_name; |
585 | 4 | } |
586 | 28 | return file_description; |
587 | 28 | } |
588 | | |
589 | 12 | std::string IcebergTableReader::_data_file_path() const { |
590 | 12 | if (_iceberg_params.has_value() && _iceberg_params->__isset.original_file_path) { |
591 | 11 | return _iceberg_params->original_file_path; |
592 | 11 | } |
593 | 1 | DORIS_CHECK(_current_task != nullptr); |
594 | 1 | DORIS_CHECK(_current_task->data_file != nullptr); |
595 | 1 | return _current_task->data_file->path; |
596 | 12 | } |
597 | | |
598 | 9 | Status IcebergTableReader::_append_row_position_output_column(format::FileScanRequest* request) { |
599 | 9 | const auto row_position_column_id = format::LocalColumnId(format::ROW_POSITION_COLUMN_ID); |
600 | 9 | _append_file_scan_column(request, row_position_column_id, &request->non_predicate_columns); |
601 | 9 | _row_position_block_position = request->local_positions.at(row_position_column_id).value(); |
602 | 9 | return Status::OK(); |
603 | 9 | } |
604 | | |
605 | | const format::ColumnDefinition* IcebergTableReader::_find_equality_delete_data_field( |
606 | 17 | const EqualityDeleteFilter& filter, size_t key_idx) const { |
607 | 17 | DORIS_CHECK(key_idx < filter.field_ids.size()); |
608 | 17 | DORIS_CHECK(key_idx < filter.field_names.size()); |
609 | 17 | if (mapping_mode() != format::TableColumnMappingMode::BY_NAME) { |
610 | 14 | const int field_id = filter.field_ids[key_idx]; |
611 | 14 | const auto field_it = std::ranges::find_if( |
612 | 15 | _data_reader.file_schema, [field_id](const format::ColumnDefinition& field) { |
613 | 15 | return field.has_identifier_field_id() && |
614 | 15 | field.get_identifier_field_id() == field_id; |
615 | 15 | }); |
616 | 14 | return field_it == _data_reader.file_schema.end() ? nullptr : &*field_it; |
617 | 14 | } |
618 | | |
619 | | // Equality keys are hidden scan dependencies and need not appear in the query projection. |
620 | | // Resolve their current name and aliases from the full table schema supplied by FE, falling |
621 | | // back to the delete-file name when history metadata is unavailable. Reuse ColumnMapper's |
622 | | // exact BY_NAME rules so case, string identifiers, and aliases on either side stay consistent. |
623 | 3 | auto table_field = _find_equality_delete_table_field(filter, key_idx); |
624 | 3 | return format::find_column_by_name(*table_field, _data_reader.file_schema); |
625 | 17 | } |
626 | | |
627 | | std::optional<format::ColumnDefinition> IcebergTableReader::_find_equality_delete_table_field( |
628 | 10 | const EqualityDeleteFilter& filter, size_t key_idx) const { |
629 | 10 | DORIS_CHECK(key_idx < filter.field_ids.size()); |
630 | 10 | DORIS_CHECK(key_idx < filter.field_names.size()); |
631 | 10 | const int field_id = filter.field_ids[key_idx]; |
632 | 10 | auto table_field = _find_current_table_column_by_field_id(field_id, filter.key_types[key_idx]); |
633 | 10 | if (!table_field.has_value()) { |
634 | 4 | const auto projected_field = std::ranges::find_if( |
635 | 4 | _projected_columns, [field_id](const format::ColumnDefinition& field) { |
636 | 4 | return field.has_identifier_field_id() && |
637 | 4 | field.get_identifier_field_id() == field_id; |
638 | 4 | }); |
639 | 4 | if (projected_field != _projected_columns.end()) { |
640 | | // Older scan descriptors and focused unit tests may omit history_schema_info. Keep the |
641 | | // projected metadata as a compatibility fallback, but never require projection when |
642 | | // the complete current schema is available. |
643 | 1 | table_field = *projected_field; |
644 | 1 | } |
645 | 4 | } |
646 | 10 | if (!table_field.has_value()) { |
647 | 3 | table_field = format::ColumnDefinition { |
648 | 3 | .identifier = {}, |
649 | 3 | .name = filter.field_names[key_idx], |
650 | 3 | .type = filter.key_types[key_idx], |
651 | 3 | }; |
652 | 3 | } |
653 | 10 | return table_field; |
654 | 10 | } |
655 | | |
656 | | std::string IcebergTableReader::_delete_file_cache_key(const char* prefix, |
657 | 30 | const std::string& path) const { |
658 | 30 | DORIS_CHECK(prefix != nullptr); |
659 | 30 | std::string fs_name; |
660 | 30 | if (_current_task != nullptr && _current_task->data_file != nullptr) { |
661 | 30 | fs_name = _current_task->data_file->fs_name; |
662 | 30 | } |
663 | | // Delete descriptors can reuse the same path text in different filesystem namespaces. Encode |
664 | | // both variable-length strings so neither an fs/path boundary nor equality field-id suffixes |
665 | | // can be reinterpreted as path content; scan-level credentials/properties are shared here. |
666 | 30 | std::ostringstream key; |
667 | 30 | key << prefix << fs_name.size() << ':' << fs_name << ':' << path.size() << ':' << path; |
668 | 30 | return key.str(); |
669 | 30 | } |
670 | | |
671 | | void IcebergTableReader::_append_equality_delete_row_count_carrier( |
672 | 7 | format::FileScanRequest* request) { |
673 | 7 | DORIS_CHECK(request != nullptr); |
674 | | // Columnar readers establish a filter batch's row count from predicate columns. If all |
675 | | // equality keys are missing, the predicate consists only of NULL literals and the filter block |
676 | | // would otherwise have zero rows. Use the virtual row-position column as the carrier instead |
677 | | // of an arbitrary physical column. For example, a data file may start with an unsupported |
678 | | // TIME_MILLIS leaf while the query projects only a supported `id`; selecting that TIME leaf as |
679 | | // a hidden carrier would make Parquet reject a column the query never requested. Row position |
680 | | // has one value per input row in both Parquet and ORC, is already used by delete predicates, |
681 | | // and is explicitly excluded from physical logical-type validation. |
682 | 7 | _append_file_scan_column(request, format::LocalColumnId(format::ROW_POSITION_COLUMN_ID), |
683 | 7 | &request->predicate_columns); |
684 | 7 | } |
685 | | |
686 | 48 | Status IcebergTableReader::_append_equality_delete_predicates(format::FileScanRequest* request) { |
687 | 48 | DORIS_CHECK(request != nullptr); |
688 | 48 | for (const auto& filter : _equality_delete_filters) { |
689 | 17 | auto delete_predicate = |
690 | 17 | std::make_shared<EqualityDeletePredicate>(filter.delete_block, filter.field_ids); |
691 | 17 | DCHECK_EQ(filter.field_ids.size(), filter.key_types.size()); |
692 | 17 | bool has_missing_key = false; |
693 | 34 | for (size_t idx = 0; idx < filter.field_ids.size(); ++idx) { |
694 | 17 | const auto* field = _find_equality_delete_data_field(filter, idx); |
695 | 17 | if (field == nullptr) { |
696 | 7 | auto table_field = _find_equality_delete_table_field(filter, idx); |
697 | 7 | DORIS_CHECK(table_field.has_value()); |
698 | 7 | VExprSPtr key_expr; |
699 | 7 | RETURN_IF_ERROR(build_missing_equality_delete_key_expr( |
700 | 7 | *table_field, filter.key_types[idx], &key_expr)); |
701 | 7 | delete_predicate->add_child(key_expr); |
702 | 7 | has_missing_key = true; |
703 | 7 | continue; |
704 | 7 | } |
705 | 10 | const auto field_column_id = format::LocalColumnId(field->file_local_id()); |
706 | 10 | _append_file_scan_column(request, field_column_id, &request->predicate_columns); |
707 | 10 | const auto block_position = request->local_positions.at(field_column_id).value(); |
708 | 10 | auto slot = VSlotRef::create_shared(cast_set<int>(block_position), |
709 | 10 | cast_set<int>(block_position), -1, field->type, |
710 | 10 | field->name); |
711 | 10 | if (field->type->equals(*filter.key_types[idx])) { |
712 | 9 | delete_predicate->add_child(std::move(slot)); |
713 | 9 | } else { |
714 | 1 | auto cast_expr = Cast::create_shared(filter.key_types[idx]); |
715 | 1 | cast_expr->add_child(std::move(slot)); |
716 | 1 | delete_predicate->add_child(std::move(cast_expr)); |
717 | 1 | } |
718 | 10 | } |
719 | 17 | if (has_missing_key && request->predicate_columns.empty()) { |
720 | 7 | _append_equality_delete_row_count_carrier(request); |
721 | 7 | } |
722 | 17 | request->delete_conjuncts.push_back( |
723 | 17 | VExprContext::create_shared(std::move(delete_predicate))); |
724 | 17 | } |
725 | 48 | return Status::OK(); |
726 | 48 | } |
727 | | |
728 | | Status IcebergTableReader::_create_delete_file_reader(const TIcebergDeleteFileDesc& delete_file, |
729 | | const TFileScanRangeParams& scan_params, |
730 | | IcebergDeleteFileIOContext* delete_io_ctx, |
731 | 28 | std::unique_ptr<format::FileReader>* reader) { |
732 | 28 | DORIS_CHECK(delete_io_ctx != nullptr); |
733 | 28 | DORIS_CHECK(reader != nullptr); |
734 | 28 | if (!delete_file.__isset.file_format) { |
735 | 0 | return Status::InternalError("Iceberg delete file is missing file format"); |
736 | 0 | } |
737 | 28 | if (delete_file.file_format != TFileFormatType::FORMAT_PARQUET && |
738 | 28 | delete_file.file_format != TFileFormatType::FORMAT_ORC) { |
739 | 0 | return Status::NotSupported("Unsupported Iceberg delete file format {}", |
740 | 0 | delete_file.file_format); |
741 | 0 | } |
742 | 28 | auto delete_range = build_iceberg_delete_file_range(delete_file.path); |
743 | 28 | if (_current_task != nullptr && _current_task->data_file != nullptr && |
744 | 28 | !_current_task->data_file->fs_name.empty()) { |
745 | 4 | delete_range.__set_fs_name(_current_task->data_file->fs_name); |
746 | 4 | } |
747 | 28 | auto system_properties = _delete_file_system_properties(scan_params); |
748 | 28 | auto file_description = _delete_file_description(delete_range); |
749 | 28 | std::shared_ptr<io::IOContext> io_ctx(&delete_io_ctx->io_ctx, [](io::IOContext*) {}); |
750 | 28 | const bool enable_mapping_timestamp_tz = scan_params.__isset.enable_mapping_timestamp_tz && |
751 | 28 | scan_params.enable_mapping_timestamp_tz; |
752 | 28 | const bool enable_mapping_varbinary = |
753 | 28 | scan_params.__isset.enable_mapping_varbinary && scan_params.enable_mapping_varbinary; |
754 | 28 | if (delete_file.file_format == TFileFormatType::FORMAT_PARQUET) { |
755 | | // Delete and data files must parse raw binary fields with the same scan-level mapping. |
756 | 28 | *reader = std::make_unique<format::parquet::ParquetReader>( |
757 | 28 | system_properties, file_description, io_ctx, _scanner_profile, std::nullopt, |
758 | 28 | enable_mapping_timestamp_tz, enable_mapping_varbinary); |
759 | 28 | } else { |
760 | 0 | *reader = std::make_unique<format::orc::OrcReader>(system_properties, file_description, |
761 | 0 | io_ctx, _scanner_profile, std::nullopt, |
762 | 0 | enable_mapping_timestamp_tz); |
763 | 0 | } |
764 | 28 | RETURN_IF_ERROR((*reader)->init(_runtime_state)); |
765 | 28 | return Status::OK(); |
766 | 28 | } |
767 | | |
768 | | Status IcebergTableReader::_read_position_delete_file(const TIcebergDeleteFileDesc& delete_file, |
769 | | const TFileScanRangeParams& scan_params, |
770 | | IcebergDeleteFileIOContext* delete_io_ctx, |
771 | 12 | PositionDeleteRowsCollector* collector) { |
772 | 12 | DORIS_CHECK(collector != nullptr); |
773 | 12 | std::unique_ptr<format::FileReader> reader; |
774 | 12 | RETURN_IF_ERROR(_create_delete_file_reader(delete_file, scan_params, delete_io_ctx, &reader)); |
775 | 12 | DORIS_CHECK(reader != nullptr); |
776 | | |
777 | 12 | std::vector<format::ColumnDefinition> schema; |
778 | 12 | RETURN_IF_ERROR(reader->get_schema(&schema)); |
779 | 12 | format::ColumnDefinition* file_path_field = nullptr; |
780 | 12 | format::ColumnDefinition* pos_field = nullptr; |
781 | 24 | for (auto& field : schema) { |
782 | 24 | if (field.name == ICEBERG_FILE_PATH) { |
783 | 12 | file_path_field = &field; |
784 | 12 | } else if (field.name == ICEBERG_ROW_POS) { |
785 | 12 | pos_field = &field; |
786 | 12 | } |
787 | 24 | } |
788 | 12 | if (file_path_field == nullptr || pos_field == nullptr) { |
789 | 0 | return Status::InternalError("Position delete file is missing required columns"); |
790 | 0 | } |
791 | | |
792 | 12 | auto request = std::make_shared<format::FileScanRequest>(); |
793 | 12 | request->non_predicate_columns = { |
794 | 12 | format::LocalColumnIndex::top_level( |
795 | 12 | format::LocalColumnId(file_path_field->file_local_id())), |
796 | 12 | format::LocalColumnIndex::top_level(format::LocalColumnId(pos_field->file_local_id()))}; |
797 | 12 | request->local_positions = { |
798 | 12 | {format::LocalColumnId(file_path_field->file_local_id()), |
799 | 12 | format::LocalIndex(ICEBERG_FILE_PATH_BLOCK_POSITION)}, |
800 | 12 | {format::LocalColumnId(pos_field->file_local_id()), |
801 | 12 | format::LocalIndex(ICEBERG_ROW_POS_BLOCK_POSITION)}, |
802 | 12 | }; |
803 | 12 | RETURN_IF_ERROR(reader->open(request)); |
804 | | |
805 | 12 | bool eof = false; |
806 | 12 | auto build_position_delete_block = [](const format::ColumnDefinition& file_path_field, |
807 | 22 | const format::ColumnDefinition& pos_field) -> Block { |
808 | 22 | Block block; |
809 | 22 | block.insert( |
810 | 22 | {file_path_field.type->create_column(), file_path_field.type, ICEBERG_FILE_PATH}); |
811 | 22 | block.insert({pos_field.type->create_column(), pos_field.type, ICEBERG_ROW_POS}); |
812 | 22 | return block; |
813 | 22 | }; |
814 | 32 | while (!eof) { |
815 | 22 | Block block = build_position_delete_block(*file_path_field, *pos_field); |
816 | 22 | size_t read_rows = 0; |
817 | 22 | RETURN_IF_ERROR(reader->get_block(&block, &read_rows, &eof)); |
818 | 22 | RETURN_IF_ERROR(collector->collect(block, read_rows)); |
819 | 22 | } |
820 | 10 | return reader->close(); |
821 | 12 | } |
822 | | |
823 | | Status IcebergTableReader::_init_position_delete_rows( |
824 | 13 | const std::vector<TIcebergDeleteFileDesc>& delete_files) { |
825 | 13 | DORIS_CHECK(_split_cache != nullptr); |
826 | 13 | TFileScanRangeParams delete_scan_params = |
827 | 13 | _scan_params == nullptr ? TFileScanRangeParams() : *_scan_params; |
828 | 13 | format::DeleteRows position_delete_rows; |
829 | 13 | IcebergDeleteFileIOContext delete_io_ctx(_runtime_state); |
830 | 13 | for (const auto& delete_file : delete_files) { |
831 | 13 | Status read_status = Status::OK(); |
832 | | // A position delete file normally references many data files. Cache the complete |
833 | | // path-to-position map once; caching only the current data file would still rescan the |
834 | | // shared delete file for every subsequent split. |
835 | 13 | auto* rows_by_data_file = |
836 | 13 | _split_cache->get<PositionDeleteRowsCollector::PositionDeleteFile>( |
837 | 13 | _delete_file_cache_key("iceberg_v2_position_delete_", delete_file.path), |
838 | 13 | [&]() -> PositionDeleteRowsCollector::PositionDeleteFile* { |
839 | 12 | auto result = std::make_unique< |
840 | 12 | PositionDeleteRowsCollector::PositionDeleteFile>(); |
841 | 12 | PositionDeleteRowsCollector collector(result.get()); |
842 | 12 | read_status = _read_position_delete_file( |
843 | 12 | delete_file, delete_scan_params, &delete_io_ctx, &collector); |
844 | 12 | if (!read_status.ok()) { |
845 | 2 | return nullptr; |
846 | 2 | } |
847 | 12 | for (auto& [_, rows] : *result) { |
848 | 12 | std::ranges::sort(rows); |
849 | 12 | } |
850 | 10 | return result.release(); |
851 | 12 | }); |
852 | 13 | RETURN_IF_ERROR(read_status); |
853 | 11 | DORIS_CHECK(rows_by_data_file != nullptr); |
854 | 11 | const auto rows_it = rows_by_data_file->find(_data_file_path()); |
855 | 11 | if (rows_it == rows_by_data_file->end()) { |
856 | 0 | continue; |
857 | 0 | } |
858 | 11 | auto first = rows_it->second.begin(); |
859 | 11 | auto last = rows_it->second.end(); |
860 | | // Bounds are inclusive Iceberg position statistics supplied by FE. Apply them after the |
861 | | // cached per-data-file vector is sorted so irrelevant positions are sliced without a scan. |
862 | 11 | if (delete_file.__isset.position_lower_bound) { |
863 | 1 | first = std::lower_bound(first, last, delete_file.position_lower_bound); |
864 | 1 | } |
865 | 11 | if (delete_file.__isset.position_upper_bound) { |
866 | 1 | last = std::upper_bound(first, last, delete_file.position_upper_bound); |
867 | 1 | } |
868 | 11 | position_delete_rows.insert(position_delete_rows.end(), first, last); |
869 | 11 | } |
870 | 11 | if (position_delete_rows.empty()) { |
871 | 0 | return Status::OK(); |
872 | 0 | } |
873 | | // Position delete files and deletion vectors both become row-position deletes for the |
874 | | // common TableReader DeletePredicate path. Keep the merged rows in a member vector because |
875 | | // DeletePredicate stores a reference to the vector used by _delete_rows. |
876 | 11 | _position_delete_rows_storage.insert(_position_delete_rows_storage.end(), |
877 | 11 | position_delete_rows.begin(), position_delete_rows.end()); |
878 | 11 | std::sort(_position_delete_rows_storage.begin(), _position_delete_rows_storage.end()); |
879 | 11 | _position_delete_rows_storage.erase( |
880 | 11 | std::unique(_position_delete_rows_storage.begin(), _position_delete_rows_storage.end()), |
881 | 11 | _position_delete_rows_storage.end()); |
882 | 11 | _delete_rows = &_position_delete_rows_storage; |
883 | 11 | return Status::OK(); |
884 | 11 | } |
885 | | |
886 | | Status IcebergTableReader::_init_equality_delete_predicates( |
887 | 17 | const std::vector<TIcebergDeleteFileDesc>& delete_files) { |
888 | 17 | DORIS_CHECK(_split_cache != nullptr); |
889 | 17 | TFileScanRangeParams delete_scan_params = |
890 | 17 | _scan_params == nullptr ? TFileScanRangeParams() : *_scan_params; |
891 | 17 | IcebergDeleteFileIOContext delete_io_ctx(_runtime_state); |
892 | 17 | for (const auto& delete_file : delete_files) { |
893 | 17 | RETURN_IF_ERROR( |
894 | 17 | _read_equality_delete_file(delete_file, delete_scan_params, &delete_io_ctx)); |
895 | 17 | } |
896 | 17 | return Status::OK(); |
897 | 17 | } |
898 | | |
899 | | Status IcebergTableReader::_resolve_equality_delete_fields( |
900 | | const TIcebergDeleteFileDesc& delete_file, |
901 | | const std::vector<format::ColumnDefinition>& schema, |
902 | 16 | std::vector<format::ColumnDefinition>* delete_fields, EqualityDeleteFilter* result) const { |
903 | 16 | DORIS_CHECK(delete_fields != nullptr); |
904 | 16 | DORIS_CHECK(result != nullptr); |
905 | 16 | for (const auto field_id : delete_file.field_ids) { |
906 | 16 | const auto field_it = |
907 | 16 | std::ranges::find_if(schema, [field_id](const format::ColumnDefinition& field) { |
908 | 16 | return field.has_identifier_field_id() && |
909 | 16 | field_id == field.get_identifier_field_id(); |
910 | 16 | }); |
911 | 16 | if (field_it == schema.end()) { |
912 | 0 | return Status::InternalError("Can not find field id {} in equality delete file {}", |
913 | 0 | field_id, delete_file.path); |
914 | 0 | } |
915 | 16 | if (!field_it->children.empty()) { |
916 | 0 | return Status::NotSupported( |
917 | 0 | "Iceberg equality delete does not support complex column {}", field_it->name); |
918 | 0 | } |
919 | 16 | delete_fields->push_back(*field_it); |
920 | 16 | result->field_ids.push_back(field_id); |
921 | 16 | result->field_names.push_back(field_it->name); |
922 | 16 | result->key_types.push_back(field_it->type); |
923 | 16 | } |
924 | 16 | return Status::OK(); |
925 | 16 | } |
926 | | |
927 | | Status IcebergTableReader::_load_equality_delete_file(const TIcebergDeleteFileDesc& delete_file, |
928 | | const TFileScanRangeParams& scan_params, |
929 | | IcebergDeleteFileIOContext* delete_io_ctx, |
930 | 16 | EqualityDeleteFilter* result) { |
931 | 16 | DORIS_CHECK(result != nullptr); |
932 | 16 | std::unique_ptr<format::FileReader> reader; |
933 | 16 | RETURN_IF_ERROR(_create_delete_file_reader(delete_file, scan_params, delete_io_ctx, &reader)); |
934 | 16 | DORIS_CHECK(reader != nullptr); |
935 | | |
936 | 16 | std::vector<format::ColumnDefinition> schema; |
937 | 16 | RETURN_IF_ERROR(reader->get_schema(&schema)); |
938 | 16 | std::vector<format::ColumnDefinition> delete_fields; |
939 | 16 | RETURN_IF_ERROR(_resolve_equality_delete_fields(delete_file, schema, &delete_fields, result)); |
940 | | |
941 | 16 | auto request = std::make_shared<format::FileScanRequest>(); |
942 | 16 | Block delete_block_template; |
943 | 32 | for (size_t idx = 0; idx < delete_fields.size(); ++idx) { |
944 | 16 | const auto& delete_field = delete_fields[idx]; |
945 | 16 | const auto local_column_id = format::LocalColumnId(delete_field.file_local_id()); |
946 | 16 | request->non_predicate_columns.push_back( |
947 | 16 | format::LocalColumnIndex::top_level(local_column_id)); |
948 | 16 | request->local_positions.emplace(local_column_id, format::LocalIndex(idx)); |
949 | 16 | delete_block_template.insert( |
950 | 16 | {delete_field.type->create_column(), delete_field.type, delete_field.name}); |
951 | 16 | } |
952 | 16 | RETURN_IF_ERROR(reader->open(request)); |
953 | | |
954 | 16 | MutableBlock mutable_delete_block(delete_block_template.clone_empty()); |
955 | 16 | bool eof = false; |
956 | 48 | while (!eof) { |
957 | 32 | Block block = delete_block_template.clone_empty(); |
958 | 32 | size_t read_rows = 0; |
959 | 32 | RETURN_IF_ERROR(reader->get_block(&block, &read_rows, &eof)); |
960 | 32 | if (read_rows > 0) { |
961 | 16 | RETURN_IF_ERROR(mutable_delete_block.merge(block)); |
962 | 16 | } |
963 | 32 | } |
964 | 16 | RETURN_IF_ERROR(reader->close()); |
965 | 16 | result->delete_block = mutable_delete_block.to_block(); |
966 | 16 | return Status::OK(); |
967 | 16 | } |
968 | | |
969 | | Status IcebergTableReader::_read_equality_delete_file(const TIcebergDeleteFileDesc& delete_file, |
970 | | const TFileScanRangeParams& scan_params, |
971 | 17 | IcebergDeleteFileIOContext* delete_io_ctx) { |
972 | 17 | if (!delete_file.__isset.field_ids || delete_file.field_ids.empty()) { |
973 | 0 | return Status::InternalError("Iceberg equality delete file is missing field ids"); |
974 | 0 | } |
975 | 17 | std::ostringstream cache_key; |
976 | 17 | cache_key << _delete_file_cache_key("iceberg_v2_equality_delete_", delete_file.path); |
977 | 17 | cache_key << ':' << delete_file.field_ids.size(); |
978 | 17 | for (const auto field_id : delete_file.field_ids) { |
979 | 17 | cache_key << ':' << field_id; |
980 | 17 | } |
981 | 17 | Status read_status = Status::OK(); |
982 | | // Include the ordered equality ids in the key because the same physical delete file can be |
983 | | // projected with different key layouts. The cached block and its key metadata are immutable |
984 | | // after construction and therefore safe to copy into each split-local predicate. |
985 | 17 | auto* cached_filter = _split_cache->get<EqualityDeleteFilter>( |
986 | 17 | cache_key.str(), [&]() -> EqualityDeleteFilter* { |
987 | 16 | auto result = std::make_unique<EqualityDeleteFilter>(); |
988 | 16 | read_status = _load_equality_delete_file(delete_file, scan_params, delete_io_ctx, |
989 | 16 | result.get()); |
990 | 16 | if (!read_status.ok()) { |
991 | 0 | return nullptr; |
992 | 0 | } |
993 | 16 | return result.release(); |
994 | 16 | }); |
995 | 17 | RETURN_IF_ERROR(read_status); |
996 | 17 | DORIS_CHECK(cached_filter != nullptr); |
997 | 17 | _equality_delete_filters.push_back(*cached_filter); |
998 | 17 | return Status::OK(); |
999 | 17 | } |
1000 | | |
1001 | 9 | Status IcebergTableReader::_materialize_row_lineage_row_id(Block* table_block, size_t column_idx) { |
1002 | 9 | if (_row_lineage_columns.first_row_id < 0) { |
1003 | 2 | return Status::OK(); |
1004 | 2 | } |
1005 | 7 | DORIS_CHECK(_row_position_block_position < _data_reader.block_template.columns()); |
1006 | 7 | const auto& row_position_column = assert_cast<const ColumnInt64&>( |
1007 | 7 | *_data_reader.block_template.get_by_position(_row_position_block_position).column); |
1008 | 7 | DORIS_CHECK(row_position_column.size() == table_block->rows()); |
1009 | 7 | auto column = IColumn::mutate( |
1010 | 7 | table_block->get_by_position(column_idx).column->convert_to_full_column_if_const()); |
1011 | 7 | auto* nullable_column = assert_cast<ColumnNullable*>(column.get()); |
1012 | 7 | auto& null_map = nullable_column->get_null_map_data(); |
1013 | 7 | auto& data = assert_cast<ColumnInt64&>(*nullable_column->get_nested_column_ptr()).get_data(); |
1014 | 7 | DORIS_CHECK(null_map.size() == row_position_column.size()); |
1015 | 7 | DORIS_CHECK(data.size() == row_position_column.size()); |
1016 | 23 | for (size_t row = 0; row < row_position_column.size(); ++row) { |
1017 | 16 | if (null_map[row]) { |
1018 | 10 | null_map[row] = 0; |
1019 | 10 | data[row] = _row_lineage_columns.first_row_id + row_position_column.get_element(row); |
1020 | 10 | } |
1021 | 16 | } |
1022 | 7 | table_block->replace_by_position(column_idx, std::move(column)); |
1023 | 7 | return Status::OK(); |
1024 | 9 | } |
1025 | | |
1026 | 1 | Status IcebergTableReader::_materialize_iceberg_rowid(Block* table_block, size_t column_idx) { |
1027 | 1 | DORIS_CHECK(_row_position_block_position < _data_reader.block_template.columns()); |
1028 | 1 | const auto& row_position_column = assert_cast<const ColumnInt64&>( |
1029 | 1 | *_data_reader.block_template.get_by_position(_row_position_block_position).column); |
1030 | 1 | DORIS_CHECK(row_position_column.size() == table_block->rows()); |
1031 | | |
1032 | 1 | const auto& type = table_block->get_by_position(column_idx).type; |
1033 | 1 | auto column = type->create_column(); |
1034 | 1 | auto* nullable_column = check_and_get_column<ColumnNullable>(column.get()); |
1035 | 1 | auto* struct_column = nullable_column != nullptr |
1036 | 1 | ? check_and_get_column<ColumnStruct>( |
1037 | 1 | nullable_column->get_nested_column_ptr().get()) |
1038 | 1 | : check_and_get_column<ColumnStruct>(column.get()); |
1039 | 1 | DORIS_CHECK(struct_column != nullptr); |
1040 | 1 | DORIS_CHECK(struct_column->tuple_size() >= 4); |
1041 | | |
1042 | 1 | const auto rows = row_position_column.size(); |
1043 | 1 | const auto file_path = _data_file_path(); |
1044 | 1 | const int32_t partition_spec_id = |
1045 | 1 | _iceberg_params.has_value() && _iceberg_params->__isset.partition_spec_id |
1046 | 1 | ? _iceberg_params->partition_spec_id |
1047 | 1 | : 0; |
1048 | 1 | const std::string partition_data_json = |
1049 | 1 | _iceberg_params.has_value() && _iceberg_params->__isset.partition_data_json |
1050 | 1 | ? _iceberg_params->partition_data_json |
1051 | 1 | : ""; |
1052 | | |
1053 | 1 | auto& file_path_column = struct_column->get_column(0); |
1054 | 1 | auto& row_pos_column = struct_column->get_column(1); |
1055 | 1 | auto& spec_id_column = struct_column->get_column(2); |
1056 | 1 | auto& partition_data_column = struct_column->get_column(3); |
1057 | 1 | file_path_column.reserve(rows); |
1058 | 1 | row_pos_column.reserve(rows); |
1059 | 1 | spec_id_column.reserve(rows); |
1060 | 1 | partition_data_column.reserve(rows); |
1061 | 3 | for (size_t row = 0; row < rows; ++row) { |
1062 | 2 | file_path_column.insert_data(file_path.data(), file_path.size()); |
1063 | 2 | const int64_t row_pos = row_position_column.get_element(row); |
1064 | 2 | row_pos_column.insert_data(reinterpret_cast<const char*>(&row_pos), sizeof(row_pos)); |
1065 | 2 | spec_id_column.insert_data(reinterpret_cast<const char*>(&partition_spec_id), |
1066 | 2 | sizeof(partition_spec_id)); |
1067 | 2 | partition_data_column.insert_data(partition_data_json.data(), partition_data_json.size()); |
1068 | 2 | } |
1069 | 1 | if (nullable_column != nullptr) { |
1070 | 1 | nullable_column->get_null_map_data().resize_fill(rows, 0); |
1071 | 1 | } |
1072 | 1 | table_block->replace_by_position(column_idx, std::move(column)); |
1073 | 1 | return Status::OK(); |
1074 | 1 | } |
1075 | | |
1076 | | Status IcebergTableReader::_materialize_row_lineage_last_updated_sequence_number( |
1077 | 8 | Block* table_block, size_t column_idx) { |
1078 | 8 | if (_row_lineage_columns.last_updated_sequence_number < 0) { |
1079 | 2 | return Status::OK(); |
1080 | 2 | } |
1081 | 6 | auto column = IColumn::mutate( |
1082 | 6 | table_block->get_by_position(column_idx).column->convert_to_full_column_if_const()); |
1083 | 6 | auto* nullable_column = assert_cast<ColumnNullable*>(column.get()); |
1084 | 6 | auto& null_map = nullable_column->get_null_map_data(); |
1085 | 6 | auto& data = assert_cast<ColumnInt64&>(*nullable_column->get_nested_column_ptr()).get_data(); |
1086 | 6 | DORIS_CHECK(null_map.size() == table_block->rows()); |
1087 | 6 | DORIS_CHECK(data.size() == table_block->rows()); |
1088 | 20 | for (size_t row = 0; row < table_block->rows(); ++row) { |
1089 | 14 | if (null_map[row]) { |
1090 | 8 | null_map[row] = 0; |
1091 | 8 | data[row] = _row_lineage_columns.last_updated_sequence_number; |
1092 | 8 | } |
1093 | 14 | } |
1094 | 6 | table_block->replace_by_position(column_idx, std::move(column)); |
1095 | 6 | return Status::OK(); |
1096 | 8 | } |
1097 | | |
1098 | 8 | bool IcebergTableReader::_need_row_lineage_row_id() const { |
1099 | 8 | if (_data_reader.column_mapper != nullptr) { |
1100 | 7 | for (const auto& mapping : _data_reader.column_mapper->mappings()) { |
1101 | 7 | if (mapping.virtual_column_type == format::TableVirtualColumnType::ROW_ID) { |
1102 | 7 | return true; |
1103 | 7 | } |
1104 | 7 | } |
1105 | 7 | } |
1106 | 1 | return std::ranges::any_of(_projected_columns, is_projected_row_lineage_row_id); |
1107 | 8 | } |
1108 | | |
1109 | 40 | bool IcebergTableReader::_need_iceberg_rowid() const { |
1110 | 40 | if (_data_reader.column_mapper != nullptr) { |
1111 | 44 | for (const auto& mapping : _data_reader.column_mapper->mappings()) { |
1112 | 44 | if (mapping.virtual_column_type == format::TableVirtualColumnType::ICEBERG_ROWID) { |
1113 | 1 | return true; |
1114 | 1 | } |
1115 | 44 | } |
1116 | 40 | } |
1117 | 39 | return std::ranges::any_of(_projected_columns, is_projected_iceberg_rowid); |
1118 | 40 | } |
1119 | | |
1120 | | } // namespace doris::format::iceberg |