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 <gen_cpp/Exprs_types.h> |
21 | | #include <rapidjson/document.h> |
22 | | #include <rapidjson/stringbuffer.h> |
23 | | #include <rapidjson/writer.h> |
24 | | |
25 | | #include <algorithm> |
26 | | #include <deque> |
27 | | #include <memory> |
28 | | #include <sstream> |
29 | | #include <string_view> |
30 | | #include <utility> |
31 | | |
32 | | #include "common/cast_set.h" |
33 | | #include "common/consts.h" |
34 | | #include "core/assert_cast.h" |
35 | | #include "core/block/block.h" |
36 | | #include "core/column/column_const.h" |
37 | | #include "core/column/column_nullable.h" |
38 | | #include "core/column/column_string.h" |
39 | | #include "core/column/column_struct.h" |
40 | | #include "core/column/column_vector.h" |
41 | | #include "core/data_type/data_type_array.h" |
42 | | #include "core/data_type/data_type_map.h" |
43 | | #include "core/data_type/data_type_number.h" |
44 | | #include "core/data_type/data_type_struct.h" |
45 | | #include "core/data_type/define_primitive_type.h" |
46 | | #include "core/field.h" |
47 | | #include "exprs/vliteral.h" |
48 | | #include "exprs/vslot_ref.h" |
49 | | #include "format/table/deletion_vector_reader.h" |
50 | | #include "format/table/iceberg_default_value.h" |
51 | | #include "format_v2/expr/cast.h" |
52 | | #include "format_v2/expr/equality_delete_predicate.h" |
53 | | #include "format_v2/orc/orc_reader.h" |
54 | | #include "format_v2/parquet/parquet_reader.h" |
55 | | #include "format_v2/parquet/reader/column_reader.h" |
56 | | #include "format_v2/table_reader.h" |
57 | | #include "io/file_factory.h" |
58 | | #include "util/debug_points.h" |
59 | | #include "util/string_util.h" |
60 | | #include "util/url_coding.h" |
61 | | |
62 | | namespace doris::format::iceberg { |
63 | | |
64 | | static constexpr const char* ROW_LINEAGE_ROW_ID = "_row_id"; |
65 | | static constexpr int32_t ROW_LINEAGE_ROW_ID_FIELD_ID = 2147483540; |
66 | | |
67 | | template <typename T> |
68 | 0 | static std::string join_values_for_debug(const std::vector<T>& values) { |
69 | 0 | std::ostringstream out; |
70 | 0 | out << "["; |
71 | 0 | for (size_t idx = 0; idx < values.size(); ++idx) { |
72 | 0 | if (idx > 0) { |
73 | 0 | out << ", "; |
74 | 0 | } |
75 | 0 | out << values[idx]; |
76 | 0 | } |
77 | 0 | out << "]"; |
78 | 0 | return out.str(); |
79 | 0 | } |
80 | | |
81 | 1 | static bool is_projected_row_lineage_row_id(const format::ColumnDefinition& column) { |
82 | | // Iceberg row lineage columns can be bound by field id when a mapper has already been built, |
83 | | // but customize_file_scan_request() is also exercised directly by scan-request tests before the |
84 | | // mapper exists. In that path, inspect the projected table schema so row-position dependencies |
85 | | // are still added for `_row_id`. |
86 | 1 | return column.name == ROW_LINEAGE_ROW_ID || |
87 | 1 | (column.has_identifier_field_id() && |
88 | 0 | column.get_identifier_field_id() == ROW_LINEAGE_ROW_ID_FIELD_ID); |
89 | 1 | } |
90 | | |
91 | 67 | static bool is_projected_iceberg_rowid(const format::ColumnDefinition& column) { |
92 | 67 | return column.name == BeConsts::ICEBERG_ROWID_COL; |
93 | 67 | } |
94 | | |
95 | 36 | static int iceberg_hex_value(char value) { |
96 | 36 | if (value >= '0' && value <= '9') { |
97 | 24 | return value - '0'; |
98 | 24 | } |
99 | 12 | if (value >= 'a' && value <= 'f') { |
100 | 12 | return value - 'a' + 10; |
101 | 12 | } |
102 | 0 | if (value >= 'A' && value <= 'F') { |
103 | 0 | return value - 'A' + 10; |
104 | 0 | } |
105 | 0 | return -1; |
106 | 0 | } |
107 | | |
108 | 1 | static Status decode_iceberg_hex(std::string_view encoded, std::string* decoded) { |
109 | 1 | DORIS_CHECK(decoded != nullptr); |
110 | 1 | if ((encoded.size() & 1U) != 0) { |
111 | 0 | return Status::InvalidArgument("Invalid odd-length Iceberg binary default"); |
112 | 0 | } |
113 | 1 | decoded->resize(encoded.size() / 2); |
114 | 19 | for (size_t index = 0; index < encoded.size(); index += 2) { |
115 | 18 | const int high = iceberg_hex_value(encoded[index]); |
116 | 18 | const int low = iceberg_hex_value(encoded[index + 1]); |
117 | 18 | if (high < 0 || low < 0) { |
118 | 0 | return Status::InvalidArgument("Invalid hexadecimal Iceberg binary default"); |
119 | 0 | } |
120 | 18 | (*decoded)[index / 2] = static_cast<char>((high << 4) | low); |
121 | 18 | } |
122 | 1 | return Status::OK(); |
123 | 1 | } |
124 | | |
125 | 1 | static Status decode_iceberg_json_binary(std::string_view encoded, std::string* decoded) { |
126 | 1 | DORIS_CHECK(decoded != nullptr); |
127 | 1 | const bool is_uuid = encoded.size() == 36 && encoded[8] == '-' && encoded[13] == '-' && |
128 | 1 | encoded[18] == '-' && encoded[23] == '-'; |
129 | 1 | if (!is_uuid) { |
130 | 1 | return decode_iceberg_hex(encoded, decoded); |
131 | 1 | } |
132 | | |
133 | 0 | std::string uuid_hex; |
134 | 0 | uuid_hex.reserve(32); |
135 | 0 | for (size_t index = 0; index < encoded.size(); ++index) { |
136 | 0 | if (index != 8 && index != 13 && index != 18 && index != 23) { |
137 | 0 | uuid_hex.push_back(encoded[index]); |
138 | 0 | } |
139 | 0 | } |
140 | 0 | return decode_iceberg_hex(uuid_hex, decoded); |
141 | 1 | } |
142 | | |
143 | 3 | static std::string iceberg_json_scalar_text(const rapidjson::Value& value) { |
144 | 3 | if (value.IsString()) { |
145 | 2 | return {value.GetString(), value.GetStringLength()}; |
146 | 2 | } |
147 | 1 | rapidjson::StringBuffer buffer; |
148 | 1 | rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); |
149 | 1 | value.Accept(writer); |
150 | 1 | return {buffer.GetString(), buffer.GetSize()}; |
151 | 3 | } |
152 | | |
153 | 1 | static void normalize_iceberg_json_timestamp(PrimitiveType primitive_type, std::string* value) { |
154 | 1 | if (primitive_type != TYPE_DATETIME && primitive_type != TYPE_DATETIMEV2 && |
155 | 1 | primitive_type != TYPE_TIMESTAMPTZ) { |
156 | 1 | return; |
157 | 1 | } |
158 | 0 | if (const size_t separator = value->find('T'); separator != std::string::npos) { |
159 | 0 | (*value)[separator] = ' '; |
160 | 0 | } |
161 | 0 | if (primitive_type == TYPE_TIMESTAMPTZ) { |
162 | 0 | return; |
163 | 0 | } |
164 | 0 | if (value->ends_with('Z')) { |
165 | 0 | value->pop_back(); |
166 | 0 | return; |
167 | 0 | } |
168 | 0 | const size_t time_start = value->find(' '); |
169 | 0 | if (time_start == std::string::npos) { |
170 | 0 | return; |
171 | 0 | } |
172 | 0 | const size_t offset = value->find_first_of("+-", time_start + 1); |
173 | 0 | if (offset != std::string::npos) { |
174 | 0 | value->erase(offset); |
175 | 0 | } |
176 | 0 | } |
177 | | |
178 | | static Status build_v2_null_default(const format::ColumnDefinition& field, |
179 | 3 | const DataTypePtr& data_type, Field* result) { |
180 | 3 | DORIS_CHECK(data_type != nullptr); |
181 | 3 | DORIS_CHECK(result != nullptr); |
182 | 3 | if (field.is_optional.has_value() && !*field.is_optional) { |
183 | 0 | return Status::InvalidArgument("Required Iceberg field '{}' has a null default", |
184 | 0 | field.name); |
185 | 0 | } |
186 | 3 | if (!data_type->is_nullable()) { |
187 | 0 | return Status::InternalError( |
188 | 0 | "Optional Iceberg field '{}' has a null default, but its Doris type '{}' is not " |
189 | 0 | "nullable", |
190 | 0 | field.name, data_type->get_name()); |
191 | 0 | } |
192 | 3 | *result = Field(); |
193 | 3 | return Status::OK(); |
194 | 3 | } |
195 | | |
196 | | static const format::ColumnDefinition* find_v2_struct_child(const format::ColumnDefinition& field, |
197 | 8 | const std::string& name) { |
198 | 8 | const auto exact_child = std::ranges::find_if( |
199 | 11 | field.children, [&](const auto& candidate) { return iequal(candidate.name, name); }); |
200 | 8 | if (exact_child != field.children.end()) { |
201 | 8 | return &*exact_child; |
202 | 8 | } |
203 | 0 | const auto aliased_child = std::ranges::find_if(field.children, [&](const auto& candidate) { |
204 | 0 | return std::ranges::any_of(candidate.name_mapping, |
205 | 0 | [&](const auto& alias) { return iequal(alias, name); }); |
206 | 0 | }); |
207 | 0 | return aliased_child == field.children.end() ? nullptr : &*aliased_child; |
208 | 8 | } |
209 | | |
210 | | static Status build_v2_initial_default_field(const format::ColumnDefinition& field, |
211 | | const DataTypePtr& data_type, |
212 | | std::deque<std::string>* binary_storage, |
213 | | Field* result); |
214 | | |
215 | | static Status build_v2_json_default_field(const format::ColumnDefinition& field, |
216 | | const DataTypePtr& data_type, |
217 | | const rapidjson::Value& json_value, |
218 | | std::deque<std::string>* binary_storage, Field* result); |
219 | | |
220 | | static Status build_v2_json_struct_default(const format::ColumnDefinition& field, |
221 | | const DataTypePtr& value_type, |
222 | | const rapidjson::Value& json_value, |
223 | 6 | std::deque<std::string>* binary_storage, Field* result) { |
224 | 6 | if (!json_value.IsObject()) { |
225 | 0 | return Status::InvalidArgument("Invalid Iceberg struct default for field '{}'", field.name); |
226 | 0 | } |
227 | | |
228 | 6 | const auto& struct_type = assert_cast<const DataTypeStruct&>(*value_type); |
229 | 6 | Struct struct_value; |
230 | 6 | struct_value.reserve(struct_type.get_elements().size()); |
231 | 14 | for (size_t index = 0; index < struct_type.get_elements().size(); ++index) { |
232 | 8 | const auto* child = find_v2_struct_child(field, struct_type.get_element_name(index)); |
233 | 8 | if (child == nullptr || !child->has_identifier_field_id()) { |
234 | 0 | return Status::InvalidArgument( |
235 | 0 | "Iceberg struct default for field '{}' has incomplete child metadata", |
236 | 0 | field.name); |
237 | 0 | } |
238 | | |
239 | 8 | const std::string child_id = std::to_string(child->get_identifier_field_id()); |
240 | 8 | const auto member = json_value.FindMember(child_id.c_str()); |
241 | 8 | Field child_value; |
242 | 8 | if (member == json_value.MemberEnd()) { |
243 | 8 | RETURN_IF_ERROR(build_v2_initial_default_field(*child, struct_type.get_element(index), |
244 | 8 | binary_storage, &child_value)); |
245 | 8 | } else { |
246 | 0 | RETURN_IF_ERROR(build_v2_json_default_field(*child, struct_type.get_element(index), |
247 | 0 | member->value, binary_storage, |
248 | 0 | &child_value)); |
249 | 0 | } |
250 | 8 | struct_value.push_back(std::move(child_value)); |
251 | 8 | } |
252 | 6 | *result = Field::create_field<TYPE_STRUCT>(std::move(struct_value)); |
253 | 6 | return Status::OK(); |
254 | 6 | } |
255 | | |
256 | | // The child ColumnDefinition, recursively transported from the item TField, describes the element |
257 | | // schema and its field-level default metadata. It cannot represent a particular list literal's |
258 | | // length or per-position values, so the parent initial-default keeps those values in Iceberg's |
259 | | // single-value JSON array. |
260 | | static Status build_v2_json_array_default(const format::ColumnDefinition& field, |
261 | | const DataTypePtr& value_type, |
262 | | const rapidjson::Value& json_value, |
263 | 1 | std::deque<std::string>* binary_storage, Field* result) { |
264 | 1 | if (!json_value.IsArray() || field.children.size() != 1) { |
265 | 0 | return Status::InvalidArgument("Invalid Iceberg list default for field '{}'", field.name); |
266 | 0 | } |
267 | | |
268 | 1 | const auto& array_type = assert_cast<const DataTypeArray&>(*value_type); |
269 | 1 | Array array_value; |
270 | 1 | array_value.reserve(json_value.Size()); |
271 | 2 | for (const auto& json_element : json_value.GetArray()) { |
272 | 2 | Field element_value; |
273 | 2 | RETURN_IF_ERROR(build_v2_json_default_field(field.children.front(), |
274 | 2 | array_type.get_nested_type(), json_element, |
275 | 2 | binary_storage, &element_value)); |
276 | 2 | array_value.push_back(std::move(element_value)); |
277 | 2 | } |
278 | 1 | *result = Field::create_field<TYPE_ARRAY>(std::move(array_value)); |
279 | 1 | return Status::OK(); |
280 | 1 | } |
281 | | |
282 | | // The child ColumnDefinitions, recursively transported from the key/value TFields, describe entry |
283 | | // schemas and field-level default metadata. They cannot represent the number, order, or concrete |
284 | | // values of map entries, so the parent initial-default keeps the entries in Iceberg's single-value |
285 | | // JSON key/value arrays. |
286 | | static Status build_v2_json_map_default(const format::ColumnDefinition& field, |
287 | | const DataTypePtr& value_type, |
288 | | const rapidjson::Value& json_value, |
289 | 1 | std::deque<std::string>* binary_storage, Field* result) { |
290 | 1 | if (!json_value.IsObject() || !json_value.HasMember("keys") || !json_value["keys"].IsArray() || |
291 | 1 | !json_value.HasMember("values") || !json_value["values"].IsArray() || |
292 | 1 | field.children.size() != 2) { |
293 | 0 | return Status::InvalidArgument("Invalid Iceberg map default for field '{}'", field.name); |
294 | 0 | } |
295 | 1 | const auto& keys = json_value["keys"]; |
296 | 1 | const auto& values = json_value["values"]; |
297 | 1 | if (keys.Size() != values.Size()) { |
298 | 0 | return Status::InvalidArgument( |
299 | 0 | "Iceberg map default for field '{}' has {} keys but {} values", field.name, |
300 | 0 | keys.Size(), values.Size()); |
301 | 0 | } |
302 | | |
303 | 1 | const auto& map_type = assert_cast<const DataTypeMap&>(*value_type); |
304 | 1 | Array key_fields; |
305 | 1 | Array value_fields; |
306 | 1 | key_fields.reserve(keys.Size()); |
307 | 1 | value_fields.reserve(values.Size()); |
308 | 2 | for (rapidjson::SizeType index = 0; index < keys.Size(); ++index) { |
309 | 1 | Field key_value; |
310 | 1 | Field mapped_value; |
311 | 1 | RETURN_IF_ERROR(build_v2_json_default_field(field.children[0], map_type.get_key_type(), |
312 | 1 | keys[index], binary_storage, &key_value)); |
313 | 1 | RETURN_IF_ERROR(build_v2_json_default_field(field.children[1], map_type.get_value_type(), |
314 | 1 | values[index], binary_storage, &mapped_value)); |
315 | 1 | key_fields.push_back(std::move(key_value)); |
316 | 1 | value_fields.push_back(std::move(mapped_value)); |
317 | 1 | } |
318 | 1 | Map map_value; |
319 | 1 | map_value.push_back(Field::create_field<TYPE_ARRAY>(std::move(key_fields))); |
320 | 1 | map_value.push_back(Field::create_field<TYPE_ARRAY>(std::move(value_fields))); |
321 | 1 | *result = Field::create_field<TYPE_MAP>(std::move(map_value)); |
322 | 1 | return Status::OK(); |
323 | 1 | } |
324 | | |
325 | | static Status build_v2_json_scalar_default(const format::ColumnDefinition& field, |
326 | | const DataTypePtr& value_type, |
327 | | const rapidjson::Value& json_value, |
328 | 3 | std::deque<std::string>* binary_storage, Field* result) { |
329 | 3 | const auto primitive_type = value_type->get_primitive_type(); |
330 | 3 | std::string serialized_value = iceberg_json_scalar_text(json_value); |
331 | 3 | const bool binary_like = |
332 | 3 | field.initial_default_value_is_base64 || primitive_type == TYPE_VARBINARY; |
333 | 3 | if (binary_like) { |
334 | 1 | if (!json_value.IsString()) { |
335 | 0 | return Status::InvalidArgument( |
336 | 0 | "Iceberg binary default for field '{}' is not a JSON string", field.name); |
337 | 0 | } |
338 | 1 | binary_storage->emplace_back(); |
339 | 1 | RETURN_IF_ERROR(decode_iceberg_json_binary(serialized_value, &binary_storage->back())); |
340 | 1 | if (primitive_type == TYPE_VARBINARY) { |
341 | 0 | *result = Field::create_field<TYPE_VARBINARY>(StringView(binary_storage->back())); |
342 | 1 | } else if (is_string_type(primitive_type)) { |
343 | 1 | *result = Field::create_field<TYPE_STRING>(binary_storage->back()); |
344 | 1 | } else { |
345 | 0 | return Status::InvalidArgument( |
346 | 0 | "Iceberg binary default for field '{}' has incompatible Doris type '{}'", |
347 | 0 | field.name, value_type->get_name()); |
348 | 0 | } |
349 | 1 | return Status::OK(); |
350 | 1 | } |
351 | | |
352 | 2 | if (is_string_type(primitive_type)) { |
353 | 1 | if (!json_value.IsString()) { |
354 | 0 | return Status::InvalidArgument("Iceberg string default for field '{}' is not a string", |
355 | 0 | field.name); |
356 | 0 | } |
357 | 1 | *result = Field::create_field<TYPE_STRING>(std::move(serialized_value)); |
358 | 1 | return Status::OK(); |
359 | 1 | } |
360 | 1 | normalize_iceberg_json_timestamp(primitive_type, &serialized_value); |
361 | 1 | if (doris::iceberg::detail::parse_non_finite_default(primitive_type, serialized_value, |
362 | 1 | result)) { |
363 | 0 | return Status::OK(); |
364 | 0 | } |
365 | 1 | RETURN_IF_ERROR(value_type->get_serde()->from_fe_string(serialized_value, *result)); |
366 | 1 | return Status::OK(); |
367 | 1 | } |
368 | | |
369 | | static Status build_v2_json_default_field(const format::ColumnDefinition& field, |
370 | | const DataTypePtr& data_type, |
371 | | const rapidjson::Value& json_value, |
372 | 12 | std::deque<std::string>* binary_storage, Field* result) { |
373 | 12 | DORIS_CHECK(data_type != nullptr); |
374 | 12 | DORIS_CHECK(binary_storage != nullptr); |
375 | 12 | DORIS_CHECK(result != nullptr); |
376 | 12 | if (json_value.IsNull()) { |
377 | 1 | return build_v2_null_default(field, data_type, result); |
378 | 1 | } |
379 | | |
380 | 11 | const auto value_type = remove_nullable(data_type); |
381 | 11 | switch (value_type->get_primitive_type()) { |
382 | 6 | case TYPE_STRUCT: |
383 | 6 | return build_v2_json_struct_default(field, value_type, json_value, binary_storage, result); |
384 | 1 | case TYPE_ARRAY: |
385 | 1 | return build_v2_json_array_default(field, value_type, json_value, binary_storage, result); |
386 | 1 | case TYPE_MAP: |
387 | 1 | return build_v2_json_map_default(field, value_type, json_value, binary_storage, result); |
388 | 3 | default: |
389 | 3 | return build_v2_json_scalar_default(field, value_type, json_value, binary_storage, result); |
390 | 11 | } |
391 | 11 | } |
392 | | |
393 | | static Status build_v2_initial_default_field(const format::ColumnDefinition& field, |
394 | | const DataTypePtr& data_type, |
395 | | std::deque<std::string>* binary_storage, |
396 | 42 | Field* result) { |
397 | 42 | DORIS_CHECK(data_type != nullptr); |
398 | 42 | DORIS_CHECK(binary_storage != nullptr); |
399 | 42 | DORIS_CHECK(result != nullptr); |
400 | 42 | if (!field.initial_default_value.has_value()) { |
401 | 2 | if (field.is_optional.has_value() && !*field.is_optional) { |
402 | 0 | return Status::InvalidArgument( |
403 | 0 | "Required Iceberg field '{}' is missing from the data file and has no initial " |
404 | 0 | "default", |
405 | 0 | field.name); |
406 | 0 | } |
407 | 2 | return build_v2_null_default(field, data_type, result); |
408 | 2 | } |
409 | | |
410 | 40 | const auto value_type = remove_nullable(data_type); |
411 | 40 | const auto primitive_type = value_type->get_primitive_type(); |
412 | 40 | if (is_complex_type(primitive_type)) { |
413 | 8 | rapidjson::Document document; |
414 | 8 | document.Parse(field.initial_default_value->data(), field.initial_default_value->size()); |
415 | 8 | if (document.HasParseError()) { |
416 | 0 | return Status::InvalidArgument("Invalid Iceberg JSON initial default for field '{}'", |
417 | 0 | field.name); |
418 | 0 | } |
419 | 8 | return build_v2_json_default_field(field, data_type, document, binary_storage, result); |
420 | 8 | } |
421 | | |
422 | 32 | if (field.initial_default_value_is_base64 || primitive_type == TYPE_VARBINARY) { |
423 | 5 | binary_storage->emplace_back(); |
424 | 5 | if (!base64_decode(*field.initial_default_value, &binary_storage->back())) { |
425 | 0 | return Status::InvalidArgument("Invalid Base64 Iceberg initial default for field {}", |
426 | 0 | field.name); |
427 | 0 | } |
428 | 5 | if (primitive_type == TYPE_VARBINARY) { |
429 | 3 | *result = Field::create_field<TYPE_VARBINARY>(StringView(binary_storage->back())); |
430 | 3 | } else if (is_string_type(primitive_type)) { |
431 | 2 | *result = Field::create_field<TYPE_STRING>(binary_storage->back()); |
432 | 2 | } else { |
433 | 0 | return Status::InvalidArgument( |
434 | 0 | "Base64 Iceberg initial default has incompatible Doris type {} for field {}", |
435 | 0 | data_type->get_name(), field.name); |
436 | 0 | } |
437 | 5 | return Status::OK(); |
438 | 5 | } |
439 | | |
440 | 27 | if (doris::iceberg::detail::parse_non_finite_default(primitive_type, |
441 | 27 | *field.initial_default_value, result)) { |
442 | 3 | return Status::OK(); |
443 | 3 | } |
444 | 24 | RETURN_IF_ERROR(value_type->get_serde()->from_fe_string(*field.initial_default_value, *result)); |
445 | 23 | return Status::OK(); |
446 | 24 | } |
447 | | |
448 | | static Status build_initial_default_literal(const format::ColumnDefinition& table_field, |
449 | 34 | VExprSPtr* literal) { |
450 | 34 | DORIS_CHECK(table_field.type != nullptr); |
451 | 34 | DORIS_CHECK(table_field.initial_default_value.has_value()); |
452 | 34 | DORIS_CHECK(literal != nullptr); |
453 | | |
454 | 34 | std::deque<std::string> binary_storage; |
455 | 34 | Field initial_default; |
456 | 34 | RETURN_IF_ERROR(build_v2_initial_default_field(table_field, table_field.type, &binary_storage, |
457 | 34 | &initial_default)); |
458 | | // VLiteral inserts the Field into an owning column before binary_storage is destroyed. |
459 | 33 | *literal = VLiteral::create_shared(table_field.type, initial_default); |
460 | 33 | return Status::OK(); |
461 | 34 | } |
462 | | |
463 | 42 | Status prepare_iceberg_initial_default_exprs(format::ColumnDefinition* column) { |
464 | 42 | DORIS_CHECK(column != nullptr); |
465 | 42 | if (column->initial_default_value.has_value()) { |
466 | 22 | VExprSPtr literal; |
467 | 22 | RETURN_IF_ERROR(build_initial_default_literal(*column, &literal)); |
468 | 21 | column->default_expr = VExprContext::create_shared(std::move(literal)); |
469 | 21 | } |
470 | 41 | for (auto& child : column->children) { |
471 | 21 | RETURN_IF_ERROR(prepare_iceberg_initial_default_exprs(&child)); |
472 | 21 | } |
473 | 41 | return Status::OK(); |
474 | 41 | } |
475 | | |
476 | | static Status build_missing_equality_delete_key_expr(const format::ColumnDefinition& table_field, |
477 | | const DataTypePtr& delete_key_type, |
478 | | bool require_complete_metadata, |
479 | 18 | VExprSPtr* key_expr) { |
480 | 18 | DORIS_CHECK(delete_key_type != nullptr); |
481 | 18 | DORIS_CHECK(key_expr != nullptr); |
482 | 18 | if (!table_field.initial_default_value.has_value()) { |
483 | 6 | if (require_complete_metadata && !table_field.is_optional.has_value()) { |
484 | 0 | return Status::InvalidArgument( |
485 | 0 | "Iceberg equality delete field '{}' is missing optionality metadata", |
486 | 0 | table_field.name); |
487 | 0 | } |
488 | 6 | if (table_field.is_optional.has_value() && !*table_field.is_optional) { |
489 | 0 | return Status::InvalidArgument("Missing required field: {}", table_field.name); |
490 | 0 | } |
491 | | // A newly added optional field without an initial default is logically NULL in older |
492 | | // files. EqualityDeletePredicate treats NULL == NULL as a match. |
493 | 6 | *key_expr = VLiteral::create_shared(make_nullable(delete_key_type), Field()); |
494 | 6 | return Status::OK(); |
495 | 6 | } |
496 | | |
497 | 12 | VExprSPtr literal; |
498 | 12 | RETURN_IF_ERROR(build_initial_default_literal(table_field, &literal)); |
499 | 12 | if (table_field.type->equals(*delete_key_type)) { |
500 | 12 | *key_expr = std::move(literal); |
501 | 12 | return Status::OK(); |
502 | 12 | } |
503 | 0 | auto cast_expr = Cast::create_shared(delete_key_type); |
504 | 0 | cast_expr->add_child(literal); |
505 | 0 | *key_expr = std::move(cast_expr); |
506 | 0 | return Status::OK(); |
507 | 12 | } |
508 | | |
509 | | static bool find_equality_delete_column_path(const std::vector<format::ColumnDefinition>& fields, |
510 | | int32_t field_id, |
511 | 65 | std::vector<const format::ColumnDefinition*>* path) { |
512 | 65 | DORIS_CHECK(path != nullptr); |
513 | 65 | for (const auto& field : fields) { |
514 | 63 | path->push_back(&field); |
515 | 63 | if (field.has_identifier_field_id() && field.get_identifier_field_id() == field_id) { |
516 | 48 | return true; |
517 | 48 | } |
518 | 15 | if (find_equality_delete_column_path(field.children, field_id, path)) { |
519 | 13 | return true; |
520 | 13 | } |
521 | 2 | path->pop_back(); |
522 | 2 | } |
523 | 4 | return false; |
524 | 65 | } |
525 | | |
526 | | class NestedStructFieldExpr final : public VExpr { |
527 | | public: |
528 | | NestedStructFieldExpr(DataTypePtr data_type, std::vector<size_t> child_indexes, |
529 | | std::string expr_name) |
530 | 29 | : VExpr(std::move(data_type), false), |
531 | 29 | _child_indexes(std::move(child_indexes)), |
532 | 29 | _expr_name(std::move(expr_name)) { |
533 | 29 | _node_type = TExprNodeType::FUNCTION_CALL; |
534 | 29 | } |
535 | | |
536 | | Status prepare(RuntimeState* state, const RowDescriptor& row_desc, |
537 | 29 | VExprContext* context) override { |
538 | 29 | RETURN_IF_ERROR_OR_PREPARED(VExpr::prepare(state, row_desc, context)); |
539 | 29 | _prepare_finished = true; |
540 | 29 | return Status::OK(); |
541 | 29 | } |
542 | | |
543 | | Status open(RuntimeState* state, VExprContext* context, |
544 | 29 | FunctionContext::FunctionStateScope scope) override { |
545 | 29 | RETURN_IF_ERROR_OR_PREPARED(VExpr::open(state, context, scope)); |
546 | 0 | _open_finished = true; |
547 | 0 | return Status::OK(); |
548 | 0 | } |
549 | | |
550 | 29 | void close(VExprContext* context, FunctionContext::FunctionStateScope scope) override { |
551 | 29 | VExpr::close(context, scope); |
552 | 29 | } |
553 | | |
554 | | Status execute_column_impl(VExprContext* context, const Block* block, const Selector* selector, |
555 | 35 | size_t count, ColumnPtr& result_column) const override { |
556 | 35 | DORIS_CHECK(_children.size() == 1); |
557 | 35 | ColumnPtr current; |
558 | 35 | RETURN_IF_ERROR( |
559 | 35 | _children.front()->execute_column(context, block, selector, count, current)); |
560 | 35 | current = current->convert_to_full_column_if_const(); |
561 | | |
562 | 35 | std::vector<ColumnPtr> ancestor_nullable_columns; |
563 | 35 | std::vector<const NullMap*> ancestor_null_maps; |
564 | 35 | for (const size_t child_index : _child_indexes) { |
565 | 35 | if (const auto* nullable = check_and_get_column<ColumnNullable>(*current); |
566 | 35 | nullable != nullptr) { |
567 | 35 | ancestor_nullable_columns.push_back(current); |
568 | 35 | ancestor_null_maps.push_back(&nullable->get_null_map_data()); |
569 | 35 | current = nullable->get_nested_column_ptr(); |
570 | 35 | } |
571 | 35 | const auto* struct_column = check_and_get_column<ColumnStruct>(*current); |
572 | 35 | DORIS_CHECK(struct_column != nullptr); |
573 | 35 | DORIS_CHECK(child_index < struct_column->tuple_size()); |
574 | 35 | current = struct_column->get_column_ptr(child_index); |
575 | 35 | } |
576 | 35 | if (const auto* nullable = check_and_get_column<ColumnNullable>(*current); |
577 | 35 | nullable != nullptr) { |
578 | 35 | ancestor_nullable_columns.push_back(current); |
579 | 35 | ancestor_null_maps.push_back(&nullable->get_null_map_data()); |
580 | 35 | current = nullable->get_nested_column_ptr(); |
581 | 35 | } |
582 | 35 | if (ancestor_null_maps.empty()) { |
583 | 0 | result_column = make_nullable(current); |
584 | 0 | return Status::OK(); |
585 | 0 | } |
586 | | |
587 | 35 | auto result = ColumnNullable::create(remove_nullable(_data_type)->create_column(), |
588 | 35 | ColumnUInt8::create()); |
589 | 35 | auto& result_data = result->get_nested_column(); |
590 | 35 | auto& result_null_map = result->get_null_map_data(); |
591 | 35 | result_data.reserve(count); |
592 | 35 | result_null_map.reserve(count); |
593 | 102 | for (size_t row = 0; row < count; ++row) { |
594 | 67 | const bool is_null = |
595 | 110 | std::ranges::any_of(ancestor_null_maps, [row](const NullMap* null_map) { |
596 | 110 | DORIS_CHECK(null_map != nullptr); |
597 | 110 | DORIS_CHECK(row < null_map->size()); |
598 | 110 | return (*null_map)[row] != 0; |
599 | 110 | }); |
600 | 67 | if (is_null) { |
601 | 24 | result_data.insert_default(); |
602 | 24 | result_null_map.push_back(1); |
603 | 43 | } else { |
604 | 43 | result_data.insert_from(*current, row); |
605 | 43 | result_null_map.push_back(0); |
606 | 43 | } |
607 | 67 | } |
608 | 35 | result_column = std::move(result); |
609 | 35 | return Status::OK(); |
610 | 35 | } |
611 | | |
612 | 35 | const std::string& expr_name() const override { return _expr_name; } |
613 | | |
614 | 0 | Status clone_node(VExprSPtr* cloned_expr) const override { |
615 | 0 | DORIS_CHECK(cloned_expr != nullptr); |
616 | 0 | *cloned_expr = std::make_shared<NestedStructFieldExpr>(*this); |
617 | 0 | return Status::OK(); |
618 | 0 | } |
619 | | |
620 | | private: |
621 | | std::vector<size_t> _child_indexes; |
622 | | std::string _expr_name; |
623 | | }; |
624 | | |
625 | | class AncestorNullDefaultExpr final : public VExpr { |
626 | | public: |
627 | | AncestorNullDefaultExpr(DataTypePtr data_type, std::string expr_name) |
628 | 2 | : VExpr(std::move(data_type), false), _expr_name(std::move(expr_name)) { |
629 | 2 | _node_type = TExprNodeType::FUNCTION_CALL; |
630 | 2 | } |
631 | | |
632 | | Status prepare(RuntimeState* state, const RowDescriptor& row_desc, |
633 | 2 | VExprContext* context) override { |
634 | 2 | RETURN_IF_ERROR_OR_PREPARED(VExpr::prepare(state, row_desc, context)); |
635 | 2 | _prepare_finished = true; |
636 | 2 | return Status::OK(); |
637 | 2 | } |
638 | | |
639 | | Status open(RuntimeState* state, VExprContext* context, |
640 | 2 | FunctionContext::FunctionStateScope scope) override { |
641 | 2 | RETURN_IF_ERROR_OR_PREPARED(VExpr::open(state, context, scope)); |
642 | 0 | _open_finished = true; |
643 | 0 | return Status::OK(); |
644 | 0 | } |
645 | | |
646 | 2 | void close(VExprContext* context, FunctionContext::FunctionStateScope scope) override { |
647 | 2 | VExpr::close(context, scope); |
648 | 2 | } |
649 | | |
650 | | Status execute_column_impl(VExprContext* context, const Block* block, const Selector* selector, |
651 | 2 | size_t count, ColumnPtr& result_column) const override { |
652 | 2 | DORIS_CHECK(_children.size() == 2); |
653 | 2 | ColumnPtr ancestor; |
654 | 2 | RETURN_IF_ERROR( |
655 | 2 | _children.front()->execute_column(context, block, selector, count, ancestor)); |
656 | 2 | ancestor = ancestor->convert_to_full_column_if_const(); |
657 | 2 | const NullMap* ancestor_null_map = nullptr; |
658 | 2 | if (const auto* nullable = check_and_get_column<ColumnNullable>(*ancestor); |
659 | 2 | nullable != nullptr) { |
660 | 2 | ancestor_null_map = &nullable->get_null_map_data(); |
661 | 2 | } |
662 | | |
663 | 2 | ColumnPtr default_value; |
664 | 2 | RETURN_IF_ERROR( |
665 | 2 | _children.back()->execute_column(context, block, selector, count, default_value)); |
666 | 2 | default_value = default_value->convert_to_full_column_if_const(); |
667 | 2 | const NullMap* default_null_map = nullptr; |
668 | 2 | const IColumn* default_data = default_value.get(); |
669 | 2 | if (const auto* nullable = check_and_get_column<ColumnNullable>(*default_value); |
670 | 2 | nullable != nullptr) { |
671 | 2 | default_null_map = &nullable->get_null_map_data(); |
672 | 2 | default_data = &nullable->get_nested_column(); |
673 | 2 | } |
674 | | |
675 | 2 | auto result = ColumnNullable::create(remove_nullable(_data_type)->create_column(), |
676 | 2 | ColumnUInt8::create()); |
677 | 2 | auto& result_data = result->get_nested_column(); |
678 | 2 | auto& result_null_map = result->get_null_map_data(); |
679 | 2 | result_data.reserve(count); |
680 | 2 | result_null_map.reserve(count); |
681 | 8 | for (size_t row = 0; row < count; ++row) { |
682 | 6 | const bool ancestor_is_null = |
683 | 6 | ancestor_null_map != nullptr && (*ancestor_null_map)[row] != 0; |
684 | 6 | const bool default_is_null = |
685 | 6 | default_null_map != nullptr && (*default_null_map)[row] != 0; |
686 | 6 | if (ancestor_is_null || default_is_null) { |
687 | 2 | result_data.insert_default(); |
688 | 2 | result_null_map.push_back(1); |
689 | 4 | } else { |
690 | 4 | result_data.insert_from(*default_data, row); |
691 | 4 | result_null_map.push_back(0); |
692 | 4 | } |
693 | 6 | } |
694 | 2 | result_column = std::move(result); |
695 | 2 | return Status::OK(); |
696 | 2 | } |
697 | | |
698 | 2 | const std::string& expr_name() const override { return _expr_name; } |
699 | | |
700 | 0 | Status clone_node(VExprSPtr* cloned_expr) const override { |
701 | 0 | DORIS_CHECK(cloned_expr != nullptr); |
702 | 0 | *cloned_expr = std::make_shared<AncestorNullDefaultExpr>(*this); |
703 | 0 | return Status::OK(); |
704 | 0 | } |
705 | | |
706 | | private: |
707 | | std::string _expr_name; |
708 | | }; |
709 | | |
710 | | static Status build_nested_equality_delete_key_expr( |
711 | | const std::vector<const format::ColumnDefinition*>& path, VExprSPtr root_expr, |
712 | 82 | VExprSPtr* key_expr) { |
713 | 82 | DORIS_CHECK(!path.empty()); |
714 | 82 | DORIS_CHECK(root_expr != nullptr); |
715 | 82 | DORIS_CHECK(key_expr != nullptr); |
716 | 82 | const auto* root = path.front(); |
717 | 82 | DORIS_CHECK(root != nullptr); |
718 | 82 | DORIS_CHECK(root->type != nullptr); |
719 | 82 | VExprSPtr result = std::move(root_expr); |
720 | 82 | std::vector<size_t> child_indexes; |
721 | 82 | std::string expr_name = root->name; |
722 | 111 | for (size_t index = 1; index < path.size(); ++index) { |
723 | 29 | const auto* parent = path[index - 1]; |
724 | 29 | const auto* child = path[index]; |
725 | 29 | DORIS_CHECK(parent != nullptr); |
726 | 29 | DORIS_CHECK(child != nullptr); |
727 | 29 | DORIS_CHECK(parent->type != nullptr); |
728 | 29 | DORIS_CHECK(child->type != nullptr); |
729 | 29 | if (remove_nullable(parent->type)->get_primitive_type() != TYPE_STRUCT) { |
730 | 0 | return Status::NotSupported( |
731 | 0 | "Iceberg equality delete field {} has non-struct ancestor {}", child->name, |
732 | 0 | parent->name); |
733 | 0 | } |
734 | 29 | const auto child_it = |
735 | 29 | std::ranges::find_if(parent->children, [child](const auto& candidate) { |
736 | 29 | if (candidate.has_identifier_field_id() && child->has_identifier_field_id()) { |
737 | 25 | return candidate.get_identifier_field_id() == |
738 | 25 | child->get_identifier_field_id(); |
739 | 25 | } |
740 | 4 | return candidate.name == child->name; |
741 | 29 | }); |
742 | 29 | DORIS_CHECK(child_it != parent->children.end()); |
743 | 29 | child_indexes.push_back(cast_set<size_t>(child_it - parent->children.begin())); |
744 | 29 | expr_name += "." + child->name; |
745 | 29 | } |
746 | 82 | if (!child_indexes.empty()) { |
747 | 29 | auto nested_field = std::make_shared<NestedStructFieldExpr>( |
748 | 29 | make_nullable(path.back()->type), std::move(child_indexes), std::move(expr_name)); |
749 | 29 | nested_field->add_child(result); |
750 | 29 | result = std::move(nested_field); |
751 | 29 | } |
752 | 82 | *key_expr = std::move(result); |
753 | 82 | return Status::OK(); |
754 | 82 | } |
755 | | |
756 | | static Status build_equality_delete_key_expr( |
757 | | const std::vector<const format::ColumnDefinition*>& path, size_t block_position, |
758 | 64 | VExprSPtr* key_expr) { |
759 | 64 | DORIS_CHECK(!path.empty()); |
760 | 64 | const auto* root = path.front(); |
761 | 64 | DORIS_CHECK(root != nullptr); |
762 | 64 | DORIS_CHECK(root->type != nullptr); |
763 | 64 | VExprSPtr root_expr = |
764 | 64 | VSlotRef::create_shared(cast_set<int>(block_position), cast_set<int>(block_position), |
765 | 64 | -1, root->type, root->name); |
766 | 64 | return build_nested_equality_delete_key_expr(path, std::move(root_expr), key_expr); |
767 | 64 | } |
768 | | |
769 | | Status IcebergTableReader::annotate_projected_column(const TFileScanSlotInfo& slot_info, |
770 | | format::ProjectedColumnBuildContext* context, |
771 | 16 | format::ColumnDefinition* column) const { |
772 | 16 | RETURN_IF_ERROR(format::TableReader::annotate_projected_column(slot_info, context, column)); |
773 | 16 | DORIS_CHECK(context != nullptr); |
774 | 16 | DORIS_CHECK(column != nullptr); |
775 | 16 | if (!supports_iceberg_scan_semantics_v2(context->scan_params)) { |
776 | 2 | return Status::OK(); |
777 | 2 | } |
778 | 14 | if (!context->schema_column.has_value()) { |
779 | 0 | return Status::OK(); |
780 | 0 | } |
781 | | |
782 | 14 | auto& schema_column = *context->schema_column; |
783 | 14 | RETURN_IF_ERROR(prepare_iceberg_initial_default_exprs(&schema_column)); |
784 | 13 | column->initial_default_value = schema_column.initial_default_value; |
785 | 13 | column->initial_default_value_is_base64 = schema_column.initial_default_value_is_base64; |
786 | 13 | column->is_optional = schema_column.is_optional; |
787 | 13 | if (schema_column.default_expr != nullptr) { |
788 | | // The Iceberg typed literal is authoritative. In particular, this replaces FE's generic |
789 | | // string expression for Base64-transported UUID/BINARY/FIXED defaults. |
790 | 9 | column->default_expr = schema_column.default_expr; |
791 | 9 | } else if (schema_column.is_optional.has_value() && !*schema_column.is_optional) { |
792 | | // FE's generic external-column metadata currently treats Iceberg columns as nullable. Clear |
793 | | // that fallback so a physically missing required field is rejected by the Iceberg mapper. |
794 | 1 | column->default_expr = nullptr; |
795 | 1 | } |
796 | 13 | return Status::OK(); |
797 | 14 | } |
798 | | |
799 | 0 | static std::string iceberg_delete_file_debug_string(const TIcebergDeleteFileDesc& delete_file) { |
800 | 0 | std::ostringstream out; |
801 | 0 | out << "TIcebergDeleteFileDesc{path=" << (delete_file.__isset.path ? delete_file.path : "null") |
802 | 0 | << ", content=" << (delete_file.__isset.content ? delete_file.content : -1) |
803 | 0 | << ", file_format=" |
804 | 0 | << (delete_file.__isset.file_format ? static_cast<int>(delete_file.file_format) : -1) |
805 | 0 | << ", position_lower_bound=" |
806 | 0 | << (delete_file.__isset.position_lower_bound ? delete_file.position_lower_bound : -1) |
807 | 0 | << ", position_upper_bound=" |
808 | 0 | << (delete_file.__isset.position_upper_bound ? delete_file.position_upper_bound : -1) |
809 | 0 | << ", field_ids=" |
810 | 0 | << (delete_file.__isset.field_ids ? join_values_for_debug(delete_file.field_ids) : "[]") |
811 | 0 | << ", content_offset=" |
812 | 0 | << (delete_file.__isset.content_offset ? delete_file.content_offset : -1) |
813 | 0 | << ", content_size_in_bytes=" |
814 | 0 | << (delete_file.__isset.content_size_in_bytes ? delete_file.content_size_in_bytes : -1) |
815 | 0 | << "}"; |
816 | 0 | return out.str(); |
817 | 0 | } |
818 | | |
819 | | static std::string iceberg_delete_files_debug_string( |
820 | 0 | const std::vector<TIcebergDeleteFileDesc>& delete_files) { |
821 | 0 | std::ostringstream out; |
822 | 0 | out << "["; |
823 | 0 | for (size_t idx = 0; idx < delete_files.size(); ++idx) { |
824 | 0 | if (idx > 0) { |
825 | 0 | out << ", "; |
826 | 0 | } |
827 | 0 | out << iceberg_delete_file_debug_string(delete_files[idx]); |
828 | 0 | } |
829 | 0 | out << "]"; |
830 | 0 | return out.str(); |
831 | 0 | } |
832 | | |
833 | 0 | static std::string iceberg_params_debug_string(const std::optional<TIcebergFileDesc>& params) { |
834 | 0 | if (!params.has_value()) { |
835 | 0 | return "null"; |
836 | 0 | } |
837 | 0 | const auto& iceberg_params = *params; |
838 | 0 | std::ostringstream out; |
839 | 0 | out << "TIcebergFileDesc{format_version=" |
840 | 0 | << (iceberg_params.__isset.format_version ? iceberg_params.format_version : -1) |
841 | 0 | << ", content=" << (iceberg_params.__isset.content ? iceberg_params.content : -1) |
842 | 0 | << ", original_file_path=" |
843 | 0 | << (iceberg_params.__isset.original_file_path ? iceberg_params.original_file_path : "null") |
844 | 0 | << ", row_count=" << (iceberg_params.__isset.row_count ? iceberg_params.row_count : -1) |
845 | 0 | << ", partition_spec_id=" |
846 | 0 | << (iceberg_params.__isset.partition_spec_id ? iceberg_params.partition_spec_id : 0) |
847 | 0 | << ", has_partition_data_json=" << iceberg_params.__isset.partition_data_json |
848 | 0 | << ", first_row_id=" |
849 | 0 | << (iceberg_params.__isset.first_row_id ? iceberg_params.first_row_id : -1) |
850 | 0 | << ", last_updated_sequence_number=" |
851 | 0 | << (iceberg_params.__isset.last_updated_sequence_number |
852 | 0 | ? iceberg_params.last_updated_sequence_number |
853 | 0 | : -1) |
854 | 0 | << ", delete_file_count=" |
855 | 0 | << (iceberg_params.__isset.delete_files ? iceberg_params.delete_files.size() : 0) |
856 | 0 | << ", delete_files=" |
857 | 0 | << (iceberg_params.__isset.delete_files |
858 | 0 | ? iceberg_delete_files_debug_string(iceberg_params.delete_files) |
859 | 0 | : "[]") |
860 | 0 | << ", has_serialized_split=" << iceberg_params.__isset.serialized_split << "}"; |
861 | 0 | return out.str(); |
862 | 0 | } |
863 | | |
864 | | IcebergTableReader::PositionDeleteRowsCollector::PositionDeleteRowsCollector( |
865 | | PositionDeleteFile* rows_by_data_file) |
866 | 12 | : _rows_by_data_file(rows_by_data_file) { |
867 | 12 | DORIS_CHECK(_rows_by_data_file != nullptr); |
868 | 12 | } |
869 | | |
870 | | Status IcebergTableReader::PositionDeleteRowsCollector::collect(const Block& block, |
871 | 22 | size_t read_rows) { |
872 | 22 | if (read_rows == 0) { |
873 | 10 | return Status::OK(); |
874 | 10 | } |
875 | 12 | const auto& file_path_column_ptr = |
876 | 12 | block.get_by_position(ICEBERG_FILE_PATH_BLOCK_POSITION).column; |
877 | 12 | const auto& pos_column_ptr = block.get_by_position(ICEBERG_ROW_POS_BLOCK_POSITION).column; |
878 | 12 | if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*file_path_column_ptr); |
879 | 12 | nullable_column != nullptr && nullable_column->has_null(0, read_rows)) { |
880 | 1 | return Status::Corruption("Iceberg position delete column file_path contains null values"); |
881 | 1 | } |
882 | 11 | if (const auto* nullable_column = check_and_get_column<ColumnNullable>(*pos_column_ptr); |
883 | 11 | nullable_column != nullptr && nullable_column->has_null(0, read_rows)) { |
884 | 1 | return Status::Corruption("Iceberg position delete column pos contains null values"); |
885 | 1 | } |
886 | 10 | const auto& file_path_column = |
887 | 10 | assert_cast<const ColumnString&>(*remove_nullable(file_path_column_ptr)); |
888 | 10 | const auto& pos_column = assert_cast<const ColumnInt64&>(*remove_nullable(pos_column_ptr)); |
889 | 26 | for (size_t row = 0; row < read_rows; ++row) { |
890 | 16 | const auto file_path = file_path_column.get_data_at(row).to_string(); |
891 | 16 | (*_rows_by_data_file)[file_path].push_back(pos_column.get_element(row)); |
892 | 16 | } |
893 | 10 | return Status::OK(); |
894 | 11 | } |
895 | | |
896 | 79 | Status IcebergTableReader::prepare_split(const format::SplitReadOptions& options) { |
897 | 79 | { |
898 | 79 | SCOPED_TIMER(_profile.total_timer); |
899 | 79 | SCOPED_TIMER(_profile.prepare_split_timer); |
900 | 79 | _row_lineage_columns = {}; |
901 | 79 | _iceberg_params.reset(); |
902 | 79 | _delete_predicates_initialized = false; |
903 | 79 | _position_delete_rows_storage.clear(); |
904 | 79 | _equality_delete_filters.clear(); |
905 | 79 | _split_cache = options.cache; |
906 | 79 | if (options.current_range.__isset.table_format_params && |
907 | 79 | options.current_range.table_format_params.__isset.iceberg_params) { |
908 | 77 | const auto& iceberg_params = options.current_range.table_format_params.iceberg_params; |
909 | 77 | _iceberg_params = iceberg_params; |
910 | 77 | if (iceberg_params.__isset.first_row_id) { |
911 | 9 | _row_lineage_columns.first_row_id = iceberg_params.first_row_id; |
912 | 9 | } |
913 | 77 | if (iceberg_params.__isset.last_updated_sequence_number) { |
914 | 6 | _row_lineage_columns.last_updated_sequence_number = |
915 | 6 | iceberg_params.last_updated_sequence_number; |
916 | 6 | } |
917 | 77 | } |
918 | 79 | } |
919 | 79 | RETURN_IF_ERROR(TableReader::prepare_split(options)); |
920 | 76 | SCOPED_TIMER(_profile.total_timer); |
921 | 76 | SCOPED_TIMER(_profile.prepare_split_timer); |
922 | 76 | if (current_split_pruned()) { |
923 | 0 | return Status::OK(); |
924 | 0 | } |
925 | | // Iceberg data files are immutable once referenced by a snapshot; updates create new data files |
926 | | // at new paths instead of overwriting existing files. This lets the Parquet V2 reader use page |
927 | | // cache when the scan range does not carry an mtime, without extending V1's path::0 behavior to |
928 | | // mutable Hive/local files. |
929 | 76 | mark_current_data_file_immutable(); |
930 | 76 | if (_is_table_level_count_active()) { |
931 | 1 | return Status::OK(); |
932 | 1 | } |
933 | 75 | DBUG_EXECUTE_IF("IcebergTableReader.prepare_split.before_delete_file_scan", |
934 | 75 | DBUG_RUN_CALLBACK()); |
935 | 75 | RETURN_IF_ERROR(_init_delete_predicates(options.current_range.table_format_params)); |
936 | 73 | return Status::OK(); |
937 | 75 | } |
938 | | |
939 | 0 | std::string IcebergTableReader::debug_string() const { |
940 | 0 | size_t position_delete_file_count = 0; |
941 | 0 | size_t equality_delete_file_count = 0; |
942 | 0 | size_t deletion_vector_file_count = 0; |
943 | 0 | if (_iceberg_params.has_value() && _iceberg_params->__isset.delete_files) { |
944 | 0 | for (const auto& delete_file : _iceberg_params->delete_files) { |
945 | 0 | if (!delete_file.__isset.content) { |
946 | 0 | continue; |
947 | 0 | } |
948 | 0 | if (delete_file.content == POSITION_DELETE) { |
949 | 0 | ++position_delete_file_count; |
950 | 0 | } else if (delete_file.content == EQUALITY_DELETE) { |
951 | 0 | ++equality_delete_file_count; |
952 | 0 | } else if (delete_file.content == DELETION_VECTOR) { |
953 | 0 | ++deletion_vector_file_count; |
954 | 0 | } |
955 | 0 | } |
956 | 0 | } |
957 | |
|
958 | 0 | std::ostringstream equality_filters; |
959 | 0 | equality_filters << "["; |
960 | 0 | for (size_t idx = 0; idx < _equality_delete_filters.size(); ++idx) { |
961 | 0 | if (idx > 0) { |
962 | 0 | equality_filters << ", "; |
963 | 0 | } |
964 | 0 | const auto& filter = _equality_delete_filters[idx]; |
965 | 0 | equality_filters << "EqualityDeleteFilter{field_ids=" |
966 | 0 | << join_values_for_debug(filter.field_ids) << ", key_types=["; |
967 | 0 | for (size_t type_idx = 0; type_idx < filter.key_types.size(); ++type_idx) { |
968 | 0 | if (type_idx > 0) { |
969 | 0 | equality_filters << ", "; |
970 | 0 | } |
971 | 0 | equality_filters << (filter.key_types[type_idx] == nullptr |
972 | 0 | ? "null" |
973 | 0 | : filter.key_types[type_idx]->get_name()); |
974 | 0 | } |
975 | 0 | equality_filters << "], delete_block_rows=" << filter.delete_block.rows() |
976 | 0 | << ", delete_block_columns=" << filter.delete_block.columns() << "}"; |
977 | 0 | } |
978 | 0 | equality_filters << "]"; |
979 | |
|
980 | 0 | std::ostringstream out; |
981 | 0 | out << "IcebergTableReader{base=" << format::TableReader::debug_string() |
982 | 0 | << ", iceberg_params=" << iceberg_params_debug_string(_iceberg_params) |
983 | 0 | << ", row_lineage_first_row_id=" << _row_lineage_columns.first_row_id |
984 | 0 | << ", row_lineage_last_updated_sequence_number=" |
985 | 0 | << _row_lineage_columns.last_updated_sequence_number |
986 | 0 | << ", need_row_lineage_row_id=" << _need_row_lineage_row_id() |
987 | 0 | << ", need_iceberg_rowid=" << _need_iceberg_rowid() |
988 | 0 | << ", row_position_block_position=" << _row_position_block_position |
989 | 0 | << ", delete_predicates_initialized=" << _delete_predicates_initialized |
990 | 0 | << ", position_delete_file_count=" << position_delete_file_count |
991 | 0 | << ", equality_delete_file_count=" << equality_delete_file_count |
992 | 0 | << ", deletion_vector_file_count=" << deletion_vector_file_count |
993 | 0 | << ", position_delete_rows_storage_count=" << _position_delete_rows_storage.size() |
994 | 0 | << ", equality_delete_filter_count=" << _equality_delete_filters.size() |
995 | 0 | << ", equality_delete_filters=" << equality_filters.str() << "}"; |
996 | 0 | return out.str(); |
997 | 0 | } |
998 | | |
999 | 59 | Status IcebergTableReader::materialize_virtual_columns(Block* table_block) { |
1000 | 136 | for (size_t column_idx = 0; column_idx < _data_reader.column_mapper->mappings().size(); |
1001 | 77 | ++column_idx) { |
1002 | 77 | const auto& mapping = _data_reader.column_mapper->mappings()[column_idx]; |
1003 | 77 | switch (mapping.virtual_column_type) { |
1004 | 9 | case format::TableVirtualColumnType::ROW_ID: |
1005 | 9 | RETURN_IF_ERROR(_materialize_row_lineage_row_id(table_block, column_idx)); |
1006 | 9 | break; |
1007 | 9 | case format::TableVirtualColumnType::LAST_UPDATED_SEQUENCE_NUMBER: |
1008 | 8 | RETURN_IF_ERROR( |
1009 | 8 | _materialize_row_lineage_last_updated_sequence_number(table_block, column_idx)); |
1010 | 8 | break; |
1011 | 8 | case format::TableVirtualColumnType::ICEBERG_ROWID: |
1012 | 1 | RETURN_IF_ERROR(_materialize_iceberg_rowid(table_block, column_idx)); |
1013 | 1 | break; |
1014 | 59 | case format::TableVirtualColumnType::INVALID: |
1015 | 59 | break; |
1016 | 77 | } |
1017 | 77 | } |
1018 | 59 | return Status::OK(); |
1019 | 59 | } |
1020 | | |
1021 | 72 | Status IcebergTableReader::customize_file_scan_request(format::FileScanRequest* file_request) { |
1022 | 72 | RETURN_IF_ERROR(TableReader::customize_file_scan_request(file_request)); |
1023 | 72 | if ((_row_lineage_columns.first_row_id >= 0 && _need_row_lineage_row_id()) || |
1024 | 72 | _need_iceberg_rowid()) { |
1025 | 9 | RETURN_IF_ERROR(_append_row_position_output_column(file_request)); |
1026 | 9 | } |
1027 | 72 | RETURN_IF_ERROR(_append_equality_delete_predicates(file_request)); |
1028 | 71 | return Status::OK(); |
1029 | 72 | } |
1030 | | |
1031 | 70 | bool IcebergTableReader::_supports_aggregate_pushdown(TPushAggOp::type agg_type) const { |
1032 | 70 | if (!TableReader::_supports_aggregate_pushdown(agg_type)) { |
1033 | 70 | return false; |
1034 | 70 | } |
1035 | 0 | return _equality_delete_filters.empty(); |
1036 | 70 | } |
1037 | | |
1038 | | Status IcebergTableReader::_parse_deletion_vector_file(const TTableFormatFileDesc& t_desc, |
1039 | | DeleteFileDesc* desc, |
1040 | 86 | bool* has_delete_file) { |
1041 | 86 | DORIS_CHECK(desc != nullptr); |
1042 | 86 | DORIS_CHECK(has_delete_file != nullptr); |
1043 | 86 | *has_delete_file = false; |
1044 | 86 | if (!t_desc.__isset.iceberg_params) { |
1045 | 2 | return Status::OK(); |
1046 | 2 | } |
1047 | 84 | const auto& iceberg_params = t_desc.iceberg_params; |
1048 | 84 | if (!iceberg_params.__isset.format_version || |
1049 | 84 | iceberg_params.format_version < MIN_SUPPORT_DELETE_FILES_VERSION || |
1050 | 84 | !iceberg_params.__isset.delete_files || iceberg_params.delete_files.empty()) { |
1051 | 16 | return Status::OK(); |
1052 | 16 | } |
1053 | | |
1054 | 68 | const TIcebergDeleteFileDesc* deletion_vector = nullptr; |
1055 | 70 | for (const auto& delete_file : iceberg_params.delete_files) { |
1056 | 70 | if (!delete_file.__isset.content || delete_file.content != DELETION_VECTOR) { |
1057 | 55 | continue; |
1058 | 55 | } |
1059 | 15 | if (deletion_vector != nullptr) { |
1060 | 1 | return Status::DataQualityError("This iceberg data file has multiple DVs."); |
1061 | 1 | } |
1062 | 14 | deletion_vector = &delete_file; |
1063 | 14 | } |
1064 | 67 | if (deletion_vector == nullptr) { |
1065 | 54 | return Status::OK(); |
1066 | 54 | } |
1067 | 13 | size_t bytes_read = 0; |
1068 | 13 | RETURN_IF_ERROR(validate_iceberg_deletion_vector_descriptor(*deletion_vector, bytes_read)); |
1069 | | |
1070 | 11 | const std::string data_file_path = iceberg_params.__isset.original_file_path |
1071 | 11 | ? iceberg_params.original_file_path |
1072 | 11 | : _data_file_path(); |
1073 | 11 | desc->key = build_iceberg_deletion_vector_cache_key(data_file_path, *deletion_vector); |
1074 | 11 | desc->path = deletion_vector->path; |
1075 | 11 | desc->start_offset = deletion_vector->content_offset; |
1076 | 11 | desc->size = static_cast<int64_t>(bytes_read); |
1077 | 11 | desc->file_size = -1; |
1078 | 11 | desc->format = DeleteFileDesc::Format::ICEBERG; |
1079 | 11 | *has_delete_file = true; |
1080 | 11 | return Status::OK(); |
1081 | 13 | } |
1082 | | |
1083 | 75 | Status IcebergTableReader::_init_delete_predicates(const TTableFormatFileDesc& t_desc) { |
1084 | 75 | if (!t_desc.__isset.iceberg_params || _delete_predicates_initialized) { |
1085 | 2 | _delete_predicates_initialized = true; |
1086 | 2 | return Status::OK(); |
1087 | 2 | } |
1088 | 73 | const auto& iceberg_params = t_desc.iceberg_params; |
1089 | 73 | if (!iceberg_params.__isset.format_version || |
1090 | 73 | iceberg_params.format_version < MIN_SUPPORT_DELETE_FILES_VERSION || |
1091 | 73 | !iceberg_params.__isset.delete_files || iceberg_params.delete_files.empty()) { |
1092 | 16 | _delete_predicates_initialized = true; |
1093 | 16 | return Status::OK(); |
1094 | 16 | } |
1095 | | |
1096 | 57 | std::vector<TIcebergDeleteFileDesc> position_delete_files; |
1097 | 57 | std::vector<TIcebergDeleteFileDesc> equality_delete_files; |
1098 | 58 | for (const auto& delete_file : iceberg_params.delete_files) { |
1099 | 58 | if (!delete_file.__isset.content) { |
1100 | 0 | continue; |
1101 | 0 | } |
1102 | 58 | if (delete_file.content == POSITION_DELETE) { |
1103 | 14 | position_delete_files.push_back(delete_file); |
1104 | 44 | } else if (delete_file.content == EQUALITY_DELETE) { |
1105 | 41 | equality_delete_files.push_back(delete_file); |
1106 | 41 | } |
1107 | 58 | } |
1108 | | // Per Iceberg scan planning, position delete files apply only when there is no deletion vector |
1109 | | // for the data file. DVs and position deletes now intentionally use different in-memory |
1110 | | // representations, so use the Roaring pointer as the DV sentinel. |
1111 | 57 | if (_deletion_vector != nullptr) { |
1112 | 3 | position_delete_files.clear(); |
1113 | 3 | } |
1114 | | // Initialize position and equality delete predicates. Position delete files contain row |
1115 | | // positions of deleted rows, which can be directly added to `_delete_rows`. Equality delete |
1116 | | // files contain values of deleted rows, which require reading the files and building |
1117 | | // predicates for later filtering. |
1118 | 57 | if (!position_delete_files.empty()) { |
1119 | 13 | RETURN_IF_ERROR(_init_position_delete_rows(position_delete_files)); |
1120 | 13 | } |
1121 | 55 | if (!equality_delete_files.empty()) { |
1122 | 41 | RETURN_IF_ERROR(_init_equality_delete_predicates(equality_delete_files)); |
1123 | 41 | } |
1124 | | |
1125 | 55 | _delete_predicates_initialized = true; |
1126 | 55 | return Status::OK(); |
1127 | 55 | } |
1128 | | |
1129 | | std::shared_ptr<io::FileSystemProperties> IcebergTableReader::_delete_file_system_properties( |
1130 | 52 | const TFileScanRangeParams& scan_params) { |
1131 | 52 | auto system_properties = std::make_shared<io::FileSystemProperties>(); |
1132 | 52 | system_properties->system_type = |
1133 | 52 | scan_params.__isset.file_type ? scan_params.file_type : TFileType::FILE_LOCAL; |
1134 | 52 | system_properties->properties = scan_params.properties; |
1135 | 52 | system_properties->hdfs_params = scan_params.hdfs_params; |
1136 | 52 | if (scan_params.__isset.broker_addresses) { |
1137 | 0 | system_properties->broker_addresses.assign(scan_params.broker_addresses.begin(), |
1138 | 0 | scan_params.broker_addresses.end()); |
1139 | 0 | } |
1140 | 52 | return system_properties; |
1141 | 52 | } |
1142 | | |
1143 | | std::unique_ptr<io::FileDescription> IcebergTableReader::_delete_file_description( |
1144 | 52 | const TFileRangeDesc& range) { |
1145 | 52 | auto file_description = std::make_unique<io::FileDescription>(); |
1146 | 52 | file_description->path = range.path; |
1147 | 52 | file_description->file_size = range.__isset.file_size ? range.file_size : -1; |
1148 | 52 | file_description->range_start_offset = range.__isset.start_offset ? range.start_offset : 0; |
1149 | 52 | file_description->range_size = range.__isset.size ? range.size : -1; |
1150 | | // Iceberg delete files follow the same immutable-file contract as data files: a snapshot |
1151 | | // references a fixed object and later changes publish a new file rather than replacing it. |
1152 | 52 | file_description->is_immutable = true; |
1153 | 52 | if (range.__isset.fs_name) { |
1154 | 4 | file_description->fs_name = range.fs_name; |
1155 | 4 | } |
1156 | 52 | return file_description; |
1157 | 52 | } |
1158 | | |
1159 | 12 | std::string IcebergTableReader::_data_file_path() const { |
1160 | 12 | if (_iceberg_params.has_value() && _iceberg_params->__isset.original_file_path) { |
1161 | 11 | return _iceberg_params->original_file_path; |
1162 | 11 | } |
1163 | 1 | DORIS_CHECK(_current_task != nullptr); |
1164 | 1 | DORIS_CHECK(_current_task->data_file != nullptr); |
1165 | 1 | return _current_task->data_file->path; |
1166 | 12 | } |
1167 | | |
1168 | 9 | Status IcebergTableReader::_append_row_position_output_column(format::FileScanRequest* request) { |
1169 | 9 | const auto row_position_column_id = format::LocalColumnId(format::ROW_POSITION_COLUMN_ID); |
1170 | 9 | _append_file_scan_column(request, row_position_column_id, &request->non_predicate_columns); |
1171 | 9 | _row_position_block_position = request->local_positions.at(row_position_column_id).value(); |
1172 | 9 | return Status::OK(); |
1173 | 9 | } |
1174 | | |
1175 | | Status IcebergTableReader::_find_equality_delete_data_field( |
1176 | | const EqualityDeleteFilter& filter, size_t key_idx, |
1177 | 41 | EqualityDeleteColumnPath* const data_path, bool* const complete_path) const { |
1178 | 41 | DORIS_CHECK(key_idx < filter.field_ids.size()); |
1179 | 41 | DORIS_CHECK(key_idx < filter.field_names.size()); |
1180 | 41 | DORIS_CHECK(data_path != nullptr); |
1181 | 41 | DORIS_CHECK(complete_path != nullptr); |
1182 | 41 | data_path->clear(); |
1183 | 41 | *complete_path = false; |
1184 | | |
1185 | 41 | auto schema_path = |
1186 | 41 | _find_table_column_identity_path_by_field_id(filter.field_ids[key_idx], true); |
1187 | 41 | std::vector<const format::ColumnDefinition*> table_path; |
1188 | 41 | if (schema_path.has_value()) { |
1189 | 50 | for (const auto& field : *schema_path) { |
1190 | 50 | table_path.push_back(&field); |
1191 | 50 | } |
1192 | 32 | } else { |
1193 | 9 | static_cast<void>(find_equality_delete_column_path(_projected_columns, |
1194 | 9 | filter.field_ids[key_idx], &table_path)); |
1195 | 9 | } |
1196 | 41 | if (table_path.empty() && mapping_mode() != format::TableColumnMappingMode::BY_NAME) { |
1197 | 1 | const int field_id = filter.field_ids[key_idx]; |
1198 | 1 | *complete_path = |
1199 | 1 | find_equality_delete_column_path(_data_reader.file_schema, field_id, data_path); |
1200 | 1 | return Status::OK(); |
1201 | 1 | } |
1202 | | |
1203 | | // Equality keys are hidden scan dependencies and need not appear in the query projection. |
1204 | | // Reuse ColumnMapper's exact BY_NAME rules at every ancestor so a nested key keeps its |
1205 | | // physical path, including historical aliases for ID-less files. |
1206 | 40 | std::optional<format::ColumnDefinition> legacy_table_field; |
1207 | 40 | if (table_path.empty() && !supports_iceberg_scan_semantics_v2(_scan_params)) { |
1208 | 0 | legacy_table_field.emplace(); |
1209 | 0 | legacy_table_field->name = filter.field_names[key_idx]; |
1210 | 0 | legacy_table_field->type = filter.key_types[key_idx]; |
1211 | 0 | table_path.push_back(&*legacy_table_field); |
1212 | 0 | } |
1213 | 40 | if (table_path.empty()) { |
1214 | 0 | return Status::InvalidArgument( |
1215 | 0 | "Iceberg equality delete field id {} is absent from current and historical table " |
1216 | 0 | "schema metadata", |
1217 | 0 | filter.field_ids[key_idx]); |
1218 | 0 | } |
1219 | 40 | const std::vector<format::ColumnDefinition>* candidates = &_data_reader.file_schema; |
1220 | 74 | for (size_t index = 0; index < table_path.size(); ++index) { |
1221 | 52 | const auto* table_field = table_path[index]; |
1222 | 52 | DORIS_CHECK(table_field != nullptr); |
1223 | 52 | const format::ColumnDefinition* data_field = nullptr; |
1224 | 52 | if (mapping_mode() == format::TableColumnMappingMode::BY_NAME) { |
1225 | 15 | data_field = format::find_column_by_name(*table_field, *candidates); |
1226 | 37 | } else { |
1227 | 37 | DORIS_CHECK(table_field->has_identifier_field_id()); |
1228 | 37 | data_field = format::find_column_by_field_id( |
1229 | 37 | *table_field, *candidates, |
1230 | 37 | supports_iceberg_scan_semantics_v1(_scan_params) && |
1231 | 37 | _format == FileFormat::PARQUET); |
1232 | 37 | } |
1233 | 52 | if (data_field == nullptr && mapping_mode() == format::TableColumnMappingMode::BY_NAME && |
1234 | 52 | index + 1 == table_path.size() && !table_field->has_name_mapping) { |
1235 | | // Schema-history fallback can carry a post-snapshot leaf rename when the target |
1236 | | // snapshot's parent has expired. Retry the delete file's original leaf name, but |
1237 | | // never bypass an explicit authoritative Iceberg mapping. |
1238 | 7 | format::ColumnDefinition delete_file_field; |
1239 | 7 | delete_file_field.name = filter.field_names[key_idx]; |
1240 | 7 | data_field = format::find_column_by_name(delete_file_field, *candidates); |
1241 | 7 | } |
1242 | 52 | if (data_field == nullptr) { |
1243 | 18 | return Status::OK(); |
1244 | 18 | } |
1245 | 34 | data_path->push_back(data_field); |
1246 | 34 | candidates = &data_field->children; |
1247 | 34 | } |
1248 | 22 | *complete_path = true; |
1249 | 22 | return Status::OK(); |
1250 | 40 | } |
1251 | | |
1252 | | Status IcebergTableReader::_find_equality_delete_table_field( |
1253 | | const EqualityDeleteFilter& filter, size_t key_idx, |
1254 | 1 | format::ColumnDefinition* table_field) const { |
1255 | 1 | DORIS_CHECK(key_idx < filter.field_ids.size()); |
1256 | 1 | DORIS_CHECK(key_idx < filter.field_names.size()); |
1257 | 1 | DORIS_CHECK(table_field != nullptr); |
1258 | 1 | const int field_id = filter.field_ids[key_idx]; |
1259 | 1 | auto resolved = _find_table_column_by_field_id(field_id, filter.key_types[key_idx], true); |
1260 | 1 | if (!resolved.has_value()) { |
1261 | 1 | const auto projected_field = std::ranges::find_if( |
1262 | 1 | _projected_columns, [field_id](const format::ColumnDefinition& field) { |
1263 | 1 | return field.has_identifier_field_id() && |
1264 | 1 | field.get_identifier_field_id() == field_id; |
1265 | 1 | }); |
1266 | 1 | if (projected_field != _projected_columns.end()) { |
1267 | | // Older scan descriptors and focused unit tests may omit history_schema_info. Keep the |
1268 | | // projected metadata as a compatibility fallback, but never require projection when |
1269 | | // the complete current schema is available. |
1270 | 0 | resolved = *projected_field; |
1271 | 0 | } |
1272 | 1 | } |
1273 | 1 | if (!resolved.has_value() && !supports_iceberg_scan_semantics_v2(_scan_params)) { |
1274 | 0 | resolved = format::ColumnDefinition { |
1275 | 0 | .identifier = {}, |
1276 | 0 | .name = filter.field_names[key_idx], |
1277 | 0 | .type = filter.key_types[key_idx], |
1278 | 0 | }; |
1279 | 0 | } |
1280 | 1 | if (!resolved.has_value()) { |
1281 | 1 | return Status::InvalidArgument( |
1282 | 1 | "Iceberg equality delete field id {} is absent from current and historical table " |
1283 | 1 | "schema metadata", |
1284 | 1 | field_id); |
1285 | 1 | } |
1286 | 0 | *table_field = std::move(*resolved); |
1287 | 0 | return Status::OK(); |
1288 | 1 | } |
1289 | | |
1290 | | std::string IcebergTableReader::_delete_file_cache_key(const char* prefix, |
1291 | 54 | const std::string& path) const { |
1292 | 54 | DORIS_CHECK(prefix != nullptr); |
1293 | 54 | std::string fs_name; |
1294 | 54 | if (_current_task != nullptr && _current_task->data_file != nullptr) { |
1295 | 54 | fs_name = _current_task->data_file->fs_name; |
1296 | 54 | } |
1297 | | // Delete descriptors can reuse the same path text in different filesystem namespaces. Encode |
1298 | | // both variable-length strings so neither an fs/path boundary nor equality field-id suffixes |
1299 | | // can be reinterpreted as path content; scan-level credentials/properties are shared here. |
1300 | 54 | std::ostringstream key; |
1301 | 54 | key << prefix << fs_name.size() << ':' << fs_name << ':' << path.size() << ':' << path; |
1302 | 54 | return key.str(); |
1303 | 54 | } |
1304 | | |
1305 | | void IcebergTableReader::_append_equality_delete_row_count_carrier( |
1306 | 16 | format::FileScanRequest* request) { |
1307 | 16 | DORIS_CHECK(request != nullptr); |
1308 | | // Columnar readers establish a filter batch's row count from predicate columns. If all |
1309 | | // equality keys are missing, the predicate consists only of NULL literals and the filter block |
1310 | | // would otherwise have zero rows. Use the virtual row-position column as the carrier instead |
1311 | | // of an arbitrary physical column. For example, a data file may start with an unsupported |
1312 | | // TIME_MILLIS leaf while the query projects only a supported `id`; selecting that TIME leaf as |
1313 | | // a hidden carrier would make Parquet reject a column the query never requested. Row position |
1314 | | // has one value per input row in both Parquet and ORC, is already used by delete predicates, |
1315 | | // and is explicitly excluded from physical logical-type validation. |
1316 | 16 | _append_file_scan_column(request, format::LocalColumnId(format::ROW_POSITION_COLUMN_ID), |
1317 | 16 | &request->predicate_columns); |
1318 | 16 | } |
1319 | | |
1320 | | Status IcebergTableReader::_build_missing_equality_delete_key_expr( |
1321 | | const EqualityDeleteFilter& filter, size_t key_idx, |
1322 | | const EqualityDeleteColumnPath& data_path, format::FileScanRequest* const request, |
1323 | 19 | VExprSPtr* const key_expr) { |
1324 | 19 | DORIS_CHECK(request != nullptr); |
1325 | 19 | DORIS_CHECK(key_expr != nullptr); |
1326 | 19 | auto table_path = _find_table_column_path_by_field_id(filter.field_ids[key_idx], |
1327 | 19 | filter.key_types[key_idx], true); |
1328 | 19 | if (!table_path.has_value() || data_path.size() >= table_path->size()) { |
1329 | 1 | format::ColumnDefinition table_field; |
1330 | 1 | RETURN_IF_ERROR(_find_equality_delete_table_field(filter, key_idx, &table_field)); |
1331 | 0 | table_path = std::vector<format::ColumnDefinition> {std::move(table_field)}; |
1332 | 0 | } |
1333 | 18 | const size_t missing_index = data_path.size() < table_path->size() ? data_path.size() : 0; |
1334 | 18 | auto& missing_root = (*table_path)[missing_index]; |
1335 | 18 | DORIS_CHECK(missing_root.type != nullptr); |
1336 | 18 | VExprSPtr missing_root_expr; |
1337 | 18 | RETURN_IF_ERROR(build_missing_equality_delete_key_expr( |
1338 | 18 | missing_root, missing_root.type, supports_iceberg_scan_semantics_v2(_scan_params), |
1339 | 18 | &missing_root_expr)); |
1340 | 18 | std::vector<const format::ColumnDefinition*> missing_path; |
1341 | 42 | for (size_t path_index = missing_index; path_index < table_path->size(); ++path_index) { |
1342 | 24 | missing_path.push_back(&(*table_path)[path_index]); |
1343 | 24 | } |
1344 | 18 | VExprSPtr default_expr; |
1345 | 18 | RETURN_IF_ERROR(build_nested_equality_delete_key_expr( |
1346 | 18 | missing_path, std::move(missing_root_expr), &default_expr)); |
1347 | 18 | const auto* table_leaf = missing_path.back(); |
1348 | 18 | DORIS_CHECK(table_leaf != nullptr); |
1349 | 18 | DORIS_CHECK(table_leaf->type != nullptr); |
1350 | 18 | if (!table_leaf->type->equals(*filter.key_types[key_idx])) { |
1351 | 1 | auto cast_expr = Cast::create_shared(filter.key_types[key_idx]); |
1352 | 1 | cast_expr->add_child(default_expr); |
1353 | 1 | default_expr = std::move(cast_expr); |
1354 | 1 | } |
1355 | 18 | if (data_path.empty()) { |
1356 | 16 | *key_expr = std::move(default_expr); |
1357 | 16 | return Status::OK(); |
1358 | 16 | } |
1359 | | |
1360 | 2 | const auto* root = data_path.front(); |
1361 | 2 | const auto field_column_id = format::LocalColumnId(root->file_local_id()); |
1362 | 2 | _append_file_scan_column(request, field_column_id, &request->predicate_columns); |
1363 | 2 | const auto block_position = request->local_positions.at(field_column_id).value(); |
1364 | 2 | VExprSPtr ancestor_expr; |
1365 | 2 | RETURN_IF_ERROR(build_equality_delete_key_expr(data_path, block_position, &ancestor_expr)); |
1366 | 2 | auto combined_expr = std::make_shared<AncestorNullDefaultExpr>( |
1367 | 2 | make_nullable(filter.key_types[key_idx]), |
1368 | 2 | ancestor_expr->expr_name() + "." + table_leaf->name); |
1369 | 2 | combined_expr->add_child(ancestor_expr); |
1370 | 2 | combined_expr->add_child(default_expr); |
1371 | 2 | *key_expr = std::move(combined_expr); |
1372 | 2 | return Status::OK(); |
1373 | 2 | } |
1374 | | |
1375 | 72 | Status IcebergTableReader::_append_equality_delete_predicates(format::FileScanRequest* request) { |
1376 | 72 | DORIS_CHECK(request != nullptr); |
1377 | 72 | for (const auto& filter : _equality_delete_filters) { |
1378 | 41 | auto delete_predicate = |
1379 | 41 | std::make_shared<EqualityDeletePredicate>(filter.delete_block, filter.field_ids); |
1380 | 41 | DCHECK_EQ(filter.field_ids.size(), filter.key_types.size()); |
1381 | 41 | bool has_missing_key = false; |
1382 | 81 | for (size_t idx = 0; idx < filter.field_ids.size(); ++idx) { |
1383 | 41 | EqualityDeleteColumnPath data_path; |
1384 | 41 | bool complete_path = false; |
1385 | 41 | RETURN_IF_ERROR( |
1386 | 41 | _find_equality_delete_data_field(filter, idx, &data_path, &complete_path)); |
1387 | 41 | if (!complete_path) { |
1388 | 19 | VExprSPtr key_expr; |
1389 | 19 | RETURN_IF_ERROR(_build_missing_equality_delete_key_expr(filter, idx, data_path, |
1390 | 19 | request, &key_expr)); |
1391 | 18 | delete_predicate->add_child(key_expr); |
1392 | 18 | has_missing_key = true; |
1393 | 18 | continue; |
1394 | 19 | } |
1395 | 22 | const auto* root = data_path.front(); |
1396 | 22 | const auto* field = data_path.back(); |
1397 | 22 | const auto field_column_id = format::LocalColumnId(root->file_local_id()); |
1398 | 22 | _append_file_scan_column(request, field_column_id, &request->predicate_columns); |
1399 | 22 | const auto block_position = request->local_positions.at(field_column_id).value(); |
1400 | 22 | VExprSPtr key_expr; |
1401 | 22 | RETURN_IF_ERROR(build_equality_delete_key_expr(data_path, block_position, &key_expr)); |
1402 | 22 | if (field->type->equals(*filter.key_types[idx])) { |
1403 | 21 | delete_predicate->add_child(key_expr); |
1404 | 21 | } else { |
1405 | 1 | auto cast_expr = Cast::create_shared(filter.key_types[idx]); |
1406 | 1 | cast_expr->add_child(key_expr); |
1407 | 1 | delete_predicate->add_child(cast_expr); |
1408 | 1 | } |
1409 | 22 | } |
1410 | 40 | if (has_missing_key && request->predicate_columns.empty()) { |
1411 | 16 | _append_equality_delete_row_count_carrier(request); |
1412 | 16 | } |
1413 | 40 | request->delete_conjuncts.push_back( |
1414 | 40 | VExprContext::create_shared(std::move(delete_predicate))); |
1415 | 40 | } |
1416 | 71 | return Status::OK(); |
1417 | 72 | } |
1418 | | |
1419 | | Status IcebergTableReader::_create_delete_file_reader(const TIcebergDeleteFileDesc& delete_file, |
1420 | | const TFileScanRangeParams& scan_params, |
1421 | | IcebergDeleteFileIOContext* delete_io_ctx, |
1422 | 52 | std::unique_ptr<format::FileReader>* reader) { |
1423 | 52 | DORIS_CHECK(delete_io_ctx != nullptr); |
1424 | 52 | DORIS_CHECK(reader != nullptr); |
1425 | 52 | if (!delete_file.__isset.file_format) { |
1426 | 0 | return Status::InternalError("Iceberg delete file is missing file format"); |
1427 | 0 | } |
1428 | 52 | if (delete_file.file_format != TFileFormatType::FORMAT_PARQUET && |
1429 | 52 | delete_file.file_format != TFileFormatType::FORMAT_ORC) { |
1430 | 0 | return Status::NotSupported("Unsupported Iceberg delete file format {}", |
1431 | 0 | delete_file.file_format); |
1432 | 0 | } |
1433 | 52 | auto delete_range = build_iceberg_delete_file_range(delete_file.path); |
1434 | 52 | if (_current_task != nullptr && _current_task->data_file != nullptr && |
1435 | 52 | !_current_task->data_file->fs_name.empty()) { |
1436 | 4 | delete_range.__set_fs_name(_current_task->data_file->fs_name); |
1437 | 4 | } |
1438 | 52 | auto system_properties = _delete_file_system_properties(scan_params); |
1439 | 52 | auto file_description = _delete_file_description(delete_range); |
1440 | 52 | std::shared_ptr<io::IOContext> io_ctx(&delete_io_ctx->io_ctx, [](io::IOContext*) {}); |
1441 | 52 | const bool enable_mapping_timestamp_tz = scan_params.__isset.enable_mapping_timestamp_tz && |
1442 | 52 | scan_params.enable_mapping_timestamp_tz; |
1443 | 52 | const bool enable_mapping_varbinary = |
1444 | 52 | scan_params.__isset.enable_mapping_varbinary && scan_params.enable_mapping_varbinary; |
1445 | 52 | if (delete_file.file_format == TFileFormatType::FORMAT_PARQUET) { |
1446 | | // Delete and data files must parse raw binary fields with the same scan-level mapping. |
1447 | 42 | *reader = std::make_unique<format::parquet::ParquetReader>( |
1448 | 42 | system_properties, file_description, io_ctx, _scanner_profile, std::nullopt, |
1449 | 42 | enable_mapping_timestamp_tz, enable_mapping_varbinary); |
1450 | 42 | } else { |
1451 | 10 | *reader = std::make_unique<format::orc::OrcReader>(system_properties, file_description, |
1452 | 10 | io_ctx, _scanner_profile, std::nullopt, |
1453 | 10 | enable_mapping_timestamp_tz); |
1454 | 10 | } |
1455 | 52 | RETURN_IF_ERROR((*reader)->init(_runtime_state)); |
1456 | 52 | return Status::OK(); |
1457 | 52 | } |
1458 | | |
1459 | | Status IcebergTableReader::_read_position_delete_file(const TIcebergDeleteFileDesc& delete_file, |
1460 | | const TFileScanRangeParams& scan_params, |
1461 | | IcebergDeleteFileIOContext* delete_io_ctx, |
1462 | 12 | PositionDeleteRowsCollector* collector) { |
1463 | 12 | DORIS_CHECK(collector != nullptr); |
1464 | 12 | std::unique_ptr<format::FileReader> reader; |
1465 | 12 | RETURN_IF_ERROR(_create_delete_file_reader(delete_file, scan_params, delete_io_ctx, &reader)); |
1466 | 12 | DORIS_CHECK(reader != nullptr); |
1467 | | |
1468 | 12 | std::vector<format::ColumnDefinition> schema; |
1469 | 12 | RETURN_IF_ERROR(reader->get_schema(&schema)); |
1470 | 12 | format::ColumnDefinition* file_path_field = nullptr; |
1471 | 12 | format::ColumnDefinition* pos_field = nullptr; |
1472 | 24 | for (auto& field : schema) { |
1473 | 24 | if (field.name == ICEBERG_FILE_PATH) { |
1474 | 12 | file_path_field = &field; |
1475 | 12 | } else if (field.name == ICEBERG_ROW_POS) { |
1476 | 12 | pos_field = &field; |
1477 | 12 | } |
1478 | 24 | } |
1479 | 12 | if (file_path_field == nullptr || pos_field == nullptr) { |
1480 | 0 | return Status::InternalError("Position delete file is missing required columns"); |
1481 | 0 | } |
1482 | | |
1483 | 12 | auto request = std::make_shared<format::FileScanRequest>(); |
1484 | 12 | request->non_predicate_columns = { |
1485 | 12 | format::LocalColumnIndex::top_level( |
1486 | 12 | format::LocalColumnId(file_path_field->file_local_id())), |
1487 | 12 | format::LocalColumnIndex::top_level(format::LocalColumnId(pos_field->file_local_id()))}; |
1488 | 12 | request->local_positions = { |
1489 | 12 | {format::LocalColumnId(file_path_field->file_local_id()), |
1490 | 12 | format::LocalIndex(ICEBERG_FILE_PATH_BLOCK_POSITION)}, |
1491 | 12 | {format::LocalColumnId(pos_field->file_local_id()), |
1492 | 12 | format::LocalIndex(ICEBERG_ROW_POS_BLOCK_POSITION)}, |
1493 | 12 | }; |
1494 | 12 | RETURN_IF_ERROR(reader->open(request)); |
1495 | | |
1496 | 12 | bool eof = false; |
1497 | 12 | auto build_position_delete_block = [](const format::ColumnDefinition& file_path_field, |
1498 | 22 | const format::ColumnDefinition& pos_field) -> Block { |
1499 | 22 | Block block; |
1500 | 22 | block.insert( |
1501 | 22 | {file_path_field.type->create_column(), file_path_field.type, ICEBERG_FILE_PATH}); |
1502 | 22 | block.insert({pos_field.type->create_column(), pos_field.type, ICEBERG_ROW_POS}); |
1503 | 22 | return block; |
1504 | 22 | }; |
1505 | 32 | while (!eof) { |
1506 | 22 | Block block = build_position_delete_block(*file_path_field, *pos_field); |
1507 | 22 | size_t read_rows = 0; |
1508 | 22 | RETURN_IF_ERROR(reader->get_block(&block, &read_rows, &eof)); |
1509 | 22 | RETURN_IF_ERROR(collector->collect(block, read_rows)); |
1510 | 22 | } |
1511 | 10 | return reader->close(); |
1512 | 12 | } |
1513 | | |
1514 | | Status IcebergTableReader::_init_position_delete_rows( |
1515 | 13 | const std::vector<TIcebergDeleteFileDesc>& delete_files) { |
1516 | 13 | DORIS_CHECK(_split_cache != nullptr); |
1517 | 13 | TFileScanRangeParams delete_scan_params = |
1518 | 13 | _scan_params == nullptr ? TFileScanRangeParams() : *_scan_params; |
1519 | 13 | format::DeleteRows position_delete_rows; |
1520 | 13 | IcebergDeleteFileIOContext delete_io_ctx(_runtime_state); |
1521 | 13 | for (const auto& delete_file : delete_files) { |
1522 | 13 | Status read_status = Status::OK(); |
1523 | | // A position delete file normally references many data files. Cache the complete |
1524 | | // path-to-position map once; caching only the current data file would still rescan the |
1525 | | // shared delete file for every subsequent split. |
1526 | 13 | auto* rows_by_data_file = |
1527 | 13 | _split_cache->get<PositionDeleteRowsCollector::PositionDeleteFile>( |
1528 | 13 | _delete_file_cache_key("iceberg_v2_position_delete_", delete_file.path), |
1529 | 13 | [&]() -> PositionDeleteRowsCollector::PositionDeleteFile* { |
1530 | 12 | auto result = std::make_unique< |
1531 | 12 | PositionDeleteRowsCollector::PositionDeleteFile>(); |
1532 | 12 | PositionDeleteRowsCollector collector(result.get()); |
1533 | 12 | read_status = _read_position_delete_file( |
1534 | 12 | delete_file, delete_scan_params, &delete_io_ctx, &collector); |
1535 | 12 | if (!read_status.ok()) { |
1536 | 2 | return nullptr; |
1537 | 2 | } |
1538 | 12 | for (auto& [_, rows] : *result) { |
1539 | 12 | std::ranges::sort(rows); |
1540 | 12 | } |
1541 | 10 | return result.release(); |
1542 | 12 | }); |
1543 | 13 | RETURN_IF_ERROR(read_status); |
1544 | 11 | DORIS_CHECK(rows_by_data_file != nullptr); |
1545 | 11 | const auto rows_it = rows_by_data_file->find(_data_file_path()); |
1546 | 11 | if (rows_it == rows_by_data_file->end()) { |
1547 | 0 | continue; |
1548 | 0 | } |
1549 | 11 | auto first = rows_it->second.begin(); |
1550 | 11 | auto last = rows_it->second.end(); |
1551 | | // Bounds are inclusive Iceberg position statistics supplied by FE. Apply them after the |
1552 | | // cached per-data-file vector is sorted so irrelevant positions are sliced without a scan. |
1553 | 11 | if (delete_file.__isset.position_lower_bound) { |
1554 | 1 | first = std::lower_bound(first, last, delete_file.position_lower_bound); |
1555 | 1 | } |
1556 | 11 | if (delete_file.__isset.position_upper_bound) { |
1557 | 1 | last = std::upper_bound(first, last, delete_file.position_upper_bound); |
1558 | 1 | } |
1559 | 11 | position_delete_rows.insert(position_delete_rows.end(), first, last); |
1560 | 11 | } |
1561 | 11 | if (position_delete_rows.empty()) { |
1562 | 0 | return Status::OK(); |
1563 | 0 | } |
1564 | | // Position delete files and deletion vectors both become row-position deletes for the |
1565 | | // common TableReader DeletePredicate path. Keep the merged rows in a member vector because |
1566 | | // DeletePredicate stores a reference to the vector used by _delete_rows. |
1567 | 11 | _position_delete_rows_storage.insert(_position_delete_rows_storage.end(), |
1568 | 11 | position_delete_rows.begin(), position_delete_rows.end()); |
1569 | 11 | std::sort(_position_delete_rows_storage.begin(), _position_delete_rows_storage.end()); |
1570 | 11 | _position_delete_rows_storage.erase( |
1571 | 11 | std::unique(_position_delete_rows_storage.begin(), _position_delete_rows_storage.end()), |
1572 | 11 | _position_delete_rows_storage.end()); |
1573 | 11 | _delete_rows = &_position_delete_rows_storage; |
1574 | 11 | return Status::OK(); |
1575 | 11 | } |
1576 | | |
1577 | | Status IcebergTableReader::_init_equality_delete_predicates( |
1578 | 41 | const std::vector<TIcebergDeleteFileDesc>& delete_files) { |
1579 | 41 | DORIS_CHECK(_split_cache != nullptr); |
1580 | 41 | TFileScanRangeParams delete_scan_params = |
1581 | 41 | _scan_params == nullptr ? TFileScanRangeParams() : *_scan_params; |
1582 | 41 | IcebergDeleteFileIOContext delete_io_ctx(_runtime_state); |
1583 | 41 | for (const auto& delete_file : delete_files) { |
1584 | 41 | RETURN_IF_ERROR( |
1585 | 41 | _read_equality_delete_file(delete_file, delete_scan_params, &delete_io_ctx)); |
1586 | 41 | } |
1587 | 41 | return Status::OK(); |
1588 | 41 | } |
1589 | | |
1590 | | Status IcebergTableReader::_resolve_equality_delete_fields( |
1591 | | const TIcebergDeleteFileDesc& delete_file, |
1592 | | const std::vector<format::ColumnDefinition>& schema, |
1593 | 40 | std::vector<EqualityDeleteColumnPath>* delete_paths, EqualityDeleteFilter* result) const { |
1594 | 40 | DORIS_CHECK(delete_paths != nullptr); |
1595 | 40 | DORIS_CHECK(result != nullptr); |
1596 | 40 | for (const auto field_id : delete_file.field_ids) { |
1597 | 40 | EqualityDeleteColumnPath path; |
1598 | 40 | if (!find_equality_delete_column_path(schema, field_id, &path)) { |
1599 | 0 | return Status::InternalError("Can not find field id {} in equality delete file {}", |
1600 | 0 | field_id, delete_file.path); |
1601 | 0 | } |
1602 | 40 | const auto* field = path.back(); |
1603 | 40 | if (!field->children.empty()) { |
1604 | 0 | return Status::NotSupported( |
1605 | 0 | "Iceberg equality delete does not support complex column {}", field->name); |
1606 | 0 | } |
1607 | 40 | const auto key_type = path.size() > 1 ? make_nullable(field->type) : field->type; |
1608 | 40 | delete_paths->push_back(std::move(path)); |
1609 | 40 | result->field_ids.push_back(field_id); |
1610 | 40 | result->field_names.push_back(field->name); |
1611 | 40 | result->key_types.push_back(key_type); |
1612 | 40 | } |
1613 | 40 | return Status::OK(); |
1614 | 40 | } |
1615 | | |
1616 | | Status IcebergTableReader::_load_equality_delete_file(const TIcebergDeleteFileDesc& delete_file, |
1617 | | const TFileScanRangeParams& scan_params, |
1618 | | IcebergDeleteFileIOContext* delete_io_ctx, |
1619 | 40 | EqualityDeleteFilter* result) { |
1620 | 40 | DORIS_CHECK(result != nullptr); |
1621 | 40 | std::unique_ptr<format::FileReader> reader; |
1622 | 40 | RETURN_IF_ERROR(_create_delete_file_reader(delete_file, scan_params, delete_io_ctx, &reader)); |
1623 | 40 | DORIS_CHECK(reader != nullptr); |
1624 | | |
1625 | 40 | std::vector<format::ColumnDefinition> schema; |
1626 | 40 | RETURN_IF_ERROR(reader->get_schema(&schema)); |
1627 | 40 | std::vector<EqualityDeleteColumnPath> delete_paths; |
1628 | 40 | RETURN_IF_ERROR(_resolve_equality_delete_fields(delete_file, schema, &delete_paths, result)); |
1629 | | |
1630 | 40 | auto request = std::make_shared<format::FileScanRequest>(); |
1631 | 40 | format::FileScanRequestBuilder request_builder(request.get()); |
1632 | 40 | for (const auto& path : delete_paths) { |
1633 | 40 | DORIS_CHECK(!path.empty()); |
1634 | 40 | RETURN_IF_ERROR(request_builder.add_non_predicate_column( |
1635 | 40 | format::LocalColumnId(path.front()->file_local_id()))); |
1636 | 40 | } |
1637 | 40 | Block file_block_template; |
1638 | 40 | std::vector<const format::ColumnDefinition*> roots(request->local_positions.size()); |
1639 | 40 | for (const auto& path : delete_paths) { |
1640 | 40 | const auto* root = path.front(); |
1641 | 40 | const auto position = |
1642 | 40 | request->local_positions.at(format::LocalColumnId(root->file_local_id())); |
1643 | 40 | roots[position.value()] = root; |
1644 | 40 | } |
1645 | 40 | for (const auto* root : roots) { |
1646 | 40 | DORIS_CHECK(root != nullptr); |
1647 | 40 | file_block_template.insert({root->type->create_column(), root->type, root->name}); |
1648 | 40 | } |
1649 | | |
1650 | 40 | std::vector<VExprContextSPtr> key_exprs; |
1651 | 40 | key_exprs.reserve(delete_paths.size()); |
1652 | 40 | RowDescriptor row_desc; |
1653 | 40 | for (const auto& path : delete_paths) { |
1654 | 40 | const auto root_column_id = format::LocalColumnId(path.front()->file_local_id()); |
1655 | 40 | VExprSPtr key_expr; |
1656 | 40 | RETURN_IF_ERROR(build_equality_delete_key_expr( |
1657 | 40 | path, request->local_positions.at(root_column_id).value(), &key_expr)); |
1658 | 40 | auto context = VExprContext::create_shared(std::move(key_expr)); |
1659 | 40 | RETURN_IF_ERROR(context->prepare(_runtime_state, row_desc)); |
1660 | 40 | RETURN_IF_ERROR(context->open(_runtime_state)); |
1661 | 40 | key_exprs.push_back(std::move(context)); |
1662 | 40 | } |
1663 | 40 | RETURN_IF_ERROR(reader->open(request)); |
1664 | | |
1665 | 40 | Block delete_block_template; |
1666 | 80 | for (size_t index = 0; index < delete_paths.size(); ++index) { |
1667 | 40 | const auto* field = delete_paths[index].back(); |
1668 | 40 | const auto& key_type = result->key_types[index]; |
1669 | 40 | delete_block_template.insert({key_type->create_column(), key_type, field->name}); |
1670 | 40 | } |
1671 | 40 | MutableBlock mutable_delete_block(delete_block_template.clone_empty()); |
1672 | 40 | bool eof = false; |
1673 | 120 | while (!eof) { |
1674 | 80 | Block block = file_block_template.clone_empty(); |
1675 | 80 | size_t read_rows = 0; |
1676 | 80 | RETURN_IF_ERROR(reader->get_block(&block, &read_rows, &eof)); |
1677 | 80 | if (read_rows > 0) { |
1678 | 40 | Block key_block; |
1679 | 40 | for (const auto& context : key_exprs) { |
1680 | 40 | ColumnWithTypeAndName key; |
1681 | 40 | RETURN_IF_ERROR(context->execute(&block, key)); |
1682 | 40 | key_block.insert(std::move(key)); |
1683 | 40 | } |
1684 | 40 | RETURN_IF_ERROR(mutable_delete_block.merge(key_block)); |
1685 | 40 | } |
1686 | 80 | } |
1687 | 40 | RETURN_IF_ERROR(reader->close()); |
1688 | 40 | result->delete_block = mutable_delete_block.to_block(); |
1689 | 40 | return Status::OK(); |
1690 | 40 | } |
1691 | | |
1692 | | Status IcebergTableReader::_read_equality_delete_file(const TIcebergDeleteFileDesc& delete_file, |
1693 | | const TFileScanRangeParams& scan_params, |
1694 | 41 | IcebergDeleteFileIOContext* delete_io_ctx) { |
1695 | 41 | if (!delete_file.__isset.field_ids || delete_file.field_ids.empty()) { |
1696 | 0 | return Status::InternalError("Iceberg equality delete file is missing field ids"); |
1697 | 0 | } |
1698 | 41 | std::ostringstream cache_key; |
1699 | 41 | cache_key << _delete_file_cache_key("iceberg_v2_equality_delete_", delete_file.path); |
1700 | 41 | cache_key << ':' << delete_file.field_ids.size(); |
1701 | 41 | for (const auto field_id : delete_file.field_ids) { |
1702 | 41 | cache_key << ':' << field_id; |
1703 | 41 | } |
1704 | 41 | Status read_status = Status::OK(); |
1705 | | // Include the ordered equality ids in the key because the same physical delete file can be |
1706 | | // projected with different key layouts. The cached block and its key metadata are immutable |
1707 | | // after construction and therefore safe to copy into each split-local predicate. |
1708 | 41 | auto* cached_filter = _split_cache->get<EqualityDeleteFilter>( |
1709 | 41 | cache_key.str(), [&]() -> EqualityDeleteFilter* { |
1710 | 40 | auto result = std::make_unique<EqualityDeleteFilter>(); |
1711 | 40 | read_status = _load_equality_delete_file(delete_file, scan_params, delete_io_ctx, |
1712 | 40 | result.get()); |
1713 | 40 | if (!read_status.ok()) { |
1714 | 0 | return nullptr; |
1715 | 0 | } |
1716 | 40 | return result.release(); |
1717 | 40 | }); |
1718 | 41 | RETURN_IF_ERROR(read_status); |
1719 | 41 | DORIS_CHECK(cached_filter != nullptr); |
1720 | 41 | _equality_delete_filters.push_back(*cached_filter); |
1721 | 41 | return Status::OK(); |
1722 | 41 | } |
1723 | | |
1724 | 9 | Status IcebergTableReader::_materialize_row_lineage_row_id(Block* table_block, size_t column_idx) { |
1725 | 9 | if (_row_lineage_columns.first_row_id < 0) { |
1726 | 2 | return Status::OK(); |
1727 | 2 | } |
1728 | 7 | DORIS_CHECK(_row_position_block_position < _data_reader.block_template.columns()); |
1729 | 7 | const auto& row_position_column = assert_cast<const ColumnInt64&>( |
1730 | 7 | *_data_reader.block_template.get_by_position(_row_position_block_position).column); |
1731 | 7 | DORIS_CHECK(row_position_column.size() == table_block->rows()); |
1732 | 7 | auto column = IColumn::mutate( |
1733 | 7 | table_block->get_by_position(column_idx).column->convert_to_full_column_if_const()); |
1734 | 7 | auto* nullable_column = assert_cast<ColumnNullable*>(column.get()); |
1735 | 7 | auto& null_map = nullable_column->get_null_map_data(); |
1736 | 7 | auto& data = assert_cast<ColumnInt64&>(*nullable_column->get_nested_column_ptr()).get_data(); |
1737 | 7 | DORIS_CHECK(null_map.size() == row_position_column.size()); |
1738 | 7 | DORIS_CHECK(data.size() == row_position_column.size()); |
1739 | 23 | for (size_t row = 0; row < row_position_column.size(); ++row) { |
1740 | 16 | if (null_map[row]) { |
1741 | 10 | null_map[row] = 0; |
1742 | 10 | data[row] = _row_lineage_columns.first_row_id + row_position_column.get_element(row); |
1743 | 10 | } |
1744 | 16 | } |
1745 | 7 | table_block->replace_by_position(column_idx, std::move(column)); |
1746 | 7 | return Status::OK(); |
1747 | 9 | } |
1748 | | |
1749 | 1 | Status IcebergTableReader::_materialize_iceberg_rowid(Block* table_block, size_t column_idx) { |
1750 | 1 | DORIS_CHECK(_row_position_block_position < _data_reader.block_template.columns()); |
1751 | 1 | const auto& row_position_column = assert_cast<const ColumnInt64&>( |
1752 | 1 | *_data_reader.block_template.get_by_position(_row_position_block_position).column); |
1753 | 1 | DORIS_CHECK(row_position_column.size() == table_block->rows()); |
1754 | | |
1755 | 1 | const auto& type = table_block->get_by_position(column_idx).type; |
1756 | 1 | auto column = type->create_column(); |
1757 | 1 | auto* nullable_column = check_and_get_column<ColumnNullable>(column.get()); |
1758 | 1 | auto* struct_column = nullable_column != nullptr |
1759 | 1 | ? check_and_get_column<ColumnStruct>( |
1760 | 1 | nullable_column->get_nested_column_ptr().get()) |
1761 | 1 | : check_and_get_column<ColumnStruct>(column.get()); |
1762 | 1 | DORIS_CHECK(struct_column != nullptr); |
1763 | 1 | DORIS_CHECK(struct_column->tuple_size() >= 4); |
1764 | | |
1765 | 1 | const auto rows = row_position_column.size(); |
1766 | 1 | const auto file_path = _data_file_path(); |
1767 | 1 | const int32_t partition_spec_id = |
1768 | 1 | _iceberg_params.has_value() && _iceberg_params->__isset.partition_spec_id |
1769 | 1 | ? _iceberg_params->partition_spec_id |
1770 | 1 | : 0; |
1771 | 1 | const std::string partition_data_json = |
1772 | 1 | _iceberg_params.has_value() && _iceberg_params->__isset.partition_data_json |
1773 | 1 | ? _iceberg_params->partition_data_json |
1774 | 1 | : ""; |
1775 | | |
1776 | 1 | auto& file_path_column = struct_column->get_column(0); |
1777 | 1 | auto& row_pos_column = struct_column->get_column(1); |
1778 | 1 | auto& spec_id_column = struct_column->get_column(2); |
1779 | 1 | auto& partition_data_column = struct_column->get_column(3); |
1780 | 1 | file_path_column.reserve(rows); |
1781 | 1 | row_pos_column.reserve(rows); |
1782 | 1 | spec_id_column.reserve(rows); |
1783 | 1 | partition_data_column.reserve(rows); |
1784 | 3 | for (size_t row = 0; row < rows; ++row) { |
1785 | 2 | file_path_column.insert_data(file_path.data(), file_path.size()); |
1786 | 2 | const int64_t row_pos = row_position_column.get_element(row); |
1787 | 2 | row_pos_column.insert_data(reinterpret_cast<const char*>(&row_pos), sizeof(row_pos)); |
1788 | 2 | spec_id_column.insert_data(reinterpret_cast<const char*>(&partition_spec_id), |
1789 | 2 | sizeof(partition_spec_id)); |
1790 | 2 | partition_data_column.insert_data(partition_data_json.data(), partition_data_json.size()); |
1791 | 2 | } |
1792 | 1 | if (nullable_column != nullptr) { |
1793 | 1 | nullable_column->get_null_map_data().resize_fill(rows, 0); |
1794 | 1 | } |
1795 | 1 | table_block->replace_by_position(column_idx, std::move(column)); |
1796 | 1 | return Status::OK(); |
1797 | 1 | } |
1798 | | |
1799 | | Status IcebergTableReader::_materialize_row_lineage_last_updated_sequence_number( |
1800 | 8 | Block* table_block, size_t column_idx) { |
1801 | 8 | if (_row_lineage_columns.last_updated_sequence_number < 0) { |
1802 | 2 | return Status::OK(); |
1803 | 2 | } |
1804 | 6 | auto column = IColumn::mutate( |
1805 | 6 | table_block->get_by_position(column_idx).column->convert_to_full_column_if_const()); |
1806 | 6 | auto* nullable_column = assert_cast<ColumnNullable*>(column.get()); |
1807 | 6 | auto& null_map = nullable_column->get_null_map_data(); |
1808 | 6 | auto& data = assert_cast<ColumnInt64&>(*nullable_column->get_nested_column_ptr()).get_data(); |
1809 | 6 | DORIS_CHECK(null_map.size() == table_block->rows()); |
1810 | 6 | DORIS_CHECK(data.size() == table_block->rows()); |
1811 | 20 | for (size_t row = 0; row < table_block->rows(); ++row) { |
1812 | 14 | if (null_map[row]) { |
1813 | 8 | null_map[row] = 0; |
1814 | 8 | data[row] = _row_lineage_columns.last_updated_sequence_number; |
1815 | 8 | } |
1816 | 14 | } |
1817 | 6 | table_block->replace_by_position(column_idx, std::move(column)); |
1818 | 6 | return Status::OK(); |
1819 | 8 | } |
1820 | | |
1821 | 8 | bool IcebergTableReader::_need_row_lineage_row_id() const { |
1822 | 8 | if (_data_reader.column_mapper != nullptr) { |
1823 | 7 | for (const auto& mapping : _data_reader.column_mapper->mappings()) { |
1824 | 7 | if (mapping.virtual_column_type == format::TableVirtualColumnType::ROW_ID) { |
1825 | 7 | return true; |
1826 | 7 | } |
1827 | 7 | } |
1828 | 7 | } |
1829 | 1 | return std::ranges::any_of(_projected_columns, is_projected_row_lineage_row_id); |
1830 | 8 | } |
1831 | | |
1832 | 64 | bool IcebergTableReader::_need_iceberg_rowid() const { |
1833 | 64 | if (_data_reader.column_mapper != nullptr) { |
1834 | 68 | for (const auto& mapping : _data_reader.column_mapper->mappings()) { |
1835 | 68 | if (mapping.virtual_column_type == format::TableVirtualColumnType::ICEBERG_ROWID) { |
1836 | 1 | return true; |
1837 | 1 | } |
1838 | 68 | } |
1839 | 64 | } |
1840 | 63 | return std::ranges::any_of(_projected_columns, is_projected_iceberg_rowid); |
1841 | 64 | } |
1842 | | |
1843 | | } // namespace doris::format::iceberg |