-
Notifications
You must be signed in to change notification settings - Fork 4k
[fix](types) Fix binary value ownership and timestamp primitives #68301
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b2bb9ea
44eef01
f68c0ba
0765475
74fc447
221207e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -635,6 +635,10 @@ DataTypePtr DataTypeFactory::create_data_type( | |
| } else if (primitive_type == TYPE_AGG_STATE) { | ||
| // Do nothing | ||
| nested = std::make_shared<DataTypeAggState>(); | ||
| } else if (primitive_type == TYPE_VARBINARY) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Complete execution support for this newly reconstructible type in the default conditional evaluators. With the default
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The normal COALESCE/CASE dispatch omissions and the alternate short-circuit paths predate this PR. The protobuf datatype reconstruction change does not introduce those evaluator branches. Adding VARBINARY execution support is outside this follow-up. |
||
| // Serialized execution types must retain VARBINARY(n)'s byte limit across RPCs. | ||
| return create_data_type(primitive_type, is_nullable, 0, 0, | ||
| scalar_type.has_len() ? scalar_type.len() : -1); | ||
| } else if (primitive_type == TYPE_VARIANT) { | ||
| nested = std::make_shared<DataTypeVariantV2>(node.variant_max_subcolumns_count(), | ||
| node.variant_enable_doc_mode()); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,13 +18,17 @@ | |
| #include "core/data_type_serde/data_type_varbinary_serde.h" | ||
|
|
||
| #include <cstring> | ||
| #include <limits> | ||
|
|
||
| #include "common/config.h" | ||
| #include "core/column/column_varbinary.h" | ||
| #include "core/data_type_serde/arrow_validation.h" | ||
| #include "core/data_type_serde/parquet_decode_source.h" | ||
| #include "exprs/function/string_hex_util.h" | ||
| #include "util/url_coding.h" | ||
|
|
||
| namespace doris { | ||
|
|
||
| namespace { | ||
|
|
||
| class VarbinaryParquetConsumer final : public ParquetFixedValueConsumer, | ||
|
|
@@ -301,6 +305,82 @@ Status DataTypeVarbinarySerDe::deserialize_one_cell_from_json(IColumn& column, S | |
| return Status::OK(); | ||
| } | ||
|
|
||
| Status DataTypeVarbinarySerDe::from_string(StringRef& str, IColumn& column, | ||
| const FormatOptions& options) const { | ||
| // Partition structs use the same hex representation as nested VARBINARY output. Decode it | ||
| // before appending so arbitrary bytes survive JSON transport instead of becoming NULL. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Preserve the top-level partition-string contract here. With
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The top-level and nested encodings differ, but the pre-PR VARBINARY SerDe inherited from_string() returning NotSupported. Thus this is not a regression of a previously supported Hive partition decoder. Adding that decoder is outside the current primitive changes. |
||
| if (str.size < 2 || str.data[0] != '0' || str.data[1] != 'x' || (str.size - 2) % 2 != 0 || | ||
| str.size - 2 > std::numeric_limits<int>::max()) { | ||
| return Status::InvalidArgument("Invalid VARBINARY hex representation"); | ||
| } | ||
| // The INT_MAX guard also makes narrowing to the decoder's 32-bit offset type safe. | ||
| const auto hex_size = cast_set<ColumnString::Offset>(str.size - 2); | ||
| std::string bytes(hex_size / 2, '\0'); | ||
| if (string_hex::hex_decode(str.data + 2, hex_size, bytes.data()) != bytes.size()) { | ||
| return Status::InvalidArgument("Invalid VARBINARY hex representation"); | ||
| } | ||
| assert_cast<ColumnVarbinary&>(column).insert_data(bytes.data(), bytes.size()); | ||
| return Status::OK(); | ||
| } | ||
|
|
||
| Status DataTypeVarbinarySerDe::deserialize_one_cell_from_hive_text( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Propagate the Hive BINARY decoding contract instead of guessing it from each cell. Hive 4 supports |
||
| IColumn& column, Slice& slice, const FormatOptions& options, | ||
| int hive_text_complex_type_delimiter_level) const { | ||
| // Hive LazyBinary uses lenient Base64 (including URL-safe letters and whitespace), | ||
| // falling back to the original bytes for non-Base64 input or an empty decoding. | ||
| // Keep this separate from JSON/CSV: those formats do not share Hive's encoding contract. | ||
| std::string encoded; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Put these full-cell Hive staging buffers behind Doris's checked allocator. For every valid binary field this reserves |
||
| encoded.reserve(slice.size); | ||
| bool padding = false; | ||
| for (size_t i = 0; i < slice.size; ++i) { | ||
| const char c = slice.data[i]; | ||
| if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { | ||
| continue; | ||
| } | ||
| if (c == '=') { | ||
| padding = true; | ||
| continue; | ||
| } | ||
| if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || | ||
| c == '+' || c == '/' || c == '-' || c == '_')) { | ||
| return deserialize_one_cell_from_json(column, slice, options); | ||
| } | ||
| if (!padding) { | ||
| encoded.push_back(c == '-' ? '+' : c == '_' ? '/' : c); | ||
| } | ||
| } | ||
| // Commons Codec ignores a trailing sextet and accepts omitted padding. | ||
| if (encoded.size() % 4 == 1) { | ||
| encoded.pop_back(); | ||
| } | ||
| encoded.append((4 - encoded.size() % 4) % 4, '='); | ||
| std::string decoded; | ||
| if (!base64_decode(encoded, &decoded) || decoded.empty()) { | ||
| return deserialize_one_cell_from_json(column, slice, options); | ||
| } | ||
| assert_cast<ColumnVarbinary&>(column).insert_data(decoded.data(), decoded.size()); | ||
| return Status::OK(); | ||
| } | ||
|
|
||
| Status DataTypeVarbinarySerDe::deserialize_column_from_hive_text_vector( | ||
| IColumn& column, std::vector<Slice>& slices, uint64_t* num_deserialized, | ||
| const FormatOptions& options, int hive_text_complex_type_delimiter_level) const { | ||
| DESERIALIZE_COLUMN_FROM_HIVE_TEXT_VECTOR() | ||
| return Status::OK(); | ||
| } | ||
|
|
||
| Status DataTypeVarbinarySerDe::serialize_one_cell_to_hive_text( | ||
| const IColumn& column, int64_t row_num, BufferWritable& bw, FormatOptions& options, | ||
| int hive_text_complex_type_delimiter_level) const { | ||
| auto [data_column, data_row] = check_column_const_set_readability(column, row_num); | ||
| const auto value = assert_cast<const ColumnVarbinary&>(*data_column).get_data_at(data_row); | ||
| // Encoding is required on write as well, or a Hive reader will reinterpret binary bytes. | ||
| std::string encoded; | ||
| base64_encode(value.to_string(), &encoded); | ||
| bw.write(encoded.data(), encoded.size()); | ||
| return Status::OK(); | ||
| } | ||
|
|
||
| void DataTypeVarbinarySerDe::to_string(const IColumn& column, size_t row_num, BufferWritable& bw, | ||
| const FormatOptions& options) const { | ||
| const auto& value = assert_cast<const ColumnVarbinary&>(column).get_data()[row_num]; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,8 @@ | |
|
|
||
| #include "common/compare.h" | ||
| #include "core/accurate_comparison.h" | ||
| #include "core/allocator.h" | ||
| #include "core/allocator_fwd.h" | ||
| #include "core/data_type/data_type_decimal.h" | ||
| #include "core/data_type/define_primitive_type.h" | ||
| #include "core/data_type/primitive_type.h" | ||
|
|
@@ -91,6 +93,50 @@ bool decimal_less_or_equal(Decimal128V3 x, Decimal128V3 y, UInt32 xs, UInt32 ys) | |
| return dec_less_or_equal<TYPE_DECIMAL128I>(x, y, xs, ys); | ||
| } | ||
|
|
||
| namespace { | ||
| // Expression literals can outlive decoder pages and source columns. | ||
| // Keep the view first for Field::get(), and fit ownership into the existing Field storage. | ||
| struct OwnedBinaryField { | ||
| StringView view; | ||
| char* bytes = nullptr; | ||
| size_t byte_size = 0; | ||
|
|
||
| explicit OwnedBinaryField(const StringView& value) { | ||
| // Inline views already own their bytes; preserve their allocation-free representation. | ||
| if (value.isInline()) { | ||
| view = value; | ||
| return; | ||
| } | ||
| // Charge retained payloads and deep-copy peaks through Doris's checked allocator. | ||
| // Keep a standard-layout owner so the leading view remains accessible via Field::get(). | ||
| bytes = static_cast<char*>(Allocator<false> {}.alloc(value.size())); | ||
| byte_size = value.size(); | ||
| memcpy(bytes, value.data(), value.size()); | ||
| view = StringView(bytes, value.size()); | ||
| } | ||
| OwnedBinaryField(const OwnedBinaryField&) = delete; | ||
| OwnedBinaryField& operator=(const OwnedBinaryField&) = delete; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Allocate this retained payload through Doris's checked allocator. Every non-inline VARBINARY value materialized as a Field now duplicates value.size() bytes with raw new[]; this is reached by ordinary column extraction plus Iceberg defaults and Parquet metadata, and the declared length can be Integer.MAX_VALUE. The current jemalloc hook only routes allocation calls and does not consume/release the task tracker, while raw new[] also skips Allocator::memory_check and its controlled MEM_ALLOC_FAILED path. A large value (and the temporary/destination deep-copy peak) can therefore exceed a query or process limit without Doris charging it or rejecting it at the configured limit. Please use DorisUniqueBufferPtr or an equivalent allocator-backed owner that still fits Field::storage, and cover allocation/destruction under a limited tracker.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 221207e and synchronized to #68297 (0a220b4). Long binary payloads now use Allocator for checked allocation and release. The RAII owner remains standard-layout and fits Field::storage, preserving the leading StringView access. It stores the allocation length separately because Field::get() exposes a mutable view. Inline payloads remain allocation-free. Added limited-tracker tests for retained/copy accounting, replacement and destruction, oversized allocation rejection, and failed copy/assignment preserving the original value. Both new tests failed with the original new[] implementation. All 40 focused ASAN BE tests pass on each branch. Also stabilized the failing historical-offset fixture with fixed zones so the large negative offsets and second-precision round trips do not depend on the host's historical Manila/Guam tzdata. This change stays within the existing PR scope. clang-format 16 and master build hygiene passed. Full clang-tidy remains blocked by pre-existing diagnostics, including the unmatched NOLINTEND in core/types.h. CI has been retriggered on both PRs. |
||
| OwnedBinaryField& operator=(OwnedBinaryField&& other) noexcept { | ||
| release_bytes(); | ||
| view = other.view; | ||
| bytes = std::exchange(other.bytes, nullptr); | ||
| byte_size = std::exchange(other.byte_size, 0); | ||
| return *this; | ||
| } | ||
| ~OwnedBinaryField() { release_bytes(); } | ||
|
|
||
| private: | ||
| void release_bytes() const { | ||
| if (bytes != nullptr) { | ||
| // Field::get() exposes a mutable view; release the original allocation size. | ||
| Allocator<false> {}.free(bytes, byte_size); | ||
| } | ||
| } | ||
| }; | ||
| static_assert(std::is_standard_layout_v<OwnedBinaryField>); | ||
| static_assert(offsetof(OwnedBinaryField, view) == 0); | ||
| } // namespace | ||
|
|
||
| template <PrimitiveType Type> | ||
| void Field::create_concrete(typename PrimitiveTypeTraits<Type>::CppType&& x) { | ||
| // In both Field and PODArray, small types may be stored as wider types, | ||
|
|
@@ -99,7 +145,12 @@ void Field::create_concrete(typename PrimitiveTypeTraits<Type>::CppType&& x) { | |
| // we must initialize the entire wide stored type, and not just the | ||
| // nominal type. | ||
| using StorageType = typename PrimitiveTypeTraits<Type>::CppType; | ||
| new (&storage) StorageType(std::move(x)); | ||
| if constexpr (Type == TYPE_VARBINARY) { | ||
| static_assert(sizeof(OwnedBinaryField) <= sizeof(storage)); | ||
| new (&storage) OwnedBinaryField(x); | ||
| } else { | ||
| new (&storage) StorageType(std::move(x)); | ||
| } | ||
| type = Type; | ||
| DCHECK_NE(type, PrimitiveType::INVALID_TYPE); | ||
| } | ||
|
|
@@ -112,7 +163,11 @@ void Field::create_concrete(const typename PrimitiveTypeTraits<Type>::CppType& x | |
| // we must initialize the entire wide stored type, and not just the | ||
| // nominal type. | ||
| using StorageType = typename PrimitiveTypeTraits<Type>::CppType; | ||
| new (&storage) StorageType(x); | ||
| if constexpr (Type == TYPE_VARBINARY) { | ||
| new (&storage) OwnedBinaryField(x); | ||
| } else { | ||
| new (&storage) StorageType(x); | ||
| } | ||
| type = Type; | ||
| DCHECK_NE(type, PrimitiveType::INVALID_TYPE); | ||
| } | ||
|
|
@@ -241,6 +296,8 @@ Field& Field::operator=(const Field& rhs) { | |
| if (this != &rhs) { | ||
| if (type != rhs.type) { | ||
| destroy(); | ||
| // A failed allocation while changing types must leave a destructible Field. | ||
| type = TYPE_NULL; | ||
| create(rhs); | ||
| } else { | ||
| assign(rhs); /// This assigns string or vector without deallocation of existing buffer. | ||
|
|
@@ -646,12 +703,20 @@ void Field::assign(const Field& field) { | |
| /// Assuming same types. | ||
| template <PrimitiveType Type> | ||
| void Field::assign_concrete(typename PrimitiveTypeTraits<Type>::CppType&& x) { | ||
| if constexpr (Type == TYPE_VARBINARY) { | ||
| *reinterpret_cast<OwnedBinaryField*>(&storage) = OwnedBinaryField(x); | ||
| return; | ||
| } | ||
| auto* MAY_ALIAS ptr = reinterpret_cast<typename PrimitiveTypeTraits<Type>::CppType*>(&storage); | ||
| *ptr = std::forward<typename PrimitiveTypeTraits<Type>::CppType>(x); | ||
| } | ||
|
|
||
| template <PrimitiveType Type> | ||
| void Field::assign_concrete(const typename PrimitiveTypeTraits<Type>::CppType& x) { | ||
| if constexpr (Type == TYPE_VARBINARY) { | ||
| *reinterpret_cast<OwnedBinaryField*>(&storage) = OwnedBinaryField(x); | ||
| return; | ||
| } | ||
| auto* MAY_ALIAS ptr = reinterpret_cast<typename PrimitiveTypeTraits<Type>::CppType*>(&storage); | ||
| *ptr = std::forward<const typename PrimitiveTypeTraits<Type>::CppType>(x); | ||
| } | ||
|
|
@@ -683,6 +748,10 @@ const typename PrimitiveTypeTraits<T>::CppType& Field::get() const { | |
|
|
||
| template <PrimitiveType T> | ||
| void Field::destroy() { | ||
| if constexpr (T == TYPE_VARBINARY) { | ||
| reinterpret_cast<OwnedBinaryField*>(&storage)->~OwnedBinaryField(); | ||
| return; | ||
| } | ||
| using TargetType = typename PrimitiveTypeTraits<T>::CppType; | ||
| DCHECK(T == type || ((is_string_type(type) && is_string_type(T)))) | ||
| << "Type mismatch: requested " << type_to_string(T) << ", actual " << get_type_name(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
|
|
||
| #include "core/value/timestamptz_value.h" | ||
|
|
||
| #include "common/exception.h" | ||
| #include "exprs/function/cast/cast_to_timestamptz_impl.hpp" | ||
|
|
||
| namespace doris { | ||
|
|
@@ -38,6 +39,13 @@ std::string TimestampTzValue::to_string(const cctz::time_zone& tz, int scale) co | |
| auto lookup_result = tz.lookup(cur_tz_time); | ||
|
|
||
| cctz::civil_second civ = lookup_result.cs; | ||
| // UTC storage bounds do not guarantee a representable session-local year. Reject | ||
| // overflow before DateTimeV2 formatting could produce an offset-only wire value. | ||
| if (civ.year() < 0 || civ.year() > 9999) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Keep the remaining TIMESTAMP_NS error paths from re-entering this throwing formatter. With the session zone +08:00, the maximum UTC TIMESTAMPTZ first fails local conversion because the year becomes 10000; strict TIMESTAMPTZ-to-TIMESTAMP_NS cast and TIMESTAMPTZ/TIMESTAMP_NS comparison then call to_string(local_zone) while building their InvalidArgument Status, so this exception escapes before that Status is returned. The parallel cast paths changed in this PR already render utc_dt() instead. Please update these two missed callers the same way and add strict-cast and comparison boundary tests.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 0765475. Both remaining TIMESTAMP_NS error paths now format the valid UTC value instead of calling the session-local formatter again. ASAN tests cover both year endpoints, strict errors/non-strict NULL results, and both comparison operand orders; all 31 focused BE tests passed. |
||
| throw Exception(ErrorCode::INVALID_ARGUMENT, | ||
| "TIMESTAMPTZ local year is outside [0, 9999]: year={}, timezone={}", | ||
| civ.year(), tz.name()); | ||
| } | ||
| auto time_offset = lookup_result.offset; | ||
|
|
||
| bool is_negative_offset = time_offset < 0; | ||
|
|
@@ -65,6 +73,14 @@ std::string TimestampTzValue::to_string(const cctz::time_zone& tz, int scale) co | |
| buffer[len++] = ':'; | ||
| buffer[len++] = static_cast<char>('0' + offset_mins / 10); | ||
| buffer[len++] = '0' + offset_mins % 10; | ||
| // Historical zones can have sub-minute offsets. Dropping their seconds changes the | ||
| // instant represented by the client-visible wall clock and offset when read back. | ||
| const int offset_seconds = abs_offset % 60; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Update the parallel
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The separate to_iso8601 implementation and its historical-offset/year-boundary limitations predate this PR. It is unchanged here; repairing that formatter would broaden the follow-up beyond newly introduced defects.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Keep MySQL column metadata wide enough for the new format. With scale 6, appending
yiguolei marked this conversation as resolved.
|
||
| if (offset_seconds != 0) { | ||
| buffer[len++] = ':'; | ||
| buffer[len++] = static_cast<char>('0' + offset_seconds / 10); | ||
| buffer[len++] = static_cast<char>('0' + offset_seconds % 10); | ||
| } | ||
| return {buffer, static_cast<size_t>(len)}; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -102,6 +102,13 @@ inline HashKeyType get_hash_key_type_fixed(const std::vector<DataTypePtr>& data_ | |
| } | ||
|
|
||
| inline HashKeyType get_hash_key_type(const std::vector<DataTypePtr>& data_types) { | ||
| // Reject binary before the multi-key serialization fallback can enable joins or grouping. | ||
| for (const auto& type : data_types) { | ||
| if (type->get_primitive_type() == TYPE_VARBINARY) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Preserve the existing serialized path for supported multi-key consumers. This pre-check now runs before the multi-key branch, so it also rejects keys that previously selected HashKeyType::serialized: default-enabled PartitionTopN windows, multi-column INTERSECT/EXCEPT, and recursive UNION DISTINCT. Their PartitionedHashMapVariants, SetDataVariants, and DistinctDataVariants all implement serialized keys, and ColumnVarbinary supplies serialization; only single-key VARBINARY was already unsupported. Please move the rejection to the join/group consumers that require it (or otherwise retain these byte-safe consumers) and add multi-key window, set-operation, and recursive-CTE coverage.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The rejection is intentional for this change. Preserving previously reachable serialized hash consumers is a compatibility concern, so the common VARBINARY hash restriction is retained. |
||
| throw Exception(ErrorCode::NOT_IMPLEMENTED_ERROR, | ||
| "VARBINARY hash keys are not supported"); | ||
| } | ||
| } | ||
| if (data_types.size() > 1) { | ||
| return get_hash_key_type_fixed(data_types); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个reserve 是错的。 不是 +num,应该是计算一下offsets【num】 - 0 ?