Coverage Report

Created: 2026-07-29 06:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format/table/iceberg_default_value.h
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
#pragma once
19
20
#include <gen_cpp/ExternalTableSchema_types.h>
21
#include <rapidjson/document.h>
22
#include <rapidjson/stringbuffer.h>
23
#include <rapidjson/writer.h>
24
25
#include <cstddef>
26
#include <deque>
27
#include <string>
28
#include <string_view>
29
#include <unordered_map>
30
#include <utility>
31
32
#include "common/status.h"
33
#include "core/assert_cast.h"
34
#include "core/column/column.h"
35
#include "core/data_type/data_type.h"
36
#include "core/data_type/data_type_array.h"
37
#include "core/data_type/data_type_map.h"
38
#include "core/data_type/data_type_nullable.h"
39
#include "core/data_type/data_type_struct.h"
40
#include "core/data_type/primitive_type.h"
41
#include "core/field.h"
42
#include "util/string_util.h"
43
#include "util/url_coding.h"
44
45
namespace doris::iceberg {
46
47
namespace detail {
48
49
15
inline const schema::external::TField* get_field_ptr(const schema::external::TFieldPtr& field_ptr) {
50
15
    if (!field_ptr.__isset.field_ptr || field_ptr.field_ptr == nullptr) {
51
0
        return nullptr;
52
0
    }
53
15
    return field_ptr.field_ptr.get();
54
15
}
55
56
inline const schema::external::TField* find_struct_child(
57
6
        const schema::external::TStructField& struct_field, const std::string& name) {
58
6
    if (!struct_field.__isset.fields) {
59
0
        return nullptr;
60
0
    }
61
10
    for (const auto& child_ptr : struct_field.fields) {
62
10
        const auto* child = get_field_ptr(child_ptr);
63
10
        if (child != nullptr && child->__isset.name && iequal(child->name, name)) {
64
4
            return child;
65
4
        }
66
10
    }
67
2
    for (const auto& child_ptr : struct_field.fields) {
68
2
        const auto* child = get_field_ptr(child_ptr);
69
2
        if (child == nullptr || !child->__isset.name_mapping) {
70
0
            continue;
71
0
        }
72
2
        for (const auto& alias : child->name_mapping) {
73
2
            if (iequal(alias, name)) {
74
2
                return child;
75
2
            }
76
2
        }
77
2
    }
78
0
    return nullptr;
79
2
}
80
81
76
inline int hex_value(char c) {
82
76
    if (c >= '0' && c <= '9') {
83
52
        return c - '0';
84
52
    }
85
24
    if (c >= 'a' && c <= 'f') {
86
17
        return c - 'a' + 10;
87
17
    }
88
7
    if (c >= 'A' && c <= 'F') {
89
6
        return c - 'A' + 10;
90
6
    }
91
1
    return -1;
92
7
}
93
94
5
inline Status decode_hex(std::string_view encoded, std::string* decoded) {
95
5
    DORIS_CHECK(decoded != nullptr);
96
5
    if ((encoded.size() & 1U) != 0) {
97
1
        return Status::InvalidArgument("Invalid odd-length Iceberg binary default");
98
1
    }
99
4
    decoded->resize(encoded.size() / 2);
100
41
    for (size_t index = 0; index < encoded.size(); index += 2) {
101
38
        const int high = hex_value(encoded[index]);
102
38
        const int low = hex_value(encoded[index + 1]);
103
38
        if (high < 0 || low < 0) {
104
1
            return Status::InvalidArgument("Invalid hexadecimal Iceberg binary default");
105
1
        }
106
37
        (*decoded)[index / 2] = static_cast<char>((high << 4) | low);
107
37
    }
108
3
    return Status::OK();
109
4
}
110
111
2
inline Status decode_json_binary(std::string_view encoded, std::string* decoded) {
112
2
    DORIS_CHECK(decoded != nullptr);
113
2
    const bool is_uuid = encoded.size() == 36 && encoded[8] == '-' && encoded[13] == '-' &&
114
2
                         encoded[18] == '-' && encoded[23] == '-';
115
2
    if (is_uuid) {
116
1
        std::string uuid_hex;
117
1
        uuid_hex.reserve(32);
118
37
        for (size_t index = 0; index < encoded.size(); ++index) {
119
36
            if (index != 8 && index != 13 && index != 18 && index != 23) {
120
32
                uuid_hex.push_back(encoded[index]);
121
32
            }
122
36
        }
123
1
        return decode_hex(uuid_hex, decoded);
124
1
    }
125
1
    return decode_hex(encoded, decoded);
126
2
}
127
128
3
inline std::string json_scalar_text(const rapidjson::Value& value) {
129
3
    if (value.IsString()) {
130
2
        return {value.GetString(), value.GetStringLength()};
131
2
    }
132
1
    rapidjson::StringBuffer buffer;
133
1
    rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
134
1
    value.Accept(writer);
135
1
    return {buffer.GetString(), buffer.GetSize()};
136
3
}
137
138
6
inline void normalize_timestamp_for_doris(PrimitiveType primitive_type, std::string* value) {
139
6
    if (primitive_type != TYPE_DATETIME && primitive_type != TYPE_DATETIMEV2 &&
140
6
        primitive_type != TYPE_TIMESTAMPTZ) {
141
2
        return;
142
2
    }
143
4
    if (const size_t separator = value->find('T'); separator != std::string::npos) {
144
3
        (*value)[separator] = ' ';
145
3
    }
146
4
    if (primitive_type == TYPE_TIMESTAMPTZ) {
147
1
        return;
148
1
    }
149
3
    if (value->ends_with('Z')) {
150
1
        value->pop_back();
151
1
        return;
152
1
    }
153
2
    const size_t time_start = value->find(' ');
154
2
    if (time_start == std::string::npos) {
155
1
        return;
156
1
    }
157
1
    const size_t offset = value->find_first_of("+-", time_start + 1);
158
1
    if (offset != std::string::npos) {
159
1
        value->erase(offset);
160
1
    }
161
1
}
162
163
inline Status make_null_field(const schema::external::TField& field, const DataTypePtr& data_type,
164
8
                              Field* result) {
165
8
    DORIS_CHECK(data_type != nullptr);
166
8
    DORIS_CHECK(result != nullptr);
167
8
    if (field.__isset.is_optional && !field.is_optional) {
168
1
        return Status::InvalidArgument("Required Iceberg field '{}' has a null default",
169
1
                                       field.name);
170
1
    }
171
7
    if (!data_type->is_nullable()) {
172
1
        return Status::InternalError(
173
1
                "Optional Iceberg field '{}' has a null default, but its Doris type '{}' is not "
174
1
                "nullable",
175
1
                field.name, data_type->get_name());
176
1
    }
177
6
    *result = Field();
178
6
    return Status::OK();
179
7
}
180
181
inline Status build_initial_default_field(const schema::external::TField& field,
182
                                          const DataTypePtr& data_type,
183
                                          std::deque<std::string>* binary_storage, Field* result);
184
185
inline Status build_json_default_field(const schema::external::TField& field,
186
                                       const DataTypePtr& data_type,
187
                                       const rapidjson::Value& json_value,
188
                                       std::deque<std::string>* binary_storage, Field* result);
189
190
inline Status build_json_struct_default(const schema::external::TField& field,
191
                                        const DataTypePtr& value_type,
192
                                        const rapidjson::Value& json_value,
193
4
                                        std::deque<std::string>* binary_storage, Field* result) {
194
4
    if (!json_value.IsObject() || !field.__isset.nestedField ||
195
4
        !field.nestedField.__isset.struct_field || !field.nestedField.struct_field.__isset.fields) {
196
0
        return Status::InvalidArgument("Invalid Iceberg struct default for field '{}'", field.name);
197
0
    }
198
199
4
    const auto& struct_type = assert_cast<const DataTypeStruct&>(*value_type);
200
4
    Struct struct_value;
201
4
    struct_value.reserve(struct_type.get_elements().size());
202
10
    for (size_t index = 0; index < struct_type.get_elements().size(); ++index) {
203
6
        const auto& child_name = struct_type.get_element_name(index);
204
6
        const auto* child = find_struct_child(field.nestedField.struct_field, child_name);
205
6
        if (child == nullptr || !child->__isset.id) {
206
0
            return Status::InvalidArgument(
207
0
                    "Iceberg struct default for field '{}' is missing metadata for projected "
208
0
                    "child '{}'",
209
0
                    field.name, child_name);
210
0
        }
211
212
6
        const std::string child_id = std::to_string(child->id);
213
6
        const auto member = json_value.FindMember(child_id.c_str());
214
6
        Field child_value;
215
6
        if (member == json_value.MemberEnd()) {
216
6
            RETURN_IF_ERROR(build_initial_default_field(*child, struct_type.get_element(index),
217
6
                                                        binary_storage, &child_value));
218
6
        } else {
219
0
            RETURN_IF_ERROR(build_json_default_field(*child, struct_type.get_element(index),
220
0
                                                     member->value, binary_storage, &child_value));
221
0
        }
222
6
        struct_value.push_back(std::move(child_value));
223
6
    }
224
4
    *result = Field::create_field<TYPE_STRUCT>(std::move(struct_value));
225
4
    return Status::OK();
226
4
}
227
228
// The recursive item TField describes the element schema and its field-level default metadata. It
229
// cannot represent a particular list literal's length or per-position values, so the parent
230
// initial-default keeps those values in Iceberg's single-value JSON array.
231
inline Status build_json_array_default(const schema::external::TField& field,
232
                                       const DataTypePtr& value_type,
233
                                       const rapidjson::Value& json_value,
234
1
                                       std::deque<std::string>* binary_storage, Field* result) {
235
1
    if (!json_value.IsArray() || !field.__isset.nestedField ||
236
1
        !field.nestedField.__isset.array_field ||
237
1
        !field.nestedField.array_field.__isset.item_field) {
238
0
        return Status::InvalidArgument("Invalid Iceberg list default for field '{}'", field.name);
239
0
    }
240
1
    const auto* element = get_field_ptr(field.nestedField.array_field.item_field);
241
1
    if (element == nullptr) {
242
0
        return Status::InvalidArgument(
243
0
                "Iceberg list default for field '{}' has incomplete element metadata", field.name);
244
0
    }
245
246
1
    const auto& array_type = assert_cast<const DataTypeArray&>(*value_type);
247
1
    Array array_value;
248
1
    array_value.reserve(json_value.Size());
249
2
    for (const auto& json_element : json_value.GetArray()) {
250
2
        Field element_value;
251
2
        RETURN_IF_ERROR(build_json_default_field(*element, array_type.get_nested_type(),
252
2
                                                 json_element, binary_storage, &element_value));
253
2
        array_value.push_back(std::move(element_value));
254
2
    }
255
1
    *result = Field::create_field<TYPE_ARRAY>(std::move(array_value));
256
1
    return Status::OK();
257
1
}
258
259
// The recursive key/value TFields describe entry schemas and field-level default metadata. They
260
// cannot represent the number, order, or concrete values of map entries, so the parent
261
// initial-default keeps the entries in Iceberg's single-value JSON key/value arrays.
262
inline Status build_json_map_default(const schema::external::TField& field,
263
                                     const DataTypePtr& value_type,
264
                                     const rapidjson::Value& json_value,
265
1
                                     std::deque<std::string>* binary_storage, Field* result) {
266
1
    if (!json_value.IsObject() || !json_value.HasMember("keys") || !json_value["keys"].IsArray() ||
267
1
        !json_value.HasMember("values") || !json_value["values"].IsArray() ||
268
1
        !field.__isset.nestedField || !field.nestedField.__isset.map_field ||
269
1
        !field.nestedField.map_field.__isset.key_field ||
270
1
        !field.nestedField.map_field.__isset.value_field) {
271
0
        return Status::InvalidArgument("Invalid Iceberg map default for field '{}'", field.name);
272
0
    }
273
1
    const auto& keys = json_value["keys"];
274
1
    const auto& values = json_value["values"];
275
1
    if (keys.Size() != values.Size()) {
276
0
        return Status::InvalidArgument(
277
0
                "Iceberg map default for field '{}' has {} keys but {} values", field.name,
278
0
                keys.Size(), values.Size());
279
0
    }
280
281
1
    const auto* key = get_field_ptr(field.nestedField.map_field.key_field);
282
1
    const auto* value = get_field_ptr(field.nestedField.map_field.value_field);
283
1
    if (key == nullptr || value == nullptr) {
284
0
        return Status::InvalidArgument(
285
0
                "Iceberg map default for field '{}' has incomplete key/value metadata", field.name);
286
0
    }
287
288
1
    const auto& map_type = assert_cast<const DataTypeMap&>(*value_type);
289
1
    Array key_fields;
290
1
    Array value_fields;
291
1
    key_fields.reserve(keys.Size());
292
1
    value_fields.reserve(values.Size());
293
2
    for (rapidjson::SizeType index = 0; index < keys.Size(); ++index) {
294
1
        Field key_value;
295
1
        Field mapped_value;
296
1
        RETURN_IF_ERROR(build_json_default_field(*key, map_type.get_key_type(), keys[index],
297
1
                                                 binary_storage, &key_value));
298
1
        RETURN_IF_ERROR(build_json_default_field(*value, map_type.get_value_type(), values[index],
299
1
                                                 binary_storage, &mapped_value));
300
1
        key_fields.push_back(std::move(key_value));
301
1
        value_fields.push_back(std::move(mapped_value));
302
1
    }
303
1
    Map map_value;
304
1
    map_value.push_back(Field::create_field<TYPE_ARRAY>(std::move(key_fields)));
305
1
    map_value.push_back(Field::create_field<TYPE_ARRAY>(std::move(value_fields)));
306
1
    *result = Field::create_field<TYPE_MAP>(std::move(map_value));
307
1
    return Status::OK();
308
1
}
309
310
inline Status build_json_scalar_default(const schema::external::TField& field,
311
                                        const DataTypePtr& value_type,
312
                                        const rapidjson::Value& json_value,
313
3
                                        std::deque<std::string>* binary_storage, Field* result) {
314
3
    const auto primitive_type = value_type->get_primitive_type();
315
3
    std::string serialized_value = json_scalar_text(json_value);
316
3
    const bool binary_like = (field.__isset.initial_default_value_is_base64 &&
317
3
                              field.initial_default_value_is_base64) ||
318
3
                             primitive_type == TYPE_VARBINARY;
319
3
    if (binary_like) {
320
1
        if (!json_value.IsString()) {
321
0
            return Status::InvalidArgument(
322
0
                    "Iceberg binary default for field '{}' is not a JSON string", field.name);
323
0
        }
324
1
        binary_storage->emplace_back();
325
1
        RETURN_IF_ERROR(decode_json_binary(serialized_value, &binary_storage->back()));
326
1
        if (primitive_type == TYPE_VARBINARY) {
327
0
            *result = Field::create_field<TYPE_VARBINARY>(StringView(binary_storage->back()));
328
1
        } else if (is_string_type(primitive_type)) {
329
1
            *result = Field::create_field<TYPE_STRING>(binary_storage->back());
330
1
        } else {
331
0
            return Status::InvalidArgument(
332
0
                    "Iceberg binary default for field '{}' has incompatible Doris type '{}'",
333
0
                    field.name, value_type->get_name());
334
0
        }
335
1
        return Status::OK();
336
1
    }
337
338
2
    if (is_string_type(primitive_type)) {
339
1
        if (!json_value.IsString()) {
340
0
            return Status::InvalidArgument("Iceberg string default for field '{}' is not a string",
341
0
                                           field.name);
342
0
        }
343
1
        *result = Field::create_field<TYPE_STRING>(std::move(serialized_value));
344
1
        return Status::OK();
345
1
    }
346
1
    normalize_timestamp_for_doris(primitive_type, &serialized_value);
347
1
    RETURN_IF_ERROR(value_type->get_serde()->from_fe_string(serialized_value, *result));
348
1
    return Status::OK();
349
1
}
350
351
inline Status build_json_default_field(const schema::external::TField& field,
352
                                       const DataTypePtr& data_type,
353
                                       const rapidjson::Value& json_value,
354
10
                                       std::deque<std::string>* binary_storage, Field* result) {
355
10
    DORIS_CHECK(data_type != nullptr);
356
10
    DORIS_CHECK(binary_storage != nullptr);
357
10
    DORIS_CHECK(result != nullptr);
358
10
    if (json_value.IsNull()) {
359
1
        return make_null_field(field, data_type, result);
360
1
    }
361
362
9
    const auto value_type = remove_nullable(data_type);
363
9
    switch (value_type->get_primitive_type()) {
364
4
    case TYPE_STRUCT:
365
4
        return build_json_struct_default(field, value_type, json_value, binary_storage, result);
366
1
    case TYPE_ARRAY:
367
1
        return build_json_array_default(field, value_type, json_value, binary_storage, result);
368
1
    case TYPE_MAP:
369
1
        return build_json_map_default(field, value_type, json_value, binary_storage, result);
370
3
    default:
371
3
        return build_json_scalar_default(field, value_type, json_value, binary_storage, result);
372
9
    }
373
9
}
374
375
inline Status build_initial_default_field(const schema::external::TField& field,
376
                                          const DataTypePtr& data_type,
377
37
                                          std::deque<std::string>* binary_storage, Field* result) {
378
37
    DORIS_CHECK(data_type != nullptr);
379
37
    DORIS_CHECK(binary_storage != nullptr);
380
37
    DORIS_CHECK(result != nullptr);
381
37
    if (!field.__isset.initial_default_value) {
382
5
        if (field.__isset.is_optional && !field.is_optional) {
383
1
            return Status::InvalidArgument(
384
1
                    "Required Iceberg field '{}' is missing from the data file and has no initial "
385
1
                    "default",
386
1
                    field.name);
387
1
        }
388
4
        return make_null_field(field, data_type, result);
389
5
    }
390
391
32
    const auto value_type = remove_nullable(data_type);
392
32
    const auto primitive_type = value_type->get_primitive_type();
393
32
    if (is_complex_type(primitive_type)) {
394
6
        rapidjson::Document document;
395
6
        document.Parse(field.initial_default_value.data(), field.initial_default_value.size());
396
6
        if (document.HasParseError()) {
397
0
            return Status::InvalidArgument("Invalid Iceberg JSON initial default for field '{}'",
398
0
                                           field.name);
399
0
        }
400
6
        if (primitive_type == TYPE_STRUCT &&
401
6
            (!document.IsObject() || document.MemberCount() != 0)) {
402
0
            return Status::InvalidArgument(
403
0
                    "Iceberg struct field '{}' has a non-empty initial default", field.name);
404
0
        }
405
6
        return build_json_default_field(field, data_type, document, binary_storage, result);
406
6
    }
407
408
26
    const bool default_is_base64 = (field.__isset.initial_default_value_is_base64 &&
409
26
                                    field.initial_default_value_is_base64) ||
410
26
                                   primitive_type == TYPE_VARBINARY;
411
26
    if (default_is_base64) {
412
3
        binary_storage->emplace_back();
413
3
        if (!base64_decode(field.initial_default_value, &binary_storage->back())) {
414
0
            return Status::InvalidArgument("Invalid Base64 Iceberg initial default for field '{}'",
415
0
                                           field.name);
416
0
        }
417
3
        if (primitive_type == TYPE_VARBINARY) {
418
2
            *result = Field::create_field<TYPE_VARBINARY>(StringView(binary_storage->back()));
419
2
        } else if (is_string_type(primitive_type)) {
420
1
            *result = Field::create_field<TYPE_STRING>(binary_storage->back());
421
1
        } else {
422
0
            return Status::InvalidArgument(
423
0
                    "Iceberg field '{}' marks its initial default as Base64, but Doris type '{}' "
424
0
                    "cannot contain binary data",
425
0
                    field.name, value_type->get_name());
426
0
        }
427
3
        return Status::OK();
428
3
    }
429
430
23
    RETURN_IF_ERROR(value_type->get_serde()->from_fe_string(field.initial_default_value, *result));
431
23
    return Status::OK();
432
23
}
433
434
} // namespace detail
435
436
// Builds an owned one-row column for an Iceberg field that is absent from an old data file.
437
// Complex values follow Iceberg's JSON single-value encoding. Struct members omitted from the
438
// encoded value are recursively populated from the child field's own initial default.
439
inline Status create_initial_default_column(const schema::external::TField& field,
440
31
                                            const DataTypePtr& data_type, ColumnPtr* result) {
441
31
    DORIS_CHECK(data_type != nullptr);
442
31
    DORIS_CHECK(result != nullptr);
443
444
31
    auto column = data_type->create_column();
445
31
    std::deque<std::string> binary_storage;
446
31
    Field value;
447
31
    RETURN_IF_ERROR(detail::build_initial_default_field(field, data_type, &binary_storage, &value));
448
    // The column copies every String/StringView leaf before binary_storage is destroyed.
449
30
    column->insert(value);
450
451
30
    *result = std::move(column);
452
30
    return Status::OK();
453
31
}
454
455
14
inline ColumnPtr repeat_initial_default_column(const ColumnPtr& default_column, size_t rows) {
456
14
    DORIS_CHECK(default_column);
457
14
    DORIS_CHECK_EQ(default_column->size(), 1);
458
459
14
    auto repeated_column = default_column->clone_empty();
460
14
    repeated_column->insert_many_from(*default_column, 0, rows);
461
14
    return repeated_column;
462
14
}
463
464
inline Status append_initial_default(
465
        const schema::external::TField& field, const DataTypePtr& data_type, size_t rows,
466
        std::unordered_map<int32_t, std::pair<DataTypePtr, ColumnPtr>>* prepared_values,
467
9
        ColumnPtr* destination) {
468
9
    DORIS_CHECK(data_type != nullptr);
469
9
    DORIS_CHECK(prepared_values != nullptr);
470
9
    DORIS_CHECK(destination != nullptr);
471
9
    DORIS_CHECK(field.__isset.id);
472
473
9
    auto prepared_value = prepared_values->find(field.id);
474
9
    if (prepared_value == prepared_values->end()) {
475
8
        ColumnPtr default_column;
476
8
        RETURN_IF_ERROR(create_initial_default_column(field, data_type, &default_column));
477
8
        prepared_value =
478
8
                prepared_values
479
8
                        ->emplace(field.id, std::make_pair(data_type, std::move(default_column)))
480
8
                        .first;
481
8
    } else {
482
        // One Iceberg field ID resolves to one query type. Hold the first DataTypePtr so equivalent
483
        // complex types reconstructed for later Blocks reuse the same prepared value.
484
1
        DORIS_CHECK(prepared_value->second.first->equals(*data_type));
485
1
    }
486
487
9
    auto mutable_destination = IColumn::mutate(std::move(*destination));
488
9
    mutable_destination->insert_many_from(*prepared_value->second.second, 0, rows);
489
9
    *destination = std::move(mutable_destination);
490
9
    return Status::OK();
491
9
}
492
493
} // namespace doris::iceberg