Coverage Report

Created: 2026-08-14 11:09

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/exprs/function/parse/variant_string_parse.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 "exprs/function/parse/variant_string_parse.h"
19
20
#include <cctz/time_zone.h>
21
#include <fmt/compile.h>
22
#include <fmt/format.h>
23
24
#include <algorithm>
25
#include <array>
26
#include <chrono>
27
#include <cstdint>
28
#include <limits>
29
#include <string_view>
30
#include <unordered_set>
31
#include <utility>
32
33
#include "common/config.h"
34
#include "common/exception.h"
35
#include "core/value/variant/variant_batch_builder.h"
36
#include "core/value/variant/variant_parquet_encoding.h"
37
#include "util/json/simd_json_parser.h"
38
#include "util/utf8_check.h"
39
40
namespace doris {
41
namespace {
42
43
using variant_json::FormattedScalar;
44
45
2.53k
void append_char(FormattedScalar* result, char value) {
46
2.53k
    result->bytes[result->size++] = value;
47
2.53k
}
48
49
610
void append_unsigned(FormattedScalar* result, uint64_t value, size_t minimum_digits = 1) {
50
610
    std::array<char, 32> reversed {};
51
610
    size_t digits = 0;
52
1.13k
    do {
53
1.13k
        reversed[digits++] = static_cast<char>('0' + value % 10);
54
1.13k
        value /= 10;
55
1.13k
    } while (value != 0);
56
1.18k
    while (digits < minimum_digits) {
57
575
        reversed[digits++] = '0';
58
575
    }
59
2.32k
    while (digits != 0) {
60
1.71k
        append_char(result, reversed[--digits]);
61
1.71k
    }
62
610
}
63
64
122
void append_year(FormattedScalar* result, int64_t year) {
65
122
    if (year >= 0 && year <= 9999) {
66
122
        append_unsigned(result, static_cast<uint64_t>(year), 4);
67
122
        return;
68
122
    }
69
0
    if (year >= 0) {
70
0
        append_char(result, '+');
71
0
        append_unsigned(result, static_cast<uint64_t>(year), 4);
72
0
        return;
73
0
    }
74
0
    append_char(result, '-');
75
0
    append_unsigned(result, static_cast<uint64_t>(-(year + 1)) + 1, 4);
76
0
}
77
78
122
void append_date_time(FormattedScalar* result, const cctz::civil_second& civil, bool include_time) {
79
122
    append_year(result, civil.year());
80
122
    append_char(result, '-');
81
122
    append_unsigned(result, civil.month(), 2);
82
122
    append_char(result, '-');
83
122
    append_unsigned(result, civil.day(), 2);
84
122
    if (!include_time) {
85
70
        return;
86
70
    }
87
52
    append_char(result, ' ');
88
52
    append_unsigned(result, civil.hour(), 2);
89
52
    append_char(result, ':');
90
52
    append_unsigned(result, civil.minute(), 2);
91
52
    append_char(result, ':');
92
52
    append_unsigned(result, civil.second(), 2);
93
52
}
94
95
55
void append_fraction(FormattedScalar* result, uint64_t fraction, uint8_t digits) {
96
55
    append_char(result, '.');
97
55
    append_unsigned(result, fraction, digits);
98
55
}
99
100
12
void append_offset(FormattedScalar* result, int offset_seconds) {
101
12
    const bool negative = offset_seconds < 0;
102
12
    const uint64_t magnitude_seconds =
103
12
            negative ? static_cast<uint64_t>(-(static_cast<int64_t>(offset_seconds)))
104
12
                     : static_cast<uint64_t>(offset_seconds);
105
12
    append_char(result, negative ? '-' : '+');
106
12
    append_unsigned(result, magnitude_seconds / 3600, 2);
107
12
    append_char(result, ':');
108
12
    append_unsigned(result, (magnitude_seconds % 3600) / 60, 2);
109
12
    if (magnitude_seconds % 60 != 0) {
110
0
        append_char(result, ':');
111
0
        append_unsigned(result, magnitude_seconds % 60, 2);
112
0
    }
113
12
}
114
115
55
std::pair<int64_t, uint64_t> split_epoch(int64_t value, int64_t units_per_second) {
116
55
    int64_t seconds = value / units_per_second;
117
55
    int64_t fraction = value % units_per_second;
118
55
    if (fraction < 0) {
119
14
        --seconds;
120
14
        fraction += units_per_second;
121
14
    }
122
55
    return {seconds, static_cast<uint64_t>(fraction)};
123
55
}
124
125
4.42k
StringRef to_string_ref(std::string_view value) {
126
4.42k
    return {value.data(), value.size()};
127
4.42k
}
128
129
3.35k
void require_json_key_length(std::string_view key, uint32_t maximum) {
130
3.35k
    if (key.size() > maximum) {
131
4
        throw Exception(ErrorCode::INVALID_ARGUMENT,
132
4
                        "Variant JSON object key length {} exceeds maximum {} bytes", key.size(),
133
4
                        maximum);
134
4
    }
135
3.35k
}
136
137
class JsonTreeCollector {
138
public:
139
    JsonTreeCollector(VariantBatchBuilder::Row& builder, const JsonToVariantOptions& options)
140
1.26k
            : _builder(builder), _options(options) {}
141
142
5.19k
    void collect(SimdJSONParser::Element element, uint32_t depth) {
143
5.19k
        variant_json::require_json_depth(depth);
144
5.19k
        if (element.isNull()) {
145
41
            _builder.add_null();
146
5.15k
        } else if (element.isBool()) {
147
16
            _builder.add_bool(element.getBool());
148
5.13k
        } else if (element.isInt64()) {
149
1.89k
            _builder.add_int(element.getInt64());
150
3.24k
        } else if (element.isUInt64()) {
151
1
            _builder.add_largeint(static_cast<__int128>(element.getUInt64()));
152
3.24k
        } else if (element.isDouble()) {
153
1
            _builder.add_double(element.getDouble());
154
3.24k
        } else if (element.isString()) {
155
1.09k
            _builder.add_string(to_string_ref(element.getString()));
156
2.14k
        } else if (element.isArray()) {
157
564
            collect_array(element.getArray(), depth);
158
1.58k
        } else if (element.isObject()) {
159
1.58k
            collect_object(element.getObject(), depth);
160
1.58k
        }
161
5.19k
    }
162
163
private:
164
564
    void collect_array(const SimdJSONParser::Array& array, uint32_t depth) {
165
564
        auto scope = _builder.start_array();
166
591
        for (SimdJSONParser::Element child : array) {
167
591
            collect(child, depth + 1);
168
591
        }
169
564
        scope.finish();
170
564
    }
171
172
1.58k
    void collect_object(const SimdJSONParser::Object& object, uint32_t depth) {
173
1.58k
        auto scope = _builder.start_object();
174
1.58k
        if (!_options.check_duplicate_json_path) {
175
3.30k
            for (const auto& [key, child] : object) {
176
3.30k
                require_json_key_length(key, _options.max_json_key_length);
177
3.30k
                scope.add_key(to_string_ref(key));
178
3.30k
                collect(child, depth + 1);
179
3.30k
            }
180
1.55k
            scope.finish();
181
1.55k
            return;
182
1.55k
        }
183
184
29
        std::unordered_set<std::string_view> seen_keys;
185
29
        seen_keys.reserve(object.size());
186
46
        for (const auto& [key, child] : object) {
187
46
            require_json_key_length(key, _options.max_json_key_length);
188
46
            if (seen_keys.emplace(key).second) {
189
35
                scope.add_key(to_string_ref(key));
190
35
                collect(child, depth + 1);
191
35
            } else {
192
11
                validate_ignored(child, depth + 1);
193
11
            }
194
46
        }
195
29
        scope.finish();
196
29
    }
197
198
141
    void validate_ignored(SimdJSONParser::Element element, uint32_t depth) const {
199
141
        variant_json::require_json_depth(depth);
200
141
        if (element.isArray()) {
201
129
            for (SimdJSONParser::Element child : element.getArray()) {
202
129
                validate_ignored(child, depth + 1);
203
129
            }
204
129
        } else if (element.isObject()) {
205
2
            for (const auto& [key, child] : element.getObject()) {
206
2
                require_json_key_length(key, _options.max_json_key_length);
207
2
                validate_ignored(child, depth + 1);
208
2
            }
209
2
        }
210
141
    }
211
212
    VariantBatchBuilder::Row& _builder;
213
    const JsonToVariantOptions& _options;
214
};
215
216
} // namespace
217
218
namespace variant_json {
219
220
44.3k
void require_json_depth(uint32_t depth) {
221
44.3k
    if (depth > VARIANT_MAX_NESTING_DEPTH) {
222
5
        throw Exception(ErrorCode::INVALID_ARGUMENT,
223
5
                        "Variant JSON traversal exceeds maximum depth {}",
224
5
                        VARIANT_MAX_NESTING_DEPTH);
225
5
    }
226
44.3k
}
227
228
11.7k
void require_exact_json_value(VariantRef value) {
229
11.7k
    const size_t encoded_size = value.value_size();
230
11.7k
    if (encoded_size != value.value.size) {
231
2
        throw Exception(ErrorCode::CORRUPTION,
232
2
                        "Variant value has {} trailing bytes after its {} byte root",
233
2
                        value.value.size - encoded_size, encoded_size);
234
2
    }
235
11.7k
}
236
237
41.5k
void require_valid_json_utf8(StringRef value, const char* description) {
238
41.5k
    if (value.size != 0 && !validate_utf8(value.data, value.size)) {
239
2
        throw Exception(ErrorCode::CORRUPTION, "Variant {} is not valid UTF-8", description);
240
2
    }
241
41.5k
}
242
243
26.8k
void require_json_object_key(StringRef key, StringRef previous_key, uint32_t field_index) {
244
26.8k
    require_valid_json_utf8(key, "object key");
245
26.8k
    if (field_index != 0 && previous_key.compare(key) >= 0) {
246
4
        throw Exception(ErrorCode::CORRUPTION,
247
4
                        "Variant object keys are not strictly byte-sorted at field {}",
248
4
                        field_index);
249
4
    }
250
26.8k
}
251
252
0
[[noreturn]] void throw_unsupported_json_primitive(VariantPrimitiveId id) {
253
0
    throw Exception(ErrorCode::INVALID_ARGUMENT, "Unsupported Variant JSON primitive id {}",
254
0
                    static_cast<uint8_t>(id));
255
0
}
256
257
17.3k
FormattedScalar format_json_int(int64_t value) {
258
17.3k
    FormattedScalar result;
259
17.3k
    char* end = fmt::format_to(result.bytes.data(), FMT_COMPILE("{}"), value);
260
17.3k
    result.size = end - result.bytes.data();
261
17.3k
    return result;
262
17.3k
}
263
264
4
FormattedScalar format_json_float(float value) {
265
4
    FormattedScalar result;
266
4
    char* end = fmt::format_to(result.bytes.data(), FMT_COMPILE("{:.{}g}"), value,
267
4
                               std::numeric_limits<float>::digits10 + 1);
268
4
    result.size = end - result.bytes.data();
269
4
    return result;
270
4
}
271
272
51
FormattedScalar format_json_double(double value) {
273
51
    FormattedScalar result;
274
51
    char* end = fmt::format_to(result.bytes.data(), FMT_COMPILE("{:.{}g}"), value,
275
51
                               std::numeric_limits<double>::digits10 + 1);
276
51
    result.size = end - result.bytes.data();
277
51
    return result;
278
51
}
279
280
25
FormattedScalar format_json_decimal(VariantDecimal value) {
281
25
    FormattedScalar result;
282
25
    const bool negative = value.unscaled < 0;
283
25
    std::array<char, 39> reversed {};
284
25
    size_t digits = 0;
285
25
    unsigned __int128 remaining = variant_unsigned_magnitude(value.unscaled);
286
202
    do {
287
202
        reversed[digits++] = static_cast<char>('0' + remaining % 10);
288
202
        remaining /= 10;
289
202
    } while (remaining != 0);
290
291
25
    if (negative) {
292
8
        append_char(&result, '-');
293
8
    }
294
25
    if (value.scale == 0) {
295
69
        while (digits != 0) {
296
65
            append_char(&result, reversed[--digits]);
297
65
        }
298
4
        return result;
299
4
    }
300
21
    if (digits <= value.scale) {
301
0
        append_char(&result, '0');
302
0
        append_char(&result, '.');
303
0
        for (size_t zero = digits; zero < value.scale; ++zero) {
304
0
            append_char(&result, '0');
305
0
        }
306
0
        while (digits != 0) {
307
0
            append_char(&result, reversed[--digits]);
308
0
        }
309
0
        return result;
310
0
    }
311
312
66
    while (digits > value.scale) {
313
45
        append_char(&result, reversed[--digits]);
314
45
    }
315
21
    append_char(&result, '.');
316
113
    while (digits != 0) {
317
92
        append_char(&result, reversed[--digits]);
318
92
    }
319
21
    return result;
320
21
}
321
322
70
FormattedScalar format_json_date(int32_t days_since_epoch) {
323
70
    constexpr int64_t SECONDS_PER_DAY = 86'400;
324
70
    const int64_t seconds = static_cast<int64_t>(days_since_epoch) * SECONDS_PER_DAY;
325
70
    const auto lookup =
326
70
            cctz::utc_time_zone().lookup(cctz::time_point<cctz::seconds>(cctz::seconds(seconds)));
327
70
    FormattedScalar result;
328
70
    append_date_time(&result, lookup.cs, false);
329
70
    return result;
330
70
}
331
332
FormattedScalar format_json_timestamp(int64_t value, uint8_t fractional_digits, bool utc_adjusted,
333
52
                                      const cctz::time_zone* timezone) {
334
52
    if (fractional_digits != 6 && fractional_digits != 9) {
335
0
        throw Exception(ErrorCode::INVALID_ARGUMENT,
336
0
                        "Variant timestamp JSON precision must be 6 or 9, got {}",
337
0
                        fractional_digits);
338
0
    }
339
52
    const int64_t units_per_second = fractional_digits == 6 ? 1'000'000 : 1'000'000'000;
340
52
    const auto [seconds, fraction] = split_epoch(value, units_per_second);
341
52
    const cctz::time_zone& zone =
342
52
            utc_adjusted && timezone != nullptr ? *timezone : cctz::utc_time_zone();
343
52
    const auto lookup = zone.lookup(cctz::time_point<cctz::seconds>(cctz::seconds(seconds)));
344
52
    FormattedScalar result;
345
52
    append_date_time(&result, lookup.cs, true);
346
52
    append_fraction(&result, fraction, fractional_digits);
347
52
    if (utc_adjusted) {
348
12
        append_offset(&result, lookup.offset);
349
12
    }
350
52
    return result;
351
52
}
352
353
4
FormattedScalar format_json_time_micros(int64_t value) {
354
4
    constexpr int64_t MICROS_PER_SECOND = 1'000'000;
355
4
    constexpr int64_t MICROS_PER_DAY = 86'400 * MICROS_PER_SECOND;
356
4
    if (value < 0 || value >= MICROS_PER_DAY) {
357
1
        throw Exception(ErrorCode::INVALID_ARGUMENT,
358
1
                        "Variant time value {} is outside [0, {}) microseconds", value,
359
1
                        MICROS_PER_DAY);
360
1
    }
361
3
    const auto [seconds, micros] = split_epoch(value, MICROS_PER_SECOND);
362
3
    FormattedScalar result;
363
3
    append_unsigned(&result, static_cast<uint64_t>(seconds / 3600), 2);
364
3
    append_char(&result, ':');
365
3
    append_unsigned(&result, static_cast<uint64_t>((seconds % 3600) / 60), 2);
366
3
    append_char(&result, ':');
367
3
    append_unsigned(&result, static_cast<uint64_t>(seconds % 60), 2);
368
3
    append_fraction(&result, micros, 6);
369
3
    return result;
370
4
}
371
372
3
FormattedScalar format_json_uuid(const std::array<uint8_t, 16>& value) {
373
3
    static constexpr char HEX[] = "0123456789abcdef";
374
3
    FormattedScalar result;
375
51
    for (size_t index = 0; index < value.size(); ++index) {
376
48
        if (index == 4 || index == 6 || index == 8 || index == 10) {
377
12
            append_char(&result, '-');
378
12
        }
379
48
        append_char(&result, HEX[value[index] >> 4]);
380
48
        append_char(&result, HEX[value[index] & 0x0F]);
381
48
    }
382
3
    return result;
383
3
}
384
385
} // namespace variant_json
386
387
1.09k
JsonToVariantOptions JsonToVariantOptions::current_config() {
388
1.09k
    return {.max_json_key_length = static_cast<uint32_t>(config::variant_max_json_key_length),
389
1.09k
            .throw_on_invalid_json = config::variant_throw_exeception_on_invalid_json,
390
1.09k
            .check_duplicate_json_path = config::variant_enable_duplicate_json_path_check};
391
1.09k
}
392
393
struct JsonStringToVariantEncoder::Impl {
394
    enum class State : uint8_t { COLLECTING, FINISHED, FAILED };
395
396
1.24k
    explicit Impl(JsonToVariantOptions options_) : options(options_) {
397
1.24k
        if (options.max_json_key_length == 0) {
398
1
            throw Exception(ErrorCode::INVALID_ARGUMENT,
399
1
                            "Variant maximum JSON key length must be positive");
400
1
        }
401
1.24k
    }
402
403
2.52k
    void require_collecting() const {
404
2.52k
        if (state == State::FINISHED) {
405
4
            throw Exception(ErrorCode::INVALID_ARGUMENT,
406
4
                            "Variant JSON encoder is already finished");
407
4
        }
408
2.51k
        if (state == State::FAILED) {
409
3
            throw Exception(ErrorCode::INVALID_ARGUMENT,
410
3
                            "Variant JSON encoder is in a terminal failed state");
411
3
        }
412
2.51k
    }
413
414
1.28k
    void add_json_row(StringRef json) {
415
1.28k
        auto row = builder.begin_row();
416
1.28k
        if (json.size != 0 && json.data == nullptr) {
417
1
            throw Exception(ErrorCode::INVALID_ARGUMENT,
418
1
                            "Variant JSON input has a null data pointer");
419
1
        }
420
1.28k
        if (json.size == 0) {
421
3
            auto object = row.start_object();
422
3
            object.finish();
423
3
            row.finish();
424
3
            return;
425
3
        }
426
427
1.28k
        SimdJSONParser::Element root;
428
1.28k
        if (!parser.parse(json.data, json.size, root)) {
429
14
            if (options.throw_on_invalid_json) {
430
7
                throw Exception(ErrorCode::INVALID_ARGUMENT, "Failed to parse JSON as Variant");
431
7
            }
432
7
            row.add_string(json);
433
7
            row.finish();
434
7
            return;
435
14
        }
436
1.26k
        JsonTreeCollector(row, options).collect(root, 0);
437
1.26k
        row.finish();
438
1.26k
    }
439
440
    JsonToVariantOptions options;
441
    VariantBatchBuilder builder;
442
    SimdJSONParser parser;
443
    State state = State::COLLECTING;
444
};
445
446
JsonStringToVariantEncoder::JsonStringToVariantEncoder()
447
1.07k
        : JsonStringToVariantEncoder(JsonToVariantOptions::current_config()) {}
448
449
JsonStringToVariantEncoder::JsonStringToVariantEncoder(JsonToVariantOptions options)
450
1.24k
        : _impl(std::make_unique<Impl>(options)) {}
451
452
1.24k
JsonStringToVariantEncoder::~JsonStringToVariantEncoder() = default;
453
0
JsonStringToVariantEncoder::JsonStringToVariantEncoder(JsonStringToVariantEncoder&&) noexcept =
454
        default;
455
JsonStringToVariantEncoder& JsonStringToVariantEncoder::operator=(
456
0
        JsonStringToVariantEncoder&&) noexcept = default;
457
458
1.26k
void JsonStringToVariantEncoder::add_json(StringRef json) {
459
1.26k
    _impl->require_collecting();
460
1.26k
    try {
461
1.26k
        _impl->add_json_row(json);
462
1.26k
    } catch (...) {
463
12
        _impl->state = Impl::State::FAILED;
464
12
        throw;
465
12
    }
466
1.26k
}
467
468
28
Status JsonStringToVariantEncoder::try_add_json(StringRef json) {
469
28
    _impl->require_collecting();
470
28
    try {
471
28
        _impl->add_json_row(json);
472
28
        return Status::OK();
473
28
    } catch (const Exception& exception) {
474
11
        if (exception.code() == ErrorCode::INVALID_ARGUMENT) {
475
11
            return exception.to_status();
476
11
        }
477
0
        _impl->state = Impl::State::FAILED;
478
0
        throw;
479
11
    } catch (...) {
480
0
        _impl->state = Impl::State::FAILED;
481
0
        throw;
482
0
    }
483
28
}
484
485
1.23k
VariantBatchBuilder JsonStringToVariantEncoder::finish_batch() {
486
1.23k
    _impl->require_collecting();
487
1.23k
    try {
488
1.23k
        VariantBatchBuilder block = _impl->builder.finish_batch();
489
1.23k
        _impl->state = Impl::State::FINISHED;
490
1.23k
        return block;
491
1.23k
    } catch (...) {
492
0
        _impl->state = Impl::State::FAILED;
493
0
        throw;
494
0
    }
495
1.23k
}
496
497
} // namespace doris